mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
Add media session management
This commit is contained in:
parent
2c06924792
commit
d7dc66301e
@ -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(() => {
|
||||||
|
92
front/src/player/media-session.tsx
Normal file
92
front/src/player/media-session.tsx
Normal 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;
|
||||||
|
};
|
@ -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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user