diff --git a/.changeset/many-toys-run.md b/.changeset/many-toys-run.md new file mode 100644 index 00000000..507eb85c --- /dev/null +++ b/.changeset/many-toys-run.md @@ -0,0 +1,15 @@ +--- +"partytracks": patch +--- + +Fix Safari compatibility by adding setSinkId feature detection to createAudioSink + +The createAudioSink utility now gracefully handles browsers that don't support the setSinkId API (primarily Safari on mobile and desktop). + +- Added checkSinkIdSupport helper to detect setSinkId availability +- Added isSinkIdSupported property to SinkApi interface +- Wrapped setSinkId calls with feature detection to prevent crashes +- Audio now plays through default output on unsupported browsers with a helpful console warning +- Applications can check audioSink.isSinkIdSupported to conditionally render device selection UI + +This is a backward-compatible change that fixes crashes reported in #276. diff --git a/packages/partytracks/src/client/audioSink.ts b/packages/partytracks/src/client/audioSink.ts index 5ab679a4..0cc19918 100644 --- a/packages/partytracks/src/client/audioSink.ts +++ b/packages/partytracks/src/client/audioSink.ts @@ -7,18 +7,28 @@ export interface CreateSinkOptions { sinkId?: string; } +const checkSinkIdSupport = (element: HTMLAudioElement): boolean => { + return "setSinkId" in element && typeof element.setSinkId === "function"; +}; + export interface SinkApi { attach: (pulledAudioTrack$: Observable) => Subscription; setSinkId: (sinkId: string) => void; devices$: Observable; cleanup: () => void; + isSinkIdSupported: boolean; } export const createAudioSink = ({ audioElement, sinkId = "default" }: CreateSinkOptions): SinkApi => { - audioElement.setSinkId(sinkId); + const isSinkIdSupported = checkSinkIdSupport(audioElement); + + if (isSinkIdSupported) { + audioElement.setSinkId(sinkId); + } + const sinkId$ = new BehaviorSubject(sinkId); const mediaStream = new MediaStream(); audioElement.srcObject = mediaStream; @@ -44,8 +54,14 @@ export const createAudioSink = ({ }; const setSinkId = (sinkId: string) => { - audioElement.setSinkId(sinkId); - sinkId$.next(sinkId); + if (isSinkIdSupported) { + audioElement.setSinkId(sinkId); + sinkId$.next(sinkId); + } else { + console.warn( + "setSinkId is not supported on this browser. Audio will play through the default output device." + ); + } }; const cleanup = () => { @@ -59,6 +75,7 @@ export const createAudioSink = ({ devices$: devices$.pipe( map((devices) => devices.filter((d) => d.kind === "audiooutput")) ), - cleanup + cleanup, + isSinkIdSupported }; };