diff --git a/env.d.ts b/env.d.ts index 11f02fe..95880d3 100644 --- a/env.d.ts +++ b/env.d.ts @@ -1 +1,3 @@ /// + +declare module "libass-wasm"; diff --git a/package-lock.json b/package-lock.json index e18a509..23e85ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "hls.js": "^1.5.7", "less": "^4.1.3", "less-loader": "^11.1.3", + "libass-wasm": "^4.1.0", "lodash": "^4.17.21", "mpegts.js": "^1.7.3", "nprogress": "^0.2.0", @@ -3962,6 +3963,11 @@ "node": ">= 0.8.0" } }, + "node_modules/libass-wasm": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/libass-wasm/-/libass-wasm-4.1.0.tgz", + "integrity": "sha512-+RbYT/uuI6VHExCmGyUuMg3A2gQOaCRTzSn8GGDSf3q4cEoUNiINd9u4RGfZXA1UKafW+Hv8bmcKIX4FKbSh0Q==" + }, "node_modules/lie": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", diff --git a/package.json b/package.json index a749b98..dd3e3fa 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "hls.js": "^1.5.7", "less": "^4.1.3", "less-loader": "^11.1.3", + "libass-wasm": "^4.1.0", "lodash": "^4.17.21", "mpegts.js": "^1.7.3", "nprogress": "^0.2.0", diff --git a/src/hooks/useMovie.ts b/src/hooks/useMovie.ts index 9cef361..b12493a 100644 --- a/src/hooks/useMovie.ts +++ b/src/hooks/useMovie.ts @@ -56,12 +56,28 @@ export const useMovieApi = (roomToken: string) => { headers: { Authorization: roomToken } }); - if (currentMovie.value) { - console.log(currentMovie.value); - room.currentMovie = currentMovie.value.movie; - room.currentStatus = currentMovie.value.status; - room.currentExpireId = currentMovie.value.expireId; + if (!currentMovie.value) return; + + room.currentMovie = currentMovie.value.movie; + room.currentStatus = currentMovie.value.status; + room.currentExpireId = currentMovie.value.expireId; + + const url = currentMovie.value.movie.base.url; + // when cross origin, add token to headers and query + if (url.startsWith(window.location.origin) || url.startsWith("/api/movie")) { + room.currentMovie.base.url = url.includes("?") + ? `${url}&token=${roomToken}` + : `${url}?token=${roomToken}`; } + + const defaultSubtitle = currentMovie.value.movie.base.subtitles; + for (let key in defaultSubtitle) { + if (defaultSubtitle[key].url.includes("token=")) continue; + defaultSubtitle[key].url.includes("?") + ? (defaultSubtitle[key].url = `${defaultSubtitle[key].url}&token=${roomToken}`) + : (defaultSubtitle[key].url = `${defaultSubtitle[key].url}?token=${roomToken}`); + } + room.currentMovie.base.subtitles = defaultSubtitle; } catch (err: any) { console.log(err); ElNotification({ diff --git a/src/plugins/artplayer-plugin-ass/fonts/SourceHanSansCN-Bold.woff2 b/src/plugins/artplayer-plugin-ass/fonts/SourceHanSansCN-Bold.woff2 new file mode 100644 index 0000000..28d1506 Binary files /dev/null and b/src/plugins/artplayer-plugin-ass/fonts/SourceHanSansCN-Bold.woff2 differ diff --git a/src/plugins/artplayer-plugin-ass/fonts/TimesNewRoman.ttf b/src/plugins/artplayer-plugin-ass/fonts/TimesNewRoman.ttf new file mode 100644 index 0000000..eaf5e11 Binary files /dev/null and b/src/plugins/artplayer-plugin-ass/fonts/TimesNewRoman.ttf differ diff --git a/src/plugins/artplayer-plugin-ass/index.d.ts b/src/plugins/artplayer-plugin-ass/index.d.ts new file mode 100644 index 0000000..4b51618 --- /dev/null +++ b/src/plugins/artplayer-plugin-ass/index.d.ts @@ -0,0 +1,12 @@ +import type Artplayer from "artplayer" +import type SubtitlesOctopus from "libass-wasm" +import { type Options } from "libass-wasm" + +export = artplayerPluginAss +export as namespace artplayerPluginAss +type Ass = { + name: "artplayerPluginAss" + instance: SubtitlesOctopus +} + +declare const artplayerPluginAss: (options: Options) => (art: Artplayer) => Ass diff --git a/src/plugins/artplayer-plugin-ass/index.js b/src/plugins/artplayer-plugin-ass/index.js new file mode 100644 index 0000000..a336868 --- /dev/null +++ b/src/plugins/artplayer-plugin-ass/index.js @@ -0,0 +1,110 @@ +/** + * ref: https://github.com/alist-org/alist-web/blob/main/src/components/artplayer-plugin-ass/index.js + */ +import SubtitlesOctopus from "libass-wasm" +import workerUrl from "libass-wasm/dist/js/subtitles-octopus-worker.js?url" +import wasmUrl from "libass-wasm/dist/js/subtitles-octopus-worker.wasm?url" + +import TimesNewRomanFont from "./fonts/TimesNewRoman.ttf?url" +import fallbackFont from "./fonts/SourceHanSansCN-Bold.woff2?url" + +let instance = null + +function isAbsoluteUrl(url) { + return /^https?:\/\//.test(url) +} + +function toAbsoluteUrl(url) { + if (isAbsoluteUrl(url)) return url + + // handle absolute URL when the `Worker` of `BLOB` type loading network resources + return new URL(url, document.baseURI).toString() +} + +function loadWorker({ workerUrl, wasmUrl }) { + return new Promise((resolve) => { + fetch(workerUrl) + .then((res) => res.text()) + .then((text) => { + let workerScriptContent = text + + workerScriptContent = workerScriptContent.replace( + /wasmBinaryFile\s*=\s*"(subtitles-octopus-worker\.wasm)"/g, + (_match, wasm) => { + if (!wasmUrl) { + wasmUrl = new URL(wasm, toAbsoluteUrl(workerUrl)).toString() + } else { + wasmUrl = toAbsoluteUrl(wasmUrl) + } + + return `wasmBinaryFile = "${wasmUrl}"` + }, + ) + + const workerBlob = new Blob([workerScriptContent], { + type: "text/javascript", + }) + resolve(URL.createObjectURL(workerBlob)) + }) + }) +} + +function setVisible(visible) { + if (instance.canvasParent) + instance.canvasParent.style.display = visible ? "block" : "none" +} + +function artplayerPluginAss(options) { + return async (art) => { + instance = new SubtitlesOctopus({ + // TODO: load available fonts from manage panel + availableFonts: { + "times new roman": toAbsoluteUrl(TimesNewRomanFont), + }, + workerUrl: await loadWorker({ workerUrl, wasmUrl }), + fallbackFont: toAbsoluteUrl(fallbackFont), + video: art.template.$video, + ...options, + }) + + instance.canvasParent.className = "artplayer-plugin-ass" + instance.canvasParent.style.cssText = ` + position: absolute; + width: 100%; + height: 100%; + user-select: none; + pointer-events: none; + z-index: 20; + ` + // switch subtitle track + art.on("artplayer-plugin-ass:switch", (subtitle) => { + let newSubAddr + if (subtitle.startsWith('/api/movie/proxy/')) { + newSubAddr = window.location.origin + subtitle + } else { + newSubAddr = subtitle + } + instance.freeTrack() + instance.setTrackByUrl(newSubAddr) + console.log("plugin->切换字幕:", newSubAddr); + setVisible(true) + }) + + // set subtitle visible + art.on("subtitle", (visible) => setVisible(visible)) + art.on("artplayer-plugin-ass:visible", (visible) => setVisible(visible)) + + // set subtitle offset + art.on("subtitleOffset", (offset) => (instance.timeOffset = offset)) + + // when player destory + art.on("destroy", () => instance.dispose()) + + return { + name: "artplayerPluginAss", + instance: instance, + } + } +} + +export default artplayerPluginAss diff --git a/src/plugins/subtitle.ts b/src/plugins/subtitle.ts index bba487b..045c2c7 100644 --- a/src/plugins/subtitle.ts +++ b/src/plugins/subtitle.ts @@ -1,5 +1,7 @@ import type Artplayer from "artplayer"; import type { ComponentOption } from "artplayer/types/component"; +import type { Events } from "artplayer/types/events"; +import { ElMessage } from "element-plus"; const newSubtitleHtml = (name: string): HTMLElement => { const SubtitleHtml = document.createElement("span"); @@ -39,10 +41,19 @@ export const newSubtitleControl = ( }; }), onSelect(this: Artplayer, selector: any) { - console.log("切换字幕:", selector); if (selector.html === "关闭") { this.subtitle.show = false; + this.emit("artplayer-plugin-ass:visible" as keyof Events, false); + } else if (selector.type.toLowerCase() === "ass") { + let newUrl; + if (selector.url.startsWith("/api/movie/proxy/")) { + newUrl + window.location.origin + selector.url; + } else if (!selector.url.startsWith("http")) return ElMessage.error("无效的字幕地址"); + newUrl = selector.url; + this.subtitle.show = false; + this.emit("artplayer-plugin-ass:switch" as keyof Events, newUrl); } else { + this.emit("artplayer-plugin-ass:visible" as keyof Events, false); this.subtitle.switch(selector.url, { type: selector.type }); this.subtitle.show = true; } diff --git a/src/views/Cinema.vue b/src/views/Cinema.vue index aa7bd82..9cf9e5d 100644 --- a/src/views/Cinema.vue +++ b/src/views/Cinema.vue @@ -25,6 +25,7 @@ import MovieList from "@/components/cinema/MovieList.vue"; import MoviePush from "@/components/cinema/MoviePush.vue"; import type { Subtitles } from "@/types/Movie"; import { RoomMemberPermission } from "@/types/Room"; +import artplayerPluginAss from "@/plugins/artplayer-plugin-ass"; const Player = defineAsyncComponent(() => import("@/components/Player.vue")); @@ -139,18 +140,50 @@ const playerOption = computed(() => { newLazyInitSyncPlugin(room.currentExpireId) ] }; - // when cross origin, add token to headers and query - if (option.url.startsWith(window.location.origin) || option.url.startsWith("/api/movie")) { - // option.headers = { - // ...option.headers, - // Authorization: roomToken.value - // }; - option.url = option.url.includes("?") - ? `${option.url}&token=${roomToken.value}` - : `${option.url}?token=${roomToken.value}`; - } + if (room.currentMovie.base!.subtitles) { + let defaultUrl; + let useAssPlugin = false; + + const defaultSubtitle = room.currentMovie.base!.subtitles; + + for (let key in defaultSubtitle) { + if (defaultSubtitle[key].hasOwnProperty("url")) { + if (defaultSubtitle[key].type === "ass") { + if (defaultSubtitle[key].url.startsWith("/api/movie/proxy/")) { + defaultUrl = window.location.origin + room.currentMovie.base!.subtitles[key].url; + useAssPlugin = true; + break; + } + + if (defaultSubtitle[key].url.startsWith("http")) { + defaultUrl = room.currentMovie.base!.subtitles[key].url; + useAssPlugin = true; + break; + } + + ElNotification.error({ + title: "错误", + message: "字幕文件地址错误!ASS字幕解析将失效!" + }); + useAssPlugin = false; + break; + } else { + useAssPlugin = false; + break; + } + } else break; + } + option.plugins!.push(newLazyInitSubtitlePlugin(room.currentMovie.base!.subtitles)); + // return; + useAssPlugin && + option.plugins!.push( + artplayerPluginAss({ + // debug: true, + subUrl: defaultUrl + }) + ); } return option; diff --git a/tsconfig.json b/tsconfig.json index 7710be2..7833fa4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,7 @@ "compilerOptions": { "target": "ES2016", "noImplicitAny": true, - "moduleResolution": "bundler" + "moduleResolution": "bundler", + "allowJs": true, } }