Python
The libraries DJV is built from have Python bindings, and there is a version of the DJV application written in Python on top of them. This page describes what is there, how to run it, and how to modify it. It is not an API reference — the bindings follow the C++ headers closely, so the headers and the Python examples are the reference.
The modules
Four modules make up the stack:
- ftkPy — the feather-tk user interface toolkit: widgets, layouts, observables, and the application classes.
- tlRenderPy — timelines, players, and file I/O, with the timeline and viewport widgets in the
tl.uisubmodule. - djvPy — DJV itself: the data models in the
djv.modelssubmodule — files, settings, color, audio, viewport, and tools — and the application's widgets in thedjv.uisubmodule — the viewport with its HUD and color picking, and the color, view, settings, export, and magnify widgets. - opentimelineio — the standard OTIO package. Its time types pass directly through the tlRender bindings, so a
RationalTimefrom OTIO seeks a player.
The conventional imports:
import opentimelineio as otio
import ftkPy as ftk
import tlRenderPy as tl
import djvPy as djv
Building
One switch builds all of it. In etc/Config/local.cmake:
set(DJV_PYTHON ON CACHE BOOL "")
The feather-tk and tlRender switches follow it, the superbuild adds pybind11 and builds OpenTimelineIO's Python package, and the stack is built as shared libraries so that the modules share one copy of it. The bindings compile against the Python that CMake finds; the modules only import into that same Python, which is why the launcher below exists.
Running
The build writes a launcher that knows the interpreter and the module paths:
./build-Release/bin/djv-python render.mov
The install gets a copy at bin/djv-python that finds everything beside itself. The application takes an input file, and options in the C++ application's style — see -h for the list. The ones useful for scripting:
- -b, -compare — open a comparison file and set the mode.
- -settingsFile, -logFile — the settings and log files, which default to the same directory as the C++ application:
Documents/DJV/djv-python.3.jsonand.log. Pointing these elsewhere keeps a test run out of the real settings. - -resetSettings — reset the settings to their defaults.
- -listCommands, -command — the command registry, the same one the C++ application has: every menu action is a named command,
-listCommandsprints them, and-command "Playback/Seek { \"frame\": 100 }"runs one after startup. - -exit — start the user interface and exit without showing a window.
- -screenshot — write a screenshot of the window to a file and exit.
The application
The application lives in examples/python, one file per concern the way the C++ application is arranged: djv.py is the entry point, App.py creates the models and turns the current file into a player, MainWindow.py builds the window, and the rest are the actions, menus, tool bars, and tools.
The division of labor: the widgets are the C++ application's own, wrapped whole in djv.ui — one implementation, so the two applications cannot drift apart — and the Python code is the composition around them: the models, the actions and menus, and the tool frames. A tool like Color is a stack of bellows around wrapped widgets; a tool like Files builds its own rows from feather-tk widgets where the C++ application does the same.
The application keeps feature parity with the C++ application — playlists, dragging to reorder the file list, the tools — and the toolkit's widget events all pass through to Python: a widget written in Python can draw, take the keyboard and mouse, and be a drag and drop source or target.
The actions follow the C++ IActions pattern, in IActions.py: every operation is registered as a named command with the commands model, the menu actions run the commands, and the keyboard shortcuts come from the settings — so the shortcuts editor in the settings tool applies here, and -command can run anything a menu can.
Everything follows the same pattern: the DJV models hold the state, and the widgets observe it. A widget changes a model through its setters, and updates itself from an observer — never directly from its own callback, so that every other view of the same state stays in step:
selfWeak = weakref.ref(self)
self._volumeObserver = ftk.FloatObserver(
audioModel.observeVolume,
lambda value: selfWeak()._volumeUpdate(value))
Two things in that pattern matter:
- The observer object must be kept — assign it to
self. A discarded observer disconnects. - The callbacks capture
selfweakly. A callback handed to the C++ side is held by astd::function, an edge the garbage collector cannot see, so a strong capture forms a cycle that is never collected.Util.weak()wraps a bound method for use as a widget or action callback.
Option values are copies. Reading a property, changing a field, and assigning it back is the way to change one:
options = filesModel.compareOptions
options.compare = tl.Compare.Wipe
filesModel.compareOptions = options
Modifying it
To add a tool, write a class in Tools.py deriving from IToolWidget and add it to the FACTORY dictionary; the tools model already lists the tool names, icons, and shortcuts, and the actions, menu, and tool bar offer whatever the factory implements. To add an action, follow any of the *Actions.py files: create an ftk.Action, add it to a menu in Menus.py or a tool bar in ToolBars.py. checkedCallback must be passed as a keyword argument — passed positionally it binds to the plain callback and fails when clicked.
Smaller examples are in feather-tk's and tlRender's own examples/python directories, and the Python test suites — tests/CorePyTest and tests/UIPyTest in feather-tk, tests/tlRenderPyTest in tlRender, and tests/djvPyTest here — double as working usage of most of the bound API. They run with python -m unittest discover tests, or through CTest as ftkPy-test, tlRenderPy-test, and djvPy-test.