mirror of
https://github.com/cloudreve/frontend.git
synced 2025-12-25 19:52:48 +00:00
feat(video player): support m3u8 and relative path transform (#2288)
This commit is contained in:
parent
8c26d11734
commit
587e9f29cf
|
|
@ -29,9 +29,11 @@
|
|||
"@uiw/react-color-sketch": "^2.1.1",
|
||||
"artplayer": "5.2.2",
|
||||
"artplayer-plugin-chapter": "^1.0.0",
|
||||
"artplayer-plugin-hls-control": "^1.0.1",
|
||||
"axios": "^1.6.2",
|
||||
"dayjs": "^1.11.10",
|
||||
"fuse.js": "^7.0.0",
|
||||
"hls.js": "^1.6.2",
|
||||
"i18next": "^23.7.11",
|
||||
"i18next-browser-languagedetector": "^7.2.0",
|
||||
"i18next-chained-backend": "^4.6.2",
|
||||
|
|
|
|||
|
|
@ -111,6 +111,10 @@
|
|||
"dashboard": "Dashboard"
|
||||
},
|
||||
"fileManager": {
|
||||
"quality": "Quality",
|
||||
"audioTrack": "Audio",
|
||||
"auto": "Auto",
|
||||
"default": "Default",
|
||||
"shareWithMeEmpty": "No shared files found",
|
||||
"shareWithMeEmptyDes": "If you need to see others' shares here, please save the shortcut to any location in your files when you visit a shared link.",
|
||||
"selectAll": "Select all",
|
||||
|
|
|
|||
|
|
@ -112,6 +112,10 @@
|
|||
"dashboard": "管理面板"
|
||||
},
|
||||
"fileManager": {
|
||||
"quality": "清晰度",
|
||||
"audioTrack": "音轨",
|
||||
"auto": "自动",
|
||||
"default": "默认",
|
||||
"shareWithMeEmpty": "没有找到别人的分享",
|
||||
"shareWithMeEmptyDes": "如需要在此看到别人的分享,请在访问别人分享链接时,在右上角将快捷方式保存到你的文件中的任意位置。",
|
||||
"selectAll": "全选",
|
||||
|
|
|
|||
|
|
@ -112,6 +112,10 @@
|
|||
"dashboard": "管理面板"
|
||||
},
|
||||
"fileManager": {
|
||||
"quality": "清晰度",
|
||||
"audioTrack": "音軌",
|
||||
"auto": "自動",
|
||||
"default": "預設",
|
||||
"shareWithMeEmpty": "沒有找到別人的分享",
|
||||
"shareWithMeEmptyDes": "如需要在此看到別人的分享,請在訪問別人分享連結時,在右上角將快捷方式保存到你的文件中的任意位置。",
|
||||
"selectAll": "全選",
|
||||
|
|
|
|||
|
|
@ -1,19 +1,107 @@
|
|||
import { useEffect, useRef } from "react";
|
||||
import Artplayer from "artplayer";
|
||||
import { Box, BoxProps } from "@mui/material";
|
||||
import "./artplayer.css";
|
||||
import Artplayer from "artplayer";
|
||||
import artplayerPluginChapter from "artplayer-plugin-chapter";
|
||||
import artplayerPluginHlsControl from "artplayer-plugin-hls-control";
|
||||
import Hls, { HlsConfig } from "hls.js";
|
||||
import i18next from "i18next";
|
||||
import { useEffect, useRef } from "react";
|
||||
import "./artplayer.css";
|
||||
export const CrMaskedPrefix = "https://cloudreve_masked/";
|
||||
|
||||
export interface PlayerProps extends BoxProps {
|
||||
option: any;
|
||||
getInstance?: (instance: Artplayer) => void;
|
||||
chapters?: any;
|
||||
m3u8UrlTransform?: (url: string, isPlaylist?: boolean) => Promise<string>;
|
||||
getEntityUrl?: (url: string) => Promise<string>;
|
||||
}
|
||||
|
||||
const playM3u8 =
|
||||
(
|
||||
urlTransform?: (url: string, isPlaylist?: boolean) => Promise<string>,
|
||||
getEntityUrl?: (url: string) => Promise<string>,
|
||||
) =>
|
||||
(video: HTMLVideoElement, url: string, art: Artplayer) => {
|
||||
if (Hls.isSupported()) {
|
||||
if (art.hls) art.hls.destroy();
|
||||
const hls = new Hls({
|
||||
fLoader: class extends Hls.DefaultConfig.loader {
|
||||
constructor(config: HlsConfig) {
|
||||
super(config);
|
||||
var load = this.load.bind(this);
|
||||
this.load = function (context, config, callbacks) {
|
||||
console.log("fragment loader", context);
|
||||
if (urlTransform) {
|
||||
urlTransform(context.url).then((url) => {
|
||||
console.log(url);
|
||||
console.log({ ...context, frag: { ...context.frag, relurl: url, _url: url }, url });
|
||||
const complete = callbacks.onSuccess;
|
||||
callbacks.onSuccess = (loaderResponse, stats, successContext, networkDetails) => {
|
||||
// Do something with loaderResponse.data
|
||||
loaderResponse.url = url;
|
||||
console.log("fragment loader success", loaderResponse);
|
||||
complete(loaderResponse, stats, successContext, networkDetails);
|
||||
};
|
||||
load({ ...context, frag: { ...context.frag, relurl: url, _url: url }, url }, config, callbacks);
|
||||
});
|
||||
} else {
|
||||
load(context, config, callbacks);
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
pLoader: class extends Hls.DefaultConfig.loader {
|
||||
constructor(config: HlsConfig) {
|
||||
super(config);
|
||||
var load = this.load.bind(this);
|
||||
this.load = function (context, config, callbacks) {
|
||||
console.log("playlist loader", context);
|
||||
if (urlTransform) {
|
||||
urlTransform(context.url, true).then((url) => {
|
||||
console.log(url);
|
||||
const complete = callbacks.onSuccess;
|
||||
callbacks.onSuccess = (loaderResponse, stats, successContext, networkDetails) => {
|
||||
// Do something with loaderResponse.data
|
||||
loaderResponse.url = url;
|
||||
console.log("playlist loader success", loaderResponse);
|
||||
complete(loaderResponse, stats, successContext, networkDetails);
|
||||
};
|
||||
load({ ...context, url }, config, callbacks);
|
||||
});
|
||||
} else {
|
||||
load(context, config, callbacks);
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
xhrSetup: async (xhr, url) => {
|
||||
console.log("xhrSetup", xhr, url);
|
||||
// Always send cookies, even for cross-origin calls.
|
||||
if (url.startsWith(CrMaskedPrefix)) {
|
||||
if (getEntityUrl) {
|
||||
xhr.open("GET", await getEntityUrl(url), true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
hls.loadSource(url);
|
||||
hls.attachMedia(video);
|
||||
art.hls = hls;
|
||||
art.on("destroy", () => hls.destroy());
|
||||
} else if (video.canPlayType("application/vnd.apple.mpegurl")) {
|
||||
video.src = url;
|
||||
} else {
|
||||
art.notice.show = "Unsupported playback format: m3u8";
|
||||
}
|
||||
};
|
||||
|
||||
export default function Player({
|
||||
option,
|
||||
chapters,
|
||||
getInstance,
|
||||
m3u8UrlTransform,
|
||||
getEntityUrl,
|
||||
...rest
|
||||
}: PlayerProps) {
|
||||
const artRef = useRef<Artplayer>();
|
||||
|
|
@ -21,8 +109,38 @@ export default function Player({
|
|||
useEffect(() => {
|
||||
const opts = {
|
||||
...option,
|
||||
plugins: [...option.plugins],
|
||||
plugins: [
|
||||
...option.plugins,
|
||||
artplayerPluginHlsControl({
|
||||
quality: {
|
||||
// Show qualitys in control
|
||||
control: true,
|
||||
// Show qualitys in setting
|
||||
setting: true,
|
||||
// Get the quality name from level
|
||||
getName: (level) => (level.height ? level.height + "P" : i18next.t("application:fileManager.default")),
|
||||
// I18n
|
||||
title: i18next.t("application:fileManager.quality"),
|
||||
auto: i18next.t("application:fileManager.auto"),
|
||||
},
|
||||
audio: {
|
||||
// Show audios in control
|
||||
control: true,
|
||||
// Show audios in setting
|
||||
setting: true,
|
||||
// Get the audio name from track
|
||||
getName: (track) => track.name,
|
||||
// I18n
|
||||
title: i18next.t("application:fileManager.audioTrack"),
|
||||
auto: i18next.t("application:fileManager.auto"),
|
||||
},
|
||||
}),
|
||||
],
|
||||
container: artRef.current,
|
||||
customType: {
|
||||
...option.customType,
|
||||
m3u8: playM3u8(m3u8UrlTransform, getEntityUrl),
|
||||
},
|
||||
};
|
||||
|
||||
if (chapters) {
|
||||
|
|
|
|||
|
|
@ -1,44 +1,27 @@
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts";
|
||||
import ViewerDialog, { ViewerLoading } from "../ViewerDialog.tsx";
|
||||
import React, {
|
||||
lazy,
|
||||
Suspense,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { closeVideoViewer } from "../../../redux/globalStateSlice.ts";
|
||||
import {
|
||||
Box,
|
||||
IconButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Menu,
|
||||
Tooltip,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { getFileEntityUrl } from "../../../api/api.ts";
|
||||
import { fileExtension, fileNameNoExt, getFileLinkedUri } from "../../../util";
|
||||
import { Box, IconButton, ListItemIcon, ListItemText, Menu, Tooltip, Typography, useTheme } from "@mui/material";
|
||||
import Artplayer from "artplayer";
|
||||
import dayjs from "dayjs";
|
||||
import React, { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getFileEntityUrl } from "../../../api/api.ts";
|
||||
import { FileResponse } from "../../../api/explorer.ts";
|
||||
import { closeVideoViewer } from "../../../redux/globalStateSlice.ts";
|
||||
import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts";
|
||||
import { findSubtitleOptions } from "../../../redux/thunks/viewer.ts";
|
||||
import Subtitles from "../../Icons/Subtitles.tsx";
|
||||
import {
|
||||
DenseDivider,
|
||||
SquareMenuItem,
|
||||
} from "../../FileManager/ContextMenu/ContextMenu.tsx";
|
||||
import Checkmark from "../../Icons/Checkmark.tsx";
|
||||
import SessionManager, { UserSettings } from "../../../session";
|
||||
import { fileExtension, fileNameNoExt, getFileLinkedUri } from "../../../util";
|
||||
import CrUri from "../../../util/uri.ts";
|
||||
import { DenseDivider, SquareMenuItem } from "../../FileManager/ContextMenu/ContextMenu.tsx";
|
||||
import Checkmark from "../../Icons/Checkmark.tsx";
|
||||
import Subtitles from "../../Icons/Subtitles.tsx";
|
||||
import TextEditStyle from "../../Icons/TextEditStyle.tsx";
|
||||
import ViewerDialog, { ViewerLoading } from "../ViewerDialog.tsx";
|
||||
import SubtitleStyleDialog from "./SubtitleStyleDialog.tsx";
|
||||
|
||||
const Player = lazy(() => import("./Artplayer.tsx"));
|
||||
|
||||
export const CrMaskedPrefix = "https://cloudreve_masked/";
|
||||
|
||||
export interface SubtitleStyle {
|
||||
fontSize?: number;
|
||||
fontColor?: string;
|
||||
|
|
@ -54,116 +37,16 @@ const VideoViewer = () => {
|
|||
const [loaded, setLoaded] = useState(false);
|
||||
const [art, setArt] = useState<Artplayer | null>(null);
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const currentExpire = useRef<ReturnType<typeof setTimeout> | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const currentExpire = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||
const [subtitles, setSubtitles] = useState<FileResponse[]>([]);
|
||||
const [subtitleSelected, setSubtitleSelected] = useState<FileResponse | null>(
|
||||
null,
|
||||
);
|
||||
const [subtitleSelected, setSubtitleSelected] = useState<FileResponse | null>(null);
|
||||
const [subtitleStyleOpen, setSubtitleStyleOpen] = useState(false);
|
||||
const currentUrl = useRef<string | null>(null);
|
||||
|
||||
const subtitleStyle = useMemo(() => {
|
||||
return SessionManager.getWithFallback(
|
||||
UserSettings.SubtitleStyle,
|
||||
) as SubtitleStyle;
|
||||
return SessionManager.getWithFallback(UserSettings.SubtitleStyle) as SubtitleStyle;
|
||||
}, []);
|
||||
|
||||
// refresh video src before entity url expires
|
||||
const refreshSrc = useCallback(() => {
|
||||
if (!viewerState || !viewerState.file || !art) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstLoad = !currentExpire.current;
|
||||
|
||||
dispatch(
|
||||
getFileEntityUrl({
|
||||
uris: [getFileLinkedUri(viewerState.file)],
|
||||
entity: viewerState.version,
|
||||
}),
|
||||
)
|
||||
.then((res) => {
|
||||
const current = art.currentTime;
|
||||
|
||||
let timeOut =
|
||||
dayjs(res.expires).diff(dayjs(), "millisecond") - srcRefreshMargin;
|
||||
if (timeOut < 0) {
|
||||
timeOut = 2000;
|
||||
}
|
||||
currentExpire.current = setTimeout(refreshSrc, timeOut);
|
||||
|
||||
art.switchUrl(res.urls[0]).then(() => {
|
||||
art.currentTime = current;
|
||||
});
|
||||
|
||||
if (firstLoad) {
|
||||
const subs = dispatch(findSubtitleOptions());
|
||||
setSubtitles(subs);
|
||||
if (
|
||||
subs.length > 0 &&
|
||||
subs[0].name.startsWith(fileNameNoExt(viewerState.file.name) + ".")
|
||||
) {
|
||||
switchSubtitle(subs[0]);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
onClose();
|
||||
});
|
||||
}, [viewerState?.file, art]);
|
||||
|
||||
const chapters = useMemo(() => {
|
||||
if (!viewerState || !viewerState.file?.metadata) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const chapterMap: {
|
||||
[key: string]: {
|
||||
start: number;
|
||||
end: number;
|
||||
title: string;
|
||||
};
|
||||
} = {};
|
||||
|
||||
Object.keys(viewerState.file.metadata).map((k) => {
|
||||
if (k.startsWith("stream:chapter_")) {
|
||||
const id = k.split("_")[1];
|
||||
// type = remove prefix
|
||||
const type = k.replace(`stream:chapter_${id}_`, "");
|
||||
if (!chapterMap[id]) {
|
||||
chapterMap[id] = {
|
||||
start: 0,
|
||||
end: 0,
|
||||
title: "",
|
||||
};
|
||||
}
|
||||
switch (type) {
|
||||
case "start_time":
|
||||
chapterMap[id].start = parseFloat(
|
||||
viewerState.file?.metadata?.[k] ?? "0",
|
||||
);
|
||||
break;
|
||||
case "end_time":
|
||||
chapterMap[id].end = parseFloat(
|
||||
viewerState.file?.metadata?.[k] ?? "0",
|
||||
);
|
||||
break;
|
||||
case "name":
|
||||
chapterMap[id].title = viewerState.file?.metadata?.[k] ?? "";
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return Object.values(chapterMap).map((c) => ({
|
||||
start: c.start,
|
||||
end: c.end,
|
||||
title: c.title,
|
||||
}));
|
||||
}, [viewerState]);
|
||||
|
||||
const switchSubtitle = useCallback(
|
||||
async (subtitle?: FileResponse) => {
|
||||
if (!art) {
|
||||
|
|
@ -198,6 +81,115 @@ const VideoViewer = () => {
|
|||
[art],
|
||||
);
|
||||
|
||||
const loadSubtitles = useCallback(() => {
|
||||
if (!viewerState?.file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const subs = dispatch(findSubtitleOptions());
|
||||
setSubtitles(subs);
|
||||
if (subs.length > 0 && subs[0].name.startsWith(fileNameNoExt(viewerState.file.name) + ".")) {
|
||||
switchSubtitle(subs[0]);
|
||||
}
|
||||
}, [viewerState?.file, switchSubtitle]);
|
||||
|
||||
// refresh video src before entity url expires
|
||||
const refreshSrc = useCallback(() => {
|
||||
if (!viewerState || !viewerState.file || !art) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstLoad = !currentExpire.current;
|
||||
const isM3u8 = viewerState.file.name.endsWith(".m3u8");
|
||||
if (isM3u8) {
|
||||
// For m3u8, use masked url
|
||||
const crFileUrl = new CrUri(getFileLinkedUri(viewerState.file));
|
||||
const maskedUrl = `${CrMaskedPrefix}${crFileUrl.path()}`;
|
||||
art.switchUrl(maskedUrl);
|
||||
loadSubtitles();
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(
|
||||
getFileEntityUrl({
|
||||
uris: [getFileLinkedUri(viewerState.file)],
|
||||
entity: viewerState.version,
|
||||
}),
|
||||
)
|
||||
.then((res) => {
|
||||
const current = art.currentTime;
|
||||
currentUrl.current = res.urls[0];
|
||||
|
||||
let timeOut = dayjs(res.expires).diff(dayjs(), "millisecond") - srcRefreshMargin;
|
||||
if (timeOut < 0) {
|
||||
timeOut = 2000;
|
||||
}
|
||||
currentExpire.current = setTimeout(refreshSrc, timeOut);
|
||||
|
||||
art.switchUrl(res.urls[0]).then(() => {
|
||||
art.currentTime = current;
|
||||
});
|
||||
|
||||
if (firstLoad) {
|
||||
const subs = dispatch(findSubtitleOptions());
|
||||
setSubtitles(subs);
|
||||
if (subs.length > 0 && subs[0].name.startsWith(fileNameNoExt(viewerState.file.name) + ".")) {
|
||||
switchSubtitle(subs[0]);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
onClose();
|
||||
});
|
||||
}, [viewerState?.file, art, loadSubtitles]);
|
||||
|
||||
const chapters = useMemo(() => {
|
||||
if (!viewerState || !viewerState.file?.metadata) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const chapterMap: {
|
||||
[key: string]: {
|
||||
start: number;
|
||||
end: number;
|
||||
title: string;
|
||||
};
|
||||
} = {};
|
||||
|
||||
Object.keys(viewerState.file.metadata).map((k) => {
|
||||
if (k.startsWith("stream:chapter_")) {
|
||||
const id = k.split("_")[1];
|
||||
// type = remove prefix
|
||||
const type = k.replace(`stream:chapter_${id}_`, "");
|
||||
if (!chapterMap[id]) {
|
||||
chapterMap[id] = {
|
||||
start: 0,
|
||||
end: 0,
|
||||
title: "",
|
||||
};
|
||||
}
|
||||
switch (type) {
|
||||
case "start_time":
|
||||
chapterMap[id].start = parseFloat(viewerState.file?.metadata?.[k] ?? "0");
|
||||
break;
|
||||
case "end_time":
|
||||
chapterMap[id].end = parseFloat(viewerState.file?.metadata?.[k] ?? "0");
|
||||
break;
|
||||
case "name":
|
||||
chapterMap[id].title = viewerState.file?.metadata?.[k] ?? "";
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return Object.values(chapterMap).map((c) => ({
|
||||
start: c.start,
|
||||
end: c.end,
|
||||
title: c.title,
|
||||
}));
|
||||
}, [viewerState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!art) {
|
||||
return;
|
||||
|
|
@ -260,6 +252,89 @@ const VideoViewer = () => {
|
|||
[art],
|
||||
);
|
||||
|
||||
const m3u8UrlTransform = useCallback(
|
||||
async (url: string, isPlaylist?: boolean): Promise<string> => {
|
||||
let realUrl = "";
|
||||
if (isPlaylist) {
|
||||
// Loading playlist
|
||||
|
||||
if (!currentUrl.current) {
|
||||
return url;
|
||||
}
|
||||
|
||||
const currentParsed = new URL(currentUrl.current);
|
||||
const requestParsed = new URL(url);
|
||||
if (currentParsed.origin != requestParsed.origin) {
|
||||
// Playlist is from different origin, return original URL
|
||||
return url;
|
||||
}
|
||||
|
||||
// Trim pfrefix(currentParsed.pathname) of requestParsed.pathname to get relative path
|
||||
const currentPathParts = currentParsed.pathname.split("/");
|
||||
const requestPathParts = requestParsed.pathname.split("/");
|
||||
|
||||
// Find where paths diverge
|
||||
let i = 0;
|
||||
while (
|
||||
i < currentPathParts.length &&
|
||||
i < requestPathParts.length &&
|
||||
currentPathParts[i] === requestPathParts[i]
|
||||
) {
|
||||
i++;
|
||||
}
|
||||
|
||||
// Get relative path by joining remaining parts
|
||||
const relativePath = requestPathParts.slice(i).join("/");
|
||||
|
||||
if (!viewerState?.file) {
|
||||
return url;
|
||||
}
|
||||
|
||||
const currentFileUrl = new CrUri(getFileLinkedUri(viewerState?.file));
|
||||
const base = i == 0 ? new CrUri(currentFileUrl.base()) : currentFileUrl.parent();
|
||||
realUrl = base.join(relativePath).path();
|
||||
return `${CrMaskedPrefix}${realUrl}`;
|
||||
} else {
|
||||
// Loading fragment
|
||||
if (url.startsWith("http://") || url.startsWith("https://") || !viewerState?.file) {
|
||||
// If fragment URL is not a path, return it
|
||||
return url;
|
||||
}
|
||||
|
||||
// Request real fragment/playlist URL
|
||||
const currentFileUrl = new CrUri(getFileLinkedUri(viewerState?.file));
|
||||
const base = url.startsWith("/") ? new CrUri(currentFileUrl.base()) : currentFileUrl.parent();
|
||||
realUrl = base.join(url).path();
|
||||
return `${CrMaskedPrefix}${realUrl}`;
|
||||
}
|
||||
},
|
||||
[viewerState?.file],
|
||||
);
|
||||
|
||||
const getUnmaskedEntityUrl = useCallback(
|
||||
async (url: string) => {
|
||||
if (!viewerState?.file) {
|
||||
return url;
|
||||
}
|
||||
// remove cloudreve_masked prefix of url
|
||||
if (!url.startsWith(CrMaskedPrefix)) {
|
||||
return url;
|
||||
}
|
||||
url = url.replace(CrMaskedPrefix, "");
|
||||
const currentFileUrl = new CrUri(getFileLinkedUri(viewerState.file));
|
||||
const base = new CrUri(currentFileUrl.base());
|
||||
const realUrl = base.join(url);
|
||||
try {
|
||||
const res = await dispatch(getFileEntityUrl({ uris: [realUrl.toString()] }));
|
||||
return res.urls[0];
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return url;
|
||||
}
|
||||
},
|
||||
[dispatch, viewerState?.file],
|
||||
);
|
||||
|
||||
// TODO: Add artplayer-plugin-chapter after it's released to npm
|
||||
return (
|
||||
<ViewerDialog
|
||||
|
|
@ -304,9 +379,7 @@ const VideoViewer = () => {
|
|||
<ListItemIcon>
|
||||
<TextEditStyle fontSize={"small"} />{" "}
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
{t("application:fileManager.subtitleStyles")}
|
||||
</ListItemText>
|
||||
<ListItemText>{t("application:fileManager.subtitleStyles")}</ListItemText>
|
||||
</SquareMenuItem>
|
||||
<DenseDivider />
|
||||
{subtitles.length == 0 && (
|
||||
|
|
@ -319,9 +392,7 @@ const VideoViewer = () => {
|
|||
{subtitles.length > 0 && (
|
||||
<SquareMenuItem onClick={() => switchSubtitle()} dense>
|
||||
<em>
|
||||
<ListItemText
|
||||
primary={t("application:fileManager.disableSubtitle")}
|
||||
/>
|
||||
<ListItemText primary={t("application:fileManager.disableSubtitle")} />
|
||||
</em>
|
||||
</SquareMenuItem>
|
||||
)}
|
||||
|
|
@ -337,7 +408,7 @@ const VideoViewer = () => {
|
|||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
},
|
||||
}
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{subtitleSelected?.id == sub.id && (
|
||||
|
|
@ -352,6 +423,8 @@ const VideoViewer = () => {
|
|||
<Suspense fallback={<ViewerLoading minHeight={"calc(100vh - 350px)"} />}>
|
||||
<Player
|
||||
key={viewerState?.file?.path}
|
||||
m3u8UrlTransform={m3u8UrlTransform}
|
||||
getEntityUrl={getUnmaskedEntityUrl}
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
|
|
|
|||
10
yarn.lock
10
yarn.lock
|
|
@ -3833,6 +3833,11 @@ artplayer-plugin-chapter@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/artplayer-plugin-chapter/-/artplayer-plugin-chapter-1.0.0.tgz#cb921176bc76550d9238498774d76bd0ed41b05f"
|
||||
integrity sha512-moT7syJPeXQIv4xgm5QVucZB/5n0yMqfR6iscAyxyUIBJ08cVRmazBGRA82BSBZPqmgvSmH6bl0u8pMyEI+VcQ==
|
||||
|
||||
artplayer-plugin-hls-control@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/artplayer-plugin-hls-control/-/artplayer-plugin-hls-control-1.0.1.tgz#7d28905340d5c04121df450f80ca0eb2878ce773"
|
||||
integrity sha512-rbOeH/mzqgZuosOtxJ9NERil6siOLd9K7nsCRWARrSyT+zH3xdDo0WunYgDQiATniNySicxZ//ex/pPLxYECUg==
|
||||
|
||||
artplayer@5.2.2:
|
||||
version "5.2.2"
|
||||
resolved "https://registry.yarnpkg.com/artplayer/-/artplayer-5.2.2.tgz#889643a8e3ce12042def839b20a02e8538bafb0c"
|
||||
|
|
@ -5301,6 +5306,11 @@ highlight-words-core@^1.2.0:
|
|||
resolved "https://registry.yarnpkg.com/highlight-words-core/-/highlight-words-core-1.2.3.tgz#781f37b2a220bf998114e4ef8c8cb6c7a4802ea8"
|
||||
integrity sha512-m1O9HW3/GNHxzSIXWw1wCNXXsgLlxrP0OI6+ycGUhiUHkikqW3OrwVHz+lxeNBe5yqLESdIcj8PowHQ2zLvUvQ==
|
||||
|
||||
hls.js@^1.6.2:
|
||||
version "1.6.2"
|
||||
resolved "https://registry.yarnpkg.com/hls.js/-/hls.js-1.6.2.tgz#02272bea644b5f61f71741256618d6b629ae7834"
|
||||
integrity sha512-rx+pETSCJEDThm/JCm8CuadcAC410cVjb1XVXFNDKFuylaayHk1+tFxhkjvnMDAfqsJHxZXDAJ3Uc2d5xQyWlQ==
|
||||
|
||||
hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz"
|
||||
|
|
|
|||
Loading…
Reference in New Issue