diff --git a/package.json b/package.json index 7a1256a4..f6678488 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "dependencies": { "@headlessui/react": "^1.5.0", "crypto-js": "^4.1.1", + "fscreen": "^1.2.0", "fuse.js": "^6.4.6", "hls.js": "^1.0.7", "i18next": "^22.4.5", @@ -44,6 +45,7 @@ "devDependencies": { "@tailwindcss/line-clamp": "^0.4.2", "@types/crypto-js": "^4.1.1", + "@types/fscreen": "^1.0.1", "@types/node": "^17.0.15", "@types/react": "^17.0.39", "@types/react-dom": "^17.0.11", diff --git a/src/components/video/controls/FullscreenControl.tsx b/src/components/video/controls/FullscreenControl.tsx index 67b1e2e4..321d5665 100644 --- a/src/components/video/controls/FullscreenControl.tsx +++ b/src/components/video/controls/FullscreenControl.tsx @@ -2,8 +2,7 @@ import { Icons } from "@/components/Icon"; import { useCallback } from "react"; import { VideoPlayerIconButton } from "../parts/VideoPlayerIconButton"; import { useVideoPlayerState } from "../VideoContext"; - -const canFullscreen = document.fullscreenEnabled; +import { canFullscreen } from "../hooks/fullscreen"; interface Props { className?: string; diff --git a/src/components/video/hooks/controlVideo.ts b/src/components/video/hooks/controlVideo.ts index f4e2bdcb..33413cab 100644 --- a/src/components/video/hooks/controlVideo.ts +++ b/src/components/video/hooks/controlVideo.ts @@ -1,3 +1,6 @@ +import fscreen from "fscreen"; +import { canFullscreen, isSafari } from "./fullscreen"; + export interface PlayerControls { play(): void; pause(): void; @@ -28,12 +31,18 @@ export function populateControls( player.pause(); }, enterFullscreen() { - if (!document.fullscreenEnabled || document.fullscreenElement) return; - wrapper.requestFullscreen(); + if (!canFullscreen || fscreen.fullscreenElement) return; + if (fscreen.fullscreenEnabled) { + fscreen.requestFullscreen(wrapper); + return; + } + if (isSafari) { + (player as any).webkitEnterFullscreen(); + } }, exitFullscreen() { - if (!document.fullscreenElement) return; - document.exitFullscreen(); + if (!fscreen.fullscreenElement) return; + fscreen.exitFullscreen(); }, setTime(t) { // clamp time between 0 and max duration diff --git a/src/components/video/hooks/fullscreen.ts b/src/components/video/hooks/fullscreen.ts new file mode 100644 index 00000000..f5bd96ae --- /dev/null +++ b/src/components/video/hooks/fullscreen.ts @@ -0,0 +1,6 @@ +import fscreen from "fscreen"; + +export const isSafari = /^((?!chrome|android).)*safari/i.test( + navigator.userAgent +); +export const canFullscreen = fscreen.fullscreenEnabled || isSafari; diff --git a/src/components/video/hooks/useVideoPlayer.ts b/src/components/video/hooks/useVideoPlayer.ts index d3d2d95b..1dddf81a 100644 --- a/src/components/video/hooks/useVideoPlayer.ts +++ b/src/components/video/hooks/useVideoPlayer.ts @@ -1,3 +1,4 @@ +import fscreen from "fscreen"; import React, { MutableRefObject, useEffect, useState } from "react"; import { initialControls, @@ -108,7 +109,7 @@ function registerListeners(player: HTMLVideoElement, update: SetPlayer) { player.addEventListener("playing", playing); player.addEventListener("seeking", seeking); player.addEventListener("seeked", seeked); - document.addEventListener("fullscreenchange", fullscreenchange); + fscreen.addEventListener("fullscreenchange", fullscreenchange); player.addEventListener("timeupdate", timeupdate); player.addEventListener("loadedmetadata", loadedmetadata); player.addEventListener("volumechange", volumechange); @@ -120,7 +121,7 @@ function registerListeners(player: HTMLVideoElement, update: SetPlayer) { player.removeEventListener("playing", playing); player.removeEventListener("seeking", seeking); player.removeEventListener("seeked", seeked); - document.removeEventListener("fullscreenchange", fullscreenchange); + fscreen.removeEventListener("fullscreenchange", fullscreenchange); player.removeEventListener("timeupdate", timeupdate); player.removeEventListener("loadedmetadata", loadedmetadata); player.removeEventListener("volumechange", volumechange); diff --git a/src/views/TestView.tsx b/src/views/TestView.tsx index 5fe1db5c..503e9e3b 100644 --- a/src/views/TestView.tsx +++ b/src/views/TestView.tsx @@ -17,8 +17,7 @@ import { useCallback, useState } from "react"; // - volume control flashes old value when updating // - progress control flashes old value when updating // - captions -// - IOS support: (no volume, fullscreen video element instead of wrapper) -// - IpadOS support: (fullscreen video wrapper should work, see (lookmovie.io) ) +// - IOS & IpadOS support: (no volume) // - HLS support: feature detection otherwise use HLS.js export function TestView() { const [show, setShow] = useState(true); diff --git a/yarn.lock b/yarn.lock index a6a82c6c..9f0c1b5d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -267,6 +267,11 @@ "resolved" "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.1.1.tgz" "version" "4.1.1" +"@types/fscreen@^1.0.1": + "integrity" "sha512-hV2d0BreihMGtrg+EdAFOIl/O2EL5vhAheHJUztGE/lPFZIN8ZCpGFL8hCbtyi1CfhKjDRCf47sHjP+FwJ4q0Q==" + "resolved" "https://registry.npmjs.org/@types/fscreen/-/fscreen-1.0.1.tgz" + "version" "1.0.1" + "@types/history@^4.7.11": "integrity" "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==" "resolved" "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz" @@ -1440,6 +1445,11 @@ "resolved" "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" "version" "1.0.0" +"fscreen@^1.2.0": + "integrity" "sha512-hlq4+BU0hlPmwsFjwGGzZ+OZ9N/wq9Ljg/sq3pX+2CD7hrJsX9tJgWWK/wiNTFM212CLHWhicOoqwXyZGGetJg==" + "resolved" "https://registry.npmjs.org/fscreen/-/fscreen-1.2.0.tgz" + "version" "1.2.0" + "function-bind@^1.1.1": "integrity" "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" "resolved" "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz"