Bernard Review - Developer IPC API Guide
Welcome to the official developer guide for the Inter-Process Communication (IPC) API of Bernard Review.
This document describes how to programmatically control the player, query its status, and receive real-time playback event callbacks. It is designed for pipeline engineers, technical directors (TDs), and technical artists who want to integrate Bernard Review into studio environments (such as Autodesk Maya, Foundry Nuke, SideFX Houdini, or production tracking systems like Autodesk Flow Production Tracking / ShotGrid, and Kitsu).
Table of Contents
- Understanding IPC & Protocol Specifications
- Command Reference (Control payloads)
- Event Reference (Callbacks)
- Status Query Details
- The Python Client API (
bernard_api.py) - Studio Integration Examples (Maya & Nuke)
- ShotGrid & Kitsu Web Sync Daemons
- Full Review Round-Trip: Playlist In, Notes Out (Bernard Engine / ShotGrid / ftrack)
- NOVA (
.nvfx) - GPU YUV upload for movies (beta)
- GPU video decode (beta)
- GPU ProRes decode (beta)
1. Understanding IPC & Protocol Specifications
What is Inter-Process Communication?
When you are working in a studio, you often want different software to communicate with each other. For example, when you change the current frame in Maya, you want Bernard Review to instantly jump to the same frame. To achieve this, Bernard Review runs a lightweight TCP Socket Server in the background on your local computer.
Protocol Details
- IP Address:
127.0.0.1(localhost - your local machine) - Port:
5556by default; a player started with--ipc-port <n>listens on<n>(see Running two players on one machine). A port still held by another listener at startup (a player closing, which keeps its IPC until itsreview-exported) is tried again 0.25, 0.5, 1 and 2 s later -- about 4 s, never waited for --, then the player runs without IPC; the log says which ([IPC] listening on ...,[IPC] port ... is taken,[IPC] port ... still taken). Under Linux and macOS the port is bound withSO_REUSEADDR, never under Windows. - Formatting: JSON-Lines. Every command sent to the player or event received from the player is a single JSON object terminated by a newline character (
\n). - Language Agnostic: Because it uses standard TCP and JSON, you can write clients in Python, Node.js, C#, C++, Rust, or any language of your choice.
- Not HTTP: the port speaks JSON lines only, and a web page cannot use it. A connection whose lines, before its first valid command (a JSON object with a non-empty
"cmd"string), include an HTTP request line (GET,POST,PUT,DELETE,PATCH,HEAD,OPTIONS,CONNECT,TRACEorPRIfollowed by a space, in any case, or any line whose last word is an HTTP version such asHTTP/1.1) or a browser's header line (Host:,Origin:,Referer:,User-Agent:,Content-Type:,Content-Length:,Connection:,Cookie:,Upgrade:,Accept:,Accept-*:,Access-Control-*:,Sec-*:, in any case) is closed at once, without a reply, and none of its lines runs -- not even a valid command sent after them. The player's log says so in one line, without what was sent. A client that sends JSON objects, one per line, is never affected:{"cmd": "open", "path": "GET / HTTP/1.1"}is a command like any other. This is what keeps a page open in the machine's browser (afetch(), a form, an<img>aimed at127.0.0.1:5556) from driving the player. - Whole lines, 16 MiB at most: a line may hold at most 16 MiB (16,777,216 bytes) before its newline, its
\rincluded, and no NUL byte. A line that passes the limit (the moment it does, without waiting for its newline) or holds a NUL closes the connection, without a reply, and nothing of that line or of what follows it runs; the lines before it ran as usual. The player's log says which, in one line, without what was sent. The largest legitimate request, abuild-matrixof a whole sequence, is far below (600 shots with 10 departments each: about 1.2 MB), andjson.dumps()never writes a NUL. - One client at a time: the player serves one connection; a second one waits, unread, until the first closes, and is then served (or, if it is a web page's, refused as above). A connection made meanwhile never disconnects the client in place.
Every request gets a reply
Every line you send gets exactly one reply line of its own, in the order sent -- usually within one frame of the player (a few milliseconds). There is no request-ID field: a client matches a reply to its request by the reply's "cmd", which is the name it sent. A reply is "status": "ok" or "status": "error"; an error always carries a "reason" in plain words, and never waits for your time-out.
| You send | The reply |
|---|---|
a command the player does not know, {"cmd": "no-such-command"} | {"status": "error", "cmd": "no-such-command", "reason": "unknown command: no-such-command"} |
a line that is not JSON -- here Python's str() of a dict instead of json.dumps(): {'cmd': 'play'} | {"status": "error", "reason": "invalid JSON at byte 2: a request is one JSON object per line, with a \"cmd\" string"} |
JSON that is not an object (42, ["play"]) | {"status": "error", "reason": "a request must be a JSON object with a \"cmd\" string"} |
an object with no cmd, {"frame": 3} | {"status": "error", "reason": "missing \"cmd\": a request must be a JSON object with a \"cmd\" string"} |
a cmd that is not a non-empty string, {"cmd": 7} | {"status": "error", "reason": "\"cmd\" must be a non-empty string"} |
a known command without a field it needs, {"cmd": "seek"} | {"status": "error", "cmd": "seek", "reason": "seek: a required field is missing, nothing was done (see api/README.md)"} |
a field of the wrong type that made the command fail, {"cmd": "set-fps", "value": "fast"} | {"status": "error", "cmd": "set-fps", "reason": "set-fps failed: [json.exception.type_error.302] type must be number, but is string"} |
- The name echoed in
"cmd"and in the reason is escaped as JSON and bounded: a name longer than 64 bytes of UTF-8 is cut between two characters and ends with...(at most 64 bytes in all).bernard_api.pyfiles its pending request under the same cut name, so even a very long unknown name is answered at once. - A reply to a line that had no readable
cmdhas no"cmd"field: nothing in it names a request. Send one request at a time, or send only well-formed lines, if you match replies by name. - Three commands said why under another key until this version:
set-colorspace("error"),ram-fit-applyandvoice-test("message"). All three carry"reason"now, and the old key beside it, with the same words, for one more version, for the clients that read it; read"reason". - A failing command never stops the server: the next line is read and answered as usual.
- Empty lines are skipped without a reply (they are not requests). The player's own broadcasts --
{"event": ...}lines (see Event Reference) -- are not replies; nothing a client sends is a broadcast. - The field order of a reply is not significant: read it as a JSON object (the player writes error replies with their keys in alphabetical order).
- Still under
"message". The session commands' own refusals (session-create,session-join,session-pass-baton,chat-send) still put their words under"message", as their entries below show; a client that shows the words readsreason, thenmessage, thenerror. The rules of the table above (unknown command, bad line, missing field, failed handler) always use"reason".
The Console window: the same commands, inside the player
Window > Console (Ctrl+Shift+C) is a second client of this dispatcher that needs no socket: a line typed there -- status, seek {"frame": 1001}, or a request exactly as this page writes it, {"cmd": "seek", "frame": 1001} -- becomes one request, read by the same code as a socket line and answered by the same rules above; the reply is shown in the Console, indented, and never sent to the client connected to the port (that client's own requests and events are not shown there). Tab completes a name from the list of commands this page documents, taken from the dispatcher itself when the player is built. The Console can do nothing a socket client cannot, runs no shell command, and asks before quit, export, export-recipe, export-notes, save-playlist, screenshot, dump-thumbnails, cache-purge, tracker-publish and tracker-download-media unless the line starts with !. See the user guide, section 11.1.
2. Command Reference (Control Payloads)
To control the player, open a TCP socket, connect to port 5556, and send the corresponding JSON line.
Playback Controls
Play : Starts playing the media forward.
{"cmd": "play"}Pause : Pauses playback.
{"cmd": "pause"}Seek : Instantly jumps to a frame of the clip on screen.
frameis the 0-based index in the clip (whatstatus'splayback.currentFramereports);display_frameis the number the ruler shows (the start typed for the shot, else the number in a sequence's file names, else Preferences > Default Start Frame, 1001 by default). Adisplay_framethat is not a frame of the shot is refused ("status": "error"with itsreason), not taken to the shot's end; the reply gives both,"frame"and"display_frame".{"cmd": "seek", "frame": 41} {"cmd": "seek", "display_frame": 1042}Load Video/Sequence : Opens a video file or image sequence.
{"cmd": "load", "path": "D:/projects/seq_01/shot_10/render.mov"}
Audio Controls
Set Volume : Changes the audio volume level.
- Parameter:
value(float between0.0for silent and1.0for maximum volume).
{"cmd": "set-volume", "value": 0.75}- Parameter:
Set Mute : Mutes or unmutes the audio.
- Parameter:
value(boolean:trueto mute,falseto unmute).
{"cmd": "set-mute", "value": true}- Parameter:
Toggle Mute : Swaps the current mute state (mutes if playing, unmutes if muted).
{"cmd": "toggle-mute"}
Layout & Comparison Controls
Set A/B Mode : Enables or disables the split-screen comparison mode.
- Parameter:
value(boolean:trueto enable,falseto disable).
{"cmd": "set-ab-mode", "value": true}- Parameter:
Set A/B View : While A/B is on, chooses what the viewport paints: track A alone, the wipe, track B alone, or the Difference -- the toolbar's "A | A/B | B | Diff" switch, Compare > Difference and
Shift+1/Shift+3/Shift+2/Shift+4."difference"is the wipe's variant: A in gray, the pixels where A's and B's displayed values (after the grade and the display transform) differ by more than 0.02 in magenta; the wipe line and the paint stay as in the wipe, and the timeline marks the frames where A and B differ (playback.ab_difference_bandinstatus). It is local: a review session's followers get the wipe, and it is not saved."true_difference"is the same view with a true difference in place of the tint: each channel of the picture is |A - B| of the displayed values (after the grade, the display transform, the view LUT and the channel view), times a gain of 1 to 16 (gain), clamped to 1: equal pixels are black. Compare > True Difference (Essential) and True Difference in the A/B button's right-click menu (Complete), with their Difference Gain menu (x1, x2, x4, x8, x16). Any other view,Shift+4and"difference"give back the tinted one or the plain views; the gain stays until the player quits (x1 at start).set-ab-modekeeps its meaning: in every view both tracks stay loaded, decoded and cached, so going back to the wipe is instant. The view follows the pilot in a review session and comes back with track B when the last session is reopened; it is the wipe again whenever A/B is off.- Parameter:
value(string:"a","wipe","b","difference"or"true_difference", any case);gain(optional, a whole number from 1 to 16: the true difference's gain, kept for later). - Reply:
{"status":"ok","cmd":"set-ab-view","value":"b","gain":1}, or"status":"error"with areasonfor another value, againthat is not a whole number from 1 to 16, or while A/B is off (turn it on withset-ab-modefirst); nothing changes on an error. The view in force isplayback.ab_viewinstatus, the gainplayback.ab_difference_gain.
{"cmd": "set-ab-view", "value": "b"} {"cmd": "set-ab-view", "value": "true_difference", "gain": 4}- Parameter:
Set the wipe : the A/B divider's position (
position, 0 to 1 across the picture) and/or angle (angle, degrees) -- what a drag of the divider and the wipe's angle menu set. Like the rest of A/B, it is kept per shot: coming back to a shot of the playlist puts back its B, the mode, the view and the wipe. Reply:{"status":"ok","cmd":"set-wipe","position":0.3,"angle":90};track_b.wipe_positionandtrack_b.wipe_angleinstatusread it back,playlist_playback.ab_per_shotsays how many shots keep an A/B (shots) and what the last return did (last_restore:"restored","missing","refused"or"none").{"cmd": "set-wipe", "position": 0.3, "angle": 90}Set the guides (
set-guides) and read them (get-guides): View > Guides, drawn over the picture on screen only -- never in an export, a thumbnail, the guests' stream nor the scopes. They follow the zoom and the pan, and are kept by the playlist (its.bpl, key"framing_guides", written only when not the defaults; a.bplwithout it opens with the guides off) and by the last session.- Parameters of
set-guides, all optional, only those present change:mask(string:"none","1.85","2.39","16:9","4:3"or"1:1"; one mask at a time),safe_areas(boolean: title safe 90 % and action safe 93 %, of the mask's frame when there is one),center_cross(boolean),opacity(number, 0 to 100: the darkening outside the mask's frame, 60 by default),off(boolean:trueturns everything off first, the opacity kept). Every field is checked before any is applied, and a field of another name is refused (a misspeltsafe_areais an error, not ignored). Opening a.bplwithout the key, or clearing the playlist, turns the guides off. - Reply (both commands):
{"status":"ok","cmd":"get-guides","guides":{"on":true,"mask":"2.39", "safe_areas":true,"center_cross":false,"opacity":60},"rects":{...}}. With a picture on screen,rectssays where they are drawn, in window pixels:picture,frame(the mask's, or the picture),bands(the darkened bands, 0 or 2),band_alpha(0-255), andaction_safe,title_safe,cross(x,y,arm) when shown; each rectangle is{"x","y","w","h"}. A wrong field:{"status":"error","cmd":"set-guides","reason":"..."}, nothing changed.
{"cmd": "set-guides", "mask": "2.39", "safe_areas": true, "opacity": 70}- Parameters of
Flip A / B : While A/B is on, goes from A alone to B alone and back -- the
Skey. From the wipe it shows B alone. Likeset-ab-viewit is only a view change: nothing is loaded.- Reply:
{"status":"ok","cmd":"ab-flip","value":"b"}(the view now in force), or"status":"error"with areasonwhile A/B is off.
{"cmd": "ab-flip"}- Reply:
Step Track B's Version : Puts the previous or next version of B's shot onto track B -- the
DownandUpkeys. The versions are the ones the Version Board holds for the department of B's clip (or, when B's clip is on no row, the department its file name names under the naming rules), ordered by version number: gaps are stepped over (v001,v003,v007), and the ends stop, they do not wrap. The clip goes onto B the wayset-track-bputs it there (the same refusal of a format the build lacks, the same eviction of the old B, B weighed by Smart RAM-Fit); track A, the playhead and the A/B view stay where they are. While paused, the frame under the playhead of the previous and the next version is pre-loaded, so the step shows at once (set-b-version-preload).- Parameter:
direction(string:"prev"or"next"). - Reply:
{"status":"ok","cmd":"cycle-b","from":"v002","version":"v003","preloaded":true,"path":"...","requested":false}--preloadedsays whether that frame was already in RAM -- or"status":"error"with areason: another direction, A/B off, B empty, B's clip not on the Version Board,"no version after v003"/"no version before v001"at either end, or a format this build lacks.track_b.versionandtrack_b.last_version_stepinstatusread the result back.requestedtrue: that version is not on this computer, it was asked of the pipeline (media-request) and goes onto B whenmedia-readybrings it; B is unchanged meanwhile. - After "Compare Selection" (a playlist row's menu): while B holds one of the shots it compared,
cycle-b-- like the keys -- puts one of those shots on B instead, as in a list:"next"(Up) the shot above in the playlist,"prev"(Down) the shot below, at the same frame, round at either end ("next"on the first shot puts the last on B,"prev"on the last the first);fromandversionare then the rows' names (statustrack_b.compare_selection).
{"cmd": "cycle-b", "direction": "next"}- Parameter:
Pin Track B : The pin on track B's chip in the timeline. Pinned, track B stays where it is when another clip of the playlist is opened -- by a click, the keys,
open, auto-advance or the pilot's switch: its clip and frames, its grade and its strip, its layer, its version, A/B on and the A/B view (set-ab-view). Not pinned, a clip switch empties B and turns A/B off; coming back to the shot puts its own A/B back (set-wipe). Whatever else empties B takes the pin with it: track B's Clear, and a new playlist (load-playlist,playlist-clear, Clear Playlist), which clears B as before. A pinned B whose file has gone by the next switch is cleared with a notice, as a restored session refuses a missing B (track_b.last_switchis then"missing"). The pin is saved with track B in the session and comes back with it when the last session is reopened; a session saved before the pin existed reopens unpinned. In a review session the pin is local to this seat: it is never sent, a guest's pinned B stays on the guest's B when the pilot changes clips, and the pilot's pin decides nothing for anyone else.- Parameter:
value(boolean:trueto pin,falseto unpin). - Reply:
{"status":"ok","cmd":"pin-track-b","value":true}, or"status":"error"with areason: avaluethat is not a boolean, or"track B is empty"fortruewhile B holds no clip (unpinning is always accepted).track_b.pinnedinstatusreads it back.
{"cmd": "pin-track-b", "value": true}- Parameter:
Clear Track B : Track B's Clear (the button on B's chip in the timeline): B's clip, its frames and its thumbnail strip go, A/B turns off and the pin goes with the clip. Track A and the playlist are untouched. An empty B is left as it is.
- Reply:
{"status":"ok","cmd":"clear-track-b","cleared":true};clearedisfalsewhen B held no clip.track_b.pathinstatusthen reads"".
{"cmd": "clear-track-b"}- Reply:
Pre-load B's Neighbouring Versions : Turns on or off the pre-load of the previous and the next version of B's shot (Edit > Preferences > General > Pre-load B's previous and next versions, on by default). Off, a version step decodes its frame after the command -- which is what a measurement compares with. Not saved by this command.
- Parameter:
value(boolean). - Reply:
{"status":"ok","cmd":"set-b-version-preload","value":false}.
{"cmd": "set-b-version-preload", "value": false}- Parameter:
A/B Labels : When the "A" and "B" letters are on the picture in an A/B comparison (Edit > Preferences > General > A/B labels):
"interaction"(the default: while the wipe is dragged, turned or hovered, and when the view or a clip on A or B changes, then faded out after 1.5 s),"always"or"never". Saved in the profile (session_config.json, keyab_label_mode), as the Preferences are.statusreads it back asviewport.ab_label_mode, with each drawn label'sopacityinviewport.ab_labelsandviewport.ab_label_idle_s.- Parameter:
value(string). - Reply:
{"status":"ok","cmd":"set-ab-label-mode","value":"never"}, or"status":"error"with areasonfor any other value.
{"cmd": "set-ab-label-mode", "value": "never"}- Parameter:
Align Start Frames : How track B and the contact sheet's synced cards follow the playhead (the right-click menus of the A/B button and of the contact sheet).
true(the default): by position, A's first frame shows B's first frame, as always.false: by frame number, as RV with the option off -- B (and each card) shows its frame that carries the number of the playhead's frame, held at its first frame before its start and at its last after its end. Saved in the profile (session_config.json, keyalign_start_frames).statusreads it back astrack_b.align_start_frames, with B's own frame for the playhead intrack_b.frameand the frame whose picture B shows intrack_b.shown_frame; thecontact-sheetreply asalign_start_frames.- Parameter:
value(boolean). - Reply:
{"status":"ok","cmd":"set-align-start-frames","value":false}, or"status":"error"with areason(and the setting unchanged invalue) for anything but a boolean.
{"cmd": "set-align-start-frames", "value": false}- Parameter:
Metadata Tab Track : The Inspector's Metadata tab lists track A's metadata, B's, or both side by side (A|B) while A/B is on: it follows the view (A alone: A, B alone: B, the wipe: A|B) and its A · B · A|B segment (Classique: the radio buttons A, B, A|B) chooses. This command is a click on it:
"a","b"or"ab"(any case), held until the next change of view mode (A/B turned on or off included). Refused while A/B is off, when the segment is not up, and for any other value (reason:value must be "a", "b" or "ab").statusreads it back asmetadata.- Parameter:
value(string:"a","b"or"ab"). - Reply:
{"status":"ok","cmd":"set-metadata-track","value":"ab"}, or"status":"error"with areason.
{"cmd": "set-metadata-track", "value": "b"} {"cmd": "set-metadata-track", "value": "ab"}- Parameter:
Set Color Space : Sets the input color space of track A -- the Input Color Space of the Inspector and View > Color Space -- and marks it as chosen by the user, so the clip is no longer re-detected when it is loaded again.
- Parameter:
value(string, case-sensitive). The 11 accepted names, and the entry of the list each one selects:"Linear"or"ACEScg"(Linear ACEScg),"sRGB","ACES2065-1"or"AP0"(ACES2065-1 (AP0)),"AdobeRGB"(Adobe RGB),"ProPhoto"(ProPhoto RGB),"Rec709"or"LinearsRGB"(Linear sRGB / Rec.709),"LogC3"(ARRI LogC3, EI 800) and"LogC4"(ARRI LogC4)."Auto"gives the clip on screen back to the space its file gives (its metadata, else its kind, as a load decides it) and clears "chosen by the user": Essential's color space list, "Auto (detected: ...)", is the same setting.status.color.input_space_manualsays whether the clip's space was chosen by hand andstatus.color.input_space_detectedwhich one its file gives. With an OCIO config loaded (Color > Load OCIO Config... or ACES 2.0 built-in),valuemay also be a color space of that config, by its name or one of its aliases, without case ("ACEScg","srgb_tx","ARRI LogC4"). The words above keep their meaning and are tried first, exactly as written; then the config's spaces; then, with a config only, a name of the Input Color Space list, tolerant of case and punctuation, as the export reads it ("linear acescg", an index digit). A config space with an exact built-in twin moves the built-in index to it; one without a twin (a display encoding such as Rec.1886 Rec.709, a camera log the built-in pipeline does not know) is decoded by OCIO while OCIO draws the picture (OCIO step 3); when it does not, it changes no pixel, andstatus.color.input_space_name_shownsays which (true: the picture shows that space). Without a config nothing changes: no name is kept and nothing new is written anywhere. - Reply:
{"status":"ok","cmd":"set-colorspace"}, or for a name it does not know{"status":"error","cmd":"set-colorspace","reason":"unknown color space","error":"unknown color space"}:error, the key this reply used beforereason, is kept one more version. After a color space of the config the reply also says which one and where the built-in index went:{"status":"ok","cmd":"set-colorspace","input_space_name":"ACEScg","input_space":0}for"lin_ap1"("ACEScg"itself is one of the words above: the plain reply). The other reasons:"unknown color space: neither a built-in input space nor a color space of the OCIO config in use"(a config is loaded and does not know the name),"value must be a color space name (a string)","value is empty","value is longer than 255 bytes","value has a control character","value is not UTF-8".
{"cmd": "set-colorspace", "value": "ACEScg"}- Parameter:
Set the OCIO View : Picks the session's OCIO display, view and look (OCIO step 3), the one view every clip is drawn through while an OCIO config is in use (Color > OCIO Display / View and OCIO Look do the same from the menu).
- Parameters (all optional; only the keys given change):
display(string, a display of the config),view(string, a view of that display),look(string, a look of the config put in place of the view's own looks;""goes back to the view's own looks). Adisplayalone takes its first view. A key that is not a string, or not a valid name (empty, longer than 255 bytes, a control character, not UTF-8), is refused. - Reply:
{"status":"ok","cmd":"set-ocio-view","display":"sRGB - Display","view":"ACES 2.0 - SDR 100 nits (Rec.709)","look":""}(the view now drawn), with a"note"when the seat follows a session pilot on the same config: the pilot's view stays on screen (see Review sessions). Or{"status":"error","cmd":"set-ocio-view","reason":"..."}: no OCIO config is in use, a key is not a string or not a valid name, or the config does not have the display, view or look. A pick by this command is not written tosession_config.json; a pick in the Color menu is ("ocio_view").status.ociotells what is drawn.
{"cmd": "set-ocio-view", "display": "sRGB - Display", "view": "ACES 2.0 - SDR 100 nits (Rec.709)", "look": ""}- Parameters (all optional; only the keys given change):
Set the Shot's CDL : Gives a playlist row (a shot) an ASC CDL, or takes it away (OCIO step 5; Color > Load CDL..., Apply CDL and Remove CDL do the same from the menu). A CDL is slope, offset and power (three numbers each: R, G, B) and a saturation. It is applied after the player's grade and before the display (with an OCIO config: before the config's display, view and looks; without one: before the built-in display transform), in ACEScct, exactly as OpenColorIO's
CDLTransformwith the styleCDL_NO_CLAMP(the style OCIO reads a.cc/.ccc/.cdlwith) does it: slope, offset and power (power only on values above 0), then the saturation around the Rec. 709 luma, nothing clamped. The picture is the same with or without an OCIO config.Parameters:
row(integer, 0 or more, optional; default the row on screen), then one of:- the values:
slope,offset,power(each a list of three numbers) andsaturation(a number), each optional (its identity value when absent), at least one given, and optionallyid(a string, the correction's name; at most 128 bytes, no control character). They replace the row's CDL; the CDL'ssourceis"pipeline". Every value is checked: slope 0 to 100, offset -100 to 100, power above 0 up to 100, saturation 0 to 100, all finite. file(string): the path of a.cc,.cccor.cdl, with the optionalidof the correction to take. Withoutid, the file's only correction, or else the one whose id is the row's shot code (x_tracking.shot_name, elsex_tracking.shot_id), compared exactly (case-sensitive). The file is read off the render thread (at most 4 MiB and 10000 corrections, as OpenColorIO 2 reads them; a refused file is refused with its reason and line number), and the row'ssourceis its file name, never its folders.clear:true: the row has no CDL any more (rowis the only other key it takes).onalone (trueorfalse): applies or stops applying the row's existing CDL, which is kept either way.onmay also come with values or a file: it is then the new CDL's state. Several of values,fileandclearin one command are refused.
- the values:
- Reply (broadcast like the other replies):
{"status":"ok","cmd":"set-cdl","row":2,"cdl":{"slope":[1.06,1.0,0.94],"offset":[0.01,0.0,-0.01],"power":[1.0,1.02,0.98],"saturation":0.9,"id":"shot010","source":"pipeline"}},cdlbeing the row's CDL as a.bplitem writes it (see below), ornullafterclear. Forfilethe reply is at once{"status":"ok","cmd":"set-cdl","row":2,"pending":true,"file":"seq1.ccc"}(the file's name, never its folders): the file is read in the background, then the event{"event":"cdl-loaded","row":2,"ok":true,"cdl":{...}}says what the row took, or{"event":"cdl-loaded","row":2,"ok":false,"reason":"..."}why nothing changed:"seq1.ccc: the file has no correction \"x\"","seq1.ccc has 3 corrections and none has the shot's id: give an id", a file that cannot be read or parsed (the reason says what and the line),"the shot is no longer in the playlist". Or{"status":"error","cmd":"set-cdl","reason":"...","row":2}(rowwhen known):"give the values, a file or clear, not several","no such row","the row has no CDL"(onalone on a row without one), a value that is refused,"busy: files are still being read"(16 files wait already). A file result that comes after a newer change of the same row (values,clear, Remove CDL, another file) is dropped:cdl-loadedthen says"a newer change of the shot's CDL came first". A row that already has the CDL asked for is not changed (and not saved again), and still gets theokreply.
{"cmd": "set-cdl", "row": 2, "slope": [1.06, 1.0, 0.94], "offset": [0.01, 0.0, -0.01], "power": [1.0, 1.02, 0.98], "saturation": 0.9, "id": "shot010"} {"cmd": "set-cdl", "row": 2, "file": "D:/grades/seq1.ccc"} {"cmd": "set-cdl", "row": 2, "file": "D:/grades/seq1.ccc", "id": "shot010"} {"cmd": "set-cdl", "row": 2, "on": false} {"cmd": "set-cdl", "row": 2, "clear": true}Set Color Channel Isolation : Isolates specific color channels in the viewport viewer.
- Parameter:
value(string:"RGBA","R"/"Red","G"/"Green","B"/"Blue","A"/"Alpha", or"L"/"Luma"/"Luminance"). - Parameter:
track(optional,"A"or"B", or0/1). Without it the channel shows on A and on B, which links the two tracks again, as a click on a channel does. With it only that track's channel changes, as an Alt+Click does in the player. Any othertrackis refused ("status": "error"). An unknownvalueleaves the view as it was. - Reply:
{"status":"ok","cmd":"set-channel","value":"R","track":"B","linked":false}(trackonly when one was given;linked: whether A and B now show the same channel).
{"cmd": "set-channel", "value": "R"} {"cmd": "set-channel", "value": "R", "track": "B"}- Parameter:
Reset Layout : Instantly resets the user interface panels (timeline, playlist, inspector) to their default factory positions. It also closes every copy opened with New Window.
{"cmd": "reset-layout"}New Window : Opens a copy of a view, like Window > New Window > view: a second window showing the same view on the same state, for another part of the screen or another monitor. It opens at the original's size (the view's default size when the original is not on screen, never smaller than the view's minimum), 30 px x UI scale right and down of the original per copy already open, and always wholly inside the work area of the main window's monitor, at least 24 px x UI scale from its edges. It is placed on the next frame;
windowsreads its rectangle back.- Parameter:
view(string):"media-session","inspector","timeline"(the Timeline & Version Board panel),"contact-sheet","review-session"(the session bar),"session-review","chat"or"console"(the Console window)."viewport"is refused: the viewer has no copy. - Reply:
{"status":"ok","cmd":"new-window","view":"media-session","number":1,"name":"Media & Session (Copy 1)","rect":{"x":30,"y":111,"w":383,"h":694}}(rectin ImGui's coordinates, the screen's on Windows), or"status":"error"with areason: an unknown view (the reason lists the keys), the viewer, or a view that already has 4 copies open.
{"cmd": "new-window", "view": "media-session"}- Parameter:
Close Window : Closes a copy by its name, like Window > Open Copies > copy > Close;
"all": truecloses every copy (Close All Copies). The original views are shown and hidden by their ticks in the Window menu, not by this command.- Parameters:
name(string, asnew-windowandwindowsgive it), orall(boolean). - Reply:
{"status":"ok","cmd":"close-window","name":"Media & Session (Copy 1)"},{"status":"ok","cmd":"close-window","closed":3}forall, or"status":"error"with areasonwhen no copy of that name is open.
{"cmd": "close-window", "name": "Media & Session (Copy 1)"}- Parameters:
Windows : Lists the views and the copies open, with their rectangles: what the Window menu ticks and what Open Copies lists. Copies are not saved: a new run starts without them.
- Reply:
{"status":"ok","cmd":"windows","dpi_scale":1.0,"monitor":{"x":0,"y":0,"w":1920,"h":1032},"max_copies_per_view":4,"views":[...],"copies":[...],"popups":[...],"notices":{...},"hovered":{"window":"About Bernard Review","id":2718281828},"frame":1234}.monitoris the work area of the main window's monitor (the main window's own work area where the windows cannot leave it, on Linux and macOS). Each ofviews:view(the key),title,duplicable,on_screen(drawn and not a hidden tab),rect(ornullwhen not on screen) andcopies(how many are open). Each ofcopies:view,name,number,drawn(false until its first frame),rect(its window's, or where it will open while not yet drawn) andinside_monitor.popupslists the popups open, innermost last -- Help > About's window is"About Bernard Review", a modal has its title, a menu its label and its depth,"Help###Menu_00"-- each withname,drawn(false on the frame a popup that fits its content is measured, hidden) andrect(its window's, as last drawn).noticesis About > Third-party notices' window, the player's own (it opens nothing outside the player):title("Third-Party Notices"),open,drawnandrect(nullwhile not drawn),found(false: the window says "Third-party notices not found"),folder(where the files are:licensesbeside the program, a Linux package'sshare/bernardplayer/licenses, or the source tree's),files(the names listed,THIRD_PARTY_LICENSES.mdfirst, then alphabetical; every plain file of the folder whatever its extension, hidden ones left out),selected(the file shown,""for none),text("shown","too_large"above 2 MiB,"binary"for a NUL in its first 8 KiB,"unreadable","refused"for a link that leaves the folder,"none") andtext_bytes(the size of the text shown).hoveredis the window under the mouse (its name,""for none; Dear ImGui's name: in Dark and Light the bottom panel is"Timeline###Timeline & Version Board", whose id is that of"Timeline & Version Board") and Dear ImGui's id of the item under it there (0for none): what adebug-mouseclick would press, a button's id being Dear ImGui's hash of its label (ImHashStr, a CRC-32C) seeded with its window's id, itself the hash of the window's name. Asked once a frame,windowsgives a popup's size frame by frame;frameis the interface's frame count (Dear ImGui's) of the frame last drawn, the one these rectangles are of.
{"cmd": "windows"}- Reply:
Loading Media & Playlists
Open (single file) : Opens one video file or image sequence on track A by adding it to the playlist and making that item current -- exactly what dropping the file on the window, or "Open & Play", does. A file the playlist already holds is made current rather than added a second time. The reply carries
"playlist_idx", where the clip sits in the playlist. Replies with{"status":"error","reason":"file not found",...}if the path doesn't exist -- it no longer silently reports success on a missing file. The reply does not wait for the disk: a path new to the playlist is added at once and probed on a worker (its directory listed, its length and rate read), so for a moment after the reply its item'sdurationinram-infois0andstatus.playback.transitioningistrue; the clip appears when the probe lands. Only the existence check is made before replying.{"cmd": "open", "path": "D:/projects/seq_01/shot_10/render.mov"}(
"load"is accepted as an alias for"open".)Load Playlist : Loads a full playlist file --
.bpl(Bernard Review's own JSON format, seeplaylist.schema.json),.otio,.csv,.jsonor.edl-- bringing in every item's in/out points, notes, and annotations, not just a single bare path.{"cmd": "load-playlist", "path": "D:/projects/SQ010_review.bpl"}Save Playlist : Writes the playlist to a
.bpl, as File > Save Playlist does: every item with its notes, status, grade (every field, the white balance and the offset included, seeplaylist.schema.json), whether its input color space was picked by hand ("color_space_user_set":load-playlistthen keeps that space instead of detecting it again) and paint-overs -- each stroke with the track it was drawn on and that track's version ("track","version") -- and, in"clip_annotations", the paint-overs of clips that are on no row (track B's clip opened from the file chooser) and the notes kept of rows removed (their strokes, their note in"notes", and their tracker shot's name in"shot", which Review Notes names them by: the notes follow the media), and the playlist's View > Guides ("framing_guides", only when not the defaults: seeset-guides). A build from before these keys reads the file and ignores them. The images attached to the notes are written into the folder beside the.bpl,<its name>_attachments/, and each entry of a row's"attachments"names its copy there ("file").- Parameter:
path(string). - Reply:
{"status":"ok","cmd":"save-playlist","path":"..."}, or"status":"error"with areason.
{"cmd": "save-playlist", "path": "D:/review/sh010.bpl"}- Parameter:
Attach an image to a note:
note-attachattaches image files to rowrow's note, as a drop on its note field does (Review Notes, Essential): each file is read and checked off the render thread -- its type from its bytes (PNG, JPEG, WebP, EXR or TIFF; never SVG), 16 MiB at most, 8192 pixels a side at most, 8 images a note, the same image once -- then kept in the profile's store under its SHA-256 and shown under the note. The reply only says the files were queued; the result is instatus.note_attachments(last, and the row'sattachmentsorrefusal).- Parameters:
row(integer, a row of the playlist), andpath(string) orpaths(1 to 32 strings), each 1 to 4096 bytes. - Reply:
{"status":"ok","cmd":"note-attach","row":0,"queued":1}, or"status":"error"with areason(the row not in the playlist, no path, a path empty or too long): nothing is queued then.
{"cmd": "note-attach", "row": 0, "path": "D:/refs/ref_helmet.png"}- Parameters:
Remove an image from a note:
note-detach, as the image's ✕: the note no longer has it (its file stays in the profile's store).- Parameters:
row(integer), andindex(integer, from 0) orsha256(string). - Reply:
{"status":"ok","cmd":"note-detach","row":0,"count":1}(the images left), or"status":"error"with areason("no such image on this note"...).
{"cmd": "note-detach", "row": 0, "index": 0}- Parameters:
Playlist Management
Add to Playlist : Appends a file or sequence to the current playlist.
- Parameters:
path(string);x_tracking(object, optional): the row's production-tracking ids, the same block as a.bplrow's (seeplaylist.schema.json), a pipeline's own keys kept as they came. A pipeline adds a file this way once it is downloaded, so its review can still be published (Bernard Engine). - Reply:
{"status":"ok","cmd":"playlist-add","index":N},indexbeing the new row's; withoutindexwhen no row was added yet (a vector SWF is rendered first, then added).
{"cmd": "playlist-add", "path": "D:/projects/seq_01/shot_20/render.mov"} {"cmd": "playlist-add", "path": "D:/projects/seq_01/shot_30/render.mov", "x_tracking": {"shot_id": "sh030", "task": "Comp", "version_id": "v004", "artist": "Ana"}}- Parameters:
Play Playlist Item : Switches the active item to the given index. Answered in the tick that receives it: the incoming clip's directory listing and first-frame probe run on a worker, and
status.playback.transitioningstaystrueuntil the new clip is on screen. A secondplaylist-playbefore that retargets the switch.{"cmd": "playlist-play", "index": 2}Remove Playlist Item : Removes the row at
index, as Delete Shot does. Its notes follow the media: its strokes and its note are kept (status.media_notes.kept) and come back when the same file is added again; the viewer says "sh010 removed · 1 note · 2 drawings kept" with Undo (status.media_notes.toast). Reorder Playlist Item moves a row with its notes, and keeps none. Thecountofstatus.media_notesis the notes and the drawings together.{"cmd": "playlist-remove", "index": 2}Reorder Playlist Item :
{"cmd": "playlist-move", "from": 0, "to": 3}Clear Playlist : Empties the playlist. The notes of its rows are kept for their media, as a removal keeps them (
status.media_notes.kept), and come back when the same file is added again; opening another playlist keeps them too. Only "Delete notes of this clip..." erases them.{"cmd": "playlist-clear"}
Transport & Timeline Range
- Reverse Play :
{"cmd": "reverse-play"} Step Frame : Steps the playhead by
deltaframes (negative to step backward, clamped to the valid range).{"cmd": "step-frame", "delta": -1}- Toggle Loop :
{"cmd": "toggle-loop"} Set In-Point / Set Out-Point : Omit
frameto use the current playhead.frameis a zero-based index into the sequence, not the frame number the ruler prints. On a shot whose frames are numbered from 1000, the ruler's1010is index10, and that index is what to send. Read the two numbers below as indices: they mark frames 11 and 91 of the clip, on a clip at least 91 frames long. (The timeline's typed in/out field takes the displayed number and converts it; this command does not, and its behavior is unchanged.){"cmd": "set-in", "frame": 1010} {"cmd": "set-out", "frame": 1090}- Clear In/Out :
{"cmd": "clear-in-out"} Lock In/Out : the timeline menu's Lock In/Out. While
valueistruethe mouse never moves the in or out point -- no tab, no Alt+Drag, every press on the timeline is a scrub;set-in,set-out,clear-in-outand the keys still set them. Withoutvalueit changes nothing and the reply reads the lock back; so doesstatus.playback.in_out_locked. Kept across restarts.{"cmd": "set-in-out-lock", "value": true}
Viewport
- Zoom :
{"cmd": "zoom", "value": 1.5}-- a fraction of the source resolution (1.0 = one source pixel per screen pixel, whatever the proxy tier). Leaves fit mode. Clamped to 0.01..100. - Pan :
{"cmd": "pan", "x": 10.0, "y": -5.0}-- screen pixels from the centered position. Leaves fit mode. - Fit to Screen :
{"cmd": "fit"}-- back to fit mode: the picture is re-fitted every frame until the next zoom or pan. - Flip Horizontal :
{"cmd": "flip"} - Toggle A/B Mode :
{"cmd": "toggle-ab"} Display Mode :
{"cmd": "set-theme", "value": "sombre"}-- the interface's display mode, what the display button of the menu bar and Preferences > Appearance choose:"sombre"(Dark, the default: the site's palette, the Geist fonts),"clair"(Light: the same in studio grays; around the picture a neutral 18 % gray),"neon"(Neon: Dark with deeper blacks, a cyan accent, a magenta playhead and lit tools; the viewer stays black) or"systeme"(System: Dark or Light, as the operating system is set: Windows' Choose your app mode, read fromAppsUseLightTheme, or on Linux the freedesktop portal's / GNOME'scolor-scheme; Dark when the system gives no answer, never Neon; followed while the player runs). The mode applies at the top of the next frame, without a restart (the style is set again between two frames; Dark, Light and Neon share their font atlas), and is saved in the profile (session_config.json, keyappearance). Withoutvalue, it reads the mode asked for. The names the interface shows are aliases, in any case:"dark"for"sombre","light"for"clair","system"for"systeme"("neon"is both); the keys stay what the profile saves and what the reply andstatusgive.- Reply:
{"status":"ok","cmd":"set-theme","value":"clair"}(the key, also for"light"), or"status":"error"with areasonthat namesdark, light, neon or system, for any other value."classique"is refused: the Classique mode no longer exists (2026-10-06), and the reason says so. A profile saved in Classique opens in Dark. statusreportstheme(the mode in use:"sombre","clair"or"neon", never"systeme"),theme_requested(the mode asked for,"systeme"included: the two differ until the next frame, and under System),theme_system(the system's setting as last read,"light","dark"or"unknown", ornullwhile System has not been asked for since the start),theme_rebuilds(mode changes applied since the start),theme_font_builds(how many of them built the skin's font atlas again) andtheme_button_rect(where the mode button is, fordebug-mouse; in Dark, Light and Neon, the display button's box).- The display button: one button at the right of the menu bar
holds the interface level and the display mode. Its
words say the state,
Complete · Darkafter a half-filled circle; when the bar is short it shows the theme alone, then the circle alone, its tooltip always the whole state. It sits 24 px x the UI scale left of the window buttons' zone (3 x 40 + 10 px at the bar's right end: the player draws its own Minimize, Maximize and Close there on every system). A click opens a popover: INTERFACE, a segment Essential | Complete and a sentence on the level in use; THEME, Dark, Light, Neon and System, the one asked for ticked. A level applies at once (the popover stays), a theme applies and closes it; Escape or a click outside closes it; Tab, the arrows and Enter drive it (the player's shortcuts are quiet while it is open). The same choices are in View > Interface and View > Theme.statusreportsview_control:nullin full screen, elseform("full","theme"or"icon"),label(the words shown,""for the circle alone),rect(the button's box),window_buttons([x0, x1], the window buttons' zone),margin_to_window_buttons(pixels between the two),hovered,tooltip, andpopover:nullwhile closed, else itsrect,focus(the keyboard's item: 0 Essential, 1 Complete, 2 to 4 the themes, -1 none yet),levels(level,rect,lit),hint(the sentence) andthemes(theme,rect,ticked).
- Reply:
Interface Level :
{"cmd": "set-ui-level", "value": "essential"}-- Essential or Complete, what the menu bar's switch, Window > Essential / Complete, Preferences > General > Appearance > Interface andCtrl+Shift+E(Keyboard Mapping: Toggle Essential / Complete) choose. Essential is a display filter on the same windows: the docked panels show Shots to review, the transport and the shot's note, Image and the session; the viewer a floating toolbar; the rest stays one menu away (their copies, Window > New Window, are Complete). Changing the level changes what is drawn and nothing else: no saved setting, no grade, no status, no session, not the docked layout. It applies at the next frame and is saved in the profile (session_config.json, keyui_level); a first installation is asked at its first launch ("I mostly review" / "I need all the tools"), an existing profile stays Complete and is never asked. A level set by this command answers that question. Withoutvalue, it reads the level. The question also offers, optionally, a name and an e-mail: what is typed is kept with the answer, in the profile (session_config.json, keysuser_nameanduser_email, written only when set), used by the watermark and as this seat's identity in a review session; Skip leaves the question unanswered (Complete) and keeps the defaults. The defaults: the system account's name and no e-mail -- no address is ever made up; an address a profile holds (typed, or from a signed-in account) is kept as it is. The watermark prints the fields that are set, a|between two of them and none at an end.- Reply:
{"status":"ok","cmd":"set-ui-level","value":"essential"}, or"status":"error"with areasonfor any other value, which names the value as it was sent (unknown interface level "simple": essential or complete,unknown interface level 1: essential or complete). - Essential decides how the clips decode: Smart RAM-Fit for every clip ("When Decoding Is Too Slow" is the saved
one in Essential too: Essential no longer turns Auto on). These are the values in effect while the level is
Essential; they are never written. The saved
ones -- each clip's tier (the playlist, the session's playlist in
session_config.json, the autosave) and "When Decoding Is Too Slow" (slow_decode_tierin the profile) -- stay as Complete left them and are in effect again in Complete.statusgives both:viewport.proxy_idx(the clip on screen's saved tier, 0..3 fixed, 4 Smart RAM-Fit) andviewport.proxy_idx_effective(the one in effect);decode.slow_decode_tier(saved) anddecode.slow_decode_tier_effective; eachplaylist[]entry's andram_fit.track_b'sproxy_idx(saved) andproxy_idx_effective(in effect;effective_proxy_idxis still the tier the fit gives it); andui_level_ui.auto_decoding(truein Essential, where a discreet "Auto" says so beside the counter). - A harness that starts the player on a fresh profile (a private
APPDATAwith nosession_config.json) meets the first-launch question, a modal the viewer waits behind: sendset-ui-levelfirst ("complete"keeps the player as it always was), or seed asession_config.json.tools/player_blind_soak.pyandtools/quit_exit_time.pydo. statusreportsui_level("essential"or"complete") andui_level_ui:question_pending(the first-launch question still to answer) andquestion(the boxes of its two answers,reviewandall_tools, of its optionalnameandemailfields and ofskip, while it is drawn, elsenull);switch(the menu bar'sessentialandcompletesegments and thewholepill, in Classique at both levels,nullin Dark and Light, where the display button's popover holds the two levels (view_control), and in full screen);changes(level changes since the start);before_after(Essential's Before / after toggled) andbefore_after_applies(the picture shown without its grade this frame: toggled, and Essential);auto_decoding(above);windows(the views Essential keeps back --review-session,contact-sheet,console,session-review,chat-- each with its ownflagand whether it isshown);finish_review(at both levels:close_question--null, or while the close asks about unpublished notes or dictated drafts itstext(""with drafts alone),drafts("2 dictated drafts not validated.",""without) andbuttonsby their words,Publish and Close(only with unpublished notes),Close Without Publishing,Cancel--,reminder-- the launch's "Unpublished review" line as it was said,""otherwise --,menu_item-- File > Finish Review... while the File menu is open --,stage--idle,confirm,publishing,published,failed,export_offer,waiting_notes(the note-takers of the online review still writing;textsays who, its buttonsWait,Finish AnywayorPublish Anyway,Back to Review) --,notesandstatuses(the summary's counts),not_linked(the shots the run could not send, not linked to the site connected: the summary names them, "· 2 shots not linked to Flow were not sent", and offersExport as HTML),failure(what failed, then "Everything is saved." or "Review not saved."),textandbuttons(the window's line and its buttons by their words, while it is drawn; inexport_offer, no button while the save's chooser is open, then "Review saved." or "Review not saved."),banner,banner_finishandbanner_close(Essential's banner,nullwhen not drawn); in Essential,inspector.publishandinspector.publish_labelare Publish (N) beside the tracker's pill);essential(nullin Complete; in Essential the boxes of what its panels drew:shotswith itscards,pillsandversions(per shot of the playlist,nullwhen the filter hides it),filters(All, To review),head,list,shown,addandadd_menu_open("+ Add Shots"'s menu drawn: "Add Media..." first, then Open File or Sequence... and Add Folder..., the playlist's);toolbarwith its seventools(Selection, Brush, Arrow, Shape, Text, Eraser, Laser),paint(the color and size, whose menu holds the six colors, Size, Opacity and the onion skin),onion,compare(A|B) andcompare_menu,layer(nullwith one layer),before_after,view(guides, Flip & Rotate, zoom, full screen),menus_open(paint,compare,layer,view),bar,view_lit(View lit: a flip, a rotation) andview_menu_items(View's entries by their words while its menu is open:Flip & Rotate,Flip Horizontal,Flip Vertical,Zoom 100 %,Fit,Full screen);context_line(the line over the viewer,nullwithout a shot) withline,path,version(its menu: the shot's versions;version_menu_open),who,label_aandlabel_b(while A/B is on);transport(the row of buttons over the strip, as wide as the window) withrow,buttons(in,prev_shot,first,prev,play,next,last,next_shot,out,loop,volume, left to right: Complete's actions, each frame button held inside the range),prev,playandnext(the same boxes),counter(the frame number on screen and the last one, 7 digits each),timecode,fps(nullwhen the row has no room),bar(the strip of thumbnails with the scrub, or the thin bar:strip_drawn),cache(the RAM cache's line under it),status_y(where the status row starts),in_grabandout_grab(where a press takes the in or out bracket,nullwhile they are locked),in_xandout_x(the brackets),cache_ticks(frames of the clip on screen in the RAM cache),auto,loop_menu_openandvolume_menu_open(Loop's and the sound's right-click menus) andnumber_text/timecode_text(the counter's words);status_row(under the strip of thumbnails) with its threesegmentsin the row's order -- Not Approved, Pending, Approved, the keys 1, 2 and 3 -- eachstatus("not_approved","pending","approved"),icon(the icon drawn in its badge,"cross","clock"or"check"),count(the playlist's shots in it),lit(the shot on screen's) andrect;lit(the shot on screen's status,nullwithout one),next_shotandnext_enabled(grayed on the last shot),next_label("Next Shot", or"Finish Review"on the last shot once every shot has a decision),hint(the reminder "1 / 2 / 3 decide and move to the next shot",nullwhen it has no room),row, andstrip_outline(the role of the strip's outline in the shot's status,review_approved_bg... ornull); a click on a segment sets the shot's status and stays on it, as its pill's menu does; the keys (Essential and Complete; the zoom presets are Alt+1 and Alt+2) also move to the next shot;inspectorwithtitle("SH0040 · v003"),exportandexport_menu_open(HTML; CSV and PDF grayed: no exporter),link_pillandlink_pill_rect(what the player is connected to, "Connected to Bernard Engine" or "Connected to Kitsu", "" andnullwhen nothing is: the Tracker section is gone; with a tracker, the pill opens Publish),link_badgeandlink_badge_rect(what the shot on screen is linked to, under the head: "Flow · SH010 · v003", "Not linked", "Linked to another site" or "Flow · SH010 · no version id"; "" andnullwhen no tracker is connected and the shot is linked to none),color_head(Color correction, folded by default:reset,channels,color_spacearenullwhile it is),notes,reset, the sixchannels,paint_overs(the note cards, about 150 x 84 with "frame N · timecode"),paint_over_crosses(each card's delete, the same as Review Notes'crosses) andpaint_over_crosses_shown(how many are drawn: only the one under the pointer),color_space(the shot's input color space: "Auto (detected: ...)" or Complete's list),start_session,join,end_session,chat,mic,hand,invite,give_control,footer(the session's actions, pinned at the bottom of the panel, always on screen) andscroll(what scrolls above them); in a session, the room's row at the head of the pinned block:avatars(one box per seat shown, in People's order: this seat, the pilot, then the room's),avatar_ids(whose each one is),avatars_more(the "+N" button, which opens People,nullwhile everybody fits),avatars_overflow(its N, 0 without it) andavatar_menu(the menu a click on an avatar opened -- People's own:for, the person's id, and itsitems, eachlabel,enabledand its box,rect-- elsenull), andraised_hand(the host's answers to a raised hand, as insession_ui.bar.raised_hand)), andearlier(manu/earlier-notes: the shot's Earlier notes under Notes,nullwithout the section -- Bernard Engine not connected, or a row without its ids --:head, its fold; unfolded,status_line(the line under the cards, alone without them,""none),cardsandthumbs(their boxes; a card off screen only takes its room: its thumbnails are not listed),replies(each card's replies fold,nullwithout replies or off screen),fix_tags(each card's OPEN or FIXED tag, the click that ticks it;nullfor a DONE or off screen),replies_shown,show_olderandtry_again(nullwhen not offered); folded, empty lists;title, the head's words as drawn, "Earlier notes (7)"), and, besideessential,earlier_full_picture(the picture of a thumbnail opened full size:nullwhile closed, elsestate--"loading","ready"or"failed"--,width,height,pictureandclose, their boxes ornull; never the path)). Every box is[x0, y0, x1, y1]indebug-mouse's coordinates.
{"cmd": "set-ui-level", "value": "essential"} {"cmd": "set-ui-level", "value": "complete"} {"cmd": "set-ui-level"}- Reply:
Set Track B (A/B comparison) : Assigns a file directly to track B, for comparing two versions side by side.
{"cmd": "set-track-b", "path": "D:/projects/seq_01/shot_10/v004/render.mov"}- Fullscreen :
{"cmd": "set-fullscreen", "value": true}-- enters (true) or leaves (false) fullscreen, asF11, the toolbar button and the View menu do.status.viewport.fullscreenreads it back. - Presentation Mode :
{"cmd": "set-presentation", "value": true}-- enters (true) or leaves (false) presentation mode, asShift+F11and the View menu do: the full screen with only the picture.status.viewport.presentationreads it back,status.viewport.presentation_controlssays whether the mouse has the timeline on screen now. Window Size :
{"cmd": "set-window-size", "width": 1920, "height": 1000}-- a verification tool : the main window's client area, in pixels, so that a script measures the interface at several widths. Whole numbers from 640 x 480 (the player sets no smaller size of its own) to the work area of the screen the window is on; anything else is refused with areason("width 2560 is out of bounds: 640 to 1920 (the work area of the window's screen)","height must be a whole number of pixels"), never clamped. In full screen it is refused (leave it first); a maximized window is restored first and the reply says so (left_maximized). The window is moved only as far as it must to stay on its screen.- Reply:
{"status":"ok","cmd":"set-window-size","asked":[1920,1000],"width":1920,"height":1000, "framebuffer":[1920,1000],"pos":[0,0],"work_area":[0,0,1920,1032],"left_maximized":false}-- the size obtained. Limits: a window manager may give another size, give it a frame later, or refuse it (a tiling manager); a screen smaller than 640 x 480 refuses every size.status.windowsays what the player has.
- Reply:
Interface Scale :
{"cmd": "set-ui-scale", "percent": 200}-- a verification tool: the interface scale, 100, 125, 150 or 200 (percent), the setting Preferences > General > Global UI Scale writes: it is saved in the profile (dpi_scaleinsession_config.json) and kept from one start to the next -- run it on a test profile, not on yours. As with the Preferences, what is drawn at the scale's size follows at once and the fonts and the style at the next start (restart_for_fonts). Withoutpercent, reads it back. Another value (150.5,"150",175) is refused with areason.- Reply:
{"status":"ok","cmd":"set-ui-scale","previous_percent":100,"percent":200,"ui_scale":2.0, "saved":true,"font_scale_percent":100,"restart_for_fonts":true}. - Neither command can be sent from a review session: a guest, the relay and the companion have no way to the dispatcher (only this socket and the player's Console feed it). The Console sends them without asking (they delete nothing).
- Reply:
Debug mouse : a mouse position (screen coordinates) and left button fed to the interface every frame in place of the real cursor, so a script can check what hovering and dragging do -- the fullscreen panels' auto-hide above all -- without moving anybody's mouse.
xandyset the position and turn it on,downholds or releases the left button,altholds or releases the Alt key (the timeline's Alt+Drag of an in/out point),rightpresses (true) or lets go of (false) the RIGHT button at the position already given -- a context menu, a playlist row's "Compare as B" (status.track_b.from_playlist.menu_entrysays where it was drawn) --,ctrlandshifthold or let go of Ctrl and Shift for the clicks that follow (a playlist row's Ctrl+Click and Shift+Click:status.track_b.compare_selection.picked),middlepresses (true) or lets go of (false) the MIDDLE button at the position already given (a drag with it held pans the picture; a pen's upper button is sent as a middle click),penmakes the pointer a pen:{"pressure": 0..1, "eraser": true|false, "barrel": true|false}, each key optional (nopressure: a pen without pressure;barrel: its side button held, the right press then opens the pen's quick menu with a drawing tool in hand), pressing whiledownholds, read through the same path as a real pen'sWM_POINTERonce the message is read (status.paint_over.pen);"pen": falsemakes it a mouse again. Any otherpen, a key of another type or a pressure out of 0..1 is refused:{"status":"error","cmd":"debug-mouse","reason":"..."}, the rest of the command applied.releasegives the real cursor back and lets go of all. Each stays as last sent until a later command changes it. The reply'saltreads the key back. Debug-only: while it is on, the real mouse is ignored.status.timelinesays where the ruler, the brackets and their tabs were drawn. It feeds Dear ImGui's own input events and never needed the Dear ImGui Test Engine, which the player does not contain (see "Frame, not window" under Screenshot, below).{"cmd": "debug-mouse", "x": 1720, "y": 1428, "down": false} {"cmd": "debug-mouse", "x": 1330, "y": 1716, "down": true, "alt": true} {"cmd": "debug-mouse", "x": 190, "y": 540, "right": true}Drop (files dropped from the desktop) : Hands
pathsto the same handler a real drop from the file manager reaches, with a point standing in for the cursor, so the timeline's drop targets can be driven from a script.atnames a target the last frame drew:"track_a"or"track_b"(the center of that timeline row) or"viewport_b"(the center of the empty B side of the picture); or give a screen point withxandy; with neither, the drop has no position and takes the default route. The reply'stargetsays what the drop was taken for:"insert_after_current"(track A's row: the clips go right after the current item and the first comes on screen),"load_on_track_b"(the first clip onto track B, A/B on, not added to the playlist) or"default"(the usual add-or-replace behavior; also for a folder or a playlist file, wherever it lands).{"status":"error","reason":"'track_b' is not on screen"}when the named target was not drawn -- track B's row while A/B is off, orviewport_bonce B is loaded. A file whose format this build lacks (.nvfxin a player built with-DBERNARD_ENABLE_NOVA=OFF) is refused wherever it lands, as a real drop refuses it: with nothing else dropped the reply is{"status":"error","reason":"This format is not supported in this build.","path":"<first refused>","refused":[...]}and the playlist, track A and track B are unchanged; dropped together with other files, the others go on as if dropped alone and the reply is the usual one with"refused": [...]added.{"cmd": "drop", "paths": ["D:/projects/seq_01/shot_10/v004/render.mov"], "at": "track_a"}
Contact Sheet
Contact Sheet : Opens, closes and drives the Contact Sheet 2D window, and says what each card shows. With Multi-Play (
"mode": "synced") every card plays with the playhead, in play and in pause, at its clip's own rate, from a ring of pictures prepared ahead of the playhead at a tier chosen for the card (Smart RAM-Fit for the card's size on screen, never above the clip's own tier, lowered when the cards would exceed their video-memory budget). A card that cannot show the playhead's frame says so on screen (LATE +n,MISSING,UNREADABLE) and in the reply. Every parameter is optional; the command with none only reads.- Parameters:
open(boolean: show or hide the window -- as Window > Contact Sheet 2D: in Essential, which keeps the window back,trueshows it; the reply'sopenis whether it is on screen),mode("synced": Multi-Play on, the default at every launch, the setting not being saved;"free": off until the player quits, the cards show the timeline strip's thumbnails),card_w(card width in pixels, 80 to 960),reflow(boolean: re-pack the grid),size([width, height]: undock the window and place it at the top left of the main window at that size),reset(true: zero every card's counters,elapsedandui_frames),trace(an object{"points": [[u, v], ...], "max_events": 200000}arms a diagnostic trace,falsedisarms it),trace_read(true: return the trace and empty it),layout_view(boolean: the contact sheet in the viewer, in place of the picture, as View > Contact Sheet Layout; on, Multi-Play is turned on),layout_clean(boolean: its cards as their pictures alone, no ground, outline, name, status nor badge, as its menu's Pictures Only;zoomzooms the layout view too, on its center, 1 to 5, 1 giving the grid that fits back) andpress_key("layout": the layout view's chord,Ctrl+Shift+Lunless rebound, pressed through Dear ImGui's input queue as the keyboard would -- a verification tool; the view toggles on the next frames) andtype_text(a string of at most 256 bytes, typed as the keyboard types into the field that has it -- a note being written in Dark and Light; a verification tool). - The layout view: the contact sheet drawn in the viewer instead of the picture, the reference player's
"Layout" view. Its grid is automatic: of every column count, the one with the largest cards that fit the
viewer, centered, the cards in playlist order (never below 48 px wide: past that the grid scrolls with the
wheel). It has no bar; the right-click menu keeps every setting and adds "Back to the Picture",
"Pictures Only (no borders, no info)" and "Fit". The wheel zooms on the mouse (1 to 8; at the fit a grid taller than
the viewer still scrolls, Ctrl+wheel zooms it), Alt+Drag or the
middle button pans once zoomed; back at 1 the grid fits the viewer again. While it is
shown the Contact Sheet 2D window and the bottom panel's tab say where the sheet is and draw no card. The
same key, the menu entry or
"layout_view": falsebrings the picture back. - Checks: every field is checked before any is applied. A field of the wrong type (
open,reflow,reset,trace_read,layout_view,layout_cleanmust betrueorfalse;mode"synced"or"free";press_key"layout";type_texta string of at most 256 bytes;card_wa number;sizetwo numbers;tracean object orfalse, itspointsan array of at most 64[u, v]pairs of numbers, itsmax_eventsa number) changes nothing and is answered{"cmd":"contact-sheet","status":"error","reason":"size must be [width, height], two numbers"}(the reason names the field). Numbers are brought into range:card_w80 to 960,size200 x 150 to 16384 a side,uandv0 to 1,max_events1000 to 2,000,000. Reply:
open,mode,align_start_frames(set-align-start-frames),card_w,layout_clean,layout_zoomandlayout_moved(the layout view's own zoom, and whether it is off the fit),playing,playhead,elapsed(seconds since the lastreset),ui_frames(interface frames since then),budget_bytes(what the cards' rings may take),ring_bytes(what their textures hold now),workers(producer threads started),active_workers(those taking jobs for the present demand: one per synced card or Steps pane drawn, within what the cores and the RAM allow),demand(those cards and panes),produced(from_ram,decoded,failed: pictures made since start-up),trace_armed,trace_events, andcells, one per card drawn in the last second, in playlist order:index,path,label(name and version once,"demo_sh010_comp · v001"),duration;tier(the scale,0with Multi-Play off or before the source is measured),tier_w/tier_h(the pictures' size),item_tier(the clip's own Smart RAM-Fit tier);wanted(the frame the card should show: the playhead's, or the hovered one while paused),drawn(the frame of the picture it draws,-1for none),state("exact","late","waiting"-- no picture yet, its ring being filled --,"missing"-- frames the sequence's numbering skips -- or"unreadable"),late_by,source("ring","strip","poster"or"none");displayed_frames(pictures really displayed since the reset: a new picture counts, a repeated one does not) anddisplayed_fps(those of the last second);draws,exact_draws,late_draws,waiting_draws,missing_draws,unreadable_draws(interface frames in each state, counted once per frame however many views draw the card),max_late;from_ram,decoded,failed(pictures of this card read from the RAM cache, decoded, or unreadable),ring_frames(the frame each ring slot holds,-1empty),unreadable_frames(their file numbers) andgaps([[1017, 1024]]: file numbers missing from the sequence).
- Trace: with
trace_read,traceis a list of events, one per counted draw:[t, ui_frame, card_index, wanted, drawn, playhead, state],statebeing0exact,1late,2missing,3unreadable,4waiting. When the card's picture changed (and on its first draw after the trace was armed), the event also carries[r, g, b, ...], the mean 8-bit color of a 3x3 neighborhood of the drawn texture at each of thepoints(normalized,vdownwards), then the texture's width and height -- read back from the GPU, which is how a script proves what a card displayed without a screenshot.trace_droppedcounts events pastmax_events. A trace reads textures back every time a picture changes: arm it for a measurement, not for a session.
{"cmd": "contact-sheet", "open": true, "mode": "synced", "card_w": 320, "reflow": true, "size": [1600, 900], "reset": true}{"cmd": "contact-sheet"}- Parameters:
What the Viewer Shows (View)
View (
view) : What the viewer shows, as one choice -- the picture, the contact sheet (the layout view above), the edit or the shot's steps -- as the View control at the viewer's top right chooses it (Complete; in Essential the keys and this command still switch). Not saved: every launch starts on the picture. The contact sheet keeps its own ways in (its key, View > Contact Sheet Layout,contact-sheetlayout_view):viewand they always agree. Edit and Steps are offered once this build draws them (layouts[].offered); this version draws the picture, the contact sheet and, in Complete, the edit (editbelow; in Essential it is not offered: its bottom panel has no edit band). The command with no field only reads.- Parameters:
layout("picture","contact-sheet","edit"or"steps": show it; the layout already shown stays shown -- a key or a menu row of the layout shown brings the picture back, never IPC),press_key("contact-sheet"or"edit": that layout's key --Ctrl+Shift+L,Ctrl+Shift+Munless rebound -- pressed through Dear ImGui's input queue as the keyboard would; a verification tool, the layout changes on the next frames;"steps"has no key in this version). - Checks: each field must be one of those names, spelled exactly;
layoutandpress_keytogether, another type, an unknown name or"picture"forpress_key(it has no key) change nothing and are answered{"cmd": "view", "status": "error", "reason": "layout: unknown layout \"stack\"; expected one of: picture, contact-sheet, edit, steps"}. A layout that cannot be shown now is refused the same way, with its reason:"edit: the playlist is empty","steps: the playlist is empty","steps: no shot on screen"(Steps stacks the playlist on screen, first),"edit: a Complete layout: not in Essential","steps: not in this version of the player"; the reply'slayoutis then the one still shown. Apress_keywhose layout has no key ("press_key: steps has no key in this version") or none bound ("press_key: edit has no key bound") is refused; a key pressed is answeredokat once, and the layout's own refusal, if any, comes as the player's notice, as for the keyboard. - Reply:
layout(the one shown) andlayouts, the four in the menu's order, each withlayout,offeredandrefusal(nullwhen it can be shown now). A change sends the eventview-changed.
{"cmd": "view", "layout": "contact-sheet"}{"status": "ok", "cmd": "view", "layout": "contact-sheet", "layouts": [{"layout": "picture", "offered": true, "refusal": null}, {"layout": "contact-sheet", "offered": true, "refusal": null}, {"layout": "edit", "offered": true, "refusal": null}, {"layout": "steps", "offered": false, "refusal": "not in this version of the player"}]}- Parameters:
The Edit (
edit) : The playlist as one film, as the Edit layout shows it: its rows one after the other, each its played range (its in and out marks, else its whole length; a row whose length is not known yet takes no time until it is), the edit's time from00:00:00:00. Mixed rates: each shot plays at its own rate, and the edit runs on one clock, its first shot's rate (fps): frames, timecodes and the band are on that clock, with no jump at a cut; a shot of another rate is marked with its rate in the band. Read only: a field is refused ("edit reads only: it takes no field", whatever the field); the layout itself is chosen byview.- Reply:
shots(how many),total(frames of the clock) andtotal_timecode,fps(the clock),other_rate_shots(the shots of another rate),frameandtimecode(the playhead in the edit;-1andnulloutside it),shot(the shot on screen, an index into the edit),buildsandsegment_builds(how many times the edit and its band were built: drawing builds nothing),band(the band as drawn on the last frames,nullwhen not drawn:rect,segments-- shots narrower than a pixel share one --,rate_marks(the shots marked with their rate),compact(in the panel's compact line, in place of the scrub),line),shot_list(each shot:row,first(its first frame played, 0-based in its media),length(its frames),startandedit_length(on the clock),fps(its rate), at most 5000) andshots_listed.status.editholds the same, withoutshot_list.
{"cmd": "edit"}- Reply:
Steps of a Shot (
steps-of) : The steps of a playlist row's shot (layout, anim, comp...), as the Steps view will show them: the shot is the Version Board's that holds the row's media (its naming rules, orbuild-matrix), else the row'sx_tracking.shot_name; each department of the shot is a step, and a row of the same shot whose media is not in the board adds its version to the step of itsx_tracking.task. Departments that mean the same step are one (comp,Compositing,CMP: synonyms, without case). Their order:step_orderof naming_rules.json when it has one, else Animatic, Layout, Anim, Lighting, FX, Comp, then the others by name. At most 4 steps are listed, the row's own step always among them;hiddensays how many were left out. Each step shows by default the row's own version, else its playlist row of highest number, else its latest version on this computer. Read only.- Parameter:
row(optional, 0-based playlist row; without it, the row on screen). - Checks:
rowmust be an integer in the playlist ("row must be an integer","row 12 is not in the playlist (10 rows)"); without it and with no row on screen:"no row on screen; give \"row\"". - Reply:
row,sequenceandshot(the board's;sequenceempty when the shot came fromx_tracking),steps(in order:name,default_version-- an index intoversions--,versions:name,number(-1without one),department,available,board_active,playlist_row(-1when no row plays it),path),found,hidden,current_step(the row's step,-1none) andbuilds(how many times the player looked for a shot's steps since it started: asking again with nothing changed does not look again). - naming_rules.json:
"step_order": ["Layout", "Anim", "Comp"]and"step_synonyms": {"Comp": ["cmp", "comp"]}(an array of up to 32 names; an object of up to 32 names, each with up to 16 aliases; a name of 1 to 64 characters). A wrong value, or an alias another step already goes by, is refused whole: the player logs why and keeps the default order and synonyms.
{"cmd": "steps-of", "row": 3}{"status": "ok", "cmd": "steps-of", "row": 3, "sequence": "SQ010", "shot": "SH040", "steps": [{"name": "Layout", "default_version": 0, "versions": [{"name": "v005", "number": 5, "department": "layout", "available": true, "board_active": true, "playlist_row": -1, "path": "D:/SQ010/SH040/SQ010_SH040_layout_v005.mov"}]}, {"name": "Anim", "default_version": 0, "versions": [{"name": "v012", "number": 12, "department": "anim", "available": true, "board_active": true, "playlist_row": 3, "path": "D:/SQ010/SH040/SQ010_SH040_anim_v012.mov"}]}], "found": 2, "hidden": 0, "current_step": 1, "builds": 1}- Parameter:
Steps (
steps) : The stack of the Steps view (View > Steps): up tomax_stepssteps (the reply says how many: 6 in this version; a client reads it rather than assume it) -- comp, anim, layout, animatic... --, each a whole playlist, aligned line by line (line k of every step is the same shot) and played on one clock: the playlist on screen, on track A, whose Edit cut is the clock. The step on screen, when it is another, plays on track B, shown alone, at the same frame;Up/Downin the Steps view change the step on screen (round at either end),Wwipes the pair -- the playlist on screen on A, the step on screen on B (or, with the playlist on screen on screen, the step before it) -- andShift+4shows its Difference. The sound is the step on screen's. A line whose shot token differs between the steps (sh0030in one,sh0040in another;sh0030andsh30are one) or that a step lacks is marked, never blocked. The stack is not saved. Withoutaction, the state (asstatus.steps).- Actions:
build(steps: 1 tomax_stepssteps,current: the step on screen, 0-based; the stack built anew),add(step: one step more),find-by-name(the steps the names and the Version Board give each row's shot, assteps-offinds them: the playlist on screen is the clock, the other steps their default version per line),set-current(step),look(value:"stack","wipe"or"difference"),rename(step,name),remove(step),move(step,by:-1or1),clear,display(value:"stack", one step at a time, or"layout", the steps side by side, 2 x 2 for four, 3 x 2 for six, in lockstep: the playhead waits for the slowest pane),send(lines: the playlist on screen's lines, 0-based, andstep, orvalue"new"and an optionalname: what a playlist row's menu "Send to step" does -- each row goes to its line in the step, replacing the row there, the step growing with holes; one undo step; refused for the playlist on screen's own step, a full stack, a line out of range),undo(the Undo chord, Ctrl+Z, pressed as the keyboard would: it undoes the last send while its toast shows, else the drawings' undo). - A step:
{"current": true}(the playlist on screen; one per stack),{"playlist": "<file>"}(a .bpl, OTIO, CSV or EDL, read by the importers off the render thread: the reply'sloadingcounts the readings under way; abuildthat names files is read whole, in its order, and replaces the stack once read; anaddjoins it then; a reading for a stack built anew since is dropped; a row of a .bpl without a path stays a hole, its line kept; a file over 64 MiB is refused), or{"rows": ["<media>", ...]}(1 to 5000 paths); each may carry"name"(else the step word its files share --comp,anim,lay... --, else the playlist file's name, else "Step N"). - Checks: every field first, nothing changed on a wrong one:
"unknown action ...","steps must be an array of 1 to 6 steps","a step is one of {\"current\": true}, {\"playlist\": \"<file>\"} or {\"rows\": [...]}","rows must be an array of 1 to 5000 media paths","each row must be a media path of 1 to 4096 bytes","the playlist on screen can be one step only","step 6 is not in 0..5","by must be -1 or 1","give a \"name\" that is not empty","name is longer than 64 bytes","value must be \"stack\" or \"layout\"","value must be \"stack\", \"wipe\" or \"difference\""; then the stack's own:"a stack holds 6 steps at most","the playlist on screen is already step \"Comp\"","no step 3", and forfind-by-namewhy nothing could be stacked ("only one step (Comp) was found by name"...). - Reply:
status.steps--max_steps(the most steps a stack holds),active(the Steps view is on),display("stack"or"layout"),look,current,clock(the playlist on screen's step,-1none),steps(name,source:"current","api","found by name"or the playlist file,rows,clock,on_screen,track:0A,1B,-1none),line(the row on screen) andline_mark(its mark,""none:"Line 3: sh0030 / sh0040 differ","Line 7: missing in Anim"),marks(lines,differing,missing,listed: the first 50 marked lines with each step's token),b(the step on track B, itspath, the mediaaskedof B,ready: its first picture is there,missing: the step on screen has no row on this line, the playlist on screen stays),b_next(in Stack with 2 steps or more, what B will hold on the next line, preloaded before the clip changes: itspath,""none, and the framesaskedof it so far; an image sequence only, a film is loaded by B when the clip changes),sound(a,b: whose sound plays),send(toast: what the last send says while it shows,""none;undo_armed: Ctrl+Z would undo it;last:ok,step,replaced,filled,reason;menu_drawn: a row menu drew "Send to step" lately,menu_boxits entry,nullotherwise;items: its submenu's entries while it is open, eachwordsandbox),loading,refusal(the last thing refused,""none) andbar(the bar of steps over the picture while it is drawn:buttons,plus,grid,menu_open,popup;nullotherwise),panes(in the grid, each pane as drawn:step,row,wantedanddrawn-- the frame it should show and the one it shows --,state:"exact","held","loading"or"none",box) andgate(waiting,waits,longest_wait_ms: the lockstep's waits),grade_table(the panes' pictures graded through a table of colours, each colour graded once:tableskept and thebytesthey hold,max_table_bytesa table holds at most,table_picturesgraded through a table,exact_picturesgraded pixel by pixel because a table was full). A step change is timed like a version step:track_b.last_version_step(direction"step").
{"cmd": "steps", "action": "build", "steps": [{"current": true, "name": "Comp"}, {"playlist": "D:/review/anim.bpl"}, {"rows": ["D:/SQ010/SH010_lay_v003.mov", "D:/SQ010/SH020_lay_v002.mov"], "name": "Layout"}]}{"status": "ok", "cmd": "steps", "active": true, "display": "stack", "look": "stack", "current": 0, "clock": 0, "steps": [{"name": "Comp", "source": "current", "rows": 2, "clock": true, "on_screen": true, "track": 0}, {"name": "Layout", "source": "api", "rows": 2, "clock": false, "on_screen": false, "track": 1}], "line": 0, "line_mark": "", "marks": {"lines": 2, "differing": 0, "missing": 0, "listed": []}, "loading": 1, "refusal": ""}- Actions:
Annotation Tool
Set Active Tool : Selects the drawing tool used for paint-over annotations.
- Parameter:
tool(string). Accepted:"pen"/"brush","text","arrow","rect"/"rectangle"/"shape","laser","eraser","pipette"/"eyedropper", and"select"/"move": Select, the Select / cursor tool with which a click takes a stroke of the frame (in Dark and Light the paint bar stays for it, its box saying Select). Any other value deselects the tool (the Select / cursor tool, without the bar).
{"cmd": "set-tool", "tool": "arrow"}- Parameter:
Notes Export (Production Tracking)
Export Notes : Writes the current playlist's review notes and annotated frames to
output_dirasexport.jsonandexport.html-- seenotes_payload.schema.jsonfor the JSON shape. Runs silently (no folder-picker dialog, no auto-opened browser tab, no blocking "done" dialog) so it's safe to call from an unattended pipeline script.{"cmd": "export-notes", "output_dir": "/tmp/review_exports/SQ010_pl01"}Replies immediately once the export is queued (the actual file write happens on a background thread) -- poll
export.json's modification time if you need to know when it's actually finished, or add a short delay before reading it back. A shot whose note is on a passage (written while in and out points were set) carriesframe_inandframe_out(0-based indexes, as a frame'sframe) anddisplay_frame_in/display_frame_out(the numbers the player shows, asdisplay_frame); a note on the whole shot has none of them. The CSV gives the passage's timecodes in the note'sTimecodecell, the HTML and the PDF write "1012-1040 · " before the note. Each annotated frame carriesframe(the 0-based index, the annotation's key) anddisplay_frame(the number the player shows for it); its picture is<shot>_frame_<display_frame>.jpgand the page saysFrame <display_frame>. A frame also carries, when it has them,authors(who drew its strokes, each once in the order they drew:[{"name": "Alex", "rgb": "#4c99e6"}],rgboptional, as a stroke'sauthor) andhold(its longest hold, 2 to 999 frames), andproposed(truewhile a stroke of the frame is still proposed: drawn in a session by a seat that neither drove nor hosted, neither kept nor removed); the page adds them after the frame number (Frame 1004 - Alex, Sam - held 24 frames - proposed by Sam). A reader that does not know them ignores them. The images attached to notes are copied first, then the pictures, one shot after the other (--review-dirat the close writes several shots at once). The picture carries the frame's strokes, laid with the exports' rasteriser (the onescreenshotuses with"annotations": true), flipped with the picture; older versions gave the bare graded frame, with the strokes only inexport.html's SVG overlay.Export Notes to a File : The review notes as ONE file for people, at
path(absolute, ending in.pdf,.csvor.html), with no dialog:pdf(A4 landscape, a page per shot -- a page more when its notes or pictures do not fit -- with its name, status, notes, note-takers' notes, attached pictures and drawn frames; JPEG and PNG pictures inside, a JPEG as it is; no font embedded: Helvetica for the Latin text and the PDF reader's Japanese, Chinese and Korean fonts for the rest, a character none of them writes, an emoji, shown?),csv(UTF-8 with a BOM, CRLF lines, RFC 4180: one line per note, per note-taker's note and per drawn frame; columns Shot, Version, Status, Author, Note, Timecode) orhtml(one self-contained page, every picture inside as adata:URI, no script and no address of anything elsewhere). All three are read from the sameexport.jsonasexport-notes(which is unchanged).trackis optional (0).{"cmd": "export-notes-file", "format": "csv", "path": "D:/reviews/Review_Playlist_1_2026-10-16.csv"}Replies at once (
{"status": "ok", "path", "format"}) once the export is queued, or{"status": "error", "reason"}for a format other thanpdf/csv/html, a relative path or another extension, atrackother than 0 or 1, or an empty playlist.notes-file-exportedsays when the file is written (it is written beside its name, then renamed onto it: never half a file).Screenshot : Captures a single frame of the active track-A playlist item as a PNG, graded exactly as currently shown on screen (live exposure/gamma/etc., not whatever was last saved). Synchronous -- the file exists on disk by the time the reply arrives.
{"cmd": "screenshot", "output": "/tmp/review_exports/SQ010_SH0010_f1042.png", "frame": 1042}frameis optional (defaults to the current playhead).annotationsis optional (defaultfalse):truelays track A's paint-overs of that frame on the picture as an export with flattened annotations lays them -- the viewer's rasteriser, the same shape, feather and whole-stroke opacity, the strokes flipped with the picture -- and the reply says how many with"annotations": N.trackis optional (defaults to0) -- track B is not yet supported, since unlike track A it isn't backed by aPlaylistItemat all (no grading/flip state to read). Replies{"status":"error","reason":"no active playlist item"}if nothing is loaded via the playlist, or"failed to decode frame"if the requested frame is out of range or the source is unreadable.Paint sizes. A stroke's
sizein a.bpl, inexport.jsonand in the session messages keeps its unit: pixels of an 800-pixel picture (its long side), so it already scales with the plate. Nothing is added to these files and the session protocol (LAN, online, the web page) does not change. What changes: the paint bar shows, drags, wheels and types the size in px at 1080p of the plate on screen (a value v is v x plate height / 1080 pixels of the picture) and converts it to the stored unit for the plate's aspect, so the same value has the same weight on a 1K, 2K or 4K plate; the default size is 8 px at 1080p (it was 20 stored, 48 px at 1080p); an arrow head is 2.5 thicknesses, capped at 60 px at 1080p and never under 1.5 thicknesses; the laser's size is in px at 1080p, sent in the laser message as pixels of the plate, the unit that message has always carried.Frame, not window.
screenshotdecodes the frame from its source and grades it on the CPU, as an export does: it is not a picture of the window, and it does not use the Dear ImGui Test Engine. The player does not contain the Test Engine (a test tool under its own license, compiled into the test suite only;THIRD_PARTY_LICENSES.md), and no IPC command depended on it:screenshot,dump-thumbnails,contact-sheetanddebug-mousework in every build. No IPC command writes a picture of the player's window or its interface (the review stream,stream-enable, carries the picture only, drawn offscreen, to its companion); a script that needs a picture of it captures the window from outside (PrintWindowon Windows,importorxwdon Linux).
Production Trackers (Kitsu, ftrack, Flow)
The commands of File > Tracker... (user guide 6.6). Each replies at once, "queued": true when a call to the
tracker was started, and the result comes afterwards as an event. Every call runs on the tracker thread; one at a
time, in order. No secret travels through the IPC: a request that carries a password, api_key, key,
token, secret or client_secret is refused. A site's secret comes from the credential store (the window's
Remember) or from BERNARD_TRACKER_<ID>_SECRET. No reply, event or status field holds a secret, a token or a
signed URL.
List the sites of
trackers.json:has_secretis whether the player knows a secret for it (the environment, this run's memory, or the store),nulluntil it looked.{"cmd": "tracker-sites"}Reply:
{"status": "ok", "cmd": "tracker-sites", "sites": [{"id": "studio-kitsu", "kind": "kitsu", "base_url": "https://kitsu.studio.example", "user": "artist-a@example.test", "has_secret": true}]}.Connect to a site; event
{"event": "tracker-connected", "ok": true, "site": "studio-kitsu", "user": "Artist A", "reason": ""}(on a refusalokis false andreasonsays"<code>: <message>",auth_failed: ...). A site whose address ishttp://to another machine is refused at once, before any request:{"status": "error", "reason": "site 'studio-kitsu': http:// would send the password or key in clear: use https:// ..."}.http://is accepted only forlocalhost,127.0.0.1and::1.{"cmd": "tracker-connect", "site": "studio-kitsu"}Disconnect;
forgetalso removes the site and its kept secret. Asiteother than the one connected is only forgotten.{"cmd": "tracker-disconnect", "site": "studio-kitsu", "forget": false}List the projects, then a project's playlists; events
{"event": "tracker-projects", "ok": true, "projects": [{"id", "name"}], "reason": ""}and{"event": "tracker-playlists", "ok": true, "playlists": [{"id", "name", "updated_at"}], "reason": ""}.{"cmd": "tracker-projects"} {"cmd": "tracker-playlists", "project_id": "a1b2"}Load a playlist, in place of the playlist (
replace, the default) or after it (append); event{"event": "tracker-loaded", "ok": true, "items": 12, "missing_media": 3, "reason": ""}.missing_mediacounts the rows without a path (their media is the tracker's, or none): they keep their ids, stay publishable, and are never opened, exported or sent to a session's guests. Nothing is downloaded.{"cmd": "tracker-load-playlist", "playlist_id": "c3d4", "mode": "replace"}Publish the notes of the rows loaded from the site connected that have a note or a stroke:
items(playlist indices; default every one of them),status(also set each clip's status through the site'sstatus_map; defaultfalse),dry_run(only describe the requests; only reads are sent). Reply{"status": "ok", "cmd": "tracker-publish", "queued": true, "notes": 2, "dry_run": false}, or an error when nothing can be published. While the profile has no reviewer name of its own (none, or blanks only: spaces, no-break or wide spaces), it is refused,{"status": "error", "reason": "name_needed: ..."}, and nothing is sent: a note never goes under the system's login. Give the name in Edit > Preferences (Name), or answer the question a click on Publish asks; adry_runis still allowed. Event:{"event": "tracker-publish-done", "dry_run": false, "published": 1, "skipped": 1, "failed": [{"index": 4, "reason": "forbidden: ..."}], "statuses": 0, "statuses_refused": []}, and withdry_run,requests([{"index", "requests": ["POST /api/...", ...]}], masked). A row whose note, unchanged, was published already isskipped: the fingerprint of what was sent is kept inx_tracking.published, and a note is never sent twice. Withstatus, such a row whose status changed since it was published sends its status alone, without the note or its images (counted inpublished, and instatuses, the number of statuses sent alone: 0 in adry_run, which shows that one request). A row published by an earlier version has no status recorded: the first run withstatussends nothing for it (skipped) and keeps its status as the reference; a later change goes alone. On ftrack,networkortimeouton the last step means the result is unknown. The statuses the site's versions accept are read once when the site is tested (tracker-connect, Save & test, Test again); a status the site does not have is never sent: the note still goes, andstatuses_refused([{"index", "reason"}]) names the row with a plain reason (Flow has no status for "Not approved". Pick one in File > Tracker > Status map, or create it in Flow (Admin > Status List).); a status sent alone that the site does not have is also infailed(status_not_on_site: ...). The Console asks before it.{"cmd": "tracker-publish", "items": [0, 2], "status": false, "dry_run": true}Download a row's preview (Kitsu, ftrack, Flow: a Version's uploaded movie, read again for a fresh signed URL that is never logged or returned) into
<profile>/tracker-media/<site>/; it becomes the row's media. Event{"event": "tracker-download-done", "index": 3, "ok": true, "path": "..."}or{"index": 3, "ok": false, "reason": "..."}. Refused over 2 GB. The Console asks before it.{"cmd": "tracker-download-media", "index": 3}The answer to the question asked after a load (
status.tracker.download_question):"all": true(Download All) queues every row of the site connected whose preview is not on this machine and may be downloaded; they are fetched in the playlist's order, one at a time, only while nothing plays, scrubs or seeks and no review session runs, each row's media as soon as it is here (onetracker-download-doneeach)."later": true(Later) downloads nothing. Either one closes the question; reply{"status": "ok", "cmd": "tracker-download-media", "queued": 3}.{"cmd": "tracker-download-media", "all": true}Cancel the call in progress (it stops before its next request) and the ones queued (each answered
cancelled).{"cmd": "tracker-cancel"}
Frame & Movie Export
Export : Writes frames or a movie of a clip (default: the clip on screen) -- or of every clip in the playlist -- in the formats the industry delivers in. Sequences: EXR (half/float, zip/piz/dwaa...), DPX (10/12/16-bit), TIFF (8/16/32-bit), JPG (4:4:4 by default), PNG, NOVA
.nvfx(below). Movies: ProRes 422 Proxy/LT/422/HQ/4444 and DNxHR LB/SQ/HQ/HQX/444 in MOV, H.264 and H.265 in MP4 (or MOV) in 4:2:0 / 4:2:2 / 4:4:4, MJPEG, with PCM 24-bit or AAC audio. Optional burn-in (clip, version, frame, timecode, status...) and flattened review annotations. Asynchronous: the reply says the job is queued,export-statusfollows it, anexport-doneevent ends it. One job (or one playlist batch) at a time.{"cmd": "export", "preset": "dailies_prores_lt", "output": "/renders/SQ010_SH0010_v003.mov"}{"cmd": "export", "source": "/plates/SQ010_SH0010.1001.exr", "output": "/out/SQ010_SH0010.####.dpx", "format": "dpx", "bit_depth": 10, "in": 0, "out": 119, "color": "custom", "cc": "exposure=0.5,saturation=1.1", "input_space": "acescg", "display_space": "rec709"}{"cmd": "export", "playlist": true, "preset": "dailies_prores_lt", "output": "/dailies/<clip>.mov", "burn_in": {"top_left": "{clip} {version}", "bottom_left": "{frame} {timecode}", "bottom_center": "{status}", "bottom_right": "{resolution} {fps} fps"}, "annotations": true, "frame_offset": 1001}{"cmd": "export", "source": "/raw/A005_C001.R3D", "output": "/stills/<clip>/", "format": "jpg", "chroma": "444", "resize_w": 1920, "hardware": true, "smart_tier": true, "color": "viewer"}- Parameters (every one optional but
output; apresetis applied first and the others lay over it):source(a video/RAW path, one frame of an image sequence, or a Flash.swf-- a vector one is rendered through Ruffle's exporter first, see the user guide; default the clip on screen, whose in/out points become the default range),sources(more clips: a batch,<clip>naming each output),playlist(true: one job per playlist item, with its saved grade, in/out points, strokes and status;<clip>required in a movie name or a pattern, or a folder for sequences),in/out(0-based frames, inclusive),step,proxy_scale(decode tier),smart_tier(defaulttrue: when resizing down, decode at the 1/2, 1/4 or 1/8 tier that still covers the output),hardware(defaulttrue: RED OpenCL debayer and hardware video decode, CPU fallback),resize_w/resize_h(0 keeps the size / the aspect),format(exr|dpx|tiff|jpg|png|nvfx|mov|mp4; when absent, the output's extension names it;nvfxis the player's NOVA review format, see NOVA below),output(a movie file, or for a sequence a####pattern or a folder that gets<clip>.####.<ext>;<clip>and<version>are replaced by the clip's,<recipient>by the command'srecipient-- below),start_number(the first file's number; absent in the player, the number it shows for the first frame exported: an export from the in point ofsh010.1001.exrat index 41 writes.1042; 1001 from the command line. The next files follow the frames exported,stepincluded: from 41 by 2,.1042,.1044,.1046, the numbers{frame}prints. A preset saved by an older version holds1001, the old default: read as absent),padding,compression(EXRnone|rle|zip|zips|piz|pxr24|b44|b44a|dwaa|dwab, TIFFnone|lzw|zip, NVFXhtj2k|wavelet|lossless|raw),all_layers(NVFX, defaulttrue: every layer of a multi-part EXR source becomes an AOV of the frame),temporal_gop(NVFX htj2k: 0 = intra only, N = one intra frame then N-1 P-frames predicted from the previous frame, reprojected through camera and depth when the render has them),bc6h(NVFX: also write the main pass as BC6H GPU blocks, 1 byte / pixel in VRAM),noise_floor(NVFX htj2k / wavelet: -1 or"auto"measures the frame's grain and spends no bits under it, the default; 0 or"off"keeps 0.6 % of every value; a number is the one-sigma noise in linear units),noise_floor_fade(NVFX htj2k:"lo,hi"in grain sigmas, default"0,0.5": the floor fades out near zero so the darks get no dead zone;"off"to write the floor as before; such frames carry version-2 HTJ2K chunks, see NOVA),bit_depth,quality(JPG / MJPEG / NVFX htj2k and wavelet, 92 = visually lossless),chroma(444|422|420; JPG defaults to 4:4:4, H.264/H.265 to 4:2:0, MJPEG to 4:2:2; ProRes and DNxHR are fixed by their profile),codec(prores_proxy|prores_lt|prores_422|prores_hq|prores_4444|dnxhr_lb|dnxhr_sq|dnxhr_hq|dnxhr_hqx|dnxhr_444|h264|h265|mjpeg; ProRes and DNxHR need MOV; H.264 and H.265 are written by the first encoder that opens on the machine among NVIDIA NVENC, Intel Quick Sync, AMD AMF and Windows Media Foundation on Windows, NVIDIA NVENC, VA-API and Intel Quick Sync on Linux -- the player's FFmpeg is the LGPL build, without x264 / x265, and no software H.264 / HEVC encoder is used -- and a job none of them can write fails with a message in words naming what was tried, see the user guide),crf/bitrate_kbps(H.264 / H.265; the CRF is mapped onto each encoder's constant-quality scale),audio(pcm|aac|none),fps(0 = the source's),layer(EXR AOV name),burn_in(truefor the player's Info Bar layout, or an object of slots -- below),burn_in_spec(the same object, alongsideburn_in: true),annotations(true: flatten the frame's review strokes -- brush, text, arrows, shapes -- into the pixels),watermark(true, or an object: text over every frame of a review export, see Watermark below),frame_offset(added to the source frame index for{frame}/{timecode}; absent in the player: the source shot's first number, so they print the numbers the player shows; given, 0 included, it is kept -- 0 prints the index; 0 from the command line. A preset saved by an older version holds0: read as absent),colorandcc,input_space,display_space(below). - Burn-in: the object's keys are the six slots
top_left,top_center,top_right,bottom_left,bottom_center,bottom_right(aliasestl tc tr bl bc br), each a template of tokens:{clip} {version} {frame} {source_frame} {file_number} {timecode} {fps} {resolution} {width} {height} {status} {notes} {date} {time} {user} {input_space} {display_space} {layer} {range} {duration} {path} {codec}; plusscale(text size factor, 1 = frame height / 40) andbackground(dark band). An object is the whole layout; unknown tokens stay visible on the frame.{status}is preceded by its drawn icon (a tick for Approved, a cross for Not Approved, a clock for Pending). - Color:
colorissource(pixels as decoded, no transform -- what a pipeline wants for plates),viewer(the grade of the clip on screen exactly as shown),clip(the grade saved with the playlist item) orcustom(theccvalues of the job).ccis a"key=value,..."string or an object:exposure gamma contrast brightness saturation hue r g b in_black in_white in_gamma out_black out_white temp tint offset_r offset_g offset_b, andstages_off, the stages turned off (Set Stage): their keys joined by+in the string ("stages_off=levels+gamma"), a list in the object ("stages_off": ["levels", "gamma"]); and the curvescurve_master,curve_r,curve_g,curve_b(Set Curve, ACEScct):x:ypoints joined by;in the string ("curve_r=0:0;0.5:0.6;1:1",nonefor the straight curve), a list of[x, y]pairs in the object; and the hue rangesrange_<reds|yellows|greens|cyans|blues|magentas>_<hue|saturation|lightness>andlightness(Set Hue Range), numbers in both forms ("range_greens_hue=12,lightness=-0.1"), each clamped to its bounds.input_spaceanddisplay_spacename the color spaces of the grade (acescg,srgb,ap0,adobergb,prophoto,linear srgb,logc3,logc4;rec709,p3,rec2020,displayp3,aces1,aces2, or the full names of the combos) and override the grade's own; the channel isolation is never exported. - Watermark:
"watermark": {"enabled": true, "text": "{recipient} · {date} · CONFIDENTIAL", "position": "bottom_right", "opacity": 55, "size": 3}--truealone is that default; an object switches it on unless it says"enabled": false.texttakes the tokens{recipient} {date} {time} {clip} {version} {frame} {timecode} {session} {studio}({frame}and{timecode}: each frame's own, numbered as the player shows it; an unknown token stays on the frame as written);positionis one of the nine cellstop_left top_center top_right middle_left center middle_right bottom_left bottom_center bottom_right(aliasestl tc tr ml c mr bl bc br,centre) ortiled(the text repeated over the whole frame);opacityis a percentage, 10 to 100;sizeis the text's height in percent of the frame's, 1 to 20. It is drawn after the color and the resize and before the encoder, where the burn-in is, in the burn-in's font, white at that opacity with a light shadow. Review formats only: MOV, MP4 and JPEG. A job that asks for a watermark on EXR, DPX, TIFF, PNG or NVFX is refused whoever sends it ("watermark refused: Watermarks are for review formats; plates stay clean (MOV, MP4 and JPEG take one)"): a plate is never marked.dateandtimeare the job's start (YYYY-MM-DD, HH:MM),clipandversionthe clip's,sessionthe review session's name or the playlist's. The command's ownrecipient,studioandsessionfields fill their tokens and a<recipient>ofoutput; they are not part of the job's JSON, never written into a preset, and no reply,export-statusor log line repeats them: a path there shows<recipient>where the name is. A{recipient}or a<recipient>without arecipientis refused. - Watermark:
"watermark": {"enabled": true, "text": "{recipient} · {date} · CONFIDENTIAL", "position": "bottom_right", "opacity": 55, "size": 3}--truealone is that default; an object switches it on unless it says"enabled": false.texttakes the tokens{recipient} {date} {time} {clip} {version} {frame} {timecode} {session} {studio}({frame}and{timecode}: each frame's own, numbered as the player shows it; an unknown token stays on the frame as written);positionis one of the nine cellstop_left top_center top_right middle_left center middle_right bottom_left bottom_center bottom_right(aliasestl tc tr ml c mr bl bc br,centre) ortiled(the text repeated over the whole frame);opacityis a percentage, 10 to 100;sizeis the text's height in percent of the frame's, 1 to 20. It is drawn after the color and the resize and before the encoder, where the burn-in is, in the burn-in's font, white at that opacity with a light shadow. Review formats only: MOV, MP4 and JPEG. A job that asks for a watermark on EXR, DPX, TIFF, PNG or NVFX is refused whoever sends it ("watermark refused: Watermarks are for review formats; plates stay clean (MOV, MP4 and JPEG take one)"): a plate is never marked.dateandtimeare the job's start (YYYY-MM-DD, HH:MM),clipandversionthe clip's,sessionthe review session's name or the playlist's. The command's ownrecipient,studioandsessionfields fill their tokens and a<recipient>ofoutput; they are not part of the job's JSON, never written into a preset, and no reply,export-statusor log line repeats them: a path there shows<recipient>where the name is. A{recipient}or a<recipient>without arecipientis refused. - Replies
{"status":"ok","cmd":"export","source":...,"jobs":N,"output":...}once queued, or{"status":"error","reason":...}(unknown preset, bad field, no clip on screen, a chroma the codec's profile cannot write, an export already running).
- Parameters (every one optional but
Export Status : The running or last job.
{"cmd": "export-status"}Reply:
state(idle|running|done|cancelled|failed),frames_done,frames_total,jobs_done,jobs_total(a playlist batch; 1 for one job),source,output,format, and once finishedok,error,warnings,frames_written,width,height,seconds,outputs,encoder(a movie's libavcodec encoder:h264_nvenc,hevc_amf,h264_mf,h264_vaapi,prores_ks,dnxhd...;""for a sequence or a job that did not start),encoder_options(the optional settings it opened with,"name=value": NVENC'spreset=p7,tune=hq,spatial-aq=1,temporal-aq=1,rc-lookahead=32,multipass=fullres,b_ref_mode=middle, less any the GPU refused; empty for the other encoders); for a batch alsojobs, one{source, ok, error, frames_written, outputs}per clip.queue: the Export window's queue, in its order, one{id, name, preset, state, source, output, frames_done, frames_total, error, written, seconds, encoder}per task,statebeingqueued,running,done,failedorcancelled; the fields above follow whichever job the export thread runs, a queue task's or anexport's.Export Cancel : Stops the running export after the current frame (and skips the clips left in a batch); the files already written stay. A task of the Export window's queue that is running is canceled the same way, and the queue goes on with the next one (Remove them in the queue's window).
{"cmd": "export-cancel"}Export Presets : The preset names
exportaccepts:builtin(shipped:plates_exr_half_zip,plates_exr_float_piz,dpx_10bit,tiff_16bit_viewer,jpg_review,dailies_prores_lt,master_prores_hq,master_prores_4444,master_dnxhr_444,dailies_dnxhr_sq,client_h264,client_h265, andnvfx_review,nvfx_losslessexcept in a player built without NOVA) anduser(one JSON job per file inpresets_dir, the file stem is the name; the Export dialog's "Save as preset" writes them).{"cmd": "export-presets"}Export Recipe: an export recipe is several outputs of one set of clips -- dailies, a client's H.264, the plates -- described once and run on any playlist; the Export tab of the bottom panel makes and saves them, one JSON file each in the profile's
export_recipes/folder, described byexport_recipe.schema.json. A recipe names no media and no clip: itssourceisplaylistorclip(the clip on screen), and each output's path names every clip by<clip>. It never holds a watermark's recipient.{"cmd": "export-recipe", "action": "list"}{"cmd": "export-recipe", "action": "run", "name": "Evening dailies", "recipient": "Demo Client"}{"cmd": "export-recipe", "action": "run", "clips": [0, 2], "recipe": {"bernard_export_recipe": 1, "name": "Client and plates", "source": "playlist", "start_from": "grade_as_shown", "outputs": [{"name": "Client · H.264", "preset": "client_h264", "overrides": {"resize_w": 1280}, "watermark": {"text": "{recipient} · {date} · CONFIDENTIAL", "position": "bottom_right", "opacity": 55, "size": 3}, "output": "D:/exports/client/<clip>_<recipient>.mp4"}, {"preset": "plates_exr_half_zip", "output": "D:/exports/plates/<clip>/"}]}, "recipient": "Demo Client"}list: repliesrecipes(eachname,file,source,start_from,outputs, the number of outputs) andrecipes_dir. A file that does not read as a recipe is left out and the log says why.run: the recipe once --name(a saved recipe),file(a recipe file anywhere) orrecipe(the object inline) --;clips(playlist indices, 0-based, in the order to export; default every clip; acliprecipe takes the clip on screen);recipient(who receives the export: required when an output's watermark prints{recipient}or its path says<recipient>; used for this run only, never saved, never repeated in the reply,export-statusor the log);sessionandstudio(over the recipe's ownstudio) for their tokens. Each (clip, output) becomes one job of theexportcommand's kind, clip by clip, at the end of the Export window's queue, which runs them one at a time:export-status'squeuefollows them, and each job'sexport-doneevent ends it. The clip's own in and out points, its grade (Start fromgrade_as_shown: the saved grade, the live one for the clip on screen;source: none), its strokes and its status come from the playlist, as for"playlist": true. The decoding of a clip is not shared between its outputs: each job reads the clip again.- A recipe file:
bernard_export_recipe(the format's version, 1; a later one is refused),name,source,start_from(grade_as_shownorsource),studio, andoutputs, each withname(the card's title),preset(a built-in or studio preset; it must exist where the recipe runs),overrides(theexportfields that differ from the preset's:format,codec,resize_w,display_space,burn_in...; neversource,sources,in,out,outputorwatermark;color,ccandinput_spaceare ignored: Start from and each clip decide them),watermark(above) andoutput(a movie with<clip>, a pattern with<clip>or a folder;<version>and<recipient>too). An unknown key is refused, so a clip's name cannot slip in. - Replies
{"status":"ok","cmd":"export-recipe","action":"run","recipe":...,"clips":N,"outputs":M,"jobs":K}once queued, or{"status":"error","reason":...}: no such recipe, a recipe that does not read, no clip, an output without<clip>, two outputs writing the same files, a watermark on a plate, the recipient missing.
- Headless : the same job runs without a window from the command line, for a render farm or a pipeline hook:
BernardReview --export job.json, or flags laid over a preset:BernardReview --export --preset dailies_prores_lt --source shot.mov --output shot_dailies.mov --cc "exposure=0.5" --display-space rec709 --burn-in "tl={clip} {version};bl={frame} {timecode}". Several--sources (or bare paths) make a batch with<clip>in the output. Flags:--format --codec --chroma --crf --bitrate --audio --fps --color --cc --input-space --display-space --layer --compression --temporal-gop N --bc6h --bit-depth --quality --start-number --padding --proxy-scale --resize WxH --in --out --step --burn-in [layout] --no-burn-in --annotations --no-annotations --frame-offset N --hardware --no-hardware --smart-tier --no-smart-tier --watermark --no-watermark --watermark-text T --watermark-position P --watermark-opacity N --watermark-size N --recipient NAME --studio NAME(the watermark's flags switch it on;--recipientand--studiofill its tokens and<recipient>, and the summary shows<recipient>in their place; a plate refuses the watermark here too;-ois short for--output; the--no-flags and their positive forms lay over what a preset or a job file set). Printsexport: frame N/Mlines and a JSON summary on the last line (withdecode_tier,encoder,encoder_options, andjobsfor a batch); exit code 0 on success, 1 for a bad command line, 2 when the export failed.viewerandclipcolor have no meaning without a window and becomecustom; annotations come from the player's session, not from the command line. Example job files:export_examples/. A recipe:BernardReview --export --recipe evening_dailies.json --source sh010_comp_v003.mov --source sh020_comp_v007.mov --recipient "Demo Client"runs each of its outputs for each--source(or bare path), clip by clip, one after the other; it takes--recipe,--source,--recipient,--studioand--sessiononly (a recipe carries the rest), turnsgrade_as_shownintocustomcolor as above, printsexport: <source> -> <output>anddone/FAILEDper job and a JSON summary on the last line (recipe,ok,clips,outputs,files,seconds, andjobs, one{source, output, preset, ok, error, frames_written, encoder, outputs}each,<recipient>in place of the name); exit code 0 when every job succeeded, 1 for a bad command line or recipe, 2 when a job failed.
Production-Tracking Metadata (x_tracking)
Every playlist item can now carry an optional x_tracking block -- the extension described in Section 8 is implemented. Fields: shot_id, shot_name, version_id, shotgrid_version_id (int), ftrack_id, artist, task. Set it when building the .bpl you pass to load-playlist (see playlist.schema.json); it's saved back out with the playlist (.bpl save/autosave) and round-tripped into export-notes' output per shot (see notes_payload.schema.json) -- so a connector script can route a note back to the right ShotGrid Version / ftrack entity directly by ID, with no filename matching needed.
OCIO context variables (x_tracking.ocio_context, optional, OCIO step 3): an object of strings, the variables a studio's OCIO config reads for this clip ($SHOT, $SEQ, ... in a look's file name, for example a per-shot CDL through a look whose FileTransform is ${SEQ}/${SHOT}.cc), e.g. {"SEQ": "sq010", "SHOT": "sh010"}. SHOT is otherwise x_tracking.shot_name (else shot_id) and TASK is x_tracking.task; a variable of ocio_context wins over those two. A name is [A-Za-z_][A-Za-z0-9_]* (64 bytes at most); a value is one path component: not empty, 128 bytes at most, without /, \, :, $ or a control character, and not . or ..; 16 variables at most. A bad entry is skipped and logged. A variable the clip does not give takes the config's own default (its environment: section). The process environment is never read, and nothing is guessed from file names. The key is kept through the .bpl, the autosave and export.json like any other x_tracking key.
Bounds over the IPC (playlist-add, build-matrix, play-version, media-ready):
x_tracking, a pipeline's own keys included, holds at most 64 KiB written as JSON and nests at most 8 levels (the block itself is level 1).- A
version_idor anotheridholds at most 256 bytes. - A name, a
pathor anerrortext holds at most 4 KiB.
Past a bound, the command answers {"status": "error", "cmd": ..., "reason": ...}: the reason names the field and its
size. Nothing of the command is applied. earlier-notes has bounds of its own, in its entry (Diagnostics).
A key of the block that is none of those fields (nor a tracker's below) is a pipeline's own (Bernard Engine puts {"bernard": {"file_id": ..., "version": ...}} there): it is kept as it came, any JSON value, through the .bpl save, the autosave and export-notes' export.json, after the fields above (older versions dropped such a key when the playlist was loaded).
An item loaded from a tracker playlist also carries tracker_kind (kitsu, ftrack, flow), tracker_site (the site's host), task_id, playlist_id, media_ref (the media's URL path on the site, for Flow the key-free /api/v1.1/entity/versions/<id>/uploaded_movie/<attachment id>, or the studio path the tracker gave) and, once a note was published from it, published ({note_id, hash, at}: the tracker's note id, the fingerprint of what was sent, so that an unchanged note is not published twice, and when, in UTC). They are saved in the .bpl only when set, so a playlist from before them loads as it always did, and export-notes writes each of them when set. A row loaded from a tracker without a path (its media not on this machine) is kept by the .bpl with its ids, its name and version from shot_name and version_id. The preview's size is not saved: unknown once the .bpl is reopened.
A row of the site connected whose status was picked in the status menu, when it is not what one of the three review keys sends, also carries studio_status (the status's id on the site) and studio_status_of (the player's status it gave the row: another status given since makes it void). It is what a publication with status sends for that row; .bpl and autosave keep it; export.json does not.
Color & Grading
Set Exposure : Exposure offset in stops, applied to track A -- or to track B.
- Parameter:
value(float;0.0is neutral,+1.0is one stop brighter). - Parameter:
track(optional:"A"or0, the default;"B"or1for track B). Track B's grade reaches B's viewport picture and B's timeline thumbnails, and nothing of track A's; the reply is{"status":"error",...,"reason":"unknown track (A or B)"}for another value -- any other integer included, read at its full width (4294967296is not0), and1.0. B's exposure reads back astrack_b.exposureinstatus.
{"cmd": "set-exposure", "value": 1.5}{"cmd": "set-exposure", "value": 2.0, "track": "B"}- Parameter:
Set Gamma : Gamma applied after exposure.
- Parameter:
value(float;1.0is neutral). - Parameter:
track(optional), as forset-exposure; B's gamma reads back astrack_b.gamma.
{"cmd": "set-gamma", "value": 2.2}{"cmd": "set-gamma", "value": 1.8, "track": "B"}- Parameter:
Set White Balance : the white balance of track A -- or of track B -- the WB Temp and WB Tint of the contact sheet's Quick Color Grade.
- Parameters:
temp(float, -1 cool .. +1 warm) andtint(float, -1 magenta .. +1 green),0.0neutral; give one or both, the other is left as it is. Each is brought into [-1, 1]. - Parameter:
track(optional), as forset-exposure. - Reply:
{"cmd":"set-white-balance","status":"ok","track":"A","temp":0.35,"tint":-0.2,"offset":[0.0,0.0,0.0]}, the track's white balance and offset as they now are;{"status":"error",...,"reason":...}whentrackis unknown,temportintis not a number, or neither is given -- nothing is applied then. They read back ascolor.grade.white_balance_temp/white_balance_tintinstatus(track B:track_b.grade), and in a review session the pilot's reach every follower with the rest of its look.
{"cmd": "set-white-balance", "temp": 0.35, "tint": -0.2}{"cmd": "set-white-balance", "tint": 0.1, "track": "B"}- Parameters:
Set Offset : the offset (lift) of track A -- or of track B --, Quick Color Grade's Offset (Lift).
- Parameter:
value(three floats, the lift of R, G and B,[0, 0, 0]neutral), each brought into [-1, 1]. - Parameter:
track(optional), as forset-exposure. - Reply: as
set-white-balance's,"cmd":"set-offset"; an error whenvalueis not three numbers. Reads back ascolor.grade.offset(track B:track_b.grade.offset).
{"cmd": "set-offset", "value": [0.05, 0.0, -0.02]}{"cmd": "set-offset", "value": [-0.08, 0.04, 0.12], "track": "B"}- Parameter:
Set Stage : turns a stage of track A's grade -- or of track B's -- off or back on, as the Color panel's eye does. The stages, in the order of the render:
input_space,white_balance(white balance and offset),rgb_gain,exposure,levels,brightness_contrast,hue_saturation(the global hue and saturation and the hue ranges, Set Hue Range),curves(the master and R, G, B curves, Set Curve),gamma,display_lut(the display space and the view LUT). A stage off is skipped by the viewer and by every CPU render -- export, thumbnails, the pipette, Auto-Match -- alike; its values stay in the grade. The input space's stage off reads the file's values as linear ACEScg, the display's writes the working values as they are.- Parameters:
stage(one of the keys above),on(true/false),track(optional, as forset-exposure). - Reply:
{"cmd":"set-stage","status":"ok","track":"A","stages_off":["levels"]}, the track's stages off as they now are;{"status":"error",...,"reason":...}for an unknown track or stage, or anonthat is not a boolean -- nothing is applied then. They read back ascolor.grade.stages_off(track B:track_b.grade.stages_off), are saved with the grade (.bpl, the autosave, the session, track B) and reach a review session's followers with the rest of the look.
{"cmd": "set-stage", "stage": "levels", "on": false}{"cmd": "set-stage", "stage": "gamma", "on": true, "track": "B"}- Parameters:
Color Panel :
{"cmd": "color-panel"}-- the Color panel (the Inspector's Color & Image in Dark and Light; its functions in the Color menu in every mode): what it edits, its Before / after, the Levels' picker and Auto, the presets. Without a key it only reads.- Parameters (every one optional, checked together before anything is applied):
target("A","B"or"A+B": A+B writes every change on both tracks; B and A+B need a clip on track B),before_after(truecuts the whole grade on screen without losing it -- in Complete; Essential keeps its own Before / after),picker("black","grey","white"or"none": arms the Levels' picker, whose next click on the picture sets Input Black / Input White from the pixel's luminance, or the R, G, B gains that make it neutral),levels_auto(true: Input Black and White from the scopes' histogram of the picture on screen, the darkest and brightest 0.1 % to black and white),preset_save(a name: the grade shown saved as a preset in the user's folder,color_presetsnext tosession_config.json),preset_apply(a preset's name: its look laid on the target, the clip keeping its color spaces),curves_window(true/false: the Curves window, Color > Curves...),hue_ranges_window(true/false: the Hue Ranges window, Color > Hue Ranges...). A wrong value, an unknown preset, a target without a clip on B or an Auto that cannot run is refused with areason. - Reply: the
color_panelblock ofstatus(below), with"cmd":"color-panel","status":"ok".
{"cmd": "color-panel"} {"cmd": "color-panel", "target": "A+B", "before_after": true} {"cmd": "color-panel", "picker": "grey", "levels_auto": true, "preset_save": "Warm dailies", "preset_apply": "Warm dailies"} {"cmd": "color-panel", "curves_window": true} {"cmd": "color-panel", "hue_ranges_window": true}- Parameters (every one optional, checked together before anything is applied):
Set Curve : sets or resets one curve of track A's grade -- or of track B's. A grade has four curves: the master (
master, the editor's RGB tab) andr,g,b, applied master first, then the channel's curve (out = Channel(Master(in))), after hue / saturation and before the gamma, in the viewer and in every CPU render alike. The curves work and are drawn on the ACEScct axis: the stage takes the linear ACEScg values to ACEScct (ACES S-2016-001: mid gray 0.18 at 0.4135, 1.0 at 0.5548), applies the curve, and comes back to linear, not clamped (a half's 65504 is ACEScct 1.4680, inside the stage's table, which reaches ACEScct 1.5). A curve is 2 to 16 points[x, y], both in 0..1 on that axis, sorted by x, from x = 0 to x = 1; monotone cubic (Fritsch-Carlson) between them, straight past the ends with the end's slope.- Parameters:
channel("master"-- or"rgb"--,"r","g","b"), thenpoints(the curve) orreset(true: the straight curve back),track(optional, as forset-exposure). - Reply:
{"cmd":"set-curve","status":"ok","track":"A","channel":"r","curves":{"master":[[0,0],[1,1]],"r":[...],"g":[...],"b":[...]}}, the track's four curves as they now are;{"status":"error",...,"reason":...}for an unknown track or channel, points that are not a curve (fewer than 2, more than 16, an end missing), orpointsandresettogether -- nothing is applied then. The curves read back ascolor.grade.curve_master,curve_r,curve_g,curve_b(track B:track_b.grade.*), each only when it is not the straight curve; they are saved with the grade (.bpl, the autosave, the session, track B, a preset), go with an export'scc, and reach a review session's followers (curve_master_a,curve_r_a,curve_g_a,curve_b_afor track A, the same ending in_bfor track B).
{"cmd": "set-curve", "channel": "master", "points": [[0, 0], [0.3, 0.27], [0.7, 0.74], [1, 1]]}{"cmd": "set-curve", "channel": "r", "reset": true, "track": "B"}- Parameters:
Set Hue Range : sets or resets one hue range of track A's grade -- or of track B's -- or its global lightness. A grade has six ranges,
reds,yellows,greens,cyans,blues,magentas, each with ahueshift (degrees, -180..180), asaturationgain on the chroma (0..2) and alightness(-1..1, towards black below 0, towards white above, dosed by the pixel's saturation: a gray never moves), blended softly from one range to the next; and a globallightness(every pixel, grays included). They apply right after the global hue / saturation and before the curves and the gamma, under the same eye (hue_saturation, Set Stage), in the viewer and in every CPU render alike, on linear ACEScg (the opponent chroma plane: values above 1 are neither clipped nor treated differently). The ranges' centers are the display's primaries and secondaries (sRGB / Rec.709) in ACEScg, at 4.325 (reds), 62.005 (yellows), 103.942 (greens), 184.325 (cyans), 242.005 (blues) and 283.942 (magentas) degrees on the ACEScg hue circle: a pure green of an sRGB clip is allgreensand nothing ofyellows. They stay on Rec.709 whatever the display space (see the user guide, 4.12).- Parameters:
range(one of the six,"global"-- itslightnessonly: the global hue and saturation are the grade'shueandsaturation--, or"all"withreset), then any ofhue,saturation,lightness(a value not given is kept) orreset(true: that range, the global lightness, orallof them back to neutral),track(optional, as forset-exposure). - Reply:
{"cmd":"set-hue-range","status":"ok","track":"A","range":"greens","hue_ranges":{"reds":{"hue":0,"saturation":1,"lightness":0},...,"lightness":0}}, every value of the track's ranges as they now are;{"status":"error",...,"reason":...}for an unknown track or range, a value that is not a number within its bounds, values andresettogether,allwithoutreset, orhue/saturationunderglobal-- nothing is applied then. The ranges read back ascolor.grade.range_<range>_<hue|saturation|lightness>(for examplerange_reds_hue) andcolor.grade.lightness(track B:track_b.grade.*), each only when it is not neutral; they are saved with the grade (.bpl, the autosave, the session, track B, a preset), go with an export'scc, and reach a review session's followers (range_reds_hue_a...lightness_afor track A,range_reds_hue_b...lightness_bfor track B).
{"cmd": "set-hue-range", "range": "greens", "hue": 12, "saturation": 1.2}{"cmd": "set-hue-range", "range": "all", "reset": true, "track": "B"}- Parameters:
Color Scopes :
{"cmd": "scopes"}-- the Scopes window (Color > Scopes, Window > Scopes) and the numbers of its last measure. The scopes read the picture the viewport shows -- after both grades and the display transform, before anything drawn over it (annotations, laser, HUD, info bar, watermark): the remote-review stream's capture point --, read back from the GPU at about 512 pixels on its long side (its aspect kept) and measured on a worker thread, 15 times a second at most, and only while a scope is shown: with the window closed, collapsed or a hidden tab, nothing is read back and nothing runs. A scope is a measure: it changes no grade, and nothing of it is saved with a clip, a playlist or a session.- Parameters (optional):
open(true/false) shows or hides the window;kindchooses its tab:"histogram","parade","waveform"or"vectorscope". Both are the workstation's preference, saved as the menus save them (session_config.json, keyscopes;{"open": false, "kind": "histogram"}when the key is absent). A wrong value is refused ("status":"error"with areason) and nothing is applied. - Reply: the
statusblock below, then the last results' numbers:samples(pixels measured),step,widthandheight(the picture measured: the readback's size),clip(black_pctandwhite_pct, the percentages of pixels clipped black -- every channel at code 0 -- and white -- a channel at code 255 --,blackandwhitetheir counts,black_channelandwhite_channelper R, G, B) andhistogram(bins, 256, andr,g,b,luma-- Rec.709 luma of the encoded values --, one count per bin; bin k is code k). The histogram is computed whatever tab is shown. Before a first measure, or with no picture on screen (an empty playlist, a clip loading), the neutral values:samples0,clip.black_pctandwhite_pct0,histogram.bins0 and empty lists. - A script that measures sends
{"cmd": "scopes", "open": true}, letsresultsmove on twice after the picture changed (a measure lags the picture by a frame or two), then reads the reply. statusreportsscopes:open,kind,visible(a scope drawn this frame),hz_max(15),captures(readbacks since the start),computesandresults(measures published, and taken for drawing),gl_resourcesandworker(the small framebuffer with its PBO, and the worker thread:falsewhenever no scope is shown),sourceandreadback([width, height]of the rectangle read and of the picture measured),last_compute_msandavg_compute_ms(the worker's pass),render_cost_us(the render thread's time in the readback's draw callback, moving average),latency_ms(readback issued to results published),tabs(the four tabs' boxes,[x0, y0, x1, y1]indebug-mouse's coordinates,nullwhile not drawn),plot(the scope's box),placeholder("No picture" drawn) andblack_label/white_label(the footer's figures, as drawn).- The value under the pointer: a click on a scope -- the Scopes window's or the
Color panel's -- freezes the measure under the pointer; a second click, Escape or another scope frees it. The
reply and
statusgive it as"probe",nullwhile nothing is frozen, else{"scope": "waveform" | "parade" | "vectorscope" | "histogram", ..., "label"}with, per scope,columnandlevel_pct(waveform: the picture's column, the level in the axis's % -- 0 on the graticule's 0 line, 100 on its 100 line);channel("R","G","B"),column,level_pct(parade);hue_deg(counter-clockwise from +Cb),sat_pct(% of the edge circle),cb,cr,near(a 75 % target's name when the click was in its box, elsenull) (vectorscope);level_pct,bin,pixels_pct(r,g,b,luma: the share of the samples in that bin, ornull) (histogram);labelis the words drawn, the same as the menu's Copy Value puts on the clipboard."probe_host"says whose measure it is:"window"(the Scopes window's, first) or"panel",nullfor none. statusalone addsreadoutandblock.readout.windowandreadout.panel:frozen,probe,ran(the readout ran on the last frame: a scope shown and the pointer on it or on the picture, or a measure frozen -- nothing runs otherwise),hovered,crosshair,marker(the frozen measure's mark),menu_open,labelandlabel_rect(the words and their box,[x0, y0, x1, y1]indebug-mouse's coordinates,nullwhen not drawn),runsandmeasures(frames it ran, measures taken: the cost).readout.image_pixel:valid(the pointer on the picture, a scope shown: its pixel is marked in the scope),column,rgb(the color shown there, after the grade and the display transform, as the pixel probe reads it) andreads(pixels read for the mark);readout.columns:first,last(the picture's columns under the scope's left and right edges) andimage_width.block: the Color panel's scope block, the user's preference (session_config.json, keycolor_panel_scopes, never the grade's):height(the user's, px at 100 %,nullfor the default),collapsed(its chevron),in_window(Open in Window: folded while the Scopes window is open),folded,drawn,plot_h(the box's height drawn, px),window_lines(the main window's height in pixels),default_h(120 for a window of 1080 lines or fewer, else 168),panel_h,min_h(72 px x the scale) andmax_h(half the panel). Folded, the panel draws no scope and asks for no readback.
{"cmd": "scopes"} {"cmd": "scopes", "open": true, "kind": "waveform"} {"cmd": "scopes", "open": false}- Parameters (optional):
Playback Rate & Frame Numbering
Set FPS : Overrides the playback frame rate.
{"cmd": "set-fps", "value": 23.976}Set Review Context : The review context of a shot, shown by Essential on the line over the picture ("project / sequence / shot / version", the department and the artist).
indexis the playlist's row (the shot on screen by default);project,sequence,shot,version,departmentandartistare strings, each optional: a field given replaces that shot's,""gives it back to what the file says (the Version Board's naming rules, the tracker's shot, task and artist), a field not given stays. Kept for the session (by the shot's path), not in the.bpl. The reply givescontext(the six fields as shown,index, andgiven: the fields set this way).status.review_contextsays the same for the shot on screen (nullwithout one). Refused, never cut: anindexthat is not an integer row of the playlist (a float,1.0too, a negative value, one past 32 bits or past the last row), a field longer than 200 characters.{"cmd": "set-review-context", "index": 0, "project": "Demo", "sequence": "SQ010", "shot": "SH0040", "version": "v003", "department": "comp", "artist": "Artist A"}Set Start Frame : Types the number of the first frame of the shot on screen, as a double-click on the ruler does (G2): THIS shot only -- the other shots keep theirs --, saved with its row in the
.bpl("start_frame"), and sent to the other seats of a review session by the pilot.valueis an integer up to 10 000 000; a negative one takes the shot's start away (its files' number, or Default Start Frame, numbers it again). Refused past 10 000 000 or without a shot on screen; the reply givesstart_frame(-1: none).{"cmd": "set-start", "value": 86400}
Performance & Cache
These mirror the controls in the Cache & Performance tab of the Inspector.
Set Proxy Resolution : Decode resolution, trading detail for speed and memory.
- Parameter:
proxy_idx(integer).
proxy_idxMode 0Full Res (1/1) 1Half Res (1/2) 2Quarter Res (1/4) 3Eighth Res (1/8) 4Smart RAM-Fit (chosen per clip from its resolution, length and your RAM budget) {"cmd": "set-proxy", "proxy_idx": 2}Same effect as the Inspector dropdown: sets the tier of the clip on screen (the tier is per clip, saved with the playlist, Smart RAM-Fit by default); that clip's frames at the old size are evicted and re-decoded at the new one; other cached clips are kept. The picture keeps its on-screen size (zoom is relative to the source).
{"cmd": "set-proxy", "proxy_idx": 4}The reply says the command was accepted, not that the new tier is on screen.
viewport.proxy_scalemoves within a frame; the eviction and re-decode behindviewport.decoded_widthtake from a fifth of a second to about half a second on a 2K EXR sequence. Between the two,viewport.retieringistrue— pollstatusuntil it isfalse, and only then areproxy_scaleanddecoded_widthdescribing the same thing.playback.transitioningdoes not cover this: it means a change of clip.Index change
Smart RAM-Fitmoved from3to4when theEighth Res (1/8)tier was added. Scripts written before that change will select1/8where they previously selectedSmart RAM-Fit.- Parameter:
When decoding is too slow :
{"cmd": "set-slow-decode-tier", "value": "auto"}-- the setting of Preferences > Cache & Performance > When Decoding Is Too Slow:"off"(the default: frames are skipped, with the warning; it was"auto"in earlier versions, which saved it at every save: a profile they wrote reads"off"once, and the profile then carries"slow_decode_tier_migrated": true; an"auto"chosen after that is kept) or"auto". Under Auto, a clip on Smart RAM-Fit whose decoding is slower than its frame rate while frames are being lost goes one tier lower (down to1/8) -- unless the RAM fill is bringing it in (status.decode.filling: the playlist fits, not a film left streaming), where the lost frames are the fill's and nothing moves --, and a notification says so ("Decode too slow for 24 fps: switched to 1/4"); a lower tier that is not at least 25 % faster is undone, and the tier goes back up when the tier above is predicted fast enough again. When the decode setup changes -- hardware decode switched on or off, A/B on or off, another clip on track B -- the lowered tier is lifted and every tier is measured again ("Decode setup changed: back to 1/2, measuring again"), as soon as the clip decodes again while playing: a clip held in RAM at its lowered tier keeps it. A clip keeps its lowered tier across the playlist, the tier its frames are held at in RAM: going to another clip and back does not lift it (the clip's whole cache is not decoded again); a clip removed from the playlist (and off track B) is forgotten. Every change of tier is said, a rise as a descent. A tier you fixed yourself is never changed. Under Off, the warning only. Saved in the profile (session_config.json, keyslow_decode_tier,"auto"or"off"; a file without the key reads Auto). Withoutvalue, it reads the setting.status.decodesays what the rule did.- Reply:
{"status":"ok","cmd":"set-slow-decode-tier","value":"off"}, or"status":"error"with areasonfor any other value.
- Reply:
GPU decode for NOVA (beta) : Turns the GPU decode of
.nvfxframes on or off (Preferences > Cache & Performance > GPU decode for NOVA (beta), off by default). The setting is saved in the profile (session_config.json, boolean keynova_gpu_decode, withnova_gpu_decode_chosentrue once the user chose it; absent or not chosen, this release's default), and the decoded frames in RAM are dropped and decoded again by the new rule. Withoutvalue, it only answers the state. See NOVA for what runs on the GPU and what stays on the CPU.- Parameter:
value(boolean, optional). - Reply:
{"status":"ok","cmd":"set-gpu-decode","value":true,"gpu_decode":{...}}-- thegpu_decodeobject is the onestatuscarries --, or"status":"error"with areason:"value must be true or false"for avaluethat is not a boolean (nothing changes), or"this build has no GPU decode"in a build without NOVA.
{"cmd": "set-gpu-decode", "value": true}- Parameter:
GPU YUV upload for movies (beta) : Turns the YUV upload of movie frames on or off (Preferences > Cache & Performance > GPU YUV upload for movies (beta), off by default). The setting is saved in the profile (
session_config.json, boolean keygpu_yuv_upload, withgpu_yuv_upload_chosentrue once the user chose it; absent or not chosen, this release's default). Turning it off drops the planes frames from RAM (they decode again as RGB); turning it on keeps the frames already decoded (the next ones come as planes). Withoutvalue, it only answers the state. See GPU YUV upload for movies (beta) (section 10) for what runs on the GPU and what stays on the CPU.- Parameter:
value(boolean, optional). - Reply:
{"status":"ok","cmd":"set-gpu-yuv","value":true,"gpu_yuv":{...}}-- thegpu_yuvobject is the onestatuscarries --, or"status":"error"with areason:"value must be true or false"for avaluethat is not a boolean (nothing changes).
{"cmd": "set-gpu-yuv", "value": true}- Parameter:
GPU video decode (beta) : Turns the decoding of movies by the graphics card's video decoder on or off (Preferences > Cache & Performance > GPU video decode (beta), off by default). The setting is saved in the profile (
session_config.json, boolean keygpu_video_decode, withgpu_video_decode_chosentrue once the user chose it; absent or not chosen, this release's default). Turning it off: the decoders on the card open their film again on the processor at their next read, and the planes frames are dropped from RAM unless the GPU YUV upload for movies (beta) setting keeps the converter. Withoutvalue, it only answers the state. See GPU video decode (beta) (section 11) for what the card decodes and what stays on the processor.- Parameter:
value(boolean, optional). - Reply:
{"status":"ok","cmd":"set-gpu-video-decode","value":true,"gpu_video_decode":{...}}-- thegpu_video_decodeobject is the onestatuscarries --, or"status":"error"with areason:"value must be true or false"for avaluethat is not a boolean (nothing changes).
{"cmd": "set-gpu-video-decode", "value": true}- Parameter:
GPU ProRes decode (beta) : Turns the ProRes decode on the graphics card on or off (Preferences > Cache & Performance > GPU ProRes decode (beta), off by default). The setting is saved in the profile (
session_config.json, boolean keygpu_prores_decode; absent reads off). Turning it off drops the packet frames from RAM (they decode again on the processor) and frees the engine; turning it on keeps the frames already decoded (the next ones come as packets). Withoutvalue, it only answers the state. See GPU ProRes decode (beta) (section 12) for what runs on the GPU and what stays on the CPU.- Parameter:
value(boolean, optional). - Reply:
{"status":"ok","cmd":"set-gpu-prores","value":true,"gpu_prores":{...}}-- thegpu_proresobject is the onestatuscarries --, or"status":"error"with areason:"value must be true or false"for avaluethat is not a boolean (nothing changes).
{"cmd": "set-gpu-prores", "value": true}- Parameter:
Set RAM Limit : Ceiling for the L1 RAM frame cache, in gigabytes. Clamped to the machine's available RAM.
{"cmd": "set-ram-limit-gb", "value": 32}Apply the RAM-Fit suggestion : Smart RAM-Fit never touches a tier you fixed yourself. When the playlist cannot fit RAM even with every Smart item at
1/8,status'sram_fit.suggested_fixed_tiersays which tier the fixed items would have to take, the playlist panel shows a banner offering it, and this command is that banner's Apply button. It moves every item whose fixed tier is above the suggestion down to it and leaves the rest alone. The reply is{"status":"ok","cmd":"ram-fit-apply","tier":2}(the tier applied, aproxy_idx), or an error when there is no suggestion to apply:{"status":"error","cmd":"ram-fit-apply","reason":"nothing to apply: the fit has no suggestion","message":"nothing to apply: the fit has no suggestion"}(message, the key this reply used beforereason, is kept one more version).{"cmd": "ram-fit-apply"}Precache the whole playlist : Playback > Precache Whole Playlist (
Ctrl+Shift+P). The preload window around the clip on screen (status.preload_window) widens to every clip of the playlist, whole, still under the memory ceiling; a notification says so when the ceiling is reached.valuesets it, without it the command toggles. Reply:{"status":"ok","cmd":"precache-playlist","value":true}.{"cmd": "precache-playlist", "value": true}Playlist playback : the loop mode (
loop_mode:0off,1the shot,2the playlist,3Ping-Pong -- the toolbar's loop menu; Ping-Pong plays forward to the out point, back to the in point, and again, the shot's ends when none is set, and never goes on to the next shot) and Playback > Don't wait for incomplete shots (dont_wait_incomplete_shots), either or both. While the playlist plays through to the next shot, a shot whose pass missed frames (not yet in memory) is looped, its frames shown as they arrive, until one pass is whole; only then does the playlist go on. With the option on, it goes on at once. A shot whose passes stop improving (three in a row) goes on anyway, so a playlist never hangs on it.status.playlist_playback.shot_waitsays how:missed_this_pass,missed_last_pass,passes,stalled_passes,last_end("advance","loop","advance-stalled"),loops_waiting,advances_incomplete;status.playlist_playback.old_tiercounts the frames a tier change kept on screen until replaced (kept_frames) and let go of over the cache's share (dropped_frames).continuous(optional,trueorfalse): Continuous Playback (Play All); in the Edit layout it is held on and given back on leaving Edit (a choice made in Edit stays); it is not saved. Reply:{"status":"ok","cmd":"set-playlist-playback","continuous":true,"loop_mode":2, "dont_wait_incomplete_shots":false}, or"status":"error"with areasonfor another loop mode or acontinuousthat is not a boolean.{"cmd": "set-playlist-playback", "loop_mode": 2, "dont_wait_incomplete_shots": false, "continuous": true}Full quality for the clip on screen : what a click on the viewer's Reduced 1/4 badge does (
Ctrl+Shift+F): the clip on screen decodes at 1/1 for the session, whatever the screen asks;falsegives it back its Smart tier. Withoutvalueit toggles. Reply:{"status":"ok","cmd":"full-quality","value":true}.{"cmd": "full-quality", "value": true}Set VRAM Ring Limit : caps the instant-scrub VRAM ring at
valueGB;0restores Auto (85% of the video memory available to the player). A cap never allows more than Auto would.ram-inforeportsvram_ring_budget_bytes,vram_ring_auto_bytes,vram_ring_manual_gband the budget's source. The ring counts each texture as the driver stores it (an 8-bit RGB frame at 4 bytes a pixel);vram_ring_failed_uploadscounts the textures the driver could not allocate, left out of the ring.{"cmd": "set-vram-ring-limit-gb", "value": 4}Enable Hardware Decode : Toggles GPU-assisted decoding — D3D11VA/DXVA2, VideoToolbox or VAAPI/VDPAU for regular video, and the OpenCL debayer for RED RAW. Falls back to CPU silently wherever it is unavailable. Both tracks reopen so the change takes effect immediately.
{"cmd": "set-hardware-decode", "value": true}Pre-cache a Clip : Asks the player to pull a clip's frames into the RAM cache ahead of time.
- Parameter:
clip_id(string) -- the clip's path, as reported bystatusorram-info.
{"cmd": "cache-clip-frames", "clip_id": "D:/shots/sh010/render.mov"}- Parameter:
Compression cache
The compression cache keeps compressed copies of the image sequences you open
(NOVA q97 or q92, later lossless EXR HTJ2K) on this machine only, in your own
profile (%LOCALAPPDATA%\BernardReview\CompressionCache on Windows,
~/Library/Caches/BernardReview/CompressionCache on macOS,
$XDG_CACHE_HOME/bernard-review/compression-cache or
~/.cache/bernard-review/compression-cache on Linux), never in a shared folder:
the copies are a double of the source images. A sequence is recognized by the
content of its frames, not by its path: moved, renamed, renumbered or copied, it
finds its copies again. The cache empties itself when it is opened and then
every hour, in this order: sequences not read for 30 days; above its maximum size
(20 % of the disk by default), least recently read first; while the disk has
less than 10 % free, least recently read first. Pinned sequences and the one
on screen are never removed automatically.
What exists today
The player does not compress anything yet: this version has the cache's
store and these two commands only, so the cache stays empty. The store is
opened, and its folder created, at the first cache-info or
cache-purge of a session, which is also when its hourly clean-up starts.
Cache info : read-only. The whole cache, or one entry with
entry.{"cmd": "cache-info"} {"cmd": "cache-info", "entry": "3f2a9c01d4e5b677"}Reply:
{"status": "ok", "cmd": "cache-info", "enabled": true, "root": "C:/Users/artist/AppData/Local/BernardReview/CompressionCache", "total_bytes": 18253611008, "entry_count": 2, "max_bytes": 400088905728, "disk": {"capacity": 2000444528640, "available": 912371204096}, "policy": {"max_age_days": 30.0, "max_fraction_of_disk": 0.2, "max_bytes": 0, "min_free_fraction": 0.1, "maintenance_interval_s": 3600, "stability_window_s": 10}, "last_maintenance": "2026-09-24T09:00:12Z", "entries": [ {"id": "3f2a9c01d4e5b677", "codec": "nova", "quality": 97, "encoder": "nova 1.0", "format": "nova-q97@nova 1.0", "bytes": 9126805504, "copies": 120, "frames": 120, "frames_cached": 120, "frames_pending": 0, "stale_copies": 0, "created": "2026-09-20T14:02:51Z", "last_read": "2026-09-24T08:57:03Z", "pinned": false, "open": true, "state": "complete", "paths": [ {"dir": "D:/work/sh010/plates", "pattern": "sh010_plate.####.exr", "path": "D:/work/sh010/plates/sh010_plate.####.exr", "frames": 120, "first": 1001, "last": 1120, "first_seen": "2026-09-20T14:02:51Z", "last_seen": "2026-09-24T08:57:03Z"}]}]}enabledisfalsewhen the cache could not be opened, with the reason inreasonand nothing else (no profile folder, a shared location refused, a folder that is not a cache).max_bytesis the maximum the size rule applies (0 when the disk cannot be measured and no explicit maximum is set).diskisnullwhen the disk cannot be measured. Dates are UTC. Per entry:formatis the key that makes an entry (codec, quality and encoder; changing any of them makes another entry),bytesthe size of its copies,frameshow many distinct frame contents its seen paths point to,frames_cachedhow many of those have a copy,frames_pendinghow many still need one,stale_copiescopies nothing points to any more (the source frame changed or was deleted:cache-purgewithstaleremoves them).stateiscomplete(every frame has its copy),partialorempty.pathslists every folder and pattern the sequence was seen under, with its first and last frame numbers the last time it was seen there.openistruefor the sequence on screen. An unknownentryis an error.Cache purge : removes copies, and says what it removed.
scopeis one of:all: every entry, pinned ones included, plus any leftover in the cache folder. The files are deleted from the disk and the player's log records it. The sequence on screen is kept and listed inkept.sequence: one sequence, by itsentryid, or by apathit was seen under (any of its frame files, or<dir>/<pattern>). Every entry seen under that path goes (one per copy format). An error if it is on screen.stale: frames whose source file no longer exists are forgotten; an entry left with no source at all is removed, and copies nothing points to are deleted. Pinned entries and the one on screen are kept and listed.older-than: entries last read beforebefore, a UTC date ("YYYY-MM-DD","YYYY-MM-DDTHH:MM","YYYY-MM-DDTHH:MM:SSZ") or unix seconds. Pinned entries and the one on screen are kept and listed.
A single shot (
--singleshot) shares the user's cache and refuses every purge with an error: purge it from the user's player, which keeps it.{"cmd": "cache-purge", "scope": "all"} {"cmd": "cache-purge", "scope": "sequence", "entry": "3f2a9c01d4e5b677"} {"cmd": "cache-purge", "scope": "sequence", "path": "D:/work/sh010/plates/sh010_plate.1001.exr"} {"cmd": "cache-purge", "scope": "stale"} {"cmd": "cache-purge", "scope": "older-than", "before": "2026-09-01"}Reply:
{"status": "ok", "cmd": "cache-purge", "scope": "older-than", "removed": [{"id": "77ab01c2e3f40918", "reason": "purge-older-than", "bytes": 4312088576}], "removed_entries": 1, "removed_frames": 96, "removed_bytes": 4312088576, "kept": [{"id": "3f2a9c01d4e5b677", "reason": "pinned"}], "refused": 0, "total_bytes": 13941522432, "entry_count": 1}removed_framescounts copy files,refusedthe items the purge met and left alone because deleting them would have reached outside the cache folder (a link placed inside it, for instance; the log names each one). Errors, with nothing removed:{"status": "error", "cmd": "cache-purge", "reason": "..."}for a missing or unknownscope, asequencepurge withoutentryorpathor matching nothing, anolder-thanwithout a validbefore, a sequence that is on screen, and a cache that is off.
Multi-Layer / AOV
Set Active Layer : Switches which AOV/layer of a multi-part EXR is shown. Use
layer-infoto discover the available indices.{"cmd": "set-layer", "track_idx": 0, "layer_idx": 2}
Diagnostics
Build Version Board : Fills the Version Board (sequence -> shots -> departments -> versions) from a production tracker's answer, adding the paths to the playlist. Send one command per sequence; a department maps to one version or to an array of them, the versions its right-click menu lists (a department may also appear in several commands, once per version).
- Parameters:
sequence(name),id(the tracker's sequence id, optional),shots(array of{name, id, departments}), wheredepartmentsmaps a department name to{version, path, version_id, id}, or to an array of them. - A version's other keys (all optional):
x_tracking(its ids, the block of a.bplrow: the row it is or becomes carries them, so a review of it is published on that version),playlist(false: on the board only, a row the first time it is played),available(false: its media is not on this computer,pathmay be empty; playing it sendsmedia-requestinstead of loading, see the Event Reference),active(true: the version its cell shows). - A version is found again by its
version_id, else its path: sending it again updates it. Where the naming rules filed its media when its row was added (orDefaultSeqwhen they did not match), it no longer is: a media sits in one place on the board.
{"cmd": "build-matrix", "sequence": "SQ010", "id": "seq_42", "shots": [ ... ]}Each entry of
shotslooks like this (the department name is the key):{"name": "SH010", "id": "shot_1001", "departments": {"ANIM": {"version": "v002", "path": "/proj/SQ010/SH010/SQ010_SH010_ANIM_v002.mov", "version_id": "sg_77812", "id": "task_501"}}}A department with the versions a pipeline downloads on demand (Bernard Engine):
{"name": "SH0010", "id": "item-1", "departments": {"Comp": [ {"version": "v010", "version_id": "f42:v010", "available": false, "x_tracking": {"shot_id": "item-1", "version_id": "v010"}}, {"version": "v011", "path": "N:/SQ010/SH0010/comp/SH0010_comp_v011.mov", "version_id": "f42:v011", "playlist": false}, {"version": "v012", "path": "N:/SQ010/SH0010/comp/SH0010_comp_v012.mov", "version_id": "f42:v012", "active": true}]}}- Parameters:
Read the Version Board : What the board holds after drops (grouped by the naming rule) and
build-matrix.{"cmd": "get-matrix"}Reply:
sequences(name ->{id, shots: {name -> {id, departments: {name -> {id, active_version, versions: [{name, id, path, status, available, requested}]}}}}}) andnaming_regex.available: the version's media is on this computer;requested: it was asked of the pipeline (media-request) and is not here yet.Play a Version : Plays a version of the Version Board by its id, as its cell does: on track A its row, else a new row carrying its ids; on track B, the way
set-track-bputs a clip there. A version not on this computer is asked of the pipeline instead (media-request), andmedia-readyplays it.- Parameters:
version_id(string);track("A", the default, or"B"). - Reply:
{"status":"ok","cmd":"play-version","requested":false,"track":"A"}--requestedtrue when it was asked of the pipeline -- or"status":"error"with areason(no such version, a format this build lacks).
{"cmd": "play-version", "version_id": "f42:v011", "track": "B"}- Parameters:
Media Ready : The pipeline's answer to
media-request: the version's media is on this computer now, atpath, and plays where it was asked (track A or B), as if played again; or it could not be brought,errorsaying why in the viewer's notice. A version nobody asked for is only marked available. A request lasts one hour. It goes with the playlist (a new one, or the playlist cleared), with the IPC client that would answer it (when it disconnects), and with its version when abuild-matrixno longer lists that version (abuild-matrixthat lists it again keeps the request). An answer that comes late (no request pending), or after the track that asked shows another clip, only marks the version available: what is on screen stays ("played": false, and"view_kept": truein the second case).- Parameters:
version_id(string);path(string) orerror(string). - Reply:
{"status":"ok","cmd":"media-ready","played":true,"track":"A"}, or"status":"error"with areason(no such version, neitherpathnorerror).
{"cmd": "media-ready", "version_id": "f42:v010", "path": "D:/local/SQ010/SH0010/comp/SH0010_comp_v010.mov"} {"cmd": "media-ready", "version_id": "f42:v009", "error": "not on the NAS any more"}- Parameters:
Earlier Notes : The pipeline's answer to
earlier-notes-request(manu/earlier-notes): one page of the earlier notes and corrections of a row's shot -- those of its task, as the pipeline chooses them --, newest first, shown read-only in that row's "Earlier notes" section (Essential's inspector, under Notes; each card of Review Notes in Complete), only while Bernard Engine is connected. While a row is open (seeearlier-notes-request) the pipeline may also send its newest page again, unasked, when the task's notes change. They are kept for the playlist only: never in the row's note, the.bpl,export.json, a review session or the log. The artist's FIXED tick on a card is the player's own, kept with them for the playlist: it is not sent to the pipeline, anddonestays the pipeline's.- Parameters:
key(string, the request's, at most 64 bytes),row(integer >= 0, the request's),cursor(string, the request's, at most 256 bytes:""or absent for the newest page), then eithercards(an array of at most 20) andnext_cursor(string, at most 256 bytes, optional: older notes exist, and "Show Older" sends it back ascursor; absent or"": none), orerror(string, at most 4 KiB: the notes could not be read; the section says so, with Try Again). Optional, withcards:total(integer from 0 to 1000000: the task's notes and corrections over every page; anything else is an error,total must be an integerortotal must be from 0 to 1000000). The section's head shows the count, folded or unfolded: "Earlier notes (7)" --totalwhen the pipeline sent one (never fewer than the cards held), else the cards held, with a "+" while older ones exist ("Earlier notes (20+)"); just "Earlier notes" until a page came. A newest page withouttotaldrops a total given before; an older page withtotalupdates it. - A card:
id(string, 1 to 256 bytes, once per answer) andkind("note"or"correction"), then, all optional:version(the version it was written on),verdict("Approved","Not Approved"or"Pending", shown in the player's status colors;""none),categoryandcategory_color("#rrggbb", the category's own color),done(bool:truedone,falseopen, the default),author,created_at(ISO 8601,2026-10-12T14:03:00+02:00, shown as written: its date, hour and minutes),text(at most 16 KiB),thumbnails(at most 8:path, an absolute path on one of this computer's own disks --C:/...or/...; a network share\\server\...or//server/...is refused -- to a PNG or JPEG, at most 4 KiB, read only when the card is shown, off the drawing thread, 16 MiB and 4096 pixels a side at most;frame, the integer frame it shows, optional) andreplies(at most 20, oldest first, shown folded under their note:author,created_atandtext, at most 4 KiB). A name (version,category,author) holds at most 256 bytes,created_atat most 64. An optional field may also benull, and an optional text"": as if absent. - Which answers are taken: a newest page (
cursor"") for a key of this playlist is taken whether a request is pending or not, asked or pushed: it replaces the row's newest cards. When it is not the whole list (it has anext_cursor) and its oldest card (the last of the array) is among the cards the row shows, the cards shown after that one stay after the page -- the older pages already shown, a card never twice (sameid) -- and "Show Older" goes on from where it was, the row's ownnext_cursor. Otherwise the row starts again from that page and itsnext_cursor, and an older page still awaited is no longer taken: use stable cursors (such as "before this note"), not offsets. The older pages already shown are not refreshed by a push (a card edited or deleted there stays as it was until the section is unfolded again). The fold is kept, and so is the place in that section: a reader who scrolled into its list keeps the card at the top of the view in place when cards change above it in the same section; a reader at the top of the list sees the new note arrive. An older page is added after the cards only while its request is pending, a card already shown left out; anerroris taken only as the answer of the request pending for thatcursor(else not kept). A row keeps at most 200 cards: past them, "Show Older" is gone. A key belongs to one playlist and one connection of the pipeline: a playlist loaded or cleared, or the IPC client leaving, forgets every key. A request not answered within 30 seconds expires: the section says Bernard did not answer, with Try again. - Live pushes: for an open row the pipeline sends, unasked, the same command --
cursor"", the key of the request -- on its own change events (a note or correction of the task added, edited, set done or open, deleted, a reply added), never polled, a burst of changes coalesced into one push; and stops whenearlier-notes-closednames the key. Only to a player that listsearlier-notes-liveincapabilities.json: it also takestotal, and is the one that sendsearlier-notes-closed(a player without it ignores the unknown field, takes an unasked newest page, and never sends the event). - Reply:
{"status":"ok","cmd":"earlier-notes","key":"en-1","kept":true,"cards":12}--keptfalse when nothing waits for it (a key this playlist does not know any more, or a newest page pushed for a row that is open nowhere -- itsearlier-notes-closedcrossed the push or was lost --, not taken: stop pushing it; an older page or an error nobody asked for),cardshow many cards the row holds now -- or"status":"error"with areasonthat names the field (missing, of the wrong type, past its bound): nothing of that answer is taken, and a row waiting for it says the answer could not be read.
{"cmd": "earlier-notes", "key": "en-1", "row": 3, "cursor": "", "next_cursor": "page-2", "total": 7, "cards": [{"id": "note-42", "kind": "correction", "version": "v011", "verdict": "Not Approved", "category": "Animation", "category_color": "#e0a030", "done": false, "author": "Supervisor A", "created_at": "2026-10-12T14:03:00+02:00", "text": "The elbow pops at frame 1012.", "thumbnails": [{"path": "C:/Users/me/.bernard/thumbnailcache/production 1/note-42_thumb.png", "frame": 1012}], "replies": [{"author": "Artist B", "created_at": "2026-10-12T15:10:00+02:00", "text": "Fixed in v012."}]}]} {"cmd": "earlier-notes", "key": "en-1", "row": 3, "cursor": "page-2", "error": "the task could not be read"}- Parameters:
Dump Thumbnails : Writes the active clip's timeline thumbnails to a PNG contact sheet, graded with the clip's current color settings.
- Reply:
count(how many thumbnails exist) andmissing_raw_pixels(how many lack the pre-grading pixels needed to re-grade in place).missing_raw_pixelsshould always be0; anything else means that many thumbnails would keep showing a stale grade after a color change. Alsotrack("A"),mean_rgb(the mean 8-bit R, G, B of the graded thumbnails) andchecksum(FNV-1a over each thumbnail's frame number, size and pixels, in frame order): two values to compare between two states of the strip.thumb_width/thumb_heightare a sheet cell, the pictures' size: the size kept with the pre-grading pixels, widened by the clip's pixel aspect.framesgives each thumbnail'sframe,width/height(the picture, PAR applied),raw_width/raw_height(its kept pre-grading pixels, never widened: under an anamorphic x2, a 153x86 picture kept is a 306x86 picture shown),mean_rgbandchecksum. A 2x widening repeats every column, so a correct PAR-2 strip has the samemean_rgbas the PAR-1 strip of the same frames. - Reply, both tracks:
raw_size_refusals, how many times since the player started kept thumbnail pixels were refused -- not graded, not kept -- because the size given with them did not describe them (the first one is logged as[Thumbs] kept pixels refused (...)). Should always be0; the size mix-up it guards against made the re-grade read past the pixels, and crash, under a PAR other than 1. - Parameter:
track(optional)."B"(or1) answers for track B's strip (its active layer), read back from the GL textures the timeline draws:count,missing_raw_pixels,mean_rgb,checksum,thumb_width/thumb_height(the largest), andframes, each thumbnail'sframe,width,height,mean_rgbandchecksum. TheoutputPNG is optional there.reasonis"no track-B thumbnails yet"while B's strip is empty. This is how "grade B, B's strip follows; grade A, B's strip does not move" is checked without a screenshot.
{"cmd": "dump-thumbnails", "output": "D:/tmp/thumbs.png"}{"cmd": "dump-thumbnails", "track": "B"}- Reply:
Dictated Notes (local AI)
The setting Dictated notes (local AI) (Preferences, General page, card Dictation (BETA); off by default) lets
a reviewer speak notes while the shots play. The speech engine runs on this computer: the audio never leaves it and
is never written to disk. Each phrase becomes a draft of the shot and frame on screen when the speaker began it; a
draft is marked AI in the notes panels (Essential, Review & Notes) and waits for Validate, Edit or Discard: it is
never a note on its own, and nothing writes it out (session, export, tracker, guests) until it is validated -- then
only as the note's text. Drafts live in the player's memory, not in the .bpl. The engine needs its two model files
in the player's models folder (%LOCALAPPDATA%\BernardReview\models under Windows); without them, state is
failed, models.needed lists them and models.question is what the notes panel asks. The player downloads them
only after a yes (the panel's Download, or download: true here), checks each file's size and SHA-256 against its
catalogue, and refuses -- deletes -- any other file.
Dictation : The setting, the engine's state and the drafts. Without parameters, nothing changes: the reply only says them.
- Parameters (all optional):
enabled(true/false: the setting, changed and saved as the Preferences do),language("auto","en","fr","es","de"),listen(true: opens the microphone;false: closes it; refused while the setting is off or the engine notready),download(true: the user's yes, the missing models download -- refused while the setting is off or when none is missing;false: stops the download, what arrived kept as<file>.partfor the next try),delete_partial(true: deletes those.partfiles),model_mirror(a studio mirror,"http(s)://host[:port]", 256 bytes at most,""for Hugging Face; kept in the profile; a mirror out of shape is refused when a download is asked, never replaced by the internet),studio_prompt(trueby default: with a forced language -- never in Auto -- the engine is primed with the studio's words, Shot, Plate, Frame, Grading, Comp, Tracking, Roto, Version...;falsefor a test that measures without them; not kept), anddraft:{"action": "validate" | "edit" | "discard", "id", "text"[, "new_text"]}, the draft named by its id AND its text as listed (validated, its text goes into the shot's note on a line of its own, as a typed note). - Reply:
enabled,language,state(off,loading,ready,failed,stopped),reason(why it failed),engine(the model and where it runs, as the drafts say it),backend(gpuorcpu),device(the graphics card on the GPU),hangover_ms(the pause that ends a phrase: 400 on the GPU, 500 on the CPU),listening,busy, the counterssegments,phrases,failed,segments_dropped,samples_dropped,last_transcribe_ms,unplaced(phrases said while no shot was on screen),refused,inject_error(the lastdictation-injectof a.wavthat could not be read), anddrafts: each{row, id, media_frame, shown_frame, start_unix_ms, text, language, engine, state}(row-1 once its shot left the playlist);studio_prompt,model_mirror,push_to_talk_presses(the push-to-talk key's presses read: only while the setting is on),models(needed: each{file, label, size, partial_bytes, host};question),fetch(state:idle,running,done,failed,cancelled;received,total,current,message,requests,restarts,resumed_from) andui: the strips drawn in the last frame, by row, in screen pixels[x0, y0, x1, y1](dictate,download,delete_partial,cancel,drafts: each{id, validate, edit, discard}), for a test's injected clicks. - A field of the wrong type is refused (
"status": "error"and itsreason), nothing changed.enabledandlanguageare applied beforelisten: with{"enabled": true, "listen": true}the setting is saved on, andlistenis refused while the engine is stillloading.
{"cmd": "dictation", "enabled": true, "language": "en"}{"cmd": "dictation"}- Parameters (all optional):
Inject Dictation Audio : For tests and pipelines: audio heard as if it were said now, at the speed it would have been spoken, in place of the microphone. Refused while the setting is off or the engine not
ready.- Parameters: either
wav(the absolute path of a.wavon this computer, which the player reads: PCM 16-bit or float 32-bit, mono or stereo, 8 to 192 kHz, 64 MiB and 5 minutes at most; a network path -- a share or a drive mapped to the network -- is refused and never opened) orsamples(an array of 16 kHz mono samples in [-1, 1], 4,800,000 at most). - Reply:
source(the injection's number). The drafts come later, indictation; a.wavthat cannot be read is said there ininject_error.
{"cmd": "dictation-inject", "wav": "D:/tests/voice_en_plan7.wav"}- Parameters: either
Closing the Player
Quit : Closes the player through the same path as the window's close button.
{"cmd": "quit"}The reply
{"status": "ok", "cmd": "quit"}is sent before the shutdown begins — it is the last thing that arrives on the socket, and the connection then drops as the process ends. After it, the player stops its decode, video, thumbnail and thumbnail-pre-generation threads and waits for them, waits for the proxy-scale probes still in flight, leaves any synchronized review session, releases the audio device, saves the session (playhead, layout, settings) and exits with code0.It does not put up the "save your review session?" dialog the close button shows when there are unsaved statuses, notes or drawings: nobody is there to answer a modal, so an unsaved review is lost exactly as it would be by answering "no". A script that wants the review out of the player sends
export-notesfirst.Terminating the process instead skips all of that: the session is not saved and the log ends mid-line. Kill it only if
quitdoes not answer.
A tracker's link (bernardreview://)
The installers register the bernardreview:// scheme (docs/site/en/trackers.md, "Open in Bernard Review from Flow"): the browser starts BernardReview.exe "bernardreview://flow?...", or the launch hands the link to the player already open (the single-instance channel's open-link message), and leaves with 0. The link is untrusted input: only entity_type (Playlist or Version), ids, selected_ids, project_id and server_hostname are read; the ids must be whole numbers (500 at most), the link 16 KiB at most, the host a site of trackers.json, else it is refused and nothing is sent. Kitsu (its custom action through a relay page: ours, or tools/kitsu_relay.py hosted by the studio, which keeps nothing; docs/site/en/trackers.md): bernardreview://kitsu?entity_type=Task&selected_ids=<task ids>&project_id=<id>&server_hostname=<host>, Task only and every id a Kitsu UUID; each task opens as its latest preview (the highest revision), in the link's order, the first one on screen (or, without its preview, the first that has one); the host must be a Kitsu site of trackers.json. ftrack (its action answered by tools/ftrack_action.py on the user's machine, through ftrack Connect or alone; docs/site/en/trackers.md): bernardreview://ftrack?entity_type=Version&selected_ids=<version ids>&server_hostname=<host> (those versions, in the link's order, the first one on screen) or entity_type=Playlist with one review session or list id (the whole of it); every id an ftrack UUID; the host must be an ftrack site of trackers.json. A refusal, or the end of the opening, gives the open-link event and status.tracker.open_link. Nothing of a link is ever run.
Running two players on one machine
A clip opened by itself next to a player in use: BernardReview.exe --singleshot --ipc-port <n> <file>. The process started that way copies the settings of the profile (session_config.json, imgui.ini, keymap, naming rules, color presets, layouts, export presets and recipes) into a private folder of the single shots' own folder in the temp folder (%TEMP%\BernardReview-singleshot\<pid>-<start>; on Linux /tmp/BernardReview-singleshot-<uid>, the user's own, 0700), runs the player on it (APPDATA there on Windows, XDG_CONFIG_HOME on Linux: only the configuration; the cache -- the compression cache -- stays the user's, shared with the other player, whose upkeep it is: a single shot never evicts from it, and refuses cache-purge), waits for it, then removes the folder and exits with the player's code: the single shot's autosave, history, layout and session file never replace the other player's. The launcher and its player hold a lock file beside the folder while they run: a launcher killed or a player crashed leaves its folder, which the next single shot removes once nobody holds its lock -- never one still in use. Only that folder of the temp folder is ever read. A launcher that cannot start -- its folder unusable (a file at its name; on Linux, another user's folder), its lock or its player refused -- says why on its error output and exits with code 71. The single shot never asks to restore the last session and answers on the IPC only on the port it was given (none without --ipc-port). quit on that port closes it, and the launcher with it. On macOS the profile is shared; the two other rules hold.
A review driven by a pipeline adds --review-dir <dir>: at the close, whatever closed the player, the review notes are written as export-notes writes them, without the "save your review session?" question, then review-exported is sent (see the Event Reference); as soon as the window hides, review-exporting says how many pictures follow, and the pictures of several shots are written at once (one worker per shot, its own decoder, as many as the cores, 8 at most, fewer when memory is short). They are written beside <dir>, in <dir>.part, export.json first, then that folder is renamed onto <dir>: a new <dir> never holds half a review, whatever stopped the player (create <dir>'s parent and leave <dir> to the player; a root, a drive or a name without a folder above it is never written to). A <dir> already there, or a rename refused (a scanner holding a file), gets the files one by one instead -- the pictures, export.html, then export.json -- and keeps the files the player did not write; a file that cannot be moved stays in <dir>.part, and the others go on. A <dir>.part left by a player stopped mid-export is marked as the player's by a .bernard-review-part file (or is empty): the next export to <dir> empties it; anything else at that name is left alone, and nothing is exported. review-exported comes within five minutes of the close: past 280 seconds, what is written (export.json first of all) is put in <dir> as it is, nothing more is written, and the event says which frames are missing.
Exit codes of a review launch. A launch with --review-dir that would be handed to the player already open (the
"open in the running player" setting, without --ipc-port or --singleshot) is refused: the running player would never
write that review. The refusal is a line in the log and on the error output, and the process exits with code 75
without opening anything. Start such a review with --singleshot or --ipc-port <n>, which always run a player of its own.
A --singleshot launch that cannot start (its private folder, its lock or its player refused) says why on the error
output and exits with code 71; a single shot that started exits with its player's code.
Code 0 is a normal close (or files handed to the running player without --review-dir); 3 (Windows) and 128 + a
signal number (Linux, macOS) mean the player crashed, its report in crashes/.
Installing the player from a pipeline (Windows). BernardReview-<version>-win64.exe /S /NOASSOC /D=<folder> installs
without a window. /NOASSOC writes no file association at all, whatever the components page would have chosen: the
pipeline decides which files open in the player. /D= comes last, without quotes. A silent install asks nothing, so it
also uninstalls the old player's first versions, from before the rename, without asking (the
question an interactive install asks is answered yes). It exits with code 3 when a player runs from the installation folder, and then writes nothing.
Python: client.on_review_changed(cb), client.on_playlist_changed(cb) and client.on_review_exported(cb) in
bernard_api.py receive the three events above.
The player listens on port 5556 by default. Start a second instance with
BernardReview.exe --ipc-port 5599 and connect with
BernardClient(port=5599) (or python player-cli.py --port 5599 ...); otherwise both
instances answer on the same port and a script cannot tell which one it is driving.
Audio diagnostics in status
status.audio carries, besides volume and muted: out_rms, the RMS of the
last block the audio callback wrote (0 means it wrote silence -- the one number
that answers "is there sound?" from a script); full_audio_ready and
full_audio_seconds, whether the whole track of the clip on track A has been
decoded and how long it is; decoded_seconds, how far the streaming ring
buffer has been filled by the video decoder.
Synchronised Review Sessions
A session is one player hosting a WebSocket server on TCP 9000 and every other seat connecting to it. Everything below is also reachable from the UI; the commands exist so a two-machine review can be set up and checked from a script. The host enforces the session's id, the password and the pilot role, so a guest that sends transport or annotations while not driving, or a message for another session, is simply dropped (see dropped_by_server); so is a message a seat sends under another seat's sender_id, and a Host badge (is_host) only ever stays on the host's own messages (see server_guard). It refuses a web page's WebSocket (a handshake with an Origin header) with 403.
Create (host) a session : Starts the server on this machine and connects this player to it as the pilot.
- Parameters: all optional.
name,user_name,user_email,password(empty or absent = open session),reach("network", the default, or"this_computer").
{"cmd": "session-create", "name": "SQ010 dailies", "user_name": "Reviewer 1", "password": "hunter2"}Reply:
{"status": "ok", "cmd": "session-create", "session_id": "SESS-7KQ2M4XP9BZT", "password_set": true, "links": [{"address": "192.168.1.20", "kind": "lan", "link": "ws://192.168.1.20:9000/SESS-7KQ2M4XP9BZT"}, {"address": "100.84.3.7", "kind": "vpn", "link": "ws://100.84.3.7:9000/SESS-7KQ2M4XP9BZT"}], "listen": {"reach": "network", "addresses": ["::"]}}kindislan,vpn(100.64/10, Tailscale or a studio VPN), orpublic; the rows come in that order. Send a guest the row it can actually reach, and the password through another channel than the link. Where the server listens (SessionListen.h): withreach"network"it listens on every interface ("addresses": ["::"], one dual-stack socket) when this machine has a studio-network, VPN or public IPv4 address, and on the loopback otherwise."this_computer"always listens on127.0.0.1and::1only ("listen": {"reach": "this_computer", "addresses": ["127.0.0.1", "::1"]};::1is left out on a machine without an IPv6 loopback): a session between players of this computer, or a script's, that opens no port to the network and that Windows Defender Firewall asks nothing about. The links then hold the loopback row alone ({"address": "127.0.0.1", "kind": "loopback", ...}): a session never offers an address its server does not answer on. The choice is made when the session starts and holds until it ends. Any otherreachis refused:{"status": "error", "cmd": "session-create", "reason": "reach must be \"network\" or \"this_computer\"", "message": "..."}(the same words under both keys), and no session opens.session_idisSESS-and 25 characters ofA-Z0-9drawn from OpenSSL's cryptographic generator (129 bits: on an open session the id in the link is all a guest needs); the 12-character ids of the examples, drawn by older players, are still read everywhere an id is read. The host token (32 hex characters, 128 bits, never in a reply) and this seat's participant id (<name>#and 24 hex characters, 96 bits) come from the same generator. If the generator fails, no session opens:{"status": "error", "cmd": "session-create", "message": "Could not create a secure session token"}, and the log says[Session] not started: Could not create a secure session token (the system's random generator failed);could not listen on port 9000means only that the port is taken.- Parameters: all optional.
Join a session :
addressis what the host sent.ws://host:9000/SESS-...,host:9000/SESS-...,host/SESS-...,hostalone (port 9000 assumed), and relay linkswss://relay.example.com/s/SESS-...?t=<token>(orwss://relay:port/s/SESS-...?t=...) are all accepted.http://, a port outside 1..65535, or a session id containing invalid characters are refused with{"status":"error"}rather than connecting to nothing. A player's host requires the session's id on everything a seat sends:hostalone, or another session's link, connects and is then closed by the host (close reasonsession);session-statusthen says"connected": falseandauth_errorbeginsCould not authenticate with the session: the host refused this link. The seat's participant id is drawn from the system's cryptographic generator before anything connects; if it fails, the seat does not join:{"status": "error", "cmd": "session-join", "message": "Could not create a secure session token"}.{"cmd": "session-join", "address": "ws://192.168.1.20:9000/SESS-7KQ2M4XP9BZT", "user_name": "Alex", "password": "hunter2"}Reply:
{"status":"ok","cmd":"session-join","session_id":"SESS-7KQ2M4XP9BZT","url":"ws://192.168.1.20:9000","is_host":false}Joining as the host (remote-review relay): a session created on a relay's REST API has no local server, so its host joins it like a guest and adds
host_token, the token the relay returned with the session. The first presence presents it (the relay strips it, as a LAN host strips its own), and this seat is then the host: it starts as the pilot,session-pass-batonworks, and the reply andsession-statussay"is_host": true. Guests leave it out. A wrong token is not an error here: the relay simply does not treat the seat as host, and its baton passes are dropped.{"cmd": "session-join", "address": "wss://relay.example.com/s/SESS-7KQ2M4XP9BZT?t=<jwt>", "user_name": "Reviewer 1", "password": "hunter2", "host_token": "<host_token from POST /api/v1/sessions>"}Reply:
{"status":"ok","cmd":"session-join","session_id":"SESS-7KQ2M4XP9BZT","url":"wss://relay.example.com/s/SESS-7KQ2M4XP9BZT?t=[redacted]","is_host":true}The relay's token stays in the player: the
t=of a relay link is used for the connection and nowhere else. No reply carries it:urlsays where this seat connected with the token's value replaced by[redacted], and so does an error'smessage(Could not connect to wss://...?t=[redacted]);session-statusnames no address of this seat. The interface never shows it either: the Session Review panel's Join field shows the address redacted (and joins again with the token kept in memory), the Join dialog forgets the link once joined, and the Console echoes a line typed with its secrets redacted and does not save it in its history. The joined link is never an invitation: the relay makes one link per role, each with its own JWT (the host's/s/<id>?t=<host JWT>, the guests', the web's), so the link a seat joined a relay by is its own, the host's for the host. In a relay session (this seat runs no server and joined awss://orhttps://link), Invite people's Copy link and Send and the Session Review panel's Copy Link give no link (neither that one nor aws://studio-network address): they are grayed, with the tooltipInvite guests with the browser link of Online review (Copy link in its window), andstatus'ssession_ui.invite.linkis empty. Studio-network and VPN sessions (ws://, served by a player) are unchanged. The Join fields take links up to 2,047 characters (a relay's JWT runs to several hundred). The redaction is the stream companion's rule (t=,token,jwt,Bearer, a JWT-shaped word), the one rule of the player.Leave (or close) the session :
{"cmd": "session-leave"}Online review: what the Online review window's buttons do. The host signs in to the Bernard relay with an e-mail code (
POST /api/v1/auth/email/start, then/verify: a user token of 8 h, kept in the system's credential store, Windows' Credential Manager), then starts a review: the relay creates the session (POST /api/v1/sessions), makes ONE browser link (POST /api/v1/sessions/{id}/invites, roleweb), and the player joins it as its host and streams to the browser guests.endcloses it on the relay (DELETE /api/v1/sessions/{id}: every token refused) and leaves.actionisopen,start(withname, optional) orend;startonly once the user signed in himself in the window (a local client must not sign in with its own address and send the picture elsewhere).send-code(withemail),verify(withcode, 6 digits) andsign-outare the window's, taken over IPC only with a test relay on this machine. The requests run on their own thread: the reply only says whether the action was taken ("status":"error"while a request runs or at the wrong step);status'sonline_reviewfollows them. A start with an expired sign-in sends a new code to the same address and says so inerror; the review starts once the code is verified. TEST ONLY:BERNARD_RELAY_URLin the environment replaces the relay (a fake one on 127.0.0.1).{"cmd": "online-review", "action": "send-code", "email": "alex@example.com"} {"cmd": "online-review", "action": "verify", "code": "123456"} {"cmd": "online-review", "action": "start", "name": "SQ010 Dailies"}Reply:
{"status":"ok","cmd":"online-review","action":"start"}.status'sonline_reviewis{"made": false, "open": false}until the first use (nothing is read or sent at the player's start), thenmade,open(the window),phase(unknown,signed_out,code_sent,signed_in,starting,live,ending),busy,emailandsent_to(masked:g***@example.com, never the whole address),start_after_sign_in,error(in words, empty without one),session_idlink_shown(the browser link with its token hidden:https://<relay>/join/SESS-...#t=[hidden]) andrects(start,end,copy,sign_out,notes_link,close-- its title bar's cross --,console-- Create a studio, under the code field: it opens https://review.bernardengine.com/console/ in the browser --,window-- the whole window: beside the picture, never over it --: where the window drew its buttons, screen coordinates,nullwhen not drawn);notes_link_shown(the note-taker's link, made by the window's Make a note-taker link -- an invitation with the notes right --, its token hidden;""before) andpeople(each seat as the window words it:name,role,notes_taker, androles, an array of"pilot","decider","notes", empty for a Viewer; never an e-mail);invite_role(the window's Invite as:viewer,notesordecider),decider_link_shown(the last Decider link, made for the e-mail typed, its token hidden;""before; never the e-mail) and inrectsinvite_role(the dropdown),invite_role_items(its three items, Viewer, Notes, Decider, while it is open;[]otherwise),decider_email(the Decider's e-mail field) anddecider_link(Make a Decider link, then Copy Decider link);copystays the guests' Copy link (drawn while Viewer is picked),notes_linkMake a note-taker link (then Copy note-taker link) while Notes is picked. No token, code or whole link is ever in a reply,statusor the log; only the window's Copy link gives the link.Session status : the whole state in one call.
{"cmd": "session-status"}Reply:
active,hosting(this player runs the session server),is_host(this seat is the session's host: it runs the server, or joined withhost_token),connected,session_id,links,driver_id,local_id,local_name,local_initials(the avatar initials, one or two whole characters),authenticated(this seat's opening presence, its authentication, has gone out),auth_error(why it could not, empty otherwise),sync_locked,local_view(a guest's own zoom and pan:{"local", "returning", "hands"}--localwhile its own framing is on, before it comes back to the pilot's 4 s after its last zoom or pan gesture,returningduring the 0.3 s of that return,handsthe gestures that took one since the player started;nullfor the pilot or outside a session),guest_decides(a guest of an online review that the host's policy allows: its status keys ask the host),decisions_waiting(its requests to the host not answered yet),decisions_not_delivered(those given up since the player started, each with the notice "Not delivered to the host: ..."),participants(id,name,initials,email,is_driver,hand_raised,is_muted,is_synced),missing_clips(clips of the pilot's playlist that are not on this machine),dropped_by_server(messages the host's guard refused without closing the connection),server_guard,listen(where this seat's session server listens, assession-createreplies it:{"reach": "network" | "this_computer", "addresses": [...]};nullon a seat that runs no server),viewport_syncedandvoice.server_guardis{"refused": {"<reason>": N, ...}, "rewritten": {"<field>": N, ...}}, this host's guard since the session started, only the reasons and fields that occurred (two empty objects on a seat that runs no server).refusedcounts the messages dropped or that closed their connection, by reason:spoof(a message other than a presence whosesender_idis not the one its connection registered, whatever its type, or a guest's presence about another seat),size(a presence, chat line, reaction, voice chunk, laser point, transport, seek or paint settings over 64 KiB, the relay's limit for those),voice_rate(voice past the seat's share: one and a half times real time, two seconds at once),not_driver,session,json,rate,auth,duplicate,full...rewrittencounts the relayed messages the host corrected, by field:is_host(a top-levelis_hostthat did not match the seat's role: only the host's seat carriestrue; the field is never added to a message without it),name,initials,email(a presence's texts, cut to 64, 4 and 254 characters and freed of control and text-direction characters),sender_name(a chat line's name, put back to the name its seat's presence gives). A non-zerospooforis_hoston a normal session means a seat is sending what it should not.viewport_syncedis{"received": N, "applied": N, "ignored": N}: the pilot's looks (itsviewportmessages) that reached this seat, those put on screen (not in Local Review), and the looks ignored because this seat did not know their sender as the pilot -- a seat learns who drives from the presences it receives. A seat that arrives is sent the pilot's presence before its look, soignorednormally stays at 0: a look the pilot sent before it had read the newcomer's presence (in the middle of a drag, say) is ignored once, and the look sent in reply puts it right. The three counters go back to 0 when this seat creates or joins a session. The player's log says[Session] viewport from <id> ignored: this seat does not know it as the pilotfor the first five.voiceis{"capture": bool, "playback": bool, "backend": "WASAPI", "chunks_sent": N, "chunks_played": N, "refused_unknown_sender": N, "refused_over_rate": N}: whether the microphone and the talkback speaker are actually running, which miniaudio backend opened them (WASAPI,ALSA,PulseAudio,Core Audio, ornonewhen nothing is open), and how many 100 ms chunks this seat has sent and heard.captureis false whenever the seat is muted -- mute closes the device, it does not merely stop sending. A seat plays voice only from a seat of the room (a participant it has a presence from) and, per sender, up to one and a half times real time, two seconds at once;refused_unknown_senderandrefused_over_ratecount the chunks it did not play for each reason since the player started (both stay at 0 on a normal session). A host also holds each seat to that share before relaying (server_guard.refused.voice_rate).thumbsis{"sent": N, "skipped": N, "unready": N, "queued": N}: the playlist's thumbnails this host sent to an online review's web guests since the player started (one per row at each arrival), those no message could hold, those whose row had no thumbnail yet when its turn came, and those waiting now.Test the talkback speaker : plays a 440 Hz tone through the voice-chat playback device as if another seat had sent it, so a machine with no microphone can still be checked before a review.
secondsis optional (default 1, clamped to 2 -- the ring holds two seconds).{"cmd": "voice-test", "seconds": 1}Reply:
{"status":"ok","cmd":"voice-test","seconds":1.0,"backend":"WASAPI"}, or{"status":"error","cmd":"voice-test","reason":"no playback device could be opened","message":"no playback device could be opened"}naming what failed: read"reason";"message", its old key, says the same for one more version.Pass the baton : hands the pilot's seat to another participant. Host only: the player that runs the server, or a seat that joined with
host_token(is_hostinsession-status); anyone else getsonly the host can pass the baton. The id is the"<name>#<24 hex>"fromsession-status("<name>#<6 hex>"for a seat of an older player), not the display name.{"cmd": "session-pass-baton", "participant_id": "Alex#9f2c1d0b7e4a33c85d61f0a2"}Sync Locked / Local Review :
falsedetaches this seat to scrub on its own,truesnaps it back to the pilot. While a seat follows, every clip it loads takes the pilot's last look (status.color.gradeis the pilot's, after a clip change too);falsealso gives this seat its own grade back on every clip the pilot's look replaced it on (in the playlist, and on screen for the clip on screen), as leaving the session does. A seat that comes to drive, however (given the baton, taking it back as host, or the host's own row put back by its heartbeat), drops those grades instead: what it does as pilot stays, and ticking Local Review or leaving no longer brings them back (its log says[Session] own grades dropped: this seat now drives). The pilot may sendfalsetoo: it then sends nothing only the pilot sends (notransport,seek,navigation,playlist_sync,paint_settings,annotation_syncorviewport; a seat arriving meanwhile gets its presence only), the others keep the last picture it sent, andstatus.session_ui.bar.syncreads"state": "free","label": "Local Review — not broadcasting".truefrom the pilot does not move it: it keeps its frame, clip and look, and sends them all once on the next frame (playlist, clip, play state and frame, brush, look), so every follower comes back to it.{"cmd": "session-set-sync", "locked": false}Chat :
{"cmd": "chat-send", "text": "the shadow on 104 is still too hot"} {"cmd": "chat-log", "last": 20}chat-logreplies withmessages:sender_id,sender_name,text,is_host,timestamp. A seat keeps the 500 latest lines of the chat (older ones are dropped as new ones come), sochat-logreturns at most 500.Laser pointer : one sample, in normalized viewport coordinates (0..1 from the top-left of the picture);
active: falseputs the pointer away on every other seat.{"cmd": "laser", "x": 0.42, "y": 0.65, "active": true}Laser status : what this seat has heard from the other seats' laser pointers -- the way to check that a remote participant's pointer (the web review page, another player) actually reaches this one. One row per participant heard within the last
max_age_ms(optional, default 2000, clamped to 0..3600000), in sender-id order: its last sample and how many arrived since the session started. Rows are kept after the viewport stops drawing a pointer (500 ms after its last sample) and after a sender puts it away (is_active: false); a new session starts the counts again.{"cmd": "laser-status", "max_age_ms": 2000}Reply:
{"status": "ok", "cmd": "laser-status", "session_id": "SESS-7KQ2M4XP9BZT", "max_age_ms": 2000, "lasers": [{"sender_id": "Alex#9f2c1d", "name": "Alex", "x": 0.42, "y": 0.65, "is_active": true, "size": 4.0, "color": 4278190335, "age_ms": 140, "samples": 37}]}x,yare normalized viewport coordinates (0..1 from the top-left of the picture),coloris theIM_COL32integer as received (0xAABBGGRR: 4278190335 is opaque red),nameis empty when the participant list does not know the sender,age_msis measured on this seat's clock.Stream to Remote Guests : the Sessions menu's "Stream to Remote Guests". Hands the picture to the
bernard-streamcompanion through shared memory, which encodes it for the web guests (the layout is indocs/stream_shm_contract.md). The player draws that picture itself, offscreen, at the stream frame's size, with the host's view: both grades, the A/B split, flips, the channel, Highlight Clipping, his zoom and his pan -- whatever the size of his window, his panels or the full screen, and also while his Viewport is hidden (the contact sheet layout, the window minimized: the last view is kept and the picture keeps playing). Only the picture is drawn into it: no annotation, laser, HUD text, panel or watermark (annotations and lasers reach web guests as session messages, and the player's watermark, which names the host user, is never streamed). Off by default; refused withnot hosting or driving a review sessionunless this seat is in a session as its host or its pilot, and switched off by the player when it stops being either. In an online session only the host streams (the relay lets only the host's token publish): another seat, the pilot included, is refused withOnly the host's player sends the picture to web guests.(its menu row is grayed with that sentence), and the host goes on streaming while another seat drives.widthandheightare the stream frame size, optional (default 1920x1080), clamped to 16..4096 and rounded down to even numbers. At Fit the picture fills the frame with black bars where the aspects differ; the host's zoom is kept relative to Fit (zoomed x2 from his Fit, x2 from the frame's) and changes only when he zooms, never when his window or a panel is resized; his pan is kept as a fraction of the picture (the point of the picture at the center of his viewer is at the center of the frame). The change is applied at the next frame, so the reply'sstatemay still saystarting. In an online session (this seat joined through a relay) the host's player also launches the companion itself, and stops it when streaming is switched off, when the session ends and when the player closes; on the studio network, a VPN or this computer nothing is launched (a companion started by hand still attaches). What the companion is given -- never a secret on its command line -- is indocs/stream_companion.md.{"cmd": "stream-enable", "on": true, "width": 1920, "height": 1080} {"cmd": "stream-enable", "on": false}Stream status : read-only, same block as the
stream-enablereply.{"cmd": "stream-status"}Reply:
{"status": "ok", "cmd": "stream-status", "enabled": true, "allowed": true, "state": "streaming", "error": "", "companion_pid": 51234, "subscribers": 2, "width": 1920, "height": 1080, "generation": 3, "slots_allocated": true, "gl_resources": true, "frames_captured": 812, "frames_published": 811, "frames_dropped": 4, "render_cost_us": 61.5, "publish_latency_us": 48000.0, "source_width": 1195, "source_height": 551, "shm_name": "Local\\BernardPlayer_Viewport_SHM_4242", "companion_state": "running", "companion_error": "", "encoder": "h264_nvenc"}companion_stateis where the companion the player launches is:not_bundled(this build ships no companion: it cannot stream to web guests; said whatever else holds),starting(launched, not attached to the shared memory yet),running(attached: its pid iscompanion_pid),restarting(it stopped on its own and is launched again after 1, 2, 4, 8, 16, then 30 s),stopped(not running and not wanted: streaming off, or a session that is not online), orerror(it could not be launched; tried again after the same delays).companion_errorsays why it stopped, failed or restarts ("bernard-stream exited with code 3 after 0.2 s; restarting in 2.0 s"), empty when all is well.encoderis the name the companion announced in its log (encoder: ...), empty until it does. The companion's log lines go to the player's log as[Stream] ..., every token redacted.stateisoff,starting,waiting_for_companion(shared memory open, no live companion registered),idle(a companion is attached, no guest watching: nothing is read back and no frame memory is committed),streaming, orerrorwith the reason inerror(for example the object name still held by a companion from the previous run; the player retries every second).companion_pidis 0 unless that process is alive.frames_capturedcounts readbacks,frames_publishedframes written to shared memory,frames_droppedvisible states that were superseded before they could be captured (the pipeline was busy or the companion behind: dropped, never queued).render_cost_usis the render thread's time in the streamer per frame (moving average; 0 while off).publish_latency_usis the time from the capture (in the frame the change is drawn) to its publication in shared memory, moving average: mostly the GPU finishing the frames queued before it (2 to 3 frames with vsync).source_width/source_heightare the size of the texture the last captured picture was drawn from (the decoded frame, or its proxy tier), 0 for the black screen of a clip loading. While playing, one frame is captured per new picture; while paused, a change of the picture (seek, clip, grade, A/B, Highlight Clipping, pan/zoom) is captured in the frame that draws it, and an unchanged picture once a second.Send to guests (secure distribution, beta) : the Session Review window's "Send to guests", for the host of an online review only (refused elsewhere with
Send to guests needs an online review sessionorOnly the host sends the shots to the guests). On, the host's player takes the playlist's shots at that moment (a movie or a single frame is its file; a sequence its images as the player detects them, within the row's in and out marks, never the folder's other files), seals every piece once into its own copy underdistrib/<package>/in its profile, and sends each guest whose player is in the session the package's key wrapped for that player alone (the relay'sdistrib_key), then the encrypted pieces it asks for, player to player through thebernard-streamcompanion in transfer mode (--transfer). Nothing in the clear passes the relay; nothing is kept there. Off, the shots are withdrawn from every guest (distrib_revoke: deleted on their computers) and the host's copy deleted.expiryis how long the guests can watch, one of three:end_of_session(deleted when the guest leaves the session or closes its player),48h(the default) or7d; absent, the window's choice. Reading is also closed after 24 hours without contact with the relay, and when the computer's clock went back. In a player with NOVA, each still frame (EXR, DPX, TIFF, PNG, JPEG) goes as its NOVA review copy (.nvfx, HTJ2K with losses, full resolution), made first in the host's profile (phaseconverting), and a movie (.mov,.mp4,.m4v,.mxf,.mkv,.avi,.webm,.mpg,.mpeg,.ts,.mts,.m2ts,.wmv) as one review copy per frame, its pictures only: its sound stays behind, the shot reaches the guests as a sequence (itsfilesare the movie's frames), and the window plays it at the movie's rate and saysNo sound. The originals stay on the host's computer; the original of a movie's frame is a 16-bit PNG the host makes when it is asked. A frame the copy cannot be made for goes as its original file. So does a movie that does not open, fails on the way or is too long (over 50000 frames, or past the package's 100000 files), whileallow_originalsis on; off, that movie is not sent andlinesays so (1 movie not sent: no review copy could be made, and Allow originals is off). A player without NOVA sends every file as its original. Pieces come from the host only (the guests do not pass pieces to each other yet). The window shows images and review copies; a movie received as its original file does not open there (viewer.message:This beta shows image sequences only; this shot opens in a later version.).allow_originals(boolean, optional) is the window's "Allow originals" box, on by default: off, a guest cannot get an original; it can be changed while sending.{"cmd": "distrib-send", "on": true, "expiry": "48h"} {"cmd": "distrib-send", "on": true, "allow_originals": false} {"cmd": "distrib-send", "on": false}Reply:
{"status": "ok", "cmd": "distrib-send", "expiry": "48h"}, or"status": "error"withreason.Confirm a guest (secure distribution) : nothing is wrapped for a guest, and no key, manifest or picture goes to it, before the host confirms its verification code. A guest whose player said hello shows
Waiting for youon the host's line with its 12-character code (distrib-statushost.pending), while the guest's own line readsWaiting for the host. If the host asks, read this code: 8655 d706 a0c8; the host compares the two (by voice, by phone) and clicks Confirm, or sends this command with the guest'sseatand thecoderead (spaces and case do not matter). A confirmation holds for that guest's device key and that package only: a different key in a later hello, even from the same seat, or a new package (Send to guests off and on) asks again. Kept in memory only.{"cmd": "distrib-confirm", "seat": "Guest#a1b2c3", "code": "8655 d706 a0c8"}Reply:
{"status": "ok", "cmd": "distrib-confirm"}, or"status": "error"withreason(The code does not match this guest's,This guest is not waiting for a confirmation,No guest with this seat,Only the host confirms a guest).Open a received shot : a guest opens received shot
shot(its index from 0 inreceived[].shotsofdistrib-status) of packagepackage_idin the received shot's window, as its Open button does. The access is checked first (the deadline, 24 hours offline, the clock); the files are decrypted in memory only, never written in the clear, and the guest's own watermark ({user} {email} {date} {session}, in a mosaic) is always drawn over the picture, whatever the watermark preferences say. The window checks the access again every minute while it is open. Paused at 100 % or more on a frame of a review copy (the window's100%button), the guest's player asks the host the original of that frame; it comes first in the host's queue, sealed like the rest, and replaces the review frame once it is here (the badge saysOriginal..., thenOriginal).frame(from 0) andzoom(fitor100%) pause the window on that frame at that zoom, as the guest would;receive_originalstrue asks the originals of every frame of the shot instead (the shot's right-click menu, Receive originals), slower, the window left as it is.{"cmd": "distrib-open", "package_id": "efe90a8e604a7c840e88d03a67f6b7d8", "shot": 0} {"cmd": "distrib-open", "package_id": "efe90a8e604a7c840e88d03a67f6b7d8", "shot": 0, "frame": 12, "zoom": "100%"} {"cmd": "distrib-open", "package_id": "efe90a8e604a7c840e88d03a67f6b7d8", "shot": 0, "receive_originals": true}Reply:
{"status": "ok", "cmd": "distrib-open"}(the window opens; what it shows is indistrib-statusviewer), or"status": "error"withreasonfor a malformedpackage_idorshot.Distribution status : read-only.
availableis true in an online review;roleishost,guestornone;companionis the transfer companion's state (stopped,starting,running,error,not_bundled). A host getshost:sending,expiry,phase(off,convertingwhile the review copy is made,preparingwhile the pieces are sealed,ready,failedwitherrorin plain words),line(the status line the Session Review window shows, in plain words:Preparing 40%,No guest with the player yet, and, while a guest waits for the host's Confirm,Waiting for you to confirm 1 guest, after the percent when the package is still being made; empty when not sending or when only the guests' own lines have something to say),prepared_percent,allow_originals,review_copies(the files sent as their review copy), the package'sbytes,filesandshots, and one entry per guest whose player said hello, with itslinein plain words (Waiting for youwhilewaiting_confirmis true, whatever its laststate,Waiting,Receiving 42%,Ready,Removed,No system keychain on this computer,Not enough disk space,Cannot reach Sam: still trying.), its laststate(receiving,complete,opened,deleted,refused),haveandtotal(encrypted bytes;completewithhaveundertotalwhen all it misses is a movie withheld by the box) its verificationcode,waiting_confirm(true while its code waits for the host's Confirm: nothing is sent to it before; false whilephaseisfailed: there is nothing to confirm) andlink_unreachable(true while itslineisCannot reach <guest>: still trying.: the player-to-player link has not come up for about 30 seconds and shots are still to come; the attempts go on, and the first link open clears it.errorandlineofhoststay empty: the package is fine);pendinglists those guests (seat,name,code). A guest getsguest:phase(idle,no_keychain,waiting,connecting,receiving,ready,refused,removed), itsline(while waiting,Waiting for the host. If the host asks, read this code: ...with its code),link_unreachable(true while itslineisCannot reach the host: still trying., by the same rule as the host's; each end judges on its own clock),percent(100 only when everything is there),bytes_have,bytes_total,shots_ready,shots_totaland its owncode.receivedlists the packages this computer holds (in or out of a session), their shots (labelas the host named them,files,ready,review_copy, and the frames from 0 whose original is asked,originals_asked, or here,originals_held, andnot_sent, in plain words why the shot will not come, empty when it comes),complete(also true when all that is missing is movies the host withholds, its Allow originals box unticked after the plan: the guest'slinethen readsReady, 1 movie not sent, andpercentstays under 100; ticked again, the package is receiving again),accessat the last check (open,closed_offline,closed_clock; empty before the first),end_of_session,expires_at(Unix seconds, 0 for an end-of-session package) andoriginals_allowed(the host's box).vieweris the received shot's window:open,package_id,shot,watermark(true once its watermark was drawn over the picture),frames,decodedandmessage(why it does not show the picture, in plain words). Never a key, a grant, a token or a path.{"cmd": "distrib-status"}Reply:
{"status": "ok", "cmd": "distrib-status", "available": true, "role": "host", "companion_bundled": true, "companion": "running", "host": {"sending": true, "expiry": "48h", "phase": "ready", "error": "", "line": "", "prepared_percent": 100, "bytes": 52428800, "files": 48, "shots": 2, "guests": [{"seat": "Guest#a1b2c3", "name": "Guest", "line": "Receiving 42%", "state": "receiving", "have": 22020096, "total": 52428800, "code": "8655 d706 a0c8", "waiting_confirm": false, "link_unreachable": false}], "pending": [], "allow_originals": true, "review_copies": 47}, "received": [], "viewer": {"open": false, "package_id": "", "shot": -1, "watermark": false, "frames": 0, "decoded": 0, "message": ""}}
Introspection (debugging & pipeline health checks)
- Status : see Section 4.
RAM Info :
{"cmd": "ram-info"}-- the RAM cache budget and occupancy (cache_bytes,cache_max_bytes), then one entry per playlist item initems[]withindex,path,duration,cached_frames,placeholder_frames(how many of those are the decode pool's 1x1 black stand-ins rather than pictures -- always 0 for a movie) andfill_state: what that item's background preload thread is doing right now ("decoding frame 42","complete at this proxy scale","waiting: viewer is playing/scrubbing/seeking","waiting: no free off-screen fill slot", the states of a long-GOP film neighbor (H.264, H.265: its first seconds are decoded ahead:"long-GOP: first 48 frames in memory","long-GOP: on screen, filled by its playback","long-GOP: waiting, outside the preload window", ...),"no fill thread"). The top-levelviewer_watching,is_playing_state,streaming_seek_requested[_b],scrubbing_tracks,bg_fills_runningandpreload_all_to_ramare the gates those threads wait on. A preload that is not progressing is diagnosed from this one call: if every item says "waiting: viewer..." while nothing is playing, one of the gates is a stuck flag, not a busy viewer.cached_framescounts layer 0 only. Where the whole budget went is inbreakdown[]: every cached entry grouped byclip_id(the cache's normalized path),layer,scale,pixel_type(u8,u16,half,float,bc6h,gpu-packed,yuv-planes,prores-packet),placeholderandfrom_video, withframes,bytes(what the budget counts) andbuffer_bytes(what the pixel buffers hold; equal or a little larger, since the pool hands out the best-fitting idle buffer). Each row carriesitem_index(the playlist item of that clip, or-1) and arole:on_screen(the layer shown of the item on screen),playlist_item(layer 0 of another item),other_layer(any other layer of a listed clip: the paused AOV pre-cache, or a layer shown earlier) orunlisted(a clip no playlist item names, such as track B's own file). Eachitems[]entry addscached_bytes(all layers),other_layer_framesandother_layer_bytes; the top level addscache_bytes_counted(the sum of the rows, which equalscache_bytes),other_layer_bytes,unlisted_bytes,sequence_decodes(real decodes made by the image-sequence decode pool since start, all layers: equal to the number of pictures cached means nothing was decoded twice; a change of tier makes the frames already in hand once more, at the new tier) andsequence_decodes_skipped(jobs the pool dropped because their picture was already in RAM at their tier: none is decoded twice).What the cache is meant to hold for a multi-layer EXR: every frame of the layer on screen, and, while paused, the frame under the playhead of each other layer (nearest layer first) within 10 % of the budget, so switching AOV shows a picture at once. Other layers are evicted before any frame of the layer on screen. In older versions the paused pre-cache walked every frame of every other layer up to 92 % of the budget (18.8 GB for a 0.8 GB, 23-layer, 77-frame sequence).
Track B has its own entry,
track_b, since it is not a playlist row (its sequence is cached under a key of its own, so noitems[]entry sees it).presentisfalseand nothing else is written when B is empty; otherwisekind("video"or"sequence"),path,clip_id(its key inbreakdown[]),duration,layer(the layer B shows),cached_framesandplaceholder_frames(of that layer),cached_bytes(all its layers),fit_share_bytes(what Smart RAM-Fit weighed B at: its whole clip at its tier;0while the fit has not weighed this clip yet, cannot, leaves a long movie streaming, or gave B another tier than the one its sequence decodes at -- the tier of the clip on A),fit_budget_bytes(the budget the fit shares out),shown(B is painted in the view in force) andfill_state. B fills like A: its movie by a coarse-to-fine fill thread of its own (the same states as above), its sequence by the paused walk of the render loop, which for the sequence on track A is also whatitems[]now reports as that item'sfill_state:"filling coarse to fine, stride 8"(the coarsest stride with a frame still missing, then 4, 2, 1;"filling coarse to fine: around the playhead first"before that),"complete at this proxy scale","following the playhead (playing)","waiting: RAM soft limit reached","waiting: the frames left rank below what RAM holds"(a film's fill),"waiting: track B holds its Smart RAM-Fit share"and"waiting: yields to track A (B not on screen)". The painted track goes first; in A alone B's requests never jump the decode queue, B (its look-ahead around the playhead included) takes only what A leaves of each tick, and B asks nothing while A plays (its movie fill, like any fill off screen, also waits out a scrub or a seek). B never passesfit_budget_bytesin the whole cache norfit_share_bytesof its own, and a frame of B never evicts one of A. A paused movie on B is read past the look-ahead (48 frames past the playhead) too, without waiting for playback. A sequence on B that is a playlist row's reads the frames the row has decoded:shared_fromis the row's key B reads them from (nullfor none: B not a row's media, or a film, whose key is its path, the row's own),shared_frameshow many ofcached_framesare the row's;cached_bytescounts only what B holds under its own key, so a row on B is held once.{"cmd": "ram-info", "frames": true}addscached_frame_numbers(sorted) to everyitems[]entry and totrack_b, to watch the coarse-to-fine order from outside by polling.Layer Info : Reports the AOV/layer list for a track (0 = A, 1 = B).
{"cmd": "layer-info", "track": 0}System info : read-only. What Help > About > Copy System Info puts on the clipboard, for a bug report: the build, the machine, the GPU and the libraries the player runs on, as JSON. Each value is a string, written as the block writes it,
"unknown"when it could not be read; a library or an SDK not built in says"absent".- Reply:
{"status":"ok","cmd":"system-info","product":"Bernard Review","version":"1.0.0NNN","commit":"0a1b2c3","build_date":"2026-09-28","compiler":"MSVC 19.44.35217","os":"Windows 11","os_version":"10.0.26200","gpu_vendor":"NVIDIA Corporation","gpu_renderer":"NVIDIA GeForce RTX 4090/PCIe/SSE2","gl_version":"4.6.0 NVIDIA 566.36","vram":"24564 MB (GL_NVX_gpu_memory_info)","cpu_cores":"32","ram":"63.7 GB","ffmpeg":"n7.1","libavcodec":"61.19.100","libavformat":"61.7.100","libavutil":"59.39.100","ffmpeg_license":"LGPL version 2.1 or later","ocio":"2.6.0","ocio_config":"none (built-in color pipeline)","nova":"compiled in, format 1.0","openexr":"3.4.15","openimageio":"2.5.18","r3d_sdk":"absent","braw_sdk":"absent","arri_sdk":"absent","tree_modified":true,"vram_mb":24564,"ram_bytes":68401860608,"text":"Bernard Review 1.0.0NNN\nCommit: 0a1b2c3\n..."}. The keys, in the block's order:commit(the short commit the player was built from, followed by(modified)only when the source tree had uncommitted changes at that build;"unknown"for a build without git, such as one from a source archive),build_date(the daymain.cppwas compiled),compiler,osandos_version,gpu_vendor,gpu_rendererandgl_version(OpenGL'sGL_VENDOR,GL_RENDERERandGL_VERSION),vram(the dedicated video memory fromGL_NVX_gpu_memory_info, or the free texture memory fromGL_ATI_meminfo, which saysfree;"unknown"without either),cpu_cores(logical processors),ram(physical memory),ffmpeg(av_version_info()),libavcodec,libavformat,libavutil,ffmpeg_license(avcodec_license()),ocio(the OpenColorIO library the player links,"2.6.0";"absent"in a build without OpenColorIO 2.5) andocio_config(the studio config loaded with Color > Load OCIO Config... or the built-in ACES 2.0: its file name -- never its path -- or"ACES 2.0 (OCIO built-in)", the config's own name, and its counts (the picture is drawn through it,status.ociosays whether and with which view);"none (built-in color pipeline)"without one; a load in flight or a failure added after;),nova("compiled in, format 1.0", or"not compiled in"in a player built without NOVA),openexr,openimageio,r3d_sdk,braw_sdkandarri_sdk(a version,"present, version unknown"or"absent"). Beside them:tree_modified(a boolean),vram_mbandram_bytes(the two sizes as numbers;nulland0when unknown), andtext, the block itself, character for character what the button copies:Bernard Review <version>on its first line, then oneLabel: valueline per key above, in that order. Nothing in it names the user, the machine, a folder or an address, and nothing of the media open: no key reads them, and every value is cleaned first -- a path becomes its last component (a configuration's file name), the user's, the machine's and the domain's names become<redacted>, an IPv4 address<ip>.
{"cmd": "system-info"}- Reply:
Send log window: opens Help > Send Log to the Bernard Team... as the menu does, ticks or unticks its crash report, writes its "What happened?" note (4,000 characters at most, cut beyond). It never sends: only the user's own click on the window's Send does, so a script cannot send a user's logs (their paths and lines name shots). A press of Send that IPC injected (
debug-mouse, a key or text typed for a script, and a few frames after) is refused and counted insend_log.injected_sends_refused.open(bool),include_crash(bool) andnote(string) are each optional; any other key is ignored.- Reply:
{"status":"ok","cmd":"send-log","send_log":{...}}, the same object asstatus'ssend_log.
{"cmd": "send-log", "open": true, "include_crash": false, "note": "It closed when I changed shot."}- Reply:
3. Event Reference (Callbacks)
When an artist interacts with Bernard Review (e.g. presses the Spacebar, drags the timeline, or loads a new playlist), the player automatically broadcasts JSON lines to all connected TCP clients. You can listen to these events to trigger actions in your script.
CDL file read (
cdl-loaded) : Sent once aset-cdlwith afilehas been read in the background (OCIO step 5):oktrue withcdl, the row's CDL as a.bplitem writes it, orokfalse withreason(the file's name, never its folders). Only to the command'sfile; Color > Load CDL... says its result in a notice.{"event": "cdl-loaded", "row": 2, "ok": true, "cdl": {"slope": [1.06, 1.0, 0.94], "offset": [0.01, 0.0, -0.01], "power": [1.0, 1.02, 0.98], "saturation": 0.9, "id": "shot010", "source": "seq1.ccc"}} {"event": "cdl-loaded", "row": 2, "ok": false, "reason": "seq1.ccc: line 12: ColorCorrection \"shot010\": the red slope -1 is not between 0 and 100"}View Changed (
view-changed) : Sent when what the viewer shows changed -- the View control, its key, View in the menu bar,view, the contact sheet's own ways in (contact-sheetlayout_view, its key), or a layout left with nothing to show (Edit when the playlist empties) going back to the picture.- Property:
layout("picture","contact-sheet","edit","steps").
{"event": "view-changed", "layout": "contact-sheet"}- Property:
Playback Started (
play) : Sent when the player starts playing.{"event": "play"}Playback Paused (
pause) : Sent when the player is paused.{"event": "pause"}Playhead Moved (
frame_changed) : Sent once per frame of the render loop whenever the playhead is on another frame than at the last broadcast -- as the video plays, when the user drags the timeline, and after an explicit jump (a click on the ruler, a key,seek). There is no separateseekevent: a jump is oneframe_changed.- Properties:
frame(int - the playhead, asstatus'splayback.currentFramereports it andseektakes it),display_frame(int - the number the ruler shows for it,seek'sdisplay_frame).
{"event": "frame_changed", "frame": 23, "display_frame": 1024}- Properties:
Export Finished (
export-done) : Sent once when anexportjob -- or a whole playlist batch -- ends, whether it succeeded, failed or was canceled (export-cancel).export-statusholds the full result afterwards.- Properties:
ok(bool),cancelled(bool),error(string, empty on success),frames_written(int),output(the first job's output),jobs(how many jobs the batch held;1for one job),seconds(float, the export's duration),encoder_options(array of"name=value", what that encoder opened with, seeexport-status),encoder(string: a movie's libavcodec encoder,h264_nvenc,h264_mf,prores_ks...; the last clip's for a batch;""for a sequence).
{"event": "export-done", "ok": true, "cancelled": false, "error": "", "frames_written": 120, "output": "/renders/SQ010_SH0010_v003.mov", "jobs": 1, "seconds": 14.2, "encoder": "prores_ks"}- Properties:
Review Changed (
review-changed) : Sent when the review of one or more rows has changed and then stayed put for 0.8 s -- a status set, a general note typed, a stroke drawn, moved, recolored or erased, by any means (the playlist panel, the contact sheet, a shortcut, the Console, a session's pilot) (ReviewWatchLogic.h). A stroke being drawn is one event once the pen has lifted, not one per frame; a change undone before it settles sends nothing. A playlist loaded with its statuses and notes (load-playlist) is not a review change, the same playlist loaded again over the same rows included: it sendsplaylist-changed. Nothing is scanned to find the rows (manu/review-watch-events): the places that change a review mark their rows -- a stroke its own row --, and a row is read once, when it has settled; a change made without naming its row (a status set from a menu, a grade) has every row read once then. With nothing changing, the player reads no row at all.- Properties:
rows, one per row that changed:index(0-based),path,status("Pending","Approved","Not Approved", asexport.jsonwrites it),note,annotated_frames(the frame indices carrying strokes, from 0, asexport-notesnames their JPGs) and, when the row has one,x_trackingas the.bplwrites it (a pipeline's own keys included).
{"event": "review-changed", "rows": [{"index": 0, "path": "D:/SQ010/SH0010/SQ010_SH0010_Anim_v012.mov", "status": "Not Approved", "note": "Elbow pops at the end of the jump.", "annotated_frames": [5, 20], "x_tracking": {"shot_id": "item-1", "task_id": "task-1", "bernard": {"file_id": "file-1", "version": 12}}}]}- Properties:
Playlist Changed (
playlist-changed) : Sent when the playlist's rows changed -- added, removed, reordered, a playlist loaded (the same one again included) or cleared, a row relinked or given its downloaded media, the last session's rows restored. The rows are taken as they are once they have stayed the same for 0.25 s -- rows added one by one, a session restored, are one event --: noreview-changedfollows for what they held when they came, nor for a review change still settling then.- Property:
rows(int - how many rows the playlist now has).
{"event": "playlist-changed", "rows": 12}- Property:
- Open Link (
open-link) : Abernardreview://link ended or waits:{"event": "open-link", "ok": true, "state": "opened", "items": 5, "reason": ""}.state:opened(the clips loaded, the clicked version -- Kitsu: the first task's preview -- on screen),waiting(the playlist was not empty: the Tracker window shows them with Replace / Append),refused(a bad link, or a site not set up: nothing was sent) orfailed(no connection, or the site gave nothing).reasonnames at most the link's host, never its user. Notes File Exported (
notes-file-exported) : An export to one file ended (export-notes-file, or File > Export Notes as CSV / HTML..., Finish review's export):path,format(csv,html),ok,error(""when written),complete(every picture of the review is in it) andmissing(each picture that is not, named:"shot1, frame 1010: no picture"). The "Exported" box shows the same.{"event": "notes-file-exported", "path": "D:/reviews/Review_Playlist_1_2026-10-16.html", "format": "html", "ok": true, "error": "", "complete": true, "missing": []}Review Exporting (
review-exporting) : Sent once, at the close of a player started with--review-dir <dir>, as soon as its window hides -- before the close's waits, the profile save and the export itself -- so a pipeline can show its own wait at once. Only when an export follows:review-exportedalways comes after it (within five minutes of the close). Not sent for an empty playlist (nothing is exported, noreview-exportedeither).- Properties:
dir(the folder, as given);pictures(how many annotated frames' pictures the export is to write: the frames with at least one stroke, on rows that have a media path -- the framesexport.jsonlists (a file missing on disk is counted, then said missing,"no picture");0is possible, the export still follows).review-exportedthen says which of them are missing and why. They are written into<dir>.partmeanwhile: counting the.jpgfiles there gives an idea of the progress (the last one may still be being written).
{"event": "review-exporting", "dir": "C:/Users/me/AppData/Local/Temp/bernard-review/session-42", "pictures": 5}- Properties:
Review Exported (
review-exported) : Sent once, at the close of a player started with--review-dir <dir>, after the review notes were written into that folder (export.json,export.htmland the annotated frames' JPGs, asexport-noteswrites them), and before the IPC server stops. Whatever closed the player -- its window orquit-- the review is there, and the close asks no "save your review session?" question. It comes within five minutes of the close: past 280 seconds of export, it is sent with what was written. Not sent for an empty playlist (nothing to write).- Properties:
dir(the folder);complete(true whenexport.json,export.htmland every annotated frame's picture are in the folder);published(every file written is indir; false when the folder could not be made, when a file could not be moved there -- it stays in<dir>.part-- or when the close stopped waiting for the publication, which may then still end);written(this export's files indir, in the order they were written:export.jsonfirst -- when it is listed, the decisions are there);missing(each annotated frame without its picture indir:row,shot,frame,display_frame,image-- the namesexport.jsongives it -- andreason:"no picture"it could not be decoded,"not written"its file or the folder could not be,"time limit"the close stopped waiting before it,"not published"it stayed beside the folder,"same name"an earlier picture of the review has its file name).completefalse withpublishedtrue: the decisions inexport.jsonare there, some pictures are not.
{"event": "review-exported", "dir": "C:/Users/me/AppData/Local/Temp/bernard-review/session-42", "complete": false, "published": true, "written": ["export.json", "shot_1_frame_1001.jpg"], "missing": [{"row": 3, "shot": "shot 4", "frame": 12, "display_frame": 1013, "image": "shot_4_frame_1013.jpg", "reason": "time limit"}]}- Properties:
Review Finished (
review-finished) : Sent once when the Finish Review flow ends: the review is published to the site connected (the summary, "Review published"), or, with no site connected, nothing is published and the notes' export is offered.published: falsesays only that, not that a file was saved: the playlist's.bplis written again when it has one; without one the save chooser opens and this event goes without waiting for its answer, so nothing may have been saved. Not sent while it only asks (the confirmation, the wait for the note-takers, the name), nor when the publication fails (Retry ends it, then it is sent); sent once per time the flow ends, with or without--review-dir(the close that follows isreview-exportingandreview-exported's, if asked). It names no person and no shot.- Properties:
published(true: a site received the review; false: nothing was sent, the export was offered);notesandstatuses(what the summary says was sent: notes with their drawings, and statuses alone;0whenpublishedis false);not_linked(the shots the run could not send, not linked to the site connected;0whenpublishedis false).
{"event": "review-finished", "published": true, "notes": 2, "statuses": 3, "not_linked": 0}- Properties:
Media Request (
media-request) : Sent when someone plays a version of the Version Board that is not on this computer (build-matrixwith"available": false): its cell, track A's Versions menu, a version step of track B (cycle-b, Up / Down) orplay-version. Nothing is loaded; the pipeline copies the version and answersmedia-ready. Once per version until that answer: playing it again meanwhile sends nothing new (the last track to ask gets it).- Properties:
version_id(whatmedia-readynames),sequence,shot,department,version(its name on the board),track("A"or"B": where it is to play),x_tracking(its ids, as a.bplrow writes them).
{"event": "media-request", "version_id": "f42:v010", "sequence": "SQ010", "shot": "SH0010", "department": "Comp", "version": "v010", "track": "A", "x_tracking": {"shot_id": "item-1", "version_id": "v010", "bernard": {"file": "f42", "version": 10}}}- Properties:
Earlier Notes Request (
earlier-notes-request) : Sent when someone unfolds a row's "Earlier notes" section (Essential's inspector, under Notes; a card of Review Notes in Complete) while Bernard Engine is connected, on a row carrying the pipeline's ids (x_tracking; a tracker connector's row has no such section); and on its "Show Older" (manu/earlier-notes). A row is open while its section is unfolded in its Review Notes card, or while it is the shot Essential's unfolded section shows, Essential's inspector drawn (another mode, or the panel closed, closes it). While the playlist plays, a shot whose notes were already read is shown as kept and not opened again (playing through the playlist asks each shot once); the shot playback stops on is opened. Opening a row sends the request withcursor"", and tells the pipeline to push that row's newest page again, unasked, on its own change events (see theearlier-notescommand), untilearlier-notes-closed. A section unfolded again later is asked again, with the samekeyin the same playlist: the pipeline stopped pushing while it was folded, and the cards already shown stay while the answer is awaited (no "Loading" line). Nothing more is sent for a row while its answer is awaited (30 seconds at most). A request with a non-emptycursor("Show Older") does not change what is pushed. Never at launch, never while folded. The pipeline answers with theearlier-notescommand.- Properties:
key(the player's name for that row's earlier notes in this playlist, at most 64 bytes: the answer gives it back; the same for its "Show Older"),row(the row's index, from 0, when asked),cursor(""for the newest page, else thenext_cursorof the last answer, as it came),limit(20: the most cards an answer may hold),x_tracking(the row's ids, as a.bplrow writes them).
{"event": "earlier-notes-request", "key": "en-1", "row": 3, "cursor": "", "limit": 20, "x_tracking": {"shot_id": "item-1", "task_id": "task-1", "bernard": {"file_id": "file-1", "version": 12}}}- Properties:
Earlier Notes Closed (
earlier-notes-closed) : Sent when a row is open nowhere any more -- folded in its Review Notes card and not the shot of Essential's section, Essential's section folded or moved to another shot -- or when the playlist forgets it (a playlist loaded or cleared): the pipeline stops pushing for those keys. One event for all the rows closed at that moment. Nothing is sent when the IPC client leaves: that ends every watch.- Properties:
keys(an array of one or more keys, the ones the requests gave).
{"event": "earlier-notes-closed", "keys": ["en-3", "en-7"]}- Properties:
4. Status Query Details
By sending {"cmd": "status"}\n, you can query the complete state of the player. This is useful for initializing a UI or syncing client state upon connection.
Status Response Example
{
"status": "ok",
"version": "1.0.0NNN",
"public_version": "1.0",
"theme": "sombre",
"theme_requested": "sombre",
"theme_system": null,
"theme_rebuilds": 0,
"theme_font_builds": 0,
"playback": {
"state": "playing",
"currentFrame": 23,
"reverse": false,
"startOffset": -1,
"displayFrame": 1024,
"firstFrameNumber": 1001,
"defaultStartFrame": 1001,
"timecode": "00:00:42:16",
"dropFrame": false,
"fps": 24.0,
"ab_mode": false,
"ab_view": "wipe",
"loop_iterations_missed": 0,
"dropped_frames": 0,
"frames_due": 1440,
"frames_on_time": 1440,
"late_frames": 0,
"lost_frames": 0,
"longest_iteration_ms": 17.7,
"presented_fps": 24.0,
"late_tolerance_ms": 25.0,
"display_hz": 60,
"related_clips": {"count": 4, "fingerprint": "9c1e07a2", "computes": 3}
},
"decode": {
"valid": true,
"ms_per_frame": 117.4,
"avg_decode_ms": 117.4,
"samples": 12,
"scale": 0.25,
"newest_age_s": 0.04,
"track_b": {"valid": false, "ms_per_frame": 0.0, "avg_decode_ms": 0.0, "samples": 0, "scale": 0.0, "newest_age_s": 0.0},
"budget_ms": 41.67,
"too_slow": true,
"losing_frames": true,
"warning": true,
"slow_decode_tier": "auto",
"rule_active": true,
"filling": false,
"base_scale": 0.25,
"tier_cap": 1.0,
"lowered": false,
"last_change": null,
"read_a": {"failing": false, "failures": 0, "frame": -1, "seconds": 0.0, "error": "", "unreadable": false, "last_event": null, "frames": 814},
"read_b": {"failing": false, "failures": 0, "frame": -1, "seconds": 0.0, "error": "", "unreadable": false, "last_event": null, "frames": 0},
"counts": {"sequence": 48, "sequence_served_from_ram": 0, "video_a": 814, "video_b": 0, "video_fill": 0, "video_b_held_skips": 0, "video_b_codec_frames": 0}
},
"color": {
"exposure": 1.25,
"gamma": 2.2,
"grade": {"exposure": 1.25, "gamma": 2.2, "rgb": [1.0, 1.0, 1.0], "white_balance_temp": 0.35,
"white_balance_tint": -0.2, "offset": [0.05, 0.0, 0.0], "saturation": 1.0, "mask": 0,
"input_space": 1, "display_space": 1, "...": "the other keys of a .bpl item's grading"},
"channel": "RGBA",
"channel_b": "RGBA",
"channels_linked": true
},
"audio": {
"volume": 0.8,
"muted": false
},
"viewport": {
"decoded_width": 4096,
"decoded_height": 2160,
"native_width": 8192,
"native_height": 4320,
"source_width": 8192,
"source_height": 4320,
"retiering": false,
"frame_a": 120,
"frame_b": -1,
"proxy_idx": 1,
"proxy_scale": 0.5,
"zoom": 1.0,
"fit_mode": true,
"fullscreen": false,
"timeline_reveal": 1.0,
"picture_rect": [744.2, 50.0, 1951.6, 1340.0],
"ab_labels": []
},
"track_b": {
"path": "D:/shots/sh010/plate/sh010_plate.1001.exr",
"is_video": false,
"frames": 96,
"edit_target": 1,
"wipe_position": 0.3,
"wipe_angle": 90.0,
"exposure": 1.5,
"session_restore": "restored",
"pinned": true,
"last_switch": "kept",
"from_playlist": {
"row": 1, "how": "menu", "dragged_row": -1, "over_b": false, "menu_entry": null,
"track_b_row": [12.0, 986.0, 1908.0, 1019.0],
"cache": {"key": "seq_b_d:/shots/sh010/plate/sh010_plate.1001.exr",
"shared_from": "d:/shots/sh010/plate/sh010_plate.1001.exr",
"frames": 96, "own_frames": 0, "shared_frames": 96, "own_bytes": 0.0}
},
"version": "v002",
"versions": [
{"name": "v001", "path": "D:/shots/sh010/comp/sh010_comp_v001.1001.exr", "current": false},
{"name": "v002", "path": "D:/shots/sh010/comp/sh010_comp_v002.1001.exr", "current": true},
{"name": "v003", "path": "D:/shots/sh010/comp/sh010_comp_v003.1001.exr", "current": false}
],
"version_prev": "v001",
"version_next": "v003",
"version_preload": {
"enabled": true,
"cap_bytes": 3972844748,
"other_layers_bytes": 0,
"held_bytes": 21233664,
"versions": [
{"version": "v001", "frame": 2, "resident": true, "bytes": 10616832},
{"version": "v003", "frame": 2, "resident": true, "bytes": 10616832}
]
},
"last_version_step": {"from": "v001", "to": "v002", "direction": "next", "preloaded": true, "ms": 1.5}
},
"ram_fit": {
"common_tier": 0,
"common_scale": 1.0,
"ceiling_px": 0,
"fits": true,
"status": "fits",
"suggested_fixed_tier": -1,
"unmeasured_count": 0,
"estimated_bytes": 1048412160.0,
"budget_bytes": 1073741824.0,
"streamed_video_seconds": 120.0,
"track_b": {
"present": true,
"path": "D:/shots/sh020/sh020.1001.exr",
"in_playlist": false,
"proxy_idx": 4,
"effective_proxy_idx": 1,
"effective_proxy_scale": 0.5,
"estimated_bytes": 254803968.0,
"streamed": false,
"measured": true,
"resident": 12,
"shares_row": -1,
"shares": false,
"own_bytes": 0.0
}
},
"playlist": [
{
"index": 0,
"path": "D:/shots/sh010/sh010.1001.exr",
"proxy_idx": 4,
"effective_proxy_idx": 1,
"effective_proxy_scale": 0.5,
"estimated_bytes": 199065600.0,
"streamed": false,
"measured": true
}
]
}
Response Properties
version: the build, whole ("1.0.0NNN", a three-digit counter): the counter the installer's name, the program's properties,system-info, the log and the update check use.public_version: the version the user is shown ("1.0"; About saysVersion 1.0 (build NNN)). Both come from CMakeLists.txt;versionalways starts withpublic_version.update: the check at launch --state("idle","off","running","up_to_date","available","failed"),checked,latest(the newer build the manifest names, compared on the build),available,ignored(the build Later set aside),policy("on","off_user","off_env","off_system","off_scripted"),setting,manifest_url,test_manifest,error;banner(show:"shown","held_by_session","hidden";text, the words drawn,"A new version of Bernard Review is available", with no number; the boxesrect,download,whats_new,later,[x0, y0, x1, y1]ornull);notes(state:"loaded","unavailable","none";bytes,cut,window_open,title(the What's new window's title as drawn,""while it is closed),unavailable_shownand the boxesdownload,later,link). The What's new window's title names the release's public version when the manifest gives one ("public_version": "1.1", in the platform's block or at the top: "What's new in Bernard Review 1.1"), never a build.playback.state:"playing"or"paused".playback.currentFrame: The frame index currently on screen (0-based: whatseek'sframetakes).playback.reverse: the direction of play,truebackward (reverse-play,J, or the way back of the Ping-Pong loop,set-playlist-playback'sloop_mode3); kept while paused.review_context: The review context of the shot on screen, as Essential's line over the picture shows it:project,sequence,shot,version,department,artist,index(its row) andgiven(the fieldsset-review-contextset; the others are read in the file);nullwithout a shot on screen.technical_check: The automatic technical check of the shot on screen, read in the background from the frames already in the RAM cache (never decoded for it, paused while playing):lines, the banner's lines in order (text,kind-- 0 missing frames, 1 black or empty, 2 NaN or infinite pixels, 3 unlike both neighbors, 4 clipped whites, 5 crushed blacks --,frame, the 0-based frame a click goes to,first_numberandlast_number, the numbers shown),analysedandframes(the shot's frames read so far and its length),idle(the check waits: done, paused or nothing cached yet) anddismissed(the banner's cross closed it until the next shot).playback.startOffset: The start typed for the shot on screen (set-start, a double-click on the ruler),-1when it has none.playback.displayFrame: The number the ruler shows for that frame: the shot's first number plus the index.playback.firstFrameNumber: The shot's first number: its typed start, else the number in a sequence's file names, elsedefaultStartFrame.playback.defaultStartFrame: Preferences > Default Start Frame, the first number of a movie or of a sequence whose files carry no number (1001 unless changed; profile keydefault_start_number).playback.timecode: The timecode ofdisplayFrameas the ruler writes it, counted from the number (no drift at 23.976, no step back at 29.97);HH:MM:SS;FFin drop frame.playback.dropFrame: Whether that timecode is drop frame: what the file says (a MOV's or an MXF's timecode track, an EXR's timeCode), else Preferences > Playback > Drop-frame timecode at 29.97 and 59.94 (profile keytimecode_drop_frame); only at 29.97 and 59.94.playback.fps: The playback speed in frames per second.playback.transitioning:truefrom a change of clip (open,playlist-play, a click in the playlist) until the new clip's first frame is on screen. It does not cover a proxy-tier change (viewport.retiering).playback.viewport_hidden:truewhile the viewport draws no picture because the one it holds would be the wrong one: during a change of clip, and after a change of layer (set-layer, the AOV list) until a frame of the new layer arrives. A clip change raises both flags; a layer change raises only this one. A script that waits for the new picture to be up waits for both to befalse.theme: the display mode in use,"sombre"or"clair"(never"systeme", which is drawn as one of the last two);theme_requested: the mode asked for (set-theme, the mode button, Preferences),"systeme"included, in use from the next frame;theme_system: the operating system's light or dark setting as last read ("light","dark","unknown"when it gives no answer),nullwhile System has not been asked for;theme_rebuilds: how many mode changes were applied since the start;theme_font_builds: how many of them built the skin's font atlas again (a change between Dark and Light builds none);theme_button_rect: the mode button's box[x0, y0, x1, y1]as the last frame drew it (in Dark and Light the display button's), in the screen coordinatesdebug-mousetakes, ornullwhile it is not drawn (full screen).view_control: in Dark and Light, the menu bar's display button and its popover as the last frame drew them (see Display mode above:form,label,rect,window_buttons,margin_to_window_buttons,hovered,tooltip,popover);nullin Classique and in full screen.classique_mode_button: alwaysnullsince the Classique mode was removed (2026-10-06; the key goes with the interface's clean-up). It was, in Classique, the mode button against the window buttons' zone it keeps clear, as the last frame drew it:rect[x0, y0, x1, y1],window_buttons[x0, x1](the player's Minimize, Maximize and Close, 130 px at the bar's right end on every system),margin_to_window_buttons(pixels; 8 x the interface scale under Windows, as always, 24 x the scale under Linux and macOS) andrule("windows"or"linux_macos", this build's);nullin Dark and Light, in full screen and on the first frame.window: the main window as GLFW has it --size[w, h](the client areaset-window-sizesets),framebuffer[w, h],pos[x, y],maximized,fullscreen,fullscreen_monitor(in full screen, the monitor it went on:{"rect": [x, y, w, h], "scale"}, its pixels and its content scale read at the switch;nullotherwise),work_area[x, y, w, h]of its screen (ornull) -- and the interface scale:ui_scale(the setting),ui_scale_percent,font_scale_percent(the scale the fonts and the style were built at, at start) andrestart_for_fonts(they differ: the fonts follow at the next start).identity: the profile's own name and e-mail (profile_name,profile_email: what Edit > Preferences... > General's Name / E-mail line shows and saves,""when not set), the ones in use (name,email: what the watermark prints and a session shows; the defaults or a signed-in account's where the profile has none) and that line's fields as the last frames drew them (name_field,email_field:[x0, y0, x1, y1],nullwhile the line is not on screen).wipe: the A|B wipe as the last frame drew it on the picture,nullwhile it is not shown (A/B off, A alone, B alone):swipeandangle(the shader's),line([x0, y0, x1, y1]on screen, within the picture),handle([x, y]: where the diamond or the "A|B" circle is drawn and grabbed -- the handle's persistent pivot, which the double-click turns the line through and the right-drag turns it round),handle_to_line_px(its distance to the line),pivot_sampled([u, v]in the texture's square: it follows zoom, pan, flips and quarter turns),picture([x, y, w, h]),flip_h,flip_v,rotation,dragging(a left press on the line),moving(past the drag threshold: the line follows) androtating(a right-drag).session_bar_node: the Review Session window's dock node and its session bar, as the last frame drew them:docked(the visible tab of a node split top / bottom; the rule applies only then),header_px(from the node's top to where the window's content starts, as drawn in the mode in use),bar_px(42 x the interface scale),below_px(the window's bottom border),required_px(their sum, whole pixels up: the node's least height),node_px(its height),due(the minimum is to be given: at start, at a new interface scale or display mode),applied(how many times a height was given since the start) andset_px(the last one),bar_rectandbar_shown([x0, y0, x1, y1]: the bar's box and the part of it its window shows),cut(its lower part hidden) andcut_px. The window has no title row (header_pxis0while it is alone in its node) andend_buttonsgives the bar's own buttons at its right end,copyandpin([x0, y0, x1, y1],nullwhen not drawn: Pin only in auto-hide and fullscreen).watermark: the viewer watermark. Its settings:enabled,type("text"or"logo"),has_logo,source("user_info"or"custom_text"),custom_text(as typed, tokens and all),pos_mode("fixed","floating"or"tiled"),cell(0 to 8, the 3 x 3 grid row by row) andcell_label("Bottom Left"),fixed_pos(the five places of before, 0 to 4, kept for an older player),scale,opacity,tile_angle(degrees),tile_spacing(in mark heights),legibility("off","outline"or"shadow") andpresets(their names). What the last frame drew:drawn(false while it is off, or while a floating mark is hidden),text(the text on the picture, its tokens expanded;""for a logo),rect([x0, y0, x1, y1]of a fixed or floating mark,nullwhen tiled),tiles(the mosaic's marks),tiles_truncated,tile_area(the part of the picture the mosaic covers),tile_measuresandtile_layouts(how many times the mosaic's text was measured and its marks laid since the start: they do not grow while nothing changes).button: the W button of the transport row,nullwhen it was not drawn.popover(Dark and Light, the W button's right-click):{"open": false}while it is closed; open, the boxes ofwindow,enable,types(Text, Logo),sources(User info, Custom text),text,modes(Fixed, Floating, Tiled),cells(the nine),angle,spacing,scale,opacityandlink("All watermark settings..."); a box not drawn isnull.color_panel: the Color panel (Color Panel, under Color & Grading):target("A","B","A+B"),before_afterandbefore_after_applies(on, and cutting the grade this frame: in Complete),picker("black","grey","white"ornull),pick_whyandauto_why(why the pickers or Auto cannot run now,""when they can),presets(their names, in order),presets_folder,notice(what the last picker, Auto or preset did),lut_name(the.cubeloaded,""for none), anddrawn: where the panel drew each control on its last frame,[x0, y0, x1, y1]indebug-mouse's coordinates (targets,before_after,menu,reset_all(the options' Reset Grade, Reset All Grading's action),channels,pixel_aspect,scope_tabs,scope,stages-- by key, eachrow,eye,resetandopen--,fields-- by key --,levels_histogram,levels_handles,pickers,levels_auto,lut,load_lut,clipping,stack,footer; andoptions_lines(the lines the options' line took: 1, or 2 in a narrow panel),"pipeline"(the Color pipeline block, framing Input and Output) andgroups-- by key,balance,tone,color,curves_gamma:heading,folded,dot(a stage of the group not at rest),summary(the line the group shows folded);nullfor a control not drawn),nullwhile the panel is not drawn (Classique, another tab, Essential);groups_folded(the keys of the groups folded, the user's preference, kept insession_config.jsonundercolor_panel, never in the grade); andcurves(Set Curve):window_open(the Curves window),focus(the curves editor has the keyboard focus: Delete and Backspace are its own, the playlist's Delete stands aside),uploads(how many times the curves' table went to the GPU: when a curve changed),histogram_samples(the pixels behind the editor's histogram, 0 while no editor is drawn), anddrawn-- the editor's last frame:graph,tabs(RGB, R, G, B),points(the channel's points),reset,input,output, boxes as above -- ornullwhile no editor is drawn; andhue_ranges(Set Hue Range):window_open(the Hue Ranges window),selected_panelandselected_window(the range chosen in the panel's Hue & Saturation stage and in the window:"global","reds"...), anddrawn-- the view's last frame:selector,reset,reset_all,sliders(Hue, Saturation, Lightness;nullfor one not drawn),before,after(the bars), boxes as above -- ornullwhile no view is drawn.scopes: the color scopes' window and measures (Color Scopes, under Color & Grading):open,kind,visible,hz_max,captures,computes,results,gl_resources,worker,source,readback,last_compute_ms,avg_compute_ms,render_cost_us,latency_ms,tabs,plot,placeholder,black_label,white_label,probe,probe_host,readout,block(the value under the pointer and the Color panel's scope block). Whilevisibleisfalsenothing is read back:gl_resourcesandworkerarefalseandcapturesstays as it is.session_ui: the review-session screens as the last frame drew them, in the screen coordinatesdebug-mousetakes. No command changes with them, and nothing in this block is a password: a session's password is never written instatus, in a reply, in the log or in a link (password_setinsession-createsays whether there is one).session_ui.bar: the session bar,nulloutside a session or while the bar is not drawn.frameis the frame it was drawn on;role"host"or"guest";titlethe session's name (a guest, who is never told it, sees the session id);subtitlethe second line as shown,"00:42:17 · LAN"or"… · online · 1080p"while the viewport is streamed;bannerthe host's"Guests can't see your picture: …"(the same assession_ui.stream_menu.host_banner), drawn in place of the subtitle while it is not"";transport"lan","vpn","online"(a relay),"direct"or"this_computer";syncthe chip:state("following","leading"-- this seat is the pilot --,"waiting","free"-- Local Review),labelas shown ("Following AD";"Local Review — not broadcasting"for the pilot in Local Review, which sends nothing),padlock,rect;avatars: how many faces areshown(three at most) and how many the"+N"disc stands for (overflow,overflow_label),rectthe stack;room("7 in the room"),hands("1 hand up",""for none) andhands_rect;name_rect, the name that opens the session menu;mic_muted,hand_raised,chat_open,chat_unread(messages from others since the chat was last open);buttons: the right-hand buttons in their order, each{"id", "text", "rect"}--"mic","chat","hand", then"invite"and"end_session"for the host,"leave"for a guest.raised_hand(the host's;nullwhile no hand waits and nothing is open):frame;noticethe one-line notice of the first raised hand,{"for", "text", "rect", "give_control", "not_now"}("Marie raises a hand", the boxes of its two buttons), drawn in place ofhands, elsenull;handseach yellow hand drawn as a button,{"id", "rect"}(a click asks "Give control to Marie?");confirmthat question while it is open,{"for", "text", "yes", "no"}, elsenull. Yes passes the baton; No sendshand_requestlower_hand(docs/online-review-messages.md 6e).session_ui.create: New review session while it is open, elsenull.choices: who can join, each{"id", "label", "rect"}--"anyone"(Anyone with the link) and"password"(Link + password); "Invited people only" is not offered while the relay does not know it;policy: the one chosen;password_typed/password_shown: whether the field holds something and whether Show is on (never what it holds);identity_from_account/editing_identity: the card still follows the signed-in account / its Change has opened the two fields;invite_tags: how many addresses will be invited when the session starts;advanced_open;start_enabled, andstart_blocker, why not ("Type or generate a password, or choose Anyone with the link.");rects:start,cancel,change,show,generate,advanced;network_profile: the category Windows gives the connected networks, read when the dialog opened ("public"as soon as one is Public, else"private","domain", or"unknown"elsewhere than Windows or when it cannot be read; read only, nothing is changed);public_network_notice:{"title", "text", "rect"}while the dialog shows why Windows will ask on a Public network and what to choose (the session would be reached on the studio network andnetwork_profileis"public"), elsenull. TEST ONLY: the environment variableBERNARD_TEST_NETWORK_PROFILE("public","private","domain") stands for the read.session_ui.invite: Invite people while it is open, elsenull.routes: the choices of "Where they join from", each{"id", "label", "rect"}--"online"(only in a relay session, with no link: each guest's own link comes from the Bernard Review console),"lan"(Studio network, numbered past the first),"vpn","public","this_computer"(no network); a seat in a relay session gets Online alone, a guest of a studio-network session none;route: the index chosen;link: the one link shown, any token redacted (?t=[redacted]) -- Copy link copies it whole; empty ("") in a relay session, where the joined link is this seat's own and never given;copy_link_enabledandsend_enabled: whether Copy link and Send (with its field) can be used, bothfalsein a relay session;blocked_tooltip: their tooltip when grayed,"Invite guests with their own links from the Bernard Review console"in a relay session,""otherwise;tooltip: that tooltip while it is drawn (the mouse over a grayed Copy link or Send), else"";password_set, andpassword_shown(Show is on); never the password;rects:copy_link,show,copy_password,send,done.session_ui.join: Join a session while it is open, elsenull.link_recognised: the link pasted is one the player can join (session-joinreads the same addresses);note: what is said of the session under it, always""today (the protocol names nothing before a seat is in);join_muted: "Join with microphone muted",truewhen the dialog opens -- a choice of this seat alone, whichsession-joindoes not take;name_from_account;password_typed(never the password);join_enabledandjoin_blocker;rects:join,cancel,mute_toggle,change.session_ui.people: People, the popover the bar's avatars open, while it is open, elsenull.header("In the room · 7");mute_all: whether Mute all is offered -- never today, the protocol cannot mute another seat (kProtocolHasRemoteMute); the host's the day it can -- andmute_all_rect;rows: this seat first, then the pilot, then the room's order, each{"id", "name", "role", "mic", "hand_up", "rect"}--namewith" (you)"for this seat,role"Pilot · in control","Host"or"Guest"(no device: the protocol does not say),mic"Mic on"or"Muted";menu: the menu a right click opened,{"for": id, "items": [{"label", "enabled"}]}--"Give Control"(the host's,session-pass-baton's rule),"Raise Hand"/"Lower Hand"and"Mute"/"Unmute"on one's own row (not on another's: the protocol cannot mute a seat); never "Remove from Session", which the session cannot do -- ornull;paint_rule: the host's checkbox "Only the pilot draws",{"value", "rect"}--value1 ticked, 0 not, -1 not shown (not the host);footer.session_ui.stream_menu: what the session menu's "Stream to Remote Guests" row says, whether the menu is open or not:state_word, the word at the row's right (in an online session"not bundled","off","starting","running","streaming","restarting","error";""elsewhere), anddetail, the line under it ("streaming to 2 guest(s) · encoder: h264_nvenc","Not available in this version: the bernard-stream companion is not bundled.","waiting for the companion"on a LAN;""for none); andhost_banner, the banner the host of an online session sees, persistent, while the web guests get no picture:"Guests can't see your picture: this computer has no video encoder the stream can use. Intel or AMD graphics: install the VA-API driver (intel-media-va-driver or mesa-va-drivers). NVIDIA: driver 550 or newer."once the companion exited with code 3 (no usable H.264 encoder: it is then not launched again until the stream is switched off and on,companion_state"error"),"Guests can't see your picture: this version cannot stream video (the bernard-stream companion is not bundled). Drawings and notes still reach them."when this build has no companion;""otherwise, for a seat that is not the host and outside an online session (docs/stream_companion.md section 7).
export_ui: the Export window as the last frame drew it, in the screen coordinatesdebug-mousetakes. Nothing ofexportchanges with it.opensays whether it is drawn;menu_itemis File > Export Frames / Movie... while the File menu is open, elsenull;queueis the queue's window, open or not:open, and open,header("Exports · 1 running, 2 queued"),window,clear,close, androws, one per task,{"id", "state", "name", "meta" ("48 / 77"or the state), "action" ("Cancel","Remove","Show in Folder","Retry"), "action_enabled", "action_rect", "note", "row", "bar"}. Open, it also has:frame,dpi_scale,work_area(the main window's, below the menu bar),window,header,body,footer,presets_column,settings_column(each[x0, y0, x1, y1]),height,footer_visible(the foot is inside the work area and the window: Export is on screen),settings_scroll_max/presets_scroll_max(how far each column scrolls, 0 when all of it shows),scope("clip"or"playlist"),clip_tab,playlist_tab,close,source_line(as shown),preset(the chosen preset's name,nullfor custom),preset_label("Dailies · ProRes LT","Custom"),summary(the foot's line),export_label("Export","Export 12 clips"),export_enabledandblocker(why it is disabled,""when it is not: an export running is no reason, the job waits in the queue),buttons(export,cancel,save_preset,queue-- "Exports",nullwhile the queue is empty -- each a rectangle),groups: the preset column, each{"title", "presets": [{"name", "rect"}]}in its order. Then the settings:job, the window's job asexportwould take it (without what the player adds at the start: the clip on screen, its grade, its strokes);lists:format,codec(with thelabelit has for a sequence:"Depth · compression"...) andaudio, each{"text", "rect"};encoder_note: the H.264 / HEVC note,{"text", "warning", "rect"},nullfor another codec;format_options:{"open", "summary", "rect"},nullwhen the format has none;colour:segments(Grade as shown, Source, no grade),all_modes,in_space,out_space;size:choice("source","1920_wide","half","custom"),segments,wandh(Custom's fields,nullwhile closed),line;range:mode("in_out","whole_clip","custom"),line,track(the mini-timeline),in_handle,out_handle,playhead,use_in_out,whole_clip,step;toggles:burn_in,annotations,hardware,smart_tier, each a rectangle;save_to:field,browse,parts(the name as shown,{"text", "token"}, a token beingclip,versionorrecipient) andfinal_name.nova, in a player built with NOVA, while Format is NOVA (elsenull, and absent without NOVA):profile("Wavelet","HTJ2K","Lossless","Raw"),profile_segments,quality("q92";"exact"for lossless and raw),quality_rect,grain_aware,protect_blacks(the fade on),protect_blacks_enabled(HTJ2K over a noise floor),warning(the line shown with HTJ2K, elsenull),all_layers,advancedandadvanced_summary,temporal_gop,gop_rect,bc6h,toggles(grain_aware,protect_blacks,all_layers, andbc6hwhile Advanced is open).tracker: File > Tracker...:connected,kind,site(the site's id),user,busy,task(idle,connecting,listing,loading,publishing,downloading),done/total(the jobs answered since the tracker thread was last idle, one per clip for a publication),last_error({"code", "reason"},nullwhen none; the reason is masked with the site's secret),open_link({"state", "reason"}: the lastbernardreview://link,statenone,opening,waiting,opened,refusedorfailed, as theopen-linkevent),download_question(after a load whose shots are not on this machine, once their probes landed and until answered:{"count": 3, "bytes": 4720000, "text": "3 shots are not on this machine (4.5 MB)."};nullwhen none),downloads_queued(the previews Download All still has to fetch),studio_statuses(the status menu of the rows of the site connected, read once when it was tested:[{"name": "Retake", "id": "", "status": 2, "key": true}, {"name": "In Progress", "id": "ip", "status": 0, "key": false}],statusthe player's status it gives a row, 0 Pending, 1 Approved, 2 Not Approved;keytrue for what the review key of that status does; empty without a connection or when the site's statuses could not be read: the player's three). No secret, token or signed URL.export_tab: the Export tab of the bottom panel, in the same coordinates. Always:tab(the tab's own button in the panel's tab bar while the panel is drawn, elsenull),open(the tab's content is on screen),recipe(its name),dirty(changed since loaded or saved),selected(the saved recipe loaded, -1 for none),saved(the saved recipes' names),source("playlist"or"clip"),start_from("grade_as_shown"or"source"),outputs(each{"title", "preset", "format", "output" (the path as written, tokens and all), "watermark" (the object ofexport), "problem" (why it would stop the run,"")}),ticked(the playlist indices whose box is ticked),output_settings: an output's settings window,{"open": false}while it is closed; open,index(the output) and the Export window's fields above (window,buttons.exportis its OK,job,size,save_to...) withwatermark, its Watermark row ({"allowed", "on", "line", "toggle", "edit"}); andwatermark_window, the watermark's window (a card's Edit, the row's Edit...),{"open": false}while it is closed; open,card(the card, -1 for the output settings' form),allowed,watermark(the object being edited),window,toggle,preview,preview_text(the text as the preview prints it, with a made-upRecipient),marks(where the preview draws it),text_field,tokens({"token", "rect"}, a pill that adds it),cells(the nine positions),tiled,opacity,opacity_text("55 %"),size,size_text("3 % of height"),forensic(shown, grayed out),done,cancel. The tab never holds the recipient, and nothing here names one. On screen, also:area,bar,recipe_list,buttons(save,duplicate,run),total_line("12 clips × 2 outputs = 24 files"),run_label("Export 24 files"),run_enabled,blocker(why it is disabled,""),message(the last start, save or refusal),sources(column, theplaylistandclipsegments,rows-- each{"name", "ticked", "rect"}-- andline,"+ 4 more · 10 of 12 selected"),start_from_ui(column,segments: Grade as shown, Source, no grade;input_line),cards(each{"title", "format", "out_space", "size", "burn_in", "path", "problem", "rect", "remove", "watermark"},watermarkbeing the card's block:{"on", "allowed" (false on a plate: the switch is greyed out), "line" ("Off", the text by its tokens,"Plates stay clean"), "toggle", "edit"}),outputs_column,add_card,add_menu(while "Add Output"'s menu is open:{"preset", "rect"}each),save_popup({"field", "ok"}while Save recipe asks the name, elsenull) andrecipient_popup({"field", "ok", "cancel", "entered"}while the run asks who receives it --enteredsays whether the field holds something, never what --, elsenull).playback.ab_mode:trueif A/B split-screen is active.playback.ab_view: what the viewport paints while A/B is on,"a"(A alone),"wipe","b"(B alone),"difference"or"true_difference"--set-ab-view. Always"wipe"while A/B is off.playback.ab_difference_gain: the true difference's gain, a whole number from 1 to 16 (set-ab-view'sgain, the Difference Gain menu); 1 at start, not saved.playback.ab_difference_band: the Difference view's band of changed frames,{"compared": 40, "frames": 48, "differ": 3, "idle": true, "runs": [[12, 14]]}-- the timeline's frames compared so far (A's frame with the B frame the picture shows with it, from frames already in the RAM cache on both tracks, while paused and in the Difference view only), the timeline's length in A/B (the longer of A and B: past A's end, A's last frame against B's tail), how many differ, whether its thread waits, and the runs[first, last](zero-based frames of the timeline) it marks. The counts are0,runsempty andidletrueuntil Difference is first shown; a new clip on A or B starts it again. The thread works only while a timeline draws the band (a hidden timeline: it waits).playback.in_point/playback.out_point: the in and out points, as the zero-based indicesset-inandset-outtake (out_pointis-1for "to the end" until the timeline has drawn the clip once).playback.in_out_locked: the timeline's Lock In/Out (set-in-out-lock).timeline: the timeline as the last frame drew it, in the screen coordinatesdebug-mousetakes.validisfalsewhile it is not drawn (another tab, a hidden panel) and nothing else is written.ruler_rectis the frame ruler[x0, y0, x1, y1],track_x/track_wthe track's extent,in_x/out_xthe two brackets,track_a_rect/track_b_rectthe track rows (B's while A/B is on).in_grab/out_grabare each bracket's grab tab:rectwhere it sits andofferedwhether it is out this frame (the mouse is near the bracket on the ruler, and the points are not locked; in Dark and Light the tabs are always out while the points are not locked) -- a press anywhere else on the timeline scrubs, unless Alt is held.bracket_gestureis the bracket a gesture is on (0none,1in,2out) andbracket_editingwhether it has passed the movement threshold (about 4 px timesdpi_scale) and moves the point.cache_line_a/cache_line_b: where each track's cache line (one green tick per frame in RAM) was drawn:place("ruler"along the bottom of the ruler, track A with A/B off;"row"along the top of the track's own strip, both tracks in A/B;"none"),ticks(frames drawn),outside(frames in RAM that were not drawn: past the track's own last frame, or past the axis -- they used to be drawn past the ruler's right end), andrect[x0, y0, x1, y1]the extent drawn (nullwithout a tick): insidetrack_x..track_x + track_w, and withintrack_a_rect/track_b_rectfor a row.timeline_ui: the timeline as Dark and Light drew it on their last frame, in the same coordinates; boxes are[x0, y0, x1, y1],nullwhen not drawn.skinned: alwaystrue(the Classique mode no longer exists, the field stays); in every modeloop(the Loop button's word:"Shot","Playlist","Off"),timecode(View > Display in Timecode, which a Ctrl+Click on the counter switches) andactive_tool(the tool in hand:-1Selection,0Draw,1Text,2Arrow,3Shape,4Laser,5Eraser,6Pipette); nothing else in Classique.toolbar(the Timeline tab's bar) andversion_board_toolbar(the Version Board's, the same bar on the version's length),nullwhen not drawn:row,groups(annotate,view,playback,compare_display: each group's box, caption included;nullfor a group folded away),captions(the groups' captions drawn),counter_lines(2, or1in a narrow row),fold(the last step the narrow row took:0none,1the captions,2the counter on one line,3VIEW into "···",4ANNOTATE,5fps and px,6W and Fullscreen,7A/B and its segments),overflows(every step taken and still too wide: the row's middle is cut, "···" stays whole),counter(the counter: Ctrl+Click switches frames and timecode),annotate_enabled(the paint tools drawn enabled: the pilot's seat; grayed out of it, their tooltips kept),more(the "···" button,nullwithout it),more_open,more_items(what "···" holds, by key:fps,px,ab,ab_view,draw,text,arrow,shape,laser,selection,zoom,flip,reset_pan,watermark,fullscreen, and in one linepaint_barandstatus),items(each control in the row by the same keys plusskip_back,step_back,play_reverse,play,step_forward,skip_forward,loop,mute,counter: its box over the row's height).ruler(nullwhen not drawn):header_w(the tracks' headers' column),band(the ruler's band: in/out, brackets, paint-overs, cache line),numbers(the line of its numbers over it),tagandtag_text(the playhead's tag,##CurPlayheadBtn),start,end,range(the first and last frames, and "range 48 / 77"),ticks(the numbered ticks, frames from 0),tabs(in,out: the brackets' tabs;grabbable: whether a press takes each),legend(range,paint,cache: the three texts under the tracks, the note step's chords from the keymap;range_rect: a right-click there opens the ruler's menu).tracks.a/tracks.b(nullwhen not drawn):box(the header),row2(Version, Layers / AOV, and with sound Hear and Equalizer, drawn),row3(B with a clip: Pin, Replace..., Clear, drawn),more(what the header's "···" holds, by key:version,layers,hear,equalizer,pin,replace,clear),more_button,more_open,items(each button's box:eye,tag(the A or B tag: the Editing Track),expand, and rows 2 and 3 by the keys above;nullfor one not drawn).view_menu: the View menu'sheader, and itspaint_barandwipe_lineswitches (View > Paint Bar, View > A/B Wipe Line) while the menu is open, as drawn lately,nullotherwise.ab_wipe: the A/B wipe line,shown(A/B's menu, Show Wipe Line),alpha(its opacity as last drawn: it fades out with the mouse away from the picture, and while playing once the mouse rests) andwanted(fading in). The bar'sone_line: the bar drawn as the compact panel's row of buttons, in small icons, PLAYBACK centered, and in Complete ANNOTATE and VIEW back at its left; itsscrubisnull(the scrub has its own row:one_line.scrub). In one lineitemsalso holdspaint_bar(the brush, both levels) and, in Essential,status(the status switch, in px's place;pxis then inmore_items);paint_bar_lit(the brush lit: the paint bar on screen),faces(each control's own box as drawn, by the keys ofitems--itemsgives its place over the row's height),status_boxes(the switch's three boxes, Not Approved, Pending, Approved,nulleach when not drawn),status_icons(the icon drawn in each box,"cross","clock","check",nullwhen not drawn) andstatus_lit(the box lit,0to2, the shot on screen's status;-1for none).one_line(nullwhen the panel was not drawn lately, as in auto-hide or while the fullscreen timeline is slid away):drawn(the bottom panel compact: two rows, the scrub then the buttons, with the panel's Pin, Copy and toggle at the buttons' row's right end; no ruler, track, tab nor title row, and in Essential no ANNOTATE nor VIEW),scrub(the scrub itself: its row is the panel's whole width, the scrub shortened only by the shot's first and last frames' numbers at the row's ends and 6 px on each side; the whole row scrubs, numbers included),numbers(the compact row's numbers, each{text, box}ornullwhen not drawn:firstandlast, the shot's first and last frames at the row's ends, andcurrent, the current frame's tag on the playhead, kept on the scrub and 6 px (x the scale) from its ends; in the counter's unit, frames or timecode) andpanel(the panel's box),overlay(drawn over the fullscreen viewer, where it keeps the level's mode, one line or full, and its toggle changes it for the windowed panel too),panel_h(the panel's height),full_min_px(the least height of its full content: under it, one line),one_line_px(the compact panel's height, its two rows, the panel's least: 48 px at 100 %),thumbs_min_px(the magnet: the least height that keeps the thumbnails; a dragged edge jumps from it to the line and back, never in between),toggle(the toggle left of Pin; compact, in the buttons' row under the scrub; over the fullscreen viewer in one line, at that row's right end),marks(one line: the x of each painted frame's mark on the scrub),levels(essential,complete: whether each level was last in one line).earlier_marks: the box of each earlier note's mark on Complete's ruler or the one-line scrub, empty without any (Essential's ruler:ui_level_ui.essential.transport.earlier_marks).note_band: the band of the note of the clip on screen when it is on a passage, as last drawn:ruler(Complete's ruler),one_line(the one-line scrub),essential(Essential's ruler), each[x0, y0, x1, y1]ornull(no passage, or not drawn); a click on it plays the passage in a loop.transport_icons(4b): each transport and A/B icon button drawn in the last frames, by Complete's bar or Essential's row, its id (##SkipBack,##prev_shot,##Volume, ...) -> its icon's number (4previous shot,5play backward,6previous frame,7Play,8next frame,9next shot,14the sound,15A/B,16Loop,100first frame,101last frame,102the pause while playing): both views draw them, never an image;{}when none.edit: the playlist as one film, aseditreplies withoutshot_list.view: what the viewer shows (view):layout("picture","contact-sheet","edit","steps"),layouts(asviewreplies: each withofferedandrefusal) andcontrol, the View control at the viewer's top right --drawn(on the last frames: Complete, a viewer wide enough),rect([x0, y0, x1, y1],nullwhen not drawn),menu_openandmenu_rows(each row of its open menu:layout,rect).contact_sheet_ui: the contact sheet (Window > Contact Sheet 2D, the bottom panel's Contact Sheet tab), in the same coordinates; boxes are[x0, y0, x1, y1],nullwhen not drawn. In every mode:skinned(alwaystrue: the Classique mode no longer exists, the field stays),display("grid","storyboard_all"or"storyboard_active"),card_w(the cards' width, 160 / 220 / 320 / 450, or whatcontact-sheetset),labels(Show Name & Version Labels),badges(Show Approval Status Badges) -- these four are saved in session_config.json (contact_sheet_card_w,contact_sheet_labels,contact_sheet_badges, and the display ascontact_sheet_storyboard(bool) andcontact_sheet_storyboard_mode(0 all clips, 1 active clip)) --,multi_play(never saved: on at every start),auto_reflow,paint(the Contact Sheet Paint Over bar shown),slices(Slices per Clip),selected(the cards selected).cards: each card drawn on the last frames,index(its playlist row),thumb(its picture),frame(the frame it asks for),scrubbing(the mouse scrubs it: a card hovered, or every card with Multi-Play paused -- then it shows, as a playlist row, the exact frame or the nearest one it holds, never a veil nor a LATE / MISSING badge); in Dark and Light alsopill(the status pill: a click opens the status menu),badgeandbadge_text("LATE +3","MISSING","UNREADABLE f1004";""andnullwithout one),grade(Grade: Quick Color Grade) andcard(the card's outline). In Dark and Light only,nullin Classique:bar(rect,height,controlsby key --display,size,multi_play,auto_reflow,order,view,match_colors,paint;nullfor one folded into "···" --,more(the "···" button),in_more(the keys it holds) andsegments(the display's three segments, Grid, Storyboard: all clips, Storyboard: active clip, each as wide as its words;nullwhile the segment is in "···")) andselection_bar(drawn(a card is selected),rect,lines,items:text,approved,pending,not_approved,apply_order,deselect_all). The layout view:layout_view(asked for: the sheet in the viewer) andlayout, the view as drawn on the last frames ornull--host(the window its canvas is in,"Viewport"),rect(its canvas),cols,rows,card_w(its automatic grid),grid([w, h]),offset([x, y], the grid centered in the canvas) andscroll(pixels, when the grid is taller than the viewer). While it is shown the picture is not drawn:memory.smart.viewer_pxhas no height left. The sheet's tools:paint_tool("select","brush","arrow","rectangle","circle","eraser","text");paint_bar, the Paint bar as drawn on the last frames ornull--skinned(alwaystrue: Dark and Light's bar, in the viewer's paint bar's style),rect, and in Dark and Lightcontrolsby key (select,brush,text,arrow,shape,eraser,color,size,opacity,undo,redo,clear,close);quick_grade, Quick Color Grade as drawn on the last frames ornull--skinned(Dark and Light: the Color panel's rows),item(its playlist row),rect,fields(each row's field by key, Dark and Light),values(the shot's grade by the Color panel's keys:exposure,gamma,saturation,wb_temp,wb_tint,gain_r...,offset_r...) andcopy,paste,reset(its buttons, Dark and Light).notes(Dark and Light, where a note is text written on the canvas as the viewer writes its text, with no sticker; Classique keeps its sticky notes): each note drawn on the last frames,index,text,box(its letters on screen),ink([r, g, b, a], 0-1: the Paint color it was written in when it reads on the canvas with a contrast of 3 at least, else a near black or a near white),outline(the viewer's automatic outline, the opposite shade at 80 %) andoutline_px,editing(being written);canvas_bg(the canvas' ground,[r, g, b, a]).version_board_ui: the Version Board's table in Dark and Light, as drawn on its last frames,nullin Classique or when the tab is not on screen:frozen([1, 1]: the Department column and the row of shots stay when the table scrolls),headers(the row of shots, the Department's first),depts(each department's name) andcells(each cell's card, department by department and shot by shot,nullwhere a shot has no version of a department); boxes[x0, y0, x1, y1],nullwhen scrolled away.playlist_row_menu: a playlist row's right-click menu as last drawn,nullwhile closed (one menu, Complete's playlist's and Essential's column's):row, andentriesin the menu's order, eachwordsandrect([x0, y0, x1, y1], the coordinatesdebug-mousetakes).playlist_ui: the Playlist tab, in the same coordinates; boxes are[x0, y0, x1, y1],nullwhen not drawn. In every mode:skinned(alwaystrue: the Classique mode no longer exists, the field stays),filter(the status filter:"all","pending","not_approved","approved"; never saved,"all"at every start;playlist_ui.filteris only read here, no command sets it),shownandtotal(the rows the search and the filter leave shown, out of the playlist's;"3 of 5 shown"beside the chips while a filter is on),drag_allowed(a row may be dragged to reorder only while every row is shown:falseunder a filter or a search),continuous(Continuous Playback (Play All)) andauto_detect(Auto-Detect Sequence), the two settings the header's menus hold in Dark and Light.views: one entry per window that drew the tab on its last frame (the docked panel, its copies), withwindow(its name),skinned,radios(Classique: its filter's radio buttons are drawn under the search field, which it does when the rows hold two statuses or more, or a filter is on; thenradio_buttons:all,pending,not_approved,approved, each button's box) androws(each row the search and the filter leave shown, in order, also those scrolled out of view:indexin the playlist,status(0Pending,1Approved,2Not Approved),box(where the row is, in view or not),playing,thumb_ready(the row is drawn and its thumbnail at the frame it shows is there;falsefor a row out of view: only the rows drawn ask for their thumbnail); in Dark and Light alsopill(the status pill's button: a click opens the status menu),pill_words(true: the pill with its word and "▾" at the right;false: the compact tag at the bottom right, a narrow row) andlines(the three lines' words: the name; the version, the length and the format; the memory and the session's R2 label; filled for the rows the list drew on that frame only -- the list draws the rows in view, so a row scrolled out of view has three empty strings)). In Dark and Light only:header(title(the .bpl's name, else"Untitled playlist"),summary("5 shots · 00:00:21:14","· 2 unknown"when lengths are missing),title_own_lineandadd_compact(how a narrow header folds),box,add,save,more,open_play(the four buttons),add_open,save_open,more_open(their menus drawn)),search(the search field),chips(all,pending,not_approved,approved),chips_inline(beside the field, or under it),count("3 of 5 shown",nullwith no filter),drop(the dashed drop zone under the last row),footer(nullwith an empty playlist:box,bar,segments(Pending, Approved, Not Approved;nullfor a status with no row),counts,ram_fit,unmeasured("+N not measured"),lines(1, or2when narrow)).update: the check of a newer release at launch:state("off","pending","up_to_date","available","failed"...),checked,latest(the version this platform's block of the manifest names),available,ignored(the version set aside with Later),policy(what decides whether it checks: the Preferences' box,BERNARD_NO_UPDATE_CHECK, the system key, or a launch with--ipc-port),setting,manifest_urlandtest_manifest(BERNARD_UPDATE_MANIFEST_URL, for tests),error,banner(show,rect, and the boxes ofdownload,whats_new,later) andnotes(window_open,text,bytes,cut,unavailable_shown,link). Nothing is sent but the User-Agent; the manifest is https://bernardreview.com/downloads/latest.json. The update in one click (Windows) adds:choice(Preferences > General > Updates:"ask"or"never", the same setting assetting),banner.primary(the words of the bar's first button:"Download","Update","Restart and install","Try Again", or""while the installer downloads),one_click(offeredand, when not,reason;installer_url;phase:"none","offer","downloading","paused"(the viewer plays, scrubs, or a review runs),"ready","waiting"(ready, the player busy: playback, session, export, typing),"countdown","deferred"(Not now, or a session the restart could not give back: installed at the close, not reopened),"failed","closing";download:state,received,total,percent,verdict("none","ok","size_mismatch","hash_mismatch"),error,requests,restarts,pauses,reused,may_run;clicks_refused(clicks on Update or Restart now injected by IPC, refused unless the test manifest is in use);install_requested;countdown(the seconds "Restarting to install in N s" shows,nullwhen it does not run);deferred;session_kept_why(why the countdown would not close without asking,""when it may);close_unattended;review_unsaved(the close would ask to save the review)) andlast_install(what the waiter did before this start:result--"installed","installer_failed","cancelled","verify_failed"... --,exit_code,version;nullwhen nothing was installed).banner.secondary: the last button's words ("Later","Not Now"while the countdown waits or runs). Nothing is downloaded before a click on Update; the player restarts to install only at rest, after the 10 s countdown. The player the waiter reopens restores the last session without asking.send_log: the Send log to the Bernard team window and its sending.window_open,reason("manual"from Help orsend-log,"crash"when the launch asked after a crash),crash_question("Bernard Review closed unexpectedly. Send the report?" drawn),files(what the window lists:part--"log","log1","crash"--,name,bytes; never a path),include_crash,note_chars,state("idle","sending","sent","too_large","rate_limited","storage_full","unauthorized","refused","server_error","offline"),message(the window's words),report_id("BR-7K3Q9X"once sent, elsenull),http_status,error(the server'serror, or the transport's code),attempts(the last sending's POSTs: 2 when a 401 was retried without the token),requests(every POST since the player started:0until a click on Send),injected_sends_refused(presses of Send that IPC injected, refused),bytes,offers_log_folder,anonymous(no workstation token was sent),test_url(BERNARD_SEND_LOG_URLreplaces the relay's address, for tests against a local server), and the boxessend,send_enabled,dont_send,close,open_folder,crash_box([x0, y0, x1, y1],nullwhen not drawn). Never a token. The report goes to https://review.bernardengine.com/api/v1/support/logs, multipart, 20 MB at most.release_notes: the notes of the INSTALLED version (Help > What's New...; not the update banner's What's new, which shows a newer release's notes).window_open,drawn,title(as drawn,"What's new in Bernard Review 1.0", the public version, never a build;""while closed),opened_by_itself(the window is open and it was the one the first start of a new version opened, not Help's item),auto_opens(0 or 1: how many times it opened by itself since the player started),notes_version(the build the notes were written for),lines(5 to 8 plain lines in all under New, Improved and Fixed),text(the notes as the window shows them: each non-empty heading, then-lines), and the boxeswindow,close,help_menu(the menu bar's Help header) andmenu_item(the "What's New..." item, only while Help is open) as[x0, y0, x1, y1],nullwhen not drawn. The notes open by themselves once, at the first start of a new version (the profile'ssaved_by_versiondiffers from the running build): never on a fresh installation (nosession_config.jsonat launch), never twice, and never on a launch with--ipc-port(like the update check) unless the environment hasBERNARD_SHOW_RELEASE_NOTES=1(TEST ONLY; a test writes an oldersaved_by_versionin the profile to simulate an update). Notes written for another build than the running one are never opened by themselves.inspector_cache_ui: the Inspector's Cache & Performance tab, in the same coordinates; boxes are[x0, y0, x1, y1],nullwhen not drawn. In every mode:skinned(alwaystrue: the Classique mode no longer exists, the field stays),health(the health of the decoding, read from the measures ofdecodeandplaybackas the tab's pill reads them:"keeps_up"(Decoding keeps up),"from_ram"(From RAM, decoding is slower: too slow, no frame lost),"frames_skipped"(Too slow: frames skipped, the warning),"idle"(Nothing being decoded); in A/B the worse of the two tracks),health_words(the pill's words) andclip_tier(the clip on screen's Proxy Resolution:01/1 ...31/8,4Smart RAM-Fit).views: one entry per window that drew the tab in Dark and Light on its last frame (the docked panel, its copies), withwindow(its name),health,fold(the last step a narrow tab took:0none,1the meters one under the other,2the settings' summary under "Global settings...",3each decode line under its name,4the segments as the Proxy Resolution combo,5the pill under "This clip"),meters_side_by_side,summary_under,values_under,tier_combo,pill_under,overflows(every step taken and a line still too wide: cut),scrolls(the tab is taller than the panel: its body scrolls; nothing folds by height),notices(the slow-decode notices drawn under the bars),rects(each element's box by key:clip_title,health(the pill),decode_a,decode_b(A/B only),notices,cadence("Since the clip opened: ..."),resolution,tier(the five segments, or the combo),in_effect(a copy opened from Essential: "In effect: ..."),memory_title,meter_ram,meter_vram,meter_disk,memory_free,memory_computer,memory_held,memory_limit,memory_limit_auto,memory_limit_minus,memory_limit_plus(the Memory card's RAM free, computer, held line and RAM Limit with its three buttons),memory_footer,global_settings("Global settings...": a click opens Preferences > Cache & Performance),global_summary(its summary, on the same row)) andtiers(the five segments' boxes, 1/1 to Smart RAM-Fit;nullwhen the combo is drawn). In the Complete mode the tab also draws two foldable cards under Memory:complete(trueunless the Essential mode is on),folded(the keys of the cards the user folded, kept in the profile), and in each viewcards(each card's box by key:clip,memory,global_settings,settings("Workstation settings": the same controls as Preferences > Cache & Performance, shared, applied live),details),card_heads(the title lines of the two foldable cards),settings(each setting's box),details(each detail line's box by key:ui_fps,display_texture,recycled_buffers,track_memory,clip_in_ram,ram_fit,decode_threads) anddetail_values(the text each detail line shows,nullwhen it is not drawn). In the Essential mode the tab draws neither card; the other workstation settings stay on Preferences > Cache & Performance.density_ui: the density of tab bars, section headers and docked panels, in the same coordinates; boxes are[x0, y0, x1, y1].skinned(alwaystrue: the Classique mode no longer exists, the field stays);tab_bar(h, the bar and its tabs: 24 px x the scale in Dark and Light, Dear ImGui's frame, 28, in Classique;pad_xeither side of a tab's words;gap_xbetween two tabs;under, the space over the tab's content);section_header(h, a collapsible section's header;under);factory_bottom_split(the share of the height the factory layout gives the bottom panel:0.26in Classique, less in the skin by the title row it no longer draws, never under 200 px x the scale).panels: the three docked panels in the order Media & Session, Inspector & Tools, the bottom panel, each withview("media-session","inspector","timeline"),window(its name, the identifier every command uses:"Timeline & Version Board"in every mode),title(the words shown:"Timeline"in the skin),drawn, and when drawndocked,windows_in_node(the windows sharing its dock node),title_row_hidden(Dark and Light, a panel alone in its node, Essential's panels included: its title row is hidden; in Complete its tab bar is its first row, the tab bar's empty part and the corner's triangle move and detach it),title_row_h(0when hidden),window_rect,tab_bar(rect,under,tabs: each tab'slabel(the words:"Versions"in the skin),id(its identifier:"Version Board") andrect), and when the row is hidden and holds them (Complete, and Essential's bottom panel)pinandcopy(the title row's two buttons, at the right end of the tab bar's row).notice_open: the notice of Essential's close cross is open (its first click ever: "the Window menu brings it back"; Escape or a click outside keep the panel).chip, the "..." menu button of the panel under the mouse as last drawn, in the slot left of Pin (nullwhen none was drawn in the last two frames):window(the panel's Dear ImGui name),name(the words shown),rect(the button),menu_open(its menu is open),move,enlarge,close(whether the menu has that item: Essential has Close only, an enlarged panel no Move) andenlarged;enlarged, the name of the panel enlarged to the whole window (Enlarge Panel,Ctrl+Shift+Space, or the panel menu),nullfor none;enlargesandrestores, how many times a panel was enlarged and put back since the start;splitter_resets, how many times a double-click put a splitter back where it was first pressed.paint_over: the paint over's state.tool(-1: Select / cursor, 0 brush, 1 text, 2 arrow, 3 shape, 4 laser, 5 eraser, 6 pipette),tool_name(the active-tool box's word:"Draw"for the brush, which the box and the tooltips write Brush but the status keeps under this name,"Square"/"Circle","Eraser","Select"...),shape,select_kept(the skin's bar kept for Select),settings(size,opacity,feather),eraser(before: the tool the eraser's switch gives back,size: the eraser's own,holds_settings,previous_tool),pen(what the pen said last --pen,in_contact,eraser(its eraser end or button),has_pressure,pressure(0..1) --,width(the brush's last point's thickness factor, -1 for the mouse),eraser_factor(the eraser's radius factor now, 1 without a pen pressing),eraser_end(onwhile the eraser end holds the eraser,back_to: the tool it gives back),barrel(its side button held),quick_menu(the pen's quick menu, opened by the pen's right press with a drawing tool in hand:openon the last frame,rect[x0, y0, x1, y1]ornull,opened(how many times),closed_by("stroke","escape","elsewhere"or""),any_popup_open(any popup at all, the viewer's own menu included)),ipc(a script's pen,debug-mouse'spen)),tool_box(drawn-- Dark and Light --,rect,list_open),onion(on,before,after,opacity,tint,ghosts),colour(session, whether this seat is in a session;start,[r, g, b, a], the color a tool whose color was never chosen starts from -- the participant's in a session, the mode's annotation color alone;participant,[r, g, b], this seat's participant color, the one of its badge, laser and chat name; every participant draws with his own settings,paint_bar.color),proposals(proposed drawings:may_paint, whether this seat may use the paint tools -- alone, and every participant of a session unless the host's rule says only the pilot draws;rule, the host's "Only the pilot draws" in this session:driver_only,known(the host's own choice, or apaint_ruleheard),sent(thepaint_rulemessages this host sent: at every arrival and every change),heard,ignored(one not from the host, or on the host itself);sent, the strokes this seat sent as apaint_proposalsince start (a seat that does not drive, at each release);received,applied,refused, the proposals this seat heard as the pilot, added to a frame, refused;last_refusal, why the last one was: the field refused ("frame","strokes","stroke","track_idx"...),"sender"(its own echo, or the pilot's),"sender_id"(none usable),"unknown"(a seat not in the pilot's participant list),"rate"(that seat's beyond 4 a second; a decision from the host --is_hostas the relay wrote it -- has a rate of its own, 20 a second and 40 at once, anyone else's its drawings' rate; these three before the message is parsed, for a proposal and a decision alike),"shot"(no row of that shot here),"full"(the frame would no longer fit one message),"frame"also for a frame past the end of the shot (100 000 while its length is not known yet),"waiting"(too many waiting, or too many points of that seat),"budget"(that seat's points added in this session past 200 000 -- the host's own never count, only points actually added count, a seat that leaves keeps its count for the same id, and a 65th distinct seat of a session -- each connection draws a new id -- takes the place of one that left (or of one not heard from for a minute: a fallen connection sends no departure; a seat whose place was taken starts again from nothing, within the session total), else is refused; 12 800 000 points a session from all seats together, whatever their ids),"baton"(waiting when the baton passed: each proposal or decision lost is counted),"not_host"(a decision that does not come from the host),"driver_only"(the host's rule: only the pilot draws; said to the seat by apaint_refusal"driver_only");waiting(proposals; the host's decisions wait the same way);budget_seats, the seats whose points added are counted in this session (64 at most; kept when a seat leaves, until a new seat needs its place);refusals(sent, thepaint_refusalmessages this seat sent as the pilot -- for a refusal"budget", or"rate"/"waiting"said as"rate", one a seat a second at most;said, the seqs they named;heard, those this seat took, sent to it by the pilot;named, the seqs they named that were this seat's (the words show when one is);removed, its own strokes they took off its screen (a frame of the pilot's may have taken one already);last_heard, the reason of the last one taken ("budget","rate");shown, the words it showed last ("Drawing limit reached for this session", "Too many drawings at once, try again");kept, the proposals it remembers having sent, 16 at most);seats, the seats whose proposals' rate the pilot remembers (64 at most, one silent for a minute makes room; all forgotten when the connection ends or another session begins), those held while the pilot draws, moves or types a stroke, or holds back in Local Review;decisions(sent: the host's Keep / Remove sent to another pilot,applied,refused: those this seat heard as the pilot, lost with the baton included);thumbs, the Review Notes thumbnails drawn with proposed strokes lately (their last frame within the last three):row,frame,words("Proposed · Sam"),keepandremove([x0, y0, x1, y1],nullwhere they are not drawn: on a seat that is not the host)),hold(a drawing held over N frames:value, the paint bar's Hold for new drawings, 1 to 999;drawn, per track[A, B], the strokes held from an earlier frame the viewport drew on the frame it showed last;held_frames, per track, the frames of its clip that hold a stroke beyond themselves;index_builds, how many times those frames were indexed, both tracks together, which moves only when strokes change, never per frame),selection(track,frame,index(the first selected, -1: none),dragging,box: the frame drawn round it,last_press(with thehandleit took and what the press did,press:pick,toggle,move,scale,rotate,pivot,marquee,marquee_add,let_go),let_go: why the last one was let go; and the Select tool's selection:indicesandcount(every selected drawing of the frame),mode(rectangleorlasso),select_tool(the Select tool chosen),handles(the transformation box'stop_left...leftandpivot,[x, y]indebug-mouse's coordinates,nullwhile not drawn),pivot_custom,gesture(kind,handle,angle_degwhile a rotation is dragged),keys_answer(whether Ctrl+A and Alt+arrows answer now: the Select tool, the picture under the pointer or the focus),nudges,sizes(each selected drawing's size),undo_steps(its frame's)),masks(the strokes' textures:kept,empty_kept: the strokes kept as covering nothing on screen,built_last_frame,built_total,warmed_total: the masks built ahead of their note during playback,kept_for_notes: the masks the notes on screen keep),field(the slider whose typed field is open,""),field_closed,escape(what the last Escape was the paint over's for),want_text_input,items(where the bar's sliders, swatch, onion skin switch and popover, and the box's list entries were drawn:[x0, y0, x1, y1]),display_order(the windows drawn, back to front:name,layer(1: tooltips),popup,modal,picture,rect) andbar(drawn,z(its place in that order),rect,order_holds: above the picture's windows and under every other).paint_bar: the paint bar, in the three modes; in Dark and Light one row under the picture.skinned,drawn(false while no bar is shown: the Select tool, the laser...),tool_box(the active tool's box:drawn,rect,inside_bar-- the skin's bar draws it at its slot, whatever the tool; Classique has none),tool(the box's word,"Draw"for the brush),color([r, g, b, a]),values(size,opacity0..1,softness,size_1080p(the size as the bar shows it, in px at 1080p of the plate on screen), and as shown:size_text"12"(that same value),opacity_text"100 %",softness_text"0.20"),shown(false once its close button closed it, until Toggle Paint Tools),close(the floating bar's close button; null when not drawn),drawings_shown(the eye),onion,select_mode(the Select tool's way:"rectangle"or"lasso"),clipboard(Ctrl+C / Ctrl+V of the paint over:strokesheld,copied_frameandpasted_frame, the interface's frame of the last copy and paste, -1 before any),popovers(open in its last frame:"tools","color","text_options"(Dark and Light),"onion","hold"),labels_shownandtool_name_shown(false when the bar folded for want of room: the labels first, then the tool's name),fits(false: wider than the room even folded),rect([x0, y0, x1, y1], indebug-mouse's coordinates) anditems, left to right, each{"item", "group", "rect"}(itemstool,eraser,color,size,opacity,softness,hold(the frames a new drawing shows on; its popover sets them, or the hold of the stroke Select holds),text_optionswith the Text tool,onion,eye,undo,redo,clear; groupstool,color,settings,display,history). Dark and Light:grip(the grip at the bar's left that moves it:drawn,rect,hovered,dragging) andplace(moved: the grip moved the bar, which stays there for the run, kept inside the viewer; a double-click on the grip puts it back,movedfalse;at, the bar's top left;default, its default place, centered under the picture) andhistory(undo,redo,clearas drawn last:"available"(lit),"unavailable"(grayed: nothing to undo, redo or clear on the frame, or a guest),"hovered"(lighter);undo_steps,redo_steps,strokesof the frame;nullwhile the skin's bar is not drawn).ocio: the studio's OpenColorIO config (OCIO step 1: read and listed; step 3: the picture drawn through it, below):state("builtin"without a config,"loading","loaded","error"when the last load failed and no config is in use),config(the config in use: its file name, never its path, or"ACES 2.0 (OCIO built-in)";""for none),loading(the config being read,""when none),name(the config's own name),color_spaces,displays,views(all displays together) andlooks(its active counts,0without a config),default_display,default_view, anderror(why the last load failed, in one line:"the file was not found","it cannot be read: ...","it is not valid: ...";""after a success or Use Built-in Colors). A failed load keeps the config in use, if any. The config is chosen in the Color menu or by dropping a.ociofile on the window, kept insession_config.json(ocio_config), and never taken from$OCIO. OCIO step 2 adds the color space names to files and to the wire. A.bplitem and a session entry (session_config.json,last_session.playlist) write"input_space_name"beside"grading", only whenhas_saved_settingsis true and the clip carries a name that holds; it is read back checked (a string, at most 255 bytes, no control character, valid UTF-8; a refused one is left out). A file without the key reads as before and an older player ignores it. In a review session the pilot'sviewportmessage carries, only when the pilot has a config,ocio_config_id(the config's OCIO cache id) andinput_space_name_a/input_space_name_b(strings, each only when not empty). A follower takes the names only when its own config has the same cache id and knows the name, for the time of the session only (never written to its rows or files); otherwise it ignores them (the indices still apply, as before) and its log says once that the pilot uses another config. An older peer ignores the keys. A.ociofile over 16 MiB is refused (a.ociozarchive is not held to it), and an error said instatus,system-infoor a notice cuts a drive or share path to its last part. OCIO step 3 draws the picture through the config: each clip's input space (the name shown in the Input Color Space menu) is converted to ACEScg, the grade runs as before, then ACEScg goes to the session's OCIO display and view (with the view's looks), then the session.cubeLUT if one is loaded and on. A clip whose input space has no name in the config keeps the built-in input decode (by index) and gets the OCIO display after it. Without a config nothing changes. There is one view per session, chosen in Color > OCIO Display / View and OCIO Look or byset-ocio-view; the menu's pick is kept insession_config.json(ocio_view:display,view,look) and falls back to the config's default display and view when the config does not have it.status.ocioalso holdsdisplay,viewandlook(the view drawn),drawing(bool: OCIO draws the picture on screen now),off("", or why it does not:"no_view","driver_built_in","driver_other_config","failed"),off_reason(the sentence),view_error(why the view could not be built for the clip on screen, one line, no folders; the clip is then shown with the built-in colors and a notice says why) andfollowing_driver(bool). The OCIO programs are built off the render thread (workerbp-ocio-view), once per input space, context and view, and the next and previous clips are prepared ahead; until a clip's program is ready, its picture is drawn with the built-in colors. Thumbnails,screenshot, the--review-dirframes and Viewer / Clip mode exports ("color": "viewer"/"clip") carry the same OCIO input and display; Source and Custom exports are unchanged. In a review session the pilot'sviewportmessage also carriesocio_display,ocio_viewandocio_look(strings, only from a pilot with a config,ocio_lookonly when a look is picked). A follower whose config has the same cache id draws with the pilot's view, for the session only (never saved); a follower with no config or another config while the pilot has one, or with a config while the pilot has none, shows the built-in colors with a notice. OCIO step 5 adds the shot's ASC CDL (set-cdl,status.color.cdl). A.bplitem and a session entry (session_config.json,last_session.playlist) carry it in the optional key"cdl", written only when the row has one and whateverhas_saved_settingssays, and read whateverhas_saved_settingssays, so a pipeline can write it:{"slope": [r, g, b], "offset": [r, g, b], "power": [r, g, b], "saturation": s, "id": "shot010", "source": "seq1.ccc", "on": false}. Each ofslope,offset,powerandsaturationis optional (its identity value when absent), at least one must be present;idandsourceare optional strings (sourceis the file's name: a path is cut to its name);onis optional andfalseonly when the CDL is not applied. Every value is checked (slope 0 to 100, offset -100 to 100, power above 0 up to 100, saturation 0 to 100, finite, lists of exactly three numbers). A refused CDL is left out of the picture, its reason is shown in the Color panel (CDL refused: ...) and instatus.color.cdl_refused, and it is logged. A file without the key reads as before and an older player ignores it. The CDL is applied after the player's grade and before the display, in ACEScct, as OpenColorIO'sCDLTransformwith the styleCDL_NO_CLAMPdoes, with or without a config; the same picture. Thumbnails,screenshot, the--review-dirframes and Viewer / Clip mode exports show it like the viewer (Viewer: the CDL drawn on A; Clip: the source row's); Source mode does not. The eyedropper reads it too. In a review session the pilot'sviewportmessage carriescdl_a/cdl_b({"slope", "offset", "power", "saturation"}) only when a CDL is drawn on that track; a follower draws the pilot's CDL (none when the pilot sent none, an older pilot included), an overlay never saved to its rows, and a refused value is logged. A config can still apply a per-shot CDL of its own through a look (OCIO step 3); if both are set, both apply: the player's CDL first, then the config's looks. Reading the CDL from a production tracker is not available in this version.native_dialog: the file choosers, which run off the render thread:open,title,started,ended,veil("Choosing a file..." drawn on the last frames),owner(the window the choosers belong to: an HWND on Windows, the X11 window on Linux; 0 on macOS); on Linux alsoproblem(the words of the message up now when a chooser cannot open: no helper program installed, or one that stopped on an error;""none).viewer_notices: the viewer's notices and its headline, as the last frame drew them, in the same coordinates; boxes are[x0, y0, x1, y1],nullwhen not drawn. In every mode:skinned(alwaystrue: the Classique mode no longer exists, the field stays),frame(the frame they were drawn at),notices(their keys in the stack's order:"precision"(the 8-bit cache),"alpha","layers","media"(a media notice, 30 s)),count_text("1 notice","3 notices"),collapsed,openedandunseen(the stack's state: folded into its pill by Collapse; unfolded by a notice not shown yet where the cards fit under the obstacles, otherwise left in its pill,unseen(the pill in the accent) until the cards show it;openedby a click on the pill only, which alone puts the cards over the obstacles in a viewer too small; in memory only, never saved). In Dark and Light:shown,pill(the pill alone: Collapse, or no room),folded_for_room(the cards fit nowhere under the obstacles),over_obstacles(opened by a click on the pill in a viewer too small: at the top, over them),clipped,width(330 px x the scale, less in a narrow viewer),viewport(the viewer),obstacles(what the stack goes under:headline,before_badge("Before: grade off"),label_a,label_b(while they keep their room),essential_toolbar,notifications(their part on the viewer)),stack,header,collapse,pill_box, andcards(each card:kind,box,text_box,text,warn(the 8-bit cache's warn outline),buttons:id(today's ImGui id:"16-bit Half","32-bit Float","Dismiss##Alpha","Enable Alpha","Dismiss##Layers","Dismiss##Notice"),label,primary,rect).hud: the headline over the picture:skinned,resolution(the line as drawn:"1536×864 (proxy 1/2 · original 3072×1728)"in the skin, Classique's"1536x864 (Proxy 1/2 - Original: 3072x1728)"),sizeandaside(the skin's two parts, the aside muted; the line whole insizein Classique),decode(the decode line),decode_warning(in the warning color),decode_health(the decoding's health as the Inspector's pill says it:"keeps_up","from_ram","frames_skipped","idle") andrect(the headline's box).console_ui: the Console.skinned(Dark and Light draw five filter pills with their counts, each line as its time, its level and its text; Classique its boxes and plain lines),lines(the lines the Console holds, 10 000 at most),newest_time(the newest line's time,"HH:MM:SS", local: a log line's is when it was logged, a typed line's or a reply's when it entered the Console),windows(each Console window by name, the original"Console"and its copies:counts(Errors,Warnings,Info,Debug,Commands: the lines each filter would show since this window's Clear, whatever the text filter),filters(each filter on or off),shown(the lines shown),follows(the list follows its end),pills(where each filter, pill or box, was drawn last:[x0, y0, x1, y1]) andfilter_field(the text filter's box)) andcounts(the original's; a Console never opened: every line counted). "Copy" copies the lines' text alone, without their time nor their level, in every mode.track_b: the clip on track B and the comparison around it, exactly as the session file saves them (last_session.track_binsession_config.json).pathis a movie's path, or the first frame of an image sequence (""when B is empty),is_videosays which,framesits length.edit_targetis the track the inspector edits (0= A,1= B),wipe_positionthe divider (0..1across the picture),wipe_angleits angle in degrees,exposureandgammatrack B's exposure and gamma (set-exposure/set-gammawith"track": "B"),gradetrack B's whole grade (the keys ofcolor.grade; the session saves its look, all butmask,input_spaceanddisplay_space, which B's loader and channel view decide).session_restoreis what the last "reopen the last session" did with B:"none"(no restore, or B was empty when the session was saved -- also every session saved by an older version without B),"pending"(waiting for track A's clip to come back),"restored","missing"(the file is gone: A came back alone and a notice said so), or"refused"(a format this build lacks, refused exactly asset-track-brefuses it).track_b.decoded_width/decoded_height: the size B's picture on screen was decoded at, asviewport.decoded_widthfor A (0before B's first picture).track_b.align_start_frames,track_b.frame,track_b.shown_frame: the setting Align Start Frames (set-align-start-frames), B's own frame for the playhead (its locked frame, or the playhead held to B's length, or with the setting off B's frame of A's number), and the frame whose picture B shows (-1before the first).track_b.pinned: the pin on B's chip (pin-track-b), as the session saves it:truekeeps B through the playlist's clip switches, and it is alwaysfalsewhile B is empty. Local to this seat, like everything else intrack_b.track_b.last_switch: what the last clip switch did with B --"none"(no switch yet),"kept"(pinned),"cleared"(as before: B emptied, A/B off) or"missing"(kept, then cleared because its file had gone, with a notice).track_b.from_playlist(read-only): track B taken from the playlist -- a row's right-click Compare as B, or the row dragged onto track B's row of the timeline or onto the B side of the picture (A/B on; never onto one of the picture's controls, the empty-B hint aside).rowandhow: the last row put on B that way and how ("menu","track_b_row","picture_b";-1and""before any).dragged_row: the row being dragged now (-1: none);over_b: it is over a target for B.menu_entry: where a row's menu drew "Compare as B" (its last two frames) andtrack_b_row: track B's row of the timeline (on the main window),[x0, y0, x1, y1]indebug-mouse's coordinates,nullwhen not drawn.cache: B's frames in RAM --key(B's cache key: a film's path,"seq_b_"+ a sequence's first frame;nullfor an empty B),shared_from(the playlist row's key a sequence on B reads its frames from,nullfor none),frames(the frames of B's layer B can show from RAM),own_framesandshared_frames(held under B's key / read from the row's),own_bytes(what B holds under its own key; for a film, the row's frames, under the same key). Whichever way a row's media reaches B (these three,set-track-b, a drop, B's versions, the restored session), what the row decoded is not decoded again:decode.countsproves it.track_b.compare_selection(read-only): the rows picked in the playlist and "Compare Selection" (Open RV's Stack in the A/B view).picked: the picked rows, in list order (a click, Ctrl+Click, Shift+Click; kept by their media, so a reorder moves them with it);anchor: the row a Shift+Click extends from (-1: none).offered: whether a row's menu offers "Compare Selection" now (two rows or more picked, one of them not the media on A).shots: the shots of the last "Compare Selection", as rows now, in list order -- each media once, never the media on A, never a row without media here.position: where B is among them (-1: B holds none).keys_step_selection: whether the version keys (Up/Down) andcycle-bstep B through those shots now (A/B on, B on one of two shots or more), Up the shot above and Down the shot below, stopping at either end; otherwise they step B's versions as before, andcycle-b's reply names the rows forfromandversion.last:"","selection"(the menu),"selection_next","selection_prev".menu_entry: where a row's menu drew "Compare Selection" (its last two frames),[x0, y0, x1, y1]indebug-mouse's coordinates,nullwhen not drawn. A step through the shots is timed like a version step (track_b.last_version_step).track_b.version: the version B's viewport label shows after its letter ("B v002"): the Version Board's name for B's clip, else the version in its file name,""when there is none.track_b.versions: the versions of B's shot the version keys andcycle-bwalk, in step order (by number; entries without a number are left out),currentmarking B's own entry -- none is marked when B's clip is not on the board and B steps from its own number.track_b.version_prev/version_next: wherecycle-b"prev"/"next"would go,nullat either end.track_b.version_preload: the pre-load of those two versions.enabledis the preference (set-b-version-preload);cap_bytesits ceiling, the tenth of the RAM budget it shares with the pre-cache of the other layers of an EXR (other_layers_bytes, what they hold);held_byteswhat the two versions hold, and per version theframeunder the playhead (nullwhile its frame list is still being read), whether that frame isresident, and thebytesheld under that version's track-B key (frames of earlier playhead positions included, until evicted: they are the first to go).track_b.last_version_step: the last step (nullbefore the first):fromandtoversions,direction,preloaded(its frame was in RAM at the command), andms, the time from the command to the first upload of the new version's picture to B's texture (shown at that frame's swap);nullwhile it has not arrived.playback.loop_iterations_missed: how many iterations of the render loop found track A's frame missing from the cache while playing — counted where the player gives up on the frame it wanted for that refresh and shows the nearest decoded one instead. It counts iterations, not frames: one frame that is not decoded yet is counted at every refresh until it arrives, so on a 50 Hz display a film that decodes at 3 frames a second reads about 50 a second, whatever the frame rate. Reset when a clip is opened, not by a seek. Use it to see that the loop waited; count frames withlate_framesandlost_frames.playback.dropped_frames: an alias ofplayback.loop_iterations_missed, the same number, kept under its old name for one version so existing scripts keep reading a value. It was documented as "frames the clock asked for and never got" and is not: it counts loop iterations (see above). Move toplayback.lost_framesfor frames never shown, or toloop_iterations_missedif the iteration count is what you want; the alias will be removed in a later version.playback.frames_due,playback.frames_on_time,playback.late_frames,playback.lost_frames: track A's frames on the media clock, since the clip was opened (a seek does not reset them). A frame is due when the playback clock enters it, or passes over it when one refresh covers more than one frame. It is on time when its own decoded picture is on screen at the first present after it became due, and that present's loop iteration started no later thanlate_tolerance_msafter its due time (one and a half refreshes ofdisplay_hz, at most one frame).late_framescounts the frames that were not on time — shown at a later refresh, or never.lost_framescounts the frames the screen never showed: the gaps in the frames whose picture reached the display (a vertical blank or a composition of the desktop after its present), in play order, the loop point included -- the end of a pass and the start of the next are counted in the pass of the picture on screen, as a probe reading the screen counts them (it used to count the frames the clock passed without showing, which fell in the clock's pass). Only frames the clock scheduled count: nothing while paused, on a seek or a scrub (the frame a seek lands on starts no count), on a locked A/B frame, during a file drop, while track A is hidden, or past the end of a shorter track A. A clip played from RAM at its rate reads0late and0lost.playback.longest_iteration_ms: the longest time between two presents (one iteration of the render loop, swap to swap) over the last 10 seconds, playing or not; at 60 Hz a healthy loop reads about 17.playback.presented_fps: new pictures put on screen over the last second.playback.display_hz: the refresh rate the tolerance is built on (the monitor under the window, 60 when the system does not say).playback.related_clips: the clips related to the one on track A (the other departments of its shot, the shots before and after it on the Version Board), which the cache keeps ready.count: how many;fingerprint: 8 hex digits that change when that list changes (its paths, in order);computes: how many times the list was worked out since the start. It is worked out again only when the playlist, the board, an active version or the clip on screen changes, never at every frame: during plain playbackcomputesstays still. The values are those of the last frame drawn (a status sent right after a change may read the list before it). The cache also keeps every playlist row; they are not counted here.playback.rows_turn: what the playlist's rows give each turn of the render loop.builds: how many times that state was built (again only when a row is added, removed, moved or relinked, a row's probe lands, or the clip on screen, its row or the board's related clips change);asked: how many related clips the pre-cache asked about on the last turn -- the board's, the rows just before and after the one on screen, and a slice of the other rows that moves on at each turn (4 while playing, 128 paused), so every row is still pre-cached.decode: the decode speed of the clip on track A (anddecode.track_bfor B while A/B is on), measured on the frames decoded where playback waits for them — the image workers for a sequence, the video thread for a film or camera RAW; a frame read back from the disk cache is not a decode.decode.ms_per_frameis the decoder's wall-clock busy time per frame over the last 5 seconds (at least 4 frames): with one decoder the mean decode time, with several image workers at once the time the pool needs per frame, which is what must stay under1000 / fps;avg_decode_msis the plain mean of each decode,sampleshow many frames the reading is on,scalethe tier they were decoded at (only the tier of the newest frame counts),newest_age_show long ago the last one ended.validisfalsewith fewer than 4 frames, or when nothing was decoded for 5 s (a clip entirely in RAM has no decode speed).budget_msis1000 / playback.fps.decode.too_slow:ms_per_frameabovebudget_ms.decode.losing_frames: frames were lost in the last 2 seconds of playback.decode.warning: both, while playing — what the player shows the user (the viewport's resolution line in the warning color, "frames are being skipped" in the Cache & Performance tab, one notification per clip when the tier will not be lowered). A film played from RAM while its thread decodes behind it istoo_slowwithout awarning.decode.slow_decode_tier: the setting,"auto"or"off"(set-slow-decode-tier);decode.slow_decode_tier_effectivethe one in effect (the saved one, at every level: Essential no longer turns"auto"on, see Interface Level).decode.rule_active: Auto, and the clip on Smart RAM-Fit.decode.filling: the clip's frames come from the RAM fill -- Smart RAM-Fit keeps it in RAM (the playlist fits, not a film left streaming) -- and the rule moves nothing on it: frames lost to a Play before the fill reached them are the fill's, and a lower tier would evict what it holds (the fill would start over).decode.base_scale: the tier Smart RAM-Fit gives the clip;decode.tier_cap: the highest tier the slow-decode rule allows it (1.0: none);decode.lowered: the rule holds it below Smart's tier —viewport.proxy_scaleis then the lower one, andram_fit/playlist[]still report Smart's.decode.last_change: the rule's last decision (nullbefore the first):event("down","back_no_gain"a lower tier that decoded no faster, undone;"up";"up_failed"back down after a climb that was slow again;"released"the setting turned off or the tier fixed;"setup_changed"the decode setup changed -- hardware decode, A/B, track B's clip -- and the rule starts over),fromandtotiers, thems_per_framebehind it,seconds_ago, and thenoticethe player showed.decode.read_a/decode.read_b: the film on track A / track B when a read gives no frame. A read error (a byte range refused, a share that stops answering) is never taken for the end of the file: the player seeks back to the same frame and reads it again, after 10 ms, then twice as long each time up to one second, and the clip keeps its length.failing: such a run is under way;failures: reads in a row that gave no frame;frame: the one being read;seconds: how long the run has lasted;error: what the last one hit ("read error: ...","end of file","decode error");unreadable: the run passed 5 seconds and the user was told ("Cannot read ... at frame N").last_event(nullbefore the first):event("retrying"a run began,"unreadable","read_again"the frame came,"shortened"the file really ends before the length its header gives -- the demuxer's own end of file ten times at the same frame, spaced out, three for a length estimated from the duration -- and the timeline now stops there, with a notification),frame,length_beforeandlength_after,seconds_ago, and thenoticethe player showed.frames: the film's length as the track uses it now (its strip, its cache line, the timeline'smax(A, B)),0for no film. Only the video threads fill these: an image sequence reports nothing here.decode.counts(read-only, counted since the start):sequence(real decodes of the image sequences' pool,ram-info'ssequence_decodes),sequence_served_from_ram(jobs the pool served from RAM instead,sequence_decodes_skipped),video_aandvideo_b(the frames each track's video thread decoded, the frames a seek drops included),video_fill(the frames the films' background fills decoded),video_b_held_skips(the frames B's video thread passed over because RAM held them at B's tier already: a playlist row's film put on B; never for a film with sound, whose sound B reads from those frames; on a long-GOP codec -- H.264... -- only a run of held frames the seek to the next frame not held costs less than reading: that seek decodes its GOP),video_b_codec_frames(every picture the codec gave B's video thread, the GOP a seek decodes on its way to its target included:video_bcounts the reads only). Read twice around a gesture, they say what it decoded.color.exposure: The current color exposure value in f-stops.color.gamma: The current gamma contrast curve value.color.grade: track A's whole grade, under the keys a.bplitem'sgradinghas (exposure,gamma,rgb,brightness,contrast,in_black,in_white,in_gamma,out_black,out_white,hue,saturation,white_balance_temp,white_balance_tint,offset,mask,input_space,display_space, andstages_off-- the stages the Color panel's eyes turned off, a list of their keys, only while one is; andcurve_master,curve_r,curve_g,curve_b-- the curves,[[x, y], ...]on the ACEScct axis (Set Curve), each only while it is not the straight curve; andrange_<reds|yellows|greens|cyans|blues|magentas>_<hue|saturation|lightness>andlightness-- the hue ranges (Set Hue Range), each only while it is not neutral (hue 0, saturation 1, lightness 0);playlist.schema.json). The white balance and the offset have noset-command (Quick Color Grade sets them, a.bplor the session brings them back): this is where a script reads them.track_b.gradeis track B's, the same keys.annotations.a/annotations.b: what each track shows of its clip's paint-overs:frame(the clip's own frame: A's is the playhead; B's is B's locked frame, or the playhead held to B's length),strokes(each withtrackit was drawn on,version,type,pointsand itsfirst/lastpoint, 0..1 of the picture,w_min/w_max: its thinnest and thickest point, as a factor of its size -- 1 and 1 for a stroke drawn with the mouse; a pen's pressure gives 0.1 to 1, the format in docs/online-review-messages.md 6c),row(the playlist row of the clip, -1 for none) andversion; for B alsoclipandhome("row","clip"for a clip on no row, whose paint-overs the playlist keeps by clip, or"none").annotations.clips_off_the_rows: how many clips on no row carry paint-overs.media_notes(the notes follow the media, not the row):media_notes.rows, one per playlist row,{"index", "key", "path", "count", "frames", "note"}--keyis the media's (the path as the RAM cache keys it, lower case on Windows; for a frame of an image sequence its number replaced by#, so any frame of it is the same media, andsh010_v002another one thansh010_v001),countits notes (one per frame with strokes, one for the written note),framesthe frames with strokes (the timeline's marks),notethe written note.media_notes.kept: the notes kept of media on no row (rows removed, B's clip opened from the file chooser),{"key", "path", "count", "frames", "note"}; a row of the same media added takes them back.media_notes.toast: the message at the bottom of the viewer,kind("removed","restored","none"),text,undo(Undo offered),shown,boxandundo_box([x0, y0, x1, y1], the coordinatesdebug-mousetakes).media_notes.menu_entry: Delete notes of this clip... in the row's right-click menu while it is open, elsenull;media_notes.delete_dialog: its confirmation,open,question, and itsdeleteandcancelbuttons' rectangles as last drawn.media_notes.session_nav: on a review session's follower, the pilot's last Navigation,driver_idx(the pilot's row) andlocal_idx(the row here it matched), where the pilot's strokes of that row are written.note_attachments: the images attached to notes.rows: each row whose note has images or a refusal,{"row", "attachments": [{"sha256", "name", "content_type", "bytes", "width", "height", "tile"}], "refusal"},tilebeing its thumbnail's state (not_drawnbefore a view drew it,loading,ready,missing: its bytes found nowhere -- a .bpl moved without its folder --,unreadable) andrefusalthe words said under the note ("" none);last: the last attach,{"row", "code", "reason", "sha256"}(code"" when attached, elseempty,too_large,unsupported_type,bad_dimensions,too_many,already_there,unreadable,no_shot);pending: the images still being read or decoded;view: the full-size view,open, and while opensha256,ready,failed,window,pictureandclose(boxes);drop_targets: the note fields a drop lands on,{"row", "field"};textures: thumbnails kept. Online review (the host's player; docs/online-review-messages.md § 6): each row'sguest_images, the images of its note-takers' notes,{"note_id", "relay_id", "sha256", "bytes", "width", "height", "tile"}(relay_idthe relay's id of the image in this review, also after the .bpl was loaded again;sha256"" until downloaded;tile:downloading,not_drawn,loading,ready,missing); andrelay, counts only:features(the relay'srelay_infosaid it keeps images),uploaded(the host's images it has),uploads_asked,downloads_asked(in this review). Underreview_notes_ui.cards[],imagesandimage_crossesare the tiles and their ✕,image_refusalthe refusal's line; underui_level_ui.essential.inspector,note_imagesandnote_image_crosses.review_notes_ui: the Review Notes tab as it was last drawn (by the docked panel or a copy, whichever drew last),nullbefore it was drawn. Boxes are[x0, y0, x1, y1]in the coordinatesdebug-mousetakes,nullwhen not drawn.skinned(alwaystrue: the Classique mode no longer exists, the field stays),frame,only_annotated(Only annotated shots, kept in the profile asnotes_only_annotated),export(Export as HTML),only(the box),tracker_list,tracker_state(the pill "Connected to ..." / "Not connected"),connect(Connect..., not drawn while connected),publish(Publish...),settings(Tracker settings...: File > Tracker...).rows: the playlist rows drawn as cards, in order (with Only annotated shots, a row without a note or a drawing is left out, but the clip in play).cards: one per card,card,header(a double click loads the clip),field,pill(the status menu),mark(the "to publish" / "published" / "not linked to a tracker shot" words),paints(each paint-over: a click goes to its frame) andcrosses(each paint-over's delete, drawn only while the pointer is on its picture: it erases that frame's drawing at once, as Clear frame does, and Ctrl+Z brings it back).off_lines: each clip on no row's line drawn, "Not in the playlist: sh010_v006.mov (track B) · drawings at 1016 and 1033" (track B's clip always; the others only while their line is open).off_fold: the one line the clips on no row but track B's fold under,{"text": "Notes of 5 shots not in this playlist", "rect", "open"}(closed at first and again at each playlist loaded or cleared),nullwhen there are none.empty_line: "Playlist is empty..." was drawn.tracker:entries(label,site,enabled: Bernard Engine first, not enabled yet, then the sites of trackers.json),state,can_publish(false too when every note is already published, unchanged since: nothing to send, Publish... grayed),publish_why(its tooltip while grayed: "Connect to a tracker first.", "Your 3 shots with notes are not linked to Flow. Open a Flow playlist to publish (File > Open Playlist from Flow...).", "No shot loaded from studio-a has a note or a drawing.", "Nothing to send to studio-a: 3 already published, unchanged since.";""while it can be pressed or a publication runs),summary(what a publication would send, statuses aside: "2 notes, 3 annotated frames to studio-a"),not_linkedandnot_linked_line(the shots a publication would not send, not linked to the site connected, and the window's line about them, "3 not linked: kept in the player, not sent"; 0 and""without),error.marks: each playlist row's mark,""for none.links: each card's link badge, in the cards' order (rows),text("Flow · SH010 · v003", "Not linked", "Linked to another site", "Flow · SH010 · no version id";""when nothing is said: no tracker connected and the shot linked to none) andrect.ranges: each card whose note is on a passage, itsrowand therectof its passage's label ("1012-1040"; a click plays it in a loop).earlier(manu/earlier-notes): each card's Earlier notes head drawn,rowandhead(only the cards that have the section).metadata.track: the track whose metadata the Inspector's Metadata tab leads with,"A"or"B"("A"in A|B);metadata.manualwhether that is the segment's choice (else the view's),metadata.togglewhether the A · B · A|B segment is up (A/B on),metadata.counthow many fields that track has. B's are the ones B's file gave when it was opened.metadata.mode: the columns the tab shows,"a","b"or"ab"(A and B side by side: the wipe's, or chosen).metadata.pinned: the pinned keys, in the order they were pinned -- one list for every clip, saved asmetadata_pinned_keysin session_config.json (none by default, 64 at most).metadata.differing: in A/B, how many keys differ between A and B (a key on one side only included),0otherwise.metadata.diff_only: whether "Differences only" is ticked.metadata.skinned: alwaystrue.metadata.views: one entry per window that drew the tab in Dark and Light on its last frame (the docked panel, its copies), boxes[x0, y0, x1, y1]indebug-mouse's coordinates,nullwhen not drawn:window(its name),fold(the last step a narrow tab took:0none,1the segment and "Differences only" under the search,2the key's column narrower,3each value under its key,4the segment and the box one under the other),stacked,overflows,columns(1, or 2 in A|B),shown(the rows drawn),empty(the words shown instead of rows,""when there are rows),rects(each element's box by key:search,segment,diff_only,head_key,head_a,head_b,rows(the rows' area, which scrolls)),segments(A, B and A|B's boxes),groups(each drawn group, in order:group(pinned,image,camera_render,exr_file,other),count,open,head) androws(each drawn row:key,group,row,star(a click pins or unpins the key),value_a,value_b,matrix_a,matrix_b("4×4 · show": a click opens it)).color.input_space: track A's entry of the Input Color Space list, as an index (0Linear ACEScg,1sRGB,2ACES2065-1 (AP0),3Adobe RGB,4ProPhoto RGB,5Linear sRGB / Rec.709,6ARRI LogC3 (EI 800),7ARRI LogC4) -- whatset-colorspacesets.color.input_space_manual: whether the clip on screen's input space was chosen by hand (kept on every load; Essential marks it "Set by hand");color.input_space_detected: the index its file gives (-2a data pass,-1no clip), Essential's "Auto (detected: ...)".color.display_space: the index of the display transform. With an OCIO config loaded (OCIO step 2), the clip on screen also hascolor.input_space_name(its input color space by its name in the config;nullwithout a config or without a name),color.input_space_name_from(where the name comes from:"chosen","file"(a .bpl or the last session's entry),"session"(a review session's pilot),"file_rule"(the config's file rules),"metadata"(the file states a space) or"builtin"(the index's twin);nullwithout a name),color.input_space_name_shown(a bool: the built-in pipeline draws exactly that space;falsefor a space without a twin, drawn with the clip's built-in index until the OCIO display) andcolor.input_space_name_unknown(present only when the clip carries a name the config does not know). A name the clip carries holds while its built-in index is the one it was paired with: any built-in pick, shortcut or Auto leaves it behind.color.cdl,color.cdl_drawn,color.cdl_refused(OCIO step 5, the shot's ASC CDL):color.cdlis the CDL of the shot on screen as a.bplitem writes it (slope,offset,power,saturation, andid,source,"on": falsewhen they apply), ornullwhen the row has none.color.cdl_drawn(bool): a CDL is drawn on track A now (a CDL that is not applied, or the identity, is not drawn; a follower draws its session pilot's, see Review sessions).color.cdl_refused(string, present only when a CDL given for that row, by a.bplor a session entry, was refused): why, in one line; the row then has no CDL and the Color panel says "CDL refused: ...".color.channel: The isolated color channel currently displayed ("RGBA","R","G","B","A"or"L"), track A's.color.channel_b: track B's channel view, the same names. Equal tochannelwhile the two are linked, the default.color.channels_linked: whether A and B show the same channel. An Alt+Click on a channel (orset-channelwithtrack) unlinks them; a normal click (orset-channelwithouttrack) links them again.audio.volume: The volume level between0.0and1.0.audio.muted:trueif the audio is currently muted.viewport.decoded_width/decoded_height: the size the frame on screen was actually decoded at. Dividenative_widthby it to get the effective proxy ratio; for camera RAW this is how you check that the tier you set is the tier you got.viewport.native_width/native_height: the clip's own resolution (video clips;0for image sequences). Prefersource_width/source_height, which answer for both kinds.viewport.source_width/source_height: the resolution of the source, whatever its kind — the decoder's own answer for a video, the first frame for an image sequence (measured once when the clip changes, not on everystatus).0while no clip is open, or before the clip's metadata has landed.viewport.retiering:truebetween a proxy-tier change and the first frame decoded at the new tier reaching the screen. While it istrue,proxy_scalealready describes the new tier anddecoded_widthstill the old one; when it turnsfalse,decoded_widthmatchesproxy_scale. This is notplayback.transitioning, which means a change of clip. In A/B,proxy_scaleis the higher of the two tracks' tiers (each side at least its own shown size), but a movie on track A is decoded, kept in RAM and shown at its own tier:retieringturnsfalseonce A's picture is at A's tier (decode.scale), anddecoded_widththen describes that tier (it used to staytruefor as long as a lower B was there).playback.seek: the last jump of the paused playhead (aseek, a click or a drag on the timeline, a step), phase by phase,nullbefore the first:count(jumps since the start),target(track A's frame),was_in_ram(its picture was already in RAM at the tier in force), then, in milliseconds after the turn that saw the jump andnulluntil it happens:requested_ms(asked of a decoder),decode_start_ms/decode_end_ms(a decoder took and handed over that frame; a negative start: it was already in hand),in_ram_ms,drawn_ms(its own picture in the viewport's texture) andpresented_ms(the swap that put it on screen);turns(render turns until the draw) anddrawn_scale(the tier drawn). tools/bench/scrub_latency.py reads it.viewport.frame_a/frame_b: the frame of track A / track B whose picture the viewport draws,-1when none (frame_bis-1while A/B is off). A frame that is not in RAM yet is stood in for by the nearest earlier one: while playing, aframe_athat stays behindplayback.currentFrameis a frozen picture, which the late / lost counters do not see past the end of a shorter track.viewport.proxy_idx: the Proxy Resolution dropdown index (0full,1half,2quarter,3eighth,4Smart RAM-Fit), the saved one, andviewport.proxy_idx_effectivethe one in effect (4while Essential decides, see Interface Level);viewport.proxy_scalethe scale in force for the current clip.viewport.zoom/viewport.fit_mode: the current zoom (a fraction of the source resolution) and whether fit mode is on.viewport.flip_h/viewport.flip_v/viewport.rotation: the picture's flips and its rotation (0to3, quarter turns clockwise).viewport.fullscreen/viewport.timeline_reveal/viewport.picture_rect: whether the player is fullscreen, how far the auto-hidden timeline is slid in (0hidden ..1shown; always1outside auto-hide), and the picture's rectangle on screen as last drawn ([x, y, width, height], screen coordinates -- the onesdebug-mousetakes).viewport.ab_labels: the "A" / "B" labels the last frame drew over the picture,[]when none (A/B off, or a side too thin to hold its label): one object per label,track("A"or"B"),text(the letter, then the clip's name when Preferences > General > Show the clip name beside the A/B labels is on) andrect([x, y, width, height], the same screen coordinates aspicture_rect). The labels are the player's own overlay, drawn over the picture on this screen only: they are never in the picture streamed to remote guests (stream-enable, drawn offscreen without any overlay), which is how a script can check it -- read the display under a label'srectand the stream's frame at the same place of the picture. Each label hastrack,text,rectandopacity(0..1); a label faded out to 0 is not listed.viewport.ab_label_mode: the setting A/B labels,"interaction","always"or"never"(set-ab-label-mode).viewport.ab_label_idle_s: seconds since the labels last saw an interaction (the wipe dragged, turned or hovered, the view or a clip changed). Under"interaction"they are whole up to 1.5 s, then fade out over 0.4 s.memory: the absolute ceiling and the one ledger it is counted in.total_ram_bytesis the machine's RAM (ram_simulatedwhen theBP_SIMULATED_RAM_GBtest variable gave a smaller figure),hard_ceiling_bytesthat RAM less the 20 % always left to the system,ceiling_bytesthe RAM Limit under it.postsholds every post as counted (cache,pool_idle,decode_in_flight,disk_write_queue,shared_vram_ring,thumbnails,contact_sheet, andother, the rest of the process as measured),reservedthe same with the pool and the disk write queue at their budgets,cache_allowance_byteswhat the RAM cache may hold: the ceiling less every other post. Under it, the cache also stays inside what the system still has free (Windows and Linux; macOS: the pressure rule only): the RAM available and, under Windows, the commit (RAM + page file) available -- with what a page file Windows sizes, or set below its maximum, can still grow by, the disk's last 8 GB kept (not for 30 s after a refused allocation) --, less a margin of 12 % of the RAM -- 2 points above the 10 % under which the pressure rule sheds -- at least 4 GB, at most a quarter; the background fills stop at their soft limit of that cap, and under the margin the cache gives frames back, farthest from the play head first (ram-info'scache_max_bytesis the smaller of the two). The system is read again while the cache moves (ten times a second at most), once a second while it holds frames and stays still, never while it is empty. TheBP_SIMULATED_COMMIT_GBtest variable makes the commit that many GB, and underBP_SIMULATED_RAM_GBthe RAM free is the smaller machine's too;BP_TEST_REFUSE_ALLOCATIONS=<n>refuses every n-th picture buffer of the decoders (a refused allocation never ends the player: the cache gives memory back, the frame is tried again, then skipped; a refusal that comes back with nothing decoded between waits longer each time, up to 2 s, and after six the cache is left alone, with one line in the log).process_private_bytes/process_resident_bytesare the process's private bytes (Linux: anon-rss) and working set (RSS),process_counted_byteswhat the ceiling counts,process_peak_*their highest this session.pressureis the memory-pressure rule:level(none,moderate,severe),cap_bytes(the cap it puts on the cache,0none),sheds, and what it read (psi_*from/proc/pressure/memoryon Linux,mem_available_bytes).smartis the tier of the clip on screen:screen_tier(never below what the viewer or the screen shows; 1 at zoom >= 100 %, with the scopes open, or forced),shown_frame_scale(the tier of the frame on screen),shown_reduced,badge_rect,viewer_px,screen_px,shown_px_per_src_px(the user's zoom; 0 in fit),pipette_reading(a pipette held a second on a paused picture, now),scopes_open,forced_full_clips.badgeis what the viewer's badge says:visible,text("Reduced 1/4"; for the pixel probe"Probe: original…"while its original is read and"Probe: original"once it reads it, the picture on screen staying the reduced frame; at a zoom of 100 % or more"Original…"then"Original"),ready(the original is there),source(smart: Smart's screen tier;slow_tier: the "When Decoding Is Too Slow" rule lowered it;original;none),reasonof an original (pipette,zoom),tierandtooltip.original_frameis the pipette's original: a pipette reading on a reduced frame reads the original of THAT frame only, beside the cache (the clip keeps its tier), until Play or another frame:state(idle,reading,shown),clip,frame,layer,width,height,bytes(counted inposts.decode_in_flight),read_msandshown_msfrom the request, and the countsrequests,drops,stale,failures.original_readeris its reader, one thread at a time where the latest request wins:alive,threads_started,in_flight(0 or 1),max_in_flight,waiting,posted,superseded,decoder_opens(a film's decoder is opened once per film while the thread lives; the thread ends at Play or another clip),decoder_bytes(what that decoder keeps between two frames, counted inposts.decode_in_flight) anddecoder_releases(the times room took it: it is the first thing to go when the playlist fit needs room or the system is pressed, before a tier falls or the cache sheds).pipetteis its last readout:read_scale(the tier of the frame it read),preview(true while that is a reduced frame) andrgb.preload_window: the sliding preload window, highest priority first:entries[]withitem(playlist index),frames(-1the whole clip, else its first frames) andwhy(on screen,next, start,next, plays next,previous, start,whole playlist);pausedwhile the user scrubs or draws;precache_whole_playlist.gpu_decode: the beta that decodes.nvfxframes on the graphics card (set-gpu-decode; see NOVA).enabledis the preference;settingis"user"when the user turned it on or off (Preferences, the Inspector or this IPC; kept whatever a release's default) and"default"otherwise;on_by_defaultis this release's default;summaryis the line Preferences shows under the switch, in plain words (who decodes the picture on screen and why, or why it is off);stateis"off","starting","ready","unavailable"(the card or pilot cannot do it) or"disabled"(turned off until the next start after failures);reasonis written only when there is one (why it is unavailable or disabled);decoder_allocatedisfalsewhenever the beta has been off since the start.frames_preparedcounts the frames the workers packed for the GPU,frames_gputhe frames decoded on the GPU into a texture (the VRAM ring's and the picture's),ring_copiesthe pictures copied from the VRAM ring instead of decoded again,fallbacksthe frames the CPU decoded after all at upload,failed_codeblocksthe code-blocks the GPU found broken.vram_bytesis the video memory the GPU decoder itself holds (0without a decoder; see GPU decode (beta)).clipis"gpu"when the picture of the clip on screen (track A) is a GPU-packed frame,"cpu"for any other picture,"none"without a picture; with the beta off it is never"gpu".gpu_yuv: the beta that keeps movie frames as the decoder's YUV planes and converts them on the graphics card (set-gpu-yuv; see section 10).enabledis the preference;setting,on_by_defaultandsummaryas forgpu_decode;stateis"off","starting","ready","unavailable"(the card or driver cannot do it) or"disabled"(turned off until the next start after failures);reasonis written only when there is one (why it is unavailable or disabled);converter_allocatedisfalsewhenever the beta has been off since the start.frames_planescounts the pictures the decoders handed out as planes,frames_gputhe pictures converted on the GPU (the viewer's and the VRAM ring's),fallbacksthe planes frames shown through the CPU twin.clipandclip_bare the route of track A's and track B's film:"planes","cpu-rgb"or"none"(no movie picture read since the beta was turned on, or no film on that track);clip_reasonandclip_b_reasonsay why the CPU kept it (for example"a reduced picture (proxy tier)","the source has alpha","deep 4:4:4: its planes would weigh twice the RGB picture","a pixel format the path does not take","a subsampled picture of odd size","libjpeg-turbo decodes this film straight to RGB","not armed yet","the beta is off"). With the beta off,clipis"cpu-rgb"andclip_reason"the beta is off".vram_bytesis the video memory the converter's plane textures hold now, in bytes, andstaging_bytesthe RAM of its upload buffers: both0while the beta is off, and after 3 seconds without a conversion (they are freed, and made again with the next frame).gpu_video_decode: the beta that decodes movies on the graphics card's video decoder and hands the pictures out as YUV planes (set-gpu-video-decode; see section 11).enabledis the preference;setting,on_by_defaultandsummaryas forgpu_decode;stateis"off","starting"(the device being made, or waiting for the OpenGL 4.3 converter),"ready","unavailable"(no hardware video decoder could be opened on this machine, or no OpenGL 4.3) or"disabled"(turned off until the next start after failures);reasonis written only when there is one.apiis"nvdec","d3d11va","vaapi"or"none".decoders_on_gpucounts the decoders open on the card now (at most 4),vram_bytesthe video memory they hold (an estimate per decoder: about 0.8 GB for an 8K 8-bit HEVC, 1.6 GB at 10 bits;0with none open) andvram_room_byteswhat the card's memory gives them together (half of what the VRAM ring and they may share; on a decoder card that is not the drawing one, or whose budget the player does not read, what that card itself has free less a margin of a tenth of it, at least 512 MB; 2 GB when neither is known);vram_room_sourcesays which:"drawing card budget","decoder card memory"or"fixed fallback".frames_gpucounts the pictures the card decoded,fallbacksthe decoders that opened their film again on the processor after a failure of the card,refusalsthe films the card refused at their open (FFmpeg decoded them on the processor; the next films of that kind and size stay on the processor for 2 minutes, four times longer each time the card refuses them again, up to 8 hours, or until the beta is turned on again).clipandclip_bare the route of track A's and track B's film:"nvdec","d3d11va","vaapi","cpu"or"none";clip_reasonandclip_b_reasonsay why the processor decoded it, one of:"a 4:2:2 or 4:4:4 MJPEG: the card's JPEG decoder would halve its color detail","a codec this graphics card's decoder does not take","a chroma subsampling this graphics card's decoder does not take","a bit depth this graphics card's decoder does not take","larger than this graphics card's decoder takes","a pixel format the GPU path does not take","a subsampled picture of odd size","a reduced picture (proxy tier): the processor decodes and reduces it","this graphics card's decoder does not take this film (its own answer)","the graphics card refused a film of this kind and size before","the graphics card refused this film at its open","the graphics card failed on this film: the processor decodes it","the GPU path is not ready","opened before the GPU path was ready (taken at its next seek or film)","the graphics card's video memory left for its decoders is taken","the graphics card's decoders are all in use". With the beta off,clipis"cpu"andclip_reason"the beta is off".ram-infocounts the card's frames aspixel_type"yuv-planes", as for the GPU YUV upload.gpu_prores: the beta that keeps a ProRes film's frames in RAM as the compressed ProRes packet and decodes them on the graphics card (set-gpu-prores; see section 12).enabledis the preference;stateis"off","starting","ready","unavailable"(the card, driver or FFmpeg cannot do it) or"disabled"(turned off until the next start after failures);reasonis written only when there is one (why it is unavailable or disabled);engine_allocatedisfalsewhenever the beta has been off since the start, andvulkan_devices_createdcounts the Vulkan devices this process created for the engine (0while the beta has been off since the start).vram_bytesis always present: the memory of the decoders (FFmpeg's frame pools on the card, shared with OpenGL),0while the beta never ran and once the decoders have closed (a decoder unused for 3 seconds closes and its memory goes). When the engine is allocated,deviceis the card's name,copy_freeistrue(the frames are shared with OpenGL without a copy) anddecodersthe decoders open now.frames_packetscounts the pictures the decoders handed out as packets,frames_gputhe pictures decoded on the GPU (the viewer's and the VRAM ring's),fallbacksthe packet frames shown through the processor decode.clipandclip_bare the path of track A's and track B's film:"vulkan","cpu"or"none"(no ProRes picture of that track read since the beta was turned on, or no film on that track);clip_reasonandclip_b_reasonsay why the processor kept it (for example"a reduced picture (proxy tier)","an interlaced picture","an unknown ProRes profile","a 4:2:2 picture of odd width","more than this integrated GPU decodes in real time at this frame rate","a picture size FFmpeg's Vulkan ProRes decoder gets wrong (sides not multiples of 8)","larger than the path takes","not armed yet","the beta is off"). With the beta off,clipis"cpu"andclip_reason"the beta is off"for a film on screen.ram_fit: what Smart RAM-Fit decided for the playlist as a whole. Every Smart item decodes at its own screen ceiling, never under it, whatever the budget: a playlist that does not fit keeps its tiers, holds fewer frames ahead and reads the rest from disk (earlier versions lowered a shared size ceiling until it fitted).ceiling_px(always0, no ceiling) andcommon_tier/common_scale(always0/1.0) are kept for the scripts that read them.estimated_bytesagainstbudget_bytes(the RAM limit less the margin background caching keeps free) is the sum the decision was made on.statusis"fits","needs_fixed_items","does_not_fit"or"unmeasured", andfitsis the first of those as a boolean — sofitsisfalsefor the other three.suggested_fixed_tieris-1unless the playlist would fit if the items whose tier you fixed came down to that tier — the playlist panel offers exactly that, andram-fit-applyis the offer accepted.banneris the playlist panel's RAM-fit banner:show(something to say),shown(and on screen: not waved away in this run, notifications allowed; the offer also not dismissed for good -- it is then left out --, the RAM short whatever was saved),offer(Apply would change a row you fixed),short(the RAM is short for the Smart clip on screen: it keeps its tier, fewer frames ahead, the rest read from disk),lineandquestion(its two lines; withshortand no offer, the second gives the RAM free on the computer and "close other applications"). A video longer thanstreamed_video_secondsis played from disk and left out of the sum entirely; an image sequence never is.ram_fit.unmeasured_countand"status": "unmeasured": how many items the player has not been able to measure, and therefore how many weightsestimated_bytesis short of. An item with no measured size weighs nothing — there is nothing to weigh — so a playlist holding one clip the player cannot open sums to the rest of the list. That sum used to be reported as"fits": an answer of yes to a question nobody could answer. It now reads"unmeasured"withfitsfalse, and the count says how many items are missing from the total. Precedence is deliberate: a playlist already pastbudget_byteson what is measured reports"does_not_fit"or"needs_fixed_items"even whileunmeasured_countis above zero, because those are certain and actionable and measuring the rest can only make them worse —"unmeasured"replaces"fits"and nothing else. A streamed video is never counted here: it is left out of the sum by rule rather than by ignorance, and its size would add nothing. So: to decide whether the playlist really fits, testfits; to tell "no" from "not yet", readstatus; to know how much of the list the total speaks for, readunmeasured_countagainst the length ofplaylist[]. A clip stays unmeasured until it can be opened for its dimensions — the player retries it every 30 s — so anunmeasured_countthat does not fall is a file to look at, not a wait.ram_fit.track_b: the clip on track B, which the fit weighs as one more item but which is not aplaylist[]row. Track B's frames are cached under a key of their own, so a clip shown on both tracks is held twice and a track-B file that is in no playlist is held once with nothing in the list to account for it —in_playlisttells the two cases apart.presentisfalseand no other field is written when track B is empty.effective_proxy_idxis the tier that copy decodes at,estimated_byteswhat it holds there, andresidenthow many of its frames are actually in RAM (the track-B equivalent ofram-info'scached_frames, which counts by path and therefore cannot see this copy). Ifestimated_byteshere is missing from your own sum,ram-info'scache_byteswill run ahead ofram_fit.estimated_bytesby roughly this much.shares_rowandshares: when B shows frames another clip holds -- a sequence lent a playlist row's frames (track_b.from_playlist.cache.shared_from), a film that is a row's or track A's (one copy under its path) --shares_rownames the row (-1for none, or for track A's film on no row -- no entry weighs A then, so B is weighed).sharesistrue, andestimated_bytes0, only when that row covers B: it holds B's frames at the tier B decodes at (its own tier in effect at least B's) and B holds none of its own (own_bytes, what B holds under its own key,0) -- the row's entry then weighs those frames once, at B's screen tier while B shows it, so B no longer lowers the ceiling for frames it does not hold. A row at a lower tier (a fixed 1/4 row with B on screen at 1/1), or frames B decoded itself, and B weighs as before.playlist[]: one entry per playlist item, in playlist order.proxy_idxis the tier the item asks for (4= Smart RAM-Fit), as saved;proxy_idx_effectivethe one in effect (4for every item while Essential decides, see Interface Level);effective_proxy_idxandeffective_proxy_scaleare what it actually decodes at. They differ for every Smart item the fit moved — reading onlyproxy_idx, or onlyviewport.proxy_idx, tells you nothing about the off-screen items.estimated_bytesis what that item would hold at its effective tier (0for a streamed video);ram-info'scached_framesagainst the item'sdurationsays how much of it is actually resident.measuredisfalsewhile nothing has read this item's dimensions — a truncated or half-written file, a path that will not open — and while it isfalsethe other two numbers are placeholders, not readings:estimated_bytesis0because there is nothing to weigh, andeffective_proxy_idxis0(1/1) because the tier formula answers full size for a width of0. Do not sumestimated_bytesacross the array without checkingmeasured, or checkingram_fit.unmeasured_countonce; the playlist panel draws?for both numbers on such a row, and the same field is onram_fit.track_b.
5. The Python Client API (bernard_api.py)
We provide a pre-built Python module bernard_api.py that wraps the socket protocol into simple object-oriented methods and an event-callback system.
Basic Client Usage
from bernard_api import BernardClient
import time
# Create a client instance pointing to localhost, port 5556
client = BernardClient(host="127.0.0.1", port=5556)
# Connect to the player. This starts a background thread to listen for events.
if client.connect():
# Load a video file
client.open_media("C:/videos/shot_01.mov")
# Wait for 1 second, then play
time.sleep(1)
client.play()
Errors
request() (and every method that returns the player's reply) returns an error reply as it returns any other: at once, as a dict. It does not raise; None still means a time-out or a lost connection.
reply = client.request({"cmd": "no-such-command"})
if reply is None:
print("no reply: timed out or disconnected")
elif reply.get("status") == "error":
print("refused:", reply["reason"]) # "unknown command: no-such-command"
request() raises ValueError, before anything is sent, only for a request it cannot build: no cmd, a cmd that is not a string, or a value JSON cannot carry (NaN, infinity). A refusal with no cmd in it (a hand-built line that was not JSON, sent with send_command()) goes to the callbacks registered with client.on("error", ...).
The Steps view from a script
The Steps view's stack is built and driven with the steps_* methods, each a wrapper of steps (section 2, "What
the Viewer Shows"); each returns the player's reply, the stack's state, or its refusal with a reason. Ask the player
how many steps a stack holds (steps_max(), the state's max_steps) rather than assume a number.
import time
client = BernardClient(port=5556)
client.connect()
most = client.steps_max() # how many steps a stack holds in this player
reply = client.steps_build(
[True, # the playlist on screen: the clock
"D:/review/anim.bpl", # a playlist file (.bpl, OTIO, CSV, EDL)
["D:/SQ010/SH010_lay_v003.mov", "D:/SQ010/SH020_lay_v002.mov"]], # rows, one media per line
names=["Comp", "Anim", "Layout"], current=1, display="layout")
if reply.get("status") == "error":
print("refused:", reply["reason"])
# A playlist file is read off the render thread: the stack is there once `loading` is 0 (a file that could not be
# read is said then, in `refusal`).
while True:
state = client.steps_state()
if state.get("loading") == 0 and len(state.get("steps", [])) == 3:
break
time.sleep(0.1)
client.steps_set_current(2) # Layout on screen
client.steps_look("wipe") # the pair wiped
client.steps_display("stack") # one step at a time again
client.steps_clear()
6. Studio Integration Examples (Maya & Nuke)
Here are production-ready integration scripts. They include extensive comments explaining the code mechanics for technical artists and TDs.
A. Autodesk Maya Timeline Synchronization
This script runs inside Autodesk Maya. It connects to Bernard Review and ensures that when the artist scrubs the timeline in Maya, Bernard Review jumps to the same frame. Conversely, when the video plays in Bernard Review, Maya's timeline follows in real-time.
# ==============================================================================
# MAYA & BERNARD REVIEW TIMELINE SYNC SCRIPT
# ==============================================================================
# This script sets up a bi-directional frame synchronization.
# 1. Maya to Player: ScriptJob listens for Maya timeline changes and tells the player.
# 2. Player to Maya: Sockets listen for player events and update Maya's currentTime.
# ==============================================================================
import maya.cmds as cmds
from bernard_api import BernardClient
# We store the client in a global variable to prevent Python from garbage-collecting it
global_bernard_client = None
maya_sync_job_id = None
is_updating_timeline = False # Guard variable to prevent infinite loops of feedback
def on_player_frame_changed(event_data):
"""
Callback function triggered whenever Bernard Review broadcasts a 'frame_changed' event.
"""
global is_updating_timeline
# If Maya itself triggered this update, ignore it to prevent loopback loops
if is_updating_timeline:
return
# Get the frame number from the player's JSON payload
player_frame = event_data.get("frame", 0)
# Mark that we are programmatically updating Maya's timeline
is_updating_timeline = True
try:
# Move Maya's playback head to the player's current frame
# We run this on Maya's main thread idle queue to avoid crashes
cmds.evalDeferred(f"cmds.currentTime({player_frame}, edit=True)")
finally:
# Release the guard once the update is enqueued
is_updating_timeline = False
def on_maya_time_changed():
"""
Callback function triggered whenever Maya's current time changes.
"""
global is_updating_timeline, global_bernard_client
# If the change was triggered by the player, ignore it
if is_updating_timeline:
return
# Verify the client is connected
if global_bernard_client and global_bernard_client.connected:
# Get the current frame in Maya
current_maya_frame = int(cmds.currentTime(query=True))
# Mark that we are updating, and send the seek command to the player
is_updating_timeline = True
try:
global_bernard_client.seek(current_maya_frame)
finally:
is_updating_timeline = False
def start_maya_sync():
"""
Initializes the connection and sets up the synchronization callbacks.
"""
global global_bernard_client, maya_sync_job_id
# 1. Connect to the player
global_bernard_client = BernardClient()
if not global_bernard_client.connect():
cmds.warning("Could not connect to Bernard Review. Make sure the player is open.")
return
# 2. Register callback: listen for player timeline changes
global_bernard_client.on("frame_changed", on_player_frame_changed)
# 3. Register Maya scriptJob: listen for Maya timeline changes
# 'timeChanged' is a built-in Maya event triggered when the playhead moves
maya_sync_job_id = cmds.scriptJob(event=["timeChanged", on_maya_time_changed], protected=True)
print("Bernard Review synchronization successfully established in Maya!")
def stop_maya_sync():
"""
Cleans up the callbacks and disconnects the client.
"""
global global_bernard_client, maya_sync_job_id
# Remove the Maya scriptJob to stop listening to Maya's timeline
if maya_sync_job_id is not None:
cmds.scriptJob(kill=maya_sync_job_id, force=True)
maya_sync_job_id = None
# Close the network socket
if global_bernard_client:
global_bernard_client.connected = False
if global_bernard_client.sock:
global_bernard_client.sock.close()
global_bernard_client = None
print("Bernard Review synchronization stopped.")
B. Foundry Nuke Node Render & Open Script
This script adds a custom button to Nuke Write nodes. Clicking the button renders the active frame range and immediately opens the resulting file in Bernard Review.
# ==============================================================================
# NUKE RENDER & OPEN IN BERNARD REVIEW
# ==============================================================================
# This script integrates into Nuke's menu/Write nodes.
# It renders the node's frame range and tells Bernard Review to load and play it.
# ==============================================================================
import nuke
import os
from bernard_api import BernardClient
def render_and_play_in_bernard(write_node):
"""
Renders the specified Write node and loads the output file in Bernard Review.
"""
# 1. Retrieve the file path from the Write node
output_path = nuke.filename(write_node)
if not output_path:
nuke.message("No file path configured on this Write Node.")
return
# 2. Get the rendering range from Nuke's project settings
first_frame = int(nuke.root().firstFrame())
last_frame = int(nuke.root().lastFrame())
# 3. Perform the render
nuke.message(f"Starting render of frames {first_frame} to {last_frame}...")
try:
# Execute render in Nuke (blocks until finished)
nuke.execute(write_node, first_frame, last_frame)
except Exception as e:
nuke.message(f"Render failed: {e}")
return
# 4. Connect to Bernard Review and load the rendered file
client = BernardClient()
if client.connect():
# Load the rendered video or image sequence path in the player
client.open_media(output_path)
# Seek to the first frame of the sequence
client.seek(first_frame)
# Start playback automatically
client.play()
else:
# If the player isn't open, print instructions
nuke.message("Render completed! Launch Bernard Review to view the result.")
7. ShotGrid & Kitsu Web Sync Daemons
If you want to sync the player's timeline with a web-based production tracker (like ShotGrid's Web Player), you can write a lightweight Python daemon that runs in the background. It connects to the Bernard Review via bernard_api.py and relays the events to the web app using websockets or by updating fields via the SG/Kitsu REST API.
# ==============================================================================
# MOCKUP: SHOTGRID/KITSU WEB SYNC DAEMON
# ==============================================================================
# This daemon runs in the background. It listens to frame change events from
# Bernard Review and prints the REST API payloads to send to your studio website.
# ==============================================================================
from bernard_api import BernardClient
import sys
import time
def send_time_to_shotgrid(event_data):
"""
Stub simulating sending the current playhead position to ShotGrid/Kitsu.
"""
frame = event_data.get("frame", 0)
# Inside a real pipeline, you would send an HTTP POST request to your website:
# requests.post("https://studio.shotgrid.com/api/v1/sessions/sync", json={"frame": frame})
print(f"[ShotGrid Sync] Broadcasting frame {frame} to Web Interface...")
def run_sync_daemon():
client = BernardClient()
print("Starting Studio Sync Daemon...")
# Try connecting to the player
while not client.connect():
print("Waiting for Bernard Review to start... Retrying in 5s.")
time.sleep(5)
# Register the sync function to run whenever the frame changes in the player
client.on("frame_changed", send_time_to_shotgrid)
# Keep the main daemon thread alive
try:
while client.connected:
time.sleep(1)
except KeyboardInterrupt:
print("Daemon stopped by user.")
sys.exit(0)
if __name__ == "__main__":
run_sync_daemon()
8. Full Review Round-Trip: Playlist In, Notes Out (Bernard Engine / ShotGrid / ftrack)
Section 7 above only pushes the playhead position out to a web viewer. This section covers the other two legs a production-tracking integration actually needs: loading a review session in (a playlist built by your pipeline tool) and pulling notes back out (what the supervisor typed/drew during the session) once review is done.
What works today
- Playlist in:
load-playlistaccepts a.bplfile -- Bernard Review's own JSON playlist format, documented inplaylist.schema.json. Build one from Bernard Engine / ShotGrid / ftrack by writing that JSON directly (no player-side parsing beyond what already exists) and pointingload-playlistat it. - Notes out:
export-notestriggers the player's existing review-notes export (general per-shot text notes, plus every annotated frame as a JPG with its drawn strokes) to a folder you choose, silently (no dialogs). The output shape is documented innotes_payload.schema.json. - Production-tracking IDs round-trip:
x_tracking(shot_id,shot_name,version_id,shotgrid_version_id,ftrack_id,artist,task) is set when building the.bplpassed toload-playlist, persists through.bplsave/autosave, and comes back out per-shot inexport-notes'export.json-- a connector can route a note back to the right ShotGrid Version / ftrack entity by ID directly, no filename matching needed. See thex_trackingblock in both schema files. - Screenshot:
screenshotcaptures any single frame of the active track-A item to a PNG, graded exactly as currently shown on screen. - A ready-to-adapt example tying all of this together, including the launch/
connect/retry dance and real ShotGrid/ftrack push stubs keyed off
x_tracking's IDs, is inbernard_player_connector.py.
What a pipeline can count on: capabilities.json
A version number does not tell which of the features below a player holds: two lines of development can share
numbers. capabilities.json, installed with this client (bin\api\ on Windows), lists them by
name in its "bernard" array. A pipeline reads it before starting the player, and leaves aside a player that lacks a
feature it needs. A name is only listed with its feature, and stays as long as the feature does.
| Name | What it means |
|---|---|
singleshot | --singleshot with --ipc-port: a player of its own, on a throwaway copy of the profile's settings (the cache stays the user's), answering on the IPC only on the given port |
review-dir | --review-dir <dir>: the review written there when the player closes, whatever closed it (beside it, then renamed onto it; the event within five minutes), then the review-exported event |
x-tracking-bernard | x_tracking keeps a pipeline's own keys (Bernard Engine's bernard block) through the .bpl, the autosave and export.json |
playlist-add-x-tracking | playlist-add takes the row's x_tracking and answers the new row's index |
versions-on-demand | build-matrix takes a department's versions in one array; media-request, media-ready and play-version |
earlier-notes | earlier-notes-request and earlier-notes: a row's earlier notes, read-only, in Essential's inspector and in Review Notes |
review-exporting | review-exporting: sent when the window of a --review-dir player hides at the close, with the number of pictures the export is to write; the annotated frames' pictures of several shots are written at once |
earlier-notes-live | earlier-notes also takes total and unasked newest pages for an open row; the player sends earlier-notes-closed when a row is open nowhere any more |
Known limitation
x_tracking and screenshot are both track-A / playlist-only for now: track
B (set-track-b) isn't backed by a PlaylistItem at all, so there's nowhere
to attach tracking IDs or read grading/flip state from for a track B shot.
9. NOVA (.nvfx)
NOVA (.nvfx) is the player's own review format, and it is in the default
build. A .nvfx sequence is opened like any image sequence -- open, load,
playlist-add, set-track-b, drop -- and exported with export and
"format": "nvfx" (alias nova), or an output whose extension is .nvfx.
The built-in presets nvfx_review (HTJ2K, quality 92) and nvfx_lossless
appear in export-presets, and export takes these parameters for the
format:
compression:htj2k|wavelet|lossless|raw(the NOVA profile; defaultwavelet).quality: 1..100 for the two lossy profiles, 92 = visually lossless.bit_depth: 16 (half) or 32 (float).all_layers(defaulttrue): every layer of a multi-part EXR source becomes an AOV of the frame.temporal_gop(htj2k only): 0 = intra only; N = one intra frame then N-1 P-frames predicted from the previous frame, reprojected through camera and depth when the render carries them. 0..64.bc6h: also write the main pass as BC6H GPU blocks, 1 byte / pixel in VRAM.noise_floor(htj2k / wavelet): -1 or"auto"measures the frame's grain and spends no bits under it, the default; 0 or"off"keeps 0.6 % of every value; a number is the one-sigma noise in linear units.noise_floor_fade(htj2k): the noise floor fades out near zero, so the darks of a grainy plate get no dead zone -- no exact zero the source does not have."lo,hi"in sigmas of the measured grain, default"0,0.5"(the knee is 0.002 up tolo, ramps up to the noise floor's knee athi, then stays there): the smallest ramp that left no zero on the night and interior plates it was measured on, for +0.1 to +2.0 points of size.[lo, hi]says the same;"default","on"ortruemean"0,0.5";"off"(orfalse) writes the floor as before. The comma separates the two bounds and the point is the decimal, whatever the locale:"0,2"means 0 to 2 sigma, not 0.2, and a bare number is refused. Bounds: 0 <= lo <= 20, lo < hi <= 41. No effect without a noise floor or with the other profiles. Frames written with it carry HTJ2K chunks of version 2: this player and later ones read them, a player or tool built on an older codec refuses them (it never shows wrong pixels);"off"keeps files readable everywhere. Seedocs/nova_integration.md.
{"cmd": "export", "source": "/renders/sh020/beauty.1001.exr", "output": "/review/sh020_nvfx/", "format": "nvfx", "preset": "nvfx_review"}
bernard_api.py's export() passes these through untouched, as it does every
other option. docs/nova_integration.md has what the player does with the
format and its known limits.
GPU decode (beta)
The player can decode a .nvfx color pass on the graphics card (OpenGL 4.3
compute) instead of the processor. It is a beta and off by default:
set-gpu-decode turns it on or off (Preferences > Cache & Performance),
and status.gpu_decode reports its state and counters. The picture is bit for
bit the CPU decode's. The processor still decodes track B, data passes, passes
not coded in HTJ2K, P-frames, the first frame of each clip, and every frame
when the cache precision is not half. Any GPU failure on the frame on screen
decodes that frame on the CPU at once; repeated failures turn the path off
until the next start. A card without OpenGL 4.3 compute keeps the CPU decode.
The decoder's own video memory (status.gpu_decode.vram_bytes) is bounded: two
coefficient buffers of at most 256 MB each, two half-float pictures of the
frame (the frame before its copy into the picture's texture, and the full frame
a proxy tier is reduced from: 2 x 253 MB at 8K) and three upload sets of the
packed frame with an eighth of headroom -- about 1 GB plus 3.4 times the packed
frame at 8K. It is given back when the beta is turned off or turns itself off.
The VRAM ring makes room for it: before the decoder grows for a frame larger
than any it decoded before, the ring frees that growth first (the other clips'
textures, then those farthest from the frame on screen). The ring's budget is what the card can
still give the player less everything the player already uses there, measured
again on the frame after the decoder's memory grows (the regular 5 s measure
remains for a driver that commits its memory late). Where the card's memory
cannot be read (the ring's fixed 1.5 GB) and under the ring's share of the RAM,
the decoder's memory is taken out of the ring's budget. On Windows that memory
is also part of the process's private bytes, which the RAM limit counts. A
frame neither the card nor the processor could decode is not shown: the
picture on screen stays and the frame is tried again (three such frames in a
row turn the beta off until the next start).
A player built without NOVA
The format is a build option, BERNARD_ENABLE_NOVA, on by default. A
player configured with -DBERNARD_ENABLE_NOVA=OFF behaves as if the format did
not exist: status never names it, no built-in preset produces it, no dialog
filter offers it, and the parameters above do not apply. It answers a request
for the format rather than ignoring it:
{"cmd": "open", "path": "/mnt/show/sh020.nvfx"}and{"cmd": "playlist-add", "path": "..."}reply{"status": "error", "reason": "This format is not supported in this build.", "path": "..."}.{"cmd": "set-track-b", "path": "..."}, and adropwhose target is track B ("at": "track_b"or"viewport_b"), reply the same way ("cmd"is theirs); whatever track B held stays there.- A
dropanywhere else (track A's row, a screen point, no point) refuses the file the same way, with"refused"listing every refused path; when other files were dropped with it, they go on and the reply is"ok"with that"refused"list alongside. - An
exportjob with"format": "nvfx"-- or anoutputwhose extension is.nvfx-- comes back with the same sentence among its validation errors. The name still parses on purpose, so a caller is told why instead of being handed the list of formats to compare its own request against. BernardReview --export --format nvfx ...printsexport: This format is not supported in this build.and exits non-zero; the usage line's--formatlist does not mention it.
The client has nothing of its own to hide: the same export() call is either
honoured or refused by the player depending on how it was built.
10. GPU YUV upload for movies (beta)
The player can keep a movie's frames in RAM as the YUV planes the decoder wrote
and let the graphics card convert them to RGB (OpenGL 4.3 compute) when they go
up. It is a beta and off by default: set-gpu-yuv turns it on or off
(Preferences > Cache & Performance), and status.gpu_yuv reports its state,
counters and the route of each track's film.
- What changes. Decoded frames stay planar or semi-planar (8 to 16 bits; 4:2:0,
4:2:2, 4:4:4) instead of the RGB24 picture the processor makes, so the RAM cache
holds a 4:2:0 8-bit frame in 1.5 bytes a pixel instead of 3 (4:2:2: 2, or 4 at
10-12 bits; 4:4:4 8-bit: 3, the same), and the processor skips the RGB conversion. The picture goes into the viewer's texture, RGBA8 for
8-bit sources and RGB10_A2 for deeper ones, so 10 bits are kept on screen; every
reader of that texture (grade, A/B, wipe, scopes, paint, online stream) is
unchanged.
ram-infocounts these frames aspixel_type"yuv-planes". - Color. The rule is today's processor conversion: BT.601, full range only for the "J" formats (MJPEG, full-range 8-bit H.264 / HEVC), limited otherwise; the stream's color tags are not read. The GPU picture is within 1 code of the exact conversion; against the processor's picture it is within 1 code for 4:4:4 and up to 3 (8-bit 4:2:x) or 4 (10 / 12-bit 4:2:x) codes, because the processor's own RGB writer is that far from the exact conversion. The screen also clamps out-of-range codes, while the processor's picture (swscale, as the player ships it) wraps some of them: luma 253 with U 251 in a 4:4:4 picture reads blue 0 on the processor where the screen shows 255. The processor readers (see below) and the exports read the processor's picture, so they can differ from the screen by those few codes.
- What keeps the processor conversion (automatic, picture by picture): a reduced
picture (a proxy tier below 1/1), a source with alpha, a deep 4:4:4 source (ProRes
4444, DNxHR 444 at 10-12 bits: its planes would weigh 6 bytes a pixel, twice the RGB
picture, and take twice as long to upload), a pixel format the path does not take
(RGB, palette, big-endian, packed YUYV, 4:1:1, 4:4:0, Bayer...), a subsampled picture
of odd width or height, a side above 16384 px, MJPEG films that libjpeg-turbo decodes
straight to RGB (8-bit 4:4:4 at full size), camera RAW and image sequences. The
first picture of each decoder is always the processor's.
status.gpu_yuv.clipandclip_reasonsay which route a track took. - Never a black frame. If the card fails on a frame, the processor's picture of
the same planes is shown at once; three failures in a row turn the beta off until
the next start (
state"disabled", the converter's video memory freed) and the planes frames are dropped from RAM (only those). A frame the VRAM ring could not convert is simply left out of the ring and never counts as a failure. If the processor copy fails too, the frame is not counted as shown and is tried again at the next draw. A card or driver without OpenGL 4.3 (macOS, old drivers) says"unavailable"and keeps the processor conversion.Initalso checks the conversion of a small picture of every layout it can convert (8 and 10-12 bits, three planes, NV12 and P010, 4:2:0 / 4:2:2 / 4:4:4, limited and full range, BT.601 / BT.709 / BT.2020) against the exact result at one code, and a card that fails any of them says"unavailable"and keeps the processor conversion. - What reads on the processor. The pixel probe, the curves' histogram and an annotated frame's timeline thumbnail read a planes frame through a processor copy converted on a thread of its own, so they show nothing for the moment that takes; the technical check and the A/B difference band convert on their own low-priority thread. A Levels picker's click converts the clicked frame at once (the click always reads). Planes frames are not written to the disk cache: they decode again.
- Memory. The planes go up through a pair of pixel buffers (persistently mapped
where the driver has OpenGL 4.4 buffer storage; never on Intel, as the player's own
uploads), so the copy to the card runs on its DMA instead of holding the drawing
thread. The plane textures are kept per picture layout, two layouts at most (the two
tracks of an A/B of different sizes or depths each keep theirs). They and the pixel
buffers are freed 3 seconds after the last conversion and when the beta is turned
off. A buffer the card still reads is never written: that frame goes up without it.
status.gpu_yuv.vram_bytesis what the plane textures hold (about 50 to 210 MB a layout at 8K) andstaging_byteswhat the pixel buffers hold in RAM (up to twice a frame's planes). The pixel buffers are counted in the RAM budget, the plane textures by the VRAM ring's budget (on an integrated GPU, by the RAM budget too).
11. GPU video decode (beta)
The player can let the graphics card's own video decoder decode a movie. It is a
beta and off by default: set-gpu-video-decode turns it on or off
(Preferences > Cache & Performance, and a checkbox of the same name in the
Classique tabs), and status.gpu_video_decode reports its state, counters and
the route of each track's film. Off, nothing of it runs: no thread, no device, no
decoder armed.
- What it does. The card decodes the movie through FFmpeg's hardware
acceleration. Each picture is copied once into RAM as YUV planes (one transfer
straight into the RAM cache's buffer, no conversion on the processor), and the
OpenGL 4.3 compute converter of the GPU YUV upload (section 10) turns it into RGB
on the card when it is shown. The converter serves these frames whether or not
the GPU YUV upload for movies (beta) setting is on; that setting still decides
for the films the processor decodes. The older Enable Hardware Decode (Beta)
setting (
set-hardware-decode) is unchanged; when both are on, this beta takes the films it decodes. - Which cards. Measured on NVIDIA cards (NVDEC through CUDA, and D3D11VA) and on
Intel graphics (D3D11VA), on Windows. The API is chosen automatically: NVDEC when an
NVIDIA driver is present, else D3D11VA on Windows or VAAPI on Linux; other cards
and Linux are not measured yet. NVDEC takes MJPEG 4:2:0 8-bit; H.264 4:2:0 and 4:2:2
at 8 or 10 bits; HEVC 4:2:0, 4:2:2 and 4:4:4 at 8 to 12 bits; up to 8192 px a side
(MJPEG 16384) -- what the newest NVIDIA cards decode: the player asks the card itself
once per kind of film (
cuvidGetDecoderCaps) and keeps on the processor what an older card does not take (before the 2025 generation: no H.264 10-bit nor 4:2:2, no HEVC 4:2:2, H.264 up to 4096 px), with the reason"this graphics card's decoder does not take this film (its own answer)". D3D11VA takes H.264 4:2:0 8-bit and HEVC 4:2:0 8 or 10 bits, and no MJPEG in FFmpeg; VAAPI the same, plus MJPEG 4:2:0 8-bit.status.gpu_video_decode.apisays which one is in use. - What stays on the processor (automatic, film by film): 4:2:2 and 4:4:4 MJPEG
(NVIDIA's JPEG engine hands out 4:2:0 only, which would halve the color detail),
H.264 4:4:4, a source with alpha, a reduced picture (a proxy tier below 1/1), a
subsampled picture of odd size, ProRes, DNxHR and the other codecs. Up to 4
decoders are on the card at a time (the fill's decoders take 3 at most: the last one
is kept for the film on screen); the others stay on the processor. A tier lowered
below 1/1 while a decoder is on the card sends it to the processor at the frame it
was reading.
status.gpu_video_decode.clipandclip_reasonsay which route a track took. - Video memory. Each decoder on the card holds its decode surfaces (about 0.8 GB for
an 8K 8-bit HEVC, 1.6 GB at 10 bits, less for MJPEG): they are counted
(
status.gpu_video_decode.vram_bytes), and the decoders together take at most half of the video memory the card gives the VRAM ring and them (vram_room_bytes). When NVDEC runs on another card than the one the player draws with (a laptop drawing on its integrated GPU), or the drawing card's budget is not known, the NVIDIA card's own free memory gives the room, less a tenth of the card (at least 512 MB), asked every 2 seconds (vram_room_source); 2 GB when that is not known either. A decoder beyond it stays on the processor. The VRAM ring's budget counts them, and on an integrated GPU that decodes (D3D11VA, VAAPI) the RAM budget too. - Color. The rule is today's processor conversion: BT.601, full range only for the "J" formats such as MJPEG. H.264 and HEVC give the processor's picture bit for bit (their decoders are exact); MJPEG 4:2:0 comes within 3 codes (mean 0.01 code; the card's inverse DCT). 10-bit 4:2:0 and 4:2:2 sources keep 10 bits on screen; a 10 or 12-bit HEVC 4:4:4 is decoded by the card and converted to RGB by the processor (its planes would weigh twice the RGB picture, as for step 1).
- Turning it on never makes a picture wait. The device is made on its own thread (about 0.1 s); a decoder already reading goes on with the processor and takes the card at its next picture for an all-intra film (MJPEG), at its next seek or film for a long-GOP one (H.264, HEVC: it would decode its GOP again).
- Never a black frame. If the card fails on a film (a decode call or the transfer),
that decoder opens the film again on the processor at the frame it was reading;
three failures in a row turn the beta off until the next start (
state"disabled"). A film the card refuses at its open although its answer said it would take it is decoded by the processor (its first picture with the processor open's thread count, as slice threads, then opened again on the processor) and counted inrefusals; the next films of that kind and size stay on the processor at their open until the beta is turned off and on again. - Bench only. The environment variable
BP_GPU_DECODE_APIset tonvdec,d3d11va,d3d11va:N(adapter N, 0 to 15) orvaapiforces the API instead of the automatic choice; any other value is ignored.
12. GPU ProRes decode (beta)
The player can keep a ProRes film's frames in RAM still compressed and let the
graphics card decode each one when it is shown (FFmpeg's Vulkan ProRes decoder). It
is a beta and off by default: set-gpu-prores turns it on or off
(Preferences > Cache & Performance), and status.gpu_prores reports its state,
counters and the route of each track's film.
- What changes. A ProRes film's frames stay in RAM as the ProRes packet, 5 to 10
times smaller than the RGB picture; the first pass only reads the file and
nothing is decoded by the processor. The graphics card decodes each packet when
it is shown, with FFmpeg's Vulkan ProRes decoder (compute shaders: any Vulkan 1.3
card, NVIDIA, AMD or Intel, on Windows and Linux, with an FFmpeg 8.1 or newer
built with Vulkan, as the player's Windows build is). The decoded picture is
handed to OpenGL without a copy (
GL_EXT_memory_objectandGL_EXT_semaphore: the Vulkan images are shared, never copied back to the processor) and converted to RGB by the shader of section 10 into the viewer's texture: RGB10_A2 (10 bits kept), or RGBA16 for a picture with alpha (12 bits and the alpha kept). Every reader of that texture (grade, A/B, wipe, scopes, paint, online stream) is unchanged. The profiles are 422 Proxy, 422 LT, 422, 422 HQ (decoded at 10 bits), 4444 and 4444 XQ (12 bits), with or without alpha.ram-infocounts these frames aspixel_type"prores-packet". - Color. The rule is today's processor conversion: BT.601, limited range; the stream's color tags are not read, as for the processor path. The GPU picture is within 1 code (8-bit) of the exact conversion of the processor's decode; against the processor's picture it is within 2 codes for 4:4:4 and 4 codes for 4:2:2 (the processor's own RGB writer is that far from the exact conversion), alpha within 1 code. With the beta on, the screen shows the card's picture (10 bits kept, 12 bits and alpha in RGBA16 for 4444 with alpha), while the readers that read pixels on the processor -- the pixel probe, the curves' histogram, the Levels pickers (their click reads at once), an annotated frame's timeline thumbnail, the technical check, the A/B difference band -- and the exports read the processor's picture, swscale's 8-bit RGB. They can differ from the screen by up to 4 codes (8-bit) on 4:2:2 profiles (mean about 0.9: swscale's own chroma rounding), by 1 to 2 codes on 4:4:4, alpha within 1 code.
- What keeps the processor decode (automatic, picture by picture): a reduced
picture (a proxy tier below 1/1), an interlaced picture, an unknown ProRes codec
tag, a 4:2:2 picture of odd width, a side above 16384 px, a film that asks an
integrated GPU for more than it decodes in real time, a picture size FFmpeg's
Vulkan ProRes decoder gets wrong, and the first picture of each decoder (and those
read while the beta starts). On an integrated GPU the card takes a film only if
pixels x the film's own frame rate x profile weight stays within what UHD 4:2:2 at
50 frames a second asks. The weights are 1 for the 4:2:2 profiles (Proxy, LT, 422,
HQ), 1.75 for 4444 and 4444 XQ, and alpha adds 0.25. So UHD 4:2:2 up to 50 fps
(DCI 4K, 4096 x 2160, up to 46), UHD 4444 up to 28 fps and 4444 with alpha up
to 25 fps go to the card; UHD at 60 fps, 4444 at 30 fps and anything 8K stay on the
processor (
clip_reason"more than this integrated GPU decodes in real time at this frame rate"). Measured on an Intel Arrow Lake integrated GPU (Vulkan, decode only): 4K 422 HQ 61-72 pictures a second, 4K 4444 XQ 33, 4444 with alpha 28-29, 8K 16-18 (below the processor). A discrete card has no limit. A picture whose sides are not multiples of 8 (each plane's width and the height: on 4:2:2 the width must be a multiple of 16) also stays on the processor, because FFmpeg 8.1's Vulkan ProRes decoder decodes such sizes wrongly on every card (found on the NVIDIA card and the Intel GPU alike: at 2048 x 858 the last two rows are wrong; a 4:2:2 picture 1998 wide is wrong everywhere). 2048 x 858, 1998 x 1080 and 4096 x 1716 stay on the processor; 1920 x 1080, 3840 x 2160, 4096 x 2160 and 8192 x 4320 are taken (clip_reason"a picture size FFmpeg's Vulkan ProRes decoder gets wrong (sides not multiples of 8)"). Any other codec is not touched by this setting.status.gpu_prores.clipandclip_reasonsay which route a track took. - Never a black frame. If the card fails on a frame, the processor's decode of the
same packet is shown at once; three failures in a row, or a lost graphics device
at once, turn the beta off until the next start (
state"disabled", the engine's video memory freed) and the packet frames are dropped from RAM (only those). A frame the VRAM ring could not decode is left out of the ring and never counts as a failure. A machine without what the path needs (no Vulkan 1.3 device on the card OpenGL runs on, a driver without the interop extensions, an FFmpeg without the Vulkan ProRes decoder, macOS) says"unavailable"with the reason and keeps the processor decode. The Vulkan side starts on a thread of its own when the beta is turned on (tens to hundreds of milliseconds); films keep the processor decode until it is ready (state"starting"). Turned off, the packet frames leave RAM (they decode again on the processor) and the engine is freed: nothing of the path runs and no Vulkan instance is created. Turned on, the frames already decoded stay and the next ones come as packets. The first picture of each new frame pool (in practice, of each decoder the engine opens for a profile and size not yet checked in the session) is checked once against the processor's decode of the same packet: three rows read back (the first, the middle, the last), within 6 codes (8-bit; measured 1, a wrong picture is tens of codes off). The processor's decode for it runs on a thread of its own, so the drawing thread never waits for it; the card's picture is shown meanwhile. A wrong picture turns the beta off until the next start as soon as its check ends (state"disabled", the processor's decode shown from then on), as a lost graphics device does; so does a check that could not be made twice on the same pool. The check costs one processor decode per profile and size. - Memory. The decoders' memory (FFmpeg's frame pools on the card, shared with
OpenGL: about 1.4 GB per 8K 4444 decoder, up to 4 decoders) is reported as
status.gpu_prores.vram_bytes, which is always present (0while the beta never ran and once the decoders closed). It is taken out of the VRAM ring's budget when that budget is the fixed fallback (no measure of the card) or when the card shares the system RAM (an integrated GPU, where it is also counted in the RAM budget); a measured budget (DXGI, NVIDIA/AMD counters) already includes it. A decoder unused for 3 seconds closes and its memory goes (a clip closed, the player paused or at rest): one wake of the loop, never a periodic one. The engine itself (its Vulkan device) stays while the beta is on.status.gpu_prores.decoders, present whenengine_allocatedistrue, is the number of decoders open now. - What reads on the processor. The pixel probe, the curves' histogram and an annotated frame's timeline thumbnail read a packet frame through a processor decode made on a thread of its own, so they show nothing for the moment that takes; the technical check and the A/B difference band decode on their own low-priority thread. Packet frames are not written to the disk cache: they decode again. Exports always decode from the files.