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,496 @@
1
+ <script lang="ts" setup>
2
+ import { ref, type Ref } from "vue"
3
+ import {
4
+ type TouchPanEvent,
5
+ computeBrightness,
6
+ computeVolume,
7
+ computeProgress,
8
+ } from "./helper"
9
+ import { formatTime } from "../useLzcCommon"
10
+ import PlayingLayout from "./playingLayout.vue"
11
+ import { useDebounceFn } from "@vueuse/shared"
12
+ import { t } from "@/i18n"
13
+ import type { LzcPlayer } from "@/components/Video/player"
14
+ import SubtitleLayer from "./SubtitleLayer.vue"
15
+
16
+ const props = defineProps<{
17
+ player: LzcPlayer
18
+ active?: boolean
19
+ directMode?: boolean
20
+ }>()
21
+ const sensitivityRef = ref<HTMLElement | null>(null)
22
+
23
+ const frameCanvas: Ref<HTMLCanvasElement | null> = ref(null)
24
+ const updateFrameCanvas = (toTime: number) => {
25
+ if (!frameCanvas.value) {
26
+ return
27
+ }
28
+ const info = props.player.currentPreview()
29
+ if (!info) {
30
+ return
31
+ }
32
+ const index = Math.trunc(
33
+ (toTime % (info.len * info.interval)) / info.interval,
34
+ )
35
+ const wh = Math.sqrt(info.len)
36
+ const rowIndex = Math.trunc(index / wh)
37
+ const colIndex = index == 0 ? 0 : (index % wh) - 1
38
+
39
+ const ctx = frameCanvas.value.getContext("2d")
40
+ const sx = colIndex * info.width
41
+ const sy = rowIndex * info.height
42
+ ctx?.drawImage(
43
+ info.image,
44
+ sx,
45
+ sy,
46
+ info.width,
47
+ info.height,
48
+ 0,
49
+ 0,
50
+ frameCanvas.value.width,
51
+ frameCanvas.value.height,
52
+ )
53
+ }
54
+
55
+ // tip toast
56
+ const tipToastActive = ref(false)
57
+ const tipToastValue = ref("")
58
+ const tipToastIcon = ref("")
59
+ const tipToastCloseTimeout = ref(0)
60
+ props.player.on("tipToast", (e: any) => {
61
+ tipToastActive.value = true
62
+ tipToastValue.value = e.toastValue
63
+ tipToastIcon.value = e.toastIcon
64
+ let timeout = 1000
65
+ if (e.uncloseable) {
66
+ return
67
+ }
68
+ if (e.timeout != undefined) {
69
+ timeout = e.timeout
70
+ }
71
+ setTimeout(() => {
72
+ tipToastActive.value = false
73
+ }, timeout)
74
+ })
75
+ //
76
+ const topTipIcon = ref("")
77
+ const topTipActive = ref(false)
78
+ const topTipPercent = ref(0.5)
79
+ const centerTipActive = ref(false)
80
+ const frameTipActive = ref(false)
81
+ const frameTipTime = ref("")
82
+
83
+ const progressCache: Ref<number> = ref(0)
84
+ const volumeCache: Ref<number> = ref(0)
85
+ const brightnessCache: Ref<number> = ref(0)
86
+ const currentDirection: Ref<string> = ref("")
87
+ let dataLoading = false
88
+
89
+ interface GestureRect {
90
+ left: number
91
+ top: number
92
+ width: number
93
+ height: number
94
+ }
95
+
96
+ function isIosPortraitFakeFullscreen(): boolean {
97
+ return (
98
+ props.player.isFullscreen() &&
99
+ props.player.hasClass("vjs-ios-fake-fullscreen-portrait")
100
+ )
101
+ }
102
+
103
+ function clamp(value: number, min: number, max: number): number {
104
+ return Math.max(min, Math.min(max, value))
105
+ }
106
+
107
+ function readGestureRect(): GestureRect {
108
+ const el = sensitivityRef.value
109
+ if (el) {
110
+ const rect = el.getBoundingClientRect()
111
+ if (rect.width > 0 && rect.height > 0) {
112
+ return {
113
+ left: rect.left,
114
+ top: rect.top,
115
+ width: rect.width,
116
+ height: rect.height,
117
+ }
118
+ }
119
+ }
120
+ const dim = props.player.currentDimensions()
121
+ return {
122
+ left: 0,
123
+ top: 0,
124
+ width: dim.width,
125
+ height: dim.height,
126
+ }
127
+ }
128
+
129
+ function mapDirectionForIosPortraitFakeFullscreen(
130
+ direction: TouchPanEvent["direction"],
131
+ ): TouchPanEvent["direction"] {
132
+ switch (direction) {
133
+ case "up":
134
+ return "left"
135
+ case "down":
136
+ return "right"
137
+ case "left":
138
+ return "down"
139
+ case "right":
140
+ return "up"
141
+ default:
142
+ return direction
143
+ }
144
+ }
145
+
146
+ function normalizePanEvent(event: TouchPanEvent): {
147
+ panEvent: TouchPanEvent
148
+ dimensions: { width: number; height: number }
149
+ } {
150
+ const rect = readGestureRect()
151
+ const localLeft = clamp(event.position.left - rect.left, 0, rect.width)
152
+ const localTop = clamp(event.position.top - rect.top, 0, rect.height)
153
+
154
+ if (!isIosPortraitFakeFullscreen()) {
155
+ return {
156
+ panEvent: {
157
+ ...event,
158
+ position: {
159
+ left: localLeft,
160
+ top: localTop,
161
+ },
162
+ },
163
+ dimensions: {
164
+ width: rect.width,
165
+ height: rect.height,
166
+ },
167
+ }
168
+ }
169
+
170
+ // In iOS portrait fake fullscreen, the player container is rotated 90deg.
171
+ // Map pan coordinates back to the visual (pre-rotation) player coordinate system.
172
+ return {
173
+ panEvent: {
174
+ ...event,
175
+ direction: mapDirectionForIosPortraitFakeFullscreen(event.direction),
176
+ position: {
177
+ left: localTop,
178
+ top: clamp(rect.width - localLeft, 0, rect.width),
179
+ },
180
+ offset: {
181
+ x: event.offset.y,
182
+ y: -event.offset.x,
183
+ },
184
+ delta: {
185
+ x: event.delta.y,
186
+ y: -event.delta.x,
187
+ },
188
+ },
189
+ dimensions: {
190
+ width: rect.height,
191
+ height: rect.width,
192
+ },
193
+ }
194
+ }
195
+
196
+ const handlePan = async (event: TouchPanEvent) => {
197
+ const { panEvent, dimensions } = normalizePanEvent(event)
198
+ if (panEvent.isFinal) {
199
+ currentDirection.value = ""
200
+
201
+ topTipActive.value = false
202
+ topTipIcon.value = ""
203
+
204
+ frameTipActive.value = false
205
+ frameTipTime.value = ""
206
+ volumeCache.value = 0
207
+ brightnessCache.value = 0
208
+ progressCache.value = 0
209
+ return
210
+ }
211
+ if (panEvent.isFirst) {
212
+ currentDirection.value = panEvent.direction
213
+ }
214
+
215
+ const totalWidth = dimensions.width
216
+ if (currentDirection.value == "up" || currentDirection.value == "down") {
217
+ if (panEvent.isFirst) {
218
+ topTipActive.value = true
219
+ dataLoading = true
220
+ try {
221
+ volumeCache.value = await props.player.volume()!
222
+ brightnessCache.value = await props.player.currentBrightness()
223
+ } finally {
224
+ dataLoading = false
225
+ }
226
+ }
227
+ if (dataLoading) {
228
+ return
229
+ }
230
+ const isBrightness = panEvent.position.left < totalWidth / 2
231
+ if (isBrightness) {
232
+ topTipIcon.value = "svguse:#icon-亮度.svg"
233
+ const { percent, brightness } = computeBrightness(
234
+ brightnessCache.value,
235
+ panEvent,
236
+ dimensions,
237
+ )
238
+ topTipPercent.value = percent
239
+ props.player.trigger({
240
+ type: "changeBrightness",
241
+ brightness: brightness,
242
+ })
243
+ } else {
244
+ topTipIcon.value = "svguse:#icon-音量.svg"
245
+ const { percent, volume } = computeVolume(
246
+ volumeCache.value,
247
+ panEvent,
248
+ dimensions,
249
+ )
250
+ topTipPercent.value = percent
251
+ props.player.volume(volume)
252
+ }
253
+ } else {
254
+ if (panEvent.isFirst) {
255
+ progressCache.value = props.player.currentTime()!
256
+ frameTipActive.value = true
257
+ props.player.removeClass("vjs-ended")
258
+ }
259
+ const toTime = computeProgress(
260
+ props.player.duration()!,
261
+ progressCache.value,
262
+ panEvent,
263
+ dimensions,
264
+ )
265
+ frameTipTime.value = formatTime(toTime)
266
+ // 更新进度条
267
+ props.player.currentTime(toTime)
268
+ props.player.trigger({ type: "timeupdate" })
269
+ updateFrameCanvas(toTime)
270
+ }
271
+ }
272
+ const resetTopTipByDb = useDebounceFn(() => resetTopTip(), 1500)
273
+ props.player.on("pause", () => {
274
+ if (props.player.ended() || props.player.seeking()) {
275
+ centerTipActive.value = false
276
+ return
277
+ }
278
+ centerTipActive.value = true
279
+ })
280
+ props.player.on("play", () => {
281
+ centerTipActive.value = false
282
+ })
283
+ props.player.on("lzcSeekBarDown", () => {
284
+ frameTipActive.value = true
285
+ })
286
+ props.player.on("lzcSeekBarMove", () => {
287
+ const time = props.player.currentTime()!
288
+ frameTipTime.value = formatTime(time)
289
+ updateFrameCanvas(time)
290
+ })
291
+ props.player.on("lzcSeekBarUp", () => {
292
+ frameTipActive.value = false
293
+ frameTipTime.value = ""
294
+ })
295
+ props.player.on("lzcVolumeShow", async () => {
296
+ topTipActive.value = true
297
+ topTipIcon.value = "svguse:#icon-音量.svg"
298
+ topTipPercent.value = (await props.player.volume()!) / 1
299
+ resetTopTipByDb()
300
+ })
301
+
302
+ const handleCenterTipClick = async () => {
303
+ if (!props.player.paused()) {
304
+ return
305
+ }
306
+ try {
307
+ await props.player.play()
308
+ } catch {
309
+ // Ignore play errors (e.g. autoplay restrictions).
310
+ }
311
+ }
312
+
313
+ function resetTopTip() {
314
+ topTipActive.value = false
315
+ topTipIcon.value = ""
316
+ }
317
+ </script>
318
+
319
+ <template>
320
+ <PlayingLayout
321
+ :player="player"
322
+ :active="props.active"
323
+ :direct-mode="props.directMode"
324
+ >
325
+ <SubtitleLayer :player="player" :active="props.active" />
326
+ <div
327
+ ref="sensitivityRef"
328
+ class="sensitivity"
329
+ v-touch-pan.prevent="handlePan"
330
+ >
331
+ <div class="top-tip-container" v-if="tipToastActive">
332
+ <div class="top-tip">
333
+ <q-icon
334
+ class="icon"
335
+ :name="tipToastIcon"
336
+ v-if="tipToastIcon"
337
+ ></q-icon>
338
+ <span class="toast-text">{{ tipToastValue }}</span>
339
+ </div>
340
+ </div>
341
+
342
+ <div class="top-tip-container" v-else-if="topTipActive">
343
+ <div class="top-tip">
344
+ <q-icon class="icon" :name="topTipIcon"></q-icon>
345
+ <div class="progress">
346
+ <div :style="`width: ${topTipPercent * 100}%;`"></div>
347
+ </div>
348
+ </div>
349
+ </div>
350
+
351
+ <div class="center-tip-container" v-else>
352
+ <div class="frame-tip" v-if="frameTipActive">
353
+ <div class="frame-img">
354
+ <q-img>
355
+ <canvas ref="frameCanvas"></canvas>
356
+ <div class="absolute-bottom text-subtitle2 frame-time">
357
+ {{ frameTipTime }}
358
+ </div>
359
+ </q-img>
360
+ </div>
361
+ </div>
362
+ <div
363
+ class="center-tip"
364
+ v-else-if="centerTipActive"
365
+ @click="handleCenterTipClick"
366
+ >
367
+ <q-icon class="icon" name="svguse:#icon-暂停.svg"></q-icon>
368
+ {{
369
+ t(
370
+ "src.components.video.lzc_overlay.playing.play_btn_text",
371
+ "已暂停",
372
+ )
373
+ }}
374
+ </div>
375
+ </div>
376
+ </div>
377
+ </PlayingLayout>
378
+ </template>
379
+
380
+ <style lang="scss" scoped>
381
+ @mixin vjs-overlay-note-background() {
382
+ background: rgb(0, 0, 0, 0.45);
383
+ border-radius: 0.6rem;
384
+ }
385
+
386
+ @mixin vjs-overlay-note-text() {
387
+ font-size: 1.2rem;
388
+ font-weight: 500;
389
+ color: #ffffff;
390
+ line-height: 1.7rem;
391
+ padding: 0.9rem 1.3rem;
392
+ margin: auto;
393
+ }
394
+
395
+ @mixin vjs-overlay-note-background-top() {
396
+ position: absolute;
397
+ width: 100%;
398
+ top: 8rem;
399
+ }
400
+
401
+ .sensitivity {
402
+ flex: 1;
403
+ display: flex;
404
+ flex-direction: column;
405
+ }
406
+ .top-tip-container {
407
+ display: flex;
408
+ height: 3.4rem;
409
+ flex-direction: row;
410
+ justify-content: center;
411
+
412
+ .top-tip {
413
+ display: flex;
414
+ flex-direction: row;
415
+ align-items: center;
416
+ @include vjs-overlay-note-background;
417
+ @include vjs-overlay-note-text;
418
+
419
+ .icon {
420
+ height: 1.6rem;
421
+ width: 1.6rem;
422
+ margin-right: 0.8rem;
423
+ }
424
+
425
+ .progress {
426
+ width: 13rem;
427
+ height: 0.2rem;
428
+ background-color: rgba(255, 255, 255, 0.3);
429
+ border-radius: 0.1rem;
430
+
431
+ > div {
432
+ background-color: rgba(255, 255, 255, 1);
433
+ height: 0.2rem;
434
+ }
435
+ }
436
+
437
+ .toast-text {
438
+ font-size: 1.2rem;
439
+ font-weight: 500;
440
+ color: #ffffff;
441
+ }
442
+ }
443
+ }
444
+
445
+ .center-tip-container {
446
+ display: flex;
447
+ height: 100%;
448
+ flex-direction: row;
449
+ justify-content: center;
450
+ align-items: center;
451
+
452
+ .center-tip {
453
+ display: flex;
454
+ flex-direction: row;
455
+ align-items: center;
456
+ @include vjs-overlay-note-background;
457
+ @include vjs-overlay-note-text;
458
+
459
+ .icon {
460
+ height: 1.6rem;
461
+ width: 1.6rem;
462
+ margin-right: 0.8rem;
463
+ }
464
+ }
465
+
466
+ .frame-tip {
467
+ display: flex;
468
+ flex-direction: row;
469
+ align-items: center;
470
+
471
+ .frame-img {
472
+ width: 12rem;
473
+ height: 6.76rem;
474
+ margin-right: 1.6rem;
475
+
476
+ canvas {
477
+ width: 100%;
478
+ height: 100%;
479
+ }
480
+ }
481
+
482
+ .frame-time {
483
+ padding: unset;
484
+ margin: 0.6rem auto;
485
+ text-align: center;
486
+ background: unset;
487
+
488
+ height: 1.7rem;
489
+ font-size: 1.2rem;
490
+ font-weight: 500;
491
+ color: #ffffff;
492
+ line-height: 1.7rem;
493
+ }
494
+ }
495
+ }
496
+ </style>
@@ -0,0 +1,122 @@
1
+ <script lang="ts" setup>
2
+ import { useQuasar } from "quasar"
3
+ import { devices } from "./useCast"
4
+ import Cast from "./cast.vue"
5
+ import { isMobile } from "@/use/useUtils"
6
+ import FullscreenIcon from "@/icons/全屏.svg"
7
+ import CastIcon from "@/icons/投屏.svg"
8
+ import ClientPlayerIcon from "@/icons/懒猫播放器-icon.png"
9
+ import type { LzcPlayer } from "@/components/Video/player"
10
+ import {
11
+ hasClientPlayerCapability,
12
+ initClientPlayerCapability,
13
+ openClientPlayer,
14
+ } from "@/components/Video/clientPlayer"
15
+ import { useHistoryInfo } from "@/stores/playlist"
16
+
17
+ const props = defineProps<{
18
+ player: LzcPlayer
19
+ directMode?: boolean
20
+ }>()
21
+ const store = useHistoryInfo()
22
+ const fullscreen = () => {
23
+ if (props.player.isFullscreen()) {
24
+ props.player.exitFullscreen()
25
+ } else {
26
+ props.player.requestFullscreen()
27
+ }
28
+ }
29
+ const q = useQuasar()
30
+ const open = () => {
31
+ q.dialog({
32
+ component: Cast,
33
+ componentProps: {
34
+ player: props.player,
35
+ },
36
+ })
37
+ }
38
+ const openNativePlayer = () => {
39
+ void openClientPlayer(props.player, store.infos)
40
+ }
41
+
42
+ void initClientPlayerCapability()
43
+ </script>
44
+
45
+ <template>
46
+ <button
47
+ v-if="isMobile()"
48
+ @click="fullscreen"
49
+ data-no-play-toggle
50
+ class="icon-button fullscreen-button"
51
+ type="button"
52
+ >
53
+ <img :src="FullscreenIcon" class="w-[20px] h-[20px]" />
54
+ </button>
55
+ <span
56
+ v-if="!props.directMode && devices.length >= 0"
57
+ class="cast-button-wrap"
58
+ data-no-play-toggle
59
+ >
60
+ <button
61
+ v-if="hasClientPlayerCapability"
62
+ @click="openNativePlayer"
63
+ data-no-play-toggle
64
+ class="icon-button client-player-button"
65
+ type="button"
66
+ >
67
+ <img :src="ClientPlayerIcon" class="w-[20px] h-[20px]" />
68
+ </button>
69
+ <button
70
+ @click="open"
71
+ data-no-play-toggle
72
+ class="icon-button"
73
+ type="button"
74
+ >
75
+ <img :src="CastIcon" class="w-[20px] h-[20px]" />
76
+ </button>
77
+ </span>
78
+ </template>
79
+
80
+ <style lang="scss" scoped>
81
+ .cast-button-wrap {
82
+ position: relative;
83
+ display: inline-flex;
84
+ width: 36px;
85
+ height: 36px;
86
+ align-items: center;
87
+ justify-content: center;
88
+ }
89
+
90
+ .icon-button {
91
+ display: inline-flex;
92
+ width: 36px;
93
+ height: 36px;
94
+ padding: 0;
95
+ align-items: center;
96
+ justify-content: center;
97
+ border: 0;
98
+ border-radius: 50%;
99
+ background: rgba(0, 0, 0, 0.4);
100
+ cursor: pointer;
101
+
102
+ &:hover {
103
+ background: rgba(0, 0, 0, 0.6);
104
+ }
105
+ }
106
+
107
+ .client-player-button {
108
+ position: absolute;
109
+ left: 0;
110
+ bottom: calc(100% + 8px);
111
+ }
112
+
113
+ @media screen and (max-width: 666px) {
114
+ .fullscreen-button {
115
+ position: relative;
116
+ }
117
+
118
+ .client-player-button {
119
+ bottom: calc(200% + 16px);
120
+ }
121
+ }
122
+ </style>