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,1431 @@
1
+ import type {
2
+ Subtitle,
3
+ VideoInfo,
4
+ VideoQualityLevel,
5
+ VideoResolution,
6
+ } from "@/model"
7
+ import { ref } from "vue"
8
+ import throttle from "lodash.throttle"
9
+ import { getFileLinkWithDefault } from "@/use/useVideoFrame"
10
+ import { LzcApp } from "@/use/useSdk"
11
+ import { getSubtitleInfo } from "./getSubtitleInfo"
12
+ import { join } from "path-browserify"
13
+ import type { LzcPlayer, SourceObject } from "./player"
14
+ import { subtitleDB } from "@/use/useSubtitle"
15
+ import { useHistoryInfo } from "@/stores/playlist"
16
+ import { isSourceEqual } from "@/use/useUtils"
17
+ import {
18
+ fetchOriginPlaybackDecision,
19
+ fetchOriginHlsLevels,
20
+ type OriginPlaybackDecision,
21
+ } from "./directPlay"
22
+
23
+ // 后端定义好的参数
24
+ const previewConfig = {
25
+ width: 120,
26
+ height: 96,
27
+ interval: 5, // 缩略图的时间间隔
28
+ tile: 4, // 缩略图的个数 16, tile=4x4
29
+ len: 16,
30
+ }
31
+
32
+ const LEGACY_SUBTITLE_TYPE_PARAM = "sub_type"
33
+ const LEGACY_SUBTITLE_INDEX_PARAM = "subtitle"
34
+ const LEGACY_SUBTITLE_CUSTOM_PARAM = "custom_sub"
35
+
36
+ export function useVideoPreview(baseUrlPrefix: string, player: LzcPlayer) {
37
+ const cache: Map<string, HTMLImageElement> = new Map()
38
+ let loading: boolean = false
39
+
40
+ function currentPage() {
41
+ return Math.trunc(
42
+ player.currentTime()! / (previewConfig.len * previewConfig.interval),
43
+ )
44
+ }
45
+
46
+ function genPreviewUrl(info: VideoInfo, page = 0): string | undefined {
47
+ const pageStart = page * previewConfig.len * previewConfig.interval
48
+ const filename = info.path
49
+ if (filename == undefined) {
50
+ return
51
+ }
52
+ const uri = `${baseUrlPrefix}/screenshot/${encodeURIComponent(
53
+ filename,
54
+ )}?start=${pageStart}`
55
+ uri.replace(/\/\//, "/")
56
+ return uri
57
+ }
58
+
59
+ function _loadPreview(page: number): any {
60
+ if (loading) {
61
+ return
62
+ }
63
+ const totalPage = Math.ceil(
64
+ player.duration()! / (previewConfig.len * previewConfig.interval),
65
+ )
66
+ if (page > totalPage) {
67
+ return
68
+ }
69
+
70
+ const info = player.currentVideoInfo()
71
+ if (!info) {
72
+ return
73
+ }
74
+ if (!info.fromNetdisk && !info.path) {
75
+ return
76
+ }
77
+
78
+ const url = genPreviewUrl(info, page)
79
+ if (url == undefined) {
80
+ return
81
+ }
82
+
83
+ if (cache.has(url)) {
84
+ return _loadPreview(page + 1)
85
+ }
86
+
87
+ const img = new Image()
88
+ img.src = url
89
+
90
+ loading = true
91
+ img.onload = () => {
92
+ cache.set(url, img)
93
+ loading = false
94
+ }
95
+ }
96
+
97
+ function loadPreview() {
98
+ const page = currentPage()
99
+ return _loadPreview(page)
100
+ }
101
+
102
+ function getPreview() {
103
+ const page = currentPage()
104
+ const info = player.currentVideoInfo()
105
+ if (!info) {
106
+ return
107
+ }
108
+ if (!info.fromNetdisk && !info.path) {
109
+ return
110
+ }
111
+ const url = genPreviewUrl(info, page)
112
+ if (url == undefined) {
113
+ return
114
+ }
115
+ return cache.get(url)
116
+ }
117
+
118
+ return { loadPreview, getPreview, currentPage }
119
+ }
120
+
121
+ interface UseSourceInternalOptions {
122
+ externalPrevNextControl?: {
123
+ prev?: boolean
124
+ next?: boolean
125
+ }
126
+ }
127
+
128
+ export function useSource(
129
+ player: LzcPlayer,
130
+ options: UseSourceInternalOptions = {},
131
+ ) {
132
+ const isDirectPlayerMode = player.options_.playMode === "direct"
133
+ const store = useHistoryInfo()
134
+ const externalPrevNextControl = {
135
+ prev: options.externalPrevNextControl?.prev === true,
136
+ next: options.externalPrevNextControl?.next === true,
137
+ }
138
+ let openVideoSeq = 0
139
+ let loadedMetadataSeekSeq = 0
140
+ const safePlay = () => {
141
+ void Promise.resolve(player.play()).catch((err: any) => {
142
+ if (err?.name !== "AbortError") {
143
+ console.error("play failed after source update", err)
144
+ }
145
+ })
146
+ }
147
+ const invalidateLoadedMetadataSeek = () => {
148
+ loadedMetadataSeekSeq += 1
149
+ }
150
+ const isSameVideo = (a?: VideoInfo | null, b?: VideoInfo | null) => {
151
+ if (!a || !b) return false
152
+ if (a.path && b.path) {
153
+ return a.path === b.path
154
+ }
155
+ try {
156
+ return isSourceEqual(a.sourceUrl, b)
157
+ } catch {
158
+ return a.sourceUrl === b.sourceUrl
159
+ }
160
+ }
161
+ const mergeHistoryInfo = (
162
+ incoming: VideoInfo,
163
+ history?: VideoInfo | null,
164
+ ): VideoInfo => {
165
+ if (!history) {
166
+ return incoming
167
+ }
168
+ return {
169
+ ...history,
170
+ ...incoming,
171
+ _id: history._id,
172
+ updateTime: history.updateTime,
173
+ currentTime:
174
+ typeof incoming.currentTime === "number" && incoming.currentTime > 0
175
+ ? incoming.currentTime
176
+ : history.currentTime,
177
+ duration:
178
+ typeof incoming.duration === "number" && incoming.duration > 0
179
+ ? incoming.duration
180
+ : history.duration,
181
+ invalid: history.invalid,
182
+ subtitles: history.subtitles ?? incoming.subtitles,
183
+ beforeHiddenSubtitle:
184
+ history.beforeHiddenSubtitle ?? incoming.beforeHiddenSubtitle,
185
+ path: incoming.path || history.path,
186
+ fromNetdisk: incoming.fromNetdisk || history.fromNetdisk,
187
+ name: incoming.name || history.name,
188
+ }
189
+ }
190
+ const scheduleLoadedMetadataSeek = (
191
+ seekTo: number,
192
+ expectedInfo?: VideoInfo | null,
193
+ afterLoadedMetadata?: () => void,
194
+ ) => {
195
+ invalidateLoadedMetadataSeek()
196
+ const seq = loadedMetadataSeekSeq
197
+ player.one("loadedmetadata", () => {
198
+ if (seq !== loadedMetadataSeekSeq) return
199
+ if (expectedInfo) {
200
+ const currentInfo = player.currentVideoInfo?.()
201
+ if (!isSameVideo(currentInfo, expectedInfo)) {
202
+ return
203
+ }
204
+ }
205
+ player.currentTime(seekTo)
206
+ afterLoadedMetadata?.()
207
+ })
208
+ }
209
+ const normalizeQualityToMaster = (url: string) => {
210
+ if (!/\.m3u8(\?|$)/i.test(url)) return url
211
+ const [base, search] = url.split("?")
212
+ const replaced = base.replace(/\/quality-[^/]*\.m3u8$/i, "/master.m3u8")
213
+ if (!search) return replaced
214
+ return `${replaced}?${search}`
215
+ }
216
+
217
+ let videoDisplayName: string = ""
218
+ let autoSwitchResolution: boolean = false
219
+ let subtitleLoading = false
220
+ let baseUrlPrefix = "/_lzc/media"
221
+ if (player.options_.mediaPrefix) {
222
+ if (player.options_.mediaPrefix.endsWith("/")) {
223
+ baseUrlPrefix = player.options_.mediaPrefix.substring(
224
+ 0,
225
+ player.options_.mediaPrefix.length - 1,
226
+ )
227
+ } else {
228
+ baseUrlPrefix = player.options_.mediaPrefix
229
+ }
230
+ }
231
+
232
+ const previewImageCache = useVideoPreview(baseUrlPrefix, player)
233
+ const qualityLevels = player.qualityLevels()
234
+
235
+ function autoEnableAndDisableResolution(
236
+ index?: number,
237
+ options: { trigger?: boolean } = {},
238
+ ) {
239
+ if (index !== undefined) {
240
+ autoSwitchResolution = false
241
+ } else {
242
+ // autoSwitchResolution = true;
243
+ return
244
+ }
245
+ for (let i = 0; i < qualityLevels.levels_.length; i++) {
246
+ if (autoSwitchResolution) {
247
+ qualityLevels.levels_[i].enabled = true
248
+ } else {
249
+ if (i == index) {
250
+ qualityLevels.levels_[i].enabled = true
251
+ } else {
252
+ qualityLevels.levels_[i].enabled = false
253
+ }
254
+ }
255
+ }
256
+ if (options.trigger !== false) {
257
+ qualityLevels.trigger({ type: "change", selectedIndex: index })
258
+ }
259
+ }
260
+
261
+ // 注意 player 中的函数重新保存到其他变量上需要 bind 函数的上下文
262
+ function init(info: VideoInfo) {
263
+ getFileLinkWithDefault(info._id).then((link) => {
264
+ player.poster(link)
265
+ })
266
+ const normalizedSrc =
267
+ isDirectPlayerMode
268
+ ? (info.resolvedSourceUrl || "").trim() ||
269
+ normalizeQualityToMaster(info.sourceUrl)
270
+ : (info.resolvedSourceUrl || "").trim() ||
271
+ (originDecision?.originMode === "direct"
272
+ ? originDecision.originDirectUrl
273
+ : originDecision?.originHlsUrl) ||
274
+ normalizeQualityToMaster(info.sourceUrl)
275
+ currentTransportMode = /\.m3u8(\?|$)/i.test(normalizedSrc)
276
+ ? "hls"
277
+ : "direct"
278
+ restorePlaybackRateAfterSourceReload()
279
+ player.src(normalizedSrc)
280
+
281
+ store.ready.then(() => {
282
+ const currentIndex = store.infos.findIndex((i: VideoInfo) =>
283
+ isSourceEqual(i.sourceUrl, info),
284
+ )
285
+ const total = store.infos.length
286
+ if (!externalPrevNextControl.prev) {
287
+ player.enablePrev(currentIndex !== 0)
288
+ }
289
+ if (!externalPrevNextControl.next) {
290
+ player.enableNext(currentIndex !== total - 1)
291
+ }
292
+ })
293
+
294
+ if (info.name) {
295
+ videoDisplayName = info.name
296
+ }
297
+ if (info.fromNetdisk && !LzcApp.isTvOsWebShell()) {
298
+ player.on(
299
+ "timeupdate",
300
+ throttle(() => {
301
+ // HaveEnoughData = 4, Idle = 1, Loading = 2
302
+ if (player.networkState() == 1 || player.networkState() == 2) {
303
+ previewImageCache.loadPreview()
304
+ }
305
+ }, 2000),
306
+ )
307
+ }
308
+ }
309
+
310
+ function videoSrc() {
311
+ const src = player.currentSrc()
312
+ return src.split("?")[0]
313
+ }
314
+
315
+ function videoName() {
316
+ if (videoDisplayName !== "") {
317
+ return videoDisplayName
318
+ }
319
+ const currSrc = videoSrc()
320
+ return currSrc?.split("/").slice(-1)[0]
321
+ }
322
+
323
+ function currentPreview() {
324
+ const image = previewImageCache.getPreview()
325
+ if (!image) {
326
+ return
327
+ }
328
+ const page = previewImageCache.currentPage()
329
+ return {
330
+ ...previewConfig,
331
+ page,
332
+ image,
333
+ }
334
+ }
335
+
336
+ const defaultAvaliableSub = ref<Subtitle | undefined>()
337
+ let subtitleHidden = false
338
+ let lastVisibleSubtitle: Subtitle | undefined = undefined
339
+ let netdiskPath: string | undefined = undefined
340
+ let resolvedPlayableSource: string | undefined = undefined
341
+ let subtitleSourceOverride: string | undefined = undefined
342
+ let resolutionDisplayLock: VideoResolution | null = null
343
+ let resolutionDisplayLockTimer: ReturnType<typeof setTimeout> | null = null
344
+ let originDecision: OriginPlaybackDecision | undefined = undefined
345
+ let currentTransportMode: "hls" | "direct" = "hls"
346
+ let logicalResolutionLevels: VideoQualityLevel[] = []
347
+ let manifestResolutionLevels: VideoQualityLevel[] = []
348
+ let directOriginFallbackPending = false
349
+
350
+ const setSubtitleLoading = (value: boolean) => {
351
+ if (subtitleLoading === value) {
352
+ return
353
+ }
354
+ subtitleLoading = value
355
+ player.trigger("subtitleloadingchange")
356
+ }
357
+
358
+ player.isSubtitleLoading = () => subtitleLoading
359
+ player.logicalQualityLevels = () => logicalResolutionLevels
360
+
361
+ const refreshLogicalResolutionLevels = () => {
362
+ if (isDirectPlayerMode) {
363
+ logicalResolutionLevels = []
364
+ return
365
+ }
366
+ const rawLevels =
367
+ qualityLevels.levels_.length > 0 ? qualityLevels.levels_ : manifestResolutionLevels
368
+
369
+ const hlsLevels: VideoQualityLevel[] = [...rawLevels]
370
+ .map((level) => ({
371
+ ...level,
372
+ transport: "hls" as "hls" | "direct",
373
+ }))
374
+ .sort((a, b) => {
375
+ if (b.height === a.height) {
376
+ return b.bitrate - a.bitrate
377
+ }
378
+ return b.height - a.height
379
+ })
380
+
381
+ if (originDecision) {
382
+ const hasOrigin = hlsLevels.some((level) => level.label === "origin")
383
+ if (!hasOrigin) {
384
+ hlsLevels.unshift({
385
+ id: "origin",
386
+ label: "origin",
387
+ height: originDecision.mediaSource.height || 0,
388
+ width: originDecision.mediaSource.width || 0,
389
+ bitrate: originDecision.mediaSource.bitrate || 0,
390
+ transport: originDecision.originMode,
391
+ })
392
+ } else {
393
+ const originLevel = hlsLevels.find((level) => level.label === "origin")
394
+ if (originLevel) {
395
+ originLevel.transport = originDecision.originMode
396
+ }
397
+ }
398
+ }
399
+ logicalResolutionLevels = hlsLevels
400
+ }
401
+
402
+ const syncManifestResolutionLevels = (levels: VideoQualityLevel[]) => {
403
+ manifestResolutionLevels = levels.map((level, index) => ({
404
+ ...level,
405
+ id: `manifest-${level.label || "level"}-${level.height}-${level.width}-${level.bitrate}-${index}`,
406
+ enabled: level.enabled ?? true,
407
+ }))
408
+ refreshLogicalResolutionLevels()
409
+ }
410
+
411
+ const clearResolutionDisplayLockTimer = () => {
412
+ if (resolutionDisplayLockTimer) {
413
+ clearTimeout(resolutionDisplayLockTimer)
414
+ resolutionDisplayLockTimer = null
415
+ }
416
+ }
417
+
418
+ const setResolutionDisplayLock = (snapshot?: VideoResolution) => {
419
+ clearResolutionDisplayLockTimer()
420
+ if (!snapshot) {
421
+ resolutionDisplayLock = null
422
+ return
423
+ }
424
+ resolutionDisplayLock = { ...snapshot }
425
+ // Fallback unlock to avoid stale lock if source reload fails unexpectedly.
426
+ resolutionDisplayLockTimer = setTimeout(() => {
427
+ resolutionDisplayLock = null
428
+ resolutionDisplayLockTimer = null
429
+ }, 8000)
430
+ }
431
+
432
+ const releaseResolutionDisplayLock = () => {
433
+ clearResolutionDisplayLockTimer()
434
+ resolutionDisplayLock = null
435
+ }
436
+
437
+ const buildSubtitleInfoUrl = (
438
+ path: string | undefined,
439
+ search = "",
440
+ ): string | undefined => {
441
+ if (!path) {
442
+ return undefined
443
+ }
444
+ const base = join(baseUrlPrefix, "hls", path, "subtitle-info")
445
+ if (!search) {
446
+ return base
447
+ }
448
+ return `${base}${search.startsWith("?") ? search : `?${search}`}`
449
+ }
450
+
451
+ const escapeRegExp = (value: string) => {
452
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
453
+ }
454
+
455
+ const normalizeSubtitleMediaUrl = (url?: string): string | undefined => {
456
+ const raw = (url || "").trim()
457
+ if (!raw) {
458
+ return raw
459
+ }
460
+ if (/^(https?:)?\/\//i.test(raw) || /^(blob:|data:)/i.test(raw)) {
461
+ return raw
462
+ }
463
+ const mediaHlsPrefix = `${baseUrlPrefix}/hls/`
464
+ if (raw.startsWith(mediaHlsPrefix)) {
465
+ return raw
466
+ }
467
+ if (raw.startsWith("/hls/")) {
468
+ return `${baseUrlPrefix}${raw}`
469
+ }
470
+ if (raw.startsWith("hls/")) {
471
+ return `${baseUrlPrefix}/${raw}`
472
+ }
473
+ return raw
474
+ }
475
+
476
+ const normalizeSubtitle = (subtitle: Subtitle): Subtitle => {
477
+ return {
478
+ ...subtitle,
479
+ vtt_url: normalizeSubtitleMediaUrl(subtitle.vtt_url),
480
+ ass_url: normalizeSubtitleMediaUrl(subtitle.ass_url),
481
+ ass_fonts: (subtitle.ass_fonts || [])
482
+ .map((item) => normalizeSubtitleMediaUrl(item))
483
+ .filter((item): item is string => !!item),
484
+ }
485
+ }
486
+
487
+ const hasRenderableSubtitleSource = (subtitle?: Subtitle): boolean => {
488
+ return !!((subtitle?.vtt_url || subtitle?.ass_url || "").trim())
489
+ }
490
+
491
+ const isSelectableSubtitle = (subtitle?: Subtitle): subtitle is Subtitle => {
492
+ if (!subtitle) {
493
+ return false
494
+ }
495
+ return (
496
+ hasRenderableSubtitleSource(subtitle) ||
497
+ canUseLegacySubtitleProtocol(subtitle)
498
+ )
499
+ }
500
+
501
+ const canUseLegacySubtitleProtocol = (subtitle?: Subtitle): boolean => {
502
+ if (!subtitle) {
503
+ return false
504
+ }
505
+ if (hasRenderableSubtitleSource(subtitle)) {
506
+ return false
507
+ }
508
+ return Number.isInteger(subtitle.stream_index) && subtitle.stream_index >= 0
509
+ }
510
+
511
+ const getLegacySubtitleTypeValue = (subtitle: Subtitle): string => {
512
+ // media legacy protocol: 1 => internal, 2 => external
513
+ return subtitle.is_external ? "2" : "1"
514
+ }
515
+
516
+ const getLegacyProbeSubtitle = () => {
517
+ if (subtitleHidden) {
518
+ return lastVisibleSubtitle ?? defaultAvaliableSub.value
519
+ }
520
+ return defaultAvaliableSub.value
521
+ }
522
+
523
+ player.isUsingNativeSubtitleFallback = () => {
524
+ return canUseLegacySubtitleProtocol(getLegacyProbeSubtitle())
525
+ }
526
+
527
+ /**
528
+ * 修改字幕初始化时间,防止changeSubtitle中loadedmetadata在配置未加载前改变播放时间
529
+ */
530
+ function initSubtitles() {
531
+ const seq = openVideoSeq
532
+ const info = player.currentVideoInfo()
533
+ if (!info) {
534
+ setSubtitleLoading(false)
535
+ return
536
+ }
537
+ let SubtitlePath: string | undefined = undefined
538
+
539
+ if (info.subtitleInfoUrl) {
540
+ SubtitlePath = info.subtitleInfoUrl
541
+ if (info.path) {
542
+ netdiskPath = info.path
543
+ if (netdiskPath?.startsWith("/")) {
544
+ netdiskPath = encodeURIComponent(netdiskPath.substring(1))
545
+ }
546
+ }
547
+ } else if (info.path) {
548
+ netdiskPath = info.path
549
+ if (netdiskPath?.startsWith("/")) {
550
+ netdiskPath = encodeURIComponent(netdiskPath.substring(1))
551
+ }
552
+ SubtitlePath = buildSubtitleInfoUrl(netdiskPath)
553
+ } else {
554
+ const originPath = new URL(info.sourceUrl)
555
+ const url = originPath.origin + originPath.pathname
556
+ const searchPath = originPath.search || ""
557
+ const mediaHlsPrefixPattern = new RegExp(
558
+ `^https?:\\/\\/[^/]+${escapeRegExp(`${baseUrlPrefix}/hls`)}`,
559
+ )
560
+ netdiskPath = decodeURIComponent(url)
561
+ .replace(mediaHlsPrefixPattern, "")
562
+ .replace(/\/[^/]*\.m3u8$/, "")
563
+ if (netdiskPath?.startsWith("/")) {
564
+ netdiskPath = encodeURIComponent(netdiskPath.substring(1))
565
+ }
566
+ SubtitlePath = buildSubtitleInfoUrl(netdiskPath, searchPath)
567
+ }
568
+ const subtitleHistory = subtitleDB.findOne(netdiskPath || "")
569
+ const lastSelectedSubtitle = subtitleDB.findLastSelected()
570
+
571
+ player.getAvaliableSubtitles = () => []
572
+
573
+ player.currentSubtitle = () => undefined
574
+
575
+ setSubtitleLoading(true)
576
+
577
+ void getSubtitleInfo(SubtitlePath)
578
+ .then(async (subtitles?) => {
579
+ if (seq !== openVideoSeq) {
580
+ return
581
+ }
582
+ if (subtitles === undefined || subtitles.length === 0) {
583
+ console.info("No subtitles found")
584
+ return
585
+ }
586
+ const normalizedSubtitles = subtitles.map(normalizeSubtitle)
587
+
588
+ player.getAvaliableSubtitles = () => {
589
+ return normalizedSubtitles
590
+ }
591
+
592
+ const fallbackSubtitle =
593
+ normalizedSubtitles.find((i) => isSelectableSubtitle(i)) ??
594
+ normalizedSubtitles[0]
595
+ if (!fallbackSubtitle) {
596
+ return
597
+ }
598
+ const [subtitleHistoryResult, lastSelectedSubtitleResult] =
599
+ await Promise.all([subtitleHistory, lastSelectedSubtitle])
600
+ if (seq !== openVideoSeq) {
601
+ return
602
+ }
603
+
604
+ const subtitleByHistory = normalizedSubtitles.find(
605
+ (i) =>
606
+ i.name === subtitleHistoryResult?.name &&
607
+ i.stream_index === subtitleHistoryResult?.index,
608
+ )
609
+ const subtitleByLastName = subtitleByHistory
610
+ ? undefined
611
+ : normalizedSubtitles.find(
612
+ (i) =>
613
+ isSelectableSubtitle(i) &&
614
+ i.name === lastSelectedSubtitleResult?.name,
615
+ )
616
+ const subtitleByLastType =
617
+ subtitleByHistory || subtitleByLastName
618
+ ? undefined
619
+ : typeof lastSelectedSubtitleResult?.is_external === "boolean"
620
+ ? normalizedSubtitles.find(
621
+ (i) =>
622
+ isSelectableSubtitle(i) &&
623
+ i.is_external === lastSelectedSubtitleResult.is_external,
624
+ )
625
+ : undefined
626
+
627
+ const defaultSubtitle =
628
+ subtitleByHistory ??
629
+ subtitleByLastName ??
630
+ subtitleByLastType ??
631
+ fallbackSubtitle
632
+ defaultAvaliableSub.value = defaultSubtitle
633
+ lastVisibleSubtitle = defaultSubtitle
634
+
635
+ player.currentSubtitle = () => {
636
+ const current = defaultAvaliableSub.value
637
+ if (!current) return undefined
638
+ return {
639
+ name: current.name,
640
+ language: current.language,
641
+ stream_index: current.stream_index,
642
+ codec_name: current.codec_name,
643
+ is_external: current.is_external,
644
+ path: current.path,
645
+ vtt_url: normalizeSubtitleMediaUrl(current.vtt_url),
646
+ track_id: current.track_id,
647
+ ass_url: normalizeSubtitleMediaUrl(current.ass_url),
648
+ ass_fonts: (current.ass_fonts || [])
649
+ .map((item) => normalizeSubtitleMediaUrl(item))
650
+ .filter((item): item is string => !!item),
651
+ ass_renderable: current.ass_renderable,
652
+ ass_unavailable_reason: current.ass_unavailable_reason,
653
+ }
654
+ }
655
+
656
+ setSubtitleLoading(false)
657
+ player.trigger("subtitleReady")
658
+
659
+ if (defaultAvaliableSub.value && !player.isSubtitleHidden?.()) {
660
+ player.changeSubtitle(defaultAvaliableSub.value)
661
+ }
662
+ })
663
+ .finally(() => {
664
+ if (seq !== openVideoSeq) {
665
+ return
666
+ }
667
+ setSubtitleLoading(false)
668
+ })
669
+ }
670
+
671
+ const _src = player.src.bind(player)
672
+
673
+ const getOriginPreferredSource = () => {
674
+ if (!originDecision) {
675
+ return undefined
676
+ }
677
+ return originDecision.originMode === "direct"
678
+ ? originDecision.originDirectUrl
679
+ : originDecision.originHlsUrl
680
+ }
681
+
682
+ const forceOriginFallbackToHLS = () => {
683
+ if (isDirectPlayerMode) {
684
+ return
685
+ }
686
+ if (!originDecision) {
687
+ return
688
+ }
689
+ originDecision = {
690
+ ...originDecision,
691
+ originMode: "hls",
692
+ }
693
+ const current = player.currentVideoInfo?.()
694
+ if (current) {
695
+ current.playMode = "hls"
696
+ current.resolvedSourceUrl = originDecision.originHlsUrl
697
+ }
698
+ currentTransportMode = "hls"
699
+ refreshLogicalResolutionLevels()
700
+ }
701
+
702
+ const getCurrentPlayableSourceUrl = () => {
703
+ const preferred = (subtitleSourceOverride || resolvedPlayableSource || "").trim()
704
+ if (preferred) {
705
+ return preferred
706
+ }
707
+ const source = (player.currentSource?.().src || player.currentSrc() || "").trim()
708
+ if (source && !/^blob:/i.test(source)) {
709
+ return source
710
+ }
711
+ const infoSource = (player.currentVideoInfo?.()?.sourceUrl || "").trim()
712
+ if (infoSource) {
713
+ return normalizeQualityToMaster(infoSource)
714
+ }
715
+ return ""
716
+ }
717
+
718
+ const parseSourceUrl = (raw: string) => {
719
+ if (!raw) return undefined
720
+ try {
721
+ const baseHref =
722
+ typeof window !== "undefined"
723
+ ? window.location.href
724
+ : "http://localhost/"
725
+ return new URL(raw, baseHref)
726
+ } catch (err) {
727
+ console.error("Failed to parse current source url", { raw, err })
728
+ return undefined
729
+ }
730
+ }
731
+
732
+ const toOutputSourceUrl = (parsed: URL, raw: string) => {
733
+ if (/^(https?:)?\/\//i.test(raw)) {
734
+ return parsed.toString()
735
+ }
736
+ if (raw.startsWith("//")) {
737
+ return parsed.toString().replace(/^[^:]+:/, "")
738
+ }
739
+ return `${parsed.pathname}${parsed.search}${parsed.hash}`
740
+ }
741
+
742
+ const hasLegacySubtitleParamsInSource = () => {
743
+ const raw = getCurrentPlayableSourceUrl()
744
+ const parsed = parseSourceUrl(raw)
745
+ if (!parsed) {
746
+ return false
747
+ }
748
+ return (
749
+ parsed.searchParams.has(LEGACY_SUBTITLE_TYPE_PARAM) ||
750
+ parsed.searchParams.has(LEGACY_SUBTITLE_INDEX_PARAM) ||
751
+ parsed.searchParams.has(LEGACY_SUBTITLE_CUSTOM_PARAM)
752
+ )
753
+ }
754
+
755
+ const buildLegacySubtitleSwitchUrl = (
756
+ mode: "select" | "clear",
757
+ subtitle?: Subtitle,
758
+ ): string | undefined => {
759
+ const raw = getCurrentPlayableSourceUrl()
760
+ const parsed = parseSourceUrl(raw)
761
+ if (!parsed) {
762
+ return undefined
763
+ }
764
+
765
+ parsed.searchParams.delete(LEGACY_SUBTITLE_CUSTOM_PARAM)
766
+
767
+ if (mode === "select") {
768
+ if (!subtitle) {
769
+ return undefined
770
+ }
771
+ parsed.searchParams.set(
772
+ LEGACY_SUBTITLE_TYPE_PARAM,
773
+ getLegacySubtitleTypeValue(subtitle),
774
+ )
775
+ parsed.searchParams.set(
776
+ LEGACY_SUBTITLE_INDEX_PARAM,
777
+ `${subtitle.stream_index}`,
778
+ )
779
+ } else {
780
+ parsed.searchParams.delete(LEGACY_SUBTITLE_TYPE_PARAM)
781
+ parsed.searchParams.delete(LEGACY_SUBTITLE_INDEX_PARAM)
782
+ }
783
+
784
+ return toOutputSourceUrl(parsed, raw)
785
+ }
786
+
787
+ const getPreservedPlaybackRate = () => {
788
+ const playbackRateValue = player.playbackRate?.()
789
+ const preservedPlaybackRate =
790
+ typeof playbackRateValue === "number" && Number.isFinite(playbackRateValue)
791
+ ? playbackRateValue
792
+ : undefined
793
+ if (preservedPlaybackRate == null || preservedPlaybackRate <= 0) {
794
+ return undefined
795
+ }
796
+ return preservedPlaybackRate
797
+ }
798
+
799
+ const restorePlaybackRateAfterSourceReload = () => {
800
+ const preservedPlaybackRate = getPreservedPlaybackRate()
801
+ if (preservedPlaybackRate === undefined || preservedPlaybackRate === 1) {
802
+ return
803
+ }
804
+ player.one("loadedmetadata", () => {
805
+ player.playbackRate?.(preservedPlaybackRate)
806
+ })
807
+ }
808
+
809
+ const reloadSourceWithSeek = (nextSrc: string) => {
810
+ const seekTo = (player.duration() !== Infinity && player.currentTime()) || 0
811
+ const expectedInfo = player.currentVideoInfo?.()
812
+ scheduleLoadedMetadataSeek(seekTo, expectedInfo)
813
+ restorePlaybackRateAfterSourceReload()
814
+ resolvedPlayableSource = nextSrc
815
+ subtitleSourceOverride = nextSrc
816
+ currentTransportMode = /\.m3u8(\?|$)/i.test(nextSrc) ? "hls" : "direct"
817
+ _src({
818
+ src: nextSrc,
819
+ type: currentTransportMode === "hls" ? "application/x-mpegURL" : "",
820
+ })
821
+ safePlay()
822
+ }
823
+
824
+ const reloadSourceWithSeekAndResolution = (
825
+ nextSrc: string,
826
+ level: VideoQualityLevel,
827
+ ) => {
828
+ const seekTo = (player.duration() !== Infinity && player.currentTime()) || 0
829
+ const expectedInfo = player.currentVideoInfo?.()
830
+ scheduleLoadedMetadataSeek(seekTo, expectedInfo, () => {
831
+ const index = findCurrentQualityLevelIndex(level)
832
+ if (index >= 0) {
833
+ autoEnableAndDisableResolution(index, { trigger: false })
834
+ baseChangeResolution(qualityLevels.levels_[index] || level)
835
+ } else {
836
+ baseChangeResolution(level)
837
+ }
838
+ releaseResolutionDisplayLock()
839
+ })
840
+ restorePlaybackRateAfterSourceReload()
841
+ resolvedPlayableSource = nextSrc
842
+ subtitleSourceOverride = nextSrc
843
+ currentTransportMode = /\.m3u8(\?|$)/i.test(nextSrc) ? "hls" : "direct"
844
+ _src({
845
+ src: nextSrc,
846
+ type: currentTransportMode === "hls" ? "application/x-mpegURL" : "",
847
+ })
848
+ if (currentTransportMode === "hls") {
849
+ baseChangeResolution(level)
850
+ }
851
+ safePlay()
852
+ }
853
+
854
+ const applyLegacySubtitleSelection = (subtitle: Subtitle) => {
855
+ if (!canUseLegacySubtitleProtocol(subtitle)) {
856
+ return false
857
+ }
858
+ const nextSrc = buildLegacySubtitleSwitchUrl("select", subtitle)
859
+ if (!nextSrc) {
860
+ return false
861
+ }
862
+ const currentSrc = getCurrentPlayableSourceUrl()
863
+ if (currentSrc === nextSrc) {
864
+ return true
865
+ }
866
+ reloadSourceWithSeek(nextSrc)
867
+ return true
868
+ }
869
+
870
+ const clearLegacySubtitleSelection = () => {
871
+ if (
872
+ !canUseLegacySubtitleProtocol(getLegacyProbeSubtitle()) &&
873
+ !hasLegacySubtitleParamsInSource()
874
+ ) {
875
+ return false
876
+ }
877
+ const nextSrc = buildLegacySubtitleSwitchUrl("clear")
878
+ if (!nextSrc) {
879
+ return false
880
+ }
881
+ const currentSrc = getCurrentPlayableSourceUrl()
882
+ if (currentSrc === nextSrc) {
883
+ return true
884
+ }
885
+ reloadSourceWithSeek(nextSrc)
886
+ return true
887
+ }
888
+
889
+ player.changeSubtitle = (sub: Subtitle | number) => {
890
+ if (isDirectPlayerMode) {
891
+ return
892
+ }
893
+ const subs = player.getAvaliableSubtitles()
894
+ if (subs === undefined) {
895
+ return
896
+ }
897
+ let selectedSubtitle: Subtitle | undefined
898
+ const updateSelection = (sub: Subtitle) => {
899
+ if (netdiskPath) {
900
+ subtitleDB.update(netdiskPath, {
901
+ name: sub.name,
902
+ index: sub.stream_index,
903
+ })
904
+ }
905
+ subtitleDB.updateLastSelected({
906
+ name: sub.name,
907
+ is_external: sub.is_external,
908
+ })
909
+ defaultAvaliableSub.value = sub
910
+ }
911
+
912
+ if (typeof sub == "number") {
913
+ if (sub >= subs.length) {
914
+ return
915
+ }
916
+ selectedSubtitle = subs[sub]
917
+ if (selectedSubtitle) {
918
+ updateSelection(selectedSubtitle)
919
+ }
920
+ } else {
921
+ const canonicalSubtitle = subs.find(
922
+ (item) =>
923
+ item.name === sub.name && item.stream_index === sub.stream_index,
924
+ )
925
+ if (canonicalSubtitle) {
926
+ selectedSubtitle = canonicalSubtitle
927
+ updateSelection(canonicalSubtitle)
928
+ }
929
+ }
930
+ if (selectedSubtitle) {
931
+ subtitleHidden = false
932
+ lastVisibleSubtitle = selectedSubtitle
933
+ if (!applyLegacySubtitleSelection(selectedSubtitle)) {
934
+ if (hasRenderableSubtitleSource(selectedSubtitle)) {
935
+ clearLegacySubtitleSelection()
936
+ }
937
+ }
938
+ }
939
+ player.trigger("subtitlechange")
940
+ }
941
+
942
+ player.clearSubtitle = () => {
943
+ if (isDirectPlayerMode) {
944
+ return
945
+ }
946
+ if (netdiskPath) {
947
+ subtitleDB.update(netdiskPath, {})
948
+ }
949
+ clearLegacySubtitleSelection()
950
+ player.trigger("subtitlechange")
951
+ }
952
+
953
+ player.isSubtitleHidden = () => subtitleHidden
954
+
955
+ player.setSubtitleHidden = (value: boolean) => {
956
+ if (isDirectPlayerMode) return
957
+ if (subtitleHidden === value) return
958
+ subtitleHidden = value
959
+ if (value) {
960
+ const current = player.currentSubtitle?.()
961
+ if (current) {
962
+ lastVisibleSubtitle = current
963
+ }
964
+ player.clearSubtitle()
965
+ }
966
+ player.trigger("subtitlevisibilitychange")
967
+ }
968
+
969
+ player.toggleSubtitleVisibility = () => {
970
+ if (isDirectPlayerMode) return
971
+ if (subtitleHidden) {
972
+ subtitleHidden = false
973
+ player.trigger("subtitlevisibilitychange")
974
+ const next = lastVisibleSubtitle ?? defaultAvaliableSub.value
975
+ if (next) {
976
+ player.changeSubtitle(next)
977
+ }
978
+ return
979
+ }
980
+ player.setSubtitleHidden(true)
981
+ }
982
+
983
+ function src2(): string
984
+ function src2(sources: string | SourceObject | SourceObject[]): void
985
+ function src2(sources?: any): any {
986
+ if (!sources || sources == null || sources == undefined) {
987
+ return player.currentSrc()
988
+ }
989
+
990
+ const format = (url: string, stype = "") => {
991
+ const info = player.currentVideoInfo()
992
+ if (!info) {
993
+ return {
994
+ src: normalizeQualityToMaster(url),
995
+ type: stype,
996
+ }
997
+ }
998
+ if (
999
+ isDirectPlayerMode ||
1000
+ (info.fromNetdisk && info.path) ||
1001
+ (url.startsWith("/") && !url.startsWith(baseUrlPrefix)) // 兼容代码
1002
+ ) {
1003
+ const resolved = (
1004
+ isDirectPlayerMode ? info.sourceUrl : info.resolvedSourceUrl || ""
1005
+ ).trim()
1006
+ if (resolved) {
1007
+ return {
1008
+ src: resolved,
1009
+ type: /\.m3u8(\?|$)/i.test(resolved) ? "application/x-mpegURL" : "",
1010
+ }
1011
+ }
1012
+ let path = info.path
1013
+ if (path?.startsWith("/")) {
1014
+ path = path.substring(1)
1015
+ }
1016
+ if (path != null) {
1017
+ path = encodeURIComponent(path)
1018
+ }
1019
+ return {
1020
+ src: `${baseUrlPrefix}/hls/${path || url}/master.m3u8`.replace(
1021
+ /\/\//,
1022
+ "/",
1023
+ ),
1024
+ type: "application/x-mpegURL",
1025
+ }
1026
+ } else {
1027
+ return {
1028
+ src: normalizeQualityToMaster(url),
1029
+ type: stype,
1030
+ }
1031
+ }
1032
+ }
1033
+
1034
+ let newSources: SourceObject[] = []
1035
+ if (Array.isArray(sources)) {
1036
+ newSources = sources.map((s: SourceObject) => {
1037
+ return format(s.src, s.type)
1038
+ })
1039
+ } else if (typeof sources == "string") {
1040
+ newSources.push(format(sources, ""))
1041
+ } else {
1042
+ newSources.push(format(sources.src, sources.type))
1043
+ }
1044
+ if (newSources[0]?.src) {
1045
+ resolvedPlayableSource = `${newSources[0].src}`.trim()
1046
+ currentTransportMode = /\.m3u8(\?|$)/i.test(resolvedPlayableSource)
1047
+ ? "hls"
1048
+ : "direct"
1049
+ }
1050
+ _src(newSources)
1051
+ }
1052
+ player.src = src2
1053
+
1054
+ player.currentResolution = () => {
1055
+ if (resolutionDisplayLock) {
1056
+ return resolutionDisplayLock
1057
+ }
1058
+ if (currentTransportMode === "direct" && originDecision) {
1059
+ return {
1060
+ id: "origin",
1061
+ res: Math.min(
1062
+ originDecision.mediaSource.height || 0,
1063
+ originDecision.mediaSource.width || 0,
1064
+ ),
1065
+ auto: false,
1066
+ origin: true,
1067
+ width: originDecision.mediaSource.width || 0,
1068
+ height: originDecision.mediaSource.height || 0,
1069
+ }
1070
+ }
1071
+ const quality = player.qualityLevels()
1072
+ const idx = quality.selectedIndex_
1073
+ if (idx === -1) {
1074
+ const en = quality.levels_.find((level) => level.enabled === true)
1075
+ if (!en) {
1076
+ return undefined
1077
+ }
1078
+ return {
1079
+ id: en.id,
1080
+ res: Math.min(en.height, en.width),
1081
+ auto: autoSwitchResolution,
1082
+ origin: en.label === "origin",
1083
+ width: en.width,
1084
+ height: en.height,
1085
+ }
1086
+ }
1087
+ const res = quality.levels_[idx]
1088
+ if (res) {
1089
+ return {
1090
+ id: res.id,
1091
+ res: Math.min(res.height, res.width),
1092
+ auto: autoSwitchResolution,
1093
+ origin: res.label === "origin",
1094
+ width: res.width,
1095
+ height: res.height,
1096
+ }
1097
+ }
1098
+ const fallback = quality.levels_.find((level) => level.enabled === true)
1099
+ if (!fallback) {
1100
+ return undefined
1101
+ }
1102
+ return {
1103
+ id: fallback.id,
1104
+ res: Math.min(fallback.height, fallback.width),
1105
+ auto: autoSwitchResolution,
1106
+ origin: fallback.label === "origin",
1107
+ width: fallback.width,
1108
+ height: fallback.height,
1109
+ }
1110
+ }
1111
+
1112
+ const baseChangeResolution = player.changeResolution.bind(player)
1113
+
1114
+ const findCurrentQualityLevelIndex = (level: VideoQualityLevel) => {
1115
+ const levels = player.qualityLevels().levels_
1116
+ if (level.label === "origin" || level.id === "origin") {
1117
+ const originIndex = levels.findIndex((item) => item.label === "origin")
1118
+ if (originIndex >= 0) {
1119
+ return originIndex
1120
+ }
1121
+ }
1122
+
1123
+ const exactMatchIndex = levels.findIndex(
1124
+ (l: VideoQualityLevel) =>
1125
+ l.label === level.label &&
1126
+ l.height == level.height &&
1127
+ l.width == level.width &&
1128
+ l.bitrate == level.bitrate,
1129
+ )
1130
+ if (exactMatchIndex >= 0) {
1131
+ return exactMatchIndex
1132
+ }
1133
+
1134
+ const shapeMatchIndex = levels.findIndex(
1135
+ (l: VideoQualityLevel) =>
1136
+ l.label === level.label &&
1137
+ l.height == level.height &&
1138
+ l.width == level.width,
1139
+ )
1140
+ if (shapeMatchIndex >= 0) {
1141
+ return shapeMatchIndex
1142
+ }
1143
+
1144
+ const bitrateMatchIndex = levels.findIndex(
1145
+ (l: VideoQualityLevel) =>
1146
+ l.height == level.height &&
1147
+ l.width == level.width &&
1148
+ l.bitrate == level.bitrate,
1149
+ )
1150
+ if (bitrateMatchIndex >= 0) {
1151
+ return bitrateMatchIndex
1152
+ }
1153
+
1154
+ return levels.findIndex((l: VideoQualityLevel) => l.id == level.id)
1155
+ }
1156
+
1157
+ if (autoSwitchResolution == false) {
1158
+ qualityLevels.on("addqualitylevel", (event: any) => {
1159
+ const level = event.qualityLevel
1160
+ if (Math.min(level.height, level.width) > 720) {
1161
+ level.enabled = false
1162
+ } else {
1163
+ level.enabled = true
1164
+ }
1165
+
1166
+ const selectedIndex = qualityLevels.levels_.findIndex(
1167
+ (l) => l.enabled === true,
1168
+ )
1169
+ qualityLevels.selectedIndex_ = selectedIndex
1170
+ if (selectedIndex !== -1) {
1171
+ const selectedLevel = qualityLevels.levels_[selectedIndex]
1172
+ if (selectedLevel) {
1173
+ baseChangeResolution(selectedLevel)
1174
+ }
1175
+ }
1176
+ refreshLogicalResolutionLevels()
1177
+ })
1178
+ }
1179
+ qualityLevels.on("change", () => {
1180
+ refreshLogicalResolutionLevels()
1181
+ })
1182
+ player.supportResolution = () => {
1183
+ if (isDirectPlayerMode) {
1184
+ return []
1185
+ }
1186
+ refreshLogicalResolutionLevels()
1187
+ let arr: VideoQualityLevel[] = autoSwitchResolution
1188
+ ? [
1189
+ ...logicalResolutionLevels,
1190
+ { id: "auto", label: "auto", height: 0, width: 0, bitrate: 0 },
1191
+ ]
1192
+ : [...logicalResolutionLevels]
1193
+ arr.sort((a, b) => {
1194
+ if (b.height == a.height) {
1195
+ return b.bitrate - a.bitrate
1196
+ }
1197
+ return b.height - a.height
1198
+ })
1199
+ if (arr.length === 0) {
1200
+ arr = [
1201
+ {
1202
+ id: "auto",
1203
+ label: "auto",
1204
+ height: 0,
1205
+ width: 0,
1206
+ bitrate: 0,
1207
+ },
1208
+ ]
1209
+ }
1210
+ return arr
1211
+ }
1212
+
1213
+ player.changeResolution = function (level: VideoQualityLevel) {
1214
+ if (isDirectPlayerMode) {
1215
+ return
1216
+ }
1217
+ if (level.id == "auto") {
1218
+ autoSwitchResolution = true
1219
+ baseChangeResolution(level)
1220
+ return
1221
+ } else if (level.label === "origin" || level.id === "origin") {
1222
+ autoSwitchResolution = false
1223
+ const originSrc = getOriginPreferredSource()
1224
+ if (originSrc && originDecision?.originMode === "direct") {
1225
+ setResolutionDisplayLock({
1226
+ id: "origin",
1227
+ res: Math.min(level.height, level.width),
1228
+ auto: false,
1229
+ origin: true,
1230
+ width: level.width,
1231
+ height: level.height,
1232
+ })
1233
+ reloadSourceWithSeek(originSrc)
1234
+ return
1235
+ }
1236
+
1237
+ const originIndex = findCurrentQualityLevelIndex(level)
1238
+ if (originIndex >= 0) {
1239
+ autoEnableAndDisableResolution(originIndex, { trigger: false })
1240
+ baseChangeResolution(qualityLevels.levels_[originIndex] || level)
1241
+ releaseResolutionDisplayLock()
1242
+ return
1243
+ }
1244
+
1245
+ if (originSrc) {
1246
+ setResolutionDisplayLock({
1247
+ id: "origin",
1248
+ res: Math.min(level.height, level.width),
1249
+ auto: false,
1250
+ origin: true,
1251
+ width: level.width,
1252
+ height: level.height,
1253
+ })
1254
+ reloadSourceWithSeekAndResolution(originSrc, level)
1255
+ return
1256
+ }
1257
+ } else {
1258
+ autoSwitchResolution = false
1259
+ const index = findCurrentQualityLevelIndex(level)
1260
+ if (currentTransportMode === "direct" && originDecision?.originHlsUrl) {
1261
+ setResolutionDisplayLock({
1262
+ id: level.id,
1263
+ res: Math.min(level.height, level.width),
1264
+ auto: false,
1265
+ origin: false,
1266
+ width: level.width,
1267
+ height: level.height,
1268
+ })
1269
+ reloadSourceWithSeekAndResolution(originDecision.originHlsUrl, level)
1270
+ return
1271
+ }
1272
+ autoEnableAndDisableResolution(index, { trigger: false })
1273
+ baseChangeResolution(level)
1274
+ }
1275
+ }
1276
+
1277
+ player.on("error", () => {
1278
+ if (isDirectPlayerMode) {
1279
+ return
1280
+ }
1281
+ const currentResolution = player.currentResolution()
1282
+ if (
1283
+ currentTransportMode === "direct" &&
1284
+ currentResolution?.origin &&
1285
+ originDecision?.originHlsUrl &&
1286
+ !directOriginFallbackPending
1287
+ ) {
1288
+ directOriginFallbackPending = true
1289
+ forceOriginFallbackToHLS()
1290
+ reloadSourceWithSeekAndResolution(originDecision.originHlsUrl, {
1291
+ id: "origin",
1292
+ label: "origin",
1293
+ height: originDecision.mediaSource.height || 0,
1294
+ width: originDecision.mediaSource.width || 0,
1295
+ bitrate: originDecision.mediaSource.bitrate || 0,
1296
+ })
1297
+ setTimeout(() => {
1298
+ directOriginFallbackPending = false
1299
+ }, 0)
1300
+ }
1301
+ })
1302
+ // player 加载视频唯一的入口
1303
+ player.on("openVideo", async (e: any) => {
1304
+ openVideoSeq += 1
1305
+ const seq = openVideoSeq
1306
+ invalidateLoadedMetadataSeek()
1307
+ let info = e.info as VideoInfo
1308
+
1309
+ subtitleHidden = false
1310
+ lastVisibleSubtitle = undefined
1311
+ defaultAvaliableSub.value = undefined
1312
+ netdiskPath = undefined
1313
+ resolvedPlayableSource = undefined
1314
+ subtitleSourceOverride = undefined
1315
+ originDecision = undefined
1316
+ currentTransportMode = "direct"
1317
+ logicalResolutionLevels = []
1318
+ manifestResolutionLevels = []
1319
+ directOriginFallbackPending = false
1320
+ player.getAvaliableSubtitles = () => []
1321
+ player.currentSubtitle = () => undefined
1322
+ setSubtitleLoading(false)
1323
+ releaseResolutionDisplayLock()
1324
+
1325
+ if (info.requestHistory) {
1326
+ await store.ready
1327
+ const result = store.getHistoryInfo(info.sourceUrl)
1328
+ if (result) {
1329
+ info = mergeHistoryInfo(info, result)
1330
+ }
1331
+ }
1332
+ videoDisplayName = info.name
1333
+
1334
+ player.currentVideoInfo = () => {
1335
+ return info
1336
+ }
1337
+
1338
+ if (!player.isCastMode()) {
1339
+ if (isDirectPlayerMode) {
1340
+ info.playMode = "direct"
1341
+ info.originDirectUrl = (info.sourceUrl || "").trim()
1342
+ info.resolvedSourceUrl = info.originDirectUrl
1343
+ } else if (info.fromNetdisk && info.path) {
1344
+ try {
1345
+ originDecision = await fetchOriginPlaybackDecision(
1346
+ info,
1347
+ player.options_.mediaPrefix,
1348
+ )
1349
+ if (originDecision) {
1350
+ info.playMode = originDecision.originMode
1351
+ info.resolvedSourceUrl =
1352
+ originDecision.originMode === "direct"
1353
+ ? originDecision.originDirectUrl
1354
+ : originDecision.originHlsUrl
1355
+ info.originDirectUrl = originDecision.originDirectUrl
1356
+ info.originHlsUrl = originDecision.originHlsUrl
1357
+ info.subtitleInfoUrl = originDecision.subtitleInfoUrl
1358
+ try {
1359
+ const manifestLevels = await fetchOriginHlsLevels(
1360
+ originDecision.originHlsUrl,
1361
+ )
1362
+ if (seq === openVideoSeq) {
1363
+ syncManifestResolutionLevels(manifestLevels)
1364
+ }
1365
+ } catch (err) {
1366
+ console.error("Failed to load origin hls levels", err)
1367
+ }
1368
+ }
1369
+ } catch (err) {
1370
+ console.error("Failed to resolve origin playback decision", err)
1371
+ }
1372
+ }
1373
+ init(info)
1374
+ refreshLogicalResolutionLevels()
1375
+
1376
+ player.isNetdiskSource = () => !!info.fromNetdisk
1377
+
1378
+ if (info.duration - info.currentTime >= 3) {
1379
+ player.one("canplay", function () {
1380
+ if (seq !== openVideoSeq) return
1381
+ player.currentTime(info.currentTime)
1382
+ if (!isDirectPlayerMode) {
1383
+ initSubtitles()
1384
+ }
1385
+ player.currentVideoInfo = () => {
1386
+ return {
1387
+ ...info,
1388
+ duration: player.duration() ?? info.duration,
1389
+ currentTime: player.currentTime() ?? info.currentTime,
1390
+ name: player.currentVideoName() ?? info.name,
1391
+ }
1392
+ }
1393
+ })
1394
+ } else {
1395
+ player.one("canplay", () => {
1396
+ if (seq !== openVideoSeq) return
1397
+ if (!isDirectPlayerMode) {
1398
+ initSubtitles()
1399
+ }
1400
+ player.currentVideoInfo = () => {
1401
+ return {
1402
+ ...info,
1403
+ duration: player.duration() ?? info.duration,
1404
+ currentTime: player.currentTime() ?? info.currentTime,
1405
+ name: player.currentVideoName() ?? info.name,
1406
+ }
1407
+ }
1408
+ })
1409
+ }
1410
+ }
1411
+ })
1412
+
1413
+ player.currentVideoName = videoName
1414
+ player.currentPreview = currentPreview
1415
+ player.isCastMode = function () {
1416
+ return player.currentType() === "video/lzc-cast"
1417
+ }
1418
+ player.reloadSource = function (index?: number) {
1419
+ const seekTo = (player.duration() !== Infinity && player.currentTime()) || 0
1420
+ const expectedInfo = player.currentVideoInfo?.()
1421
+ scheduleLoadedMetadataSeek(seekTo, expectedInfo)
1422
+ restorePlaybackRateAfterSourceReload()
1423
+ player.one("loadedmetadata", function () {
1424
+ if (index != undefined) {
1425
+ autoEnableAndDisableResolution(index)
1426
+ }
1427
+ })
1428
+ player.src(player.currentSource())
1429
+ safePlay()
1430
+ }
1431
+ }