Merge pull request #721 from qtchaos/mediasession

Implement MediaSession support
This commit is contained in:
William Oldham 2024-03-07 15:54:57 +00:00 committed by GitHub
commit 9409922efd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 184 additions and 0 deletions

View File

@ -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) {
<VideoContainer />
<ProgressSaver />
<KeyboardEvents />
<MediaSession />
<div className="relative h-screen overflow-hidden">
<VideoClickTarget showingControls={props.showingControls} />
<HeadUpdater />

View File

@ -0,0 +1,182 @@
import { useCallback, useEffect, useRef } from "react";
import { usePlayerStore } from "@/stores/player/store";
import { usePlayerMeta } from "../hooks/usePlayerMeta";
export function MediaSession() {
const { setDirectMeta } = usePlayerMeta();
const setShouldStartFromBeginning = usePlayerStore(
(s) => s.setShouldStartFromBeginning,
);
const shouldUpdatePositionState = useRef(false);
const lastPlaybackPosition = useRef(0);
const data = usePlayerStore.getState();
const changeEpisode = useCallback(
(change: number) => {
const nextEp = data.meta?.episodes?.find(
(v) => v.number === (data.meta?.episode?.number ?? 0) + change,
);
if (!data.meta || !nextEp) return;
const metaCopy = { ...data.meta };
metaCopy.episode = nextEp;
setShouldStartFromBeginning(true);
setDirectMeta(metaCopy);
},
[data.meta, setDirectMeta, setShouldStartFromBeginning],
);
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 = 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 (data.mediaPlaying.isPaused) {
navigator.mediaSession.playbackState = "paused";
} else {
navigator.mediaSession.playbackState = "playing";
}
}, [data.mediaPlaying.isPaused]);
useEffect(() => {
if (!("mediaSession" in navigator)) return;
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 && 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(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 && !data.mediaPlaying.isLoading) {
shouldUpdatePositionState.current = false;
updatePositionState(data.progress.time);
}
lastPlaybackPosition.current = data.progress.time;
}, [updatePositionState, data.progress.time, data.mediaPlaying.isLoading]);
useEffect(() => {
if (
!("mediaSession" in navigator) ||
(!data.mediaPlaying.isLoading &&
data.mediaPlaying.isPlaying &&
!data.display)
)
return;
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,
artist,
artwork: [
{
src: data.meta?.poster ?? "",
sizes: "342x513",
type: "image/png",
},
],
});
navigator.mediaSession.setActionHandler("play", () => {
if (data.mediaPlaying.isLoading) return;
data.display?.play();
updatePositionState(data.progress.time);
});
navigator.mediaSession.setActionHandler("pause", () => {
if (data.mediaPlaying.isLoading) return;
data.display?.pause();
updatePositionState(data.progress.time);
});
navigator.mediaSession.setActionHandler("seekto", (e) => {
if (!e.seekTime) return;
data.display?.setTime(e.seekTime);
updatePositionState(e.seekTime);
});
if ((data.meta?.episode?.number ?? 1) !== 1) {
navigator.mediaSession.setActionHandler("previoustrack", () => {
changeEpisode(-1);
});
} else {
navigator.mediaSession.setActionHandler("previoustrack", null);
}
if (data.meta?.episode?.number !== data.meta?.episodes?.length) {
navigator.mediaSession.setActionHandler("nexttrack", () => {
changeEpisode(1);
});
} else {
navigator.mediaSession.setActionHandler("nexttrack", null);
}
}, [
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,
data.meta?.season?.number,
]);
return null;
}