summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/de.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/en.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/es.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/it.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/video-player.js37
-rw-r--r--packages/meshbay-hub/tests/test_video_subtitles.py37
-rw-r--r--packages/meshbay-node/src/meshbay_node/media_probe.py14
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py6
-rw-r--r--packages/meshbay-node/tests/test_stream_subtitle_tracks.py31
15 files changed, 116 insertions, 29 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
index dd53bcf..9d9319d 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
@@ -254,6 +254,8 @@ export default {
'video.subtitles': 'Untertitel',
'video.subtitles_off': 'Aus',
'video.subtitle_track_n': 'Spur {n}',
+ 'video.subtitles_forced': 'erzwungen (nur Schrifttafeln)',
+ 'video.subtitles_sdh': 'für Hörgeschädigte',
'video.err_subtitle': 'Diese Untertitelspur konnte nicht geladen werden',
'video.close': 'Schließen (Esc)',
'preview.pdf_fallback': 'Dieser Browser zeigt das PDF nicht direkt an. Laden Sie es '
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
index e9f3d8d..a928a52 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
@@ -253,6 +253,8 @@ export default {
'video.subtitles': 'Subtitles',
'video.subtitles_off': 'Off',
'video.subtitle_track_n': 'Track {n}',
+ 'video.subtitles_forced': 'forced (signs only)',
+ 'video.subtitles_sdh': 'for the deaf and hard of hearing',
'video.err_subtitle': 'Could not load this subtitle track',
'video.close': 'Close (Esc)',
'preview.pdf_fallback': 'This browser will not display the PDF inline. Download it instead — it was decrypted here either way.',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
index 68fbe10..2e6ea26 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
@@ -252,6 +252,8 @@ export default {
'video.subtitles': 'Subtítulos',
'video.subtitles_off': 'Ninguno',
'video.subtitle_track_n': 'Pista {n}',
+ 'video.subtitles_forced': 'forzados (solo carteles)',
+ 'video.subtitles_sdh': 'para sordos',
'video.err_subtitle': 'No se pudo cargar esta pista de subtítulos',
'video.close': 'Cerrar (Esc)',
'preview.pdf_fallback': 'Este navegador no mostrará el PDF integrado. Descárguelo '
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
index 679a458..5e6f258 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
@@ -253,6 +253,8 @@ export default {
'video.subtitles': 'Sous-titres',
'video.subtitles_off': 'Aucun',
'video.subtitle_track_n': 'Piste {n}',
+ 'video.subtitles_forced': 'forcés (panneaux seulement)',
+ 'video.subtitles_sdh': 'sourds et malentendants',
'video.err_subtitle': 'Impossible de charger cette piste de sous-titres',
'video.close': 'Fermer (Échap)',
'preview.pdf_fallback': 'Ce navigateur n’affichera pas le PDF directement. '
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
index 9a34c93..ba594d2 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
@@ -253,6 +253,8 @@ export default {
'video.subtitles': 'Sottotitoli',
'video.subtitles_off': 'Nessuno',
'video.subtitle_track_n': 'Traccia {n}',
+ 'video.subtitles_forced': 'forzati (solo cartelli)',
+ 'video.subtitles_sdh': 'per non udenti',
'video.err_subtitle': 'Impossibile caricare questa traccia di sottotitoli',
'video.close': 'Chiudi (Esc)',
'preview.pdf_fallback': 'Questo browser non mostrerà il PDF nella pagina. Lo scarichi '
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
index 1d58e2c..9bbafb2 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
@@ -250,6 +250,8 @@ export default {
'video.subtitles': '字幕',
'video.subtitles_off': 'なし',
'video.subtitle_track_n': 'トラック {n}',
+ 'video.subtitles_forced': '強制(表示物のみ)',
+ 'video.subtitles_sdh': '聴覚障害者向け',
'video.err_subtitle': 'この字幕トラックを読み込めませんでした',
'video.close': '閉じる(Esc)',
'preview.pdf_fallback': 'このブラウザーはページ内に PDF を表示しません。'
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
index 5bef61f..8f28d0c 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
@@ -254,6 +254,8 @@ export default {
'video.subtitles': 'Ondertiteling',
'video.subtitles_off': 'Geen',
'video.subtitle_track_n': 'Spoor {n}',
+ 'video.subtitles_forced': 'geforceerd (alleen tekstborden)',
+ 'video.subtitles_sdh': 'voor doven en slechthorenden',
'video.err_subtitle': 'Dit ondertitelspoor kon niet worden geladen',
'video.close': 'Sluiten (Esc)',
'preview.pdf_fallback': 'Deze browser toont de PDF niet in de pagina zelf. Download '
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
index 8989880..fadf622 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
@@ -259,6 +259,8 @@ export default {
'video.subtitles': 'Napisy',
'video.subtitles_off': 'Brak',
'video.subtitle_track_n': 'Ścieżka {n}',
+ 'video.subtitles_forced': 'wymuszone (tylko napisy w tle)',
+ 'video.subtitles_sdh': 'dla niesłyszących',
'video.err_subtitle': 'Nie udało się wczytać tej ścieżki napisów',
'video.close': 'Zamknij (Esc)',
'preview.pdf_fallback': 'Ta przeglądarka nie wyświetli pliku PDF na stronie. Proszę '
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
index f1b8643..39da2b2 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
@@ -254,6 +254,8 @@ export default {
'video.subtitles': 'Legendas',
'video.subtitles_off': 'Nenhuma',
'video.subtitle_track_n': 'Faixa {n}',
+ 'video.subtitles_forced': 'forçadas (apenas letreiros)',
+ 'video.subtitles_sdh': 'para surdos',
'video.err_subtitle': 'Não foi possível carregar esta faixa de legendas',
'video.close': 'Fechar (Esc)',
'preview.pdf_fallback': 'Este navegador não exibirá o PDF na própria página. Baixe '
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
index 91da464..8639592 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
@@ -248,6 +248,8 @@ export default {
'video.subtitles': '字幕',
'video.subtitles_off': '关闭',
'video.subtitle_track_n': '字幕 {n}',
+ 'video.subtitles_forced': '强制(仅字幕牌)',
+ 'video.subtitles_sdh': '听障人士专用',
'video.err_subtitle': '无法加载此字幕轨道',
'video.close': '关闭(Esc)',
'preview.pdf_fallback': '此浏览器不会在页面内显示该 PDF。请改为下载——无论如何它都已在本地解密。',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/video-player.js b/packages/meshbay-hub/src/meshbay_hub/static/video-player.js
index 3bb9124..1522539 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/video-player.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/video-player.js
@@ -113,7 +113,15 @@ function audioTrackLabel(track) {
function subtitleTrackLabel(track) {
const name = _languageName(track.lang)
|| t('video.subtitle_track_n', { n: track.i + 1 });
- return track.title ? `${name} — ${track.title}` : name;
+ // The disposition wins over the container's title, and is translated, so a
+ // forced track reads as one in the viewer's own language rather than as the
+ // English word a muxer happened to type — or as nothing at all, which is
+ // what a forced track with no title tag looked like.
+ const kind = track.forced ? t('video.subtitles_forced')
+ : track.sdh ? t('video.subtitles_sdh')
+ : null;
+ const detail = kind || track.title;
+ return detail ? `${name} — ${detail}` : name;
}
function _mseSupported(codec) {
@@ -1159,33 +1167,6 @@ function VideoPlayer({ entry, transportRef, gekRef, onClose, onDownload }) {
'mode =', v.textTracks[0] && v.textTracks[0].mode,
'cues =', v.textTracks[0] && v.textTracks[0].cues
? v.textTracks[0].cues.length : 'none');
- if (!subtitleUrl) return;
- // The state above is a single instant, and every part of it can be right
- // there and wrong a second later: a re-render can replace the <track>,
- // which resets the mode nothing would set again. What decides whether a
- // subtitle is on screen is `activeCues`, and that is the one number no
- // log has ever carried. Sampled for ten seconds, with the cue that ought
- // to be showing at this instant, so "none active" and "active but not
- // painted" stop being the same observation.
- let n = 0;
- const probe = setInterval(() => {
- const tt = v.textTracks[0];
- if (!tt) { console.log('[MeshBay] subtitle probe: the track is gone'); return; }
- const cues = tt.cues ? Array.from(tt.cues) : [];
- const now = v.currentTime;
- const should = cues.find((c) => c.startTime <= now && c.endTime >= now);
- console.log('[MeshBay] subtitle probe', ++n,
- 't =', now.toFixed(1), 'mode =', tt.mode,
- 'cues =', cues.length,
- 'active =', tt.activeCues ? tt.activeCues.length : 'null',
- 'due here =', should ? `[${should.startTime.toFixed(1)}-`
- + `${should.endTime.toFixed(1)}] ${should.text.slice(0, 40)}` : 'none',
- 'first/last =', cues.length
- ? `${cues[0].startTime.toFixed(1)}..${cues[cues.length - 1].endTime.toFixed(1)}`
- : '-');
- if (n >= 5) clearInterval(probe);
- }, 2000);
- return () => clearInterval(probe);
}, [subtitleUrl]);
useEffect(() => {
diff --git a/packages/meshbay-hub/tests/test_video_subtitles.py b/packages/meshbay-hub/tests/test_video_subtitles.py
index 9f0c08d..6867076 100644
--- a/packages/meshbay-hub/tests/test_video_subtitles.py
+++ b/packages/meshbay-hub/tests/test_video_subtitles.py
@@ -110,7 +110,10 @@ def _label_cases(tmp_path, app, cases, locale="en"):
])
script.write_text(
f"const getLocale = () => '{locale}';\n"
- "const t = (k, p) => `${k}:${p.n}`;\n"
+ # Not every key is interpolated — the forced/SDH qualifiers take no
+ # parameters — and a stub that assumes one throws where the real `t`
+ # returns a string. A fixture narrower than production tests itself.
+ "const t = (k, p) => (p ? `${k}:${p.n}` : k);\n"
+ src
+ "\nconst out = JSON.parse(process.argv[2]).map(subtitleTrackLabel);\n"
"console.log(JSON.stringify(out));\n")
@@ -150,6 +153,38 @@ def test_the_container_title_tells_a_forced_track_from_a_full_one(tmp_path, app)
assert "Forced" in out[0] and "SDH" in out[1]
+def test_a_forced_track_is_named_as_one_in_the_reader_s_language(tmp_path, app):
+ """The entry that was reported as a broken feature.
+
+ A forced track shows signage and foreign dialogue only — on a real film,
+ 77 seconds of text across 2h32 — so picking it and seeing nothing is its
+ normal behaviour. It has to be possible to tell it from the full track
+ beside it, which carries the same language tag, and the disposition says
+ so where the container's English title tag is often simply absent.
+ """
+ out = _label_cases(tmp_path, app, [
+ {"i": 0, "lang": "fre", "forced": True},
+ {"i": 1, "lang": "fre"},
+ {"i": 2, "lang": "eng", "sdh": True},
+ ])
+ assert out[0] != out[1], f"a forced track must not read like the full one: {out}"
+ # The catalogues are stubbed here, so what is asserted is that the label
+ # goes through `t` at all: the qualifier has to be translated, not the
+ # English word a muxer typed into the container.
+ assert out[0] == 'French — video.subtitles_forced', out
+ assert out[1] == 'French', out
+ assert out[2] == 'English — video.subtitles_sdh', out
+
+
+def test_the_disposition_wins_over_the_container_title(tmp_path, app):
+ """A muxer's "Forced" is one spelling of many, and in one language."""
+ out = _label_cases(tmp_path, app, [
+ {"i": 0, "lang": "fre", "title": "FORCE VF", "forced": True},
+ ])
+ assert 'FORCE VF' not in out[0], out
+ assert out[0] == 'French — video.subtitles_forced', out
+
+
def test_an_untagged_track_is_numbered_not_called_unknown(tmp_path, app):
"""A file with no language tags still needs distinguishable entries."""
out = _label_cases(tmp_path, app, [{"i": 3, "lang": None}, {"i": 4, "lang": "und"}])
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])