/** * Audio-route detection. * * AirPlay is not Cast: it is an audio session route. The local engine keeps * playing and keeps owning transport — the sound simply comes out somewhere * else. Nothing here installs an output or takes commands away from the engine, * which is exactly what distinguishes it from a Cast session. * * Detection is structural: decisions read `portType`, never the localized * port name. */ import type { AudioSession } from '../specs/AudioSession.nitro'; import type { OutputPresence } from './outputPresence'; type CallbackGuard = () => boolean; export interface RouteWiring { wireAirPlayDetection( session: AudioSession, isCurrentEvent?: CallbackGuard ): void; } export function createRouteWiring(presence: OutputPresence): RouteWiring { let onAirPlay = false; return { wireAirPlayDetection( session: AudioSession, isCurrentEvent: CallbackGuard = () => true ): void { session.onRouteChange((event) => { if (!isCurrentEvent()) return; // Only on genuine transitions: route changes fire for reasons that // have nothing to do with AirPlay, and republishing on each would // churn every useCastState subscriber. if ( event.reason === 'newDeviceAvailable' && event.output?.portType === 'airplay' ) { if (onAirPlay) return; onAirPlay = true; presence.setRoute({ name: event.output.name ?? 'AirPlay', type: 'airplay', }); } else if ( event.reason === 'oldDeviceUnavailable' && event.removedOutput?.portType === 'airplay' ) { if (!onAirPlay) return; onAirPlay = false; presence.setRoute(undefined); } }); }, }; }