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,748 @@
1
+ <script lang="ts" setup>
2
+ import {
3
+ ref,
4
+ computed,
5
+ onMounted,
6
+ onUnmounted,
7
+ provide,
8
+ unref,
9
+ watch,
10
+ getCurrentInstance,
11
+ createApp,
12
+ nextTick,
13
+ } from "vue"
14
+ import type { NativePlayerAPI } from "./native/NativePlayer"
15
+ import NativePlayerComponent from "./native/NativePlayer.vue"
16
+ import NativeControls from "./native/NativeControls.vue"
17
+ import NativeModal from "./native/NativeModal.vue"
18
+ import { useSource } from "./useSource"
19
+ import { useHistoryInfo } from "@/stores/playlist"
20
+ import { isMobile } from "@/use/useUtils"
21
+ import { nativePlayerKey } from "./native/playerKey"
22
+ import { useNativePlayerState } from "./native/useNativePlayerState"
23
+ import { useNativePlayerHistory } from "./native/useNativePlayerHistory"
24
+ import { useNativePlayerPlaylist } from "./native/useNativePlayerPlaylist"
25
+ import { useNativePlayerModal } from "./native/useNativePlayerModal"
26
+ import { useNativePlayerFullscreen } from "./native/useNativePlayerFullscreen"
27
+ import { useEventListener } from "@vueuse/core"
28
+ import { useKeyBind } from "@/use/useKeyBind"
29
+ import type { Subtitle } from "@/model"
30
+ import LzcOverlay from "./components/LzcOverlay/index.vue"
31
+ import { useNativeCastMiddleware } from "./native/useNativeCastMiddleware"
32
+ import BackgroundSvg from "@/icons/移动端_背景.webp?inline"
33
+ import "./native/native-player.css"
34
+
35
+ const emit = defineEmits<{
36
+ (e: "back"): void
37
+ (e: "init", player: NativePlayerAPI): void
38
+ (e: "fullscreenchange", value: boolean, id: string): void
39
+ (e: "controlBarActive", show: boolean): void
40
+ }>()
41
+
42
+ const props = withDefaults(
43
+ defineProps<{
44
+ onInit?: (player: NativePlayerAPI) => void
45
+ options?: {
46
+ mediaPrefix?: string
47
+ autoplay?: boolean
48
+ preload?: string
49
+ showTopBar?: boolean
50
+ playMode?: "standard" | "direct"
51
+ hidePrevButton?: boolean
52
+ hideNextButton?: boolean
53
+ hidePlayList?: boolean
54
+ playPrev?: () => void
55
+ playNext?: () => void
56
+ [key: string]: unknown
57
+ }
58
+ poster?: string
59
+ shortcutToggle?: boolean
60
+ containerId?: string
61
+ }>(),
62
+ {
63
+ poster: BackgroundSvg,
64
+ shortcutToggle: true,
65
+ containerId: "native-player",
66
+ },
67
+ )
68
+ const safePlay = (p: NativePlayerAPI, scene: string) => {
69
+ void Promise.resolve(p.play()).catch((err: any) => {
70
+ if (err?.name !== "AbortError") {
71
+ console.error(`${scene} play failed`, err)
72
+ }
73
+ })
74
+ }
75
+
76
+ const isDirectMode = computed(() => props.options?.playMode === "direct")
77
+
78
+ function createDialogFallback(q: any, getContainer?: () => HTMLElement | null) {
79
+ return (options?: {
80
+ component?: any
81
+ componentProps?: Record<string, unknown>
82
+ }) => {
83
+ if (typeof document === "undefined" || !options?.component) {
84
+ return {
85
+ onOk: () => {},
86
+ onCancel: () => {},
87
+ onDismiss: () => {},
88
+ }
89
+ }
90
+ const host = document.createElement("div")
91
+ host.setAttribute("data-no-play-toggle", "")
92
+ const container = getContainer?.() ?? null
93
+ const parent = container ?? document.body
94
+ parent.appendChild(host)
95
+ if (container) {
96
+ host.style.position = "absolute"
97
+ host.style.inset = "0"
98
+ host.style.zIndex = "50"
99
+ host.style.pointerEvents = "auto"
100
+ }
101
+ let app: ReturnType<typeof createApp> | null = null
102
+ const cleanup = () => {
103
+ if (app) {
104
+ app.unmount()
105
+ app = null
106
+ }
107
+ if (host.parentNode) host.parentNode.removeChild(host)
108
+ }
109
+ app = createApp(options.component, {
110
+ ...(options.componentProps || {}),
111
+ inline: true,
112
+ onClose: cleanup,
113
+ onOk: cleanup,
114
+ onHide: cleanup,
115
+ })
116
+ app.config.globalProperties.$q = q
117
+ app.provide("_q_", q)
118
+ app.mount(host)
119
+ nextTick(() => {
120
+ const inst = app?._instance?.proxy as any
121
+ if (inst?.show) inst.show()
122
+ })
123
+ return {
124
+ onOk: () => {},
125
+ onCancel: cleanup,
126
+ onDismiss: cleanup,
127
+ }
128
+ }
129
+ }
130
+
131
+ function ensureQuasarGlobals(getContainer?: () => HTMLElement | null) {
132
+ const inst = getCurrentInstance()
133
+ if (!inst) return null
134
+ const globals = inst.appContext.config.globalProperties as any
135
+ if (!globals.$q) {
136
+ globals.$q = {
137
+ iconMapFn: null,
138
+ lang: { rtl: false },
139
+ iconSet: {},
140
+ platform: { is: { ios: false, desktop: true, mobile: false } },
141
+ screen: { width: 0, height: 0, name: "md" },
142
+ config: {},
143
+ dark: { isActive: false },
144
+ }
145
+ }
146
+ const baseQ = globals.$q
147
+ if (baseQ.iconMapFn === undefined) baseQ.iconMapFn = null
148
+ if (!baseQ.lang) baseQ.lang = { rtl: false }
149
+ if (!baseQ.iconSet) baseQ.iconSet = {}
150
+ if (!baseQ.platform) {
151
+ baseQ.platform = { is: { ios: false, desktop: true, mobile: false } }
152
+ }
153
+ if (!baseQ.screen) {
154
+ baseQ.screen = { width: 0, height: 0, name: "md" }
155
+ }
156
+ if (!baseQ.config) baseQ.config = {}
157
+ if (!baseQ.dark) baseQ.dark = { isActive: false }
158
+
159
+ const fallbackDialog = createDialogFallback(baseQ, getContainer)
160
+ const baseDialog =
161
+ typeof baseQ.dialog === "function" ? baseQ.dialog.bind(baseQ) : undefined
162
+ const scopedQ = Object.create(baseQ)
163
+ scopedQ.dialog = (options?: {
164
+ component?: any
165
+ componentProps?: Record<string, unknown>
166
+ }) => {
167
+ if (baseDialog) {
168
+ return baseDialog(options)
169
+ }
170
+ return fallbackDialog(options)
171
+ }
172
+ return scopedQ
173
+ }
174
+
175
+ /** 无封面时传 undefined,避免 poster="null" 字符串导致 video 使用无效地址 */
176
+ const effectivePoster = computed(() => {
177
+ const p = props.poster
178
+ if (p == null || p === "" || String(p) === "null") return undefined
179
+ return p
180
+ })
181
+
182
+ const containerRef = ref<HTMLElement | null>(null)
183
+ const player = ref<NativePlayerAPI | null>(null)
184
+ provide(nativePlayerKey, player)
185
+ const quasar = ensureQuasarGlobals(() => containerRef.value)
186
+ if (quasar) {
187
+ provide("_q_", quasar)
188
+ }
189
+
190
+ const store = useHistoryInfo()
191
+ const state = useNativePlayerState(player)
192
+ const history = useNativePlayerHistory(player, store)
193
+ const playlist = useNativePlayerPlaylist(player, store, {
194
+ playPrev: props.options?.playPrev,
195
+ playNext: props.options?.playNext,
196
+ })
197
+ const modal = useNativePlayerModal()
198
+ const fullscreen = useNativePlayerFullscreen(player, {
199
+ showTopBar: props.options?.showTopBar,
200
+ })
201
+ const controlsActive = ref(true)
202
+ let controlsTimer: ReturnType<typeof setTimeout> | null = null
203
+ let rootClickTimer: ReturnType<typeof setTimeout> | null = null
204
+ const beforeHiddenSubtitle = ref<Subtitle | null>(null)
205
+ const isCursorInactive = computed(
206
+ () => !isMobile() && state.isPlaying.value && !controlsActive.value,
207
+ )
208
+
209
+ function emitControlBar(value: boolean) {
210
+ if (controlsActive.value === value) return
211
+ controlsActive.value = value
212
+ emit("controlBarActive", value)
213
+ }
214
+
215
+ function scheduleControlsHide() {
216
+ if (controlsTimer) {
217
+ clearTimeout(controlsTimer)
218
+ controlsTimer = null
219
+ }
220
+ if (!state.isPlaying.value) {
221
+ emitControlBar(true)
222
+ return
223
+ }
224
+ controlsTimer = setTimeout(() => {
225
+ emitControlBar(false)
226
+ }, 3000)
227
+ }
228
+
229
+ function activateControls() {
230
+ emitControlBar(true)
231
+ scheduleControlsHide()
232
+ }
233
+
234
+ function getTargetElement(target: EventTarget | null): Element | null {
235
+ if (!target) return null
236
+ if (target instanceof Element) return target
237
+ if (target instanceof Node) return target.parentElement
238
+ return null
239
+ }
240
+
241
+ function isEditableTarget(target: EventTarget | null): boolean {
242
+ const el = getTargetElement(target)
243
+ if (!(el instanceof HTMLElement)) return false
244
+ if (el.isContentEditable) return true
245
+ return !!el.closest("input, textarea, select, [contenteditable='true']")
246
+ }
247
+
248
+ function shouldSkipClickToggle(target: EventTarget | null): boolean {
249
+ const el = getTargetElement(target)
250
+ if (!el) return false
251
+ if (el.closest("[data-no-play-toggle]")) return true
252
+ if (
253
+ el.closest(
254
+ "button, a, input, textarea, select, img, svg, path, [role='button'], [role='link'], .q-icon, .q-btn",
255
+ )
256
+ ) {
257
+ return true
258
+ }
259
+ return false
260
+ }
261
+
262
+ function togglePlayPauseByUser(scene: string) {
263
+ const p = player.value
264
+ if (!p || p.isDisposed() || p.isCastMode?.()) return
265
+ if (p.paused()) {
266
+ safePlay(p, scene)
267
+ } else {
268
+ p.pause()
269
+ }
270
+ }
271
+
272
+ function clearPendingRootClick() {
273
+ if (!rootClickTimer) return
274
+ clearTimeout(rootClickTimer)
275
+ rootClickTimer = null
276
+ }
277
+
278
+ function toggleFullscreenByUser() {
279
+ const p = player.value
280
+ if (!p || p.isDisposed()) return
281
+ if (p.isFullscreen()) {
282
+ p.exitFullscreen()
283
+ } else {
284
+ p.requestFullscreen()
285
+ }
286
+ }
287
+
288
+ function onRootClick(e: MouseEvent) {
289
+ if (isMobile()) return
290
+ activateControls()
291
+ if (e.button !== 0) return
292
+ if (shouldSkipClickToggle(e.target)) return
293
+ clearPendingRootClick()
294
+ rootClickTimer = setTimeout(() => {
295
+ rootClickTimer = null
296
+ togglePlayPauseByUser("click toggle")
297
+ }, 220)
298
+ }
299
+
300
+ function onRootDoubleClick(e: MouseEvent) {
301
+ if (isMobile()) return
302
+ activateControls()
303
+ if (e.button !== 0) return
304
+ if (shouldSkipClickToggle(e.target)) return
305
+ clearPendingRootClick()
306
+ toggleFullscreenByUser()
307
+ }
308
+
309
+ function onRootMouseMove() {
310
+ if (isMobile()) return
311
+ activateControls()
312
+ }
313
+
314
+ function onRootTouchStart(e: TouchEvent) {
315
+ if (!isMobile()) {
316
+ activateControls()
317
+ return
318
+ }
319
+ if (shouldSkipClickToggle(e.target)) return
320
+ if (controlsTimer) {
321
+ clearTimeout(controlsTimer)
322
+ controlsTimer = null
323
+ }
324
+ if (controlsActive.value) {
325
+ emitControlBar(false)
326
+ return
327
+ }
328
+ emitControlBar(true)
329
+ scheduleControlsHide()
330
+ }
331
+
332
+ const shortcutToggle = computed(() => props.shortcutToggle)
333
+
334
+ function onBack() {
335
+ const p = player.value
336
+ if (isMobile() && p?.isFullscreen()) {
337
+ p.exitFullscreen()
338
+ } else {
339
+ history.updateHistory(false)
340
+ player.value?.dispose()
341
+ player.value = null
342
+ emit("back")
343
+ }
344
+ }
345
+
346
+ function openModal(page: string, ev: Event) {
347
+ modal.openModal(page, ev, containerRef.value)
348
+ activateControls()
349
+ }
350
+
351
+ function updateSubtitleHiddenHistory(p: NativePlayerAPI) {
352
+ const info = p.currentVideoInfo()
353
+ if (!info) return
354
+ if (p.isSubtitleHidden()) {
355
+ const sub = beforeHiddenSubtitle.value
356
+ if (sub) {
357
+ store.updateOrCreateHistory(
358
+ {
359
+ ...info,
360
+ beforeHiddenSubtitle: sub,
361
+ } as any,
362
+ null,
363
+ )
364
+ }
365
+ } else {
366
+ store.updateOrCreateHistory(
367
+ {
368
+ ...info,
369
+ beforeHiddenSubtitle: undefined,
370
+ } as any,
371
+ null,
372
+ )
373
+ }
374
+ }
375
+
376
+ function restoreHiddenSubtitle(p: NativePlayerAPI) {
377
+ const info = p.currentVideoInfo()
378
+ if (!info?.sourceUrl) return
379
+ const historyInfo = store.getHistoryInfo(info.sourceUrl)
380
+ const saved = historyInfo?.beforeHiddenSubtitle
381
+ if (!saved) return
382
+ const available = p.getAvaliableSubtitles?.() || []
383
+ const found = available.find(
384
+ (sub) => sub.name === saved.name && sub.stream_index === saved.stream_index,
385
+ )
386
+ if (found) {
387
+ beforeHiddenSubtitle.value = saved
388
+ p.setSubtitleHidden(true)
389
+ }
390
+ }
391
+
392
+ function hasAvailableSubtitles(p: NativePlayerAPI): boolean {
393
+ const available = p.getAvaliableSubtitles?.()
394
+ return Array.isArray(available) && available.length > 0
395
+ }
396
+
397
+ function getSortedPlaybackRates(p: NativePlayerAPI): number[] {
398
+ const rates = p
399
+ .playbackRates()
400
+ .filter((rate) => Number.isFinite(rate) && rate > 0)
401
+ .sort((a, b) => a - b)
402
+ return Array.from(new Set(rates))
403
+ }
404
+
405
+ function getSteppedPlaybackRate(
406
+ rates: number[],
407
+ currentRate: number,
408
+ direction: 1 | -1,
409
+ ): number | null {
410
+ const epsilon = 1e-6
411
+ if (direction > 0) {
412
+ for (const rate of rates) {
413
+ if (rate > currentRate + epsilon) return rate
414
+ }
415
+ return null
416
+ }
417
+ for (let i = rates.length - 1; i >= 0; i -= 1) {
418
+ if (rates[i] < currentRate - epsilon) return rates[i]
419
+ }
420
+ return null
421
+ }
422
+
423
+ function adjustPlaybackRateByShortcut(direction: 1 | -1) {
424
+ const p = player.value
425
+ if (!p || p.isDisposed() || p.isCastMode?.()) return
426
+ const currentRateValue = p.playbackRate()
427
+ if (
428
+ typeof currentRateValue !== "number" ||
429
+ !Number.isFinite(currentRateValue)
430
+ ) {
431
+ return
432
+ }
433
+ const rates = getSortedPlaybackRates(p)
434
+ if (!rates.length) return
435
+ const nextRate = getSteppedPlaybackRate(rates, currentRateValue, direction)
436
+ if (nextRate == null) return
437
+ p.playbackRate(nextRate)
438
+ }
439
+
440
+ function consumeKeyboardEvent(e: KeyboardEvent) {
441
+ e.preventDefault()
442
+ e.stopPropagation()
443
+ }
444
+
445
+ useEventListener("keydown", (e: KeyboardEvent) => {
446
+ if (!shortcutToggle.value) return
447
+ if (isMobile()) return
448
+ if (
449
+ e.shiftKey &&
450
+ !e.ctrlKey &&
451
+ !e.altKey &&
452
+ !e.metaKey &&
453
+ !e.repeat &&
454
+ !isEditableTarget(e.target)
455
+ ) {
456
+ if (e.code === "ArrowRight") {
457
+ consumeKeyboardEvent(e)
458
+ adjustPlaybackRateByShortcut(1)
459
+ return
460
+ }
461
+ if (e.code === "ArrowLeft") {
462
+ consumeKeyboardEvent(e)
463
+ adjustPlaybackRateByShortcut(-1)
464
+ return
465
+ }
466
+ }
467
+ if (e.code === "Space") {
468
+ if (isEditableTarget(e.target)) return
469
+ e.preventDefault()
470
+ togglePlayPauseByUser("keyboard toggle")
471
+ }
472
+ })
473
+
474
+ useKeyBind(
475
+ {
476
+ ArrowLeft: () => {
477
+ const p = player.value
478
+ if (!p) return
479
+ p.currentTime(Math.max(0, (p.currentTime() as number) - 5))
480
+ },
481
+ ArrowRight: () => {
482
+ const p = player.value
483
+ if (!p) return
484
+ p.currentTime(
485
+ Math.min(p.duration() as number, (p.currentTime() as number) + 5),
486
+ )
487
+ },
488
+ ArrowUp: async () => {
489
+ const p = player.value
490
+ if (!p) return
491
+ const v = await Promise.resolve(p.volume() as number)
492
+ p.volume(Math.min(100, v * 100 + 5) / 100)
493
+ p.trigger("lzcVolumeShow")
494
+ },
495
+ ArrowDown: async () => {
496
+ const p = player.value
497
+ if (!p) return
498
+ const v = await Promise.resolve(p.volume() as number)
499
+ p.volume(Math.max(0, v * 100 - 5) / 100)
500
+ p.trigger("lzcVolumeShow")
501
+ },
502
+ },
503
+ shortcutToggle,
504
+ )
505
+
506
+ useEventListener("keydown", (e: KeyboardEvent) => {
507
+ if (!shortcutToggle.value) return
508
+ if (e.altKey && (e.key === "h" || e.key === "H")) {
509
+ const p = player.value
510
+ if (!p || p.isDisposed()) return
511
+ if (isDirectMode.value) return
512
+ if (!hasAvailableSubtitles(p)) return
513
+ e.preventDefault()
514
+ p.toggleSubtitleVisibility()
515
+ }
516
+ })
517
+
518
+ function onPlayerReady(p: NativePlayerAPI) {
519
+ player.value = p
520
+ useSource(p as any, {
521
+ externalPrevNextControl: {
522
+ prev: !!props.options?.playPrev,
523
+ next: !!props.options?.playNext,
524
+ },
525
+ })
526
+ if (!isDirectMode.value) {
527
+ useNativeCastMiddleware(p as any)
528
+ }
529
+
530
+ if (containerRef.value) {
531
+ p.setFullscreenTarget(containerRef.value)
532
+ p.setContainer(containerRef.value)
533
+ }
534
+
535
+ playlist.bind(p)
536
+ p.lzcModal = () => ({
537
+ openModal: (page: string) => {
538
+ modal.modalPage.value = page
539
+ modal.modalVisible.value = true
540
+ },
541
+ close: modal.closeModal,
542
+ })
543
+ p.on("openVideo", () => {
544
+ modal.closeModal()
545
+ beforeHiddenSubtitle.value = null
546
+ activateControls()
547
+ setTimeout(() => {
548
+ state.videoName.value = p.currentVideoName() || ""
549
+ }, 0)
550
+ p.one("subtitleReady", () => restoreHiddenSubtitle(p))
551
+ p.one("canplay", () => {
552
+ if (!p.isCastMode?.() && props.options?.autoplay !== false) {
553
+ safePlay(p, "autoplay")
554
+ }
555
+ history.updateHistory(true)
556
+ })
557
+ })
558
+
559
+ state.bind(p)
560
+ history.bind(p)
561
+ fullscreen.bind(p)
562
+ p.on("fullscreenchange", () =>
563
+ emit("fullscreenchange", p.isFullscreen(), p.id()),
564
+ )
565
+
566
+ if (effectivePoster.value) p.poster(effectivePoster.value)
567
+ p.on("back", onBack)
568
+ p.on("play", scheduleControlsHide)
569
+ p.on("pause", () => emitControlBar(true))
570
+ p.on("ended", () => {
571
+ if (p.isCastMode()) return
572
+ if (p.nextEnabled && p.playNext) {
573
+ p.playNext()
574
+ return
575
+ }
576
+ p.pause()
577
+ })
578
+ p.on("subtitlevisibilitychange", () => updateSubtitleHiddenHistory(p))
579
+ const originalToggle = p.toggleSubtitleVisibility?.bind(p)
580
+ if (originalToggle) {
581
+ p.toggleSubtitleVisibility = () => {
582
+ if (!hasAvailableSubtitles(p)) return
583
+ const current = p.currentSubtitle?.()
584
+ if (!p.isSubtitleHidden() && current) {
585
+ beforeHiddenSubtitle.value = current
586
+ }
587
+ originalToggle()
588
+ updateSubtitleHiddenHistory(p)
589
+ }
590
+ }
591
+ if (props.onInit) props.onInit(p)
592
+ emit("init", p)
593
+ state.videoName.value = p.currentVideoName() || ""
594
+ if (typeof window !== "undefined") {
595
+ ;(window as any).player = p
596
+ }
597
+ }
598
+
599
+ function onPlayerDestroy() {
600
+ state.unbindIfNeeded()
601
+ history.unbindIfNeeded()
602
+ fullscreen.unbindIfNeeded()
603
+ player.value = null
604
+ if (typeof window !== "undefined") {
605
+ ;(window as any).player = null
606
+ }
607
+ }
608
+
609
+ onMounted(() => {
610
+ const meta = document.createElement("meta")
611
+ meta.name = "referrer"
612
+ meta.content = "no-referrer"
613
+ document.head.append(meta)
614
+ activateControls()
615
+ })
616
+
617
+ watch(
618
+ () => state.isPlaying.value,
619
+ () => {
620
+ scheduleControlsHide()
621
+ },
622
+ )
623
+
624
+ watch(
625
+ () => modal.modalVisible.value,
626
+ (visible) => {
627
+ if (visible) {
628
+ emitControlBar(true)
629
+ } else {
630
+ scheduleControlsHide()
631
+ }
632
+ },
633
+ )
634
+
635
+ onUnmounted(() => {
636
+ state.unbindIfNeeded()
637
+ history.unbindIfNeeded()
638
+ fullscreen.unbindIfNeeded()
639
+ clearPendingRootClick()
640
+ if (controlsTimer) {
641
+ clearTimeout(controlsTimer)
642
+ controlsTimer = null
643
+ }
644
+ history.debouncedUpdateHistory.cancel()
645
+ history.updateHistory(false)
646
+ })
647
+ </script>
648
+
649
+ <template>
650
+ <div
651
+ ref="containerRef"
652
+ :id="props.containerId"
653
+ class="vjs-theme-lzc lzc-video-player lzc-player-root"
654
+ :class="{
655
+ 'lzc-user-active': !isCursorInactive,
656
+ 'lzc-user-inactive': isCursorInactive,
657
+ }"
658
+ @mousemove="onRootMouseMove"
659
+ @touchstart="onRootTouchStart"
660
+ @click.capture="onRootClick"
661
+ @dblclick.capture="onRootDoubleClick"
662
+ >
663
+ <div
664
+ class="lzc-video-stage relative w-full h-full min-h-0 overflow-hidden flex flex-col"
665
+ >
666
+ <NativePlayerComponent
667
+ :options="{
668
+ mediaPrefix: props.options?.mediaPrefix,
669
+ showTopBar: props.options?.showTopBar,
670
+ playMode: props.options?.playMode,
671
+ }"
672
+ :poster="effectivePoster"
673
+ @ready="onPlayerReady"
674
+ @destroy="onPlayerDestroy"
675
+ />
676
+ <div class="lzc-player-foreground absolute inset-0 z-[20]" v-if="player">
677
+ <LzcOverlay
678
+ :player="player as any"
679
+ :active="controlsActive"
680
+ :direct-mode="isDirectMode"
681
+ />
682
+ </div>
683
+ <div
684
+ v-show="unref(state.isSourceLoading)"
685
+ class="lzc-player-source-loading absolute inset-0 bg-black pointer-events-none z-[10]"
686
+ aria-hidden="true"
687
+ ></div>
688
+ <div
689
+ v-show="unref(state.isSourceLoading) || unref(state.showSpinner)"
690
+ class="lzc-player-spinner absolute inset-0 flex items-center justify-center pointer-events-none z-[30]"
691
+ aria-hidden="true"
692
+ >
693
+ <div
694
+ class="w-9 h-9 -m-[22px_0_0_-22px] rounded-full border-2 border-[#5f86ff] border-r-transparent border-b-[#5f86ff] border-l-transparent animate-spin"
695
+ ></div>
696
+ </div>
697
+ <NativeModal
698
+ class="lzc-player-modal"
699
+ data-no-play-toggle
700
+ :visible="unref(modal.modalVisible)"
701
+ :active-page="unref(modal.modalPage)"
702
+ :position="unref(modal.modalPosition)"
703
+ :direct-mode="isDirectMode"
704
+ @close="modal.closeModal"
705
+ />
706
+ <div
707
+ data-no-play-toggle
708
+ class="lzc-player-controls absolute left-0 right-0 bottom-0 z-[40]"
709
+ :class="{ invisible: !controlsActive }"
710
+ >
711
+ <NativeControls
712
+ :current-time="unref(state.currentTime)"
713
+ :duration="unref(state.duration)"
714
+ :is-playing="unref(state.isPlaying)"
715
+ :is-fullscreen="unref(state.isFullscreen)"
716
+ :prev-enabled="unref(state.prevEnabled)"
717
+ :next-enabled="unref(state.nextEnabled)"
718
+ :hide-prev-button="props.options?.hidePrevButton"
719
+ :hide-next-button="props.options?.hideNextButton"
720
+ :hide-play-list="props.options?.hidePlayList || isDirectMode"
721
+ :hide-subtitle="isDirectMode"
722
+ :hide-resolution="isDirectMode"
723
+ @open-modal="openModal"
724
+ />
725
+ </div>
726
+ </div>
727
+ </div>
728
+ </template>
729
+
730
+ <style scoped>
731
+ .lzc-player-root {
732
+ width: 100%;
733
+ height: 100%;
734
+ min-height: 0;
735
+ overflow: hidden;
736
+ position: relative;
737
+ background: #000;
738
+ }
739
+
740
+ .lzc-player-root.lzc-user-active {
741
+ cursor: default;
742
+ }
743
+
744
+ .lzc-player-root.lzc-user-inactive,
745
+ .lzc-player-root.lzc-user-inactive * {
746
+ cursor: none !important;
747
+ }
748
+ </style>