Add transmux support to the player

This commit is contained in:
Zoe Roux 2022-10-10 18:47:34 +09:00
parent 6534f3bd25
commit 6bcff8d9a2
6 changed files with 106 additions and 15 deletions

View File

@ -75,6 +75,11 @@ namespace Kyoo.Abstractions.Models
/// </summary>
public string Title { get; set; }
/// <summary>
/// The summary of this episode.
/// </summay>
public string Overview { get; set; }
/// <summary>
/// The release date of this episode. It can be null if unknown.
/// </summary>
@ -211,6 +216,7 @@ namespace Kyoo.Abstractions.Models
EpisodeNumber = ep.EpisodeNumber,
AbsoluteNumber = ep.AbsoluteNumber,
Title = ep.Title,
Overview = ep.Overview,
ReleaseDate = ep.ReleaseDate,
Path = ep.Path,
Images = ep.Show.Images,

View File

@ -26,6 +26,7 @@
"@jellyfin/libass-wasm": "^4.1.1",
"@mui/icons-material": "^5.8.4",
"@mui/material": "^5.8.7",
"hls.js": "^1.2.4",
"jotai": "^1.8.4",
"next": "12.2.2",
"next-translate": "^1.5.0",

View File

@ -120,6 +120,10 @@ const WatchMovieP = z.preprocess(
* The title of this episode.
*/
name: z.string(),
/**
* The sumarry of this episode.
*/
overview: z.string().nullable(),
/**
* The release date of this episode. It can be null if unknown.
*/

View File

@ -25,9 +25,13 @@ import { useFetch } from "~/utils/query";
import { ErrorPage } from "~/components/errors";
import { useState, useEffect, PointerEvent as ReactPointerEvent } from "react";
import { Box } from "@mui/material";
import { useAtomValue, useSetAtom } from "jotai";
import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { Hover, LoadingIndicator } from "./components/hover";
import { fullscreenAtom, playAtom, useSubtitleController, useVideoController } from "./state";
import { useRouter } from "next/router";
import Head from "next/head";
import { makeTitle } from "~/utils/utils";
import { episodeDisplayNumber } from "~/components/episode";
// 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)
@ -40,10 +44,11 @@ const query = (slug: string): QueryIdentifier<WatchItem> => ({
const Player: QueryPage<{ slug: string }> = ({ slug }) => {
const { data, error } = useFetch(query(slug));
const { playerRef, videoProps, onVideoClick } = useVideoController();
const { playerRef, videoProps, onVideoClick } = useVideoController(data?.link);
const setFullscreen = useSetAtom(fullscreenAtom);
const router = useRouter();
const isPlaying = useAtomValue(playAtom);
const [isPlaying, setPlay] = useAtom(playAtom);
const [showHover, setHover] = useState(false);
const [mouseMoved, setMouseMoved] = useState(false);
const [menuOpenned, setMenuOpen] = useState(false);
@ -55,7 +60,7 @@ const Player: QueryPage<{ slug: string }> = ({ slug }) => {
mouseCallback = setTimeout(() => {
setMouseMoved(false);
}, 2500);
}
};
useEffect(() => {
const handler = (e: PointerEvent) => {
@ -67,6 +72,10 @@ const Player: QueryPage<{ slug: string }> = ({ slug }) => {
return () => document.removeEventListener("pointermove", handler);
});
useEffect(() => {
setPlay(true);
}, [slug, setPlay]);
useSubtitleController(playerRef, data?.subtitles, data?.fonts);
useEffect(() => {
@ -79,6 +88,24 @@ const Player: QueryPage<{ slug: string }> = ({ slug }) => {
return (
<>
{data && (
<Head>
<title>
{makeTitle(
data.isMovie
? data.name
: data.showTitle +
" " +
episodeDisplayNumber({
seasonNumber: data.seasonNumber,
episodeNumber: data.episodeNumber,
absoluteNumber: data.absoluteNumber,
}),
)}
</title>
<meta name="description" content={data.overview ?? undefined} />
</Head>
)}
<style jsx global>{`
::cue {
background-color: transparent;
@ -91,7 +118,6 @@ const Player: QueryPage<{ slug: string }> = ({ slug }) => {
>
<Box
component="video"
src={data?.link.direct}
{...videoProps}
onPointerDown={(e: ReactPointerEvent<HTMLVideoElement>) => {
if (e.pointerType === "mouse") {
@ -102,6 +128,14 @@ const Player: QueryPage<{ slug: string }> = ({ slug }) => {
mouseHasMoved();
}
}}
onEnded={() => {
if (!data) return;
if (data.isMovie) router.push(`/movie/${data.slug}`);
else
router.push(
data.nextEpisode ? `/watch/${data.nextEpisode.slug}` : `/show/${data.showSlug}`,
);
}}
sx={{
position: "fixed",
top: 0,

View File

@ -19,20 +19,33 @@
*/
import { BoxProps } from "@mui/material";
import { atom, useSetAtom } from "jotai";
import { atom, useAtom, useSetAtom } from "jotai";
import { useRouter } from "next/router";
import { RefObject, useEffect, useRef } from "react";
import { Font, Track } from "~/models/resources/watch-item";
import { bakedAtom } from "~/utils/jotai-utils";
// @ts-ignore
import SubtitleOctopus from "@jellyfin/libass-wasm/dist/js/subtitles-octopus";
import Hls from "hls.js";
enum PlayMode {
Direct,
Transmux,
}
const playModeAtom = atom<PlayMode>(PlayMode.Direct);
export const playerAtom = atom<RefObject<HTMLVideoElement> | null>(null);
export const [_playAtom, playAtom] = bakedAtom(true, (get, _, value) => {
export const [_playAtom, playAtom] = bakedAtom(true, async (get, set, value) => {
const player = get(playerAtom);
if (!player?.current) return;
if (value) {
player.current.play();
try {
await player.current.play();
} catch (e) {
if (e instanceof DOMException && e.name === "NotSupportedError") set(playModeAtom, PlayMode.Transmux);
else if (!(e instanceof DOMException && e.name === "NotAllowedError")) console.log(e);
}
} else {
player.current.pause();
}
@ -71,10 +84,13 @@ export const [_, fullscreenAtom] = bakedAtom(false, async (_, set, value, baker)
} catch {}
});
export const useVideoController = () => {
let hls: Hls | null = null;
export const useVideoController = (links?: { direct: string; transmux: string }) => {
const player = useRef<HTMLVideoElement>(null);
const setPlayer = useSetAtom(playerAtom);
const setPlay = useSetAtom(_playAtom);
const setPPlay = useSetAtom(playAtom);
const setLoad = useSetAtom(loadAtom);
const setProgress = useSetAtom(_progressAtom);
const setBuffered = useSetAtom(bufferedAtom);
@ -82,15 +98,40 @@ export const useVideoController = () => {
const setVolume = useSetAtom(_volumeAtom);
const setMuted = useSetAtom(_mutedAtom);
const setFullscreen = useSetAtom(fullscreenAtom);
const [playMode, setPlayMode] = useAtom(playModeAtom);
setPlayer(player);
useEffect(() => {
if (!player.current) return;
if (player.current.paused) player.current.play();
setPlay(!player.current.paused);
}, [setPlay]);
useEffect(() => {
setPlayMode(PlayMode.Direct);
}, [links, setPlayMode]);
useEffect(() => {
const src = playMode === PlayMode.Direct ? links?.direct : links?.transmux;
if (!player?.current || !src) return;
if (
playMode == PlayMode.Direct ||
player.current.canPlayType("application/vnd.apple.mpegurl")
) {
player.current.src = src;
} else {
if (hls === null) hls = new Hls();
hls.loadSource(src);
hls.attachMedia(player.current);
hls.on(Hls.Events.MANIFEST_LOADED, async () => {
try {
await player.current?.play();
} catch {}
});
}
}, [playMode, links, player]);
useEffect(() => {
if (!player?.current?.duration) return;
setDuration(player.current.duration);
@ -109,6 +150,10 @@ export const useVideoController = () => {
onPause: () => setPlay(false),
onWaiting: () => setLoad(true),
onCanPlay: () => setLoad(false),
onError: () => {
if (player?.current?.error?.code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED)
setPlayMode(PlayMode.Transmux);
},
onTimeUpdate: () => setProgress(player?.current?.currentTime ?? 0),
onDurationChange: () => setDuration(player?.current?.duration ?? 0),
onProgress: () =>
@ -130,11 +175,7 @@ export const useVideoController = () => {
videoProps,
onVideoClick: () => {
if (!player.current) return;
if (player.current.paused) {
player.current.play();
} else {
player.current.pause();
}
setPPlay(player.current.paused);
},
};
};

View File

@ -1460,6 +1460,11 @@ has@^1.0.3:
dependencies:
function-bind "^1.1.1"
hls.js@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/hls.js/-/hls.js-1.2.4.tgz#761d7f700c3da5c1701257c24c1db7ababea14eb"
integrity sha512-yC3K79Kzq1W+OgjT12JxKMDXv9DbfvulppxmPBl7D04SaTyd2IwWk5eNASQV1mUaPlKbjr16yI9292qpSGo0ig==
hoist-non-react-statics@^3.3.1:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"