Add hls support on the web

This commit is contained in:
Zoe Roux 2022-12-31 00:07:47 +09:00
parent 4b92b8a38e
commit 67da1563be
2 changed files with 70 additions and 55 deletions

View File

@ -18,20 +18,13 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Font, Track, WatchItem } from "@kyoo/models"; import { Track, WatchItem } from "@kyoo/models";
import { atom, useAtomValue, useSetAtom } from "jotai"; import { atom, useAtomValue, useSetAtom } from "jotai";
import { useEffect, useLayoutEffect, useRef } from "react"; import { memo, useEffect, useLayoutEffect, useRef } from "react";
import NativeVideo, { VideoProperties as VideoProps } from "./video"; import NativeVideo, { VideoProperties as VideoProps } from "./video";
import { bakedAtom } from "../jotai-utils"; import { bakedAtom } from "../jotai-utils";
import { Platform } from "react-native"; import { Platform } from "react-native";
enum PlayMode {
Direct,
Transmux,
}
const playModeAtom = atom<PlayMode>(PlayMode.Direct);
export const playAtom = atom(true); export const playAtom = atom(true);
export const loadAtom = atom(false); export const loadAtom = atom(false);
@ -70,7 +63,9 @@ export const [privateFullscreen, fullscreenAtom] = bakedAtom(
export const subtitleAtom = atom<Track | null>(null); export const subtitleAtom = atom<Track | null>(null);
export const Video = ({ const MemoVideo = memo(NativeVideo);
export const Video = memo(function _Video({
links, links,
setError, setError,
fonts, fonts,
@ -78,7 +73,7 @@ export const Video = ({
}: { }: {
links?: WatchItem["link"]; links?: WatchItem["link"];
setError: (error: string | undefined) => void; setError: (error: string | undefined) => void;
} & VideoProps) => { } & Partial<VideoProps>) {
const ref = useRef<NativeVideo | null>(null); const ref = useRef<NativeVideo | null>(null);
const isPlaying = useAtomValue(playAtom); const isPlaying = useAtomValue(playAtom);
const setLoad = useSetAtom(loadAtom); const setLoad = useSetAtom(loadAtom);
@ -110,37 +105,13 @@ export const Video = ({
const subtitle = useAtomValue(subtitleAtom); const subtitle = useAtomValue(subtitleAtom);
// 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]);
if (!links) return null; if (!links) return null;
return ( return (
<NativeVideo <MemoVideo
ref={ref} ref={ref}
{...props} {...props}
source={{ uri: links.direct }} // @ts-ignore Web only
source={{ uri: links.direct, transmux: links.transmux }}
paused={!isPlaying} paused={!isPlaying}
muted={isMuted} muted={isMuted}
volume={volume} volume={volume}
@ -170,4 +141,4 @@ export const Video = ({
// }, // },
/> />
); );
}; });

View File

@ -19,25 +19,39 @@
*/ */
import { Font, Track } from "@kyoo/models"; import { Font, Track } from "@kyoo/models";
import { forwardRef, RefObject, useEffect, useImperativeHandle, useRef } from "react"; import {
import { VideoProperties } from "react-native-video"; forwardRef,
import { useAtomValue } from "jotai"; RefObject,
useEffect,
useImperativeHandle,
useLayoutEffect,
useRef,
} from "react";
import { VideoProps } from "react-native-video";
import { atom, useAtom, useAtomValue } from "jotai";
import { useYoshiki } from "yoshiki"; import { useYoshiki } from "yoshiki";
import SubtitleOctopus from "libass-wasm"; import SubtitleOctopus from "libass-wasm";
import { subtitleAtom } from "./state"; import { subtitleAtom } from "./state";
// import Hls from "hls.js"; import Hls from "hls.js";
// let hls: Hls | null = null;
// TODO fallback via links and hls.
declare module "react-native-video" { declare module "react-native-video" {
interface VideoProperties { interface VideoProperties {
fonts?: Font[]; fonts?: Font[];
} }
export type VideoProps = Omit<VideoProperties, "source"> & {
source: { uri?: string; transmux?: string };
};
} }
const Video = forwardRef<{ seek: (value: number) => void }, VideoProperties>(function _Video( enum PlayMode {
Direct,
Transmux,
}
const playModeAtom = atom<PlayMode>(PlayMode.Direct);
let hls: Hls | null = null;
const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function _Video(
{ source, paused, muted, volume, onBuffer, onLoad, onProgress, onError, fonts }, { source, paused, muted, volume, onBuffer, onLoad, onProgress, onError, fonts },
forwaredRef, forwaredRef,
) { ) {
@ -56,7 +70,7 @@ const Video = forwardRef<{ seek: (value: number) => void }, VideoProperties>(fun
useEffect(() => { useEffect(() => {
if (paused) ref.current?.pause(); if (paused) ref.current?.pause();
else ref.current?.play(); else ref.current?.play().catch(() => {});
}, [paused]); }, [paused]);
useEffect(() => { useEffect(() => {
if (!ref.current || !volume) return; if (!ref.current || !volume) return;
@ -67,10 +81,33 @@ const Video = forwardRef<{ seek: (value: number) => void }, VideoProperties>(fun
const subtitle = useAtomValue(subtitleAtom); const subtitle = useAtomValue(subtitleAtom);
useSubtitle(ref, subtitle, fonts); useSubtitle(ref, subtitle, fonts);
const [playMode, setPlayMode] = useAtom(playModeAtom);
useEffect(() => {
setPlayMode(PlayMode.Direct);
}, [source.uri, setPlayMode]);
useLayoutEffect(() => {
console.log("toto");
const src = playMode === PlayMode.Direct ? source?.uri : source?.transmux;
if (!ref?.current || !src) return;
if (playMode == PlayMode.Direct || ref.current.canPlayType("application/vnd.apple.mpegurl")) {
ref.current.src = src;
} else {
if (hls === null) hls = new Hls();
hls.loadSource(src);
hls.attachMedia(ref.current);
hls.on(Hls.Events.MANIFEST_LOADED, async () => {
try {
await ref.current?.play();
} catch {}
});
}
}, [playMode, source?.uri, source?.transmux]);
return ( return (
<video <video
ref={ref} ref={ref}
src={typeof source === "number" ? undefined : source.uri}
muted={muted} muted={muted}
autoPlay={!paused} autoPlay={!paused}
onCanPlay={() => onBuffer?.call(null, { isBuffering: false })} onCanPlay={() => onBuffer?.call(null, { isBuffering: false })}
@ -89,11 +126,18 @@ const Video = forwardRef<{ seek: (value: number) => void }, VideoProperties>(fun
seekableDuration: 0, seekableDuration: 0,
}); });
}} }}
onError={() => onError={() => {
onError?.call(null, { if (
error: { "": "", errorString: ref.current?.error?.message ?? "Unknown error" }, ref?.current?.error?.code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED &&
}) playMode !== PlayMode.Transmux
} )
setPlayMode(PlayMode.Transmux);
else {
onError?.call(null, {
error: { "": "", errorString: ref.current?.error?.message ?? "Unknown error" },
});
}
}}
{...css({ width: "100%", height: "100%" })} {...css({ width: "100%", height: "100%" })}
/> />
); );