diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-17 15:50:55 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-17 15:50:55 +0200 |
| commit | 3dcce17f9c616c8528a93bc625eb462d7fffb8f9 (patch) | |
| tree | 3fcd64d9e0b5bef9e917052f8269907325dbc350 /docs/MESHBAY_NODE_PROTOCOL.md | |
| parent | d472725167c1d335996059d24b7b74f3730768fb (diff) | |
| download | meshbay-3dcce17f9c616c8528a93bc625eb462d7fffb8f9.tar.gz | |
fix(node): a seek reports where the picture begins, not where it was asked
Copied video can only start on a keyframe, so `-ss t` with `-c:v copy`
delivers the keyframe at or before `t`. The node reported `t` anyway and the
client builds `SourceBuffer.timestampOffset` out of that number, so everything
downstream believed the picture stood a few seconds further along than it did.
That was a wrong label while only the scrubber read it — it is recorded as
such, low priority, in the design document. Subtitles made it a wrong answer:
their cues carry the source's own absolute timestamps, so the mismatch put
every line on screen before it was spoken. Reported from real use on an H264
title, where seeking to 600 s, 2650 s and 5000 s lands on keyframes 0.82 s,
1.56 s and 4.64 s earlier.
The copy path now resolves the request to that keyframe, seeks to it, and
reports it. The bytes delivered do not change — ffmpeg lands on the same frame
either way — only the number that names them. The look-up reads the thirty
seconds before the request and cost 0.12–0.51 s on that title, which is the
price already costed in §15.3 and never paid. Re-encoded video is untouched:
it can begin exactly where it is asked to, and does.
Two details worth their lines. The keyframe is passed back to ffmpeg at six
decimals, because rounding a keyframe's own timestamp down puts it before the
frame it names and selects the previous one — the same fault again, smaller.
And the ffprobe window ends past the request, since an interval whose end is
the request never emits a keyframe sitting exactly on it: the resume position
is the one place a viewer asks for the same instant twice, and it would have
been answered a whole GOP early.
The test decodes the first frame served and matches it against the source
frame at the position announced, rather than comparing `start` to an expected
number — both sides of that comparison would be reading the same ffprobe and
would agree by construction.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UGY17EPph5LsLzePPXhUVc
Diffstat (limited to 'docs/MESHBAY_NODE_PROTOCOL.md')
| -rw-r--r-- | docs/MESHBAY_NODE_PROTOCOL.md | 2 |
1 files changed, 1 insertions, 1 deletions
diff --git a/docs/MESHBAY_NODE_PROTOCOL.md b/docs/MESHBAY_NODE_PROTOCOL.md index 64f32e3..2aede26 100644 --- a/docs/MESHBAY_NODE_PROTOCOL.md +++ b/docs/MESHBAY_NODE_PROTOCOL.md @@ -1671,7 +1671,7 @@ array while MediaSource consumes it a segment at a time. | Concurrent transcodes | 8 node-wide, semaphore on the transport context | | Seeking | a new `stream_req` with `start`; the previous stream is retired first, ffmpeg respawned with `-ss` | | Accurate seek | **off when the video is copied, on when it is re-encoded.** Copied video has to begin on a keyframe and cannot be trimmed to the request; re-encoded audio can, and is. Leaving both at the default put a whole GOP of silence at the head of every seek and left sound and picture a GOP apart — with correct timestamps throughout, so nothing downstream could detect it | -| `start` in `stream_init` | the value actually used — seeking lands on the keyframe at or before the request, and the client adds it back as `SourceBuffer.timestampOffset` | +| `start` in `stream_init` | the value actually used, and on the copy path that is the **keyframe at or before the request**, resolved by a bounded look-up before ffmpeg is spawned (0.12–0.51 s, measured). The client adds it back as `SourceBuffer.timestampOffset`; a subtitle cue carries the source's absolute time, so reporting the request instead would put every line on screen one GOP before it is spoken | | `audio_tracks` in `stream_init` | every audio track: `i` (the **audio ordinal**, what `-map 0:a:<n>` takes, never the container stream index), `lang`, `title`, `codec`, `ch`. Empty for a file with no audio | | `audio_track` in `stream_req` | which ordinal to map. Absent, out of range or malformed is the first track | | `audio_track` in `stream_init` | the ordinal actually used, for the same reason `start` is reported: a list drawn before the file was replaced on disk can name a track that is no longer there, and the client must show what is playing rather than what it asked for. `null` when the file has no audio | |