feat(packages): airplay button (#1531)

This commit is contained in:
Santiago Puppo
2026-05-27 10:22:31 -07:00
committed by GitHub
parent 760870fdf2
commit 338020e1d5
50 changed files with 869 additions and 2 deletions
@@ -6,6 +6,18 @@ import { isMediaRemotePlaybackCapable } from '../../media/predicate';
import { exitFullscreen, isFullscreen } from '../../presentation/fullscreen';
import { isRemotePlaybackConnected, requestRemotePlayback } from '../../presentation/remote-playback';
/** WebKit-only addition to HTMLMediaElement exposing the active AirPlay flag. */
interface WebKitAirplayMedia extends HTMLMediaElement {
readonly webkitCurrentPlaybackTargetIsWireless: boolean;
}
/** WebKit-specific availability event payload (not in lib.dom). */
type WebkitAvailabilityEvent = Event & { availability: 'available' | 'not-available' };
function isWebKitAirplayCapable(media: EventTarget): media is WebKitAirplayMedia {
return 'WebKitPlaybackTargetAvailabilityEvent' in globalThis && 'webkitCurrentPlaybackTargetIsWireless' in media;
}
export const remotePlaybackFeature = definePlayerFeature({
name: 'remotePlayback',
state: ({ target }): MediaRemotePlaybackState => ({
@@ -23,7 +35,7 @@ export const remotePlaybackFeature = definePlayerFeature({
await exitFullscreen(media);
}
return requestRemotePlayback(media);
return await requestRemotePlayback(media);
},
}),
@@ -32,6 +44,31 @@ export const remotePlaybackFeature = definePlayerFeature({
if (!isMediaRemotePlaybackCapable(media)) return;
// Safari's W3C `media.remote` events don't fire reliably for AirPlay
// session changes. When WebKit's AirPlay APIs are available, drive both
// state slices off the WebKit events and skip the W3C listeners entirely
// so the two paths can't double-write or conflict.
if (isWebKitAirplayCapable(media)) {
const syncConnection = () => {
set({
remotePlaybackState: media.webkitCurrentPlaybackTargetIsWireless ? 'connected' : 'disconnected',
});
};
const syncAvailability = (event: Event) => {
const { availability } = event as WebkitAvailabilityEvent;
set({ remotePlaybackAvailability: availability === 'available' ? 'available' : 'unavailable' });
};
listen(media, 'webkitplaybacktargetavailabilitychanged', syncAvailability, { signal });
listen(media, 'webkitcurrentplaybacktargetiswirelesschanged', syncConnection, { signal });
// Sync initial connection state in case AirPlay was already active.
syncConnection();
return;
}
// W3C Remote Playback path (Chromium / Edge with the cast extension).
const syncState = () => set({ remotePlaybackState: media.remote.state as RemotePlaybackConnectionState });
syncState();