diff options
Diffstat (limited to 'packages/meshbay-client/src/cast-chromecast.js')
| -rw-r--r-- | packages/meshbay-client/src/cast-chromecast.js | 120 |
1 files changed, 92 insertions, 28 deletions
diff --git a/packages/meshbay-client/src/cast-chromecast.js b/packages/meshbay-client/src/cast-chromecast.js index 1355cdd..55deb5b 100644 --- a/packages/meshbay-client/src/cast-chromecast.js +++ b/packages/meshbay-client/src/cast-chromecast.js @@ -78,19 +78,25 @@ class CastChromecast { constructor() { this._bonjour = null; this._browser = null; + this._scanTimer = null; this._devices = new Map(); this._client = null; this._player = null; this._connectedDevice = null; } - async discover() { + /** + * Start a scan and return at once; `devices()` reads what it has found so far. + * + * The scan runs its full length because a receiver coming back from a reset + * can take several seconds to answer, but most answer within two, and a + * picker that waits the full length to show any of them is a picker that is + * slow every single time. So the page polls and lists each one as it lands. + * A scan started over an unfinished one replaces it, timer included. + */ + startScan() { this._devices.clear(); - - if (this._browser) { - this._browser.stop(); - this._browser = null; - } + this._stopScan(); if (!this._bonjour) { this._bonjour = new Bonjour(); @@ -98,30 +104,39 @@ class CastChromecast { debug('[cast-chromecast] scanning for devices...'); - return new Promise((resolve) => { - this._browser = this._bonjour.find({ type: 'googlecast' }, (service) => { - const id = service.txt?.id || service.name; - const name = service.txt?.fn || service.name; - const host = service.addresses?.find((a) => /^\d+\.\d+\.\d+\.\d+$/.test(a)) - || (service.referer && service.referer.address); - const port = service.port || 8009; + this._browser = this._bonjour.find({ type: 'googlecast' }, (service) => { + const id = service.txt?.id || service.name; + const name = service.txt?.fn || service.name; + const host = service.addresses?.find((a) => /^\d+\.\d+\.\d+\.\d+$/.test(a)) + || (service.referer && service.referer.address); + const port = service.port || 8009; - if (host && id) { - this._devices.set(id, { id, name, host, port }); - debug(`[cast-chromecast] discovered: "${name}" at ${host}:${port}`); - } - }); - - setTimeout(() => { - if (this._browser) { - this._browser.stop(); - this._browser = null; - } - const devices = Array.from(this._devices.values()); - debug(`[cast-chromecast] scan complete: ${devices.length} device(s)`); - resolve(devices); - }, SCAN_DURATION_MS); + if (host && id) { + this._devices.set(id, { id, name, host, port }); + debug(`[cast-chromecast] discovered: "${name}" at ${host}:${port}`); + } }); + + this._scanTimer = setTimeout(() => { + this._stopScan(); + debug(`[cast-chromecast] scan complete: ${this._devices.size} device(s)`); + }, SCAN_DURATION_MS); + } + + devices() { + return { + devices: Array.from(this._devices.values()), + scanning: this._browser !== null, + }; + } + + _stopScan() { + clearTimeout(this._scanTimer); + this._scanTimer = null; + if (this._browser) { + this._browser.stop(); + this._browser = null; + } } async connect(deviceId, mediaUrl, subtitle) { @@ -159,6 +174,7 @@ class CastChromecast { player.on('status', (status) => { debug(`[cast-chromecast] player status: ${status.playerState}`); + this._noteStatus(status); }); debug(`[cast-chromecast] loading with ${subtitle?.url ? 'subtitles' : 'no subtitles'}`); @@ -170,6 +186,7 @@ class CastChromecast { }); }); + this._noteStatus(status); debug(`[cast-chromecast] loaded on "${device.name}", state: ${status.playerState}`); return { deviceName: device.name, playerState: status.playerState }; } @@ -185,6 +202,7 @@ class CastChromecast { resolve(s); }); }); + this._noteStatus(status); debug(`[cast-chromecast] reloaded, state: ${status.playerState}`); return { playerState: status.playerState }; } @@ -200,10 +218,55 @@ class CastChromecast { this._cleanup(); } + /** + * What the receiver last said, kept so the page can show where the + * *television* is: its playhead is not the local one, which started earlier + * and drifts. `currentTime` is on the stream's timeline — zero at the point + * the relay started — and the page adds that start. + */ + _noteStatus(status) { + if (!status) return; + this._lastStatus = { + playerState: status.playerState || null, + idleReason: status.idleReason || null, + position: Number(status.currentTime) || 0, + at: Date.now(), + }; + } + + /** The receiver's position now: extrapolated while it plays, as its own clock does. */ + _position() { + const s = this._lastStatus; + if (!s) return null; + return s.playerState === 'PLAYING' ? s.position + (Date.now() - s.at) / 1000 : s.position; + } + + async pause() { + if (!this._player) throw new Error('Not connected'); + const status = await new Promise((resolve, reject) => { + this._player.pause((err, s) => (err ? reject(err) : resolve(s))); + }); + this._noteStatus(status); + return { playerState: status && status.playerState }; + } + + async play() { + if (!this._player) throw new Error('Not connected'); + const status = await new Promise((resolve, reject) => { + this._player.play((err, s) => (err ? reject(err) : resolve(s))); + }); + this._noteStatus(status); + return { playerState: status && status.playerState }; + } + getStatus() { + const s = this._lastStatus; return { connected: this._client !== null, deviceName: this._connectedDevice?.name || null, + playerState: s ? s.playerState : null, + idleReason: s ? s.idleReason : null, + position: this._client !== null ? this._position() : null, }; } @@ -214,6 +277,7 @@ class CastChromecast { this._client = null; this._player = null; this._connectedDevice = null; + this._lastStatus = null; } } |