diff --git a/src/components/player/display/base.ts b/src/components/player/display/base.ts index 29f7f0aa..e62c7f4c 100644 --- a/src/components/player/display/base.ts +++ b/src/components/player/display/base.ts @@ -87,6 +87,7 @@ export function makeVideoElementDisplayInterface(): DisplayInterface { let videoElement: HTMLVideoElement | null = null; let containerElement: HTMLElement | null = null; let isFullscreen = false; + let isPictureInPicture = false; let isPausedBeforeSeeking = false; let isSeeking = false; let startAt = 0; @@ -326,6 +327,16 @@ export function makeVideoElementDisplayInterface(): DisplayInterface { vid.currentTime = startAt; } + function webkitPresentationModeChange() { + if (!videoElement) return; + const webkitPlayer = videoElement as any; + const isInWebkitPip = + webkitPlayer.webkitPresentationMode === "picture-in-picture"; + isPictureInPicture = isInWebkitPip; + // Use native tracks in WebKit PiP mode for iOS compatibility + emit("needstrack", isInWebkitPip); + } + function setSource() { if (!videoElement || !source) return; setupSource(videoElement, source); @@ -406,6 +417,10 @@ export function makeVideoElementDisplayInterface(): DisplayInterface { } }, ); + videoElement.addEventListener( + "webkitpresentationmodechanged", + webkitPresentationModeChange, + ); videoElement.addEventListener("ratechange", () => { if (videoElement) emit("playbackrate", videoElement.playbackRate); }); @@ -453,6 +468,15 @@ export function makeVideoElementDisplayInterface(): DisplayInterface { } fscreen.addEventListener("fullscreenchange", fullscreenChange); + function pictureInPictureChange() { + isPictureInPicture = !!document.pictureInPictureElement; + // Use native tracks in PiP mode for better compatibility with iOS and other platforms + emit("needstrack", isPictureInPicture); + } + + document.addEventListener("enterpictureinpicture", pictureInPictureChange); + document.addEventListener("leavepictureinpicture", pictureInPictureChange); + return { on, off, @@ -462,6 +486,14 @@ export function makeVideoElementDisplayInterface(): DisplayInterface { destroy: () => { destroyVideoElement(); fscreen.removeEventListener("fullscreenchange", fullscreenChange); + document.removeEventListener( + "enterpictureinpicture", + pictureInPictureChange, + ); + document.removeEventListener( + "leavepictureinpicture", + pictureInPictureChange, + ); }, load(ops) { if (!ops.source) unloadSource(); diff --git a/src/components/player/hooks/useCaptions.ts b/src/components/player/hooks/useCaptions.ts index 0ae0e613..00a0d331 100644 --- a/src/components/player/hooks/useCaptions.ts +++ b/src/components/player/hooks/useCaptions.ts @@ -23,11 +23,13 @@ export function useCaptions() { const captionList = usePlayerStore((s) => s.captionList); const getHlsCaptionList = usePlayerStore((s) => s.display?.getCaptionList); + const source = usePlayerStore((s) => s.source); const getSubtitleTracks = usePlayerStore((s) => s.display?.getSubtitleTracks); const setSubtitlePreference = usePlayerStore( (s) => s.display?.setSubtitlePreference, ); + const setCaptionAsTrack = usePlayerStore((s) => s.setCaptionAsTrack); const captions = useMemo( () => @@ -82,6 +84,11 @@ export function useCaptions() { setCaption(captionToSet); resetSubtitleSpecificSettings(); setLanguage(caption.language); + + // Use native tracks for MP4 streams instead of custom rendering + if (source?.type === "file") { + setCaptionAsTrack(true); + } }, [ setIsOpenSubtitles, @@ -91,6 +98,8 @@ export function useCaptions() { resetSubtitleSpecificSettings, getSubtitleTracks, setSubtitlePreference, + source, + setCaptionAsTrack, ], );