diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-17 16:18:30 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-17 16:18:30 +0200 |
| commit | 0fed6786fe304195df66776787c5b5b82d321bcf (patch) | |
| tree | fdce22ba79b830efe9a2802958aa9b8e7131ca23 /packages/meshbay-node/tests | |
| parent | 289a00afa33c6b7d2f77e46cdae56a593b21cf9a (diff) | |
| download | meshbay-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 'packages/meshbay-node/tests')
| -rw-r--r-- | packages/meshbay-node/tests/test_stream_seek_reports_the_keyframe.py | 51 |
1 files changed, 36 insertions, 15 deletions
diff --git a/packages/meshbay-node/tests/test_stream_seek_reports_the_keyframe.py b/packages/meshbay-node/tests/test_stream_seek_reports_the_keyframe.py index acd2ebb..9aea2b4 100644 --- a/packages/meshbay-node/tests/test_stream_seek_reports_the_keyframe.py +++ b/packages/meshbay-node/tests/test_stream_seek_reports_the_keyframe.py @@ -9,16 +9,24 @@ further along than it did. That was a wrong label while only the scrubber read it. It became a wrong *answer* when subtitles arrived: their cues carry the source's own absolute -timestamps, so the mismatch put every line on screen a GOP before it was -spoken. Measured on a real H264 title, seeking to 600 s, 2650 s and 5000 s -landed on keyframes 0.82 s, 1.56 s and 4.64 s earlier. +timestamps, so the mismatch put every line on screen before it was spoken. -**The assertion is on the decoded picture, not on the number.** A test that -only compared `stream_init["start"]` against an expected keyframe would agree -with the implementation by construction — both would be reading the same -ffprobe. The first frame delivered is decoded and matched against the source -frame at the position the node claims, which is a statement about what was -served rather than about what was computed. +**Where the seek lands is measured, not predicted, and that distinction is +this module's subject.** The first attempt scanned ffprobe's key frames and +took the last one at or before the request. It was wrong twice: Matroska's +Cues index only some keyframes, so the 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. On a real title, one seek answered 4909.863 s from the frame list and +delivered 4907.236 s — 2.65 s of subtitles standing away from the voice, which +is what a viewer reported after the first fix. + +**The assertion is therefore on the decoded picture, not on the number.** A +test comparing `stream_init["start"]` against an expected keyframe would agree +with the implementation by construction, both reading the same probe. The +first frame delivered is decoded and matched against the source frame at the +position the node claims, which is a statement about what was served rather +than about what was computed. """ import shutil @@ -33,7 +41,7 @@ from meshbay_common.webcrypto import chunk_key_aes, decrypt_chunk_aes from meshbay_node.indexer.group_index import GroupIndex from meshbay_node.transport.webrtc_server import ( WebRTCPeerSession, - _keyframe_at_or_before, + _seek_lands_at, ) from conftest import needs_subprocess, one_root @@ -121,17 +129,30 @@ def _frame_md5(path: Path, at: float | None = None) -> str: return hashlib.md5(proc.stdout).hexdigest() +_MAPS = ["-map", "0:v:0", "-map", "0:a:0"] + + @pytest.mark.asyncio -async def test_the_lookup_finds_the_keyframe_the_seek_will_land_on(tmp_path): +async def test_the_probe_measures_where_the_seek_lands(tmp_path): + """Measured with the mapping the stream will use, never predicted. + + A scan of ffprobe's key frames answers a different question: Matroska + indexes only some keyframes, and the landing point also moves with which + audio track is mapped, because the container is positioned where every + mapped stream has data. On a real title those two answers were 4909.863 + and 4907.236 — 2.65 s of subtitles standing away from the voice. + """ clip = tmp_path / "clip.mp4" _make_h264_clip(clip) - assert await _keyframe_at_or_before(clip, _SEEK_TO) == pytest.approx( + assert await _seek_lands_at(clip, _SEEK_TO, _MAPS) == pytest.approx( _EXPECTED_KEYFRAME, abs=0.05) # A position that *is* a keyframe answers itself, not the one before. - assert await _keyframe_at_or_before(clip, 20.0) == pytest.approx(20.0, abs=0.05) - # Before the first one there is nothing earlier to find. - assert await _keyframe_at_or_before(clip, 0) == 0.0 + assert await _seek_lands_at(clip, 20.0, _MAPS) == pytest.approx(20.0, abs=0.05) + # A seek never lands after what was asked for. + for t in (7.0, 15.0, 23.0): + landed = await _seek_lands_at(clip, t, _MAPS) + assert landed is not None and landed <= t + 0.05, (t, landed) @pytest.mark.asyncio |