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;
}