From a98445ce246509b0d2600df14907f72b448a6d10 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Thu, 17 Sep 2026 10:12:31 +0200 Subject: feat: let the viewer pick the audio track The streaming path mapped 0:a:0 unconditionally, so a dubbed film played in whichever language was muxed first and the others were unreachable. The node now enumerates the tracks in stream_init and honours audio_track in stream_req; switching is the seek path, since one ffmpeg carries one track. MNP 3.2, additive: the player draws its selector from the node's own list and never from a version number, so an older node is never asked for a track it would ignore. MNP_MIN_SUPPORTED does not move. Co-Authored-By: Claude Opus 5 --- .../src/meshbay_node/transport/webrtc_server.py | 46 +++++++++++++++++++--- 1 file changed, 41 insertions(+), 5 deletions(-) (limited to 'packages/meshbay-node/src/meshbay_node/transport') 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 99ba3c9..b39941d 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py +++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py @@ -5948,11 +5948,14 @@ class WebRTCPeerSession: file_hash = bytes.fromhex(entry.id) try: - codec_str, duration, has_audio, _width, _height, raw_video_codec = \ - await _probe_video(str(file_path)) + probe = await _probe_video(str(file_path)) except Exception as e: self._send({"type": "error", "detail": f"Probe failed: {e}"}) return + codec_str = probe.codec + duration = probe.duration + has_audio = probe.has_audio + raw_video_codec = probe.raw_codec_name # No video stream at all is the only thing this path cannot serve, and # it is the only thing refused here. A source with no MSE codec string @@ -6051,8 +6054,23 @@ class WebRTCPeerSession: codec_str = "avc1.640029,mp4a.40.2" if has_audio else "avc1.640029" else: codec_args = ["-c:v", "copy"] + # Which audio track. A dubbed film carries several and the first one is + # not a neutral default — it is whatever the person who muxed the file + # happened to put first, which across a real library is overwhelmingly + # one language. Out of range falls back to the first rather than + # refusing: the client's list comes from a `stream_init` that may + # predate the file being replaced on disk, and a viewer who asked for + # the second track of a file that now has one wants the film, not an + # error. `stream_init` says which track was actually used, the same way + # it says which `start` was actually used and for the same reason. + try: + audio_track = int(msg.get("audio_track", 0) or 0) + except (TypeError, ValueError): + audio_track = 0 + if not 0 <= audio_track < len(probe.audio_tracks): + audio_track = 0 if has_audio: - map_args += ["-map", "0:a:0"] + map_args += ["-map", f"0:a:{audio_track}"] # Downmixed to stereo: a WEB-DL's 5.1 track becomes 6-channel AAC # with no "-ac", which ffprobe and VLC accept fine but which some # browsers' MSE decoder rejects outright once real fragments are @@ -6081,6 +6099,22 @@ class WebRTCPeerSession: # this is what the client adds back (`SourceBuffer.timestampOffset`) # to put the fragments where they belong on the timeline. "start": start, + # The track list is how a client discovers that this node can + # switch language at all — there is no version check anywhere in + # the player. A node that does not send it gets no selector, and + # the client then never sends `audio_track` to a peer that would + # ignore it and serve the wrong language without saying so. + "audio_tracks": [ + { + "i": tr.ordinal, + "lang": tr.language, + "title": tr.title, + "codec": tr.codec_name, + "ch": tr.channels, + } + for tr in probe.audio_tracks + ], + "audio_track": audio_track if has_audio else None, }) # A client that says nothing gets the old behaviour, which is why this @@ -6097,8 +6131,10 @@ class WebRTCPeerSession: self._stream_started_at = time.monotonic() self._stream_segments = 0 reason = "eof" - log.info("stream: stream_init sent file=%s paced=%s credits=%d start=%.1fs", - file_id[:12], paced, self._stream_credit, start) + log.info("stream: stream_init sent file=%s paced=%s credits=%d start=%.1fs " + "audio=%s/%d", + file_id[:12], paced, self._stream_credit, start, + audio_track if has_audio else "-", len(probe.audio_tracks)) try: while True: if paced and not await self._await_stream_credit(): -- cgit v1.2.3