diff options
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/transport.js | 7 | ||||
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py | 56 |
2 files changed, 51 insertions, 12 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js index 1b9abb3..4ee5810 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js @@ -579,8 +579,11 @@ class MeshBayTransport { * MediaSource consumes a segment at a time — which is fine for a clip and * fatal for anything worth streaming. */ - requestStream(fileId, credits = STREAM_CREDITS) { - this._send({ type: 'stream_req', v: '0.1', file_id: fileId, credits }); + requestStream(fileId, credits = STREAM_CREDITS, start = 0) { + // `start` is a seek: the node retires whatever this session was streaming + // and spawns ffmpeg again from there. Omitted or zero is the film's + // beginning, which is what an 0.1 node understands. + this._send({ type: 'stream_req', v: '0.1', file_id: fileId, credits, start }); } /** Room for `n` more segments. */ 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 5f5c9ff..6b87e31 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py +++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py @@ -463,18 +463,29 @@ class WebRTCPeerSession: # whatever one likes into the operator's file. def _f(key: str, n: int = 24) -> str: return str(msg.get(key))[:n].replace("\n", " ") + if msg.get("event"): + # Once per stream or per seek, not once per five seconds — + # and a seek nobody asked for looks exactly like a viewer + # dragging the scrubber from this side, so it has to be + # visible without turning DEBUG on. + log.info( + "stream: client %s target=%s t=%ss offset=%s ready=%s " + "duration=%s ranges=[%s]", + _f("event", 16), _f("target"), _f("t"), _f("offset"), + _f("ready"), _f("duration"), _f("ranges", 120)) # Debug: one line every five seconds per viewer. Run the daemon # with --log-level debug to see inside a player that is # misbehaving — it is the only view of the browser there is # when the browser is a phone. - log.debug( - "stream: client t=%ss ahead=%ss ready=%s paused=%s " - "stalled=%s q=%s inflight=%s appending=%s updating=%s " - "quota=%s ms=%s err=%s ranges=[%s] (sent=%d)", - _f("t"), _f("ahead"), _f("ready"), _f("paused"), - _f("stalled"), _f("q"), _f("inflight"), _f("appending"), - _f("updating"), _f("quota"), _f("ms"), _f("err", 80), - _f("ranges", 120), self._stream_segments) + else: + log.debug( + "stream: client t=%ss ahead=%ss ready=%s paused=%s " + "stalled=%s q=%s inflight=%s appending=%s updating=%s " + "quota=%s ms=%s err=%s ranges=[%s] (sent=%d)", + _f("t"), _f("ahead"), _f("ready"), _f("paused"), + _f("stalled"), _f("q"), _f("inflight"), _f("appending"), + _f("updating"), _f("quota"), _f("ms"), _f("err", 80), + _f("ranges", 120), self._stream_segments) elif mtype == MNP.STREAM_STOP: age = (time.monotonic() - self._stream_started_at if self._stream_started_at else -1) @@ -2056,8 +2067,29 @@ class WebRTCPeerSession: self._send({"type": "error", "detail": "Unsupported video codec"}) return + # Where to begin. Seeking is a stream restarted somewhere else: the + # viewer moves the scrubber, this session's previous stream is retired + # by _replace_stream, and ffmpeg is spawned again with -ss. + try: + start = float(msg.get("start", 0) or 0) + except (TypeError, ValueError): + start = 0.0 + # Past the end would produce an empty stream and a player waiting for + # segments that are never coming. + if duration and start >= duration - 1: + start = max(0.0, duration - 5) + start = max(0.0, start) + + # -ss BEFORE -i, which seeks by the container index rather than by + # decoding up to the point: milliseconds on a 500 MB film instead of + # tens of seconds. It lands on the keyframe at or before `start`, so + # the picture can begin a few seconds earlier than asked — which is + # what every streaming player does, and why the client is told the + # value used rather than left to assume its own. + seek_args = ["-ss", f"{start:.3f}"] if start > 0 else [] proc = await asyncio.create_subprocess_exec( "ffmpeg", "-hide_banner", "-loglevel", "error", + *seek_args, "-i", str(file_path), "-c", "copy", "-movflags", "frag_keyframe+empty_moov+default_base_moof", @@ -2071,6 +2103,10 @@ class WebRTCPeerSession: "file_id": file_id, "codec": codec_str, "duration": duration, + # ffmpeg restarts its timestamps at zero whatever we seek to, so + # this is what the client adds back (`SourceBuffer.timestampOffset`) + # to put the fragments where they belong on the timeline. + "start": start, }) # A client that says nothing gets the old behaviour, which is why this @@ -2087,8 +2123,8 @@ 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", - file_id[:12], paced, self._stream_credit) + log.info("stream: stream_init sent file=%s paced=%s credits=%d start=%.1fs", + file_id[:12], paced, self._stream_credit, start) try: while True: if paced and not await self._await_stream_credit(): |