From 063c5f0750b775362e45e21a97e0dc2250c89f77 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Fri, 18 Sep 2026 11:05:47 +0200 Subject: fix(cast): keep the Chromecast progress log out of the terminal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01UGY17EPph5LsLzePPXhUVc --- packages/meshbay-client/src/cast-chromecast.js | 40 +++++++++++++++++------ packages/meshbay-hub/tests/test_cast_subtitles.py | 27 +++++++++++++++ 2 files changed, 57 insertions(+), 10 deletions(-) (limited to 'packages') 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 -- cgit v1.2.3