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,913 @@
1
+ import Hls from "hls.js"
2
+ import base from "@lazycatcloud/sdk/dist/extentions/base"
3
+ import type { VideoInfo, VideoQualityLevel, VideoResolution } from "@/model"
4
+ import type { Subtitle } from "@/model"
5
+ import { EventEmitter } from "./EventEmitter"
6
+ import type { LzcPlayer } from "../player"
7
+ import { controller as webview } from "../useWebview"
8
+ import {
9
+ doContinueAction,
10
+ doPauseAction,
11
+ } from "../components/LzcOverlay/useCast"
12
+
13
+ const IOS_FAKE_FULLSCREEN_TRANSITION_MS = 260
14
+
15
+ export function isIos() {
16
+ return (
17
+ /iphone|ipad|ipod/.test(window.navigator.userAgent.toLowerCase()) ||
18
+ !!navigator.userAgent.match(/Mac OS X\s+[^\s)]+.*?Mobile/)
19
+ )
20
+ }
21
+ /** Stub compatible with videojs-contrib-quality-levels. */
22
+ export function createStubQualityLevels() {
23
+ const listeners: Record<string, ((e: any) => void)[]> = {}
24
+ return {
25
+ levels_: [] as Array<{
26
+ id: string
27
+ label: string
28
+ height: number
29
+ width: number
30
+ bitrate: number
31
+ enabled: boolean
32
+ }>,
33
+ selectedIndex_: -1,
34
+ on(event: string, fn: (e: any) => void) {
35
+ if (!listeners[event]) listeners[event] = []
36
+ listeners[event].push(fn)
37
+ },
38
+ trigger(e: any) {
39
+ const name = (e && e.type) || "change"
40
+ ;(listeners[name] || []).forEach((fn) => fn(e))
41
+ },
42
+ }
43
+ }
44
+
45
+ export type StubQualityLevels = ReturnType<typeof createStubQualityLevels>
46
+
47
+ export interface NativePlayerOptions {
48
+ mediaPrefix?: string
49
+ showTopBar?: boolean
50
+ playMode?: "standard" | "direct"
51
+ }
52
+
53
+ /** Public API type consumed by provide/inject and composables. */
54
+ export interface NativePlayerAPI extends LzcPlayer {
55
+ options_: NativePlayerOptions & { mediaPrefix: string }
56
+ setContainer(el: HTMLElement | null): void
57
+ _src: (
58
+ sources?:
59
+ | string
60
+ | { src: string; type?: string }
61
+ | Array<{ src: string; type?: string }>,
62
+ ) => string | void
63
+ qualityLevels(): StubQualityLevels
64
+ currentVideoName(): string
65
+ }
66
+
67
+ export class NativePlayer implements NativePlayerAPI, LzcPlayer {
68
+ private video: HTMLVideoElement
69
+ private events = new EventEmitter()
70
+ private hls: Hls | null = null
71
+ private _disposed = false
72
+ private _currentVideoInfo: VideoInfo | null = null
73
+ private _videoDisplayName = ""
74
+ private _fullscreenTarget: HTMLElement | null = null
75
+ private _container: HTMLElement | null = null
76
+ private _prevEnabled = false
77
+ private _nextEnabled = false
78
+ private _castMode = false
79
+ private _castSrc = ""
80
+ private _castPaused = false
81
+ private _scrubbing = false
82
+ private _durationOverride: number | null = null
83
+ private _currentTimeOverride: number | null = null
84
+ private _pendingHlsLevel: VideoQualityLevel | null = null
85
+ private _poster = ""
86
+ readonly options_: NativePlayerOptions & { mediaPrefix: string }
87
+ private _qualityLevels = createStubQualityLevels()
88
+ private _iosFakeFullscreen = false
89
+ private _iosFakeFullscreenCleanupTimer: ReturnType<typeof setTimeout> | null =
90
+ null
91
+ private _onViewportChange = () => {
92
+ if (!this._iosFakeFullscreen) return
93
+ this._syncIosFakeFullscreenLayout()
94
+ }
95
+
96
+ /** useSource may replace this with src2; keep original setSrc internally. */
97
+ _src: (
98
+ sources?:
99
+ | string
100
+ | { src: string; type?: string }
101
+ | Array<{ src: string; type?: string }>,
102
+ ) => string | void = () => ""
103
+
104
+ /** Optional callbacks for previous/next episode. */
105
+ playPrev?: () => void
106
+ playNext?: () => void
107
+
108
+ /** Optional modal bridge injected by player component. */
109
+ lzcModal?: () => { openModal: (page: string) => void; close: () => void }
110
+
111
+ /** Called by useSource via player.qualityLevels(). */
112
+ qualityLevels(): StubQualityLevels {
113
+ return this._qualityLevels
114
+ }
115
+
116
+ /** Exposed for controls: whether previous is available. */
117
+ get prevEnabled(): boolean {
118
+ return this._prevEnabled
119
+ }
120
+
121
+ /** Exposed for controls: whether next is available. */
122
+ get nextEnabled(): boolean {
123
+ return this._nextEnabled
124
+ }
125
+
126
+ private _onDocumentFullscreenChange = () => {
127
+ this.events.trigger("fullscreenchange")
128
+ }
129
+
130
+ constructor(videoEl: HTMLVideoElement, options: NativePlayerOptions = {}) {
131
+ this.video = videoEl
132
+ this.options_ = {
133
+ mediaPrefix: options.mediaPrefix ?? "/_lzc/media",
134
+ showTopBar: options.showTopBar !== false,
135
+ playMode: options.playMode ?? "standard",
136
+ }
137
+ this._src = this.setSrc.bind(this)
138
+ this._bindVideoEvents()
139
+ document.addEventListener(
140
+ "fullscreenchange",
141
+ this._onDocumentFullscreenChange,
142
+ )
143
+ this.on("changeBrightness", (e: any) => {
144
+ if (webview.IsApp()) {
145
+ webview.SetScreentBrightness(e?.brightness ?? 1)
146
+ } else {
147
+ console.log("adjust brightness only supported in app")
148
+ }
149
+ })
150
+ this.on("CastPause", () => {
151
+ if (this._castMode) {
152
+ this._castPaused = true
153
+ }
154
+ })
155
+ this.on("CastPlay", () => {
156
+ if (this._castMode) {
157
+ this._castPaused = false
158
+ }
159
+ })
160
+ }
161
+
162
+ private _bindVideoEvents() {
163
+ const evNames = [
164
+ "loadstart",
165
+ "loadedmetadata",
166
+ "loadeddata",
167
+ "canplay",
168
+ "canplaythrough",
169
+ "playing",
170
+ "play",
171
+ "pause",
172
+ "ended",
173
+ "seeking",
174
+ "seeked",
175
+ "timeupdate",
176
+ "volumechange",
177
+ "error",
178
+ "waiting",
179
+ ] as const
180
+ evNames.forEach((name) => {
181
+ this.video.addEventListener(name, (e) => this.events.trigger(name, e))
182
+ })
183
+ }
184
+
185
+ private _isHlsUrl(url: string): boolean {
186
+ return (
187
+ /\.m3u8(\?|$)/i.test(url) ||
188
+ /application\/x-mpegURL|application\/vnd\.apple\.mpegurl/i.test(url)
189
+ )
190
+ }
191
+
192
+ private setSrc(
193
+ sources?:
194
+ | string
195
+ | { src: string; type?: string }
196
+ | Array<{ src: string; type?: string }>,
197
+ ): string | void {
198
+ if (sources === undefined || sources === null) {
199
+ return this.currentSrc()
200
+ }
201
+ let src: string
202
+ let type = ""
203
+ if (typeof sources === "string") {
204
+ src = sources
205
+ } else if (Array.isArray(sources)) {
206
+ if (sources.length === 0) return
207
+ src = sources[0].src
208
+ type = sources[0].type ?? ""
209
+ } else {
210
+ src = sources.src
211
+ type = sources.type ?? ""
212
+ }
213
+ if (type === "video/lzc-cast") {
214
+ this._castMode = true
215
+ this._castSrc = src
216
+ this._castPaused = false
217
+ this._currentTimeOverride = 0
218
+ this._durationOverride = null
219
+ return undefined
220
+ }
221
+ if (this._castMode) {
222
+ this._castMode = false
223
+ this._castPaused = false
224
+ }
225
+ this._loadSource(src, type)
226
+ return undefined
227
+ }
228
+
229
+ private _loadSource(url: string, type: string) {
230
+ this._destroyHls()
231
+ this.resetQualityLevels()
232
+ const isHls = this._isHlsUrl(url) || type === "application/x-mpegURL"
233
+ if (!isHls) {
234
+ this._pendingHlsLevel = null
235
+ this.video.src = url
236
+ this.video.load()
237
+ }
238
+ if (isHls && !Hls.isSupported()) {
239
+ this._pendingHlsLevel = null
240
+ this.video.src = url
241
+ this.video.load()
242
+ }
243
+ if (isHls && Hls.isSupported()) {
244
+ this.hls = new Hls({ enableWorker: true })
245
+ this.hls.loadSource(url)
246
+ this.hls.attachMedia(this.video)
247
+ this.hls.on(Hls.Events.MANIFEST_PARSED, (_e, data) => {
248
+ this._qualityLevels.levels_ = (data.levels || []).map(
249
+ (lev: any, i: number) => {
250
+ const name = `${lev?.name || ""}`.trim()
251
+ const height = lev.height || 0
252
+ const label =
253
+ name === "origin"
254
+ ? "origin"
255
+ : name
256
+ ? /p$/i.test(name)
257
+ ? name
258
+ : `${name}P`
259
+ : height > 0
260
+ ? `${height}P`
261
+ : ""
262
+ return {
263
+ id: String(i),
264
+ label,
265
+ height,
266
+ width: lev.width || 0,
267
+ bitrate: lev.bitrate || 0,
268
+ enabled: true,
269
+ }
270
+ },
271
+ )
272
+ this._qualityLevels.levels_.forEach((level) => {
273
+ this._qualityLevels.trigger({
274
+ type: "addqualitylevel",
275
+ qualityLevel: level,
276
+ })
277
+ })
278
+ const pendingLevel = this._pendingHlsLevel
279
+ this._pendingHlsLevel = null
280
+ const pendingIndex = pendingLevel
281
+ ? this.findQualityLevelIndex(pendingLevel)
282
+ : -1
283
+ const currentIndex = this._qualityLevels.selectedIndex_
284
+ const selectedIndex =
285
+ pendingIndex >= 0 &&
286
+ pendingIndex < this._qualityLevels.levels_.length
287
+ ? pendingIndex
288
+ : currentIndex >= 0 && currentIndex < this._qualityLevels.levels_.length
289
+ ? currentIndex
290
+ : 0
291
+ this.applyHlsLevelIndex(selectedIndex, { trigger: true })
292
+ })
293
+ }
294
+ }
295
+
296
+ private applyHlsLevelIndex(index: number, options: { trigger?: boolean } = {}) {
297
+ if (!this.hls || index < 0 || index >= this._qualityLevels.levels_.length) {
298
+ return false
299
+ }
300
+ this.hls.currentLevel = index
301
+ this._qualityLevels.selectedIndex_ = index
302
+ this._qualityLevels.levels_.forEach((level, levelIndex) => {
303
+ level.enabled = levelIndex === index
304
+ })
305
+ if (options.trigger !== false) {
306
+ this._qualityLevels.trigger({ type: "change", selectedIndex: index })
307
+ }
308
+ return true
309
+ }
310
+
311
+ private findQualityLevelIndex(level: VideoQualityLevel) {
312
+ if (level.label === "origin" || level.id === "origin") {
313
+ const originIndex = this._qualityLevels.levels_.findIndex(
314
+ (item) => item.label === "origin",
315
+ )
316
+ if (originIndex >= 0) {
317
+ return originIndex
318
+ }
319
+ }
320
+
321
+ const idIndex = parseInt(level.id, 10)
322
+ if (
323
+ !Number.isNaN(idIndex) &&
324
+ idIndex >= 0 &&
325
+ idIndex < this._qualityLevels.levels_.length
326
+ ) {
327
+ return idIndex
328
+ }
329
+
330
+ return this._qualityLevels.levels_.findIndex(
331
+ (item) =>
332
+ item.id === level.id &&
333
+ item.height === level.height &&
334
+ item.bitrate === level.bitrate,
335
+ )
336
+ }
337
+
338
+ private resetQualityLevels(options: { trigger?: boolean } = {}) {
339
+ this._qualityLevels.levels_ = []
340
+ this._qualityLevels.selectedIndex_ = -1
341
+ if (options.trigger !== false) {
342
+ this._qualityLevels.trigger({ type: "change", selectedIndex: -1 })
343
+ }
344
+ }
345
+
346
+ private _destroyHls() {
347
+ if (this.hls) {
348
+ this.hls.destroy()
349
+ this.hls = null
350
+ }
351
+ }
352
+
353
+ play() {
354
+ if (this._castMode) {
355
+ this._castPaused = false
356
+ return doContinueAction().then(() => {
357
+ this.trigger("play")
358
+ })
359
+ }
360
+ return this.video.play()
361
+ }
362
+
363
+ pause() {
364
+ if (this._castMode) {
365
+ this._castPaused = true
366
+ void doPauseAction().then(() => {
367
+ this.trigger("pause")
368
+ })
369
+ return
370
+ }
371
+ this.video.pause()
372
+ }
373
+
374
+ paused(): boolean {
375
+ if (this._castMode) {
376
+ return this._castPaused
377
+ }
378
+ return this.video.paused
379
+ }
380
+
381
+ currentTime(): number
382
+ currentTime(seconds: number): void
383
+ currentTime(seconds?: number): number | void {
384
+ if (seconds === undefined) {
385
+ if (this._castMode && this._currentTimeOverride != null) {
386
+ return this._currentTimeOverride
387
+ }
388
+ return this.video.currentTime
389
+ }
390
+ if (this._castMode) {
391
+ this._currentTimeOverride = seconds
392
+ return
393
+ }
394
+ this.video.currentTime = seconds
395
+ }
396
+
397
+ duration(): number
398
+ duration(seconds: number): void
399
+ duration(seconds?: number): number | void {
400
+ if (seconds !== undefined) {
401
+ this._durationOverride = seconds
402
+ return
403
+ }
404
+ if (this._castMode && this._durationOverride != null) {
405
+ return this._durationOverride
406
+ }
407
+ return this.video.duration ?? 0
408
+ }
409
+
410
+ volume(): number
411
+ volume(percent: number): void
412
+ volume(percent?: number): number | Promise<number> | void {
413
+ if (webview.IsApp()) {
414
+ if (typeof percent === "number") {
415
+ webview.SetVoice(percent)
416
+ return
417
+ }
418
+ return webview.GetCurrentVolume()
419
+ }
420
+ if (percent === undefined) return this.video.volume
421
+ this.video.volume = Math.max(0, Math.min(1, percent))
422
+ }
423
+
424
+ muted(): boolean
425
+ muted(muted: boolean): void
426
+ muted(muted?: boolean): boolean | void {
427
+ if (muted === undefined) return this.video.muted
428
+ this.video.muted = muted
429
+ }
430
+
431
+ currentSrc(): string {
432
+ if (this._castMode && this._castSrc) return this._castSrc
433
+ return this.video.currentSrc || this.video.src || ""
434
+ }
435
+
436
+ src(
437
+ sources?:
438
+ | string
439
+ | { src: string; type?: string }
440
+ | Array<{ src: string; type?: string }>,
441
+ ): string | void {
442
+ return this._src(sources)
443
+ }
444
+
445
+ poster(url?: string): string | void {
446
+ if (url === undefined) return this._poster
447
+ this._poster = url
448
+ }
449
+
450
+ on(event: string, fn: (e?: any) => void) {
451
+ this.events.on(event, fn)
452
+ }
453
+
454
+ one(event: string, fn: (e?: any) => void) {
455
+ this.events.one(event, fn)
456
+ }
457
+
458
+ off(event: string, fn?: (e?: any) => void) {
459
+ this.events.off(event, fn)
460
+ }
461
+
462
+ ready(fn?: () => void) {
463
+ if (typeof fn !== "function") return
464
+ Promise.resolve().then(() => {
465
+ if (!this._disposed) fn()
466
+ })
467
+ }
468
+
469
+ trigger(
470
+ eventOrName: string | { type: string; [key: string]: unknown },
471
+ data?: unknown,
472
+ ) {
473
+ this.events.trigger(eventOrName as any, data)
474
+ }
475
+
476
+ dimensions(_w: number, _h: number) {
477
+ // no-op for minimal
478
+ }
479
+
480
+ id(): string {
481
+ return this.video.id || "native-player"
482
+ }
483
+
484
+ isDisposed(): boolean {
485
+ return this._disposed
486
+ }
487
+
488
+ dispose() {
489
+ if (this._disposed) return
490
+ this._disposed = true
491
+ this._setIosFakeFullscreen(false, { immediate: true })
492
+ document.removeEventListener(
493
+ "fullscreenchange",
494
+ this._onDocumentFullscreenChange,
495
+ )
496
+ this._destroyHls()
497
+ this.events.dispose()
498
+ this.video.src = ""
499
+ this.trigger("dispose")
500
+ }
501
+
502
+ /** Patched by useSource. */
503
+ currentVideoInfo(): VideoInfo | null {
504
+ return this._currentVideoInfo
505
+ }
506
+
507
+ currentVideoName(): string {
508
+ return this._videoDisplayName || this.currentSrc().split("/").pop() || ""
509
+ }
510
+
511
+ currentPreview(): undefined {
512
+ return undefined
513
+ }
514
+
515
+ getAvaliableSubtitles(): Subtitle[] | undefined {
516
+ return []
517
+ }
518
+
519
+ isSubtitleLoading(): boolean {
520
+ return false
521
+ }
522
+
523
+ currentSubtitle(): Subtitle | undefined {
524
+ return undefined
525
+ }
526
+
527
+ changeSubtitle(_sub: Subtitle | number) {}
528
+ clearSubtitle() {}
529
+ toggleSubtitleVisibility() {}
530
+ isSubtitleHidden(): boolean {
531
+ return false
532
+ }
533
+ setSubtitleHidden(_v: boolean) {}
534
+
535
+ isCastMode(): boolean {
536
+ return this._castMode
537
+ }
538
+
539
+ currentType(): string {
540
+ return this._castMode ? "video/lzc-cast" : "video/html5"
541
+ }
542
+
543
+ isNetdiskSource(): boolean {
544
+ return false
545
+ }
546
+
547
+ reloadSource(_index?: number) {}
548
+
549
+ supportResolution(): VideoQualityLevel[] {
550
+ return this._qualityLevels.levels_.length
551
+ ? [...this._qualityLevels.levels_]
552
+ : [{ id: "auto", label: "auto", height: 0, width: 0, bitrate: 0 }]
553
+ }
554
+
555
+ currentResolution(): VideoResolution | undefined {
556
+ const q = this._qualityLevels
557
+ if (q.selectedIndex_ < 0 || !q.levels_[q.selectedIndex_]) return undefined
558
+ const lev = q.levels_[q.selectedIndex_]
559
+ return {
560
+ id: lev.id,
561
+ res: Math.min(lev.height, lev.width),
562
+ width: lev.width,
563
+ height: lev.height,
564
+ auto: false,
565
+ origin: lev.label === "origin",
566
+ }
567
+ }
568
+
569
+ changeResolution(level: VideoQualityLevel) {
570
+ if (level.id === "auto") {
571
+ if (this.hls) {
572
+ this.hls.currentLevel = -1
573
+ this._qualityLevels.selectedIndex_ = -1
574
+ this._pendingHlsLevel = null
575
+ this._qualityLevels.levels_.forEach((item) => {
576
+ item.enabled = true
577
+ })
578
+ this._qualityLevels.trigger({ type: "change", selectedIndex: -1 })
579
+ }
580
+ return
581
+ }
582
+ const idx = this.findQualityLevelIndex(level)
583
+ if (idx < 0) {
584
+ if (!this.hls || this._qualityLevels.levels_.length === 0) {
585
+ this._pendingHlsLevel = { ...level }
586
+ }
587
+ return
588
+ }
589
+ if (!this.applyHlsLevelIndex(idx, { trigger: true })) {
590
+ this._pendingHlsLevel = { ...level }
591
+ }
592
+ }
593
+
594
+ enablePrev(v: boolean) {
595
+ this._prevEnabled = v
596
+ this.trigger("prevNextChange")
597
+ }
598
+
599
+ enableNext(v: boolean) {
600
+ this._nextEnabled = v
601
+ this.trigger("prevNextChange")
602
+ }
603
+
604
+ playbackRate(): number
605
+ playbackRate(rate: number): void
606
+ playbackRate(rate?: number): number | void {
607
+ if (rate === undefined) return this.video.playbackRate
608
+ this.video.playbackRate = rate
609
+ this.trigger("ratechange")
610
+ }
611
+
612
+ private _playbackRates = [0.75, 1, 1.25, 1.5, 2, 3]
613
+ playbackRates(rates?: number[]): number[] {
614
+ if (rates !== undefined) this._playbackRates = rates
615
+ return this._playbackRates
616
+ }
617
+
618
+ setFullscreenTarget(el: HTMLElement | null) {
619
+ this._fullscreenTarget = el
620
+ }
621
+
622
+ setContainer(el: HTMLElement | null) {
623
+ this._container = el
624
+ }
625
+
626
+ private _fullscreenElement(): HTMLElement {
627
+ return this._container || this._fullscreenTarget || this.video
628
+ }
629
+
630
+ private _readViewportSize(): { width: number; height: number } {
631
+ if (typeof window === "undefined") {
632
+ return { width: 0, height: 0 }
633
+ }
634
+ const vv = window.visualViewport
635
+ const width =
636
+ vv?.width ?? window.innerWidth ?? document.documentElement.clientWidth
637
+ const height =
638
+ vv?.height ?? window.innerHeight ?? document.documentElement.clientHeight
639
+ return {
640
+ width: Number.isFinite(width) ? width : 0,
641
+ height: Number.isFinite(height) ? height : 0,
642
+ }
643
+ }
644
+
645
+ private _syncIosFakeFullscreenLayout() {
646
+ const el = this._fullscreenElement()
647
+ const { width: viewportWidth, height: viewportHeight } =
648
+ this._readViewportSize()
649
+ const portrait = viewportHeight >= viewportWidth
650
+ const layoutWidth = portrait ? viewportHeight : viewportWidth
651
+ const layoutHeight = portrait ? viewportWidth : viewportHeight
652
+ const rotate = portrait ? 90 : 0
653
+
654
+ el.classList.add("vjs-ios-fake-fullscreen")
655
+ el.classList.remove("vjs-ios-fake-fullscreen-exiting")
656
+ el.classList.toggle("vjs-ios-fake-fullscreen-portrait", portrait)
657
+ el.classList.toggle("vjs-ios-fake-fullscreen-landscape", !portrait)
658
+ el.style.setProperty("--lzc-ios-fs-width", `${Math.round(layoutWidth)}px`)
659
+ el.style.setProperty("--lzc-ios-fs-height", `${Math.round(layoutHeight)}px`)
660
+ el.style.setProperty("--lzc-ios-fs-rotate", `${rotate}deg`)
661
+ }
662
+
663
+ private _clearIosFakeFullscreenLayout() {
664
+ const el = this._fullscreenElement()
665
+ el.classList.remove(
666
+ "vjs-ios-fake-fullscreen",
667
+ "vjs-ios-fake-fullscreen-portrait",
668
+ "vjs-ios-fake-fullscreen-landscape",
669
+ "vjs-ios-fake-fullscreen-exiting",
670
+ )
671
+ el.style.removeProperty("--lzc-ios-fs-width")
672
+ el.style.removeProperty("--lzc-ios-fs-height")
673
+ el.style.removeProperty("--lzc-ios-fs-rotate")
674
+ }
675
+
676
+ private _clearIosFakeFullscreenCleanupTimer() {
677
+ if (!this._iosFakeFullscreenCleanupTimer) return
678
+ clearTimeout(this._iosFakeFullscreenCleanupTimer)
679
+ this._iosFakeFullscreenCleanupTimer = null
680
+ }
681
+
682
+ private _prefersReducedMotion(): boolean {
683
+ if (
684
+ typeof window === "undefined" ||
685
+ typeof window.matchMedia !== "function"
686
+ ) {
687
+ return false
688
+ }
689
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches
690
+ }
691
+
692
+ private _animateIosFakeFullscreenExit() {
693
+ const el = this._fullscreenElement()
694
+ const { width, height } = this._readViewportSize()
695
+
696
+ el.classList.add("vjs-ios-fake-fullscreen")
697
+ el.classList.add("vjs-ios-fake-fullscreen-exiting")
698
+ el.classList.remove(
699
+ "vjs-ios-fake-fullscreen-portrait",
700
+ "vjs-ios-fake-fullscreen-landscape",
701
+ )
702
+
703
+ const applyFinal = () => {
704
+ el.style.setProperty("--lzc-ios-fs-width", `${Math.round(width)}px`)
705
+ el.style.setProperty("--lzc-ios-fs-height", `${Math.round(height)}px`)
706
+ el.style.setProperty("--lzc-ios-fs-rotate", "0deg")
707
+ }
708
+
709
+ if (
710
+ typeof window !== "undefined" &&
711
+ typeof window.requestAnimationFrame === "function"
712
+ ) {
713
+ window.requestAnimationFrame(applyFinal)
714
+ } else {
715
+ applyFinal()
716
+ }
717
+
718
+ const duration = this._prefersReducedMotion()
719
+ ? 0
720
+ : IOS_FAKE_FULLSCREEN_TRANSITION_MS
721
+ this._iosFakeFullscreenCleanupTimer = setTimeout(() => {
722
+ this._iosFakeFullscreenCleanupTimer = null
723
+ this._clearIosFakeFullscreenLayout()
724
+ }, duration)
725
+ }
726
+
727
+ private _bindIosFakeFullscreenListeners() {
728
+ if (typeof window === "undefined") return
729
+ window.addEventListener("resize", this._onViewportChange)
730
+ window.addEventListener("orientationchange", this._onViewportChange)
731
+ const vv = window.visualViewport
732
+ if (vv) {
733
+ vv.addEventListener("resize", this._onViewportChange)
734
+ }
735
+ }
736
+
737
+ private _unbindIosFakeFullscreenListeners() {
738
+ if (typeof window === "undefined") return
739
+ window.removeEventListener("resize", this._onViewportChange)
740
+ window.removeEventListener("orientationchange", this._onViewportChange)
741
+ const vv = window.visualViewport
742
+ if (vv) {
743
+ vv.removeEventListener("resize", this._onViewportChange)
744
+ }
745
+ }
746
+
747
+ private _setIosFakeFullscreen(
748
+ value: boolean,
749
+ options?: { immediate?: boolean },
750
+ ) {
751
+ if (this._iosFakeFullscreen === value) {
752
+ if (value) {
753
+ this._syncIosFakeFullscreenLayout()
754
+ }
755
+ return
756
+ }
757
+ this._clearIosFakeFullscreenCleanupTimer()
758
+ this._iosFakeFullscreen = value
759
+ if (value) {
760
+ this._bindIosFakeFullscreenListeners()
761
+ this._syncIosFakeFullscreenLayout()
762
+ } else {
763
+ this._unbindIosFakeFullscreenListeners()
764
+ if (options?.immediate) {
765
+ this._clearIosFakeFullscreenLayout()
766
+ } else {
767
+ this._animateIosFakeFullscreenExit()
768
+ }
769
+ }
770
+ this.events.trigger("fullscreenchange")
771
+ }
772
+
773
+ private _isIosSafari(): boolean {
774
+ return isIos() || base.isIosWebShell()
775
+ }
776
+
777
+ requestFullscreen() {
778
+ if (this._isIosSafari()) {
779
+ this._setIosFakeFullscreen(true)
780
+ return
781
+ }
782
+ const el = this._fullscreenElement()
783
+ const request =
784
+ (el as any).requestFullscreen ||
785
+ (el as any).webkitRequestFullscreen ||
786
+ (el as any).webkitRequestFullScreen
787
+ if (typeof request === "function") {
788
+ try {
789
+ const result = request.call(el)
790
+ if (result && typeof result.catch === "function") {
791
+ result.catch(() => {})
792
+ }
793
+ return
794
+ } catch {
795
+ return
796
+ }
797
+ }
798
+ }
799
+
800
+ exitFullscreen() {
801
+ if (this._isIosSafari()) {
802
+ this._setIosFakeFullscreen(false)
803
+ return
804
+ }
805
+ if (document.fullscreenElement) {
806
+ document.exitFullscreen()
807
+ }
808
+ }
809
+
810
+ isFullscreen(): boolean {
811
+ if (this._isIosSafari()) {
812
+ return this._iosFakeFullscreen
813
+ }
814
+ const el = this._fullscreenElement()
815
+ return !!document.fullscreenElement && document.fullscreenElement === el
816
+ }
817
+
818
+ isUsingNativeSubtitleFallback(): boolean {
819
+ return false
820
+ }
821
+
822
+ /** Set by useSource hooks. */
823
+ setCurrentVideoInfo(info: VideoInfo | null) {
824
+ this._currentVideoInfo = info
825
+ }
826
+
827
+ setVideoDisplayName(name: string) {
828
+ this._videoDisplayName = name
829
+ }
830
+
831
+ /** Expose internal video element for useSource bindings. */
832
+ $video(): HTMLVideoElement {
833
+ return this.video
834
+ }
835
+
836
+ networkState(): number {
837
+ return this.video.networkState ?? 0
838
+ }
839
+
840
+ currentSource(): { src: string; type?: string } {
841
+ const src = this.currentSrc()
842
+ const isHls = this._isHlsUrl(src)
843
+ if (this._castMode) {
844
+ return { src, type: "video/lzc-cast" }
845
+ }
846
+ return { src, type: isHls ? "application/x-mpegURL" : "" }
847
+ }
848
+
849
+ currentWidth(): number {
850
+ const el = this._fullscreenElement()
851
+ return el.getBoundingClientRect().width
852
+ }
853
+
854
+ currentDimensions(): { width: number; height: number } {
855
+ const el = this._fullscreenElement()
856
+ const rect = el.getBoundingClientRect()
857
+ return { width: rect.width, height: rect.height }
858
+ }
859
+
860
+ addClass(name: string) {
861
+ const el = this._fullscreenElement()
862
+ el.classList.add(name)
863
+ }
864
+
865
+ removeClass(name: string) {
866
+ const el = this._fullscreenElement()
867
+ el.classList.remove(name)
868
+ }
869
+
870
+ hasClass(name: string): boolean {
871
+ const el = this._fullscreenElement()
872
+ return el.classList.contains(name)
873
+ }
874
+
875
+ ended(): boolean {
876
+ if (this._castMode) return false
877
+ return this.video.ended
878
+ }
879
+
880
+ seeking(): boolean {
881
+ if (this._castMode) return false
882
+ return this.video.seeking
883
+ }
884
+
885
+ scrubbing(): boolean {
886
+ return this._scrubbing
887
+ }
888
+
889
+ setScrubbing(value: boolean) {
890
+ this._scrubbing = value
891
+ }
892
+
893
+ bufferedEnd(): number {
894
+ if (this._castMode) {
895
+ return (this.currentTime() as number) || 0
896
+ }
897
+ const ranges = this.video.buffered
898
+ if (!ranges || ranges.length === 0) return 0
899
+ return ranges.end(ranges.length - 1)
900
+ }
901
+
902
+ error(): MediaError | null {
903
+ return this.video.error
904
+ }
905
+
906
+ async currentBrightness(): Promise<number> {
907
+ if (webview.IsApp()) {
908
+ return await webview.GetScreenBrightNess()
909
+ }
910
+ console.log("adjust brightness only supported in app")
911
+ return 1
912
+ }
913
+ }