Add srt support on the web

This commit is contained in:
Zoe Roux 2023-07-31 23:57:58 +09:00
parent 8e9cd2d2f3
commit e0ee364929
3 changed files with 53 additions and 24 deletions

View File

@ -34,6 +34,7 @@
"react-native-video": "^6.0.0-alpha.5", "react-native-video": "^6.0.0-alpha.5",
"react-native-web": "0.19.1", "react-native-web": "0.19.1",
"solito": "^3.0.0", "solito": "^3.0.0",
"srt-webvtt": "^2.0.0",
"superjson": "^1.12.2", "superjson": "^1.12.2",
"sweetalert2": "^11.7.12", "sweetalert2": "^11.7.12",
"yoshiki": "1.2.2", "yoshiki": "1.2.2",

View File

@ -18,7 +18,7 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { getToken, Subtitle } from "@kyoo/models"; import { getToken, queryFn, Subtitle } from "@kyoo/models";
import { import {
forwardRef, forwardRef,
RefObject, RefObject,
@ -37,6 +37,8 @@ import { playAtom, PlayMode, playModeAtom, subtitleAtom } from "./state";
import Hls, { Level } from "hls.js"; import Hls, { Level } from "hls.js";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Menu } from "@kyoo/primitives"; import { Menu } from "@kyoo/primitives";
import toVttBlob from "srt-webvtt";
import { getDisplayName } from "./components/right-buttons";
let hls: Hls | null = null; let hls: Hls | null = null;
@ -100,7 +102,7 @@ const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function
useEffect(() => { useEffect(() => {
if (!ref.current || paused === ref.current.paused) return; if (!ref.current || paused === ref.current.paused) return;
if (paused) ref.current?.pause(); if (paused) ref.current?.pause();
else ref.current?.play().catch(() => { }); else ref.current?.play().catch(() => {});
}, [paused]); }, [paused]);
useEffect(() => { useEffect(() => {
if (!ref.current || !volume) return; if (!ref.current || !volume) return;
@ -110,14 +112,12 @@ const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function
const subtitle = useAtomValue(subtitleAtom); const subtitle = useAtomValue(subtitleAtom);
useSubtitle(ref, subtitle, fonts); useSubtitle(ref, subtitle, fonts);
useLayoutEffect(() => { useLayoutEffect(() => {
(async () => { (async () => {
if (!ref?.current || !source.uri) return; if (!ref?.current || !source.uri) return;
if (!hls || oldHls.current !== source.hls) { if (!hls || oldHls.current !== source.hls) {
// Reinit the hls player when we change track. // Reinit the hls player when we change track.
if (hls) if (hls) hls.destroy();
hls.destroy();
hls = null; hls = null;
hls = await initHls(); hls = await initHls();
// Still load the hls source to list available qualities. // Still load the hls source to list available qualities.
@ -134,7 +134,7 @@ const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function
hls.on(Hls.Events.MANIFEST_LOADED, async () => { hls.on(Hls.Events.MANIFEST_LOADED, async () => {
try { try {
await ref.current?.play(); await ref.current?.play();
} catch { } } catch {}
}); });
hls.on(Hls.Events.ERROR, (_, d) => { hls.on(Hls.Events.ERROR, (_, d) => {
if (!d.fatal || !hls?.media) return; if (!d.fatal || !hls?.media) return;
@ -201,7 +201,11 @@ export default Video;
let htmlTrack: HTMLTrackElement | null; let htmlTrack: HTMLTrackElement | null;
let subOcto: SubtitleOctopus | null; let subOcto: SubtitleOctopus | null;
const useSubtitle = (player: RefObject<HTMLVideoElement>, value: Subtitle | null, fonts?: string[]) => { const useSubtitle = (
player: RefObject<HTMLVideoElement>,
value: Subtitle | null,
fonts?: string[],
) => {
useEffect(() => { useEffect(() => {
if (!player.current) return; if (!player.current) return;
@ -224,20 +228,23 @@ const useSubtitle = (player: RefObject<HTMLVideoElement>, value: Subtitle | null
} else if (value.codec === "vtt" || value.codec === "subrip") { } else if (value.codec === "vtt" || value.codec === "subrip") {
removeOctoSub(); removeOctoSub();
if (player.current.textTracks.length > 0) player.current.textTracks[0].mode = "hidden"; if (player.current.textTracks.length > 0) player.current.textTracks[0].mode = "hidden";
const addSubtitle = async () => {
const track: HTMLTrackElement = htmlTrack ?? document.createElement("track"); const track: HTMLTrackElement = htmlTrack ?? document.createElement("track");
track.kind = "subtitles"; track.kind = "subtitles";
track.label = value.displayName; track.label = getDisplayName(value);
if (value.language) track.srclang = value.language; if (value.language) track.srclang = value.language;
track.src = value.link; track.src = value.codec === "subrip" ? await toWebVtt(value.link) : value.link;
track.className = "subtitle_container"; track.className = "subtitle_container";
track.default = true; track.default = true;
track.onload = () => { track.onload = () => {
if (player.current) player.current.textTracks[0].mode = "showing"; if (player.current) player.current.textTracks[0].mode = "showing";
}; };
if (!htmlTrack) { if (!htmlTrack) {
player.current.appendChild(track);
htmlTrack = track; htmlTrack = track;
if (player.current) player.current.appendChild(track);
} }
};
addSubtitle();
} else if (value.codec === "ass") { } else if (value.codec === "ass") {
removeHtmlSubtitle(); removeHtmlSubtitle();
removeOctoSub(); removeOctoSub();
@ -255,6 +262,19 @@ const useSubtitle = (player: RefObject<HTMLVideoElement>, value: Subtitle | null
}, [player, value, fonts]); }, [player, value, fonts]);
}; };
const toWebVtt = async (srtUrl: string) => {
const token = await getToken();
const query = await fetch(srtUrl, {
headers: token
? {
Authorization: token,
}
: undefined,
});
const srt = await query.blob();
return await toVttBlob(srt);
};
export const AudiosMenu = (props: ComponentProps<typeof Menu>) => { export const AudiosMenu = (props: ComponentProps<typeof Menu>) => {
if (!hls || hls.audioTracks.length < 2) return null; if (!hls || hls.audioTracks.length < 2) return null;
return ( return (
@ -283,10 +303,10 @@ export const QualitiesMenu = (props: ComponentProps<typeof Menu>) => {
}); });
const levelName = (label: Level, auto?: boolean): string => { const levelName = (label: Level, auto?: boolean): string => {
const height = `${label.height}p` const height = `${label.height}p`;
if (auto) return height; if (auto) return height;
return label.uri.includes("original") ? `${t("player.transmux")} (${height})` : height; return label.uri.includes("original") ? `${t("player.transmux")} (${height})` : height;
} };
return ( return (
<Menu {...props}> <Menu {...props}>

View File

@ -12851,6 +12851,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"srt-webvtt@npm:^2.0.0":
version: 2.0.0
resolution: "srt-webvtt@npm:2.0.0"
checksum: 457645e902929c1b4a5691bb58195a7dc3b77699a62fa551bf13ce89e4900d919d4e0595a5e17641b4c0a6e24ec9e2794b1c728e8a625a76cf0c2304cb20356e
languageName: node
linkType: hard
"ssri@npm:^8.0.1": "ssri@npm:^8.0.1":
version: 8.0.1 version: 8.0.1
resolution: "ssri@npm:8.0.1" resolution: "ssri@npm:8.0.1"
@ -14142,6 +14149,7 @@ __metadata:
react-native-video: ^6.0.0-alpha.5 react-native-video: ^6.0.0-alpha.5
react-native-web: 0.19.1 react-native-web: 0.19.1
solito: ^3.0.0 solito: ^3.0.0
srt-webvtt: ^2.0.0
superjson: ^1.12.2 superjson: ^1.12.2
sweetalert2: ^11.7.12 sweetalert2: ^11.7.12
typescript: ^4.9.5 typescript: ^4.9.5