/** * Player-scoped registry of alternate playback outputs. * * Core knows only that an output can take playback over and hand it back. It * has no idea what a Cast session is, what a receiver queue looks like, or how * content gets transferred — that all lives in the package that owns the * output. This module is the whole of core's side of that boundary. */ import { noopLogger, type AviationLogger } from '../logger'; import type { AviationStore } from '../store'; import type { PlaybackEngine } from '../specs/PlaybackEngine.nitro'; import type { PlaybackOutputHandle } from '../ports/PlaybackOutput'; import type { OutputPresence } from './outputPresence'; export interface OutputWiring { registerOutput( name: string, engine: PlaybackEngine | null ): PlaybackOutputHandle; resetState(): void; } export function createOutputWiring( store: AviationStore, presence: OutputPresence, log: AviationLogger = noopLogger ): OutputWiring { let generation = 0; let claimedBy: number | null = null; return { registerOutput(name, engine): PlaybackOutputHandle { generation++; const id = generation; // Every method is generation-guarded: a handle whose output has been // superseded or disposed must not publish over its replacement. const isCurrent = () => id === generation; return { claim(device): void { if (!isCurrent()) return; log.info('output', 'claimed playback', { name, device: device.name }); claimedBy = id; presence.setSession(device); }, release(): void { if (!isCurrent() || claimedBy !== id) return; log.info('output', 'released playback', { name }); claimedBy = null; presence.setSession(undefined); }, publishQueueIndex(index): void { if (!isCurrent() || claimedBy !== id || !engine) return; store.setCurrentItem(engine.currentItem, 'queue', index); }, dispose(): void { if (!isCurrent()) return; if (claimedBy === id) { claimedBy = null; presence.setSession(undefined); } generation++; }, }; }, resetState(): void { generation++; claimedBy = null; presence.setSession(undefined); }, }; }