mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-05-13 10:11:35 +00:00
Merge pull request #1260 from Stremio/feat/support-safari-fullscreen-video-mode
Some checks are pending
Build / build (push) Waiting to run
Some checks are pending
Build / build (push) Waiting to run
Player: Add fullscreen support on safari
This commit is contained in:
commit
90bd2c9c3b
7 changed files with 65 additions and 19 deletions
|
|
@ -19,7 +19,7 @@
|
||||||
"@stremio/stremio-colors": "5.2.0",
|
"@stremio/stremio-colors": "5.2.0",
|
||||||
"@stremio/stremio-core-web": "0.57.0",
|
"@stremio/stremio-core-web": "0.57.0",
|
||||||
"@stremio/stremio-icons": "5.10.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",
|
"a-color-picker": "1.2.1",
|
||||||
"bowser": "2.14.1",
|
"bowser": "2.14.1",
|
||||||
"buffer": "6.0.3",
|
"buffer": "6.0.3",
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,8 @@ importers:
|
||||||
specifier: 5.10.0
|
specifier: 5.10.0
|
||||||
version: 5.10.0
|
version: 5.10.0
|
||||||
'@stremio/stremio-video':
|
'@stremio/stremio-video':
|
||||||
specifier: 0.0.78
|
specifier: 0.0.79
|
||||||
version: 0.0.78
|
version: 0.0.79
|
||||||
a-color-picker:
|
a-color-picker:
|
||||||
specifier: 1.2.1
|
specifier: 1.2.1
|
||||||
version: 1.2.1
|
version: 1.2.1
|
||||||
|
|
@ -1400,8 +1400,8 @@ packages:
|
||||||
'@stremio/stremio-icons@5.10.0':
|
'@stremio/stremio-icons@5.10.0':
|
||||||
resolution: {integrity: sha512-Zw/vGC3D2yeQfk8xv/tfMJTDvbCPOI91tBg4XpR2+EgbZSX8Xvm7Vz457PIhFPhTAwdOPHp0VX0M3gzjbt0zOg==}
|
resolution: {integrity: sha512-Zw/vGC3D2yeQfk8xv/tfMJTDvbCPOI91tBg4XpR2+EgbZSX8Xvm7Vz457PIhFPhTAwdOPHp0VX0M3gzjbt0zOg==}
|
||||||
|
|
||||||
'@stremio/stremio-video@0.0.78':
|
'@stremio/stremio-video@0.0.79':
|
||||||
resolution: {integrity: sha512-Kz6uj20vpyDZoFy/kEbim4l+TxyRKl9STjGqPT3cIBdpG2e+GhJuaBVkF1liL7vZvSQA8EPzLbBqwxhP92Ikvg==}
|
resolution: {integrity: sha512-RfxmeG+rQdoflfqBybRB9vSqb3cjc3Fd5T9uNNUbwC287apQsNyjYOlvk7sfRlQ1Za/UdFsyoqR5vGBSBDzlhQ==}
|
||||||
|
|
||||||
'@stylistic/eslint-plugin-jsx@4.4.1':
|
'@stylistic/eslint-plugin-jsx@4.4.1':
|
||||||
resolution: {integrity: sha512-83SInq4u7z71vWwGG+6ViOtlOmZ6tSrDkMPhrvdBBTGMLA0gs22WSdhQ4vZP3oJ5Xg4ythvqeUiFSedvVxzhyA==}
|
resolution: {integrity: sha512-83SInq4u7z71vWwGG+6ViOtlOmZ6tSrDkMPhrvdBBTGMLA0gs22WSdhQ4vZP3oJ5Xg4ythvqeUiFSedvVxzhyA==}
|
||||||
|
|
@ -6450,7 +6450,7 @@ snapshots:
|
||||||
|
|
||||||
'@stremio/stremio-icons@5.10.0': {}
|
'@stremio/stremio-icons@5.10.0': {}
|
||||||
|
|
||||||
'@stremio/stremio-video@0.0.78':
|
'@stremio/stremio-video@0.0.79':
|
||||||
dependencies:
|
dependencies:
|
||||||
buffer: 6.0.3
|
buffer: 6.0.3
|
||||||
color: 4.2.3
|
color: 4.2.3
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ export type FullscreenContextValue = readonly [
|
||||||
exitFullscreen: () => void,
|
exitFullscreen: () => void,
|
||||||
toggleFullscreen: () => void,
|
toggleFullscreen: () => void,
|
||||||
supported: boolean,
|
supported: boolean,
|
||||||
|
setVideoElement: (el: HTMLVideoElement | null) => void,
|
||||||
];
|
];
|
||||||
|
|
||||||
const FullscreenContext = createContext<FullscreenContextValue | null>(null);
|
const FullscreenContext = createContext<FullscreenContextValue | null>(null);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// Copyright (C) 2017-2026 Smart code 203358507
|
// 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 { withCoreSuspender } from '../CoreSuspender';
|
||||||
import onShortcut from '../Shortcuts/onShortcut';
|
import onShortcut from '../Shortcuts/onShortcut';
|
||||||
import useSettings from '../useSettings';
|
import useSettings from '../useSettings';
|
||||||
|
|
@ -11,35 +11,50 @@ type Props = {
|
||||||
children: React.ReactNode,
|
children: React.ReactNode,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hasWebkitFullscreen = typeof HTMLVideoElement !== 'undefined' &&
|
||||||
|
typeof HTMLVideoElement.prototype.webkitEnterFullscreen === 'function';
|
||||||
|
|
||||||
const FullscreenProvider = ({ children }: Props) => {
|
const FullscreenProvider = ({ children }: Props) => {
|
||||||
const shell = useShell();
|
const shell = useShell();
|
||||||
const [settings] = useSettings();
|
const [settings] = useSettings();
|
||||||
const escExitFullscreen = settings.escExitFullscreen;
|
const escExitFullscreen = settings.escExitFullscreen;
|
||||||
|
|
||||||
|
const videoElementRef = useRef<HTMLVideoElement | null>(null);
|
||||||
|
const [hasVideoElement, setHasVideoElement] = useState(false);
|
||||||
|
|
||||||
const [fullscreen, setFullscreen] = useState<boolean>(() => {
|
const [fullscreen, setFullscreen] = useState<boolean>(() => {
|
||||||
if (typeof document === 'undefined') return false;
|
if (typeof document === 'undefined') return false;
|
||||||
return document.fullscreenElement === document.documentElement;
|
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 () => {
|
const requestFullscreen = useCallback(async () => {
|
||||||
if (shell.active) {
|
if (shell.active) {
|
||||||
shell.send('win-set-visibility', { fullscreen: true });
|
shell.send('win-set-visibility', { fullscreen: true });
|
||||||
} else {
|
} else if (document.fullscreenEnabled) {
|
||||||
try {
|
try {
|
||||||
await document.documentElement.requestFullscreen();
|
await document.documentElement.requestFullscreen();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error enabling fullscreen', err);
|
console.error('Error enabling fullscreen', err);
|
||||||
}
|
}
|
||||||
|
} else if (videoElementRef.current && hasWebkitFullscreen) {
|
||||||
|
(videoElementRef.current as any).webkitEnterFullscreen();
|
||||||
}
|
}
|
||||||
}, [shell]);
|
}, [shell]);
|
||||||
|
|
||||||
const exitFullscreen = useCallback(() => {
|
const exitFullscreen = useCallback(() => {
|
||||||
if (shell.active) {
|
if (shell.active) {
|
||||||
shell.send('win-set-visibility', { fullscreen: false });
|
shell.send('win-set-visibility', { fullscreen: false });
|
||||||
} else {
|
} else if (document.fullscreenElement === document.documentElement) {
|
||||||
if (document.fullscreenElement === document.documentElement) {
|
document.exitFullscreen();
|
||||||
document.exitFullscreen();
|
} else if (videoElementRef.current && (videoElementRef.current as any).webkitDisplayingFullscreen) {
|
||||||
}
|
(videoElementRef.current as any).webkitExitFullscreen();
|
||||||
}
|
}
|
||||||
}, [shell]);
|
}, [shell]);
|
||||||
|
|
||||||
|
|
@ -50,6 +65,8 @@ const FullscreenProvider = ({ children }: Props) => {
|
||||||
onShortcut('fullscreen', toggleFullscreen, [toggleFullscreen]);
|
onShortcut('fullscreen', toggleFullscreen, [toggleFullscreen]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const videoElement = videoElementRef.current;
|
||||||
|
|
||||||
const onWindowVisibilityChanged = (state: WindowVisibility) => {
|
const onWindowVisibilityChanged = (state: WindowVisibility) => {
|
||||||
setFullscreen(state.isFullscreen === true);
|
setFullscreen(state.isFullscreen === true);
|
||||||
};
|
};
|
||||||
|
|
@ -58,6 +75,10 @@ const FullscreenProvider = ({ children }: Props) => {
|
||||||
setFullscreen(document.fullscreenElement === document.documentElement);
|
setFullscreen(document.fullscreenElement === document.documentElement);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onWebkitFullscreenChange = () => {
|
||||||
|
setFullscreen((videoElement as any)?.webkitDisplayingFullscreen === true);
|
||||||
|
};
|
||||||
|
|
||||||
const onKeyDown = (event: KeyboardEvent) => {
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.code === 'Escape' && escExitFullscreen) {
|
if (event.code === 'Escape' && escExitFullscreen) {
|
||||||
exitFullscreen();
|
exitFullscreen();
|
||||||
|
|
@ -71,19 +92,21 @@ const FullscreenProvider = ({ children }: Props) => {
|
||||||
shell.on('win-visibility-changed', onWindowVisibilityChanged);
|
shell.on('win-visibility-changed', onWindowVisibilityChanged);
|
||||||
document.addEventListener('keydown', onKeyDown);
|
document.addEventListener('keydown', onKeyDown);
|
||||||
document.addEventListener('fullscreenchange', onFullscreenChange);
|
document.addEventListener('fullscreenchange', onFullscreenChange);
|
||||||
|
videoElement?.addEventListener('webkitbeginfullscreen', onWebkitFullscreenChange);
|
||||||
|
videoElement?.addEventListener('webkitendfullscreen', onWebkitFullscreenChange);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
shell.off('win-visibility-changed', onWindowVisibilityChanged);
|
shell.off('win-visibility-changed', onWindowVisibilityChanged);
|
||||||
document.removeEventListener('keydown', onKeyDown);
|
document.removeEventListener('keydown', onKeyDown);
|
||||||
document.removeEventListener('fullscreenchange', onFullscreenChange);
|
document.removeEventListener('fullscreenchange', onFullscreenChange);
|
||||||
|
videoElement?.removeEventListener('webkitbeginfullscreen', onWebkitFullscreenChange);
|
||||||
|
videoElement?.removeEventListener('webkitendfullscreen', onWebkitFullscreenChange);
|
||||||
};
|
};
|
||||||
}, [shell, toggleFullscreen, exitFullscreen, escExitFullscreen]);
|
}, [shell, toggleFullscreen, exitFullscreen, escExitFullscreen, hasVideoElement]);
|
||||||
|
|
||||||
const supported = shell.active || document.fullscreenEnabled === true;
|
|
||||||
|
|
||||||
const value = useMemo<FullscreenContextValue>(
|
const value = useMemo<FullscreenContextValue>(
|
||||||
() => [fullscreen, requestFullscreen, exitFullscreen, toggleFullscreen, supported],
|
() => [fullscreen, requestFullscreen, exitFullscreen, toggleFullscreen, supported, setVideoElement],
|
||||||
[fullscreen, requestFullscreen, exitFullscreen, toggleFullscreen, supported]
|
[fullscreen, requestFullscreen, exitFullscreen, toggleFullscreen, supported, setVideoElement]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,13 @@ const Player = ({ urlParams, queryParams }) => {
|
||||||
|
|
||||||
const [immersed, setImmersed] = React.useState(true);
|
const [immersed, setImmersed] = React.useState(true);
|
||||||
const setImmersedDebounced = React.useCallback(debounce(setImmersed, 3000), []);
|
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 [optionsMenuOpen, , closeOptionsMenu, toggleOptionsMenu] = useBinaryState(false);
|
||||||
const [subtitlesMenuOpen, , closeSubtitlesMenu, toggleSubtitlesMenu] = useBinaryState(false);
|
const [subtitlesMenuOpen, , closeSubtitlesMenu, toggleSubtitlesMenu] = useBinaryState(false);
|
||||||
|
|
@ -536,7 +542,7 @@ const Player = ({ urlParams, queryParams }) => {
|
||||||
}
|
}
|
||||||
}, [settings.pauseOnMinimize, shell.windowClosed, shell.windowHidden]);
|
}, [settings.pauseOnMinimize, shell.windowClosed, shell.windowHidden]);
|
||||||
|
|
||||||
useMediaSession(video.state, player, onPlayRequested, onPauseRequested, onNextVideoRequested);
|
useMediaSession(video.state, player, fullscreen, onPlayRequested, onPauseRequested, onNextVideoRequested);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const onMediaKey = (action) => {
|
const onMediaKey = (action) => {
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,22 @@ import { MediaStatus } from 'stremio/common/useShell';
|
||||||
const useMediaSession = (
|
const useMediaSession = (
|
||||||
videoState: VideoState,
|
videoState: VideoState,
|
||||||
player: Player,
|
player: Player,
|
||||||
|
fullscreen: boolean,
|
||||||
onPlayRequested: () => void,
|
onPlayRequested: () => void,
|
||||||
onPauseRequested: () => void,
|
onPauseRequested: () => void,
|
||||||
onNextVideoRequested: () => void,
|
onNextVideoRequested: () => void,
|
||||||
) => {
|
) => {
|
||||||
const shell = useShell();
|
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
|
// Playback state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (navigator.mediaSession) {
|
if (navigator.mediaSession) {
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ const useVideo = () => {
|
||||||
extraSubtitlesTextColor: null,
|
extraSubtitlesTextColor: null,
|
||||||
extraSubtitlesBackgroundColor: null,
|
extraSubtitlesBackgroundColor: null,
|
||||||
extraSubtitlesOutlineColor: null,
|
extraSubtitlesOutlineColor: null,
|
||||||
|
fullscreen: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const dispatch = (action, options) => {
|
const dispatch = (action, options) => {
|
||||||
|
|
@ -147,6 +148,10 @@ const useVideo = () => {
|
||||||
setProp('videoScale', scale);
|
setProp('videoScale', scale);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setFullscreen = (state) => {
|
||||||
|
setProp('fullscreen', state);
|
||||||
|
};
|
||||||
|
|
||||||
const setSubtitlesTextColor = (color) => {
|
const setSubtitlesTextColor = (color) => {
|
||||||
setProp('subtitlesTextColor', color);
|
setProp('subtitlesTextColor', color);
|
||||||
setProp('extraSubtitlesTextColor', color);
|
setProp('extraSubtitlesTextColor', color);
|
||||||
|
|
@ -244,6 +249,7 @@ const useVideo = () => {
|
||||||
setSubtitlesOutlineColor,
|
setSubtitlesOutlineColor,
|
||||||
setExtraSubtitlesTrack,
|
setExtraSubtitlesTrack,
|
||||||
setVideoScale,
|
setVideoScale,
|
||||||
|
setFullscreen,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue