Phase 12: frame-accurate timecode via an overscan band — vgstrip encoder/decoder, sweep writes tmcd + creation_time + sidecar, dumpd /time, Caddy /time routes, MediaMTX microsecond names, /dock/clock/ page, dock band setup via obs-websocket, Go Live band composite + toggle, portal crops the band

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-10-31 16:07:56 -05:00
parent 89841c3d2a
commit 3fcb8fbe99
18 changed files with 515 additions and 25 deletions

View File

@@ -293,9 +293,32 @@ the expiry, and that the path starts with that user's name. MediaMTX hot-reloads
but **not** the auth section: after changing auth, `docker compose restart mediamtx` between streams.
`configure.py` reloads Caddy in place and restarts only copyparty.
## Timecode for multicam editing (frame accurate)
Every published stream carries a QuickTime **timecode track** (`tmcd`, time of day in Eastern, at the file's
frame rate) and a UTC `creation_time`, written by the sweep during the lossless remux, plus a sidecar
`<Title>__<ts>.json` with the exact capture time (the Jellyfin plugin reads it). Resolve and Premiere read the
track natively: drop clips from several streamers on a multicam timeline, sync by timecode.
Where the time comes from, best first:
1. **Overscan band** — a thin strip of extra rows below the picture carrying the *server* clock, encoded
(black/white cells, format in `bin/vgstrip.py`) plus a small readable clock. The server decodes it
(`bin/vgstrip.py`) at the start, middle and end of each recording → the timecode is frame-accurate at the
start and the sidecar records drift over the file. Game image untouched. OBS: dock → *Timecode band* →
"Add band + clock source" (canvas grows by W/160 rows, a locked browser source `Vanguard Clock` →
`/dock/clock/` sits in the band; only while not streaming). Go Live: the "Timecode band" checkbox (default on).
2. **Server arrival time** (no band) — microseconds in the recording filename. Within ~0.1–0.3 s per streamer
(encoder + network delay); coarse but always available.
Viewing: the live portal crops the band; Jellyfin shows it as a thin strip at the bottom (VHS-style);
**editors export at the game resolution (e.g. 2560×1440) which drops the band** — finals carry nothing.
Cross-check when it matters: everyone is in the same voice chat, so audio-waveform sync is sample-accurate.
Why continuous and not a start burst: a PC clock drifts vs the server (10 ppm ≈ 4 frames over 2 h).
## Layout on the server
/srv/media/streams/<user>/<Title>/.live/<ts>.mp4 recording in progress (hidden from Jellyfin/copyparty)
/srv/media/streams/<user>/<Title>__<ts>.json capture time (UTC ms), timecode, source (strip|arrival), drift
/srv/media/streams/<user>/<Title>__<ts>.mp4 finished stream, one flat MP4 per recording, directly in
the streamer's folder (several files inside one title
folder would be ONE Jellyfin episode with "versions")
@@ -317,7 +340,7 @@ upscaled; aspect kept).
| input | action |
|-------|--------|
| already compliant | nothing |
| MediaMTX recording in `<Title>/.live/` (fragmented MP4) | once idle 3 min: lossless remux to `<user>/<Title>__<ts>.mp4`, `.live` source and empty title folder removed |
| MediaMTX recording in `<Title>/.live/` (fragmented MP4) | once idle 3 min: decode the timecode band (or use arrival time), lossless remux to `<user>/<Title>__<ts>.mp4` with a `tmcd` track + `creation_time`, write the `.json` sidecar, remove `.live` and the empty title folder |
| in a Syncthing folder (`.stfolder` marker) | compliant / index-only: untouched; otherwise convert → `name.mp4`, add raw to `.stlocal`, delete raw (one copy) |
| HEVC / 10-bit / VFR / MKV / >1440p / >60fps / non-AAC | full-res re-encode (x264 veryfast CRF 18) → `name.mp4`; source kept in `.src/` |
| ffmpeg fails or duration mismatch | `name.normalize-failed.log` written, file skipped until that log is deleted |