From 00066ba788f36e51838053462f8a64ad36586636 Mon Sep 17 00:00:00 2001 From: qtchaos <72168435+qtchaos@users.noreply.github.com> Date: Fri, 5 Jan 2024 21:19:59 +0200 Subject: [PATCH 1/5] somewhat ok mediasession implementation --- src/components/player/base/Container.tsx | 2 + .../player/internals/MediaSession.ts | 178 ++++++++++++++++++ 2 files changed, 180 insertions(+) create mode 100644 src/components/player/internals/MediaSession.ts diff --git a/src/components/player/base/Container.tsx b/src/components/player/base/Container.tsx index 9fc10bff..2a68ae31 100644 --- a/src/components/player/base/Container.tsx +++ b/src/components/player/base/Container.tsx @@ -4,6 +4,7 @@ import { OverlayDisplay } from "@/components/overlays/OverlayDisplay"; import { CastingInternal } from "@/components/player/internals/CastingInternal"; import { HeadUpdater } from "@/components/player/internals/HeadUpdater"; import { KeyboardEvents } from "@/components/player/internals/KeyboardEvents"; +import { MediaSession } from "@/components/player/internals/MediaSession"; import { MetaReporter } from "@/components/player/internals/MetaReporter"; import { ProgressSaver } from "@/components/player/internals/ProgressSaver"; import { ThumbnailScraper } from "@/components/player/internals/ThumbnailScraper"; @@ -91,6 +92,7 @@ export function Container(props: PlayerProps) { +
diff --git a/src/components/player/internals/MediaSession.ts b/src/components/player/internals/MediaSession.ts new file mode 100644 index 00000000..f1be96e7 --- /dev/null +++ b/src/components/player/internals/MediaSession.ts @@ -0,0 +1,178 @@ +import { useCallback, useEffect, useRef } from "react"; + +import { usePlayerStore } from "@/stores/player/store"; + +import { usePlayerMeta } from "../hooks/usePlayerMeta"; + +export function MediaSession() { + const display = usePlayerStore((s) => s.display); + const mediaPlaying = usePlayerStore((s) => s.mediaPlaying); + const meta = usePlayerStore((s) => s.meta); + const progress = usePlayerStore((s) => s.progress); + const { setDirectMeta } = usePlayerMeta(); + const setShouldStartFromBeginning = usePlayerStore( + (s) => s.setShouldStartFromBeginning, + ); + + const shouldUpdatePositionState = useRef(false); + const lastPlaybackPosition = useRef(0); + + const dataRef = useRef({ + display, + mediaPlaying, + progress, + meta, + }); + + useEffect(() => { + dataRef.current = { + display, + mediaPlaying, + progress, + meta, + }; + }, [display, mediaPlaying, progress, meta]); + + const changeEpisode = useCallback( + (change: number) => { + const nextEp = meta?.episodes?.find( + (v) => v.number === (meta?.episode?.number ?? 0) + change, + ); + + if (!meta || !nextEp) return; + const metaCopy = { ...meta }; + metaCopy.episode = nextEp; + setShouldStartFromBeginning(true); + setDirectMeta(metaCopy); + }, + [setDirectMeta, meta, setShouldStartFromBeginning], + ); + + const updatePositionState = useCallback((position: number) => { + // If the updated position needs to be buffered, queue an update + if (position > dataRef.current.progress.buffered) { + shouldUpdatePositionState.current = true; + } + if (position > dataRef.current.progress.duration) return; + + lastPlaybackPosition.current = dataRef.current.progress.time; + navigator.mediaSession.setPositionState({ + duration: dataRef.current.progress.duration, + playbackRate: dataRef.current.mediaPlaying.playbackRate, + position, + }); + }, []); + + useEffect(() => { + if (!("mediaSession" in navigator)) return; + + // If not already updating the position state, and the media is loading, queue an update + if ( + !shouldUpdatePositionState.current && + dataRef.current.mediaPlaying.isLoading + ) { + shouldUpdatePositionState.current = true; + } + + // If the user has skipped (or MediaSession desynced) by more than 5 seconds, queue an update + if ( + Math.abs(dataRef.current.progress.time - lastPlaybackPosition.current) > + 5 && + !dataRef.current.mediaPlaying.isLoading && + !shouldUpdatePositionState.current + ) { + shouldUpdatePositionState.current = true; + } + + // If not loading and the position state is queued, update it + if ( + shouldUpdatePositionState.current && + !dataRef.current.mediaPlaying.isLoading + ) { + shouldUpdatePositionState.current = false; + updatePositionState(dataRef.current.progress.time); + } + + lastPlaybackPosition.current = dataRef.current.progress.time; + navigator.mediaSession.playbackState = dataRef.current.mediaPlaying + .isPlaying + ? "playing" + : "paused"; + + navigator.mediaSession.metadata = new MediaMetadata({ + title: dataRef.current.meta?.episode?.title, + artist: dataRef.current.meta?.title, + artwork: [ + { + src: dataRef.current.meta?.poster ?? "", + sizes: "342x513", + type: "image/png", + }, + ], + }); + + navigator.mediaSession.setActionHandler("play", () => { + if (dataRef.current.mediaPlaying.isLoading) return; + dataRef.current.display?.play(); + + navigator.mediaSession.playbackState = "playing"; + updatePositionState(dataRef.current.progress.time); + }); + + navigator.mediaSession.setActionHandler("pause", () => { + if (dataRef.current.mediaPlaying.isLoading) return; + dataRef.current.display?.pause(); + + navigator.mediaSession.playbackState = "paused"; + updatePositionState(dataRef.current.progress.time); + }); + + navigator.mediaSession.setActionHandler("seekbackward", (evt) => { + const skipTime = evt.seekOffset ?? 10; + dataRef.current.display?.setTime( + dataRef.current.progress.time - skipTime, + ); + updatePositionState(dataRef.current.progress.time - skipTime); + }); + + navigator.mediaSession.setActionHandler("seekforward", (evt) => { + const skipTime = evt.seekOffset ?? 10; // Time to skip in seconds + dataRef.current.display?.setTime( + dataRef.current.progress.time + skipTime, + ); + updatePositionState(dataRef.current.progress.time + skipTime); + }); + + navigator.mediaSession.setActionHandler("seekto", (e) => { + if (!e.seekTime) return; + dataRef.current.display?.setTime(e.seekTime); + updatePositionState(e.seekTime); + }); + + if (dataRef.current.meta?.episode?.number !== 1) { + navigator.mediaSession.setActionHandler("previoustrack", () => { + changeEpisode(-1); + }); + } else { + navigator.mediaSession.setActionHandler("previoustrack", null); + } + + if ( + dataRef.current.meta?.episode?.number !== + dataRef.current.meta?.episodes?.length + ) { + navigator.mediaSession.setActionHandler("nexttrack", () => { + changeEpisode(1); + }); + } else { + navigator.mediaSession.setActionHandler("nexttrack", null); + } + }, [ + changeEpisode, + meta, + setDirectMeta, + setShouldStartFromBeginning, + updatePositionState, + ]); + return null; +} From fb68efa5222df1fd3583ac2c0f0c5bbd09006891 Mon Sep 17 00:00:00 2001 From: qtchaos <72168435+qtchaos@users.noreply.github.com> Date: Sat, 6 Jan 2024 18:43:42 +0200 Subject: [PATCH 2/5] Rework MediaSession to be less bad --- .../player/internals/MediaSession.ts | 75 +++++++++---------- 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/src/components/player/internals/MediaSession.ts b/src/components/player/internals/MediaSession.ts index f1be96e7..7290217c 100644 --- a/src/components/player/internals/MediaSession.ts +++ b/src/components/player/internals/MediaSession.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef } from "react"; +import { useCallback, useEffect, useMemo, useRef } from "react"; import { usePlayerStore } from "@/stores/player/store"; @@ -66,6 +66,22 @@ export function MediaSession() { useEffect(() => { if (!("mediaSession" in navigator)) return; + // If the media is paused, update the navigator + if (mediaPlaying.isPaused) { + navigator.mediaSession.playbackState = "paused"; + } else { + navigator.mediaSession.playbackState = "playing"; + } + }, [mediaPlaying.isPaused]); + + useEffect(() => { + if (!("mediaSession" in navigator)) return; + + updatePositionState(dataRef.current.progress.time); + }, [mediaPlaying.playbackRate, updatePositionState]); + + useEffect(() => { + if (!("mediaSession" in navigator)) return; // If not already updating the position state, and the media is loading, queue an update if ( !shouldUpdatePositionState.current && @@ -76,8 +92,7 @@ export function MediaSession() { // If the user has skipped (or MediaSession desynced) by more than 5 seconds, queue an update if ( - Math.abs(dataRef.current.progress.time - lastPlaybackPosition.current) > - 5 && + Math.abs(progress.time - lastPlaybackPosition.current) >= 5 && !dataRef.current.mediaPlaying.isLoading && !shouldUpdatePositionState.current ) { @@ -90,21 +105,29 @@ export function MediaSession() { !dataRef.current.mediaPlaying.isLoading ) { shouldUpdatePositionState.current = false; - updatePositionState(dataRef.current.progress.time); + updatePositionState(progress.time); } - lastPlaybackPosition.current = dataRef.current.progress.time; - navigator.mediaSession.playbackState = dataRef.current.mediaPlaying - .isPlaying - ? "playing" - : "paused"; + lastPlaybackPosition.current = progress.time; + }, [updatePositionState, progress.time]); + + useEffect(() => { + if ( + !("mediaSession" in navigator) || + dataRef.current.mediaPlaying.hasPlayedOnce || + dataRef.current.progress.duration === 0 + ) + return; + + const title = meta?.episode?.title ?? meta?.title ?? ""; + const artist = meta?.type === "movie" ? undefined : meta?.title ?? ""; navigator.mediaSession.metadata = new MediaMetadata({ - title: dataRef.current.meta?.episode?.title, - artist: dataRef.current.meta?.title, + title, + artist, artwork: [ { - src: dataRef.current.meta?.poster ?? "", + src: meta?.poster ?? "", sizes: "342x513", type: "image/png", }, @@ -115,7 +138,6 @@ export function MediaSession() { if (dataRef.current.mediaPlaying.isLoading) return; dataRef.current.display?.play(); - navigator.mediaSession.playbackState = "playing"; updatePositionState(dataRef.current.progress.time); }); @@ -123,33 +145,16 @@ export function MediaSession() { if (dataRef.current.mediaPlaying.isLoading) return; dataRef.current.display?.pause(); - navigator.mediaSession.playbackState = "paused"; updatePositionState(dataRef.current.progress.time); }); - navigator.mediaSession.setActionHandler("seekbackward", (evt) => { - const skipTime = evt.seekOffset ?? 10; - dataRef.current.display?.setTime( - dataRef.current.progress.time - skipTime, - ); - updatePositionState(dataRef.current.progress.time - skipTime); - }); - - navigator.mediaSession.setActionHandler("seekforward", (evt) => { - const skipTime = evt.seekOffset ?? 10; // Time to skip in seconds - dataRef.current.display?.setTime( - dataRef.current.progress.time + skipTime, - ); - updatePositionState(dataRef.current.progress.time + skipTime); - }); - navigator.mediaSession.setActionHandler("seekto", (e) => { if (!e.seekTime) return; dataRef.current.display?.setTime(e.seekTime); updatePositionState(e.seekTime); }); - if (dataRef.current.meta?.episode?.number !== 1) { + if ((dataRef.current.meta?.episode?.number ?? 1) !== 1) { navigator.mediaSession.setActionHandler("previoustrack", () => { changeEpisode(-1); }); @@ -167,12 +172,6 @@ export function MediaSession() { } else { navigator.mediaSession.setActionHandler("nexttrack", null); } - }, [ - changeEpisode, - meta, - setDirectMeta, - setShouldStartFromBeginning, - updatePositionState, - ]); + }, [changeEpisode, updatePositionState, meta]); return null; } From c6f359d4ea67beb26c097a44790f4e2d46756836 Mon Sep 17 00:00:00 2001 From: qtchaos <72168435+qtchaos@users.noreply.github.com> Date: Sat, 6 Jan 2024 19:31:05 +0200 Subject: [PATCH 3/5] Remove unused import in MediaSession.ts --- src/components/player/internals/MediaSession.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/player/internals/MediaSession.ts b/src/components/player/internals/MediaSession.ts index 7290217c..6c325aed 100644 --- a/src/components/player/internals/MediaSession.ts +++ b/src/components/player/internals/MediaSession.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useRef } from "react"; +import { useCallback, useEffect, useRef } from "react"; import { usePlayerStore } from "@/stores/player/store"; From 761e952ce269d704a6996793d790d9c466c2b4d8 Mon Sep 17 00:00:00 2001 From: qtchaos <72168435+qtchaos@users.noreply.github.com> Date: Fri, 9 Feb 2024 17:51:47 +0200 Subject: [PATCH 4/5] fix: update if conditions to allow for updates after changing episodes --- .../player/internals/MediaSession.ts | 142 +++++++++--------- 1 file changed, 70 insertions(+), 72 deletions(-) diff --git a/src/components/player/internals/MediaSession.ts b/src/components/player/internals/MediaSession.ts index 6c325aed..e5127c0e 100644 --- a/src/components/player/internals/MediaSession.ts +++ b/src/components/player/internals/MediaSession.ts @@ -5,10 +5,6 @@ import { usePlayerStore } from "@/stores/player/store"; import { usePlayerMeta } from "../hooks/usePlayerMeta"; export function MediaSession() { - const display = usePlayerStore((s) => s.display); - const mediaPlaying = usePlayerStore((s) => s.mediaPlaying); - const meta = usePlayerStore((s) => s.meta); - const progress = usePlayerStore((s) => s.progress); const { setDirectMeta } = usePlayerMeta(); const setShouldStartFromBeginning = usePlayerStore( (s) => s.setShouldStartFromBeginning, @@ -17,117 +13,107 @@ export function MediaSession() { const shouldUpdatePositionState = useRef(false); const lastPlaybackPosition = useRef(0); - const dataRef = useRef({ - display, - mediaPlaying, - progress, - meta, - }); - - useEffect(() => { - dataRef.current = { - display, - mediaPlaying, - progress, - meta, - }; - }, [display, mediaPlaying, progress, meta]); + const data = usePlayerStore.getState(); const changeEpisode = useCallback( (change: number) => { - const nextEp = meta?.episodes?.find( - (v) => v.number === (meta?.episode?.number ?? 0) + change, + const nextEp = data.meta?.episodes?.find( + (v) => v.number === (data.meta?.episode?.number ?? 0) + change, ); - if (!meta || !nextEp) return; - const metaCopy = { ...meta }; + if (!data.meta || !nextEp) return; + const metaCopy = { ...data.meta }; metaCopy.episode = nextEp; setShouldStartFromBeginning(true); setDirectMeta(metaCopy); }, - [setDirectMeta, meta, setShouldStartFromBeginning], + [data.meta, setDirectMeta, setShouldStartFromBeginning], ); - const updatePositionState = useCallback((position: number) => { - // If the updated position needs to be buffered, queue an update - if (position > dataRef.current.progress.buffered) { - shouldUpdatePositionState.current = true; - } - if (position > dataRef.current.progress.duration) return; + const updatePositionState = useCallback( + (position: number) => { + // If the updated position needs to be buffered, queue an update + if (position > data.progress.buffered) { + shouldUpdatePositionState.current = true; + } + if (position > data.progress.duration) return; - lastPlaybackPosition.current = dataRef.current.progress.time; - navigator.mediaSession.setPositionState({ - duration: dataRef.current.progress.duration, - playbackRate: dataRef.current.mediaPlaying.playbackRate, - position, - }); - }, []); + lastPlaybackPosition.current = data.progress.time; + navigator.mediaSession.setPositionState({ + duration: data.progress.duration, + playbackRate: data.mediaPlaying.playbackRate, + position, + }); + }, + [ + data.mediaPlaying.playbackRate, + data.progress.buffered, + data.progress.duration, + data.progress.time, + ], + ); useEffect(() => { if (!("mediaSession" in navigator)) return; // If the media is paused, update the navigator - if (mediaPlaying.isPaused) { + if (data.mediaPlaying.isPaused) { navigator.mediaSession.playbackState = "paused"; } else { navigator.mediaSession.playbackState = "playing"; } - }, [mediaPlaying.isPaused]); + }, [data.mediaPlaying.isPaused]); useEffect(() => { if (!("mediaSession" in navigator)) return; - updatePositionState(dataRef.current.progress.time); - }, [mediaPlaying.playbackRate, updatePositionState]); + updatePositionState(data.progress.time); + }, [data.progress.time, data.mediaPlaying.playbackRate, updatePositionState]); useEffect(() => { if (!("mediaSession" in navigator)) return; // If not already updating the position state, and the media is loading, queue an update - if ( - !shouldUpdatePositionState.current && - dataRef.current.mediaPlaying.isLoading - ) { + if (!shouldUpdatePositionState.current && data.mediaPlaying.isLoading) { shouldUpdatePositionState.current = true; } // If the user has skipped (or MediaSession desynced) by more than 5 seconds, queue an update if ( - Math.abs(progress.time - lastPlaybackPosition.current) >= 5 && - !dataRef.current.mediaPlaying.isLoading && + Math.abs(data.progress.time - lastPlaybackPosition.current) >= 5 && + !data.mediaPlaying.isLoading && !shouldUpdatePositionState.current ) { shouldUpdatePositionState.current = true; } // If not loading and the position state is queued, update it - if ( - shouldUpdatePositionState.current && - !dataRef.current.mediaPlaying.isLoading - ) { + if (shouldUpdatePositionState.current && !data.mediaPlaying.isLoading) { shouldUpdatePositionState.current = false; - updatePositionState(progress.time); + updatePositionState(data.progress.time); } - lastPlaybackPosition.current = progress.time; - }, [updatePositionState, progress.time]); + lastPlaybackPosition.current = data.progress.time; + }, [updatePositionState, data.progress.time, data.mediaPlaying.isLoading]); useEffect(() => { if ( !("mediaSession" in navigator) || - dataRef.current.mediaPlaying.hasPlayedOnce || - dataRef.current.progress.duration === 0 + (!data.mediaPlaying.isLoading && + data.mediaPlaying.isPlaying && + !data.display) ) return; - const title = meta?.episode?.title ?? meta?.title ?? ""; - const artist = meta?.type === "movie" ? undefined : meta?.title ?? ""; + const title = data.meta?.episode?.title ?? data.meta?.title ?? ""; + const artist = + data.meta?.type === "movie" ? undefined : data.meta?.title ?? ""; navigator.mediaSession.metadata = new MediaMetadata({ title, artist, artwork: [ { - src: meta?.poster ?? "", + src: data.meta?.poster ?? "", sizes: "342x513", type: "image/png", }, @@ -135,26 +121,26 @@ export function MediaSession() { }); navigator.mediaSession.setActionHandler("play", () => { - if (dataRef.current.mediaPlaying.isLoading) return; - dataRef.current.display?.play(); + if (data.mediaPlaying.isLoading) return; + data.display?.play(); - updatePositionState(dataRef.current.progress.time); + updatePositionState(data.progress.time); }); navigator.mediaSession.setActionHandler("pause", () => { - if (dataRef.current.mediaPlaying.isLoading) return; - dataRef.current.display?.pause(); + if (data.mediaPlaying.isLoading) return; + data.display?.pause(); - updatePositionState(dataRef.current.progress.time); + updatePositionState(data.progress.time); }); navigator.mediaSession.setActionHandler("seekto", (e) => { if (!e.seekTime) return; - dataRef.current.display?.setTime(e.seekTime); + data.display?.setTime(e.seekTime); updatePositionState(e.seekTime); }); - if ((dataRef.current.meta?.episode?.number ?? 1) !== 1) { + if ((data.meta?.episode?.number ?? 1) !== 1) { navigator.mediaSession.setActionHandler("previoustrack", () => { changeEpisode(-1); }); @@ -162,16 +148,28 @@ export function MediaSession() { navigator.mediaSession.setActionHandler("previoustrack", null); } - if ( - dataRef.current.meta?.episode?.number !== - dataRef.current.meta?.episodes?.length - ) { + if (data.meta?.episode?.number !== data.meta?.episodes?.length) { navigator.mediaSession.setActionHandler("nexttrack", () => { changeEpisode(1); }); } else { navigator.mediaSession.setActionHandler("nexttrack", null); } - }, [changeEpisode, updatePositionState, meta]); + }, [ + changeEpisode, + updatePositionState, + data.mediaPlaying.hasPlayedOnce, + data.mediaPlaying.isLoading, + data.progress.duration, + data.progress.time, + data.meta?.episode?.number, + data.meta?.episodes?.length, + data.display, + data.mediaPlaying, + data.meta?.episode?.title, + data.meta?.title, + data.meta?.type, + data.meta?.poster, + ]); return null; } From 256f9f9df90df04c3236e09481f53222693a9fba Mon Sep 17 00:00:00 2001 From: qtchaos <72168435+qtchaos@users.noreply.github.com> Date: Tue, 5 Mar 2024 00:33:31 +0200 Subject: [PATCH 5/5] feat: add season/episode to the start of title --- .../internals/{MediaSession.ts => MediaSession.tsx} | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) rename src/components/player/internals/{MediaSession.ts => MediaSession.tsx} (93%) diff --git a/src/components/player/internals/MediaSession.ts b/src/components/player/internals/MediaSession.tsx similarity index 93% rename from src/components/player/internals/MediaSession.ts rename to src/components/player/internals/MediaSession.tsx index e5127c0e..1dc7e737 100644 --- a/src/components/player/internals/MediaSession.ts +++ b/src/components/player/internals/MediaSession.tsx @@ -104,9 +104,15 @@ export function MediaSession() { ) return; - const title = data.meta?.episode?.title ?? data.meta?.title ?? ""; - const artist = - data.meta?.type === "movie" ? undefined : data.meta?.title ?? ""; + let title: string | undefined; + let artist: string | undefined; + + if (data.meta?.type === "movie") { + title = data.meta?.title; + } else if (data.meta?.type === "show") { + artist = data.meta?.title; + title = `S${data.meta?.season?.number} E${data.meta?.episode?.number}: ${data.meta?.episode?.title}`; + } navigator.mediaSession.metadata = new MediaMetadata({ title, @@ -170,6 +176,7 @@ export function MediaSession() { data.meta?.title, data.meta?.type, data.meta?.poster, + data.meta?.season?.number, ]); return null; }