From e0ee36492998b8af66f156ebfb7a4af2d4d4a6b1 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 31 Jul 2023 23:57:58 +0900 Subject: [PATCH] Add srt support on the web --- front/apps/web/package.json | 1 + front/packages/ui/src/player/video.web.tsx | 68 ++++++++++++++-------- front/yarn.lock | 8 +++ 3 files changed, 53 insertions(+), 24 deletions(-) diff --git a/front/apps/web/package.json b/front/apps/web/package.json index 954db2fd..17434ba9 100644 --- a/front/apps/web/package.json +++ b/front/apps/web/package.json @@ -34,6 +34,7 @@ "react-native-video": "^6.0.0-alpha.5", "react-native-web": "0.19.1", "solito": "^3.0.0", + "srt-webvtt": "^2.0.0", "superjson": "^1.12.2", "sweetalert2": "^11.7.12", "yoshiki": "1.2.2", diff --git a/front/packages/ui/src/player/video.web.tsx b/front/packages/ui/src/player/video.web.tsx index 2a5ce477..39e64832 100644 --- a/front/packages/ui/src/player/video.web.tsx +++ b/front/packages/ui/src/player/video.web.tsx @@ -18,7 +18,7 @@ * along with Kyoo. If not, see . */ -import { getToken, Subtitle } from "@kyoo/models"; +import { getToken, queryFn, Subtitle } from "@kyoo/models"; import { forwardRef, RefObject, @@ -37,6 +37,8 @@ import { playAtom, PlayMode, playModeAtom, subtitleAtom } from "./state"; import Hls, { Level } from "hls.js"; import { useTranslation } from "react-i18next"; import { Menu } from "@kyoo/primitives"; +import toVttBlob from "srt-webvtt"; +import { getDisplayName } from "./components/right-buttons"; let hls: Hls | null = null; @@ -100,7 +102,7 @@ const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function useEffect(() => { if (!ref.current || paused === ref.current.paused) return; if (paused) ref.current?.pause(); - else ref.current?.play().catch(() => { }); + else ref.current?.play().catch(() => {}); }, [paused]); useEffect(() => { if (!ref.current || !volume) return; @@ -110,14 +112,12 @@ const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function const subtitle = useAtomValue(subtitleAtom); useSubtitle(ref, subtitle, fonts); - useLayoutEffect(() => { (async () => { if (!ref?.current || !source.uri) return; if (!hls || oldHls.current !== source.hls) { // Reinit the hls player when we change track. - if (hls) - hls.destroy(); + if (hls) hls.destroy(); hls = null; hls = await initHls(); // 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 () => { try { await ref.current?.play(); - } catch { } + } catch {} }); hls.on(Hls.Events.ERROR, (_, d) => { if (!d.fatal || !hls?.media) return; @@ -145,8 +145,8 @@ const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function }); } })(); - // onError changes should not restart the playback. - // eslint-disable-next-line react-hooks/exhaustive-deps + // onError changes should not restart the playback. + // eslint-disable-next-line react-hooks/exhaustive-deps }, [source.uri, source.hls]); const setPlay = useSetAtom(playAtom); @@ -201,7 +201,11 @@ export default Video; let htmlTrack: HTMLTrackElement | null; let subOcto: SubtitleOctopus | null; -const useSubtitle = (player: RefObject, value: Subtitle | null, fonts?: string[]) => { +const useSubtitle = ( + player: RefObject, + value: Subtitle | null, + fonts?: string[], +) => { useEffect(() => { if (!player.current) return; @@ -224,20 +228,23 @@ const useSubtitle = (player: RefObject, value: Subtitle | null } else if (value.codec === "vtt" || value.codec === "subrip") { removeOctoSub(); if (player.current.textTracks.length > 0) player.current.textTracks[0].mode = "hidden"; - const track: HTMLTrackElement = htmlTrack ?? document.createElement("track"); - track.kind = "subtitles"; - track.label = value.displayName; - if (value.language) track.srclang = value.language; - track.src = value.link; - track.className = "subtitle_container"; - track.default = true; - track.onload = () => { - if (player.current) player.current.textTracks[0].mode = "showing"; + const addSubtitle = async () => { + const track: HTMLTrackElement = htmlTrack ?? document.createElement("track"); + track.kind = "subtitles"; + track.label = getDisplayName(value); + if (value.language) track.srclang = value.language; + track.src = value.codec === "subrip" ? await toWebVtt(value.link) : value.link; + track.className = "subtitle_container"; + track.default = true; + track.onload = () => { + if (player.current) player.current.textTracks[0].mode = "showing"; + }; + if (!htmlTrack) { + htmlTrack = track; + if (player.current) player.current.appendChild(track); + } }; - if (!htmlTrack) { - player.current.appendChild(track); - htmlTrack = track; - } + addSubtitle(); } else if (value.codec === "ass") { removeHtmlSubtitle(); removeOctoSub(); @@ -255,6 +262,19 @@ const useSubtitle = (player: RefObject, value: Subtitle | null }, [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) => { if (!hls || hls.audioTracks.length < 2) return null; return ( @@ -283,10 +303,10 @@ export const QualitiesMenu = (props: ComponentProps) => { }); const levelName = (label: Level, auto?: boolean): string => { - const height = `${label.height}p` + const height = `${label.height}p`; if (auto) return height; return label.uri.includes("original") ? `${t("player.transmux")} (${height})` : height; - } + }; return ( diff --git a/front/yarn.lock b/front/yarn.lock index d91149aa..023b71d2 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -12851,6 +12851,13 @@ __metadata: languageName: node 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": version: 8.0.1 resolution: "ssri@npm:8.0.1" @@ -14142,6 +14149,7 @@ __metadata: react-native-video: ^6.0.0-alpha.5 react-native-web: 0.19.1 solito: ^3.0.0 + srt-webvtt: ^2.0.0 superjson: ^1.12.2 sweetalert2: ^11.7.12 typescript: ^4.9.5