'use strict'; const Bonjour = require('bonjour-service').Bonjour; const CastClient = require('castv2-client').Client; const DefaultMediaReceiver = require('castv2-client').DefaultMediaReceiver; const SCAN_DURATION_MS = 6000; class CastChromecast { constructor() { this._bonjour = null; this._browser = null; this._devices = new Map(); this._client = null; this._player = null; this._connectedDevice = null; } async discover() { this._devices.clear(); if (this._browser) { this._browser.stop(); this._browser = null; } if (!this._bonjour) { this._bonjour = new Bonjour(); } console.log('[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; if (host && id) { this._devices.set(id, { id, name, host, port }); console.log(`[cast-chromecast] discovered: "${name}" at ${host}:${port}`); } }); setTimeout(() => { if (this._browser) { this._browser.stop(); this._browser = null; } const devices = Array.from(this._devices.values()); console.log(`[cast-chromecast] scan complete: ${devices.length} device(s)`); resolve(devices); }, SCAN_DURATION_MS); }); } async connect(deviceId, mediaUrl) { const device = this._devices.get(deviceId); if (!device) throw new Error(`Unknown device: ${deviceId}`); await this.disconnect(); console.log(`[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}`); this._cleanup(); }); client.connect(device.host, () => resolve()); setTimeout(() => reject(new Error('Connection timeout')), 10000); }); this._client = client; this._connectedDevice = device; const player = await new Promise((resolve, reject) => { client.launch(DefaultMediaReceiver, (err, p) => { if (err) return reject(err); resolve(p); }); }); this._player = player; player.on('status', (status) => { console.log(`[cast-chromecast] player status: ${status.playerState}`); }); const media = { contentId: mediaUrl, contentType: 'video/mp4', streamType: 'LIVE', }; const status = await new Promise((resolve, reject) => { player.load(media, { autoplay: true }, (err, s) => { if (err) return reject(err); resolve(s); }); }); console.log(`[cast-chromecast] loaded on "${device.name}", state: ${status.playerState}`); return { deviceName: device.name, playerState: status.playerState }; } async reload(mediaUrl) { if (!this._player) throw new Error('Not connected'); console.log(`[cast-chromecast] reloading stream on "${this._connectedDevice?.name}"`); const media = { contentId: mediaUrl, contentType: 'video/mp4', streamType: 'LIVE', }; const status = await new Promise((resolve, reject) => { this._player.load(media, { autoplay: true }, (err, s) => { if (err) return reject(err); resolve(s); }); }); console.log(`[cast-chromecast] reloaded, state: ${status.playerState}`); return { playerState: status.playerState }; } async disconnect() { if (this._player) { try { await new Promise((resolve) => { this._player.stop(() => resolve()); }); } catch { /* already stopped */ } } this._cleanup(); } getStatus() { return { connected: this._client !== null, deviceName: this._connectedDevice?.name || null, }; } _cleanup() { if (this._client) { try { this._client.close(); } catch { /* ignore */ } } this._client = null; this._player = null; this._connectedDevice = null; } } module.exports = CastChromecast;