summaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-17 16:18:30 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-17 16:18:30 +0200
commit0fed6786fe304195df66776787c5b5b82d321bcf (patch)
treefdce22ba79b830efe9a2802958aa9b8e7131ca23 /docs
parent289a00afa33c6b7d2f77e46cdae56a593b21cf9a (diff)
downloadmeshbay-0fed6786fe304195df66776787c5b5b82d321bcf.tar.gz
fix(node): measure where a seek lands instead of predicting it
The previous fix read ffprobe's key frames and took the last one at or before the request. It was wrong twice, and a viewer felt the difference: subtitles went from 5 s early to 2–3 s late. Matroska's Cues index only some keyframes, so an index seek backs off to an indexed one that the frame list does not single out. And the landing point moves with **which streams are mapped**, because the container is positioned where every mapped stream has data — on the reported title, a seek to 4913.7 s landed at 4909.863 with video alone and at 4907.236 with the second audio track mapped beside it. The frame scan gave the first number; the stream delivered the second; the gap was 2.65 s, and the measured audio displacement in the served stream was 2.65 s. So the node asks ffmpeg instead: the same seek, the same mapping, one copied frame under `-copyts`, and the answer read back off the result. 0.06–0.07 s, cheaper than the scan it replaces. The `-ss` argument stays at the request, so the bytes served are exactly the ones served before — only the number naming them changes. The probe runs after the audio track is resolved, because it cannot be right before that is known. An answer after the request, or further before it than any real keyframe gap, is discarded in favour of the old label: a number wrong by seconds beats a fabricated one. Found by decoding the served stream and locating its first frame in the source, which put it at 4907.213 s against an announced 4909.863 s. The test does the same thing rather than comparing the announced number against a second reading of the same probe. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UGY17EPph5LsLzePPXhUVc
Diffstat (limited to 'docs')
-rw-r--r--docs/MESHBAY_DESIGN.md21
-rw-r--r--docs/MESHBAY_NODE_PROTOCOL.md2
2 files changed, 14 insertions, 9 deletions
diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md
index 7ce1168..8f91a87 100644
--- a/docs/MESHBAY_DESIGN.md
+++ b/docs/MESHBAY_DESIGN.md
@@ -2052,14 +2052,19 @@ MSE string) fed to a source buffer, with the node holding one slot per viewer.
from the end and echoed back; the client supplies the timestamp offset, because
copying timestamps does not preserve position.
- **`stream_init.start` names where the picture begins, never where the viewer
- dragged.** Copied video can only begin on a keyframe, so on that path the node
- resolves the request to the keyframe at or before it, seeks to that, and reports
- that. The client builds `SourceBuffer.timestampOffset` out of this number, and a
- subtitle cue carries the source's own absolute time: one GOP of disagreement
- between the two puts every line on screen before it is spoken. The look-up is a
- bounded read of the thirty seconds before the request, measured at 0.12–0.51 s
- on a real title, and only the copy path needs it — re-encoded video begins
- exactly where it is asked to.
+ dragged**, and on the copy path that position is **measured, never predicted**.
+ The client builds `SourceBuffer.timestampOffset` out of this number and a
+ subtitle cue carries the source's own absolute time, so every second of
+ disagreement puts a line on screen a second away from the voice saying it.
+ Reading the key frames and taking the last one at or before the request answers
+ a different question twice over: Matroska's Cues index only some keyframes, so
+ an index seek backs off to an indexed one that can be much earlier, and the
+ landing point moves with **which streams are mapped**, because the container is
+ positioned where every mapped stream has data — one real seek answered 4909.863 s
+ from the frame list and delivered 4907.236 s. So the node runs the same seek with
+ the same mapping, copies one frame under `-copyts`, and reads the answer back:
+ 0.06–0.07 s, cheaper than the scan it replaced. Only the copy path needs it —
+ re-encoded video begins exactly where it is asked to.
- **A seek trims what it can, and what it can differs per stream — which is a
desync, not an inconvenience.** Accurate seeking cannot trim copied video, which
must begin on a keyframe, but it does trim re-encoded audio to the exact request.
diff --git a/docs/MESHBAY_NODE_PROTOCOL.md b/docs/MESHBAY_NODE_PROTOCOL.md
index 2aede26..a53d494 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, 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 |
+| `start` in `stream_init` | the value actually used. On the copy path it is **measured** before the stream is served — the same seek with the same stream mapping, one frame copied under `-copyts`, 0.06–0.07 s — because an index seek lands on an indexed keyframe that the frame list cannot predict and that moves with which audio track is mapped. The client adds it back as `SourceBuffer.timestampOffset`; a subtitle cue carries the source's absolute time, so reporting anything else puts every line on screen away from the voice |
| `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 |