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-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",

View File

@ -18,7 +18,7 @@
* 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 {
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<HTMLVideoElement>, value: Subtitle | null, fonts?: string[]) => {
const useSubtitle = (
player: RefObject<HTMLVideoElement>,
value: Subtitle | null,
fonts?: string[],
) => {
useEffect(() => {
if (!player.current) return;
@ -224,20 +228,23 @@ const useSubtitle = (player: RefObject<HTMLVideoElement>, 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<HTMLVideoElement>, 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<typeof Menu>) => {
if (!hls || hls.audioTracks.length < 2) return null;
return (
@ -283,10 +303,10 @@ export const QualitiesMenu = (props: ComponentProps<typeof Menu>) => {
});
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 (
<Menu {...props}>

View File

@ -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