aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-18 11:05:47 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-18 11:05:47 +0200
commit063c5f0750b775362e45e21a97e0dc2250c89f77 (patch)
tree9db3276c691bc1d49621cb144587e3a0be68d281
parentc03512aeab576a06f8d5026e5eb484897ec45f99 (diff)
downloadmeshbay-063c5f0750b775362e45e21a97e0dc2250c89f77.tar.gz
fix(cast): keep the Chromecast progress log out of the terminal
The receiver reports its state on a timer, so `player status: PLAYING` repeats for as long as a film runs and buries everything else in the terminal the app was started from. The progress lines now go through Node's own `debuglog`, which costs nothing when disabled — the message is never formatted — and is restored with: NODE_DEBUG=cast-chromecast npm start The socket error stays on console.error. It fires when the connection to the receiver dies and is the only trace of why a cast stopped; routing it through the same switch would make the one failure worth seeing the one that disappears. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UGY17EPph5LsLzePPXhUVc
-rw-r--r--packages/meshbay-client/src/cast-chromecast.js40
-rw-r--r--packages/meshbay-hub/tests/test_cast_subtitles.py27
2 files changed, 57 insertions, 10 deletions
diff --git a/packages/meshbay-client/src/cast-chromecast.js b/packages/meshbay-client/src/cast-chromecast.js
index 2dd210f..1355cdd 100644
--- a/packages/meshbay-client/src/cast-chromecast.js
+++ b/packages/meshbay-client/src/cast-chromecast.js
@@ -1,9 +1,27 @@
'use strict';
+const util = require('node:util');
+
const Bonjour = require('bonjour-service').Bonjour;
const CastClient = require('castv2-client').Client;
const DefaultMediaReceiver = require('castv2-client').DefaultMediaReceiver;
+/**
+ * Progress logging, off unless asked for:
+ *
+ * NODE_DEBUG=cast-chromecast npm start
+ *
+ * The receiver reports its state on a timer, so `player status: PLAYING`
+ * repeats for as long as a film runs and buries everything else in the
+ * terminal. Node's own `debuglog` is used rather than a flag of our own: it
+ * costs nothing when disabled — the message is never even formatted — and it
+ * is the knob a Node developer already expects to reach for.
+ *
+ * Failures do not come through here. A connection that dies silently is a
+ * connection nobody can diagnose, so those stay on `console.error`.
+ */
+const debug = util.debuglog('cast-chromecast');
+
const SCAN_DURATION_MS = 6000;
// The one track the receiver is ever told about. A side-loaded track cannot
@@ -78,7 +96,7 @@ class CastChromecast {
this._bonjour = new Bonjour();
}
- console.log('[cast-chromecast] scanning for devices...');
+ debug('[cast-chromecast] scanning for devices...');
return new Promise((resolve) => {
this._browser = this._bonjour.find({ type: 'googlecast' }, (service) => {
@@ -90,7 +108,7 @@ class CastChromecast {
if (host && id) {
this._devices.set(id, { id, name, host, port });
- console.log(`[cast-chromecast] discovered: "${name}" at ${host}:${port}`);
+ debug(`[cast-chromecast] discovered: "${name}" at ${host}:${port}`);
}
});
@@ -100,7 +118,7 @@ class CastChromecast {
this._browser = null;
}
const devices = Array.from(this._devices.values());
- console.log(`[cast-chromecast] scan complete: ${devices.length} device(s)`);
+ debug(`[cast-chromecast] scan complete: ${devices.length} device(s)`);
resolve(devices);
}, SCAN_DURATION_MS);
});
@@ -112,13 +130,15 @@ class CastChromecast {
await this.disconnect();
- console.log(`[cast-chromecast] connecting to "${device.name}" (${device.host}:${device.port})`);
+ debug(`[cast-chromecast] connecting to "${device.name}" (${device.host}:${device.port})`);
const client = new CastClient();
await new Promise((resolve, reject) => {
client.on('error', (err) => {
- console.log(`[cast-chromecast] client error: ${err.message}`);
+ // Not `debug`: this fires when the socket to the receiver dies, and it
+ // is the only trace of why the cast stopped.
+ console.error(`[cast-chromecast] client error: ${err.message}`);
this._cleanup();
});
client.connect(device.host, () => resolve());
@@ -138,10 +158,10 @@ class CastChromecast {
this._player = player;
player.on('status', (status) => {
- console.log(`[cast-chromecast] player status: ${status.playerState}`);
+ debug(`[cast-chromecast] player status: ${status.playerState}`);
});
- console.log(`[cast-chromecast] loading with ${subtitle?.url ? 'subtitles' : 'no subtitles'}`);
+ debug(`[cast-chromecast] loading with ${subtitle?.url ? 'subtitles' : 'no subtitles'}`);
const status = await new Promise((resolve, reject) => {
player.load(mediaFor(mediaUrl, subtitle), loadOptionsFor(subtitle),
(err, s) => {
@@ -150,13 +170,13 @@ class CastChromecast {
});
});
- console.log(`[cast-chromecast] loaded on "${device.name}", state: ${status.playerState}`);
+ debug(`[cast-chromecast] loaded on "${device.name}", state: ${status.playerState}`);
return { deviceName: device.name, playerState: status.playerState };
}
async reload(mediaUrl, subtitle) {
if (!this._player) throw new Error('Not connected');
- console.log(`[cast-chromecast] reloading stream on "${this._connectedDevice?.name}"`
+ debug(`[cast-chromecast] reloading stream on "${this._connectedDevice?.name}"`
+ ` with ${subtitle?.url ? 'subtitles' : 'no subtitles'}`);
const status = await new Promise((resolve, reject) => {
this._player.load(mediaFor(mediaUrl, subtitle), loadOptionsFor(subtitle),
@@ -165,7 +185,7 @@ class CastChromecast {
resolve(s);
});
});
- console.log(`[cast-chromecast] reloaded, state: ${status.playerState}`);
+ debug(`[cast-chromecast] reloaded, state: ${status.playerState}`);
return { playerState: status.playerState };
}
diff --git a/packages/meshbay-hub/tests/test_cast_subtitles.py b/packages/meshbay-hub/tests/test_cast_subtitles.py
index 722a56a..3fe627d 100644
--- a/packages/meshbay-hub/tests/test_cast_subtitles.py
+++ b/packages/meshbay-hub/tests/test_cast_subtitles.py
@@ -391,3 +391,30 @@ def test_a_stream_without_subtitles_declares_none(loaded):
"""
assert "tracks" not in loaded["without"]["media"]
assert loaded["without"]["options"]["activeTrackIds"] == []
+
+
+@chromecast_only
+def test_progress_logging_is_off_unless_asked_for():
+ """
+ The receiver reports its state on a timer, so `player status: PLAYING`
+ repeats for as long as a film runs. Left on `console.log` it fills the
+ terminal the app was started from and buries everything else in it.
+
+ Read rather than run because the noisy line only fires with a device on the
+ network. What is checked is the property that matters: nothing in this
+ module reaches the terminal unconditionally except a failure. `console.warn`
+ and `console.info` are named too — a future line using either would be just
+ as loud, and this guard would not otherwise see it.
+ """
+ src = CHROMECAST.read_text(encoding="utf-8")
+ unconditional = [
+ line.strip() for line in src.splitlines()
+ if ("console.log(" in line or "console.warn(" in line
+ or "console.info(" in line)
+ and not line.lstrip().startswith(("*", "//"))]
+ assert unconditional == [], (
+ "these reach the terminal whatever the user asked for; route them "
+ f"through `debug` or `console.error`: {unconditional}")
+ # And the failure path is still audible, or a cast that dies takes its own
+ # explanation with it.
+ assert "console.error(`[cast-chromecast] client error" in src