diff --git a/src/common/Fullscreen/FullscreenContext.ts b/src/common/Fullscreen/FullscreenContext.ts index b2be6b3a8..a9f6e799f 100644 --- a/src/common/Fullscreen/FullscreenContext.ts +++ b/src/common/Fullscreen/FullscreenContext.ts @@ -8,6 +8,7 @@ export type FullscreenContextValue = readonly [ exitFullscreen: () => void, toggleFullscreen: () => void, supported: boolean, + setVideoElement: (el: HTMLVideoElement | null) => void, ]; const FullscreenContext = createContext(null); diff --git a/src/common/Fullscreen/FullscreenProvider.tsx b/src/common/Fullscreen/FullscreenProvider.tsx index 9a0b9f876..9bc4c26b8 100644 --- a/src/common/Fullscreen/FullscreenProvider.tsx +++ b/src/common/Fullscreen/FullscreenProvider.tsx @@ -1,6 +1,6 @@ // Copyright (C) 2017-2026 Smart code 203358507 -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { withCoreSuspender } from '../CoreSuspender'; import onShortcut from '../Shortcuts/onShortcut'; import useSettings from '../useSettings'; @@ -21,35 +21,46 @@ const isTextInputFocused = () => { activeElement.isContentEditable); }; +const hasWebkitFullscreen = typeof HTMLVideoElement !== 'undefined' && + typeof HTMLVideoElement.prototype.webkitEnterFullscreen === 'function'; + const FullscreenProvider = ({ children }: Props) => { const shell = useShell(); const [settings] = useSettings(); const escExitFullscreen = settings.escExitFullscreen; + const videoElementRef = useRef(null); + const [hasVideoElement, setHasVideoElement] = useState(false); + const [fullscreen, setFullscreen] = useState(() => { if (typeof document === 'undefined') return false; return document.fullscreenElement === document.documentElement; }); + const setVideoElement = useCallback((el: HTMLVideoElement | null) => { + videoElementRef.current = el; + setHasVideoElement(el !== null); + }, []); + + const supported = shell.active || document.fullscreenEnabled === true || (hasVideoElement && hasWebkitFullscreen); + const requestFullscreen = useCallback(async () => { if (shell.active) { shell.send('win-set-visibility', { fullscreen: true }); - } else { - try { - await document.documentElement.requestFullscreen(); - } catch (err) { - console.error('Error enabling fullscreen', err); - } + } else if (document.fullscreenEnabled) { + await document.documentElement.requestFullscreen(); + } else if (videoElementRef.current && hasWebkitFullscreen) { + (videoElementRef.current as any).webkitEnterFullscreen(); } }, [shell]); const exitFullscreen = useCallback(() => { if (shell.active) { shell.send('win-set-visibility', { fullscreen: false }); - } else { - if (document.fullscreenElement === document.documentElement) { - document.exitFullscreen(); - } + } else if (document.fullscreenElement === document.documentElement) { + document.exitFullscreen(); + } else if (videoElementRef.current && (videoElementRef.current as any).webkitDisplayingFullscreen) { + (videoElementRef.current as any).webkitExitFullscreen(); } }, [shell]); @@ -65,6 +76,8 @@ const FullscreenProvider = ({ children }: Props) => { onShortcut('fullscreen', toggleFullscreenFromShortcut, [toggleFullscreenFromShortcut]); useEffect(() => { + const videoElement = videoElementRef.current; + const onWindowVisibilityChanged = (state: WindowVisibility) => { setFullscreen(state.isFullscreen === true); }; @@ -73,6 +86,10 @@ const FullscreenProvider = ({ children }: Props) => { setFullscreen(document.fullscreenElement === document.documentElement); }; + const onWebkitFullscreenChange = () => { + setFullscreen((videoElement as any)?.webkitDisplayingFullscreen === true); + }; + const onKeyDown = (event: KeyboardEvent) => { if (event.code === 'Escape' && escExitFullscreen) { exitFullscreen(); @@ -86,19 +103,21 @@ const FullscreenProvider = ({ children }: Props) => { shell.on('win-visibility-changed', onWindowVisibilityChanged); document.addEventListener('keydown', onKeyDown); document.addEventListener('fullscreenchange', onFullscreenChange); + videoElement?.addEventListener('webkitbeginfullscreen', onWebkitFullscreenChange); + videoElement?.addEventListener('webkitendfullscreen', onWebkitFullscreenChange); return () => { shell.off('win-visibility-changed', onWindowVisibilityChanged); document.removeEventListener('keydown', onKeyDown); document.removeEventListener('fullscreenchange', onFullscreenChange); + videoElement?.removeEventListener('webkitbeginfullscreen', onWebkitFullscreenChange); + videoElement?.removeEventListener('webkitendfullscreen', onWebkitFullscreenChange); }; - }, [shell, toggleFullscreen, exitFullscreen, escExitFullscreen]); - - const supported = shell.active || document.fullscreenEnabled === true; + }, [shell, toggleFullscreen, exitFullscreen, escExitFullscreen, hasVideoElement]); const value = useMemo( - () => [fullscreen, requestFullscreen, exitFullscreen, toggleFullscreen, supported], - [fullscreen, requestFullscreen, exitFullscreen, toggleFullscreen, supported] + () => [fullscreen, requestFullscreen, exitFullscreen, toggleFullscreen, supported, setVideoElement], + [fullscreen, requestFullscreen, exitFullscreen, toggleFullscreen, supported, setVideoElement] ); return ( diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 5a76be3d9..eb2d8d3a5 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -70,7 +70,13 @@ const Player = ({ urlParams, queryParams }) => { const [immersed, setImmersed] = React.useState(true); const setImmersedDebounced = React.useCallback(debounce(setImmersed, 3000), []); - const [, , , toggleFullscreen] = useFullscreen(); + const [fullscreen, , , toggleFullscreen, , setVideoElement] = useFullscreen(); + + React.useEffect(() => { + const el = video.containerRef.current?.querySelector('video'); + setVideoElement(el || null); + return () => setVideoElement(null); + }, [video.state.manifest]); const [optionsMenuOpen, , closeOptionsMenu, toggleOptionsMenu] = useBinaryState(false); const [subtitlesMenuOpen, , closeSubtitlesMenu, toggleSubtitlesMenu] = useBinaryState(false); @@ -536,7 +542,7 @@ const Player = ({ urlParams, queryParams }) => { } }, [settings.pauseOnMinimize, shell.windowClosed, shell.windowHidden]); - useMediaSession(video.state, player, onPlayRequested, onPauseRequested, onNextVideoRequested); + useMediaSession(video.state, player, fullscreen, onPlayRequested, onPauseRequested, onNextVideoRequested); React.useEffect(() => { const onMediaKey = (action) => { diff --git a/src/routes/Player/useMediaSession.ts b/src/routes/Player/useMediaSession.ts index 4a7d4e99c..7af09bd3e 100644 --- a/src/routes/Player/useMediaSession.ts +++ b/src/routes/Player/useMediaSession.ts @@ -5,12 +5,22 @@ import { MediaStatus } from 'stremio/common/useShell'; const useMediaSession = ( videoState: VideoState, player: Player, + fullscreen: boolean, onPlayRequested: () => void, onPauseRequested: () => void, onNextVideoRequested: () => void, ) => { const shell = useShell(); + useEffect(() => { + if (!('audioSession' in navigator)) return; + const audioSession = (navigator as any).audioSession; + audioSession.type = fullscreen ? 'ambient' : 'playback'; + return () => { + audioSession.type = 'playback'; + }; + }, [fullscreen]); + // Playback state useEffect(() => { if (navigator.mediaSession) { diff --git a/src/routes/Player/useVideo.js b/src/routes/Player/useVideo.js index 241a5af00..570888ff1 100644 --- a/src/routes/Player/useVideo.js +++ b/src/routes/Player/useVideo.js @@ -40,6 +40,7 @@ const useVideo = () => { extraSubtitlesTextColor: null, extraSubtitlesBackgroundColor: null, extraSubtitlesOutlineColor: null, + fullscreen: null, }); const dispatch = (action, options) => { @@ -147,6 +148,10 @@ const useVideo = () => { setProp('videoScale', scale); }; + const setFullscreen = (state) => { + setProp('fullscreen', state); + }; + const setSubtitlesTextColor = (color) => { setProp('subtitlesTextColor', color); setProp('extraSubtitlesTextColor', color); @@ -244,6 +249,7 @@ const useVideo = () => { setSubtitlesOutlineColor, setExtraSubtitlesTrack, setVideoScale, + setFullscreen, }; };