diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-17 15:06:57 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-17 15:06:57 +0200 |
| commit | d472725167c1d335996059d24b7b74f3730768fb (patch) | |
| tree | 1ffca323c9dfe682c8fff2a20da1cfae0106f830 /packages/meshbay-node | |
| parent | 30980ba0994d3e99d41c5252555320d6cbb1f5a9 (diff) | |
| download | meshbay-d472725167c1d335996059d24b7b74f3730768fb.tar.gz | |
feat: tell a forced subtitle track from a full one
Reported as "I click a subtitle and nothing appears", on three films. Nothing
was broken. The track selected was the container's forced track, which carries
signage and foreign dialogue only: measured on the film in question, 30 cues
and 77 seconds of text across 2h32 — 0.8% of the running time, against 1559
cues and 41.8% for the full track sitting beside it under the same language
tag. At all three positions tested there was genuinely no cue to show; the
full track would have shown one at two of them.
So the defect is that the menu could not say which was which. The label used
the container's title tag, which said "Forced" on that film and says nothing
at all on most, and no other field was carried. The disposition is the half
that is always there: `probe_video` now reads `forced` and `hearing_impaired`,
`stream_init` carries them, and the label states them in the reader's own
language rather than repeating an English word a muxer happened to type.
The node fixture grows a forced track with no title, because a title would
let the old code pass. The label harness's `t` stub took a parameters object
unconditionally and threw on a key that has none — a fixture narrower than
production, fixed here rather than worked around.
Also removes the activeCues probe that found this. It answered its question:
mode showing, cues 30, active 0, none due at that instant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UGY17EPph5LsLzePPXhUVc
Diffstat (limited to 'packages/meshbay-node')
3 files changed, 51 insertions, 0 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/media_probe.py b/packages/meshbay-node/src/meshbay_node/media_probe.py index 06b1e28..81174c8 100644 --- a/packages/meshbay-node/src/meshbay_node/media_probe.py +++ b/packages/meshbay-node/src/meshbay_node/media_probe.py @@ -69,6 +69,16 @@ class SubtitleTrack: language: str | None title: str | None codec_name: str | None + # **A forced track is not a shorter version of the full one.** It carries + # only signage and the lines spoken in another language — measured on a + # real film: 30 cues and 77 seconds of text across 2h32, against 1559 cues + # and 41% of the running time for the full track beside it. Picking it and + # seeing nothing for ten minutes is the ordinary outcome, not a fault, and + # nothing in the menu let a viewer tell those two apart. The disposition is + # what says which it is; the title tag that would also say it ("Forced", + # "SDH") is absent as often as it is present. + forced: bool = False + hearing_impaired: bool = False @dataclass @@ -143,6 +153,7 @@ async def probe_video(path: str) -> VideoProbe: "-show_entries", "stream=codec_name,profile,level,codec_type,width,height,channels", "-show_entries", "stream_tags=language,title", + "-show_entries", "stream_disposition=forced,hearing_impaired", "-show_entries", "format=duration", "-of", "json", path, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, @@ -192,11 +203,14 @@ async def probe_video(path: str) -> VideoProbe: if codec_name not in TEXT_SUBTITLE_CODECS: continue tags = s.get("tags") or {} + disp = s.get("disposition") or {} subtitle_tracks.append(SubtitleTrack( ordinal=ordinal, language=(tags.get("language") or "").strip() or None, title=(tags.get("title") or "").strip() or None, codec_name=codec_name, + forced=bool(disp.get("forced")), + hearing_impaired=bool(disp.get("hearing_impaired")), )) has_audio = bool(audio_tracks) diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py index 1e85f29..2863679 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py +++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py @@ -6267,6 +6267,12 @@ class WebRTCPeerSession: "lang": tr.language, "title": tr.title, "codec": tr.codec_name, + # What tells a full translation from signage-only. Without + # it the two are the same menu entry, and picking the + # forced one shows nothing for minutes at a time — which + # reads as a broken feature and was reported as one. + "forced": tr.forced, + "sdh": tr.hearing_impaired, } for tr in probe.subtitle_tracks ], diff --git a/packages/meshbay-node/tests/test_stream_subtitle_tracks.py b/packages/meshbay-node/tests/test_stream_subtitle_tracks.py index cafb2e3..9a09edb 100644 --- a/packages/meshbay-node/tests/test_stream_subtitle_tracks.py +++ b/packages/meshbay-node/tests/test_stream_subtitle_tracks.py @@ -95,6 +95,9 @@ def _make_subtitled_clip(path: Path) -> None: "-metadata:s:s:0", "language=fre", "-metadata:s:s:1", "language=fre", "-metadata:s:s:2", "language=eng", + # Ordinal 1 is the forced one, and carries no title saying so — which + # is the case the disposition exists for. + "-disposition:s:1", "forced", str(path)], check=True, capture_output=True) @@ -209,6 +212,31 @@ def test_bitmap_codecs_are_not_offered(): @pytest.mark.asyncio +async def test_a_forced_track_says_so_without_needing_a_title(tmp_path): + """The distinction a viewer cannot make for themselves. + + A forced track carries signage and foreign dialogue only — on a real film, + 30 cues and 77 seconds of text across 2h32, against 1559 cues and 41% of + the running time for the full track beside it. Selecting it and seeing + nothing for ten minutes is its normal behaviour, and was reported as a + broken feature. The container's title tag would say it too, when it is + there; the disposition is the half that is always there. + """ + clip = tmp_path / "clip.mp4" + _make_subtitled_clip(clip) + + probe = await _probe_video(str(clip)) + by_ordinal = {tr.ordinal: tr for tr in probe.subtitle_tracks} + + assert by_ordinal[1].forced is True + assert by_ordinal[2].forced is False + assert by_ordinal[1].title is None, ( + "the fixture must carry no title on the forced track, or it does not " + "exercise the case the disposition is for") + assert all(not tr.hearing_impaired for tr in probe.subtitle_tracks) + + +@pytest.mark.asyncio async def test_stream_init_announces_the_tracks(tmp_path): """How a client discovers this node can do subtitles at all. @@ -226,6 +254,9 @@ async def test_stream_init_announces_the_tracks(tmp_path): init = next(m for m in session.sent if m.get("type") == "stream_init") assert [tr["i"] for tr in init["subtitle_tracks"]] == [1, 2] assert [tr["lang"] for tr in init["subtitle_tracks"]] == ["fre", "eng"] + assert [tr["forced"] for tr in init["subtitle_tracks"]] == [True, False], ( + "the client cannot mark a forced track it was never told about") + assert [tr["sdh"] for tr in init["subtitle_tracks"]] == [False, False] @pytest.mark.parametrize("track", [1, 2]) |