The complete reference of the Bernard Review player's API: the protocol, every command and its reply, the events, the status fields and the Python client.
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).
bernard_api.py).nvfx)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.
127.0.0.1 (localhost - your local machine)5556 by 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 its review-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 with SO_REUSEADDR, never under Windows.\n)."cmd" string), include an HTTP request line (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, CONNECT, TRACE or PRI followed by a space, in any case, or any line whose last word is an HTTP version such as HTTP/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 (a fetch(), a form, an <img> aimed at 127.0.0.1:5556) from driving the player.\r included, 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, a build-matrix of a whole sequence, is far below (600 shots with 10 departments each: about 1.2 MB), and json.dumps() never writes a NUL.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"} |
"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.py files its pending request under the same cut name, so even a very long unknown name is answered at once.cmd has 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.set-colorspace ("error"), ram-fit-apply and voice-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".{"event": ...} lines (see Event Reference) -- are not replies; nothing a client sends is a broadcast."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 reads reason, then message, then error. The rules of the table above (unknown command, bad line, missing field, failed handler) always use "reason".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.
To control the player, open a TCP socket, connect to port 5556, and send the corresponding JSON line.
{"cmd": "play"}{"cmd": "pause"}frame is the 0-based index in the clip (what status's playback.currentFrame reports); display_frame is 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). A display_frame that is not a frame of the shot is refused ("status": "error" with its reason), not taken to the shot's end; the reply gives both, "frame" and "display_frame".
{"cmd": "seek", "frame": 41}
{"cmd": "seek", "display_frame": 1042}{"cmd": "load", "path": "D:/projects/seq_01/shot_10/render.mov"}value (float between 0.0 for silent and 1.0 for maximum volume).{"cmd": "set-volume", "value": 0.75}value (boolean: true to mute, false to unmute).{"cmd": "set-mute", "value": true}{"cmd": "toggle-mute"}value (boolean: true to enable, false to disable).{"cmd": "set-ab-mode", "value": true}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_band in status). 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+4 and "difference" give back the tinted one or the plain views; the gain stays until the player quits (x1 at start). set-ab-mode keeps 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.
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).{"status":"ok","cmd":"set-ab-view","value":"b","gain":1}, or "status":"error" with a reason for another value, a gain that is not a whole number from 1 to 16, or while A/B is off (turn it on with set-ab-mode first); nothing changes on an error. The view in force is playback.ab_view in status, the gain playback.ab_difference_gain.{"cmd": "set-ab-view", "value": "b"}
{"cmd": "set-ab-view", "value": "true_difference", "gain": 4}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_position and track_b.wipe_angle in status read it back, playlist_playback.ab_per_shot says 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-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 .bpl without it opens with the guides off) and by the last session.
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: true turns everything off first, the opacity kept). Every field is checked before any is applied, and a field of another name is refused (a misspelt safe_area is an error, not ignored). Opening a .bpl without the key, or clearing the playlist, turns the guides off.{"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, rects says 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), and action_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}S key. From the wipe it shows B alone. Like set-ab-view it is only a view change: nothing is loaded.
{"status":"ok","cmd":"ab-flip","value":"b"} (the view now in force), or "status":"error" with a reason while A/B is off.{"cmd": "ab-flip"}Down and Up keys. 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 way set-track-b puts 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).
direction (string: "prev" or "next").{"status":"ok","cmd":"cycle-b","from":"v002","version":"v003","preloaded":true,"path":"...","requested":false} -- preloaded says whether that frame was already in RAM -- or "status":"error" with a reason: 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.version and track_b.last_version_step in status read the result back. requested true: that version is not on this computer, it was asked of the pipeline (media-request) and goes onto B when media-ready brings it; B is unchanged meanwhile.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); from and version are then the rows' names (status track_b.compare_selection).{"cmd": "cycle-b", "direction": "next"}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_switch is 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.
value (boolean: true to pin, false to unpin).{"status":"ok","cmd":"pin-track-b","value":true}, or "status":"error" with a reason: a value that is not a boolean, or "track B is empty" for true while B holds no clip (unpinning is always accepted). track_b.pinned in status reads it back.{"cmd": "pin-track-b", "value": true}{"status":"ok","cmd":"clear-track-b","cleared":true}; cleared is false when B held no clip. track_b.path in status then reads "".{"cmd": "clear-track-b"}value (boolean).{"status":"ok","cmd":"set-b-version-preload","value":false}.{"cmd": "set-b-version-preload", "value": false}"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, key ab_label_mode), as the Preferences are. status reads it back as viewport.ab_label_mode, with each drawn label's opacity in viewport.ab_labels and viewport.ab_label_idle_s.
value (string).{"status":"ok","cmd":"set-ab-label-mode","value":"never"}, or "status":"error" with a reason for any other value.{"cmd": "set-ab-label-mode", "value": "never"}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, key align_start_frames). status reads it back as track_b.align_start_frames, with B's own frame for the playhead in track_b.frame and the frame whose picture B shows in track_b.shown_frame; the contact-sheet reply as align_start_frames.
value (boolean).{"status":"ok","cmd":"set-align-start-frames","value":false}, or "status":"error" with a reason (and the setting unchanged in value) for anything but a boolean.{"cmd": "set-align-start-frames", "value": false}"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"). status reads it back as metadata.
value (string: "a", "b" or "ab").{"status":"ok","cmd":"set-metadata-track","value":"ab"}, or "status":"error" with a reason.{"cmd": "set-metadata-track", "value": "b"}
{"cmd": "set-metadata-track", "value": "ab"}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_manual says whether the clip's space was chosen by hand and status.color.input_space_detected which one its file gives. With an OCIO config loaded (Color > Load OCIO Config... or ACES 2.0 built-in), value may 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, and status.color.input_space_name_shown says which (true: the picture shows that space). Without a config nothing changes: no name is kept and nothing new is written anywhere.{"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 before reason, 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"}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). A display alone 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.{"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 to session_config.json; a pick in the Color menu is ("ocio_view"). status.ocio tells what is drawn.{"cmd": "set-ocio-view", "display": "sRGB - Display", "view": "ACES 2.0 - SDR 100 nits (Rec.709)", "look": ""}CDLTransform with the style CDL_NO_CLAMP (the style OCIO reads a .cc / .ccc / .cdl with) 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.
row (integer, 0 or more, optional; default the row on screen), then one of:
slope, offset, power (each a list of three numbers) and saturation (a number), each optional (its identity value when absent), at least one given, and optionally id (a string, the correction's name; at most 128 bytes, no control character). They replace the row's CDL; the CDL's source is "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, .ccc or .cdl, with the optional id of the correction to take. Without id, the file's only correction, or else the one whose id is the row's shot code (x_tracking.shot_name, else x_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's source is its file name, never its folders.clear: true: the row has no CDL any more (row is the only other key it takes).on alone (true or false): applies or stops applying the row's existing CDL, which is kept either way. on may also come with values or a file: it is then the new CDL's state. Several of values, file and clear in one command are refused.{"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"}}, cdl being the row's CDL as a .bpl item writes it (see below), or null after clear. For file the 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} (row when known): "give the values, a file or clear, not several", "no such row", "the row has no CDL" (on alone 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-loaded then 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 the ok reply.{"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}value (string: "RGBA", "R" / "Red", "G" / "Green", "B" / "Blue", "A" / "Alpha", or "L" / "Luma" / "Luminance").track (optional, "A" or "B", or 0 / 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 other track is refused ("status": "error"). An unknown value leaves the view as it was.{"status":"ok","cmd":"set-channel","value":"R","track":"B","linked":false} (track only 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"}{"cmd": "reset-layout"}windows reads its rectangle back.
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.{"status":"ok","cmd":"new-window","view":"media-session","number":1,"name":"Media & Session (Copy 1)","rect":{"x":30,"y":111,"w":383,"h":694}} (rect in ImGui's coordinates, the screen's on Windows), or "status":"error" with a reason: 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"}"all": true closes every copy (Close All Copies). The original views are shown and hidden by their ticks in the Window menu, not by this command.
name (string, as new-window and windows give it), or all (boolean).{"status":"ok","cmd":"close-window","name":"Media & Session (Copy 1)"}, {"status":"ok","cmd":"close-window","closed":3} for all, or "status":"error" with a reason when no copy of that name is open.{"cmd": "close-window", "name": "Media & Session (Copy 1)"}{"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}. monitor is 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 of views: view (the key), title, duplicable, on_screen (drawn and not a hidden tab), rect (or null when not on screen) and copies (how many are open). Each of copies: view, name, number, drawn (false until its first frame), rect (its window's, or where it will open while not yet drawn) and inside_monitor. popups lists 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 with name, drawn (false on the frame a popup that fits its content is measured, hidden) and rect (its window's, as last drawn). notices is About > Third-party notices' window, the player's own (it opens nothing outside the player): title ("Third-Party Notices"), open, drawn and rect (null while not drawn), found (false: the window says "Third-party notices not found"), folder (where the files are: licenses beside the program, a Linux package's share/bernardplayer/licenses, or the source tree's), files (the names listed, THIRD_PARTY_LICENSES.md first, 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") and text_bytes (the size of the text shown). hovered is 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 (0 for none): what a debug-mouse click 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, windows gives a popup's size frame by frame; frame is the interface's frame count (Dear ImGui's) of the frame last drawn, the one these rectangles are of.{"cmd": "windows"}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's duration in ram-info is 0 and status.playback.transitioning is true; 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, see playlist.schema.json), .otio, .csv, .json or .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, see playlist.schema.json), whether its input color space was picked by hand ("color_space_user_set": load-playlist then 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: see set-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").
path (string).{"status":"ok","cmd":"save-playlist","path":"..."}, or "status":"error" with a reason.{"cmd": "save-playlist", "path": "D:/review/sh010.bpl"}Attach an image to a note: note-attach attaches image files to row row'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 in status.note_attachments (last, and the row's attachments or refusal).
row (integer, a row of the playlist), and path (string) or paths (1 to 32 strings), each 1 to 4096 bytes.{"status":"ok","cmd":"note-attach","row":0,"queued":1}, or "status":"error" with a reason (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"}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).
row (integer), and index (integer, from 0) or sha256 (string).{"status":"ok","cmd":"note-detach","row":0,"count":1} (the images left), or "status":"error" with a reason ("no such image on this note"...).{"cmd": "note-detach", "row": 0, "index": 0}path (string); x_tracking (object, optional): the row's production-tracking ids, the same block as a .bpl row's (see playlist.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).{"status":"ok","cmd":"playlist-add","index":N}, index being the new row's; without index when 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"}}status.playback.transitioning stays true until the new clip is on screen. A second playlist-play before that retargets the switch.
{"cmd": "playlist-play", "index": 2}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. The count of status.media_notes is the notes and the drawings together.
{"cmd": "playlist-remove", "index": 2}{"cmd": "playlist-move", "from": 0, "to": 3}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"}{"cmd": "reverse-play"}delta frames (negative to step backward, clamped to the valid range).
{"cmd": "step-frame", "delta": -1}{"cmd": "toggle-loop"}frame to use the current playhead. frame is 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's 1010 is index 10, 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}{"cmd": "clear-in-out"}value is true the 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-out and the keys still set them. Without value it changes nothing and the reply reads the lock back; so does status.playback.in_out_locked. Kept across restarts.
{"cmd": "set-in-out-lock", "value": true}{"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.{"cmd": "pan", "x": 10.0, "y": -5.0} -- screen pixels from the centered position. Leaves fit mode.{"cmd": "fit"} -- back to fit mode: the picture is re-fitted every frame until the next zoom or pan.{"cmd": "flip"}{"cmd": "toggle-ab"}{"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 from AppsUseLightTheme, or on Linux the freedesktop portal's / GNOME's color-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, key appearance). Without value, 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 and status give.
{"status":"ok","cmd":"set-theme","value":"clair"} (the key, also for "light"), or "status":"error" with a reason that names dark, 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.status reports theme (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", or null while 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) and theme_button_rect (where the mode button is, for debug-mouse; in Dark, Light and Neon, the display button's box).Complete · Dark after 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. status reports view_control: null in full screen, else form ("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, and popover: null while closed, else its rect, focus (the keyboard's item: 0 Essential, 1 Complete, 2 to 4 the themes, -1 none yet), levels (level, rect, lit), hint (the sentence) and themes (theme, rect, ticked).{"cmd": "set-ui-level", "value": "essential"} -- Essential or Complete, what the menu bar's switch, Window > Essential / Complete, Preferences > General > Appearance > Interface and Ctrl+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, key ui_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. Without value, 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, keys user_name and user_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.
{"status":"ok","cmd":"set-ui-level","value":"essential"}, or "status":"error" with a reason for 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).session_config.json, the autosave) and "When Decoding Is Too Slow" (slow_decode_tier in the profile) -- stay as Complete left them and are in effect again in Complete. status gives both: viewport.proxy_idx (the clip on screen's saved tier, 0..3 fixed, 4 Smart RAM-Fit) and viewport.proxy_idx_effective (the one in effect); decode.slow_decode_tier (saved) and decode.slow_decode_tier_effective; each playlist[] entry's and ram_fit.track_b's proxy_idx (saved) and proxy_idx_effective (in effect; effective_proxy_idx is still the tier the fit gives it); and ui_level_ui.auto_decoding (true in Essential, where a discreet "Auto" says so beside the counter).APPDATA with no session_config.json) meets the first-launch question, a modal the viewer waits behind: send set-ui-level first ("complete" keeps the player as it always was), or seed a session_config.json. tools/player_blind_soak.py and tools/quit_exit_time.py do.status reports ui_level ("essential" or "complete") and ui_level_ui: question_pending (the first-launch question still to answer) and question (the boxes of its two answers, review and all_tools, of its optional name and email fields and of skip, while it is drawn, else null); switch (the menu bar's essential and complete segments and the whole pill, in Classique at both levels, null in 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) and before_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 own flag and whether it is shown); finish_review (at both levels: close_question -- null, or while the close asks about unpublished notes or dictated drafts its text ("" with drafts alone), drafts ("2 dictated drafts not validated.", "" without) and buttons by 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; text says who, its buttons Wait, Finish Anyway or Publish Anyway, Back to Review) --, notes and statuses (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 offers Export as HTML), failure (what failed, then "Everything is saved." or "Review not saved."), text and buttons (the window's line and its buttons by their words, while it is drawn; in export_offer, no button while the save's chooser is open, then "Review saved." or "Review not saved."), banner, banner_finish and banner_close (Essential's banner, null when not drawn); in Essential, inspector.publish and inspector.publish_label are Publish (N) beside the tracker's pill); essential (null in Complete; in Essential the boxes of what its panels drew: shots with its cards, pills and versions (per shot of the playlist, null when the filter hides it), filters (All, To review), head, list, shown, add and add_menu_open ("+ Add Shots"'s menu drawn: "Add Media..." first, then Open File or Sequence... and Add Folder..., the playlist's); toolbar with its seven tools (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) and compare_menu, layer (null with 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) and view_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, null without a shot) with line, path, version (its menu: the shot's versions; version_menu_open), who, label_a and label_b (while A/B is on); transport (the row of buttons over the strip, as wide as the window) with row, 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, play and next (the same boxes), counter (the frame number on screen and the last one, 7 digits each), timecode, fps (null when 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_grab and out_grab (where a press takes the in or out bracket, null while they are locked), in_x and out_x (the brackets), cache_ticks (frames of the clip on screen in the RAM cache), auto, loop_menu_open and volume_menu_open (Loop's and the sound's right-click menus) and number_text / timecode_text (the counter's words); status_row (under the strip of thumbnails) with its three segments in the row's order -- Not Approved, Pending, Approved, the keys 1, 2 and 3 -- each status ("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) and rect; lit (the shot on screen's status, null without one), next_shot and next_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", null when it has no room), row, and strip_outline (the role of the strip's outline in the shot's status, review_approved_bg... or null); 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; inspector with title ("SH0040 · v003"), export and export_menu_open (HTML; CSV and PDF grayed: no exporter), link_pill and link_pill_rect (what the player is connected to, "Connected to Bernard Engine" or "Connected to Kitsu", "" and null when nothing is: the Tracker section is gone; with a tracker, the pill opens Publish), link_badge and link_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"; "" and null when no tracker is connected and the shot is linked to none), color_head (Color correction, folded by default: reset, channels, color_space are null while it is), notes, reset, the six channels, 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) and paint_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) and scroll (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, null while everybody fits), avatars_overflow (its N, 0 without it) and avatar_menu (the menu a click on an avatar opened -- People's own: for, the person's id, and its items, each label, enabled and its box, rect -- else null), and raised_hand (the host's answers to a raised hand, as in session_ui.bar.raised_hand)), and earlier (manu/earlier-notes: the shot's Earlier notes under Notes, null without 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), cards and thumbs (their boxes; a card off screen only takes its room: its thumbnails are not listed), replies (each card's replies fold, null without replies or off screen), fix_tags (each card's OPEN or FIXED tag, the click that ticks it; null for a DONE or off screen), replies_shown, show_older and try_again (null when not offered); folded, empty lists; title, the head's words as drawn, "Earlier notes (7)"), and, beside essential, earlier_full_picture (the picture of a thumbnail opened full size: null while closed, else state -- "loading", "ready" or "failed" --, width, height, picture and close, their boxes or null; never the path)). Every box is [x0, y0, x1, y1] in debug-mouse's coordinates.{"cmd": "set-ui-level", "value": "essential"}
{"cmd": "set-ui-level", "value": "complete"}
{"cmd": "set-ui-level"}{"cmd": "set-track-b", "path": "D:/projects/seq_01/shot_10/v004/render.mov"}{"cmd": "set-fullscreen", "value": true} -- enters (true) or leaves (false) fullscreen, as F11, the toolbar button and the View menu do. status.viewport.fullscreen reads it back.{"cmd": "set-presentation", "value": true} -- enters (true) or leaves (false) presentation mode, as Shift+F11 and the View menu do: the full screen with only the picture. status.viewport.presentation reads it back, status.viewport.presentation_controls says whether the mouse has the timeline on screen now.{"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 a reason ("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.
{"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.window says what the player has.{"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_scale in session_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). Without percent, reads it back. Another value (150.5, "150", 175) is refused with a reason.
{"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}.x and y set the position and turn it on, down holds or releases the left button, alt holds or releases the Alt key (the timeline's Alt+Drag of an in/out point), right presses (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_entry says where it was drawn) --, ctrl and shift hold 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), middle presses (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), pen makes the pointer a pen: {"pressure": 0..1, "eraser": true|false, "barrel": true|false}, each key optional (no pressure: 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 while down holds, read through the same path as a real pen's WM_POINTER once the message is read (status.paint_over.pen); "pen": false makes it a mouse again. Any other pen, 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. release gives the real cursor back and lets go of all. Each stays as last sent until a later command changes it. The reply's alt reads the key back. Debug-only: while it is on, the real mouse is ignored. status.timeline says 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}paths to 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. at names 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 with x and y; with neither, the drop has no position and takes the default route. The reply's target says 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, or viewport_b once B is loaded. A file whose format this build lacks (.nvfx in 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"}"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.
open (boolean: show or hide the window -- as Window > Contact Sheet 2D: in Essential, which keeps the window back, true shows it; the reply's open is 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, elapsed and ui_frames), trace (an object {"points": [[u, v], ...], "max_events": 200000} arms a diagnostic trace, false disarms 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; zoom zooms the layout view too, on its center, 1 to 5, 1 giving the grid that fits back) and press_key ("layout": the layout view's chord, Ctrl+Shift+L unless rebound, pressed through Dear ImGui's input queue as the keyboard would -- a verification tool; the view toggles on the next frames) and type_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)."layout_view": false brings the picture back.open, reflow, reset, trace_read, layout_view, layout_clean must be true or false; mode "synced" or "free"; press_key "layout"; type_text a string of at most 256 bytes; card_w a number; size two numbers; trace an object or false, its points an array of at most 64 [u, v] pairs of numbers, its max_events a 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_w 80 to 960, size 200 x 150 to 16384 a side, u and v 0 to 1, max_events 1000 to 2,000,000.open, mode, align_start_frames (set-align-start-frames), card_w, layout_clean, layout_zoom and layout_moved (the layout view's own zoom, and whether it is off the fit), playing, playhead, elapsed (seconds since the last reset), 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, and cells, 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, 0 with 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, -1 for 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) and displayed_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, -1 empty), unreadable_frames (their file numbers) and gaps ([[1017, 1024]]: file numbers missing from the sequence).trace_read, trace is a list of events, one per counted draw: [t, ui_frame, card_index, wanted, drawn, playhead, state], state being 0 exact, 1 late, 2 missing, 3 unreadable, 4 waiting. 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 the points (normalized, v downwards), 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_dropped counts events past max_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"}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-sheet layout_view): view and 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 (edit below; in Essential it is not offered: its bottom panel has no edit band). The command with no field only reads.
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+M unless 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).layout and press_key together, another type, an unknown name or "picture" for press_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's layout is then the one still shown. A press_key whose 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 answered ok at once, and the layout's own refusal, if any, comes as the player's notice, as for the keyboard.layout (the one shown) and layouts, the four in the menu's order, each with layout, offered and refusal (null when it can be shown now). A change sends the event view-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"}]}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 from 00: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 by view.
shots (how many), total (frames of the clock) and total_timecode, fps (the clock), other_rate_shots (the shots of another rate), frame and timecode (the playhead in the edit; -1 and null outside it), shot (the shot on screen, an index into the edit), builds and segment_builds (how many times the edit and its band were built: drawing builds nothing), band (the band as drawn on the last frames, null when 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), start and edit_length (on the clock), fps (its rate), at most 5000) and shots_listed. status.edit holds the same, without shot_list.{"cmd": "edit"}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, or build-matrix), else the row's x_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 its x_tracking.task. Departments that mean the same step are one (comp, Compositing, CMP: synonyms, without case). Their order: step_order of 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; hidden says 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.
row (optional, 0-based playlist row; without it, the row on screen).row must 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\"".row, sequence and shot (the board's; sequence empty when the shot came from x_tracking), steps (in order: name, default_version -- an index into versions --, versions: name, number (-1 without one), department, available, board_active, playlist_row (-1 when no row plays it), path), found, hidden, current_step (the row's step, -1 none) and builds (how many times the player looked for a shot's steps since it started: asking again with nothing changed does not look again)."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}Steps (steps) : The stack of the Steps view (View > Steps): up to max_steps steps (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 / Down in the Steps view change the step on screen (round at either end), W wipes 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) -- and Shift+4 shows its Difference. The sound is the step on screen's. A line whose shot token differs between the steps (sh0030 in one, sh0040 in another; sh0030 and sh30 are one) or that a step lacks is marked, never blocked. The stack is not saved. Without action, the state (as status.steps).
build (steps: 1 to max_steps steps, 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, as steps-of finds 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: -1 or 1), 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, and step, or value "new" and an optional name: 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).{"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's loading counts the readings under way; a build that names files is read whole, in its order, and replaces the stack once read; an add joins 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")."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 for find-by-name why nothing could be stacked ("only one step (Comp) was found by name"...).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, -1 none), steps (name, source: "current", "api", "found by name" or the playlist file, rows, clock, on_screen, track: 0 A, 1 B, -1 none), line (the row on screen) and line_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, its path, the media asked of 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: its path, "" none, and the frames asked of 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_box its entry, null otherwise; items: its submenu's entries while it is open, each words and box), loading, refusal (the last thing refused, "" none) and bar (the bar of steps over the picture while it is drawn: buttons, plus, grid, menu_open, popup; null otherwise), panes (in the grid, each pane as drawn: step, row, wanted and drawn -- the frame it should show and the one it shows --, state: "exact", "held", "loading" or "none", box) and gate (waiting, waits, longest_wait_ms: the lockstep's waits), grade_table (the panes' pictures graded through a table of colours, each colour graded once: tables kept and the bytes they hold, max_table_bytes a table holds at most, table_pictures graded through a table, exact_pictures graded 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": ""}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"}Export Notes : Writes the current playlist's review notes and annotated frames to output_dir as export.json and export.html -- see notes_payload.schema.json for 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) carries frame_in and frame_out (0-based indexes, as a frame's frame) and display_frame_in / display_frame_out (the numbers the player shows, as display_frame); a note on the whole shot has none of them. The CSV gives the passage's timecodes in the note's Timecode cell, the HTML and the PDF write "1012-1040 · " before the note. Each annotated frame carries frame (the 0-based index, the annotation's key) and display_frame (the number the player shows for it); its picture is <shot>_frame_<display_frame>.jpg and the page says Frame <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"}], rgb optional, as a stroke's author) and hold (its longest hold, 2 to 999 frames), and proposed (true while 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-dir at the close writes several shots at once). The picture carries the frame's strokes, laid with the exports' rasteriser (the one screenshot uses with "annotations": true), flipped with the picture; older versions gave the bare graded frame, with the strokes only in export.html's SVG overlay.
Export Notes to a File : The review notes as ONE file for people, at path (absolute, ending in .pdf, .csv or .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) or html (one self-contained page, every picture inside as a data: URI, no script and no address of anything elsewhere). All three are read from the same export.json as export-notes (which is unchanged). track is 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 than pdf / csv / html, a relative path or another extension, a track other than 0 or 1, or an empty playlist. notes-file-exported says 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}
frame is optional (defaults to the current playhead). annotations is optional (default false): true lays 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. track is optional (defaults to 0) -- track B is not yet supported, since unlike track A it isn't backed by a PlaylistItem at 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 size in a .bpl, in export.json and 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. screenshot decodes 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-sheet and debug-mouse work 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 (PrintWindow on Windows, import or xwd on Linux).
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.
trackers.json: has_secret is whether the player knows a secret for it (the environment, this run's memory, or the store), null until 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}]}.{"event": "tracker-connected", "ok": true, "site": "studio-kitsu", "user": "Artist A", "reason": ""} (on a refusal ok is false and reason says "<code>: <message>", auth_failed: ...). A site whose address is http:// 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 for localhost, 127.0.0.1 and ::1.
{"cmd": "tracker-connect", "site": "studio-kitsu"}forget also removes the site and its kept secret. A site other than the one connected is only forgotten.
{"cmd": "tracker-disconnect", "site": "studio-kitsu", "forget": false}{"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"}replace, the default) or after it (append); event {"event": "tracker-loaded", "ok": true, "items": 12, "missing_media": 3, "reason": ""}. missing_media counts 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"}items (playlist indices; default every one of them), status (also set each clip's status through the site's status_map; default false), 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; a dry_run is still allowed. Event: {"event": "tracker-publish-done", "dry_run": false, "published": 1, "skipped": 1, "failed": [{"index": 4, "reason": "forbidden: ..."}], "statuses": 0, "statuses_refused": []}, and with dry_run, requests ([{"index", "requests": ["POST /api/...", ...]}], masked). A row whose note, unchanged, was published already is skipped: the fingerprint of what was sent is kept in x_tracking.published, and a note is never sent twice. With status, such a row whose status changed since it was published sends its status alone, without the note or its images (counted in published, and in statuses, the number of statuses sent alone: 0 in a dry_run, which shows that one request). A row published by an earlier version has no status recorded: the first run with status sends nothing for it (skipped) and keeps its status as the reference; a later change goes alone. On ftrack, network or timeout on 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, and statuses_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 in failed (status_not_on_site: ...). The Console asks before it.
{"cmd": "tracker-publish", "items": [0, 2], "status": false, "dry_run": true}<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 (one tracker-download-done each). "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}cancelled).
{"cmd": "tracker-cancel"}.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-status follows it, an export-done event 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"}
output; a preset is 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 (default true: when resizing down, decode at the 1/2, 1/4 or 1/8 tier that still covers the output), hardware (default true: 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; nvfx is 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's recipient -- 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 of sh010.1001.exr at index 41 writes .1042; 1001 from the command line. The next files follow the frames exported, step included: from 41 by 2, .1042, .1044, .1046, the numbers {frame} prints. A preset saved by an older version holds 1001, the old default: read as absent), padding, compression (EXR none|rle|zip|zips|piz|pxr24|b44|b44a|dwaa|dwab, TIFF none|lzw|zip, NVFX htj2k|wavelet|lossless|raw), all_layers (NVFX, default true: 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 (true for the player's Info Bar layout, or an object of slots -- below), burn_in_spec (the same object, alongside burn_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 holds 0: read as absent), color and cc, input_space, display_space (below).top_left, top_center, top_right, bottom_left, bottom_center, bottom_right (aliases tl 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}; plus scale (text size factor, 1 = frame height / 40) and background (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 is source (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) or custom (the cc values of the job). cc is 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, and stages_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 curves curve_master, curve_r, curve_g, curve_b (Set Curve, ACEScct): x:y points joined by ; in the string ("curve_r=0:0;0.5:0.6;1:1", none for the straight curve), a list of [x, y] pairs in the object; and the hue ranges range_<reds|yellows|greens|cyans|blues|magentas>_<hue|saturation|lightness> and lightness (Set Hue Range), numbers in both forms ("range_greens_hue=12,lightness=-0.1"), each clamped to its bounds. input_space and display_space name 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": {"enabled": true, "text": "{recipient} · {date} · CONFIDENTIAL", "position": "bottom_right", "opacity": 55, "size": 3} -- true alone is that default; an object switches it on unless it says "enabled": false. text takes 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); position is one of the nine cells top_left top_center top_right middle_left center middle_right bottom_left bottom_center bottom_right (aliases tl tc tr ml c mr bl bc br, centre) or tiled (the text repeated over the whole frame); opacity is a percentage, 10 to 100; size is 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. date and time are the job's start (YYYY-MM-DD, HH:MM), clip and version the clip's, session the review session's name or the playlist's. The command's own recipient, studio and session fields fill their tokens and a <recipient> of output; they are not part of the job's JSON, never written into a preset, and no reply, export-status or log line repeats them: a path there shows <recipient> where the name is. A {recipient} or a <recipient> without a recipient is refused."watermark": {"enabled": true, "text": "{recipient} · {date} · CONFIDENTIAL", "position": "bottom_right", "opacity": 55, "size": 3} -- true alone is that default; an object switches it on unless it says "enabled": false. text takes 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); position is one of the nine cells top_left top_center top_right middle_left center middle_right bottom_left bottom_center bottom_right (aliases tl tc tr ml c mr bl bc br, centre) or tiled (the text repeated over the whole frame); opacity is a percentage, 10 to 100; size is 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. date and time are the job's start (YYYY-MM-DD, HH:MM), clip and version the clip's, session the review session's name or the playlist's. The command's own recipient, studio and session fields fill their tokens and a <recipient> of output; they are not part of the job's JSON, never written into a preset, and no reply, export-status or log line repeats them: a path there shows <recipient> where the name is. A {recipient} or a <recipient> without a recipient is refused.{"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).{"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 finished ok, 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's preset=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 also jobs, 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, state being queued, running, done, failed or cancelled; the fields above follow whichever job the export thread runs, a queue task's or an export's.{"cmd": "export-cancel"}export accepts: 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, and nvfx_review, nvfx_lossless except in a player built without NOVA) and user (one JSON job per file in presets_dir, the file stem is the name; the Export dialog's "Save as preset" writes them).
{"cmd": "export-presets"}export_recipes/ folder, described by export_recipe.schema.json. A recipe names no media and no clip: its source is playlist or clip (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: replies recipes (each name, file, source, start_from, outputs, the number of outputs) and recipes_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) or recipe (the object inline) --; clips (playlist indices, 0-based, in the order to export; default every clip; a clip recipe 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-status or the log); session and studio (over the recipe's own studio) for their tokens. Each (clip, output) becomes one job of the export command's kind, clip by clip, at the end of the Export window's queue, which runs them one at a time: export-status's queue follows them, and each job's export-done event ends it. The clip's own in and out points, its grade (Start from grade_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.bernard_export_recipe (the format's version, 1; a later one is refused), name, source, start_from (grade_as_shown or source), studio, and outputs, each with name (the card's title), preset (a built-in or studio preset; it must exist where the recipe runs), overrides (the export fields that differ from the preset's: format, codec, resize_w, display_space, burn_in...; never source, sources, in, out, output or watermark; color, cc and input_space are ignored: Start from and each clip decide them), watermark (above) and output (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.{"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.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; --recipient and --studio fill its tokens and <recipient>, and the summary shows <recipient> in their place; a plate refuses the watermark here too; -o is short for --output; the --no- flags and their positive forms lay over what a preset or a job file set). Prints export: frame N/M lines and a JSON summary on the last line (with decode_tier, encoder, encoder_options, and jobs for a batch); exit code 0 on success, 1 for a bad command line, 2 when the export failed. viewer and clip color have no meaning without a window and become custom; 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, --studio and --session only (a recipe carries the rest), turns grade_as_shown into custom color as above, prints export: <source> -> <output> and done / FAILED per job and a JSON summary on the last line (recipe, ok, clips, outputs, files, seconds, and jobs, 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.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).version_id or another id holds at most 256 bytes.path or an error text 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.
Set Exposure : Exposure offset in stops, applied to track A -- or to track B.
value (float; 0.0 is neutral, +1.0 is one stop brighter).track (optional: "A" or 0, the default; "B" or 1 for 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 (4294967296 is not 0), and 1.0. B's exposure reads back as track_b.exposure in status.{"cmd": "set-exposure", "value": 1.5}
{"cmd": "set-exposure", "value": 2.0, "track": "B"}Set Gamma : Gamma applied after exposure.
value (float; 1.0 is neutral).track (optional), as for set-exposure; B's gamma reads back as track_b.gamma.{"cmd": "set-gamma", "value": 2.2}
{"cmd": "set-gamma", "value": 1.8, "track": "B"}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.
temp (float, -1 cool .. +1 warm) and tint (float, -1 magenta .. +1 green), 0.0 neutral; give one or both, the other is left as it is. Each is brought into [-1, 1].track (optional), as for set-exposure.{"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":...} when track is unknown, temp or tint is not a number, or neither is given -- nothing is applied then. They read back as color.grade.white_balance_temp / white_balance_tint in status (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"}Set Offset : the offset (lift) of track A -- or of track B --, Quick Color Grade's Offset (Lift).
value (three floats, the lift of R, G and B, [0, 0, 0] neutral), each brought into [-1, 1].track (optional), as for set-exposure.set-white-balance's, "cmd":"set-offset"; an error when value is not three numbers. Reads back as color.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"}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.
stage (one of the keys above), on (true / false), track (optional, as for set-exposure).{"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 an on that is not a boolean -- nothing is applied then. They read back as color.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"}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.
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 (true cuts 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_presets next to session_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 a reason.color_panel block of status (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}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) and r, 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.
channel ("master" -- or "rgb" --, "r", "g", "b"), then points (the curve) or reset (true: the straight curve back), track (optional, as for set-exposure).{"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), or points and reset together -- nothing is applied then. The curves read back as color.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's cc, and reach a review session's followers (curve_master_a, curve_r_a, curve_g_a, curve_b_a for track A, the same ending in _b for 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"}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 a hue shift (degrees, -180..180), a saturation gain on the chroma (0..2) and a lightness (-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 global lightness (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 all greens and nothing of yellows. They stay on Rec.709 whatever the display space (see the user guide, 4.12).
range (one of the six, "global" -- its lightness only: the global hue and saturation are the grade's hue and saturation --, or "all" with reset), then any of hue, saturation, lightness (a value not given is kept) or reset (true: that range, the global lightness, or all of them back to neutral), track (optional, as for set-exposure).{"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 and reset together, all without reset, or hue / saturation under global -- nothing is applied then. The ranges read back as color.grade.range_<range>_<hue|saturation|lightness> (for example range_reds_hue) and color.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's cc, and reach a review session's followers (range_reds_hue_a ... lightness_a for track A, range_reds_hue_b ... lightness_b for track B).{"cmd": "set-hue-range", "range": "greens", "hue": 12, "saturation": 1.2}
{"cmd": "set-hue-range", "range": "all", "reset": true, "track": "B"}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.
open (true / false) shows or hides the window; kind chooses its tab: "histogram", "parade", "waveform" or "vectorscope". Both are the workstation's preference, saved as the menus save them (session_config.json, key scopes; {"open": false, "kind": "histogram"} when the key is absent). A wrong value is refused ("status":"error" with a reason) and nothing is applied.status block below, then the last results' numbers: samples (pixels measured), step, width and height (the picture measured: the readback's size), clip (black_pct and white_pct, the percentages of pixels clipped black -- every channel at code 0 -- and white -- a channel at code 255 --, black and white their counts, black_channel and white_channel per R, G, B) and histogram (bins, 256, and r, 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: samples 0, clip.black_pct and white_pct 0, histogram.bins 0 and empty lists.{"cmd": "scopes", "open": true}, lets results move on twice after the picture changed (a measure lags the picture by a frame or two), then reads the reply.status reports scopes: open, kind, visible (a scope drawn this frame), hz_max (15), captures (readbacks since the start), computes and results (measures published, and taken for drawing), gl_resources and worker (the small framebuffer with its PBO, and the worker thread: false whenever no scope is shown), source and readback ([width, height] of the rectangle read and of the picture measured), last_compute_ms and avg_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] in debug-mouse's coordinates, null while not drawn), plot (the scope's box), placeholder ("No picture" drawn) and black_label / white_label (the footer's figures, as drawn).status give it as "probe", null while nothing is frozen, else {"scope": "waveform" | "parade" | "vectorscope" | "histogram", ..., "label"} with, per scope, column and level_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, else null) (vectorscope); level_pct, bin, pixels_pct (r, g, b, luma: the share of the samples in that bin, or null) (histogram); label is 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", null for none.status alone adds readout and block. readout.window and readout.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, label and label_rect (the words and their box, [x0, y0, x1, y1] in debug-mouse's coordinates, null when not drawn), runs and measures (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) and reads (pixels read for the mark); readout.columns: first, last (the picture's columns under the scope's left and right edges) and image_width. block: the Color panel's scope block, the user's preference (session_config.json, key color_panel_scopes, never the grade's): height (the user's, px at 100%, null for 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) and max_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}{"cmd": "set-fps", "value": 23.976}index is the playlist's row (the shot on screen by default); project, sequence, shot, version, department and artist are 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 gives context (the six fields as shown, index, and given: the fields set this way). status.review_context says the same for the shot on screen (null without one). Refused, never cut: an index that is not an integer row of the playlist (a float, 1.0 too, 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"}.bpl ("start_frame"), and sent to the other seats of a review session by the pilot. value is 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 gives start_frame (-1: none).
{"cmd": "set-start", "value": 86400}These mirror the controls in the Cache & Performance tab of the Inspector.
Set Proxy Resolution : Decode resolution, trading detail for speed and memory.
proxy_idx (integer).proxy_idx | Mode |
|---|---|
0 | Full Res (1/1) |
1 | Half Res (1/2) |
2 | Quarter Res (1/4) |
3 | Eighth Res (1/8) |
4 | Smart 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_scale moves within a frame; the eviction and re-decode behind viewport.decoded_width take from a fifth of a second to about half a second on a 2K EXR sequence. Between the two, viewport.retiering is true — poll status until it is false, and only then are proxy_scale and decoded_width describing the same thing. playback.transitioning does not cover this: it means a change of clip.
!!! warning "Index change" Smart RAM-Fit moved from 3 to 4 when the Eighth Res (1/8) tier was added. Scripts written before that change will select 1/8 where they previously selected Smart RAM-Fit.
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 to 1/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, key slow_decode_tier, "auto" or "off"; a file without the key reads Auto). Without value, it reads the setting. status.decode says what the rule did.
{"status":"ok","cmd":"set-slow-decode-tier","value":"off"}, or "status":"error" with a reason for any other value.GPU decode for NOVA (beta) : Turns the GPU decode of .nvfx frames 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 key nova_gpu_decode, with nova_gpu_decode_chosen true 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. Without value, it only answers the state. See NOVA for what runs on the GPU and what stays on the CPU.
value (boolean, optional).{"status":"ok","cmd":"set-gpu-decode","value":true,"gpu_decode":{...}} -- the gpu_decode object is the one status carries --, or "status":"error" with a reason: "value must be true or false" for a value that is not a boolean (nothing changes), or "this build has no GPU decode" in a build without NOVA.{"cmd": "set-gpu-decode", "value": true}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 key gpu_yuv_upload, with gpu_yuv_upload_chosen true 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). Without value, 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.
value (boolean, optional).{"status":"ok","cmd":"set-gpu-yuv","value":true,"gpu_yuv":{...}} -- the gpu_yuv object is the one status carries --, or "status":"error" with a reason: "value must be true or false" for a value that is not a boolean (nothing changes).{"cmd": "set-gpu-yuv", "value": true}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 key gpu_video_decode, with gpu_video_decode_chosen true 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. Without value, it only answers the state. See GPU video decode (beta) (section 11) for what the card decodes and what stays on the processor.
value (boolean, optional).{"status":"ok","cmd":"set-gpu-video-decode","value":true,"gpu_video_decode":{...}} -- the gpu_video_decode object is the one status carries --, or "status":"error" with a reason: "value must be true or false" for a value that is not a boolean (nothing changes).{"cmd": "set-gpu-video-decode", "value": true}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 key gpu_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). Without value, it only answers the state. See GPU ProRes decode (beta) (section 12) for what runs on the GPU and what stays on the CPU.
value (boolean, optional).{"status":"ok","cmd":"set-gpu-prores","value":true,"gpu_prores":{...}} -- the gpu_prores object is the one status carries --, or "status":"error" with a reason: "value must be true or false" for a value that is not a boolean (nothing changes).{"cmd": "set-gpu-prores", "value": true}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's ram_fit.suggested_fixed_tier says 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, a proxy_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 before reason, 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. value sets 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: 0 off, 1 the shot, 2 the playlist, 3 Ping-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_wait says how: missed_this_pass, missed_last_pass, passes, stalled_passes, last_end ("advance", "loop", "advance-stalled"), loops_waiting, advances_incomplete; status.playlist_playback.old_tier counts 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, true or false): 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 a reason for another loop mode or a continuous that 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; false gives it back its Smart tier. Without value it 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 value GB; 0 restores Auto (85% of the video memory available to the player). A cap never allows more than Auto would. ram-info reports vram_ring_budget_bytes, vram_ring_auto_bytes, vram_ring_manual_gb and 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_uploads counts 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.
clip_id (string) -- the clip's path, as reported by status or ram-info.{"cmd": "cache-clip-frames", "clip_id": "D:/shots/sh010/render.mov"}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.
!!! note "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"}]}]}
enabled is false when the cache could not be opened, with the reason in reason and nothing else (no profile folder, a shared location refused, a folder that is not a cache). max_bytes is the maximum the size rule applies (0 when the disk cannot be measured and no explicit maximum is set). disk is null when the disk cannot be measured. Dates are UTC. Per entry: format is the key that makes an entry (codec, quality and encoder; changing any of them makes another entry), bytes the size of its copies, frames how many distinct frame contents its seen paths point to, frames_cached how many of those have a copy, frames_pending how many still need one, stale_copies copies nothing points to any more (the source frame changed or was deleted: cache-purge with stale removes them). state is complete (every frame has its copy), partial or empty. paths lists every folder and pattern the sequence was seen under, with its first and last frame numbers the last time it was seen there. open is true for the sequence on screen. An unknown entry is an error.
Cache purge : removes copies, and says what it removed. scope is 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 in kept.sequence: one sequence, by its entry id, or by a path it 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 before before, 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_frames counts copy files, refused the 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 unknown scope, a sequence purge without entry or path or matching nothing, an older-than without a valid before, a sequence that is on screen, and a cache that is off.
layer-info to discover the available indices.
{"cmd": "set-layer", "track_idx": 0, "layer_idx": 2}sequence (name), id (the tracker's sequence id, optional), shots (array of {name, id, departments}), where departments maps a department name to {version, path, version_id, id}, or to an array of them.x_tracking (its ids, the block of a .bpl row: 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, path may be empty; playing it sends media-request instead of loading, see the Event Reference), active (true: the version its cell shows).version_id, else its path: sending it again updates it. Where the naming rules filed its media when its row was added (or DefaultSeq when 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 shots looks 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}]}}build-matrix.
{"cmd": "get-matrix"}
Reply: sequences (name -> {id, shots: {name -> {id, departments: {name -> {id, active_version, versions: [{name, id, path, status, available, requested}]}}}}}) and naming_regex. available: the version's media is on this computer; requested: it was asked of the pipeline (media-request) and is not here yet.set-track-b puts a clip there. A version not on this computer is asked of the pipeline instead (media-request), and media-ready plays it.
version_id (string); track ("A", the default, or "B").{"status":"ok","cmd":"play-version","requested":false,"track":"A"} -- requested true when it was asked of the pipeline -- or "status":"error" with a reason (no such version, a format this build lacks).{"cmd": "play-version", "version_id": "f42:v011", "track": "B"}media-request: the version's media is on this computer now, at path, and plays where it was asked (track A or B), as if played again; or it could not be brought, error saying 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 a build-matrix no longer lists that version (a build-matrix that 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": true in the second case).
version_id (string); path (string) or error (string).{"status":"ok","cmd":"media-ready","played":true,"track":"A"}, or "status":"error" with a reason (no such version, neither path nor error).{"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"}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 (see earlier-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, and done stays the pipeline's.
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 either cards (an array of at most 20) and next_cursor (string, at most 256 bytes, optional: older notes exist, and "Show Older" sends it back as cursor; absent or "": none), or error (string, at most 4 KiB: the notes could not be read; the section says so, with Try Again). Optional, with cards: total (integer from 0 to 1000000: the task's notes and corrections over every page; anything else is an error, total must be an integer or total must be from 0 to 1000000). The section's head shows the count, folded or unfolded: "Earlier notes (7)" -- total when 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 without total drops a total given before; an older page with total updates it.id (string, 1 to 256 bytes, once per answer) and kind ("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), category and category_color ("#rrggbb", the category's own color), done (bool: true done, false open, 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) and replies (at most 20, oldest first, shown folded under their note: author, created_at and text, at most 4 KiB). A name (version, category, author) holds at most 256 bytes, created_at at most 64. An optional field may also be null, and an optional text "": as if absent.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 a next_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 (same id) -- and "Show Older" goes on from where it was, the row's own next_cursor. Otherwise the row starts again from that page and its next_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; an error is taken only as the answer of the request pending for that cursor (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.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 when earlier-notes-closed names the key. Only to a player that lists earlier-notes-live in capabilities.json: it also takes total, and is the one that sends earlier-notes-closed (a player without it ignores the unknown field, takes an unasked newest page, and never sends the event).{"status":"ok","cmd":"earlier-notes","key":"en-1","kept":true,"cards":12} -- kept false 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 -- its earlier-notes-closed crossed the push or was lost --, not taken: stop pushing it; an older page or an error nobody asked for), cards how many cards the row holds now -- or "status":"error" with a reason that 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"}count (how many thumbnails exist) and missing_raw_pixels (how many lack the pre-grading pixels needed to re-grade in place). missing_raw_pixels should always be 0; anything else means that many thumbnails would keep showing a stale grade after a color change. Also track ("A"), mean_rgb (the mean 8-bit R, G, B of the graded thumbnails) and checksum (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_height are a sheet cell, the pictures' size: the size kept with the pre-grading pixels, widened by the clip's pixel aspect. frames gives each thumbnail's frame, 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_rgb and checksum. A 2x widening repeats every column, so a correct PAR-2 strip has the same mean_rgb as the PAR-1 strip of the same frames.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 be 0; the size mix-up it guards against made the re-grade read past the pixels, and crash, under a PAR other than 1.track (optional). "B" (or 1) 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), and frames, each thumbnail's frame, width, height, mean_rgb and checksum. The output PNG is optional there. reason is "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"}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.
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 not ready), 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>.part for the next try), delete_partial (true: deletes those .part files), 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 (true by 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...; false for a test that measures without them; not kept), and draft: {"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).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 (gpu or cpu), 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 counters segments, phrases, failed, segments_dropped, samples_dropped, last_transcribe_ms, unplaced (phrases said while no shot was on screen), refused, inject_error (the last dictation-inject of a .wav that could not be read), and drafts: 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) and ui: 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."status": "error" and its reason), nothing changed. enabled and language are applied before listen: with {"enabled": true, "listen": true} the setting is saved on, and listen is refused while the engine is still loading.{"cmd": "dictation", "enabled": true, "language": "en"}
{"cmd": "dictation"}ready.
wav (the absolute path of a .wav on 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) or samples (an array of 16 kHz mono samples in [-1, 1], 4,800,000 at most).source (the injection's number). The drafts come later, in dictation; a .wav that cannot be read is said there in inject_error.{"cmd": "dictation-inject", "wav": "D:/tests/voice_en_plan7.wav"}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 code 0.
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-notes first.
Terminating the process instead skips all of that: the session is not saved and the log ends mid-line. Kill it only if quit does not answer.
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.
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.
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.
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.
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": ["::"]}}
kind is lan, vpn (100.64/10, Tailscale or a studio VPN), or public; 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): with reach "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 on 127.0.0.1 and ::1 only ("listen": {"reach": "this_computer", "addresses": ["127.0.0.1", "::1"]}; ::1 is 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 other reach is 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_id is SESS- and 25 characters of A-Z0-9 drawn 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 9000 means only that the port is taken.
Join a session : address is what the host sent. ws://host:9000/SESS-..., host:9000/SESS-..., host/SESS-..., host alone (port 9000 assumed), and relay links wss://relay.example.com/s/SESS-...?t=<token> (or wss://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: host alone, or another session's link, connects and is then closed by the host (close reason session); session-status then says "connected": false and auth_error begins Could 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-baton works, and the reply and session-status say "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: url says where this seat connected with the token's value replaced by [redacted], and so does an error's message (Could not connect to wss://...?t=[redacted]); session-status names 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 a wss:// or https:// link), Invite people's Copy link and Send and the Session Review panel's Copy Link give no link (neither that one nor a ws:// studio-network address): they are grayed, with the tooltip Invite guests with the browser link of Online review (Copy link in its window), and status's session_ui.invite.link is 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, role web), and the player joins it as its host and streams to the browser guests. end closes it on the relay (DELETE /api/v1/sessions/{id}: every token refused) and leaves. action is open, start (with name, optional) or end; start only 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 (with email), verify (with code, 6 digits) and sign-out are 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's online_review follows them. A start with an expired sign-in sends a new code to the same address and says so in error; the review starts once the code is verified. TEST ONLY: BERNARD_RELAY_URL in 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's online_review is {"made": false, "open": false} until the first use (nothing is read or sent at the player's start), then made, open (the window), phase (unknown, signed_out, code_sent, signed_in, starting, live, ending), busy, email and sent_to (masked: g***@example.com, never the whole address), start_after_sign_in, error (in words, empty without one), session_id link_shown (the browser link with its token hidden: https://<relay>/join/SESS-...#t=[hidden]) and rects (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, null when 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) and people (each seat as the window words it: name, role, notes_taker, and roles, an array of "pilot", "decider", "notes", empty for a Viewer; never an e-mail); invite_role (the window's Invite as: viewer, notes or decider), decider_link_shown (the last Decider link, made for the e-mail typed, its token hidden; "" before; never the e-mail) and in rects invite_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) and decider_link (Make a Decider link, then Copy Decider link); copy stays the guests' Copy link (drawn while Viewer is picked), notes_link Make a note-taker link (then Copy note-taker link) while Notes is picked. No token, code or whole link is ever in a reply, status or 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 with host_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"} -- local while its own framing is on, before it comes back to the pilot's 4 s after its last zoom or pan gesture, returning during the 0.3 s of that return, hands the gestures that took one since the player started; null for 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, as session-create replies it: {"reach": "network" | "this_computer", "addresses": [...]}; null on a seat that runs no server), viewport_synced and voice. server_guard is {"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). refused counts the messages dropped or that closed their connection, by reason: spoof (a message other than a presence whose sender_id is 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... rewritten counts the relayed messages the host corrected, by field: is_host (a top-level is_host that did not match the seat's role: only the host's seat carries true; 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-zero spoof or is_host on a normal session means a seat is sending what it should not. viewport_synced is {"received": N, "applied": N, "ignored": N}: the pilot's looks (its viewport messages) 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, so ignored normally 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 pilot for the first five. voice is {"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, or none when nothing is open), and how many 100 ms chunks this seat has sent and heard. capture is 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_sender and refused_over_rate count 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). thumbs is {"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. seconds is 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_host in session-status); anyone else gets only the host can pass the baton. The id is the "<name>#<24 hex>" from session-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 : false detaches this seat to scrub on its own, true snaps it back to the pilot. While a seat follows, every clip it loads takes the pilot's last look (status.color.grade is the pilot's, after a clip change too); false also 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 send false too: it then sends nothing only the pilot sends (no transport, seek, navigation, playlist_sync, paint_settings, annotation_sync or viewport; a seat arriving meanwhile gets its presence only), the others keep the last picture it sent, and status.session_ui.bar.sync reads "state": "free", "label": "Local Review — not broadcasting". true from 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-log replies with messages: 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), so chat-log returns at most 500.
Laser pointer : one sample, in normalized viewport coordinates (0..1 from the top-left of the picture); active: false puts 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, y are normalized viewport coordinates (0..1 from the top-left of the picture), color is the IM_COL32 integer as received (0xAABBGGRR: 4278190335 is opaque red), name is empty when the participant list does not know the sender, age_ms is measured on this seat's clock.
Stream to Remote Guests : the Sessions menu's "Stream to Remote Guests". Hands the picture to the bernard-stream companion through shared memory, which encodes it for the web guests (the layout is in docs/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 with not hosting or driving a review session unless 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 with Only 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. width and height are 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's state may still say starting. 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 in docs/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-enable reply.
{"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_state is 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 is companion_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), or error (it could not be launched; tried again after the same delays). companion_error says 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. encoder is 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. state is off, 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, or error with the reason in error (for example the object name still held by a companion from the previous run; the player retries every second). companion_pid is 0 unless that process is alive. frames_captured counts readbacks, frames_published frames written to shared memory, frames_dropped visible states that were superseded before they could be captured (the pipeline was busy or the companion behind: dropped, never queued). render_cost_us is the render thread's time in the streamer per frame (moving average; 0 while off). publish_latency_us is 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_height are 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 session or Only 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 under distrib/<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's distrib_key), then the encrypted pieces it asks for, player to player through the bernard-stream companion 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. expiry is 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) or 7d; 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 (phase converting), 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 (its files are the movie's frames), and the window plays it at the movie's rate and says No 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), while allow_originals is on; off, that movie is not sent and line says 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" with reason.
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 you on the host's line with its 12-character code (distrib-status host.pending), while the guest's own line reads Waiting 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's seat and the code read (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" with reason (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 in received[].shots of distrib-status) of package package_id in 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's 100% 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 says Original..., then Original). frame (from 0) and zoom (fit or 100%) pause the window on that frame at that zoom, as the guest would; receive_originals true 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 in distrib-status viewer), or "status": "error" with reason for a malformed package_id or shot.
Distribution status : read-only. available is true in an online review; role is host, guest or none; companion is the transfer companion's state (stopped, starting, running, error, not_bundled). A host gets host: sending, expiry, phase (off, converting while the review copy is made, preparing while the pieces are sealed, ready, failed with error in 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's bytes, files and shots, and one entry per guest whose player said hello, with its line in plain words (Waiting for you while waiting_confirm is true, whatever its last state, Waiting, Receiving 42%, Ready, Removed, No system keychain on this computer, Not enough disk space, Cannot reach Sam: still trying.), its last state (receiving, complete, opened, deleted, refused), have and total (encrypted bytes; complete with have under total when all it misses is a movie withheld by the box) its verification code, waiting_confirm (true while its code waits for the host's Confirm: nothing is sent to it before; false while phase is failed: there is nothing to confirm) and link_unreachable (true while its line is Cannot 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. error and line of host stay empty: the package is fine); pending lists those guests (seat, name, code). A guest gets guest: phase (idle, no_keychain, waiting, connecting, receiving, ready, refused, removed), its line (while waiting, Waiting for the host. If the host asks, read this code: ... with its code), link_unreachable (true while its line is Cannot 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_total and its own code. received lists the packages this computer holds (in or out of a session), their shots (label as the host named them, files, ready, review_copy, and the frames from 0 whose original is asked, originals_asked, or here, originals_held, and not_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's line then reads Ready, 1 movie not sent, and percent stays under 100; ticked again, the package is receiving again), access at 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) and originals_allowed (the host's box). viewer is the received shot's window: open, package_id, shot, watermark (true once its watermark was drawn over the picture), frames, decoded and message (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": ""}}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 in items[] with index, 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) and fill_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-level viewer_watching, is_playing_state, streaming_seek_requested[_b], scrubbing_tracks, bg_fills_running and preload_all_to_ram are 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_frames counts layer 0 only. Where the whole budget went is in breakdown[]: every cached entry grouped by clip_id (the cache's normalized path), layer, scale, pixel_type (u8, u16, half, float, bc6h, gpu-packed, yuv-planes, prores-packet), placeholder and from_video, with frames, bytes (what the budget counts) and buffer_bytes (what the pixel buffers hold; equal or a little larger, since the pool hands out the best-fitting idle buffer). Each row carries item_index (the playlist item of that clip, or -1) and a role: 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) or unlisted (a clip no playlist item names, such as track B's own file). Each items[] entry adds cached_bytes (all layers), other_layer_frames and other_layer_bytes; the top level adds cache_bytes_counted (the sum of the rows, which equals cache_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) and sequence_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 no items[] entry sees it). present is false and nothing else is written when B is empty; otherwise kind ("video" or "sequence"), path, clip_id (its key in breakdown[]), duration, layer (the layer B shows), cached_frames and placeholder_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; 0 while 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) and fill_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 what items[] now reports as that item's fill_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 passes fit_budget_bytes in the whole cache nor fit_share_bytes of 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_from is the row's key B reads them from (null for none: B not a row's media, or a film, whose key is its path, the row's own), shared_frames how many of cached_frames are the row's; cached_bytes counts only what B holds under its own key, so a row on B is held once.
{"cmd": "ram-info", "frames": true} adds cached_frame_numbers (sorted) to every items[] entry and to track_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".
{"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":false,"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 day main.cpp was compiled), compiler, os and os_version, gpu_vendor, gpu_renderer and gl_version (OpenGL's GL_VENDOR, GL_RENDERER and GL_VERSION), vram (the dedicated video memory from GL_NVX_gpu_memory_info, or the free texture memory from GL_ATI_meminfo, which says free; "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) and ocio_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.ocio says 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_sdk and arri_sdk (a version, "present, version unknown" or "absent"). Beside them: tree_modified (a boolean), vram_mb and ram_bytes (the two sizes as numbers; null and 0 when unknown), and text, the block itself, character for character what the button copies: Bernard Review <version> on its first line, then one Label: value line 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"}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 in send_log.injected_sends_refused. open (bool), include_crash (bool) and note (string) are each optional; any other key is ignored.
{"status":"ok","cmd":"send-log","send_log":{...}}, the same object as status's send_log.{"cmd": "send-log", "open": true, "include_crash": false, "note": "It closed when I changed shot."}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-loaded) : Sent once a set-cdl with a file has been read in the background (OCIO step 5): ok true with cdl, the row's CDL as a .bpl item writes it, or ok false with reason (the file's name, never its folders). Only to the command's file; 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) : 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-sheet layout_view, its key), or a layout left with nothing to show (Edit when the playlist empties) going back to the picture.
layout ("picture", "contact-sheet", "edit", "steps").{"event": "view-changed", "layout": "contact-sheet"}play) : Sent when the player starts playing.
{"event": "play"}pause) : Sent when the player is paused.
{"event": "pause"}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 separate seek event: a jump is one frame_changed.
frame (int - the playhead, as status's playback.currentFrame reports it and seek takes it), display_frame (int - the number the ruler shows for it, seek's display_frame).{"event": "frame_changed", "frame": 23, "display_frame": 1024}export-done) : Sent once when an export job -- or a whole playlist batch -- ends, whether it succeeded, failed or was canceled (export-cancel). export-status holds the full result afterwards.
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; 1 for one job), seconds (float, the export's duration), encoder_options (array of "name=value", what that encoder opened with, see export-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"}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 sends playlist-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.
rows, one per row that changed: index (0-based), path, status ("Pending", "Approved", "Not Approved", as export.json writes it), note, annotated_frames (the frame indices carrying strokes, from 0, as export-notes names their JPGs) and, when the row has one, x_tracking as the .bpl writes 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}}}]}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 --: no review-changed follows for what they held when they came, nor for a review change still settling then.
rows (int - how many rows the playlist now has).{"event": "playlist-changed", "rows": 12}open-link) : A bernardreview:// 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) or failed (no connection, or the site gave nothing). reason names at most the link's host, never its user.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) and missing (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) : 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-exported always comes after it (within five minutes of the close). Not sent for an empty playlist (nothing is exported, no review-exported either).
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 frames export.json lists (a file missing on disk is counted, then said missing, "no picture"); 0 is possible, the export still follows). review-exported then says which of them are missing and why. They are written into <dir>.part meanwhile: counting the .jpg files 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}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.html and the annotated frames' JPGs, as export-notes writes them), and before the IPC server stops. Whatever closed the player -- its window or quit -- 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).
dir (the folder); complete (true when export.json, export.html and every annotated frame's picture are in the folder); published (every file written is in dir; 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 in dir, in the order they were written: export.json first -- when it is listed, the decisions are there); missing (each annotated frame without its picture in dir: row, shot, frame, display_frame, image -- the names export.json gives it -- and reason: "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). complete false with published true: the decisions in export.json are 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"}]}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: false says only that, not that a file was saved: the playlist's .bpl is 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 is review-exporting and review-exported's, if asked). It names no person and no shot.
published (true: a site received the review; false: nothing was sent, the export was offered); notes and statuses (what the summary says was sent: notes with their drawings, and statuses alone; 0 when published is false); not_linked (the shots the run could not send, not linked to the site connected; 0 when published is false).{"event": "review-finished", "published": true, "notes": 2, "statuses": 3, "not_linked": 0}media-request) : Sent when someone plays a version of the Version Board that is not on this computer (build-matrix with "available": false): its cell, track A's Versions menu, a version step of track B (cycle-b, Up / Down) or play-version. Nothing is loaded; the pipeline copies the version and answers media-ready. Once per version until that answer: playing it again meanwhile sends nothing new (the last track to ask gets it).
version_id (what media-ready names), sequence, shot, department, version (its name on the board), track ("A" or "B": where it is to play), x_tracking (its ids, as a .bpl row 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}}}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 with cursor "", and tells the pipeline to push that row's newest page again, unasked, on its own change events (see the earlier-notes command), until earlier-notes-closed. A section unfolded again later is asked again, with the same key in 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-empty cursor ("Show Older") does not change what is pushed. Never at launch, never while folded. The pipeline answers with the earlier-notes command.
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 the next_cursor of the last answer, as it came), limit (20: the most cards an answer may hold), x_tracking (the row's ids, as a .bpl row 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}}}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.
keys (an array of one or more keys, the ones the requests gave).{"event": "earlier-notes-closed", "keys": ["en-3", "en-7"]}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": "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
}
]
}
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 says Version 1.0 (build NNN)). Both come from CMakeLists.txt; version always starts with public_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 boxes rect, download, whats_new, later, [x0, y0, x1, y1] or null); notes (state: "loaded", "unavailable", "none"; bytes, cut, window_open, title (the What's new window's title as drawn, "" while it is closed), unavailable_shown and the boxes download, 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: what seek's frame takes).playback.reverse: the direction of play, true backward (reverse-play, J, or the way back of the Ping-Pong loop, set-playlist-playback's loop_mode 3); 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) and given (the fields set-review-context set; the others are read in the file); null without 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_number and last_number, the numbers shown), analysed and frames (the shot's frames read so far and its length), idle (the check waits: done, paused or nothing cached yet) and dismissed (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), -1 when 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, else defaultStartFrame.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 key default_start_number).playback.timecode: The timecode of displayFrame as the ruler writes it, counted from the number (no drift at 23.976, no step back at 29.97); HH:MM:SS;FF in 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 key timecode_drop_frame); only at 29.97 and 59.94.playback.fps: The playback speed in frames per second.playback.transitioning: true from 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: true while 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 be false.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), null while 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 coordinates debug-mouse takes, or null while 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); null in Classique and in full screen.classique_mode_button: always null since 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) and rule ("windows" or "linux_macos", this build's); null in Dark and Light, in full screen and on the first frame.window: the main window as GLFW has it -- size [w, h] (the client area set-window-size sets), 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; null otherwise), work_area [x, y, w, h] of its screen (or null) -- 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) and restart_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], null while the line is not on screen).wipe: the A|B wipe as the last frame drew it on the picture, null while it is not shown (A/B off, A alone, B alone): swipe and angle (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) and rotating (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) and set_px (the last one), bar_rect and bar_shown ([x0, y0, x1, y1]: the bar's box and the part of it its window shows), cut (its lower part hidden) and cut_px. The window has no title row (header_px is 0 while it is alone in its node) and end_buttons gives the bar's own buttons at its right end, copy and pin ([x0, y0, x1, y1], null when 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) and cell_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") and presets (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, null when tiled), tiles (the mosaic's marks), tiles_truncated, tile_area (the part of the picture the mosaic covers), tile_measures and tile_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, null when it was not drawn. popover (Dark and Light, the W button's right-click): {"open": false} while it is closed; open, the boxes of window, enable, types (Text, Logo), sources (User info, Custom text), text, modes (Fixed, Floating, Tiled), cells (the nine), angle, spacing, scale, opacity and link ("All watermark settings..."); a box not drawn is null.color_panel: the Color panel (Color Panel, under Color & Grading): target ("A", "B", "A+B"), before_after and before_after_applies (on, and cutting the grade this frame: in Complete), picker ("black", "grey", "white" or null), pick_why and auto_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 .cube loaded, "" for none), and drawn: where the panel drew each control on its last frame, [x0, y0, x1, y1] in debug-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, each row, eye, reset and open --, fields -- by key --, levels_histogram, levels_handles, pickers, levels_auto, lut, load_lut, clipping, stack, footer; and options_lines (the lines the options' line took: 1, or 2 in a narrow panel), "pipeline" (the Color pipeline block, framing Input and Output) and groups -- 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); null for a control not drawn), null while the panel is not drawn (Classique, another tab, Essential); groups_folded (the keys of the groups folded, the user's preference, kept in session_config.json under color_panel, never in the grade); and curves (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), and drawn -- the editor's last frame: graph, tabs (RGB, R, G, B), points (the channel's points), reset, input, output, boxes as above -- or null while no editor is drawn; and hue_ranges (Set Hue Range): window_open (the Hue Ranges window), selected_panel and selected_window (the range chosen in the panel's Hue & Saturation stage and in the window: "global", "reds"...), and drawn -- the view's last frame: selector, reset, reset_all, sliders (Hue, Saturation, Lightness; null for one not drawn), before, after (the bars), boxes as above -- or null while 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). While visible is false nothing is read back: gl_resources and worker are false and captures stays as it is.session_ui: the review-session screens as the last frame drew them, in the screen coordinates debug-mouse takes. No command changes with them, and nothing in this block is a password: a session's password is never written in status, in a reply, in the log or in a link (password_set in session-create says whether there is one).
session_ui.bar: the session bar, null outside a session or while the bar is not drawn. frame is the frame it was drawn on; role "host" or "guest"; title the session's name (a guest, who is never told it, sees the session id); subtitle the second line as shown, "00:42:17 · LAN" or "… · online · 1080p" while the viewport is streamed; banner the host's "Guests can't see your picture: …" (the same as session_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"; sync the chip: state ("following", "leading" -- this seat is the pilot --, "waiting", "free" -- Local Review), label as shown ("Following AD"; "Local Review — not broadcasting" for the pilot in Local Review, which sends nothing), padlock, rect; avatars: how many faces are shown (three at most) and how many the "+N" disc stands for (overflow, overflow_label), rect the stack; room ("7 in the room"), hands ("1 hand up", "" for none) and hands_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; null while no hand waits and nothing is open): frame; notice the 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 of hands, else null; hands each yellow hand drawn as a button, {"id", "rect"} (a click asks "Give control to Marie?"); confirm that question while it is open, {"for", "text", "yes", "no"}, else null. Yes passes the baton; No sends hand_request lower_hand (docs/online-review-messages.md 6e).session_ui.create: New review session while it is open, else null. 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, and start_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 and network_profile is "public"), else null. TEST ONLY: the environment variable BERNARD_TEST_NETWORK_PROFILE ("public", "private", "domain") stands for the read.session_ui.invite: Invite people while it is open, else null. 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_enabled and send_enabled: whether Copy link and Send (with its field) can be used, both false in 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, and password_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, else null. link_recognised: the link pasted is one the player can join (session-join reads 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", true when the dialog opens -- a choice of this seat alone, which session-join does not take; name_from_account; password_typed (never the password); join_enabled and join_blocker; rects: join, cancel, mute_toggle, change.session_ui.people: People, the popover the bar's avatars open, while it is open, else null. 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 -- and mute_all_rect; rows: this seat first, then the pilot, then the room's order, each {"id", "name", "role", "mic", "hand_up", "rect"} -- name with " (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 -- or null; paint_rule: the host's checkbox "Only the pilot draws", {"value", "rect"} -- value 1 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), and detail, 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); and host_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 coordinates debug-mouse takes. Nothing of export changes with it. open says whether it is drawn; menu_item is File > Export Frames / Movie... while the File menu is open, else null; queue is the queue's window, open or not: open, and open, header ("Exports · 1 running, 2 queued"), window, clear, close, and rows, 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, null for custom), preset_label ("Dailies · ProRes LT", "Custom"), summary (the foot's line), export_label ("Export", "Export 12 clips"), export_enabled and blocker (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", null while 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 as export would take it (without what the player adds at the start: the clip on screen, its grade, its strokes); lists: format, codec (with the label it has for a sequence: "Depth · compression"...) and audio, each {"text", "rect"}; encoder_note: the H.264 / HEVC note, {"text", "warning", "rect"}, null for another codec; format_options: {"open", "summary", "rect"}, null when 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, w and h (Custom's fields, null while 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 being clip, version or recipient) and final_name. nova, in a player built with NOVA, while Format is NOVA (else null, 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, else null), all_layers, advanced and advanced_summary, temporal_gop, gop_rect, bc6h, toggles (grain_aware, protect_blacks, all_layers, and bc6h while 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"}, null when none; the reason is masked with the site's secret), open_link ({"state", "reason"}: the last bernardreview:// link, state none, opening, waiting, opened, refused or failed, as the open-link event), 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)."}; null when 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}], status the player's status it gives a row, 0 Pending, 1 Approved, 2 Not Approved; key true 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, else null), 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.export is its OK, job, size, save_to...) with watermark, its Watermark row ({"allowed", "on", "line", "toggle", "edit"}); and watermark_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-up Recipient), 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, the playlist and clip segments, rows -- each {"name", "ticked", "rect"} -- and line, "+ 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"}, watermark being 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, else null) and recipient_popup ({"field", "ok", "cancel", "entered"} while the run asks who receives it -- entered says whether the field holds something, never what --, else null).playback.ab_mode: true if 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's gain, 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 are 0, runs empty and idle true until 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 indices set-in and set-out take (out_point is -1 for "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 coordinates debug-mouse takes. valid is false while it is not drawn (another tab, a hidden panel) and nothing else is written. ruler_rect is the frame ruler [x0, y0, x1, y1], track_x / track_w the track's extent, in_x / out_x the two brackets, track_a_rect / track_b_rect the track rows (B's while A/B is on). in_grab / out_grab are each bracket's grab tab: rect where it sits and offered whether 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_gesture is the bracket a gesture is on (0 none, 1 in, 2 out) and bracket_editing whether it has passed the movement threshold (about 4 px times dpi_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), and rect [x0, y0, x1, y1] the extent drawn (null without a tick): inside track_x .. track_x + track_w, and within track_a_rect / track_b_rect for 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], null when not drawn. skinned: always true (the Classique mode no longer exists, the field stays); in every mode loop (the Loop button's word: "Shot", "Playlist", "Off"), timecode (View > Display in Timecode, which a Ctrl+Click on the counter switches) and active_tool (the tool in hand: -1 Selection, 0 Draw, 1 Text, 2 Arrow, 3 Shape, 4 Laser, 5 Eraser, 6 Pipette); nothing else in Classique. toolbar (the Timeline tab's bar) and version_board_toolbar (the Version Board's, the same bar on the version's length), null when not drawn: row, groups (annotate, view, playback, compare_display: each group's box, caption included; null for a group folded away), captions (the groups' captions drawn), counter_lines (2, or 1 in a narrow row), fold (the last step the narrow row took: 0 none, 1 the captions, 2 the counter on one line, 3 VIEW into "···", 4 ANNOTATE, 5 fps and px, 6 W and Fullscreen, 7 A/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, null without 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 line paint_bar and status), items (each control in the row by the same keys plus skip_back, step_back, play_reverse, play, step_forward, skip_forward, loop, mute, counter: its box over the row's height). ruler (null when 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), tag and tag_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 (null when 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; null for one not drawn). view_menu: the View menu's header, and its paint_bar and wipe_line switches (View > Paint Bar, View > A/B Wipe Line) while the menu is open, as drawn lately, null otherwise. 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) and wanted (fading in). The bar's one_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; its scrub is null (the scrub has its own row: one_line.scrub). In one line items also holds paint_bar (the brush, both levels) and, in Essential, status (the status switch, in px's place; px is then in more_items); paint_bar_lit (the brush lit: the paint bar on screen), faces (each control's own box as drawn, by the keys of items -- items gives its place over the row's height), status_boxes (the switch's three boxes, Not Approved, Pending, Approved, null each when not drawn), status_icons (the icon drawn in each box, "cross", "clock", "check", null when not drawn) and status_lit (the box lit, 0 to 2, the shot on screen's status; -1 for none). one_line (null when 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} or null when not drawn: first and last, the shot's first and last frames at the row's ends, and current, 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) and panel (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] or null (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 (4 previous shot, 5 play backward, 6 previous frame, 7 Play, 8 next frame, 9 next shot, 14 the sound, 15 A/B, 16 Loop, 100 first frame, 101 last frame, 102 the pause while playing): both views draw them, never an image; {} when none.edit: the playlist as one film, as edit replies without shot_list.view: what the viewer shows (view): layout ("picture", "contact-sheet", "edit", "steps"), layouts (as view replies: each with offered and refusal) and control, the View control at the viewer's top right -- drawn (on the last frames: Complete, a viewer wide enough), rect ([x0, y0, x1, y1], null when not drawn), menu_open and menu_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], null when not drawn. In every mode: skinned (always true: 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 what contact-sheet set), 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 as contact_sheet_storyboard (bool) and contact_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 also pill (the status pill: a click opens the status menu), badge and badge_text ("LATE +3", "MISSING", "UNREADABLE f1004"; "" and null without one), grade (Grade: Quick Color Grade) and card (the card's outline). In Dark and Light only, null in Classique: bar (rect, height, controls by key -- display, size, multi_play, auto_reflow, order, view, match_colors, paint; null for one folded into "···" --, more (the "···" button), in_more (the keys it holds) and segments (the display's three segments, Grid, Storyboard: all clips, Storyboard: active clip, each as wide as its words; null while the segment is in "···")) and selection_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) and layout, the view as drawn on the last frames or null -- 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) and scroll (pixels, when the grid is taller than the viewer). While it is shown the picture is not drawn: memory.smart.viewer_px has 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 or null -- skinned (always true: Dark and Light's bar, in the viewer's paint bar's style), rect, and in Dark and Light controls by 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 or null -- 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...) and copy, 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%) and outline_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, null in 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) and cells (each cell's card, department by department and shot by shot, null where a shot has no version of a department); boxes [x0, y0, x1, y1], null when scrolled away.playlist_row_menu: a playlist row's right-click menu as last drawn, null while closed (one menu, Complete's playlist's and Essential's column's): row, and entries in the menu's order, each words and rect ([x0, y0, x1, y1], the coordinates debug-mouse takes).playlist_ui: the Playlist tab, in the same coordinates; boxes are [x0, y0, x1, y1], null when not drawn. In every mode: skinned (always true: 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.filter is only read here, no command sets it), shown and total (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: false under a filter or a search), continuous (Continuous Playback (Play All)) and auto_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), with window (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; then radio_buttons: all, pending, not_approved, approved, each button's box) and rows (each row the search and the filter leave shown, in order, also those scrolled out of view: index in the playlist, status (0 Pending, 1 Approved, 2 Not 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; false for a row out of view: only the rows drawn ask for their thumbnail); in Dark and Light also pill (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) and lines (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_line and add_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", null with no filter), drop (the dashed drop zone under the last row), footer (null with an empty playlist: box, bar, segments (Pending, Approved, Not Approved; null for a status with no row), counts, ram_fit, unmeasured ("+N not measured"), lines (1, or 2 when 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_url and test_manifest (BERNARD_UPDATE_MANIFEST_URL, for tests), error, banner (show, rect, and the boxes of download, whats_new, later) and notes (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 as setting), banner.primary (the words of the bar's first button: "Download", "Update", "Restart and install", "Try Again", or "" while the installer downloads), one_click (offered and, 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, null when 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)) and last_install (what the waiter did before this start: result -- "installed", "installer_failed", "cancelled", "verify_failed"... --, exit_code, version; null when 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 or send-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, else null), http_status, error (the server's error, 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: 0 until 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_URL replaces the relay's address, for tests against a local server), and the boxes send, send_enabled, dont_send, close, open_folder, crash_box ([x0, y0, x1, y1], null when 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 boxes window, close, help_menu (the menu bar's Help header) and menu_item (the "What's New..." item, only while Help is open) as [x0, y0, x1, y1], null when not drawn. The notes open by themselves once, at the first start of a new version (the profile's saved_by_version differs from the running build): never on a fresh installation (no session_config.json at launch), never twice, and never on a launch with --ipc-port (like the update check) unless the environment has BERNARD_SHOW_RELEASE_NOTES=1 (TEST ONLY; a test writes an older saved_by_version in 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], null when not drawn. In every mode: skinned (always true: the Classique mode no longer exists, the field stays), health (the health of the decoding, read from the measures of decode and playback as 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) and clip_tier (the clip on screen's Proxy Resolution: 0 1/1 ... 3 1/8, 4 Smart RAM-Fit). views: one entry per window that drew the tab in Dark and Light on its last frame (the docked panel, its copies), with window (its name), health, fold (the last step a narrow tab took: 0 none, 1 the meters one under the other, 2 the settings' summary under "Global settings...", 3 each decode line under its name, 4 the segments as the Proxy Resolution combo, 5 the 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)) and tiers (the five segments' boxes, 1/1 to Smart RAM-Fit; null when the combo is drawn). In the Complete mode the tab also draws two foldable cards under Memory: complete (true unless the Essential mode is on), folded (the keys of the cards the user folded, kept in the profile), and in each view cards (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) and detail_values (the text each detail line shows, null when 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 (always true: 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_x either side of a tab's words; gap_x between 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.26 in 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 with view ("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 drawn docked, 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 (0 when hidden), window_rect, tab_bar (rect, under, tabs: each tab's label (the words: "Versions" in the skin), id (its identifier: "Version Board") and rect), and when the row is hidden and holds them (Complete, and Essential's bottom panel) pin and copy (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 (null when 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) and enlarged; enlarged, the name of the panel enlarged to the whole window (Enlarge Panel, Ctrl+Shift+Space, or the panel menu), null for none; enlarges and restores, 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 (on while 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: open on the last frame, rect [x0, y0, x1, y1] or null, 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's pen)), 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 a paint_rule heard), sent (the paint_rule messages 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 a paint_proposal since 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_host as 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 a paint_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, the paint_refusal messages 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"), keep and remove ([x0, y0, x1, y1], null where 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 the handle it 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: indices and count (every selected drawing of the frame), mode (rectangle or lasso), select_tool (the Select tool chosen), handles (the transformation box's top_left ... left and pivot, [x, y] in debug-mouse's coordinates, null while not drawn), pivot_custom, gesture (kind, handle, angle_deg while 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) and bar (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, opacity 0..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: strokes held, copied_frame and pasted_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_shown and tool_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], in debug-mouse's coordinates) and items, left to right, each {"item", "group", "rect"} (items tool, 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_options with the Text tool, onion, eye, undo, redo, clear; groups tool, color, settings, display, history). Dark and Light: grip (the grip at the bar's left that moves it: drawn, rect, hovered, dragging) and place (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, moved false; at, the bar's top left; default, its default place, centered under the picture) and history (undo, redo, clear as drawn last: "available" (lit), "unavailable" (grayed: nothing to undo, redo or clear on the frame, or a guest), "hovered" (lighter); undo_steps, redo_steps, strokes of the frame; null while 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) and looks (its active counts, 0 without a config), default_display, default_view, and error (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 .ocio file on the window, kept in session_config.json (ocio_config), and never taken from $OCIO. OCIO step 2 adds the color space names to files and to the wire. A .bpl item and a session entry (session_config.json, last_session.playlist) write "input_space_name" beside "grading", only when has_saved_settings is 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's viewport message carries, only when the pilot has a config, ocio_config_id (the config's OCIO cache id) and input_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 .ocio file over 16 MiB is refused (a .ocioz archive is not held to it), and an error said in status, system-info or 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 .cube LUT 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 by set-ocio-view; the menu's pick is kept in session_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.ocio also holds display, view and look (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) and following_driver (bool). The OCIO programs are built off the render thread (worker bp-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-dir frames 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's viewport message also carries ocio_display, ocio_view and ocio_look (strings, only from a pilot with a config, ocio_look only 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 .bpl item 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 whatever has_saved_settings says, and read whatever has_saved_settings says, 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 of slope, offset, power and saturation is optional (its identity value when absent), at least one must be present; id and source are optional strings (source is the file's name: a path is cut to its name); on is optional and false only 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 in status.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's CDLTransform with the style CDL_NO_CLAMP does, with or without a config; the same picture. Thumbnails, screenshot, the --review-dir frames 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's viewport message carries cdl_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 also problem (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], null when not drawn. In every mode: skinned (always true: 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, opened and unseen (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; opened by 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, and cards (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)"), size and aside (the skin's two parts, the aside muted; the line whole in size in 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") and rect (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]) and filter_field (the text filter's box)) and counts (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_b in session_config.json). path is a movie's path, or the first frame of an image sequence ("" when B is empty), is_video says which, frames its length. edit_target is the track the inspector edits (0 = A, 1 = B), wipe_position the divider (0..1 across the picture), wipe_angle its angle in degrees, exposure and gamma track B's exposure and gamma (set-exposure / set-gamma with "track": "B"), grade track B's whole grade (the keys of color.grade; the session saves its look, all but mask, input_space and display_space, which B's loader and channel view decide). session_restore is 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 as set-track-b refuses it).track_b.decoded_width / decoded_height: the size B's picture on screen was decoded at, as viewport.decoded_width for A (0 before 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 (-1 before the first).track_b.pinned: the pin on B's chip (pin-track-b), as the session saves it: true keeps B through the playlist's clip switches, and it is always false while B is empty. Local to this seat, like everything else in track_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). row and how: the last row put on B that way and how ("menu", "track_b_row", "picture_b"; -1 and "" 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) and track_b_row: track B's row of the timeline (on the main window), [x0, y0, x1, y1] in debug-mouse's coordinates, null when not drawn. cache: B's frames in RAM -- key (B's cache key: a film's path, "seq_b_" + a sequence's first frame; null for an empty B), shared_from (the playlist row's key a sequence on B reads its frames from, null for none), frames (the frames of B's layer B can show from RAM), own_frames and shared_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.counts proves 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) and cycle-b step 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, and cycle-b's reply names the rows for from and version. 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] in debug-mouse's coordinates, null when 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 and cycle-b walk, in step order (by number; entries without a number are left out), current marking 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: where cycle-b "prev" / "next" would go, null at either end.track_b.version_preload: the pre-load of those two versions. enabled is the preference (set-b-version-preload); cap_bytes its 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_bytes what the two versions hold, and per version the frame under the playhead (null while its frame list is still being read), whether that frame is resident, and the bytes held 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 (null before the first): from and to versions, direction, preloaded (its frame was in RAM at the command), and ms, 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); null while 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 with late_frames and lost_frames.playback.dropped_frames: an alias of playback.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 to playback.lost_frames for frames never shown, or to loop_iterations_missed if 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 than late_tolerance_ms after its due time (one and a half refreshes of display_hz, at most one frame). late_frames counts the frames that were not on time — shown at a later refresh, or never. lost_frames counts 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 reads 0 late and 0 lost.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 playback computes stays 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 (and decode.track_b for 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_frame is 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 under 1000 / fps; avg_decode_ms is the plain mean of each decode, samples how many frames the reading is on, scale the tier they were decoded at (only the tier of the newest frame counts), newest_age_s how long ago the last one ended. valid is false with fewer than 4 frames, or when nothing was decoded for 5 s (a clip entirely in RAM has no decode speed). budget_ms is 1000 / playback.fps.decode.too_slow: ms_per_frame above budget_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 is too_slow without a warning.decode.slow_decode_tier: the setting, "auto" or "off" (set-slow-decode-tier); decode.slow_decode_tier_effective the 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_scale is then the lower one, and ram_fit / playlist[] still report Smart's. decode.last_change: the rule's last decision (null before 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), from and to tiers, the ms_per_frame behind it, seconds_ago, and the notice the 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 (null before 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_before and length_after, seconds_ago, and the notice the player showed. frames: the film's length as the track uses it now (its strip, its cache line, the timeline's max(A, B)), 0 for 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's sequence_decodes), sequence_served_from_ram (jobs the pool served from RAM instead, sequence_decodes_skipped), video_a and video_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_b counts 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 .bpl item's grading has (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, and stages_off -- the stages the Color panel's eyes turned off, a list of their keys, only while one is; and curve_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; and range_<reds|yellows|greens|cyans|blues|magentas>_<hue|saturation|lightness> and lightness -- 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 no set- command (Quick Color Grade sets them, a .bpl or the session brings them back): this is where a script reads them. track_b.grade is 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 with track it was drawn on, version, type, points and its first / last point, 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) and version; for B also clip and home ("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"} -- key is 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, and sh010_v002 another one than sh010_v001), count its notes (one per frame with strokes, one for the written note), frames the frames with strokes (the timeline's marks), note the 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, box and undo_box ([x0, y0, x1, y1], the coordinates debug-mouse takes). media_notes.menu_entry: Delete notes of this clip... in the row's right-click menu while it is open, else null; media_notes.delete_dialog: its confirmation, open, question, and its delete and cancel buttons' 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) and local_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"}, tile being its thumbnail's state (not_drawn before a view drew it, loading, ready, missing: its bytes found nowhere -- a .bpl moved without its folder --, unreadable) and refusal the words said under the note ("" none); last: the last attach, {"row", "code", "reason", "sha256"} (code "" when attached, else empty, 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 open sha256, ready, failed, window, picture and close (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's guest_images, the images of its note-takers' notes, {"note_id", "relay_id", "sha256", "bytes", "width", "height", "tile"} (relay_id the 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); and relay, counts only: features (the relay's relay_info said it keeps images), uploaded (the host's images it has), uploads_asked, downloads_asked (in this review). Under review_notes_ui.cards[], images and image_crosses are the tiles and their ✕, image_refusal the refusal's line; under ui_level_ui.essential.inspector, note_images and note_image_crosses.review_notes_ui: the Review Notes tab as it was last drawn (by the docked panel or a copy, whichever drew last), null before it was drawn. Boxes are [x0, y0, x1, y1] in the coordinates debug-mouse takes, null when not drawn. skinned (always true: the Classique mode no longer exists, the field stays), frame, only_annotated (Only annotated shots, kept in the profile as notes_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) and crosses (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), null when 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_linked and not_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) and rect. ranges: each card whose note is on a passage, its row and the rect of its passage's label ("1012-1040"; a click plays it in a loop). earlier (manu/earlier-notes): each card's Earlier notes head drawn, row and head (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.manual whether that is the segment's choice (else the view's), metadata.toggle whether the A · B · A|B segment is up (A/B on), metadata.count how 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 as metadata_pinned_keys in 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), 0 otherwise. metadata.diff_only: whether "Differences only" is ticked. metadata.skinned: always true.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] in debug-mouse's coordinates, null when not drawn: window (its name), fold (the last step a narrow tab took: 0 none, 1 the segment and "Differences only" under the search, 2 the key's column narrower, 3 each value under its key, 4 the 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) and rows (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 (0 Linear ACEScg, 1 sRGB, 2 ACES2065-1 (AP0), 3 Adobe RGB, 4 ProPhoto RGB, 5 Linear sRGB / Rec.709, 6 ARRI LogC3 (EI 800), 7 ARRI LogC4) -- what set-colorspace sets. 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 (-2 a data pass, -1 no 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 has color.input_space_name (its input color space by its name in the config; null without 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); null without a name), color.input_space_name_shown (a bool: the built-in pipeline draws exactly that space; false for a space without a twin, drawn with the clip's built-in index until the OCIO display) and color.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.cdl is the CDL of the shot on screen as a .bpl item writes it (slope, offset, power, saturation, and id, source, "on": false when they apply), or null when 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 .bpl or 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 to channel while the two are linked, the default.color.channels_linked: whether A and B show the same channel. An Alt+Click on a channel (or set-channel with track) unlinks them; a normal click (or set-channel without track) links them again.audio.volume: The volume level between 0.0 and 1.0.audio.muted: true if the audio is currently muted.viewport.decoded_width / decoded_height: the size the frame on screen was actually decoded at. Divide native_width by 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; 0 for image sequences). Prefer source_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 every status). 0 while no clip is open, or before the clip's metadata has landed.viewport.retiering: true between a proxy-tier change and the first frame decoded at the new tier reaching the screen. While it is true, proxy_scale already describes the new tier and decoded_width still the old one; when it turns false, decoded_width matches proxy_scale. This is not playback.transitioning, which means a change of clip. In A/B, proxy_scale is 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: retiering turns false once A's picture is at A's tier (decode.scale), and decoded_width then describes that tier (it used to stay true for as long as a lower B was there).playback.seek: the last jump of the paused playhead (a seek, a click or a drag on the timeline, a step), phase by phase, null before 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 and null until 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) and presented_ms (the swap that put it on screen); turns (render turns until the draw) and drawn_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, -1 when none (frame_b is -1 while A/B is off). A frame that is not in RAM yet is stood in for by the nearest earlier one: while playing, a frame_a that stays behind playback.currentFrame is 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 (0 full, 1 half, 2 quarter, 3 eighth, 4 Smart RAM-Fit), the saved one, and viewport.proxy_idx_effective the one in effect (4 while Essential decides, see Interface Level); viewport.proxy_scale the 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 (0 to 3, 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 (0 hidden .. 1 shown; always 1 outside auto-hide), and the picture's rectangle on screen as last drawn ([x, y, width, height], screen coordinates -- the ones debug-mouse takes).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) and rect ([x, y, width, height], the same screen coordinates as picture_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's rect and the stream's frame at the same place of the picture. Each label has track, text, rect and opacity (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_bytes is the machine's RAM (ram_simulated when the BP_SIMULATED_RAM_GB test variable gave a smaller figure), hard_ceiling_bytes that RAM less the 20% always left to the system, ceiling_bytes the RAM Limit under it. posts holds every post as counted (cache, pool_idle, decode_in_flight, disk_write_queue, shared_vram_ring, thumbnails, contact_sheet, and other, the rest of the process as measured), reserved the same with the pool and the disk write queue at their budgets, cache_allowance_bytes what 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's cache_max_bytes is 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. The BP_SIMULATED_COMMIT_GB test variable makes the commit that many GB, and under BP_SIMULATED_RAM_GB the 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_bytes are the process's private bytes (Linux: anon-rss) and working set (RSS), process_counted_bytes what the ceiling counts, process_peak_* their highest this session. pressure is the memory-pressure rule: level (none, moderate, severe), cap_bytes (the cap it puts on the cache, 0 none), sheds, and what it read (psi_* from /proc/pressure/memory on Linux, mem_available_bytes). smart is 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. badge is 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), reason of an original (pipette, zoom), tier and tooltip. original_frame is 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 in posts.decode_in_flight), read_ms and shown_ms from the request, and the counts requests, drops, stale, failures. original_reader is 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 in posts.decode_in_flight) and decoder_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). pipette is its last readout: read_scale (the tier of the frame it read), preview (true while that is a reduced frame) and rgb.preload_window: the sliding preload window, highest priority first: entries[] with item (playlist index), frames (-1 the whole clip, else its first frames) and why (on screen, next, start, next, plays next, previous, start, whole playlist); paused while the user scrubs or draws; precache_whole_playlist.gpu_decode: the beta that decodes .nvfx frames on the graphics card (set-gpu-decode; see NOVA). enabled is the preference; setting is "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_default is this release's default; summary is the line Preferences shows under the switch, in plain words (who decodes the picture on screen and why, or why it is off); state is "off", "starting", "ready", "unavailable" (the card or pilot cannot do it) or "disabled" (turned off until the next start after failures); reason is written only when there is one (why it is unavailable or disabled); decoder_allocated is false whenever the beta has been off since the start. frames_prepared counts the frames the workers packed for the GPU, frames_gpu the frames decoded on the GPU into a texture (the VRAM ring's and the picture's), ring_copies the pictures copied from the VRAM ring instead of decoded again, fallbacks the frames the CPU decoded after all at upload, failed_codeblocks the code-blocks the GPU found broken. vram_bytes is the video memory the GPU decoder itself holds (0 without a decoder; see GPU decode (beta)). clip is "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). enabled is the preference; setting, on_by_default and summary as for gpu_decode; state is "off", "starting", "ready", "unavailable" (the card or driver cannot do it) or "disabled" (turned off until the next start after failures); reason is written only when there is one (why it is unavailable or disabled); converter_allocated is false whenever the beta has been off since the start. frames_planes counts the pictures the decoders handed out as planes, frames_gpu the pictures converted on the GPU (the viewer's and the VRAM ring's), fallbacks the planes frames shown through the CPU twin. clip and clip_b are 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_reason and clip_b_reason say 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, clip is "cpu-rgb" and clip_reason "the beta is off". vram_bytes is the video memory the converter's plane textures hold now, in bytes, and staging_bytes the RAM of its upload buffers: both 0 while 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). enabled is the preference; setting, on_by_default and summary as for gpu_decode; state is "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); reason is written only when there is one. api is "nvdec", "d3d11va", "vaapi" or "none". decoders_on_gpu counts the decoders open on the card now (at most 4), vram_bytes the video memory they hold (an estimate per decoder: about 0.8 GB for an 8K 8-bit HEVC, 1.6 GB at 10 bits; 0 with none open) and vram_room_bytes what 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_source says which: "drawing card budget", "decoder card memory" or "fixed fallback". frames_gpu counts the pictures the card decoded, fallbacks the decoders that opened their film again on the processor after a failure of the card, refusals the 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). clip and clip_b are the route of track A's and track B's film: "nvdec", "d3d11va", "vaapi", "cpu" or "none"; clip_reason and clip_b_reason say 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, clip is "cpu" and clip_reason "the beta is off". ram-info counts the card's frames as pixel_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). enabled is the preference; state is "off", "starting", "ready", "unavailable" (the card, driver or FFmpeg cannot do it) or "disabled" (turned off until the next start after failures); reason is written only when there is one (why it is unavailable or disabled); engine_allocated is false whenever the beta has been off since the start, and vulkan_devices_created counts the Vulkan devices this process created for the engine (0 while the beta has been off since the start). vram_bytes is always present: the memory of the decoders (FFmpeg's frame pools on the card, shared with OpenGL), 0 while 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, device is the card's name, copy_free is true (the frames are shared with OpenGL without a copy) and decoders the decoders open now. frames_packets counts the pictures the decoders handed out as packets, frames_gpu the pictures decoded on the GPU (the viewer's and the VRAM ring's), fallbacks the packet frames shown through the processor decode. clip and clip_b are 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_reason and clip_b_reason say 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, clip is "cpu" and clip_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 (always 0, no ceiling) and common_tier / common_scale (always 0 / 1.0) are kept for the scripts that read them. estimated_bytes against budget_bytes (the RAM limit less the margin background caching keeps free) is the sum the decision was made on. status is "fits", "needs_fixed_items", "does_not_fit" or "unmeasured", and fits is the first of those as a boolean — so fits is false for the other three. suggested_fixed_tier is -1 unless the playlist would fit if the items whose tier you fixed came down to that tier — the playlist panel offers exactly that, and ram-fit-apply is the offer accepted. banner is 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), line and question (its two lines; with short and no offer, the second gives the RAM free on the computer and "close other applications"). A video longer than streamed_video_seconds is played from disk and left out of the sum entirely; an image sequence never is.ram_fit.unmeasured_count and "status": "unmeasured": how many items the player has not been able to measure, and therefore how many weights estimated_bytes is 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" with fits false, and the count says how many items are missing from the total. Precedence is deliberate: a playlist already past budget_bytes on what is measured reports "does_not_fit" or "needs_fixed_items" even while unmeasured_count is 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, test fits; to tell "no" from "not yet", read status; to know how much of the list the total speaks for, read unmeasured_count against the length of playlist[]. A clip stays unmeasured until it can be opened for its dimensions — the player retries it every 30 s — so an unmeasured_count that 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 a playlist[] 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_playlist tells the two cases apart. present is false and no other field is written when track B is empty. effective_proxy_idx is the tier that copy decodes at, estimated_bytes what it holds there, and resident how many of its frames are actually in RAM (the track-B equivalent of ram-info's cached_frames, which counts by path and therefore cannot see this copy). If estimated_bytes here is missing from your own sum, ram-info's cache_bytes will run ahead of ram_fit.estimated_bytes by roughly this much. shares_row and shares: 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_row names the row (-1 for none, or for track A's film on no row -- no entry weighs A then, so B is weighed). shares is true, and estimated_bytes 0, 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_idx is the tier the item asks for (4 = Smart RAM-Fit), as saved; proxy_idx_effective the one in effect (4 for every item while Essential decides, see Interface Level); effective_proxy_idx and effective_proxy_scale are what it actually decodes at. They differ for every Smart item the fit moved — reading only proxy_idx, or only viewport.proxy_idx, tells you nothing about the off-screen items. estimated_bytes is what that item would hold at its effective tier (0 for a streamed video); ram-info's cached_frames against the item's duration says how much of it is actually resident. measured is false while nothing has read this item's dimensions — a truncated or half-written file, a path that will not open — and while it is false the other two numbers are placeholders, not readings: estimated_bytes is 0 because there is nothing to weigh, and effective_proxy_idx is 0 (1/1) because the tier formula answers full size for a width of 0. Do not sum estimated_bytes across the array without checking measured, or checking ram_fit.unmeasured_count once; the playlist panel draws ? for both numbers on such a row, and the same field is on ram_fit.track_b.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.
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()
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'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()
Here are production-ready integration scripts. They include extensive comments explaining the code mechanics for technical artists and TDs.
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.")
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.")
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()
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.
load-playlist accepts a .bpl file -- Bernard Review's own JSON playlist format, documented in playlist.schema.json. Build one from Bernard Engine / ShotGrid / ftrack by writing that JSON directly (no player-side parsing beyond what already exists) and pointing load-playlist at it.export-notes triggers 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 in notes_payload.schema.json.x_tracking (shot_id, shot_name, version_id, shotgrid_version_id, ftrack_id, artist, task) is set when building the .bpl passed to load-playlist, persists through .bpl save/autosave, and comes back out per-shot in export-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 the x_tracking block in both schema files.screenshot captures any single frame of the active track-A item to a PNG, graded exactly as currently shown on screen.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 |
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.
.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; default wavelet).quality: 1..100 for the two lossy profiles, 92 = visually lossless.bit_depth: 16 (half) or 32 (float).all_layers (default true): 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 to lo, ramps up to the noise floor's knee at hi, 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" or true mean "0,0.5"; "off" (or false) 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. See docs/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.
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).
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 a drop whose target is track B ("at": "track_b" or "viewport_b"), reply the same way ("cmd" is theirs); whatever track B held stays there.drop anywhere 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.export job with "format": "nvfx" -- or an output whose 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 ... prints export: This format is not supported in this build. and exits non-zero; the usage line's --format list 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.
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.
ram-info counts these frames as pixel_type "yuv-planes".status.gpu_yuv.clip and clip_reason say which route a track took.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. Init also 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.status.gpu_yuv.vram_bytes is what the plane textures hold (about 50 to 210 MB a layout at 8K) and staging_bytes what 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).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.
set-hardware-decode) is unchanged; when both are on, this beta takes the films it decodes.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.api says which one is in use.status.gpu_video_decode.clip and clip_reason say which route a track took.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.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 in refusals; the next films of that kind and size stay on the processor at their open until the beta is turned off and on again.BP_GPU_DECODE_API set to nvdec, d3d11va, d3d11va:N (adapter N, 0 to 15) or vaapi forces the API instead of the automatic choice; any other value is ignored.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.
GL_EXT_memory_object and GL_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-info counts these frames as pixel_type "prores-packet".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.clip and clip_reason say which route a track took.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.status.gpu_prores.vram_bytes, which is always present (0 while 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 when engine_allocated is true, is the number of decoders open now.