{
  "_": [
    "THE STREAMING COLLECTION — 1920x1080, the scenes an operator actually cuts between.",
    "",
    "  Broadcast 3D   the produced picture (index.html)",
    "  Race cam       every camera in the race, in ONE browser source (boatcams.html)",
    "  Boat cam A..D  one camera full-frame, for cutting to a single boat",
    "",
    "IT NAMES ITS OWN COLLECTION AND PROFILE, and that pairing is the point. The canvas belongs to a",
    "PROFILE; the scenes belong to a COLLECTION; and collections are shared across profiles. So a",
    "1080p `Race cam` living in the same collection as a 5760x3240 video wall is one profile switch",
    "away from being drawn into the top-left ninth of the wall canvas with black around it — which is",
    "exactly what happened on 2026-08-06 and read as four dead cameras rather than as a layout shown",
    "at the wrong size. Two collections, two canvases, and neither can be rendered against the other.",
    "",
    "NOTHING RE-POINTS THESE SOURCES, EVER. Every camera URL asks for a SLOT (`?slot=A`), not a hull,",
    "and the race plane answers with `meta.cameraSlots` in the feed header. The tiles follow the fleet",
    "from heat to heat by themselves: no follower process to remember to start, no URLs stored in OBS",
    "that go stale the moment it is not running, and no page reload (a second of black) on rotation.",
    "",
    "`Race cam` IS ONE SOURCE, not four tiled by OBS, for two measured reasons. First, connections:",
    "each page holding its own SSE costs one of the ~6 a browser allows per origin, OBS shares one",
    "network stack across all its browser sources, and four cameras plus the programme exhausted the",
    "pool — the clip fetches then queued for ever behind them and the tiles sat black with no error",
    "and nothing in any log. The panel page holds ONE connection and relays it to its tiles. Second,",
    "layout: a CSS grid sizes itself to whatever canvas it is given, where an OBS-side tiling had to",
    "be TOLD the canvas and was told wrong.",
    "",
    "The single-boat scenes come from `${camorigin}` — see `vars._camorigin`. On this rig that is the",
    "same server on its other loopback name, which is what keeps six pages under a six-connection cap.",
    "",
    "`Video wall` IS THE THIRD-SCALE WALL, and it is here — in the 1080p collection, cuttable beside",
    "the other five — rather than in the 5760x3240 one, because THROUGH A SPLITTER THE TWO DELIVER THE",
    "SAME PIXELS. The Lindy 38062 duplicates one signal and lets each panel crop its own ninth, so the",
    "wall's real resolution is whatever single signal enters it. Scale a 5760x3240 canvas down onto a",
    "1920x1080 output and every cell lands at 640x360; lay the same grid out natively at 640x360 and",
    "every cell lands at 640x360. Identical output, one canvas, one profile, one collection — and no",
    "pair of menus to keep in step, which is the failure this whole file's header is about.",
    "",
    "IT BORROWS ITS PICTURES AND OWNS NONE OF THEM. Five of its seven cells carry sources the other",
    "scenes already define, listed here with a TRANSFORM AND NO SETTINGS: a scene item is a placement,",
    "so the same page can be full-frame in `Boat cam A` and a 640x360 tile here without loading twice.",
    "That is not tidiness, it is the connection budget — a second copy of each page would cost four",
    "more of the ~6 connections a browser allows per origin, which is precisely how the tiles went",
    "black with no error the last time. Only cell 31 adds sources, and Burgee is a third origin",
    "holding no feed connection at all.",
    "",
    "THE TEXT CELL CANNOT WIN AT THIS SCALE and the file should say so rather than imply otherwise.",
    "The 5760 wall renders results/entries at 480x540 and scales them 2x so they read from across a",
    "room; a third-scale cell is 320x360, so the same page is displayed SMALLER than its natural size.",
    "It is legible on the operator's monitor and at a fullscreen projector, not from the far side of",
    "the room. If that cell is the one that matters, that is the argument for Mosaic or a wall",
    "processor in `docs/plans/trwc-video-wall.md` §3 — not for a different transform here.",
    "",
    "The 5760x3240 layout stays in `obs/video_wall.json` and is still the right file the day the wall",
    "is driven as one spanned desktop rather than through a splitter. Two files, two canvases, one of",
    "them reachable today.",
    "",
    "  python -m v3.broadcast.live.obs_scenes apply v3/broadcast/live/obs/stream_1080.json \\",
    "      --var producer=http://localhost:8766",
    "  ...in production, keep producer/camorigin on localhost/127.0.0.1:8766; the private relay owns Cloud Run IAM",
    "",
    "`Start vessel` / `Finish vessel` are the two RACE-COMMITTEE cameras, addressed by ROLE",
    "(`?vessel=start|finish`) rather than by slot. A slot is a position within the RACING FLEET, and a",
    "race-committee vessel has no team, no sail number and no place in the order — it is a phone on a buoy",
    "slot pointed down a line. The producer publishes `meta.vesselCameras` for exactly this, so these",
    "two follow the event's own declaration (`devices.vessels`) and need no re-pointing between heats,",
    "the same property the lettered slots have.",
    "",
    "They are DARK most of the day, and that is the feature rather than a fault: each films only its",
    "own part of the race (the start sequence, and from the first boat rounding the last mark), which",
    "the SERVER enforces by varying the config those phones poll.",
    "",
    "`Break line-up` / `Break roll` ARE THE PAUSES, and a broadcast day is mostly pauses. Everything",
    "above shows a race; these two show the time between them. The line-up is the fleet roll-call —",
    "it needs no material, cannot run out, and no upload can affect it. The roll is video our own",
    "people filmed on their phones, each clip approved by a human before it ever reached this box.",
    "",
    "THE ROLL IS LAID OVER THE LINE-UP ON PURPOSE. `clear_on_media_end` makes the media source go",
    "transparent when the roll finishes, so the scene ENDS on the roll-call rather than on a frozen",
    "last frame — and the same fall-through covers a break folder that is empty, a file that has not",
    "synced yet, and a `--var breakdir=` that is wrong. A pause can therefore never end in a hole.",
    "",
    "IT IS A MEDIA SOURCE OVER ONE FILE, NOT A VLC PLAYLIST OVER A FOLDER, and the reasons are all",
    "on-air ones. `vlc_source` exists only if a VLC of matching architecture is installed, which is a",
    "machine-specific dependency in a file whose whole purpose is to be rebuildable on the second",
    "box. A playlist enumerates its directory when the scene activates, so a file replaced underneath",
    "it is discovered in front of an audience. `close_when_inactive` releases the handle whenever the",
    "scene is off programme, which is what lets the syncer overwrite the file at all — Windows will",
    "not replace a file OBS holds open. And the order is then written down in the concat rather than",
    "left to a directory listing. The syncer joins the approved clips, in approval order, into that",
    "one file.",
    "",
    "The clips carry THEIR OWN AUDIO, normalised to the programme's own loudness (-14 LUFS), so a cut",
    "to a break is not a jump in level.",
    "",
    "`Standings` IS THE SERIES, and it is what a pause is actually for. `Break line-up` says who is",
    "here and `Break roll` shows the day; neither says what the racing has DONE. The card is our own",
    "page (`core/web/standings.html`) drawing numbers the RACE OFFICE published — the producer reads",
    "them server-side and republishes them on `/standings.json`, so the picture is ours and the",
    "arithmetic is theirs, which is the only arrangement in which the card cannot disagree with the",
    "results posted on the wall.",
    "",
    "IT IS A BROWSER SOURCE ON `${producer}` LIKE EVERY OTHER PAGE HERE, and that is the whole reason",
    "it is built this way rather than pointed straight at the race office's own results page. Their",
    "page carries their navigation, their footer and a per-row edit menu; it does not refresh itself,",
    "so it would show the morning's table all afternoon; and `obs_scenes` PREFLIGHTS every origin a",
    "manifest names, so a third party's outage would have refused an apply of this whole file — the",
    "emergency scenes included. One origin, one failure domain.",
    "",
    "It keeps `programme` beneath it, so cutting to the table does not cut the commentary. That is why",
    "it is in `audio_continuity` beside the cameras rather than with the two break scenes: a break owns",
    "its own sound, while this is a graphic laid over a race that is still being talked about.",
    "",
    "COMMENTARY IS A PERMANENT BUS ACROSS EVERY VIEWREGATTA RACING PICTURE. `programme` is the bottom",
    "item in Race cam, each individual boat camera and both committee-vessel camera scenes. Their",
    "opaque picture covers its 3D picture while the SAME browser-source instance remains active and",
    "continues feeding commentary into the mixer. There is no duplicate renderer and no second voice.",
    "Break scenes deliberately omit `programme`: external or editorial break material owns its own",
    "sound, while the race renderer remains loaded (`shutdown` false) and rejoins live on return."
  ],
  "collection": "Streaming 1080p",
  "profile": "Untitled",
  "canvas": [
    1920,
    1080
  ],
  "audio_continuity": {
    "source": "programme",
    "preserve_in_scenes": [
      "Race cam",
      "Boat cam A",
      "Boat cam B",
      "Boat cam C",
      "Boat cam D",
      "Start vessel",
      "Finish vessel",
      "Standings"
    ]
  },
  "vars": {
    "producer": "http://localhost:8766",
    "_mediabase": "Where the tiles fetch their CLIPS. On the rehearsal rig one process serves both the feed and the media, and 127.0.0.1 and localhost are DIFFERENT ORIGINS to Chromium — the very same server with its own pool of connections. Production does not need this: the feed hands out signed GCS URLs, which are already another origin, and Cloud Run is HTTP/2 where the per-origin limit does not apply at all.",
    "mediabase": "http://127.0.0.1:8766/demo/data/media/",
    "_readbase": "Origin serving /v3/view/... HLS playlists. In production set this to https://apps.viewregatta.com.",
    "readbase": "http://127.0.0.1:8766",
    "_camorigin": "The four single-boat scenes are served from a SECOND ORIGIN, and on an HTTP/1.1 rig that is load-bearing arithmetic rather than a trick. Every page holding a feed connection costs one of the ~6 a browser allows per origin, and OBS shares one network stack across all its browser sources: programme + panel + four single-boat pages is exactly 6, and the one that loses the race shows no nameplate, no stamp and no card — it just never finishes loading. `127.0.0.1` and `localhost` are different origins to Chromium and the very same server, so this costs nothing and halves the load on each. IN PRODUCTION SET IT EQUAL TO `producer`: Cloud Run is HTTP/2, where requests are multiplexed over one connection and the per-origin limit does not apply.",
    "camorigin": "http://127.0.0.1:8766",
    "_standingsbase": "Where the SERIES CARD is served from. It is a page of the producer's like every other one here, and in a settled rig this is simply `producer` — it carries its own name for the same reason `camorigin` does, so a box can serve it from somewhere else without repointing the programme. That is not hypothetical: the card and its `/standings.json` route reach a deployed producer only when that producer is deployed, so until then this points at a producer running beside OBS while the programme keeps coming from wherever it already does. SET IT EQUAL TO `producer` once they are the same service. The partner export already assumes they are.",
    "standingsbase": "http://127.0.0.1:8767",
    "_burgee": "The race office's own pages, and a THIRD origin — it holds no feed connection, so the two cells in the wall's bottom-left cost nothing against the ~6-per-origin budget the cameras are spending.",
    "burgee": "http://127.0.0.1:8090",
    "_breakdir": "Where `v3.broadcast.live.break_roll` syncs the approved break clips, ON THIS MACHINE. Unlike every other var here this is a local absolute path rather than a host, so it is machine-specific and belongs on the command line: --var breakdir=D:/vr/break_roll. The default below is a placeholder, not a promise. It is NOT an error for the file to be absent — `break:roll` has the line-up beneath it and falls through to it — which is also the ordinary state of that scene before the day's first clip is approved; preflight says so and carries on. A path that is absent AND uncovered is what it refuses on.",
    "breakdir": "D:/vr/break_roll",
    "_replaydir": "Where `v3.broadcast.live.replay_take` installs the clip `day_cutter` made, ON THIS MACHINE — the same kind of local absolute path as `breakdir` and `emergencydir`, and it belongs on the command line for the same reason: --var replaydir=D:/vr/replay. It is NOT an error for the file to be absent. That is the ordinary state before the day's first replay is taken, and `replay:clip` has the programme beneath it and falls through to it.",
    "replaydir": "D:/vr/replay",
    "_emergencydir": "Local, prebuilt safety media. Build it before air with `python v3/broadcast/live/emergency_stream.py build --output <dir>/technical_hold.mp4`. This directory must remain usable with the producer, database and internet unavailable.",
    "emergencydir": "D:/vr/emergency",
    "event": "trwc-2026"
  },
  "program": "Broadcast 3D",
  "scenes": [
    {
      "name": "Technical interruption",
      "_why": [
        "The last honest picture when the producer, telemetry, database or network is unavailable.",
        "It is one LOCAL H.264/AAC file built and verified by emergency_stream.py — no browser source,",
        "no URL, no application process and no remote font. It stays open and looping so an emergency",
        "cut does not wait for Windows to open a decoder. Replacing it during a show is deliberately not",
        "supported; rebuild and preflight before air, while this source is not carrying the programme."
      ],
      "items": [
        {
          "source": "emergency:technical",
          "kind": "ffmpeg_source",
          "enabled": true,
          "settings": {
            "local_file": "${emergencydir}/technical_hold.mp4",
            "is_local_file": true,
            "looping": true,
            "restart_on_activate": false,
            "close_when_inactive": false,
            "clear_on_media_end": false,
            "hw_decode": true
          },
          "transform": {
            "positionX": 0,
            "positionY": 0,
            "alignment": 5,
            "boundsType": "OBS_BOUNDS_SCALE_INNER",
            "boundsAlignment": 5,
            "boundsWidth": 1920,
            "boundsHeight": 1080
          }
        }
      ]
    },
    {
      "name": "Emergency last video",
      "_why": [
        "A deliberately DEGRADED picture: the most recent locally validated clip over the technical",
        "card. The card is the enabled item beneath it, so an absent, empty or exhausted cache cannot",
        "make black. `last_good.mp4` is labelled as earlier material inside the cached asset; this scene",
        "must never imply that a frozen or replayed picture is live.",
        "",
        "The cache file closes while this scene is inactive so a separate cache process can atomically",
        "replace it. Once the scene is on programme it owns the open file until the operator cuts away."
      ],
      "items": [
        {
          "source": "emergency:technical",
          "enabled": true,
          "transform": {
            "positionX": 0,
            "positionY": 0,
            "alignment": 5,
            "boundsType": "OBS_BOUNDS_SCALE_INNER",
            "boundsAlignment": 5,
            "boundsWidth": 1920,
            "boundsHeight": 1080
          }
        },
        {
          "source": "emergency:last-good",
          "kind": "ffmpeg_source",
          "enabled": true,
          "settings": {
            "local_file": "${emergencydir}/last_good.mp4",
            "is_local_file": true,
            "looping": true,
            "restart_on_activate": true,
            "close_when_inactive": true,
            "clear_on_media_end": true,
            "hw_decode": true
          },
          "transform": {
            "positionX": 0,
            "positionY": 0,
            "alignment": 5,
            "boundsType": "OBS_BOUNDS_SCALE_INNER",
            "boundsAlignment": 5,
            "boundsWidth": 1920,
            "boundsHeight": 1080
          }
        }
      ]
    },
    {
      "name": "Broadcast 3D",
      "items": [
        {
          "source": "programme",
          "kind": "browser_source",
          "enabled": true,
          "settings": {
            "url": "${producer}/core/web/index.html?feed=live&res=1&adaptive=guard&resmin=1&fpsmin=24&trustedAutoplayAudio=1&mediaBase=${mediabase}",
            "width": 1920,
            "height": 1080,
            "shutdown": false,
            "restart_when_active": false,
            "reroute_audio": true
          },
          "transform": {
            "positionX": 0,
            "positionY": 0,
            "alignment": 5,
            "boundsType": "OBS_BOUNDS_SCALE_INNER",
            "boundsAlignment": 5,
            "boundsWidth": 1920,
            "boundsHeight": 1080
          }
        }
      ]
    },
    {
      "name": "Race cam",
      "items": [
        {
          "source": "programme",
          "kind": "browser_source",
          "enabled": true,
          "_why_one_source": "The already-defined programme source stays active beneath every ViewRegatta camera picture so its single commentary lane remains in the OBS mix.",
          "transform": {
            "positionX": 0,
            "positionY": 0,
            "alignment": 5,
            "boundsType": "OBS_BOUNDS_SCALE_INNER",
            "boundsAlignment": 5,
            "boundsWidth": 1920,
            "boundsHeight": 1080
          }
        },
        {
          "source": "racecam:panel",
          "kind": "browser_source",
          "enabled": true,
          "_why_one_source": "The panel is ONE page rendering a 2x2 grid of slot tiles over ONE feed connection — see the header. Four separate browser sources tiled by OBS is what exhausted the per-origin connection pool and what had to be told the canvas size.",
          "settings": {
            "url": "${producer}/core/web/boatcams.html?slots=4&feed=live&hls=1&readBase=${readbase}",
            "width": 1920,
            "height": 1080,
            "shutdown": false,
            "restart_when_active": false,
            "reroute_audio": false
          },
          "transform": {
            "positionX": 0,
            "positionY": 0,
            "alignment": 5,
            "boundsType": "OBS_BOUNDS_SCALE_INNER",
            "boundsAlignment": 5,
            "boundsWidth": 1920,
            "boundsHeight": 1080
          }
        }
      ]
    },
    {
      "name": "Boat cam A",
      "items": [
        {
          "source": "programme",
          "kind": "browser_source",
          "enabled": true,
          "transform": {
            "positionX": 0,
            "positionY": 0,
            "alignment": 5,
            "boundsType": "OBS_BOUNDS_SCALE_INNER",
            "boundsAlignment": 5,
            "boundsWidth": 1920,
            "boundsHeight": 1080
          }
        },
        {
          "source": "boatcam:slot:A",
          "kind": "browser_source",
          "enabled": true,
          "settings": {
                "url": "${camorigin}/core/web/boatcam.html?slot=A&feed=live&hls=1&readBase=${readbase}",
            "width": 1920,
            "height": 1080,
            "shutdown": false,
            "restart_when_active": false,
            "reroute_audio": false
          },
          "transform": {
            "positionX": 0,
            "positionY": 0,
            "alignment": 5,
            "boundsType": "OBS_BOUNDS_SCALE_INNER",
            "boundsAlignment": 5,
            "boundsWidth": 1920,
            "boundsHeight": 1080
          }
        }
      ]
    },
    {
      "name": "Boat cam B",
      "items": [
        {
          "source": "programme",
          "kind": "browser_source",
          "enabled": true,
          "transform": {
            "positionX": 0,
            "positionY": 0,
            "alignment": 5,
            "boundsType": "OBS_BOUNDS_SCALE_INNER",
            "boundsAlignment": 5,
            "boundsWidth": 1920,
            "boundsHeight": 1080
          }
        },
        {
          "source": "boatcam:slot:B",
          "kind": "browser_source",
          "enabled": true,
          "settings": {
                "url": "${camorigin}/core/web/boatcam.html?slot=B&feed=live&hls=1&readBase=${readbase}",
            "width": 1920,
            "height": 1080,
            "shutdown": false,
            "restart_when_active": false,
            "reroute_audio": false
          },
          "transform": {
            "positionX": 0,
            "positionY": 0,
            "alignment": 5,
            "boundsType": "OBS_BOUNDS_SCALE_INNER",
            "boundsAlignment": 5,
            "boundsWidth": 1920,
            "boundsHeight": 1080
          }
        }
      ]
    },
    {
      "name": "Boat cam C",
      "items": [
        {
          "source": "programme",
          "kind": "browser_source",
          "enabled": true,
          "transform": {
            "positionX": 0,
            "positionY": 0,
            "alignment": 5,
            "boundsType": "OBS_BOUNDS_SCALE_INNER",
            "boundsAlignment": 5,
            "boundsWidth": 1920,
            "boundsHeight": 1080
          }
        },
        {
          "source": "boatcam:slot:C",
          "kind": "browser_source",
          "enabled": true,
          "settings": {
                "url": "${camorigin}/core/web/boatcam.html?slot=C&feed=live&hls=1&readBase=${readbase}",
            "width": 1920,
            "height": 1080,
            "shutdown": false,
            "restart_when_active": false,
            "reroute_audio": false
          },
          "transform": {
            "positionX": 0,
            "positionY": 0,
            "alignment": 5,
            "boundsType": "OBS_BOUNDS_SCALE_INNER",
            "boundsAlignment": 5,
            "boundsWidth": 1920,
            "boundsHeight": 1080
          }
        }
      ]
    },
    {
      "name": "Boat cam D",
      "items": [
        {
          "source": "programme",
          "kind": "browser_source",
          "enabled": true,
          "transform": {
            "positionX": 0,
            "positionY": 0,
            "alignment": 5,
            "boundsType": "OBS_BOUNDS_SCALE_INNER",
            "boundsAlignment": 5,
            "boundsWidth": 1920,
            "boundsHeight": 1080
          }
        },
        {
          "source": "boatcam:slot:D",
          "kind": "browser_source",
          "enabled": true,
          "settings": {
                "url": "${camorigin}/core/web/boatcam.html?slot=D&feed=live&hls=1&readBase=${readbase}",
            "width": 1920,
            "height": 1080,
            "shutdown": false,
            "restart_when_active": false,
            "reroute_audio": false
          },
          "transform": {
            "positionX": 0,
            "positionY": 0,
            "alignment": 5,
            "boundsType": "OBS_BOUNDS_SCALE_INNER",
            "boundsAlignment": 5,
            "boundsWidth": 1920,
            "boundsHeight": 1080
          }
        }
      ]
    },
    {
      "name": "Start vessel",
      "items": [
        {
          "source": "programme",
          "kind": "browser_source",
          "enabled": true,
          "transform": {
            "positionX": 0,
            "positionY": 0,
            "alignment": 5,
            "boundsType": "OBS_BOUNDS_SCALE_INNER",
            "boundsAlignment": 5,
            "boundsWidth": 1920,
            "boundsHeight": 1080
          }
        },
        {
          "source": "boatcam:vessel:start",
          "kind": "browser_source",
          "enabled": true,
          "settings": {
                "url": "${camorigin}/core/web/boatcam.html?vessel=start&feed=live&hls=1&readBase=${readbase}",
            "width": 1920,
            "height": 1080,
            "shutdown": false,
            "restart_when_active": false,
            "reroute_audio": false
          },
          "transform": {
            "positionX": 0,
            "positionY": 0,
            "alignment": 5,
            "boundsType": "OBS_BOUNDS_SCALE_INNER",
            "boundsAlignment": 5,
            "boundsWidth": 1920,
            "boundsHeight": 1080
          }
        }
      ]
    },
    {
      "name": "Finish vessel",
      "items": [
        {
          "source": "programme",
          "kind": "browser_source",
          "enabled": true,
          "transform": {
            "positionX": 0,
            "positionY": 0,
            "alignment": 5,
            "boundsType": "OBS_BOUNDS_SCALE_INNER",
            "boundsAlignment": 5,
            "boundsWidth": 1920,
            "boundsHeight": 1080
          }
        },
        {
          "source": "boatcam:vessel:finish",
          "kind": "browser_source",
          "enabled": true,
          "settings": {
                "url": "${camorigin}/core/web/boatcam.html?vessel=finish&feed=live&hls=1&readBase=${readbase}",
            "width": 1920,
            "height": 1080,
            "shutdown": false,
            "restart_when_active": false,
            "reroute_audio": false
          },
          "transform": {
            "positionX": 0,
            "positionY": 0,
            "alignment": 5,
            "boundsType": "OBS_BOUNDS_SCALE_INNER",
            "boundsAlignment": 5,
            "boundsWidth": 1920,
            "boundsHeight": 1080
          }
        }
      ]
    },
    {
      "name": "Break line-up",
      "_why": [
        "The fleet roll-call as its own cuttable scene: one team at a time — flag, team name, sail",
        "numbers, hull colours, live video from one of that team's boats — off the same feed every",
        "other scene here reads, advancing on its own and looping. It is the pause picture that needs",
        "no material and cannot run out, and it was finished code sitting in NO scene file at all,",
        "which is the between-races gap this collection had.",
        "",
        "IT DEFINES `break:bed`, AND `Break roll` BORROWS IT. That order is load-bearing: a scene item",
        "is a placement, and a source's settings live on its FIRST appearance in this file. Listing",
        "the borrowing scene first would leave the definition unable to claim the name."
      ],
      "items": [
        {
          "source": "break:bed",
          "kind": "browser_source",
          "enabled": true,
          "settings": {
            "url": "${producer}/core/web/lineup.html?feed=live&dwell=8",
            "width": 1920,
            "height": 1080,
            "shutdown": false,
            "restart_when_active": false,
            "reroute_audio": false
          },
          "transform": {
            "positionX": 0,
            "positionY": 0,
            "alignment": 5,
            "boundsType": "OBS_BOUNDS_SCALE_INNER",
            "boundsAlignment": 5,
            "boundsWidth": 1920,
            "boundsHeight": 1080
          }
        }
      ]
    },
    {
      "name": "Break roll",
      "_why": [
        "Video our own people filmed on their phones, every clip approved by a human before it ever",
        "reached this machine — the approval is what FETCHES it, so an undecided clip is not sitting",
        "in this folder waiting to be cut to by mistake.",
        "",
        "`break:bed` is the SAME source `Break line-up` defines, listed here with a transform and no",
        "settings — the borrowing idiom the video wall already uses, and one page rather than two.",
        "It sits UNDER the roll (item order is bottom-first) so that `clear_on_media_end` lands the",
        "scene on the roll-call when the clips run out, and covers an empty folder and a wrong",
        "`--var breakdir=` with the same picture.",
        "",
        "`looping` is FALSE on purpose: a break roll that repeats says the day has stopped."
      ],
      "items": [
        {
          "source": "break:bed",
          "enabled": true,
          "transform": {
            "positionX": 0,
            "positionY": 0,
            "alignment": 5,
            "boundsType": "OBS_BOUNDS_SCALE_INNER",
            "boundsAlignment": 5,
            "boundsWidth": 1920,
            "boundsHeight": 1080
          }
        },
        {
          "source": "break:roll",
          "kind": "ffmpeg_source",
          "enabled": true,
          "settings": {
            "local_file": "${breakdir}/break_roll.mp4",
            "is_local_file": true,
            "looping": false,
            "restart_on_activate": true,
            "close_when_inactive": true,
            "clear_on_media_end": true,
            "hw_decode": true
          },
          "transform": {
            "positionX": 0,
            "positionY": 0,
            "alignment": 5,
            "boundsType": "OBS_BOUNDS_SCALE_INNER",
            "boundsAlignment": 5,
            "boundsWidth": 1920,
            "boundsHeight": 1080
          }
        }
      ]
    },
    {
      "name": "Replay",
      "_why": [
        "Forty seconds of the programme as it went out, cut straight back out of the recording OBS is",
        "already making (`day_cutter.py`). A stream copy, so it IS stream quality — no second encoder,",
        "no GPU, and nothing re-rendered. Taken by `replay_take.py` on this machine.",
        "",
        "`replay:bed` is the SAME `programme` source Broadcast 3D defines, listed here with a transform",
        "and no settings — the borrowing idiom the video wall and the break roll already use. It sits",
        "UNDER the clip (item order is bottom-first) so `clear_on_media_end` lands the scene on the live",
        "picture when the clip runs out, and covers an absent file and a wrong `--var replaydir=` with",
        "the same picture rather than with black.",
        "",
        "THE AUDIO IS THE CLIP'S OWN, and that is why this scene is NOT in `audio_continuity`. A replay",
        "carries the commentary from the moment it is a replay OF — the voice that described the",
        "rounding arrives with the picture of it, for free, because a stream copy takes the audio track",
        "with it. But OBS mutes an INPUT, not a scene item, so the programme underneath keeps talking",
        "unless something silences it: `replay_take` mutes `programme` for the take and restores it in a",
        "`finally`, and unmutes unconditionally on its own next start-up. Two commentators over one",
        "picture is the failure that pair exists to prevent; a broadcast left silent is the failure the",
        "start-up unmute exists to end.",
        "",
        "`looping` is FALSE for the same reason the break roll's is: a replay that repeats says the day",
        "has stopped."
      ],
      "items": [
        {
          "source": "programme",
          "enabled": true,
          "transform": {
            "positionX": 0,
            "positionY": 0,
            "alignment": 5,
            "boundsType": "OBS_BOUNDS_SCALE_INNER",
            "boundsAlignment": 5,
            "boundsWidth": 1920,
            "boundsHeight": 1080
          }
        },
        {
          "source": "replay:clip",
          "kind": "ffmpeg_source",
          "enabled": true,
          "settings": {
            "local_file": "${replaydir}/replay.mp4",
            "is_local_file": true,
            "looping": false,
            "restart_on_activate": true,
            "close_when_inactive": true,
            "clear_on_media_end": true,
            "hw_decode": true
          },
          "transform": {
            "positionX": 0,
            "positionY": 0,
            "alignment": 5,
            "boundsType": "OBS_BOUNDS_SCALE_INNER",
            "boundsAlignment": 5,
            "boundsWidth": 1920,
            "boundsHeight": 1080
          }
        }
      ]
    },
    {
      "name": "Standings",
      "_why": [
        "The series table between races — see the header. `programme` is the bottom item and carries",
        "the commentary through the cut; the card covers its picture and routes no audio of its own.",
        "",
        "The page holds NO feed connection: its table, the event's name and the venue zone its stamp",
        "is written in all arrive in one JSON from this same producer. Six pages on one origin is the",
        "budget the boat cameras already spend, and a surface that can be complete without a seventh",
        "connection does not take one.",
        "",
        "`restart_when_active` is FALSE like every other page here. The card re-reads the table on its",
        "own timer, so a reload would buy nothing and cost a white flash on every cut to it."
      ],
      "items": [
        {
          "source": "programme",
          "kind": "browser_source",
          "enabled": true,
          "transform": {
            "positionX": 0,
            "positionY": 0,
            "alignment": 5,
            "boundsType": "OBS_BOUNDS_SCALE_INNER",
            "boundsAlignment": 5,
            "boundsWidth": 1920,
            "boundsHeight": 1080
          }
        },
        {
          "source": "results:standings",
          "kind": "browser_source",
          "enabled": true,
          "settings": {
            "url": "${standingsbase}/core/web/standings.html",
            "width": 1920,
            "height": 1080,
            "shutdown": false,
            "restart_when_active": false,
            "reroute_audio": false
          },
          "transform": {
            "positionX": 0,
            "positionY": 0,
            "alignment": 5,
            "boundsType": "OBS_BOUNDS_SCALE_INNER",
            "boundsAlignment": 5,
            "boundsWidth": 1920,
            "boundsHeight": 1080
          }
        }
      ]
    },
    {
      "name": "Video wall",
      "_grid": [
        "The same 3x3 the panels crop, at a third the size. Cells stay named row-then-column as the",
        "operator names them, so cell RC sits at x=(C-1)*640, y=(R-1)*360 — the wall's own arithmetic",
        "divided by three, and the reason a cell can be checked against `obs/video_wall.json` by eye.",
        "",
        "  +--------+--------+--------+",
        "  |  11    |  12    | 13 cam A|",
        "  +  race, 1280x720 +--------+",
        "  |  21    |  22    | 23 cam B|",
        "  +--------+--------+--------+",
        "  | 31 res|ent | 32 cam D| 33 cam C|",
        "  +--------+--------+--------+"
      ],
      "items": [
        {
          "source": "programme",
          "kind": "browser_source",
          "enabled": true,
          "_cell": "11 12 21 22 — BORROWED from `Broadcast 3D`, no settings of its own: the produced picture is the produced picture, and a second copy would be a second feed connection for an identical page.",
          "transform": {
            "positionX": 0,
            "positionY": 0,
            "alignment": 5,
            "boundsType": "OBS_BOUNDS_SCALE_INNER",
            "boundsAlignment": 5,
            "boundsWidth": 1280,
            "boundsHeight": 720
          }
        },
        {
          "source": "boatcam:slot:A",
          "kind": "browser_source",
          "enabled": true,
          "_cell": "13 — borrowed from `Boat cam A`; the slot follows the fleet, so this cell never needs re-pointing either",
          "transform": {
            "positionX": 1280,
            "positionY": 0,
            "alignment": 5,
            "boundsType": "OBS_BOUNDS_SCALE_INNER",
            "boundsAlignment": 5,
            "boundsWidth": 640,
            "boundsHeight": 360
          }
        },
        {
          "source": "boatcam:slot:B",
          "kind": "browser_source",
          "enabled": true,
          "_cell": "23 — borrowed from `Boat cam B`",
          "transform": {
            "positionX": 1280,
            "positionY": 360,
            "alignment": 5,
            "boundsType": "OBS_BOUNDS_SCALE_INNER",
            "boundsAlignment": 5,
            "boundsWidth": 640,
            "boundsHeight": 360
          }
        },
        {
          "source": "boatcam:slot:C",
          "kind": "browser_source",
          "enabled": true,
          "_cell": "33 — borrowed from `Boat cam C`",
          "transform": {
            "positionX": 1280,
            "positionY": 720,
            "alignment": 5,
            "boundsType": "OBS_BOUNDS_SCALE_INNER",
            "boundsAlignment": 5,
            "boundsWidth": 640,
            "boundsHeight": 360
          }
        },
        {
          "source": "boatcam:slot:D",
          "kind": "browser_source",
          "enabled": true,
          "_cell": "32 — borrowed from `Boat cam D`",
          "transform": {
            "positionX": 640,
            "positionY": 720,
            "alignment": 5,
            "boundsType": "OBS_BOUNDS_SCALE_INNER",
            "boundsAlignment": 5,
            "boundsWidth": 640,
            "boundsHeight": 360
          }
        },
        {
          "source": "panel:results",
          "kind": "browser_source",
          "enabled": true,
          "_cell": "31 left half — recent results, beside the entry list rather than above it: both are tall lists, so splitting the cell down the middle gives each its full height instead of halving it",
          "_legibility": "Rendered at 480x540 — a sane desktop viewport, the same one the 5760 wall uses — and shown at 320x360. See the header: the text cell cannot win at a third scale, and pretending otherwise with a smaller viewport would only break the page's layout as well.",
          "settings": {
            "url": "${burgee}/e/${event}/results",
            "width": 480,
            "height": 540,
            "shutdown": false,
            "restart_when_active": false,
            "reroute_audio": false
          },
          "transform": {
            "positionX": 0,
            "positionY": 720,
            "alignment": 5,
            "boundsType": "OBS_BOUNDS_SCALE_INNER",
            "boundsAlignment": 5,
            "boundsWidth": 320,
            "boundsHeight": 360
          }
        },
        {
          "source": "panel:entries",
          "kind": "browser_source",
          "enabled": true,
          "_cell": "31 right half — the entry list",
          "settings": {
            "url": "${burgee}/e/${event}/entries",
            "width": 480,
            "height": 540,
            "shutdown": false,
            "restart_when_active": false,
            "reroute_audio": false
          },
          "transform": {
            "positionX": 320,
            "positionY": 720,
            "alignment": 5,
            "boundsType": "OBS_BOUNDS_SCALE_INNER",
            "boundsAlignment": 5,
            "boundsWidth": 320,
            "boundsHeight": 360
          }
        }
      ]
    }
  ]
}
