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,345 @@
1
+ import type { VideoInfo, VideoQualityLevel } from "@/model"
2
+
3
+ export interface DirectPlayProfile {
4
+ Container: string
5
+ Type: string
6
+ VideoCodec?: string
7
+ AudioCodec?: string
8
+ }
9
+
10
+ export interface CodecProfileCondition {
11
+ Condition: string
12
+ Property: string
13
+ Value: string
14
+ }
15
+
16
+ export interface CodecProfile {
17
+ Type: string
18
+ Codec?: string
19
+ Conditions?: CodecProfileCondition[]
20
+ }
21
+
22
+ export interface DeviceProfile {
23
+ Name: string
24
+ DirectPlayProfiles: DirectPlayProfile[]
25
+ CodecProfiles: CodecProfile[]
26
+ MaxStreamingBitrate?: number
27
+ MaxStaticBitrate?: number
28
+ }
29
+
30
+ export interface OriginPlaybackDecision {
31
+ originMode: "direct" | "hls"
32
+ originDirectUrl: string
33
+ originHlsUrl: string
34
+ subtitleInfoUrl: string
35
+ reasons: string[]
36
+ mediaSource: {
37
+ container?: string
38
+ videoCodec?: string
39
+ audioCodec?: string
40
+ bitrate?: number
41
+ width?: number
42
+ height?: number
43
+ fps?: number
44
+ }
45
+ }
46
+
47
+ function canPlayType(type: string): boolean {
48
+ if (typeof document === "undefined") {
49
+ return false
50
+ }
51
+ const video = document.createElement("video")
52
+ return !!video.canPlayType?.(type).replace(/no/i, "")
53
+ }
54
+
55
+ function canPlayAudioType(type: string): boolean {
56
+ if (typeof document === "undefined") {
57
+ return false
58
+ }
59
+ const audio = document.createElement("audio")
60
+ return !!audio.canPlayType?.(type).replace(/no/i, "")
61
+ }
62
+
63
+ function detectBrowserName() {
64
+ const ua = typeof navigator !== "undefined" ? navigator.userAgent : ""
65
+ if (/Firefox/i.test(ua)) return "Firefox"
66
+ if (/Safari/i.test(ua) && !/Chrome|Chromium|Edg/i.test(ua)) return "Safari"
67
+ return "Chromium"
68
+ }
69
+
70
+ function buildVideoDirectPlayProfiles(): DirectPlayProfile[] {
71
+ const profiles: DirectPlayProfile[] = []
72
+ const mp4VideoCodecs: string[] = []
73
+ const webmVideoCodecs: string[] = []
74
+ const mp4AudioCodecs: string[] = []
75
+ const webmAudioCodecs: string[] = []
76
+
77
+ if (
78
+ canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"') ||
79
+ canPlayType('video/mp4; codecs="avc1.64001F, mp4a.40.2"')
80
+ ) {
81
+ mp4VideoCodecs.push("h264")
82
+ mp4AudioCodecs.push("aac")
83
+ }
84
+ if (
85
+ canPlayType('video/mp4; codecs="hvc1.1.L120"') ||
86
+ canPlayType('video/mp4; codecs="hev1.1.L120"')
87
+ ) {
88
+ mp4VideoCodecs.push("hevc")
89
+ }
90
+ if (canPlayType('video/mp4; codecs="vp09.00.10.08, mp4a.40.2"')) {
91
+ mp4VideoCodecs.push("vp9")
92
+ }
93
+ if (canPlayType('video/mp4; codecs="av01.0.05M.08, mp4a.40.2"')) {
94
+ mp4VideoCodecs.push("av1")
95
+ }
96
+
97
+ if (canPlayType('video/webm; codecs="vp8, vorbis"')) {
98
+ webmVideoCodecs.push("vp8")
99
+ webmAudioCodecs.push("vorbis")
100
+ }
101
+ if (canPlayType('video/webm; codecs="vp9, opus"')) {
102
+ if (!webmVideoCodecs.includes("vp9")) {
103
+ webmVideoCodecs.push("vp9")
104
+ }
105
+ webmAudioCodecs.push("opus")
106
+ }
107
+ if (canPlayType('video/webm; codecs="av1, opus"')) {
108
+ if (!webmVideoCodecs.includes("av1")) {
109
+ webmVideoCodecs.push("av1")
110
+ }
111
+ if (!webmAudioCodecs.includes("opus")) {
112
+ webmAudioCodecs.push("opus")
113
+ }
114
+ }
115
+
116
+ if (canPlayAudioType('audio/mpeg; codecs="mp3"') && !mp4AudioCodecs.includes("mp3")) {
117
+ mp4AudioCodecs.push("mp3")
118
+ }
119
+ if (canPlayAudioType('audio/mp4; codecs="opus"') && !mp4AudioCodecs.includes("opus")) {
120
+ mp4AudioCodecs.push("opus")
121
+ }
122
+ if (canPlayAudioType('audio/mp4; codecs="ec-3"')) {
123
+ mp4AudioCodecs.push("eac3")
124
+ }
125
+ if (canPlayAudioType('audio/mp4; codecs="ac-3"')) {
126
+ mp4AudioCodecs.push("ac3")
127
+ }
128
+ if (canPlayAudioType('audio/webm; codecs="opus"') && !webmAudioCodecs.includes("opus")) {
129
+ webmAudioCodecs.push("opus")
130
+ }
131
+
132
+ if (mp4VideoCodecs.length) {
133
+ profiles.push({
134
+ Container: "mp4,m4v,mov",
135
+ Type: "Video",
136
+ VideoCodec: mp4VideoCodecs.join(","),
137
+ AudioCodec: Array.from(new Set(mp4AudioCodecs)).join(","),
138
+ })
139
+ }
140
+ if (webmVideoCodecs.length) {
141
+ profiles.push({
142
+ Container: "webm",
143
+ Type: "Video",
144
+ VideoCodec: Array.from(new Set(webmVideoCodecs)).join(","),
145
+ AudioCodec: Array.from(new Set(webmAudioCodecs)).join(","),
146
+ })
147
+ }
148
+ return profiles
149
+ }
150
+
151
+ export function buildDeviceProfile(): DeviceProfile {
152
+ const profile: DeviceProfile = {
153
+ Name: detectBrowserName(),
154
+ DirectPlayProfiles: buildVideoDirectPlayProfiles(),
155
+ CodecProfiles: [
156
+ {
157
+ Type: "Video",
158
+ Codec: "h264",
159
+ Conditions: [
160
+ { Condition: "LessThanEqual", Property: "VideoLevel", Value: "51" },
161
+ ],
162
+ },
163
+ {
164
+ Type: "Audio",
165
+ Codec: "aac,mp3,opus,vorbis,ac3,eac3",
166
+ Conditions: [
167
+ { Condition: "LessThanEqual", Property: "AudioChannels", Value: "8" },
168
+ ],
169
+ },
170
+ ],
171
+ MaxStreamingBitrate: 120000000,
172
+ MaxStaticBitrate: 120000000,
173
+ }
174
+ return profile
175
+ }
176
+
177
+ function normalizeMediaPrefix(mediaPrefix?: string): string {
178
+ if (!mediaPrefix) {
179
+ return "/_lzc/media"
180
+ }
181
+ return mediaPrefix.endsWith("/") ? mediaPrefix.slice(0, -1) : mediaPrefix
182
+ }
183
+
184
+ function buildOwnerQueryFromSourceUrl(sourceUrl?: string): string {
185
+ if (!sourceUrl) {
186
+ return ""
187
+ }
188
+ try {
189
+ const base =
190
+ typeof window !== "undefined" ? window.location.origin : "http://localhost"
191
+ const url = new URL(sourceUrl, base)
192
+ const owner = url.searchParams.get("X_LZCAPI_UID")
193
+ if (!owner) {
194
+ return ""
195
+ }
196
+ const params = new URLSearchParams()
197
+ params.set("X_LZCAPI_UID", owner)
198
+ return `?${params.toString()}`
199
+ } catch {
200
+ return ""
201
+ }
202
+ }
203
+
204
+ export async function fetchOriginPlaybackDecision(
205
+ info: VideoInfo,
206
+ mediaPrefix?: string,
207
+ ): Promise<OriginPlaybackDecision | undefined> {
208
+ if (!info.fromNetdisk || !info.path) {
209
+ return undefined
210
+ }
211
+ let path = info.path
212
+ if (path.startsWith("/")) {
213
+ path = path.slice(1)
214
+ }
215
+ const encodedPath = encodeURIComponent(path)
216
+ const prefix = normalizeMediaPrefix(mediaPrefix)
217
+ const url = `${prefix}/play/info/${encodedPath}${buildOwnerQueryFromSourceUrl(
218
+ info.sourceUrl,
219
+ )}`
220
+ const resp = await fetch(url, {
221
+ method: "POST",
222
+ headers: {
223
+ "Content-Type": "application/json",
224
+ },
225
+ body: JSON.stringify({
226
+ deviceProfile: buildDeviceProfile(),
227
+ }),
228
+ })
229
+ if (!resp.ok) {
230
+ throw new Error(`origin playback info failed: ${resp.status}`)
231
+ }
232
+ return (await resp.json()) as OriginPlaybackDecision
233
+ }
234
+
235
+ function parseAttributeList(input: string): Record<string, string> {
236
+ const attrs: Record<string, string> = {}
237
+ const pattern = /([A-Z0-9-]+)=("(?:[^"\\]|\\.)*"|[^,]*)/gi
238
+ let match: RegExpExecArray | null
239
+ while ((match = pattern.exec(input))) {
240
+ const key = match[1]
241
+ const rawValue = match[2] || ""
242
+ attrs[key] =
243
+ rawValue.startsWith('"') && rawValue.endsWith('"')
244
+ ? rawValue.slice(1, -1)
245
+ : rawValue
246
+ }
247
+ return attrs
248
+ }
249
+
250
+ function parseResolutionFromUri(uri: string): { width: number; height: number } {
251
+ const match = uri.match(
252
+ /(?:^|[\/_-])(\d{3,4})[xX](\d{3,4})(?:[\/_.-]|$)|(?:quality-|res-|_)(\d{3,4})p?(?:[\/_.-]|$)/i,
253
+ )
254
+ if (!match) {
255
+ return { width: 0, height: 0 }
256
+ }
257
+ if (match[1] && match[2]) {
258
+ return {
259
+ width: Number(match[1]) || 0,
260
+ height: Number(match[2]) || 0,
261
+ }
262
+ }
263
+ return {
264
+ width: 0,
265
+ height: Number(match[3]) || 0,
266
+ }
267
+ }
268
+
269
+ function inferLevelLabel(
270
+ attrs: Record<string, string>,
271
+ height: number,
272
+ bitrate: number,
273
+ ): string {
274
+ const name = (attrs.NAME || attrs["X-STREAM-NAME"] || "").trim()
275
+ if (/^origin$/i.test(name)) {
276
+ return "origin"
277
+ }
278
+ if (name) {
279
+ return /p$/i.test(name) ? name : `${name}P`
280
+ }
281
+ if (height > 0) {
282
+ return `${height}P`
283
+ }
284
+ if (bitrate > 0) {
285
+ return `${Math.round(bitrate / 1000)}K`
286
+ }
287
+ return ""
288
+ }
289
+
290
+ export async function fetchOriginHlsLevels(
291
+ masterUrl: string,
292
+ ): Promise<VideoQualityLevel[]> {
293
+ const resp = await fetch(masterUrl, { method: "GET" })
294
+ if (!resp.ok) {
295
+ throw new Error(`origin hls manifest failed: ${resp.status}`)
296
+ }
297
+
298
+ const text = await resp.text()
299
+ const lines = text
300
+ .split(/\r?\n/)
301
+ .map((line) => line.trim())
302
+ .filter((line) => line.length > 0)
303
+
304
+ const levels: VideoQualityLevel[] = []
305
+ let pendingAttrs: Record<string, string> | null = null
306
+
307
+ for (const line of lines) {
308
+ if (line.startsWith("#EXT-X-STREAM-INF:")) {
309
+ pendingAttrs = parseAttributeList(line.slice("#EXT-X-STREAM-INF:".length))
310
+ continue
311
+ }
312
+ if (line.startsWith("#")) {
313
+ continue
314
+ }
315
+ if (!pendingAttrs) {
316
+ continue
317
+ }
318
+
319
+ const resolution = (pendingAttrs.RESOLUTION || "").match(/^(\d+)x(\d+)$/i)
320
+ const fallbackResolution = parseResolutionFromUri(line)
321
+ const width = resolution
322
+ ? Number(resolution[1]) || 0
323
+ : fallbackResolution.width
324
+ const height = resolution
325
+ ? Number(resolution[2]) || 0
326
+ : fallbackResolution.height
327
+ const bitrate =
328
+ Number(pendingAttrs["AVERAGE-BANDWIDTH"]) ||
329
+ Number(pendingAttrs.BANDWIDTH) ||
330
+ 0
331
+ const label = inferLevelLabel(pendingAttrs, height, bitrate)
332
+
333
+ levels.push({
334
+ id: String(levels.length),
335
+ label,
336
+ height,
337
+ width,
338
+ bitrate,
339
+ enabled: true,
340
+ })
341
+ pendingAttrs = null
342
+ }
343
+
344
+ return levels
345
+ }
@@ -0,0 +1,42 @@
1
+ import type { Subtitle } from "@/model"
2
+
3
+ type SubtitleInfoResp = {
4
+ len: number
5
+ data: Subtitle[]
6
+ }
7
+
8
+ function basename(path: string): string {
9
+ return path.split('/').pop() ?? ''
10
+ }
11
+
12
+ function transform(data: Subtitle[]) {
13
+ return data.map(i => {
14
+ if (i.is_external && i.path) {
15
+ return { ...i, name: i.name || basename(i.path) }
16
+ }
17
+ return i
18
+ })
19
+ }
20
+
21
+ export async function getSubtitleInfo(
22
+ videoPath?: string,
23
+ ): Promise<Subtitle[] | undefined> {
24
+ try {
25
+ if (!videoPath) {
26
+ return
27
+ }
28
+ const resp = await fetch(videoPath)
29
+ if (!resp.ok) {
30
+ throw new Error(`Http Status: ${resp.status}`)
31
+ }
32
+ const data: SubtitleInfoResp = await resp.json()
33
+ if (data.len == 0) {
34
+ return
35
+ } else {
36
+ return transform(data.data)
37
+ }
38
+ } catch (error) {
39
+ console.error(`Failed to get subtitle info: ${error}`)
40
+ return
41
+ }
42
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * 简单事件发射器,用于 NativePlayer 与 useSource 等对接
3
+ */
4
+ type Listener = (e?: any) => void
5
+
6
+ export class EventEmitter {
7
+ private listeners: Record<string, Listener[]> = {}
8
+ private oneListeners: Record<string, Listener[]> = {}
9
+
10
+ on(event: string, fn: Listener) {
11
+ if (!this.listeners[event]) this.listeners[event] = []
12
+ this.listeners[event].push(fn)
13
+ }
14
+
15
+ one(event: string, fn: Listener) {
16
+ if (!this.oneListeners[event]) this.oneListeners[event] = []
17
+ this.oneListeners[event].push(fn)
18
+ }
19
+
20
+ off(event: string, fn?: Listener) {
21
+ if (fn) {
22
+ this.listeners[event] = (this.listeners[event] || []).filter(
23
+ (f) => f !== fn,
24
+ )
25
+ this.oneListeners[event] = (this.oneListeners[event] || []).filter(
26
+ (f) => f !== fn,
27
+ )
28
+ } else {
29
+ delete this.listeners[event]
30
+ delete this.oneListeners[event]
31
+ }
32
+ }
33
+
34
+ /**
35
+ * 支持两种用法:
36
+ * - trigger("eventName") 或 trigger("eventName", data)
37
+ * - trigger({ type: "eventName", ...data }) 与 video.js 兼容
38
+ */
39
+ trigger(
40
+ eventOrName: string | { type: string; [key: string]: unknown },
41
+ data?: unknown,
42
+ ) {
43
+ let eventName: string
44
+ let payload: unknown
45
+ if (typeof eventOrName === "string") {
46
+ eventName = eventOrName
47
+ payload = data
48
+ } else {
49
+ eventName = eventOrName.type
50
+ payload = eventOrName
51
+ }
52
+ const run = (fns: Listener[] = []) => fns.forEach((fn) => fn(payload))
53
+ run(this.oneListeners[eventName])
54
+ this.oneListeners[eventName] = []
55
+ run(this.listeners[eventName])
56
+ }
57
+
58
+ dispose() {
59
+ this.listeners = {}
60
+ this.oneListeners = {}
61
+ }
62
+ }