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.
Files changed (162) hide show
  1. package/.dockerignore +1 -0
  2. package/.eslintrc.cjs +18 -0
  3. package/.prettierrc.json +5 -0
  4. package/AGENTS.md +31 -0
  5. package/README.md +38 -0
  6. package/build.sh +10 -0
  7. package/demo/.vscode/extensions.json +3 -0
  8. package/demo/README.md +40 -0
  9. package/demo/env.d.ts +1 -0
  10. package/demo/index.html +13 -0
  11. package/demo/package-lock.json +2037 -0
  12. package/demo/package.json +25 -0
  13. package/demo/public/favicon.ico +0 -0
  14. package/demo/src/App.vue +25 -0
  15. package/demo/src/assets/base.css +70 -0
  16. package/demo/src/assets/logo.svg +1 -0
  17. package/demo/src/assets/main.css +33 -0
  18. package/demo/src/main.ts +8 -0
  19. package/demo/tsconfig.config.json +8 -0
  20. package/demo/tsconfig.json +16 -0
  21. package/demo/vite.config.ts +14 -0
  22. package/docs/progress-bar-style-analysis.md +87 -0
  23. package/env.d.ts +1 -0
  24. package/error_pages/502.html.tpl +13 -0
  25. package/i18next-parser.config.mjs +147 -0
  26. package/index.html +54 -0
  27. package/lazycat.png +0 -0
  28. package/lib/README.md +48 -0
  29. package/lib/package.json +22 -0
  30. package/lzc-build.local.yml +65 -0
  31. package/lzc-build.yml +65 -0
  32. package/lzc-manifest.yml +53 -0
  33. package/makefile +15 -0
  34. package/package.json +69 -0
  35. package/postcss.config.js +6 -0
  36. package/public/512x512.png +0 -0
  37. package/public/favicon.ico +0 -0
  38. package/public/languages/en/translation.json +125 -0
  39. package/public/languages/zh/translation.json +125 -0
  40. package/public/libass-wasm/4.1.0/default.woff2 +0 -0
  41. package/public/libass-wasm/4.1.0/subtitles-octopus-worker-legacy.js +40 -0
  42. package/public/libass-wasm/4.1.0/subtitles-octopus-worker.js +1 -0
  43. package/public/libass-wasm/4.1.0/subtitles-octopus-worker.wasm +0 -0
  44. package/public/libass-wasm/4.1.0/subtitles-octopus.js +1680 -0
  45. package/public/square-128x128.png +0 -0
  46. package/public/square-256x256.png +0 -0
  47. package/public/square-512x512.png +0 -0
  48. package/src/App.vue +18 -0
  49. package/src/assets/base.scss +104 -0
  50. package/src/assets/cloud.png +0 -0
  51. package/src/assets/logo.svg +1 -0
  52. package/src/components/Dialog/index.vue +96 -0
  53. package/src/components/MultipleEdit/choose.vue +39 -0
  54. package/src/components/PlayList/index.vue +521 -0
  55. package/src/components/Spectrum/index.vue +58 -0
  56. package/src/components/Video/NativeVideoPlayer.vue +748 -0
  57. package/src/components/Video/README.md +3 -0
  58. package/src/components/Video/clientPlayer.ts +348 -0
  59. package/src/components/Video/components/LzcModal/components/simpleList.vue +57 -0
  60. package/src/components/Video/components/LzcModal/list.vue +52 -0
  61. package/src/components/Video/components/LzcModal/playrate.vue +45 -0
  62. package/src/components/Video/components/LzcModal/resolution.vue +117 -0
  63. package/src/components/Video/components/LzcModal/subtitle.vue +499 -0
  64. package/src/components/Video/components/LzcModal/useModal.ts +18 -0
  65. package/src/components/Video/components/LzcOverlay/SubtitleLayer.vue +321 -0
  66. package/src/components/Video/components/LzcOverlay/cast.vue +253 -0
  67. package/src/components/Video/components/LzcOverlay/casting.vue +205 -0
  68. package/src/components/Video/components/LzcOverlay/error.vue +103 -0
  69. package/src/components/Video/components/LzcOverlay/helper.ts +81 -0
  70. package/src/components/Video/components/LzcOverlay/index.vue +99 -0
  71. package/src/components/Video/components/LzcOverlay/playing.vue +496 -0
  72. package/src/components/Video/components/LzcOverlay/playingButtons.vue +122 -0
  73. package/src/components/Video/components/LzcOverlay/playingLayout.vue +287 -0
  74. package/src/components/Video/components/LzcOverlay/useCast.ts +235 -0
  75. package/src/components/Video/components/LzcOverlay/useCommon.ts +41 -0
  76. package/src/components/Video/components/LzcOverlay/useOctopusRenderer.ts +230 -0
  77. package/src/components/Video/components/LzcOverlay/useSubtitleRenderEngine.ts +79 -0
  78. package/src/components/Video/components/LzcOverlay/useSubtitleTrack.ts +139 -0
  79. package/src/components/Video/components/useLzcCommon.ts +16 -0
  80. package/src/components/Video/directPlay.ts +345 -0
  81. package/src/components/Video/getSubtitleInfo.ts +42 -0
  82. package/src/components/Video/native/EventEmitter.ts +62 -0
  83. package/src/components/Video/native/NativeControls.vue +510 -0
  84. package/src/components/Video/native/NativeModal.vue +133 -0
  85. package/src/components/Video/native/NativePlayer.ts +913 -0
  86. package/src/components/Video/native/NativePlayer.vue +53 -0
  87. package/src/components/Video/native/index.ts +9 -0
  88. package/src/components/Video/native/native-player.css +183 -0
  89. package/src/components/Video/native/playerKey.ts +5 -0
  90. package/src/components/Video/native/useNativeCastMiddleware.ts +50 -0
  91. package/src/components/Video/native/useNativePlayer.ts +3 -0
  92. package/src/components/Video/native/useNativePlayerFullscreen.ts +44 -0
  93. package/src/components/Video/native/useNativePlayerHistory.ts +69 -0
  94. package/src/components/Video/native/useNativePlayerModal.ts +68 -0
  95. package/src/components/Video/native/useNativePlayerPlaylist.ts +67 -0
  96. package/src/components/Video/native/useNativePlayerState.ts +225 -0
  97. package/src/components/Video/player.ts +99 -0
  98. package/src/components/Video/theme/index.scss +291 -0
  99. package/src/components/Video/theme/videojs.css +1797 -0
  100. package/src/components/Video/useSource.ts +1431 -0
  101. package/src/components/Video/useSubtitlePreference.ts +66 -0
  102. package/src/components/Video/useWebview.ts +79 -0
  103. package/src/components/Video/videoFrame.ts +58 -0
  104. package/src/env.d.ts +3 -0
  105. package/src/i18n/README.md +392 -0
  106. package/src/i18n/index.ts +49 -0
  107. package/src/icons/Video_Player.svg +69 -0
  108. package/src/icons/box.svg +15 -0
  109. package/src/icons/client.svg +17 -0
  110. package/src/icons/logo.svg +28 -0
  111. package/src/icons//344/270/212/344/270/200/344/270/252.svg +6 -0
  112. package/src/icons//344/270/213/344/270/200/344/270/252.svg +4 -0
  113. package/src/icons//344/272/256/345/272/246.svg +13 -0
  114. package/src/icons//345/200/215/351/200/237.svg +14 -0
  115. package/src/icons//345/205/250/345/261/217.svg +16 -0
  116. package/src/icons//345/205/250/351/200/211_/345/267/262/351/200/211/344/270/255.svg +16 -0
  117. package/src/icons//345/205/250/351/200/211_/346/234/252/351/200/211/344/270/255.svg +15 -0
  118. package/src/icons//345/205/263/351/227/255/345/244/232/351/200/211.svg +14 -0
  119. package/src/icons//345/205/263/351/227/255/346/212/225/345/261/217.svg +11 -0
  120. package/src/icons//345/233/236/346/224/266/347/253/231.svg +15 -0
  121. package/src/icons//345/244/261/346/225/210.svg +17 -0
  122. package/src/icons//346/207/222/347/214/253/346/222/255/346/224/276/345/231/250-icon.png +0 -0
  123. package/src/icons//346/207/222/347/214/253/346/222/255/346/224/276/345/231/250.png +0 -0
  124. package/src/icons//346/212/225/345/261/217.svg +11 -0
  125. package/src/icons//346/212/225/351/200/201/344/270/255.jpg +0 -0
  126. package/src/icons//346/212/225/351/200/201/344/270/255.svg +21 -0
  127. package/src/icons//346/222/255/346/224/276.svg +3 -0
  128. package/src/icons//346/232/202/345/201/234.svg +4 -0
  129. package/src/icons//346/232/202/346/227/240.svg +21 -0
  130. package/src/icons//346/233/264/345/244/232/346/223/215/344/275/234.svg +11 -0
  131. package/src/icons//347/224/265/350/247/206.svg +18 -0
  132. package/src/icons//347/247/273/345/212/250/347/253/257_/350/203/214/346/231/257.webp +0 -0
  133. package/src/icons//350/203/214/346/231/257.png +0 -0
  134. package/src/icons//350/277/224/345/233/236.svg +13 -0
  135. package/src/icons//350/277/233/345/205/245/345/205/250/345/261/217.svg +13 -0
  136. package/src/icons//351/200/200/345/207/272/345/205/250/345/261/217.svg +15 -0
  137. package/src/icons//351/200/211/346/213/251.svg +15 -0
  138. package/src/icons//351/237/263/351/207/217.svg +13 -0
  139. package/src/index.d.ts +9 -0
  140. package/src/lzc-video-player.scss +7 -0
  141. package/src/lzc-video-player.ts +6 -0
  142. package/src/main.ts +62 -0
  143. package/src/model.ts +77 -0
  144. package/src/quasar-variables.sass +10 -0
  145. package/src/router/index.ts +74 -0
  146. package/src/stores/pinia.ts +3 -0
  147. package/src/stores/playlist.ts +146 -0
  148. package/src/use/useKeyBind.ts +61 -0
  149. package/src/use/useMultipleEdit.ts +60 -0
  150. package/src/use/useSdk.ts +5 -0
  151. package/src/use/useSubtitle.ts +39 -0
  152. package/src/use/useUtils.ts +22 -0
  153. package/src/use/useVideoFrame.ts +60 -0
  154. package/src/views/Home.ts +99 -0
  155. package/src/views/mobile/Home.vue +246 -0
  156. package/src/views/mobile/Player.vue +141 -0
  157. package/tailwind.config.js +15 -0
  158. package/tsconfig.config.json +8 -0
  159. package/tsconfig.json +20 -0
  160. package/vite.config.lib.ts +88 -0
  161. package/vite.config.ts +122 -0
  162. 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(/&amp;/g, "&")
29
+ .replace(/&lt;/g, "<")
30
+ .replace(/&gt;/g, ">")
31
+ .replace(/&quot;/g, '"')
32
+ .replace(/&#39;/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
+ }