From 671170a9da33337f95abb2da0fd5361585f406d8 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Mon, 4 May 2026 21:53:44 +0300 Subject: [PATCH 1/4] feat: fullscreen support on safari --- src/common/Fullscreen/FullscreenContext.ts | 2 + src/common/Fullscreen/FullscreenProvider.tsx | 37 +- .../HorizontalNavBar/HorizontalNavBar.js | 6 +- .../NavMenu/NavMenuContent.js | 6 +- src/routes/Player/Player.js | 346 +++++++++--------- src/routes/Player/useMediaSession.ts | 10 + src/routes/Player/useVideo.js | 6 + 7 files changed, 223 insertions(+), 190 deletions(-) diff --git a/src/common/Fullscreen/FullscreenContext.ts b/src/common/Fullscreen/FullscreenContext.ts index 1c9599ffb..a9f6e799f 100644 --- a/src/common/Fullscreen/FullscreenContext.ts +++ b/src/common/Fullscreen/FullscreenContext.ts @@ -7,6 +7,8 @@ export type FullscreenContextValue = readonly [ requestFullscreen: () => Promise | void, 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 2300602c5..60814e109 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]); @@ -95,8 +106,8 @@ const FullscreenProvider = ({ children }: Props) => { }, [shell, toggleFullscreen, exitFullscreen, escExitFullscreen]); const value = useMemo( - () => [fullscreen, requestFullscreen, exitFullscreen, toggleFullscreen], - [fullscreen, requestFullscreen, exitFullscreen, toggleFullscreen] + () => [fullscreen, requestFullscreen, exitFullscreen, toggleFullscreen, supported, setVideoElement], + [fullscreen, requestFullscreen, exitFullscreen, toggleFullscreen, supported, setVideoElement] ); return ( diff --git a/src/components/NavBar/HorizontalNavBar/HorizontalNavBar.js b/src/components/NavBar/HorizontalNavBar/HorizontalNavBar.js index b1644c2b3..c9679ecf8 100644 --- a/src/components/NavBar/HorizontalNavBar/HorizontalNavBar.js +++ b/src/components/NavBar/HorizontalNavBar/HorizontalNavBar.js @@ -6,7 +6,6 @@ const classnames = require('classnames'); const { default: Icon } = require('@stremio/stremio-icons/react'); const { Button, Image } = require('stremio/components'); const { useFullscreen } = require('stremio/common/Fullscreen'); -const usePWA = require('stremio/common/usePWA'); const { useHorizontalNavGamepadNavigation } = require('stremio/services/GamepadNavigation'); const SearchBar = require('./SearchBar'); const NavMenu = require('./NavMenu'); @@ -17,8 +16,7 @@ const HorizontalNavBar = React.memo(({ className, route, query, title, backButto const backButtonOnClick = React.useCallback(() => { window.history.back(); }, []); - const [fullscreen, requestFullscreen, exitFullscreen] = useFullscreen(); - const [isIOSPWA] = usePWA(); + const [fullscreen, requestFullscreen, exitFullscreen, , supported] = useFullscreen(); const renderNavMenuLabel = React.useCallback(({ ref, className, onClick, children, }) => ( diff --git a/src/components/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js b/src/components/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js index 6615e5b76..e35696584 100644 --- a/src/components/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js +++ b/src/components/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js @@ -23,8 +23,8 @@ const NavMenuContent = ({ onClick }) => { const streamingServer = useStreamingServer(); const { handlePlayUrl } = usePlayUrl(); const toast = useToast(); - const [fullscreen, requestFullscreen, exitFullscreen] = useFullscreen(); - const [isIOSPWA, isAndroidPWA] = usePWA(); + const [fullscreen, requestFullscreen, exitFullscreen, , supported] = useFullscreen(); + const [, isAndroidPWA] = usePWA(); const streamingServerWarningDismissed = React.useMemo(() => { return streamingServer.settings !== null && streamingServer.settings.type === 'Ready' || ( !isNaN(profile.settings.streamingServerWarningDismissed.getTime()) && @@ -79,7 +79,7 @@ const NavMenuContent = ({ onClick }) => { { - !isIOSPWA && !isAndroidPWA ? + supported && !isAndroidPWA ?