Add media session management

This commit is contained in:
Zoe Roux 2022-10-11 00:21:46 +09:00
parent 2c06924792
commit d7dc66301e
3 changed files with 108 additions and 12 deletions

View File

@ -37,11 +37,12 @@ type Action =
| { type: "mute" } | { type: "mute" }
| { type: "fullscreen" } | { type: "fullscreen" }
| { type: "seek"; value: number } | { type: "seek"; value: number }
| { type: "seekTo"; value: number }
| { type: "seekPercent"; value: number } | { type: "seekPercent"; value: number }
| { type: "volume"; value: number } | { type: "volume"; value: number }
| { type: "subtitle"; subtitles: Track[]; fonts: Font[] }; | { type: "subtitle"; subtitles: Track[]; fonts: Font[] };
const keyboardReducerAtom = atom<null, Action>(null, (get, set, action) => { export const reducerAtom = atom<null, Action>(null, (get, set, action) => {
switch (action.type) { switch (action.type) {
case "play": case "play":
set(playAtom, !get(playAtom)); set(playAtom, !get(playAtom));
@ -55,6 +56,9 @@ const keyboardReducerAtom = atom<null, Action>(null, (get, set, action) => {
case "seek": case "seek":
set(progressAtom, get(progressAtom) + action.value); set(progressAtom, get(progressAtom) + action.value);
break; break;
case "seekTo":
set(progressAtom, action.value);
break;
case "seekPercent": case "seekPercent":
set(progressAtom, (get(durationAtom) * action.value) / 100); set(progressAtom, (get(durationAtom) * action.value) / 100);
break; break;
@ -83,7 +87,7 @@ export const useVideoKeyboard = (
previousEpisode?: string, previousEpisode?: string,
nextEpisode?: string, nextEpisode?: string,
) => { ) => {
const reducer = useSetAtom(keyboardReducerAtom); const reducer = useSetAtom(reducerAtom);
const router = useRouter(); const router = useRouter();
useEffect(() => { useEffect(() => {

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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;
};

View File

@ -33,6 +33,7 @@ import Head from "next/head";
import { makeTitle } from "~/utils/utils"; import { makeTitle } from "~/utils/utils";
import { episodeDisplayNumber } from "~/components/episode"; import { episodeDisplayNumber } from "~/components/episode";
import { useVideoKeyboard } from "./keyboard"; 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 // 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) // 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 [menuOpenned, setMenuOpen] = useState(false);
const displayControls = showHover || !isPlaying || mouseMoved || menuOpenned; 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 = () => { const mouseHasMoved = () => {
setMouseMoved(true); setMouseMoved(true);
if (mouseCallback) clearTimeout(mouseCallback); if (mouseCallback) clearTimeout(mouseCallback);
@ -84,16 +92,7 @@ const Player: QueryPage<{ slug: string }> = ({ slug }) => {
}, [setFullscreen]); }, [setFullscreen]);
useSubtitleController(playerRef, data?.subtitles, data?.fonts); useSubtitleController(playerRef, data?.subtitles, data?.fonts);
useVideoKeyboard( useVideoKeyboard(data?.subtitles, data?.fonts, previous, next);
data?.subtitles,
data?.fonts,
data && !data.isMovie && data.previousEpisode
? `/watch/${data.previousEpisode.slug}`
: undefined,
data && !data.isMovie && data.nextEpisode
? `/watch/${data.nextEpisode.slug}`
: undefined,
);
if (error) return <ErrorPage {...error} />; if (error) return <ErrorPage {...error} />;
@ -117,6 +116,7 @@ const Player: QueryPage<{ slug: string }> = ({ slug }) => {
<meta name="description" content={data.overview ?? undefined} /> <meta name="description" content={data.overview ?? undefined} />
</Head> </Head>
)} )}
<MediaSessionManager title={data?.name} image={data?.thumbnail} next={next} previous={previous} />
<style jsx global>{` <style jsx global>{`
::cue { ::cue {
background-color: transparent; background-color: transparent;