From d7dc66301efa215d82af465cac62700b406a9771 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Tue, 11 Oct 2022 00:21:46 +0900 Subject: [PATCH] Add media session management --- front/src/player/keyboard.tsx | 8 ++- front/src/player/media-session.tsx | 92 ++++++++++++++++++++++++++++++ front/src/player/player.tsx | 20 +++---- 3 files changed, 108 insertions(+), 12 deletions(-) create mode 100644 front/src/player/media-session.tsx diff --git a/front/src/player/keyboard.tsx b/front/src/player/keyboard.tsx index 3ef0615b..f26bb526 100644 --- a/front/src/player/keyboard.tsx +++ b/front/src/player/keyboard.tsx @@ -37,11 +37,12 @@ type Action = | { type: "mute" } | { type: "fullscreen" } | { type: "seek"; value: number } + | { type: "seekTo"; value: number } | { type: "seekPercent"; value: number } | { type: "volume"; value: number } | { type: "subtitle"; subtitles: Track[]; fonts: Font[] }; -const keyboardReducerAtom = atom(null, (get, set, action) => { +export const reducerAtom = atom(null, (get, set, action) => { switch (action.type) { case "play": set(playAtom, !get(playAtom)); @@ -55,6 +56,9 @@ const keyboardReducerAtom = atom(null, (get, set, action) => { case "seek": set(progressAtom, get(progressAtom) + action.value); break; + case "seekTo": + set(progressAtom, action.value); + break; case "seekPercent": set(progressAtom, (get(durationAtom) * action.value) / 100); break; @@ -83,7 +87,7 @@ export const useVideoKeyboard = ( previousEpisode?: string, nextEpisode?: string, ) => { - const reducer = useSetAtom(keyboardReducerAtom); + const reducer = useSetAtom(reducerAtom); const router = useRouter(); useEffect(() => { diff --git a/front/src/player/media-session.tsx b/front/src/player/media-session.tsx new file mode 100644 index 00000000..5f1bc02d --- /dev/null +++ b/front/src/player/media-session.tsx @@ -0,0 +1,92 @@ +/* + * Kyoo - A portable and vast media library solution. + * Copyright (c) Kyoo. + * + * See AUTHORS.md and LICENSE file in the project root for full license information. + * + * Kyoo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Kyoo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Kyoo. If not, see . + */ + +import { useAtom, useAtomValue, useSetAtom } from "jotai"; +import { useRouter } from "next/router"; +import { useEffect } from "react"; +import { reducerAtom } from "./keyboard"; +import { durationAtom, playAtom, progressAtom } from "./state"; + +export const MediaSessionManager = ({ + title, + image, + previous, + next, +}: { + title?: string; + image?: string | null; + previous?: string; + next?: string; +}) => { + const [isPlaying, setPlay] = useAtom(playAtom); + const progress = useAtomValue(progressAtom); + const duration = useAtomValue(durationAtom); + const reducer = useSetAtom(reducerAtom); + const router = useRouter(); + + useEffect(() => { + if (!("mediaSession" in navigator)) return; + navigator.mediaSession.metadata = new MediaMetadata({ + title: title, + artwork: image ? [{ src: image }] : undefined, + }); + }, [title, image]); + + useEffect(() => { + if (!("mediaSession" in navigator)) return; + const actions: [MediaSessionAction, MediaSessionActionHandler | null][] = [ + ["play", () => setPlay(true)], + ["pause", () => setPlay(false)], + ["previoustrack", previous ? () => router.push(previous) : null], + ["nexttrack", next ? () => router.push(next) : null], + [ + "seekbackward", + (evt: MediaSessionActionDetails) => + reducer({ type: "seek", value: evt.seekOffset ? -evt.seekOffset : -10 }), + ], + [ + "seekforward", + (evt: MediaSessionActionDetails) => + reducer({ type: "seek", value: evt.seekOffset ? evt.seekOffset : 10 }), + ], + [ + "seekto", + (evt: MediaSessionActionDetails) => reducer({ type: "seekTo", value: evt.seekTime! }), + ], + ]; + + for (const [action, handler] of actions) { + try { + navigator.mediaSession.setActionHandler(action, handler); + } catch {} + } + }, [setPlay, reducer, router, previous, next]); + + useEffect(() => { + if (!("mediaSession" in navigator)) return; + navigator.mediaSession.playbackState = isPlaying ? "playing" : "paused"; + }, [isPlaying]); + useEffect(() => { + if (!("mediaSession" in navigator)) return; + navigator.mediaSession.setPositionState({ position: progress, duration, playbackRate: 1 }); + }, [progress, duration]); + + return null; +}; diff --git a/front/src/player/player.tsx b/front/src/player/player.tsx index be7d6f5d..6b53f515 100644 --- a/front/src/player/player.tsx +++ b/front/src/player/player.tsx @@ -33,6 +33,7 @@ import Head from "next/head"; import { makeTitle } from "~/utils/utils"; import { episodeDisplayNumber } from "~/components/episode"; import { useVideoKeyboard } from "./keyboard"; +import { MediaSessionManager } from "./media-session"; // Callback used to hide the controls when the mouse goes iddle. This is stored globally to clear the old timeout // if the mouse moves again (if this is stored as a state, the whole page is redrawn on mouse move) @@ -55,6 +56,13 @@ const Player: QueryPage<{ slug: string }> = ({ slug }) => { const [menuOpenned, setMenuOpen] = useState(false); const displayControls = showHover || !isPlaying || mouseMoved || menuOpenned; + const previous = + data && !data.isMovie && data.previousEpisode + ? `/watch/${data.previousEpisode.slug}` + : undefined; + const next = + data && !data.isMovie && data.nextEpisode ? `/watch/${data.nextEpisode.slug}` : undefined; + const mouseHasMoved = () => { setMouseMoved(true); if (mouseCallback) clearTimeout(mouseCallback); @@ -84,16 +92,7 @@ const Player: QueryPage<{ slug: string }> = ({ slug }) => { }, [setFullscreen]); useSubtitleController(playerRef, data?.subtitles, data?.fonts); - useVideoKeyboard( - data?.subtitles, - data?.fonts, - data && !data.isMovie && data.previousEpisode - ? `/watch/${data.previousEpisode.slug}` - : undefined, - data && !data.isMovie && data.nextEpisode - ? `/watch/${data.nextEpisode.slug}` - : undefined, - ); + useVideoKeyboard(data?.subtitles, data?.fonts, previous, next); if (error) return ; @@ -117,6 +116,7 @@ const Player: QueryPage<{ slug: string }> = ({ slug }) => { )} +