diff --git a/package.json b/package.json index 6116b8a94..c3e3683a7 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "@stremio/stremio-colors": "5.2.0", "@stremio/stremio-core-web": "0.57.0", "@stremio/stremio-icons": "5.10.0", - "@stremio/stremio-video": "0.0.78", + "@stremio/stremio-video": "0.0.79", "a-color-picker": "1.2.1", "bowser": "2.14.1", "buffer": "6.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b5c29ed51..a28953bdf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,8 +24,8 @@ importers: specifier: 5.10.0 version: 5.10.0 '@stremio/stremio-video': - specifier: 0.0.78 - version: 0.0.78 + specifier: 0.0.79 + version: 0.0.79 a-color-picker: specifier: 1.2.1 version: 1.2.1 @@ -1400,8 +1400,8 @@ packages: '@stremio/stremio-icons@5.10.0': resolution: {integrity: sha512-Zw/vGC3D2yeQfk8xv/tfMJTDvbCPOI91tBg4XpR2+EgbZSX8Xvm7Vz457PIhFPhTAwdOPHp0VX0M3gzjbt0zOg==} - '@stremio/stremio-video@0.0.78': - resolution: {integrity: sha512-Kz6uj20vpyDZoFy/kEbim4l+TxyRKl9STjGqPT3cIBdpG2e+GhJuaBVkF1liL7vZvSQA8EPzLbBqwxhP92Ikvg==} + '@stremio/stremio-video@0.0.79': + resolution: {integrity: sha512-RfxmeG+rQdoflfqBybRB9vSqb3cjc3Fd5T9uNNUbwC287apQsNyjYOlvk7sfRlQ1Za/UdFsyoqR5vGBSBDzlhQ==} '@stylistic/eslint-plugin-jsx@4.4.1': resolution: {integrity: sha512-83SInq4u7z71vWwGG+6ViOtlOmZ6tSrDkMPhrvdBBTGMLA0gs22WSdhQ4vZP3oJ5Xg4ythvqeUiFSedvVxzhyA==} @@ -6450,7 +6450,7 @@ snapshots: '@stremio/stremio-icons@5.10.0': {} - '@stremio/stremio-video@0.0.78': + '@stremio/stremio-video@0.0.79': dependencies: buffer: 6.0.3 color: 4.2.3 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 55bbf4568..5e4aecccb 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'; @@ -11,35 +11,50 @@ type Props = { children: React.ReactNode, }; +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 { + } else if (document.fullscreenEnabled) { try { await document.documentElement.requestFullscreen(); } catch (err) { console.error('Error enabling fullscreen', err); } + } 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]); @@ -50,6 +65,8 @@ const FullscreenProvider = ({ children }: Props) => { onShortcut('fullscreen', toggleFullscreen, [toggleFullscreen]); useEffect(() => { + const videoElement = videoElementRef.current; + const onWindowVisibilityChanged = (state: WindowVisibility) => { setFullscreen(state.isFullscreen === true); }; @@ -58,6 +75,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(); @@ -71,19 +92,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 5ef126674..4f32e4ec6 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, }; };