-
+
+ {description}
+
>['renderImage'];
renderThumbnail?: NonNullable>['renderThumbnail'];
+ /** Replaces the default playback error message. */
+ errorDescription?: ReactNode;
}
-export function VideoSkin({ children, className, renderPoster, renderThumbnail, ...props }: VideoSkinProps = {}) {
+export function VideoSkin({
+ children,
+ className,
+ renderPoster,
+ renderThumbnail,
+ errorDescription,
+ ...props
+}: VideoSkinProps = {}) {
return (
-
+
diff --git a/src/constants/motion.ts b/src/constants/motion.ts
index acf0929..3d74029 100644
--- a/src/constants/motion.ts
+++ b/src/constants/motion.ts
@@ -92,7 +92,9 @@ export const DOT_FIELD = {
decayMs: 350,
/** 点亮后的最大半径(px) */
litRadius: 2.2,
- baseRadius: 1
+ baseRadius: 1,
+ /** 画布像素比上限:点只有 1~2px,更高的像素比只会让栅格化开销成倍增加 */
+ maxDpr: 2
} as const;
export type RippleTone = "primary" | "destructive";
diff --git a/src/constants/torbox.ts b/src/constants/torbox.ts
index 2eadab8..52cc1e8 100644
--- a/src/constants/torbox.ts
+++ b/src/constants/torbox.ts
@@ -3,10 +3,26 @@ import type { SourceType } from "@/types/torbox";
export const TORBOX_API = "https://api.torbox.app/v1/api";
+/** streamType:转码流接口(/stream/createstream)中的类型名,与列表接口不同 */
export const SOURCES = [
- { type: "torrents", list: "/torrents/mylist", idName: "torrent_id" },
- { type: "usenet", list: "/usenet/mylist", idName: "usenet_id" },
- { type: "webdl", list: "/webdl/mylist", idName: "web_id" }
+ {
+ type: "torrents",
+ list: "/torrents/mylist",
+ idName: "torrent_id",
+ streamType: "torrent"
+ },
+ {
+ type: "usenet",
+ list: "/usenet/mylist",
+ idName: "usenet_id",
+ streamType: "usenet"
+ },
+ {
+ type: "webdl",
+ list: "/webdl/mylist",
+ idName: "web_id",
+ streamType: "webdownload"
+ }
] as const;
export const IMAGE_EXT = new Set([
@@ -29,6 +45,12 @@ export const IMAGE_EXT = new Set([
"heif"
]);
+/**
+ * 浏览器能直接播放的视频格式,网页内直接读取原文件;
+ * 其他视频(WMV、AVI、MKV 等)由 TorBox 转码为 HLS 后播放。
+ */
+export const BROWSER_VIDEO_EXT = new Set(["mp4", "m4v", "webm"]);
+
export const VIDEO_EXT = new Set([
"mp4",
"webm",
diff --git a/src/server/torbox.ts b/src/server/torbox.ts
index 37428c8..442aa05 100644
--- a/src/server/torbox.ts
+++ b/src/server/torbox.ts
@@ -164,16 +164,16 @@ export const getLibrary = async ({
return { items, errors };
};
+const MOCK_PLAY_ERROR =
+ "演示模式没有可播放的文件,配置 TORBOX_API_KEY 后可播放真实资源";
+
/** 向 TorBox 换取临时直链(仅服务端使用,不下发给客户端) */
export const requestDownloadUrl = async (
type: SourceType,
id: string,
fileId: string
) => {
- if (isMockMode())
- throw new Error(
- "演示模式没有可播放的文件,配置 TORBOX_API_KEY 后可播放真实资源"
- );
+ if (isMockMode()) throw new Error(MOCK_PLAY_ERROR);
const source = SOURCES.find((s) => s.type === type)!;
const params = new URLSearchParams({
token: apiKey(),
@@ -185,3 +185,27 @@ export const requestDownloadUrl = async (
if (typeof url !== "string" || !url) throw new Error("TorBox 未返回下载地址");
return url;
};
+
+/**
+ * 创建 TorBox 网页转码流(Pro 账号功能),返回 HLS 播放列表地址。
+ * 地址的 token 参数就是 API Key,只在服务端使用,不下发给客户端。
+ * 字幕、音轨、清晰度不传,即无字幕、第一条音轨、原始分辨率。
+ */
+export const createStream = async (
+ type: SourceType,
+ id: string,
+ fileId: string
+) => {
+ if (isMockMode()) throw new Error(MOCK_PLAY_ERROR);
+ const source = SOURCES.find((s) => s.type === type)!;
+ const params = new URLSearchParams({
+ id,
+ file_id: fileId,
+ type: source.streamType
+ });
+ const data = await torboxFetch(`/stream/createstream?${params}`);
+ const url = data?.hls_url;
+ if (typeof url !== "string" || !url)
+ throw new Error("TorBox 未返回转码播放地址");
+ return url;
+};
diff --git a/src/utils/media.ts b/src/utils/media.ts
index ba5e52d..fa16c12 100644
--- a/src/utils/media.ts
+++ b/src/utils/media.ts
@@ -1,5 +1,6 @@
// 媒体文件类型与路径处理函数
import { IMAGE_EXT, VIDEO_EXT } from "@/constants/torbox";
+import type { SourceType } from "@/types/torbox";
export type MediaKind = "video" | "image" | "other";
@@ -18,7 +19,10 @@ export const getFileExt = (name: string) => {
* 缺失、为通用二进制类型(application/octet-stream)或其他类型时按扩展名兜底。
* 例如 rmvb 常被识别为 application/vnd.rn-realmedia,按扩展名仍归为视频。
*/
-export const getMediaKind = (name: string, mimetype?: string | null): MediaKind => {
+export const getMediaKind = (
+ name: string,
+ mimetype?: string | null
+): MediaKind => {
const type = mimetype?.trim().toLowerCase() ?? "";
if (type.startsWith("video/")) return "video";
if (type.startsWith("image/")) return "image";
@@ -43,6 +47,19 @@ export const buildStreamUrl = (
return url.toString();
};
+/** TorBox 转码流(HLS)的播放列表地址,由服务端转发 */
+export const buildHlsUrl = (
+ origin: string,
+ source: SourceType,
+ itemId: number,
+ fileId: number,
+ token: string
+) => {
+ const url = new URL(`/api/hls/${source}/${itemId}/${fileId}`, origin);
+ url.searchParams.set("token", token);
+ return url.toString();
+};
+
export const toPotPlayerUrl = (streamUrl: string) => `potplayer://${streamUrl}`;
/** 全部资源的 M3U 播放列表地址,供 PotPlayer 等播放器直接打开 */