diff options
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]) |