Feat: ass subtitle support
This commit is contained in:
parent
c25efae761
commit
4b689b4495
|
@ -1 +1,3 @@
|
||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare module "libass-wasm";
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
"hls.js": "^1.5.7",
|
"hls.js": "^1.5.7",
|
||||||
"less": "^4.1.3",
|
"less": "^4.1.3",
|
||||||
"less-loader": "^11.1.3",
|
"less-loader": "^11.1.3",
|
||||||
|
"libass-wasm": "^4.1.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mpegts.js": "^1.7.3",
|
"mpegts.js": "^1.7.3",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
|
@ -3962,6 +3963,11 @@
|
||||||
"node": ">= 0.8.0"
|
"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": {
|
"node_modules/lie": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
"hls.js": "^1.5.7",
|
"hls.js": "^1.5.7",
|
||||||
"less": "^4.1.3",
|
"less": "^4.1.3",
|
||||||
"less-loader": "^11.1.3",
|
"less-loader": "^11.1.3",
|
||||||
|
"libass-wasm": "^4.1.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mpegts.js": "^1.7.3",
|
"mpegts.js": "^1.7.3",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
|
|
|
@ -56,12 +56,28 @@ export const useMovieApi = (roomToken: string) => {
|
||||||
headers: { Authorization: roomToken }
|
headers: { Authorization: roomToken }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (currentMovie.value) {
|
if (!currentMovie.value) return;
|
||||||
console.log(currentMovie.value);
|
|
||||||
room.currentMovie = currentMovie.value.movie;
|
room.currentMovie = currentMovie.value.movie;
|
||||||
room.currentStatus = currentMovie.value.status;
|
room.currentStatus = currentMovie.value.status;
|
||||||
room.currentExpireId = currentMovie.value.expireId;
|
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) {
|
} catch (err: any) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
ElNotification({
|
ElNotification({
|
||||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -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
|
|
@ -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
|
|
@ -1,5 +1,7 @@
|
||||||
import type Artplayer from "artplayer";
|
import type Artplayer from "artplayer";
|
||||||
import type { ComponentOption } from "artplayer/types/component";
|
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 newSubtitleHtml = (name: string): HTMLElement => {
|
||||||
const SubtitleHtml = document.createElement("span");
|
const SubtitleHtml = document.createElement("span");
|
||||||
|
@ -39,10 +41,19 @@ export const newSubtitleControl = (
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
onSelect(this: Artplayer, selector: any) {
|
onSelect(this: Artplayer, selector: any) {
|
||||||
console.log("切换字幕:", selector);
|
|
||||||
if (selector.html === "关闭") {
|
if (selector.html === "关闭") {
|
||||||
this.subtitle.show = false;
|
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 {
|
} else {
|
||||||
|
this.emit("artplayer-plugin-ass:visible" as keyof Events, false);
|
||||||
this.subtitle.switch(selector.url, { type: selector.type });
|
this.subtitle.switch(selector.url, { type: selector.type });
|
||||||
this.subtitle.show = true;
|
this.subtitle.show = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ import MovieList from "@/components/cinema/MovieList.vue";
|
||||||
import MoviePush from "@/components/cinema/MoviePush.vue";
|
import MoviePush from "@/components/cinema/MoviePush.vue";
|
||||||
import type { Subtitles } from "@/types/Movie";
|
import type { Subtitles } from "@/types/Movie";
|
||||||
import { RoomMemberPermission } from "@/types/Room";
|
import { RoomMemberPermission } from "@/types/Room";
|
||||||
|
import artplayerPluginAss from "@/plugins/artplayer-plugin-ass";
|
||||||
|
|
||||||
const Player = defineAsyncComponent(() => import("@/components/Player.vue"));
|
const Player = defineAsyncComponent(() => import("@/components/Player.vue"));
|
||||||
|
|
||||||
|
@ -139,18 +140,50 @@ const playerOption = computed<options>(() => {
|
||||||
newLazyInitSyncPlugin(room.currentExpireId)
|
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) {
|
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));
|
option.plugins!.push(newLazyInitSubtitlePlugin(room.currentMovie.base!.subtitles));
|
||||||
|
// return;
|
||||||
|
useAssPlugin &&
|
||||||
|
option.plugins!.push(
|
||||||
|
artplayerPluginAss({
|
||||||
|
// debug: true,
|
||||||
|
subUrl: defaultUrl
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return option;
|
return option;
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2016",
|
"target": "ES2016",
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"moduleResolution": "bundler"
|
"moduleResolution": "bundler",
|
||||||
|
"allowJs": true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue