Feat: ass subtitle support

This commit is contained in:
Lazy 2024-05-09 23:44:04 +08:00
parent c25efae761
commit 4b689b4495
11 changed files with 209 additions and 17 deletions

2
env.d.ts vendored
View File

@ -1 +1,3 @@
/// <reference types="vite/client" />
declare module "libass-wasm";

6
package-lock.json generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<options>(() => {
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;

View File

@ -11,6 +11,7 @@
"compilerOptions": {
"target": "ES2016",
"noImplicitAny": true,
"moduleResolution": "bundler"
"moduleResolution": "bundler",
"allowJs": true,
}
}