summaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-16 20:58:17 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-16 20:58:17 +0200
commitf57b2f6c0c2770fe0f456128d985e29427d2432f (patch)
tree3f502d00da42c3a82d755b1db8ec7cb29ed2c946 /docs
parente012b7e9ce55079f411943c7a1f6ccbfbd629a5f (diff)
downloadmeshbay-f57b2f6c0c2770fe0f456128d985e29427d2432f.tar.gz
docs: what this session cost to find
Five lessons, and the first is the expensive one: a test that models a fix agrees with it by construction. The buffer-ceiling test passed against a player that still hung, because the model and the fix had the same author and the same misunderstanding. Also: `no-cache` only binds a browser that asks; redeploying during someone else's test kills their session and truncates the log holding the reproduction; `updateend` fires for `remove()`; and flow control on a media stream is a window, not a debt. USERGUIDE section 7 rewritten — it still described 24 segments in flight and two transcode slots, and said "transcode" where ffmpeg does a `-c copy` remux, which is exactly why a slot costs little and why 500 MB really does go on the wire.
Diffstat (limited to 'docs')
-rw-r--r--docs/USERGUIDE.md58
1 files changed, 46 insertions, 12 deletions
diff --git a/docs/USERGUIDE.md b/docs/USERGUIDE.md
index d6ce327..abfc404 100644
--- a/docs/USERGUIDE.md
+++ b/docs/USERGUIDE.md
@@ -732,9 +732,10 @@ node but cannot substitute one.
## 7. Video Streaming
Video is streamed over the same MNP channel and played through Media Source Extensions.
-The node transcodes to fragmented MP4 on the fly and encrypts each segment exactly like a
-file chunk, so a standard `<video src=...>` cannot play it — the segments are ciphertext
-until the client decrypts them.
+The node remuxes to fragmented MP4 on the fly — the container changes, the video and audio
+streams are copied untouched — and encrypts each segment exactly like a file chunk, so a
+standard `<video src=...>` cannot play it: the segments are ciphertext until the client
+decrypts them.
```
→ {"type": "stream_req", "v": "0.1", "file_id": "<blake3 hex>"}
@@ -751,22 +752,55 @@ ffmpeg must be installed on the node for transcoding.
**Flow control.** The client says how many segments it can take — `stream_req`
carries a credit count — and the node sends no more than that until `stream_more`
-grants more. Without it the node hands ffmpeg's entire output to the channel as
-fast as it is produced, and the browser holds a whole film in memory while the
-player consumes it a segment at a time. Segments are 256 KB and the web client
-keeps 24 outstanding, so roughly 6 MB is in flight whatever the film's length. A
-client that sends no credit count gets the old unpaced behaviour.
+grants more. Segments are 256 KB.
+
+What governs the credit is the playhead, not the append: the client grants more
+only while it holds less than **90 seconds of film ahead of where you are
+watching**. That bound is the whole point. ffmpeg runs with `-c copy` — a remux,
+not a re-encode — so the bytes on the wire are the file's own, and a 500 MB film
+really does try to put 500 MB somewhere. Granting credit per append instead meant
+taking it as fast as the network could deliver, which filled the browser's
+SourceBuffer ceiling (a few hundred megabytes) in the first minute and wedged the
+player at "buffering" for good. Buffering by time costs the same for a two-hour
+film as for a two-minute clip — around 20 MB at a typical bitrate.
+
+A client that sends no credit count gets the old unpaced behaviour.
+
+**A viewer that is well ahead still says so.** Holding credit back means granting
+nothing for minutes at a time, which the node would otherwise read as a closed
+tab. The client sends `stream_more` with `n = 0` every 20 seconds: it grants no
+room but proves someone is there. The node ends a stream on silence, not on
+stinginess.
**Closing the viewer stops the stream.** `stream_stop` tells the node nobody is
-watching, so ffmpeg is killed and its transcode slot released at once. There are
-two slots; before this, leaving a video held one for the two-minute credit
-timeout, which is what made the next video answer "server busy".
+watching, so ffmpeg is killed and its slot released at once. Before this, leaving
+a video held one for the two-minute credit timeout, which is what made the next
+video answer "server busy".
+
+**How many people may watch at once.** A slot is now held for as long as someone
+is watching, so it is a limit on simultaneous viewers rather than on bursts. The
+default is 8. The operator sets it in `node.toml`:
+
+```toml
+[node]
+max_concurrent_streams = 8
+```
+
+or with `MESHBAY_MAX_CONCURRENT_STREAMS` in the environment. One ffmpeg runs per
+viewer, remuxing rather than encoding — little CPU, roughly 50 MB of memory, idle
+most of the film — so raise it on a machine with memory to spare and lower it on
+a Pi. Past the limit a viewer is told the server is busy. Zero, a negative number
+or a non-number is refused with a warning naming the setting, because a limit of
+zero is a node where no video ever plays and nothing says why.
**The player drops what has been watched.** A SourceBuffer is not a file: browsers
cap it and refuse the append that goes past, so anything more than a minute behind
the playhead is evicted. A segment refused for want of room is retried rather than
dropped — dropping it leaves a hole in the middle of the film and no error
-anywhere.
+anywhere. The retry is driven by a timer and by playback progress, never by the
+arrival of the next segment: an append refused for want of room produces no
+`updateend` and so grants no credit, and a pipeline whose only wakeup is the
+segment it is waiting for cannot restart itself.
---