diff --git a/front/apps/web/next.config.js b/front/apps/web/next.config.js index a97614aa..1b94df99 100755 --- a/front/apps/web/next.config.js +++ b/front/apps/web/next.config.js @@ -29,7 +29,8 @@ const suboctopus = path.resolve(path.dirname(require.resolve("jassub")), "../dis */ const nextConfig = { swcMinify: true, - reactStrictMode: true, + // can't be true since we would run hls cleanup twice and run on race conditions + reactStrictMode: false, output: "standalone", webpack: (config) => { config.plugins = [ diff --git a/front/packages/ui/src/player/state.tsx b/front/packages/ui/src/player/state.tsx index 9285fa34..0c7f388a 100644 --- a/front/packages/ui/src/player/state.tsx +++ b/front/packages/ui/src/player/state.tsx @@ -33,7 +33,7 @@ import { } from "react"; import { useTranslation } from "react-i18next"; import { Platform } from "react-native"; -import NativeVideo, { type VideoProps } from "./video"; +import NativeVideo, { canPlay, type VideoProps } from "./video"; export const playAtom = atom(true); export const loadAtom = atom(false); @@ -134,14 +134,12 @@ export const Video = memo(function Video({ }, [publicProgress]); const getProgress = useAtomCallback(useCallback((get) => get(progressAtom), [])); - const oldLinks = useRef(null); useEffect(() => { // Reset the state when a new video is loaded. let newMode = getLocalSetting("playmode", "direct") !== "auto" ? PlayMode.Direct : PlayMode.Hls; // Only allow direct play if the device supports it - console.log(codec, ref.current, ref.current?.canPlay?.(codec!)) - if (newMode === PlayMode.Direct && codec && ref.current?.canPlay?.(codec) === false) { + if (newMode === PlayMode.Direct && codec && !canPlay(codec)) { console.log(`Browser can't natively play ${codec}, switching to hls stream.`); newMode = PlayMode.Hls; } @@ -149,25 +147,18 @@ export const Video = memo(function Video({ setSource((newMode === PlayMode.Direct ? links?.direct : links?.hls) ?? null); setLoad(true); - if (oldLinks.current !== links) { - setPrivateProgress(startTime.current ?? 0); - setPublicProgress(startTime.current ?? 0); - } else { - // keep current time when changing between direct and hls. - startTime.current = getProgress(); - } - oldLinks.current = links; + setPrivateProgress(startTime.current ?? 0); + setPublicProgress(startTime.current ?? 0); setPlay(true); - }, [ - links, - codec, - setLoad, - setPrivateProgress, - setPublicProgress, - setPlay, - getProgress, - setPlayMode, - ]); + }, [links, codec, setLoad, setPrivateProgress, setPublicProgress, setPlay, setPlayMode]); + + // biome-ignore lint/correctness/useExhaustiveDependencies: do not change source when links change, this is done above + useEffect(() => { + setSource((mode === PlayMode.Direct ? links?.direct : links?.hls) ?? null); + // keep current time when changing between direct and hls. + startTime.current = getProgress(); + setPlay(true); + }, [mode, getProgress, setPlay]); const account = useAccount(); const defaultSubLanguage = account?.settings.subtitleLanguage; diff --git a/front/packages/ui/src/player/video.tsx b/front/packages/ui/src/player/video.tsx index ed8e7567..eb0d3139 100644 --- a/front/packages/ui/src/player/video.tsx +++ b/front/packages/ui/src/player/video.tsx @@ -29,10 +29,6 @@ declare module "react-native-video" { export type VideoProps = Omit & { source: { uri: string; hls: string | null; startPosition?: number }; }; - - interface VideoRef { - canPlay?: (codec: string) => boolean; - } } export * from "react-native-video"; @@ -133,6 +129,9 @@ const Video = forwardRef(function Video( export default Video; +// mobile should be able to play everything +export const canPlay = (codec: string) => true; + type CustomMenu = ComponentProps>>; export const AudiosMenu = ({ audios, ...props }: CustomMenu & { audios?: Audio[] }) => { const info = useAtomValue(infoAtom); diff --git a/front/packages/ui/src/player/video.web.tsx b/front/packages/ui/src/player/video.web.tsx index 9f7106dd..9be8f5a2 100644 --- a/front/packages/ui/src/player/video.web.tsx +++ b/front/packages/ui/src/player/video.web.tsx @@ -50,7 +50,7 @@ function uuidv4(): string { const client_id = typeof window === "undefined" ? "ssr" : uuidv4(); const initHls = (): Hls => { - if (hls !== null) return hls; + if (hls) hls.destroy(); const loadPolicy: LoadPolicy = { default: { maxTimeToFirstByteMs: Number.POSITIVE_INFINITY, @@ -104,10 +104,7 @@ const initHls = (): Hls => { return hls; }; -const Video = forwardRef< - { seek: (value: number) => void; canPlay: (codec: string) => boolean }, - VideoProps ->(function Video( +const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function Video( { source, paused, @@ -161,8 +158,6 @@ const Video = forwardRef< if (!ref?.current || !source.uri) return; if (!hls || oldHls.current !== source.hls) { // Reinit the hls player when we change track. - if (hls) hls.destroy(); - hls = null; hls = initHls(); hls.loadSource(source.hls!); oldHls.current = source.hls; @@ -181,11 +176,15 @@ const Video = forwardRef< }); }); } + }, [source.uri, source.hls]); + + useEffect(() => { return () => { + console.log("hls cleanup") if (hls) hls.destroy(); hls = null; }; - }, [source.uri, source.hls]); + }, []); const mode = useAtomValue(playModeAtom); const audio = useAtomValue(audioAtom); @@ -256,6 +255,12 @@ const Video = forwardRef< export default Video; +export const canPlay = (codec: string) => { + const videos = document.getElementsByTagName("video"); + const video = videos.item(0) ?? document.createElement("video"); + return !!video.canPlayType(codec); +}; + const useSubtitle = ( player: RefObject, value: Subtitle | null, @@ -385,11 +390,12 @@ export const QualitiesMenu = (props: ComponentProps) => { const [mode, setPlayMode] = useAtom(playModeAtom); const rerender = useForceRerender(); + // biome-ignore lint/correctness/useExhaustiveDependencies: Inculde hls in dependency array useEffect(() => { if (!hls) return; hls.on(Hls.Events.LEVEL_SWITCHED, rerender); return () => hls?.off(Hls.Events.LEVEL_SWITCHED, rerender); - }); + }, [hls]); const levelName = (label: Level, auto?: boolean): string => { const height = `${label.height}p`; diff --git a/front/packages/ui/src/player/watch-status-observer.tsx b/front/packages/ui/src/player/watch-status-observer.tsx index 08b4d4d8..ce52ce18 100644 --- a/front/packages/ui/src/player/watch-status-observer.tsx +++ b/front/packages/ui/src/player/watch-status-observer.tsx @@ -42,7 +42,8 @@ export const WatchStatusObserver = ({ await queryClient.invalidateQueries({ queryKey: [type === "episode" ? "show" : type, slug] }), }); const mutate = useCallback( - (type: string, slug: string, seconds: number) => + (type: string, slug: string, seconds: number) => { + if (seconds < 0 || duration <= 0) return; _mutate({ method: "POST", path: [type, slug, "watchStatus"], @@ -51,7 +52,8 @@ export const WatchStatusObserver = ({ watchedTime: Math.round(seconds), percent: Math.round((seconds / duration) * 100), }, - }), + }); + }, [_mutate, duration], ); const readProgress = useAtomCallback(