aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/media_probe.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/media_probe.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/media_probe.py57
1 files changed, 57 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 7858ebe..06b1e28 100644
--- a/packages/meshbay-node/src/meshbay_node/media_probe.py
+++ b/packages/meshbay-node/src/meshbay_node/media_probe.py
@@ -40,6 +40,37 @@ class AudioTrack:
channels: int | None
+# Subtitle codecs ffmpeg can convert to WebVTT, which is the only thing MSE
+# can be given. An allow-list rather than a bitmap deny-list: the cost of
+# wrongly excluding an exotic text codec is a track nobody can pick, and the
+# cost of wrongly including a bitmap one is a track that is picked and then
+# displays nothing, with no error to lead anyone back here.
+TEXT_SUBTITLE_CODECS = frozenset({
+ "subrip", "srt", "ass", "ssa", "mov_text", "webvtt", "text",
+ "subviewer", "subviewer1", "sami", "realtext", "stl", "jacosub",
+ "microdvd", "mpl2", "vplayer", "pjs",
+})
+
+
+@dataclass(frozen=True)
+class SubtitleTrack:
+ """
+ One selectable subtitle track, guaranteed convertible to WebVTT.
+
+ **`ordinal` counts every subtitle stream, including the bitmap ones this
+ list does not carry**, because that is what `-map 0:s:<n>` counts. The
+ same trap as `AudioTrack.ordinal` one level deeper: filtering the list and
+ numbering the survivors would give a file whose streams are PGS, SRT, SRT
+ the ordinals 0 and 1 for its two text tracks, and `-map 0:s:0` would then
+ extract the PGS stream — which produces an empty WebVTT rather than an
+ error, so the viewer gets a subtitle track with no subtitles in it.
+ """
+ ordinal: int
+ language: str | None
+ title: str | None
+ codec_name: str | None
+
+
@dataclass
class VideoProbe:
"""
@@ -57,6 +88,7 @@ class VideoProbe:
height: int | None
raw_codec_name: str | None
audio_tracks: list[AudioTrack] = field(default_factory=list)
+ subtitle_tracks: list[SubtitleTrack] = field(default_factory=list)
async def probe_video(path: str) -> VideoProbe:
@@ -93,6 +125,14 @@ async def probe_video(path: str) -> VideoProbe:
group does not want. `has_audio` stays as the single question the muxing
decisions ask, and is now `bool(audio_tracks)`.
+ **Only text subtitle tracks are reported.** A library's embedded subtitles
+ are roughly four-fifths text (subrip, ass) and one-fifth bitmap (PGS,
+ VOBSUB); a bitmap track has no path to WebVTT without OCR, so listing one
+ would offer a choice that silently displays nothing. A file whose only
+ subtitles are bitmap therefore reports none at all and gets no selector,
+ exactly like a file with no subtitles — which is a true statement about
+ what this node can serve, not a concealed failure.
+
width/height come from the same ffprobe call (one extra `-show_entries`
field, no second process spawn) — resolution is deliberately never
guessed from the filename (docs/mediacenter.md §3.5).
@@ -116,6 +156,8 @@ async def probe_video(path: str) -> VideoProbe:
width: int | None = None
height: int | None = None
audio_tracks: list[AudioTrack] = []
+ subtitle_tracks: list[SubtitleTrack] = []
+ subtitle_streams_seen = 0
for s in info.get("streams", []):
if s.get("codec_type") == "video" and not v_codec:
cn = s.get("codec_name", "")
@@ -142,6 +184,20 @@ async def probe_video(path: str) -> VideoProbe:
codec_name=s.get("codec_name") or None,
channels=s.get("channels"),
))
+ elif s.get("codec_type") == "subtitle":
+ # Counted before the filter, never after — see SubtitleTrack.
+ ordinal = subtitle_streams_seen
+ subtitle_streams_seen += 1
+ codec_name = (s.get("codec_name") or "").strip() or None
+ if codec_name not in TEXT_SUBTITLE_CODECS:
+ continue
+ tags = s.get("tags") 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,
+ ))
has_audio = bool(audio_tracks)
codec = None
@@ -155,4 +211,5 @@ async def probe_video(path: str) -> VideoProbe:
height=height,
raw_codec_name=raw_codec_name,
audio_tracks=audio_tracks,
+ subtitle_tracks=subtitle_tracks,
)