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