lzc-video-player 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +1 -0
- package/.eslintrc.cjs +18 -0
- package/.prettierrc.json +5 -0
- package/AGENTS.md +31 -0
- package/README.md +38 -0
- package/build.sh +10 -0
- package/demo/.vscode/extensions.json +3 -0
- package/demo/README.md +40 -0
- package/demo/env.d.ts +1 -0
- package/demo/index.html +13 -0
- package/demo/package-lock.json +2037 -0
- package/demo/package.json +25 -0
- package/demo/public/favicon.ico +0 -0
- package/demo/src/App.vue +25 -0
- package/demo/src/assets/base.css +70 -0
- package/demo/src/assets/logo.svg +1 -0
- package/demo/src/assets/main.css +33 -0
- package/demo/src/main.ts +8 -0
- package/demo/tsconfig.config.json +8 -0
- package/demo/tsconfig.json +16 -0
- package/demo/vite.config.ts +14 -0
- package/docs/progress-bar-style-analysis.md +87 -0
- package/env.d.ts +1 -0
- package/error_pages/502.html.tpl +13 -0
- package/i18next-parser.config.mjs +147 -0
- package/index.html +54 -0
- package/lazycat.png +0 -0
- package/lib/README.md +48 -0
- package/lib/package.json +22 -0
- package/lzc-build.local.yml +65 -0
- package/lzc-build.yml +65 -0
- package/lzc-manifest.yml +53 -0
- package/makefile +15 -0
- package/package.json +69 -0
- package/postcss.config.js +6 -0
- package/public/512x512.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/languages/en/translation.json +125 -0
- package/public/languages/zh/translation.json +125 -0
- package/public/libass-wasm/4.1.0/default.woff2 +0 -0
- package/public/libass-wasm/4.1.0/subtitles-octopus-worker-legacy.js +40 -0
- package/public/libass-wasm/4.1.0/subtitles-octopus-worker.js +1 -0
- package/public/libass-wasm/4.1.0/subtitles-octopus-worker.wasm +0 -0
- package/public/libass-wasm/4.1.0/subtitles-octopus.js +1680 -0
- package/public/square-128x128.png +0 -0
- package/public/square-256x256.png +0 -0
- package/public/square-512x512.png +0 -0
- package/src/App.vue +18 -0
- package/src/assets/base.scss +104 -0
- package/src/assets/cloud.png +0 -0
- package/src/assets/logo.svg +1 -0
- package/src/components/Dialog/index.vue +96 -0
- package/src/components/MultipleEdit/choose.vue +39 -0
- package/src/components/PlayList/index.vue +521 -0
- package/src/components/Spectrum/index.vue +58 -0
- package/src/components/Video/NativeVideoPlayer.vue +748 -0
- package/src/components/Video/README.md +3 -0
- package/src/components/Video/clientPlayer.ts +348 -0
- package/src/components/Video/components/LzcModal/components/simpleList.vue +57 -0
- package/src/components/Video/components/LzcModal/list.vue +52 -0
- package/src/components/Video/components/LzcModal/playrate.vue +45 -0
- package/src/components/Video/components/LzcModal/resolution.vue +117 -0
- package/src/components/Video/components/LzcModal/subtitle.vue +499 -0
- package/src/components/Video/components/LzcModal/useModal.ts +18 -0
- package/src/components/Video/components/LzcOverlay/SubtitleLayer.vue +321 -0
- package/src/components/Video/components/LzcOverlay/cast.vue +253 -0
- package/src/components/Video/components/LzcOverlay/casting.vue +205 -0
- package/src/components/Video/components/LzcOverlay/error.vue +103 -0
- package/src/components/Video/components/LzcOverlay/helper.ts +81 -0
- package/src/components/Video/components/LzcOverlay/index.vue +99 -0
- package/src/components/Video/components/LzcOverlay/playing.vue +496 -0
- package/src/components/Video/components/LzcOverlay/playingButtons.vue +122 -0
- package/src/components/Video/components/LzcOverlay/playingLayout.vue +287 -0
- package/src/components/Video/components/LzcOverlay/useCast.ts +235 -0
- package/src/components/Video/components/LzcOverlay/useCommon.ts +41 -0
- package/src/components/Video/components/LzcOverlay/useOctopusRenderer.ts +230 -0
- package/src/components/Video/components/LzcOverlay/useSubtitleRenderEngine.ts +79 -0
- package/src/components/Video/components/LzcOverlay/useSubtitleTrack.ts +139 -0
- package/src/components/Video/components/useLzcCommon.ts +16 -0
- package/src/components/Video/directPlay.ts +345 -0
- package/src/components/Video/getSubtitleInfo.ts +42 -0
- package/src/components/Video/native/EventEmitter.ts +62 -0
- package/src/components/Video/native/NativeControls.vue +510 -0
- package/src/components/Video/native/NativeModal.vue +133 -0
- package/src/components/Video/native/NativePlayer.ts +913 -0
- package/src/components/Video/native/NativePlayer.vue +53 -0
- package/src/components/Video/native/index.ts +9 -0
- package/src/components/Video/native/native-player.css +183 -0
- package/src/components/Video/native/playerKey.ts +5 -0
- package/src/components/Video/native/useNativeCastMiddleware.ts +50 -0
- package/src/components/Video/native/useNativePlayer.ts +3 -0
- package/src/components/Video/native/useNativePlayerFullscreen.ts +44 -0
- package/src/components/Video/native/useNativePlayerHistory.ts +69 -0
- package/src/components/Video/native/useNativePlayerModal.ts +68 -0
- package/src/components/Video/native/useNativePlayerPlaylist.ts +67 -0
- package/src/components/Video/native/useNativePlayerState.ts +225 -0
- package/src/components/Video/player.ts +99 -0
- package/src/components/Video/theme/index.scss +291 -0
- package/src/components/Video/theme/videojs.css +1797 -0
- package/src/components/Video/useSource.ts +1431 -0
- package/src/components/Video/useSubtitlePreference.ts +66 -0
- package/src/components/Video/useWebview.ts +79 -0
- package/src/components/Video/videoFrame.ts +58 -0
- package/src/env.d.ts +3 -0
- package/src/i18n/README.md +392 -0
- package/src/i18n/index.ts +49 -0
- package/src/icons/Video_Player.svg +69 -0
- package/src/icons/box.svg +15 -0
- package/src/icons/client.svg +17 -0
- package/src/icons/logo.svg +28 -0
- package/src/icons//344/270/212/344/270/200/344/270/252.svg +6 -0
- package/src/icons//344/270/213/344/270/200/344/270/252.svg +4 -0
- package/src/icons//344/272/256/345/272/246.svg +13 -0
- package/src/icons//345/200/215/351/200/237.svg +14 -0
- package/src/icons//345/205/250/345/261/217.svg +16 -0
- package/src/icons//345/205/250/351/200/211_/345/267/262/351/200/211/344/270/255.svg +16 -0
- package/src/icons//345/205/250/351/200/211_/346/234/252/351/200/211/344/270/255.svg +15 -0
- package/src/icons//345/205/263/351/227/255/345/244/232/351/200/211.svg +14 -0
- package/src/icons//345/205/263/351/227/255/346/212/225/345/261/217.svg +11 -0
- package/src/icons//345/233/236/346/224/266/347/253/231.svg +15 -0
- package/src/icons//345/244/261/346/225/210.svg +17 -0
- package/src/icons//346/207/222/347/214/253/346/222/255/346/224/276/345/231/250-icon.png +0 -0
- package/src/icons//346/207/222/347/214/253/346/222/255/346/224/276/345/231/250.png +0 -0
- package/src/icons//346/212/225/345/261/217.svg +11 -0
- package/src/icons//346/212/225/351/200/201/344/270/255.jpg +0 -0
- package/src/icons//346/212/225/351/200/201/344/270/255.svg +21 -0
- package/src/icons//346/222/255/346/224/276.svg +3 -0
- package/src/icons//346/232/202/345/201/234.svg +4 -0
- package/src/icons//346/232/202/346/227/240.svg +21 -0
- package/src/icons//346/233/264/345/244/232/346/223/215/344/275/234.svg +11 -0
- package/src/icons//347/224/265/350/247/206.svg +18 -0
- package/src/icons//347/247/273/345/212/250/347/253/257_/350/203/214/346/231/257.webp +0 -0
- package/src/icons//350/203/214/346/231/257.png +0 -0
- package/src/icons//350/277/224/345/233/236.svg +13 -0
- package/src/icons//350/277/233/345/205/245/345/205/250/345/261/217.svg +13 -0
- package/src/icons//351/200/200/345/207/272/345/205/250/345/261/217.svg +15 -0
- package/src/icons//351/200/211/346/213/251.svg +15 -0
- package/src/icons//351/237/263/351/207/217.svg +13 -0
- package/src/index.d.ts +9 -0
- package/src/lzc-video-player.scss +7 -0
- package/src/lzc-video-player.ts +6 -0
- package/src/main.ts +62 -0
- package/src/model.ts +77 -0
- package/src/quasar-variables.sass +10 -0
- package/src/router/index.ts +74 -0
- package/src/stores/pinia.ts +3 -0
- package/src/stores/playlist.ts +146 -0
- package/src/use/useKeyBind.ts +61 -0
- package/src/use/useMultipleEdit.ts +60 -0
- package/src/use/useSdk.ts +5 -0
- package/src/use/useSubtitle.ts +39 -0
- package/src/use/useUtils.ts +22 -0
- package/src/use/useVideoFrame.ts +60 -0
- package/src/views/Home.ts +99 -0
- package/src/views/mobile/Home.vue +246 -0
- package/src/views/mobile/Player.vue +141 -0
- package/tailwind.config.js +15 -0
- package/tsconfig.config.json +8 -0
- package/tsconfig.json +20 -0
- package/vite.config.lib.ts +88 -0
- package/vite.config.ts +122 -0
- package/vue-shim.d.ts +4 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import octopusScriptUrl from "libass-wasm/dist/js/subtitles-octopus.js?url"
|
|
2
|
+
import octopusWorkerScriptUrl from "libass-wasm/dist/js/subtitles-octopus-worker.js?url"
|
|
3
|
+
import octopusWorkerWasmUrl from "libass-wasm/dist/js/subtitles-octopus-worker.wasm?url"
|
|
4
|
+
import octopusLegacyWorkerScriptUrl from "libass-wasm/dist/js/subtitles-octopus-worker-legacy.js?url"
|
|
5
|
+
|
|
6
|
+
let octopusScriptPromise: Promise<void> | null = null
|
|
7
|
+
let octopusWorkerBlobUrl: string | null = null
|
|
8
|
+
let octopusLegacyWorkerBlobUrl: string | null = null
|
|
9
|
+
const assContentCache = new Map<string, string>()
|
|
10
|
+
|
|
11
|
+
function resolveBundledAssetUrl(url: string): string {
|
|
12
|
+
if (/^(https?:)?\/\//i.test(url) || /^(blob:|data:)/i.test(url)) {
|
|
13
|
+
return url
|
|
14
|
+
}
|
|
15
|
+
if (typeof window === "undefined") {
|
|
16
|
+
return url
|
|
17
|
+
}
|
|
18
|
+
return new URL(url, window.location.href).toString()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createClassicWorkerBlobUrl(source: string): string {
|
|
22
|
+
return URL.createObjectURL(
|
|
23
|
+
new Blob([source], {
|
|
24
|
+
type: "text/javascript",
|
|
25
|
+
}),
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getOctopusWorkerUrl(): string {
|
|
30
|
+
if (octopusWorkerBlobUrl) {
|
|
31
|
+
return octopusWorkerBlobUrl
|
|
32
|
+
}
|
|
33
|
+
const workerScriptUrl = resolveBundledAssetUrl(octopusWorkerScriptUrl)
|
|
34
|
+
const workerWasmUrl = resolveBundledAssetUrl(octopusWorkerWasmUrl)
|
|
35
|
+
octopusWorkerBlobUrl = createClassicWorkerBlobUrl(`
|
|
36
|
+
var __octopusLocateFile = self.Module && self.Module.locateFile;
|
|
37
|
+
self.Module = Object.assign({}, self.Module || {}, {
|
|
38
|
+
locateFile: function (path, scriptDirectory) {
|
|
39
|
+
if (path === "subtitles-octopus-worker.wasm") {
|
|
40
|
+
return ${JSON.stringify(workerWasmUrl)};
|
|
41
|
+
}
|
|
42
|
+
if (typeof __octopusLocateFile === "function") {
|
|
43
|
+
return __octopusLocateFile(path, scriptDirectory);
|
|
44
|
+
}
|
|
45
|
+
return scriptDirectory + path;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
importScripts(${JSON.stringify(workerScriptUrl)});
|
|
49
|
+
`)
|
|
50
|
+
return octopusWorkerBlobUrl
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getOctopusLegacyWorkerUrl(): string {
|
|
54
|
+
if (octopusLegacyWorkerBlobUrl) {
|
|
55
|
+
return octopusLegacyWorkerBlobUrl
|
|
56
|
+
}
|
|
57
|
+
const legacyWorkerScriptUrl = resolveBundledAssetUrl(
|
|
58
|
+
octopusLegacyWorkerScriptUrl,
|
|
59
|
+
)
|
|
60
|
+
octopusLegacyWorkerBlobUrl = createClassicWorkerBlobUrl(`
|
|
61
|
+
importScripts(${JSON.stringify(legacyWorkerScriptUrl)});
|
|
62
|
+
`)
|
|
63
|
+
return octopusLegacyWorkerBlobUrl
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeOctopusAssetUrl(input: string): string {
|
|
67
|
+
const raw = (input || "").trim()
|
|
68
|
+
if (!raw) return ""
|
|
69
|
+
if (/^(blob:|data:)/i.test(raw)) return raw
|
|
70
|
+
const encoded = encodeURI(raw)
|
|
71
|
+
return resolveBundledAssetUrl(
|
|
72
|
+
encoded.replace(/\[/g, "%5B").replace(/\]/g, "%5D"),
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function loadAssContent(url: string): Promise<string> {
|
|
77
|
+
const cached = assContentCache.get(url)
|
|
78
|
+
if (cached !== undefined) {
|
|
79
|
+
return cached
|
|
80
|
+
}
|
|
81
|
+
const response = await fetch(url, {
|
|
82
|
+
method: "GET",
|
|
83
|
+
credentials: "include",
|
|
84
|
+
cache: "force-cache",
|
|
85
|
+
})
|
|
86
|
+
if (!response.ok) {
|
|
87
|
+
throw new Error(`failed to fetch ass subtitle: ${response.status}`)
|
|
88
|
+
}
|
|
89
|
+
const content = await response.text()
|
|
90
|
+
if (!content.trim()) {
|
|
91
|
+
throw new Error("ass subtitle is empty")
|
|
92
|
+
}
|
|
93
|
+
assContentCache.set(url, content)
|
|
94
|
+
return content
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function loadOctopusScript(): Promise<void> {
|
|
98
|
+
if (typeof window === "undefined") {
|
|
99
|
+
return Promise.reject(new Error("window is not available"))
|
|
100
|
+
}
|
|
101
|
+
if (typeof (window as any).SubtitlesOctopus === "function") {
|
|
102
|
+
return Promise.resolve()
|
|
103
|
+
}
|
|
104
|
+
if (octopusScriptPromise) {
|
|
105
|
+
return octopusScriptPromise
|
|
106
|
+
}
|
|
107
|
+
octopusScriptPromise = new Promise((resolve, reject) => {
|
|
108
|
+
const script = document.createElement("script")
|
|
109
|
+
script.src = resolveBundledAssetUrl(octopusScriptUrl)
|
|
110
|
+
script.async = true
|
|
111
|
+
script.onload = () => resolve()
|
|
112
|
+
script.onerror = () =>
|
|
113
|
+
reject(new Error("failed to load subtitles-octopus script"))
|
|
114
|
+
document.head.appendChild(script)
|
|
115
|
+
})
|
|
116
|
+
return octopusScriptPromise
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface OctopusTrackOptions {
|
|
120
|
+
assUrl: string
|
|
121
|
+
fonts: string[]
|
|
122
|
+
onError?: (err: unknown) => void
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function useOctopusRenderer(video: HTMLVideoElement) {
|
|
126
|
+
let instance: any = null
|
|
127
|
+
let trackKey = ""
|
|
128
|
+
let disposed = false
|
|
129
|
+
|
|
130
|
+
const destroy = () => {
|
|
131
|
+
if (!instance) return
|
|
132
|
+
try {
|
|
133
|
+
instance.dispose?.()
|
|
134
|
+
} catch (err) {
|
|
135
|
+
if (
|
|
136
|
+
err instanceof TypeError &&
|
|
137
|
+
String(err.message || err).includes("postMessage")
|
|
138
|
+
) {
|
|
139
|
+
console.warn("Octopus instance was already torn down")
|
|
140
|
+
} else {
|
|
141
|
+
console.error("Failed to dispose octopus instance", err)
|
|
142
|
+
}
|
|
143
|
+
} finally {
|
|
144
|
+
instance = null
|
|
145
|
+
trackKey = ""
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const attach = async (options: OctopusTrackOptions): Promise<boolean> => {
|
|
150
|
+
if (disposed) {
|
|
151
|
+
return false
|
|
152
|
+
}
|
|
153
|
+
const assUrl = normalizeOctopusAssetUrl(options.assUrl || "")
|
|
154
|
+
if (!assUrl) {
|
|
155
|
+
destroy()
|
|
156
|
+
return false
|
|
157
|
+
}
|
|
158
|
+
const fonts = (options.fonts || [])
|
|
159
|
+
.map((item) => normalizeOctopusAssetUrl(item))
|
|
160
|
+
.filter((item) => !!item)
|
|
161
|
+
if (fonts.length === 0) {
|
|
162
|
+
destroy()
|
|
163
|
+
return false
|
|
164
|
+
}
|
|
165
|
+
const nextKey = JSON.stringify([assUrl, [...fonts].sort()])
|
|
166
|
+
if (instance && nextKey === trackKey) {
|
|
167
|
+
return true
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let assContent = ""
|
|
171
|
+
try {
|
|
172
|
+
assContent = await loadAssContent(assUrl)
|
|
173
|
+
} catch (err) {
|
|
174
|
+
console.error("Failed to load ass subtitle content", err)
|
|
175
|
+
options.onError?.(err)
|
|
176
|
+
destroy()
|
|
177
|
+
return false
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
await loadOctopusScript()
|
|
182
|
+
} catch (err) {
|
|
183
|
+
console.error("Failed to load octopus script", err)
|
|
184
|
+
options.onError?.(err)
|
|
185
|
+
destroy()
|
|
186
|
+
return false
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const SubtitlesOctopus = (window as any).SubtitlesOctopus
|
|
190
|
+
if (typeof SubtitlesOctopus !== "function") {
|
|
191
|
+
options.onError?.(new Error("SubtitlesOctopus is not available"))
|
|
192
|
+
destroy()
|
|
193
|
+
return false
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
destroy()
|
|
197
|
+
try {
|
|
198
|
+
instance = new SubtitlesOctopus({
|
|
199
|
+
video,
|
|
200
|
+
subContent: assContent,
|
|
201
|
+
fonts,
|
|
202
|
+
fallbackFont: fonts[0],
|
|
203
|
+
workerUrl: getOctopusWorkerUrl(),
|
|
204
|
+
legacyWorkerUrl: getOctopusLegacyWorkerUrl(),
|
|
205
|
+
onError: (err: unknown) => {
|
|
206
|
+
options.onError?.(err)
|
|
207
|
+
},
|
|
208
|
+
})
|
|
209
|
+
trackKey = nextKey
|
|
210
|
+
return true
|
|
211
|
+
} catch (err) {
|
|
212
|
+
console.error("Failed to initialize octopus", err)
|
|
213
|
+
options.onError?.(err)
|
|
214
|
+
destroy()
|
|
215
|
+
return false
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const dispose = () => {
|
|
220
|
+
disposed = true
|
|
221
|
+
destroy()
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
attach,
|
|
226
|
+
destroy,
|
|
227
|
+
dispose,
|
|
228
|
+
isAttached: () => !!instance,
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { LzcPlayer } from "@/components/Video/player"
|
|
2
|
+
import type { Subtitle } from "@/model"
|
|
3
|
+
import { ref } from "vue"
|
|
4
|
+
import { useOctopusRenderer } from "./useOctopusRenderer"
|
|
5
|
+
|
|
6
|
+
export type SubtitleRenderMode = "none" | "vtt" | "ass-octopus"
|
|
7
|
+
|
|
8
|
+
export function useSubtitleRenderEngine(player: LzcPlayer) {
|
|
9
|
+
const mode = ref<SubtitleRenderMode>("none")
|
|
10
|
+
const octopus = useOctopusRenderer(player.$video())
|
|
11
|
+
let activeRequestId = 0
|
|
12
|
+
|
|
13
|
+
const reset = () => {
|
|
14
|
+
activeRequestId += 1
|
|
15
|
+
octopus.destroy()
|
|
16
|
+
mode.value = "none"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const selectMode = async (
|
|
20
|
+
subtitle: Subtitle | undefined,
|
|
21
|
+
hidden: boolean,
|
|
22
|
+
): Promise<SubtitleRenderMode> => {
|
|
23
|
+
const requestId = ++activeRequestId
|
|
24
|
+
|
|
25
|
+
if (!subtitle || hidden) {
|
|
26
|
+
octopus.destroy()
|
|
27
|
+
mode.value = "none"
|
|
28
|
+
return mode.value
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const assUrl = (subtitle.ass_url || "").trim()
|
|
32
|
+
const assFonts = (subtitle.ass_fonts || []).filter((item) => !!item?.trim())
|
|
33
|
+
const assRenderable = subtitle.ass_renderable === true
|
|
34
|
+
const allowOctopus = assRenderable && !!assUrl && assFonts.length > 0
|
|
35
|
+
if (allowOctopus) {
|
|
36
|
+
const ok = await octopus.attach({
|
|
37
|
+
assUrl,
|
|
38
|
+
fonts: assFonts,
|
|
39
|
+
onError: (err) => {
|
|
40
|
+
console.error("Octopus render failed, fallback to VTT", err)
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
if (requestId !== activeRequestId) {
|
|
44
|
+
octopus.destroy()
|
|
45
|
+
return "none"
|
|
46
|
+
}
|
|
47
|
+
if (ok) {
|
|
48
|
+
mode.value = "ass-octopus"
|
|
49
|
+
return mode.value
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (requestId !== activeRequestId) {
|
|
54
|
+
octopus.destroy()
|
|
55
|
+
return "none"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
octopus.destroy()
|
|
59
|
+
if ((subtitle.vtt_url || "").trim()) {
|
|
60
|
+
mode.value = "vtt"
|
|
61
|
+
} else {
|
|
62
|
+
mode.value = "none"
|
|
63
|
+
}
|
|
64
|
+
return mode.value
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const dispose = () => {
|
|
68
|
+
activeRequestId += 1
|
|
69
|
+
octopus.dispose()
|
|
70
|
+
mode.value = "none"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
mode,
|
|
75
|
+
reset,
|
|
76
|
+
selectMode,
|
|
77
|
+
dispose,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
export interface SubtitleCue {
|
|
2
|
+
start: number
|
|
3
|
+
end: number
|
|
4
|
+
text: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const cueCache = new Map<string, Promise<SubtitleCue[]>>()
|
|
8
|
+
|
|
9
|
+
function parseTimeToken(token: string): number {
|
|
10
|
+
const normalized = token.trim().replace(",", ".")
|
|
11
|
+
const parts = normalized.split(":")
|
|
12
|
+
if (parts.length < 2 || parts.length > 3) return Number.NaN
|
|
13
|
+
|
|
14
|
+
const seg = parts.map((part) => Number(part))
|
|
15
|
+
if (seg.some((v) => !Number.isFinite(v))) return Number.NaN
|
|
16
|
+
|
|
17
|
+
if (parts.length === 2) {
|
|
18
|
+
const [mm, ss] = seg
|
|
19
|
+
return mm * 60 + ss
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const [hh, mm, ss] = seg
|
|
23
|
+
return hh * 3600 + mm * 60 + ss
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function decodeEntities(text: string): string {
|
|
27
|
+
return text
|
|
28
|
+
.replace(/&/g, "&")
|
|
29
|
+
.replace(/</g, "<")
|
|
30
|
+
.replace(/>/g, ">")
|
|
31
|
+
.replace(/"/g, '"')
|
|
32
|
+
.replace(/'/g, "'")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeCueText(text: string): string {
|
|
36
|
+
const withoutTags = text
|
|
37
|
+
.replace(/<\/?c(\.[^>]*)?>/gi, "")
|
|
38
|
+
.replace(/<\/?(i|b|u|ruby|rt|v(\s+[^>]*)?)>/gi, "")
|
|
39
|
+
.replace(/<[^>]+>/g, "")
|
|
40
|
+
return decodeEntities(withoutTags).trim()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function parseWebVtt(content: string): SubtitleCue[] {
|
|
44
|
+
const text = content.replace(/^\uFEFF/, "")
|
|
45
|
+
const lines = text.split(/\r?\n/)
|
|
46
|
+
const cues: SubtitleCue[] = []
|
|
47
|
+
|
|
48
|
+
let i = 0
|
|
49
|
+
while (i < lines.length) {
|
|
50
|
+
let line = lines[i].trim()
|
|
51
|
+
|
|
52
|
+
if (!line) {
|
|
53
|
+
i += 1
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (line.startsWith("WEBVTT")) {
|
|
58
|
+
i += 1
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (line.startsWith("NOTE")) {
|
|
63
|
+
i += 1
|
|
64
|
+
while (i < lines.length && lines[i].trim()) {
|
|
65
|
+
i += 1
|
|
66
|
+
}
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let timingLine = line
|
|
71
|
+
if (!timingLine.includes("-->")) {
|
|
72
|
+
i += 1
|
|
73
|
+
if (i >= lines.length) break
|
|
74
|
+
timingLine = lines[i].trim()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!timingLine.includes("-->")) {
|
|
78
|
+
i += 1
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const [startRaw, endRaw] = timingLine.split("-->")
|
|
83
|
+
const start = parseTimeToken(startRaw)
|
|
84
|
+
const end = parseTimeToken((endRaw || "").trim().split(/\s+/)[0] || "")
|
|
85
|
+
|
|
86
|
+
i += 1
|
|
87
|
+
const payload: string[] = []
|
|
88
|
+
while (i < lines.length && lines[i].trim()) {
|
|
89
|
+
payload.push(lines[i])
|
|
90
|
+
i += 1
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) {
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const parsed = normalizeCueText(payload.join("\n"))
|
|
98
|
+
if (!parsed) {
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
cues.push({
|
|
103
|
+
start,
|
|
104
|
+
end,
|
|
105
|
+
text: parsed,
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return cues
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function fetchSubtitleCues(url: string): Promise<SubtitleCue[]> {
|
|
113
|
+
const normalized = url.trim()
|
|
114
|
+
if (!normalized) {
|
|
115
|
+
return Promise.resolve([])
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const cached = cueCache.get(normalized)
|
|
119
|
+
if (cached) {
|
|
120
|
+
return cached
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const request = fetch(normalized)
|
|
124
|
+
.then((resp) => {
|
|
125
|
+
if (!resp.ok) {
|
|
126
|
+
throw new Error(`Http Status: ${resp.status}`)
|
|
127
|
+
}
|
|
128
|
+
return resp.text()
|
|
129
|
+
})
|
|
130
|
+
.then((content) => parseWebVtt(content))
|
|
131
|
+
.catch((err) => {
|
|
132
|
+
console.error("Failed to load subtitle vtt", err)
|
|
133
|
+
cueCache.delete(normalized)
|
|
134
|
+
return []
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
cueCache.set(normalized, request)
|
|
138
|
+
return request
|
|
139
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function formatTime(second: number): string {
|
|
2
|
+
if (second) {
|
|
3
|
+
const result = new Date(second * 1000).toISOString().substring(11, 19)
|
|
4
|
+
if (result.startsWith("00:")) {
|
|
5
|
+
return result.substring(3)
|
|
6
|
+
}
|
|
7
|
+
return result
|
|
8
|
+
}
|
|
9
|
+
return "00:00"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function remHeight(rem: number, pixelRatio?: false): number {
|
|
13
|
+
const result =
|
|
14
|
+
rem * parseFloat(getComputedStyle(document.documentElement).fontSize)
|
|
15
|
+
return pixelRatio ? window.devicePixelRatio * result : result
|
|
16
|
+
}
|