summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/config.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-16 20:58:08 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-16 20:58:08 +0200
commite012b7e9ce55079f411943c7a1f6ccbfbd629a5f (patch)
treee4123e029ea98bedf3e6ee94f794166c0d46e4df /packages/meshbay-node/src/meshbay_node/config.py
parent5dea19d9950518887be7eb14696f600ee023dbbd (diff)
downloadmeshbay-e012b7e9ce55079f411943c7a1f6ccbfbd629a5f.tar.gz
feat(node): the stream capacity is the operator's to set, and a log that says who stopped
Bounding the client's read-ahead changed what a transcode slot is. It used to be a burst — the browser took segments as fast as it could append them, so a slot came back within the minute whatever the length of the film. Now it is held for as long as someone is watching, so the cap counts simultaneous viewers, and two of them meant the third was refused for the next hour and a half. The right number depends on the machine, so it belongs to the operator: `[node] max_concurrent_streams` in node.toml, or MESHBAY_MAX_CONCURRENT_STREAMS. Default 8 — one ffmpeg per viewer, remuxing rather than encoding, idle on a pipe for most of the film. Zero, a negative number, a non-number and a bool are refused with a warning naming the setting: `Semaphore(0)` is not "no limit", it is a node where no video ever plays and nothing says why, and TOML `true` would have become 1 by way of `int()`. A stream also ends on the peer's silence now rather than on its stinginess. A viewer buffered well ahead deliberately grants nothing for minutes, and the old budget accumulated over the whole wait, so a keepalive that granted no credit could not keep a paused film alive. The rest is diagnosis, which is what this cost. `client_diag` carries the player's own view — readyState, refused appends, buffered ranges, the video element's error — into the node's log at DEBUG, next to the node's view of the same stream. It is the only window into a phone, and every field is stringified and cut short because all of it is peer-controlled. The node also logs the first keepalive, which distinguishes a paced client from an unpaced one at a glance, and progress every hundred segments, whose last line says where a stream stopped and which side stopped it.
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/config.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/config.py43
1 files changed, 43 insertions, 0 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/config.py b/packages/meshbay-node/src/meshbay_node/config.py
index e4444d3..3de2473 100644
--- a/packages/meshbay-node/src/meshbay_node/config.py
+++ b/packages/meshbay-node/src/meshbay_node/config.py
@@ -37,6 +37,13 @@ ui_port = 18000 # local admin UI (127.0.0.1 only)
invite_ttl_hours = 168 # 7 days
pair_ttl_hours = 24
+# How many people may watch a video at once. One ffmpeg runs per viewer for as
+# long as they watch — it remuxes rather than re-encodes, so it costs little CPU
+# and around 50 MB of memory, and spends most of the film idle. Past this, a
+# viewer is told the server is busy. Raise it on a machine with memory to spare;
+# lower it on a Pi.
+max_concurrent_streams = 8
+
# Browser and native clients reach this node over WebRTC DataChannel via hub
# signaling — no inbound port to open. QUIC is the optional direct path.
@@ -82,6 +89,11 @@ class NodeConfig:
# the SSH session that printed it.
invite_ttl_hours: int = 168 # 7 days
pair_ttl_hours: int = 24
+ # How many people may watch a video at the same time. One ffmpeg runs per
+ # viewer for as long as they watch, so this is the knob that decides when
+ # the node answers "server busy" — see MAX_CONCURRENT_TRANSCODES in
+ # transport/webrtc_server.py for what one costs.
+ max_concurrent_streams: int = 8
@dataclass
@@ -121,6 +133,30 @@ class Config:
return self.groups[0] if self.groups else GroupConfig()
+def _positive(value: object, default: int, name: str) -> int:
+ """A count that must be at least one, or the default with a word about it.
+
+ Zero is the dangerous one: `asyncio.Semaphore(0)` is not "no limit", it is
+ a node where no video ever plays and nothing in the log says why.
+ """
+ # bool before int: TOML `true` is a bool, and `int(True)` is 1 — a node
+ # where exactly one person may watch, arrived at by a typo and announced
+ # nowhere.
+ if isinstance(value, bool) or not isinstance(value, (int, str)):
+ log.warning("%s = %r is not a count — using %d", name, value, default)
+ return default
+ try:
+ n = int(value)
+ except (TypeError, ValueError):
+ log.warning("%s = %r is not a number — using %d", name, value, default)
+ return default
+ if n < 1:
+ log.warning("%s = %d would stop the feature entirely — using %d",
+ name, n, default)
+ return default
+ return n
+
+
def load_config(path: Path = DEFAULT_CONFIG_PATH) -> Config:
"""
Load config from TOML file. Supports both single [group] and
@@ -144,6 +180,9 @@ def load_config(path: Path = DEFAULT_CONFIG_PATH) -> Config:
nd.get("invite_ttl_hours", cfg.node.invite_ttl_hours))
cfg.node.pair_ttl_hours = int(
nd.get("pair_ttl_hours", cfg.node.pair_ttl_hours))
+ cfg.node.max_concurrent_streams = _positive(
+ nd.get("max_concurrent_streams", cfg.node.max_concurrent_streams),
+ cfg.node.max_concurrent_streams, "max_concurrent_streams")
# Multi-group: [[groups]] array
if "groups" in raw:
@@ -189,6 +228,10 @@ def load_config(path: Path = DEFAULT_CONFIG_PATH) -> Config:
cfg.hub.username = user
if port := os.environ.get("MESHBAY_QUIC_PORT"):
cfg.node.quic_port = int(port)
+ if streams := os.environ.get("MESHBAY_MAX_CONCURRENT_STREAMS"):
+ cfg.node.max_concurrent_streams = _positive(
+ streams, cfg.node.max_concurrent_streams,
+ "MESHBAY_MAX_CONCURRENT_STREAMS")
return cfg