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,3 @@
1
+ # lzc-video-player
2
+
3
+ ## 组件内自动根据 user-agent 区分是否移动端还是 pc 端
@@ -0,0 +1,348 @@
1
+ import { computed, ref } from "vue"
2
+ import { AppCommon } from "@lazycatcloud/sdk/dist/extentions/app_common"
3
+ import type {
4
+ NativeVideoPlayerEvent,
5
+ NativeVideoPlayerOpenPayload,
6
+ NativeVideoPlayerPlaylistItem,
7
+ } from "@lazycatcloud/sdk/dist/extentions/player"
8
+ import { MiniDB } from "@lazycatcloud/minidb"
9
+ import type { VideoInfo } from "@/model"
10
+ import type { LzcPlayer } from "./player"
11
+ import { isSourceEqual } from "@/use/useUtils"
12
+
13
+ type PlayerCapability = {
14
+ name: string
15
+ version?: string
16
+ }
17
+
18
+ type PreparedPlaylistItem = NativeVideoPlayerPlaylistItem & {
19
+ id: string
20
+ file: string
21
+ sourceInfo: VideoInfo
22
+ }
23
+
24
+ type CapabilityCache = {
25
+ promise?: Promise<boolean>
26
+ value?: boolean
27
+ }
28
+
29
+ const capabilityCache: CapabilityCache = ((
30
+ globalThis as any
31
+ ).__lzcVideoPlayerClientPlayerCapability ||= {})
32
+ const clientPlayerCapability = ref<boolean | undefined>(capabilityCache.value)
33
+ const db: MiniDB = new MiniDB()
34
+ const PROGRESS_UPDATE_INTERVAL = 5000
35
+
36
+ export const hasClientPlayerCapability = computed(
37
+ () => clientPlayerCapability.value === true,
38
+ )
39
+
40
+ function setClientPlayerCapability(value: boolean) {
41
+ capabilityCache.value = value
42
+ clientPlayerCapability.value = value
43
+ }
44
+
45
+ export function initClientPlayerCapability() {
46
+ if (capabilityCache.value !== undefined) {
47
+ clientPlayerCapability.value = capabilityCache.value
48
+ return Promise.resolve(capabilityCache.value)
49
+ }
50
+
51
+ if (!capabilityCache.promise) {
52
+ capabilityCache.promise = (async () => {
53
+ try {
54
+ const capabilities =
55
+ (await AppCommon.GetCapabilitys()) as PlayerCapability[]
56
+ const available = capabilities.some(
57
+ (capability) => capability.name === "player",
58
+ )
59
+ setClientPlayerCapability(available)
60
+ return available
61
+ } catch (error) {
62
+ console.warn("[client-player] get capabilities failed", error)
63
+ setClientPlayerCapability(false)
64
+ return false
65
+ }
66
+ })()
67
+ }
68
+
69
+ return capabilityCache.promise
70
+ }
71
+
72
+ function toAbsoluteUrl(url: string) {
73
+ return new URL(url, window.location.origin).toString()
74
+ }
75
+
76
+ function readOwnerFromSourceUrl(sourceUrl?: string) {
77
+ if (!sourceUrl) {
78
+ return ""
79
+ }
80
+ try {
81
+ const url = new URL(sourceUrl, window.location.origin)
82
+ return url.searchParams.get("X_LZCAPI_UID") || ""
83
+ } catch {
84
+ return ""
85
+ }
86
+ }
87
+
88
+ function appendOwner(url: string, owner: string) {
89
+ if (!owner) {
90
+ return url
91
+ }
92
+ return `${url}${url.includes("?") ? "&" : "?"}owner=${owner}`
93
+ }
94
+
95
+ function encodePathSegments(path: string) {
96
+ return path
97
+ .split("/")
98
+ .filter(Boolean)
99
+ .map((segment) => encodeURIComponent(segment))
100
+ .join("/")
101
+ }
102
+
103
+ function buildWebdavFileUrl(info: VideoInfo) {
104
+ if (!info.fromNetdisk || !info.path) {
105
+ return ""
106
+ }
107
+ const path = encodePathSegments(info.path)
108
+ const url = toAbsoluteUrl(`/_lzc/files/home/${path}`)
109
+ return appendOwner(url, readOwnerFromSourceUrl(info.sourceUrl))
110
+ }
111
+
112
+ function getPlayableUrl(info: VideoInfo) {
113
+ const webdavUrl = buildWebdavFileUrl(info)
114
+ if (webdavUrl) {
115
+ return webdavUrl
116
+ }
117
+ const fallback =
118
+ info.originDirectUrl ||
119
+ info.resolvedSourceUrl ||
120
+ info.sourceUrl ||
121
+ info.originHlsUrl ||
122
+ ""
123
+ return fallback ? toAbsoluteUrl(fallback) : ""
124
+ }
125
+
126
+ function getCoverUrl(player: LzcPlayer, info: VideoInfo, currentInfo: VideoInfo) {
127
+ const poster = player.poster() as string | undefined
128
+ if (isSameVideo(info, currentInfo) && poster) {
129
+ return toAbsoluteUrl(poster)
130
+ }
131
+ if (info.fromNetdisk && info.path) {
132
+ const params = new URLSearchParams()
133
+ params.set("size", "200")
134
+ return toAbsoluteUrl(`/_lzc/thumbnail2/home${info.path}?${params}`)
135
+ }
136
+ return undefined
137
+ }
138
+
139
+ function isSameVideo(a: VideoInfo, b: VideoInfo) {
140
+ try {
141
+ return isSourceEqual(a.sourceUrl, b)
142
+ } catch {
143
+ return a.sourceUrl === b.sourceUrl || (!!a.path && a.path === b.path)
144
+ }
145
+ }
146
+
147
+ function buildPlaylist(player: LzcPlayer, allInfos: VideoInfo[]) {
148
+ const currentInfo = player.currentVideoInfo()
149
+ if (!currentInfo?.sourceUrl) {
150
+ return
151
+ }
152
+
153
+ const infos = allInfos.length ? allInfos : [currentInfo]
154
+ const normalizedInfos = infos.some((info) => isSameVideo(info, currentInfo))
155
+ ? infos
156
+ : [currentInfo, ...infos]
157
+ const index = Math.max(
158
+ 0,
159
+ normalizedInfos.findIndex((info) => isSameVideo(info, currentInfo)),
160
+ )
161
+
162
+ const items: PreparedPlaylistItem[] = normalizedInfos
163
+ .map((info) => {
164
+ const file = getPlayableUrl(info)
165
+ if (!file) return undefined
166
+ return {
167
+ id: info.sourceUrl,
168
+ name: info.name,
169
+ file,
170
+ cover: getCoverUrl(player, info, currentInfo),
171
+ duration: info.duration || undefined,
172
+ startTime:
173
+ isSameVideo(info, currentInfo) && player.currentTime()
174
+ ? player.currentTime()
175
+ : info.currentTime || undefined,
176
+ sourceInfo: info,
177
+ }
178
+ })
179
+ .filter(Boolean) as PreparedPlaylistItem[]
180
+
181
+ if (!items.length) {
182
+ return
183
+ }
184
+
185
+ const payload: NativeVideoPlayerOpenPayload = {
186
+ playlist: {
187
+ index,
188
+ items: items.map((item) => ({
189
+ id: item.id,
190
+ name: item.name,
191
+ file: item.file,
192
+ cover: item.cover,
193
+ duration: item.duration,
194
+ startTime: item.startTime,
195
+ })),
196
+ },
197
+ }
198
+
199
+ return { payload, items, index }
200
+ }
201
+
202
+ function readNumber(payload: unknown, keys: string[]) {
203
+ if (!payload || typeof payload !== "object") {
204
+ return
205
+ }
206
+ const record = payload as Record<string, unknown>
207
+ for (const key of keys) {
208
+ const value = Number(record[key])
209
+ if (Number.isFinite(value)) {
210
+ return value
211
+ }
212
+ }
213
+ }
214
+
215
+ function findItemFromPayload(
216
+ payload: unknown,
217
+ items: PreparedPlaylistItem[],
218
+ fallback: PreparedPlaylistItem,
219
+ ) {
220
+ if (!payload || typeof payload !== "object") {
221
+ return fallback
222
+ }
223
+ const record = payload as Record<string, unknown>
224
+ const id =
225
+ typeof record.id === "string"
226
+ ? record.id
227
+ : typeof record.itemId === "string"
228
+ ? record.itemId
229
+ : typeof record.episodeId === "string"
230
+ ? record.episodeId
231
+ : undefined
232
+ if (id) {
233
+ return items.find((item) => item.id === id) || fallback
234
+ }
235
+
236
+ const file =
237
+ typeof record.file === "string"
238
+ ? record.file
239
+ : typeof record.sourceUrl === "string"
240
+ ? record.sourceUrl
241
+ : undefined
242
+ if (file) {
243
+ return items.find((item) => item.file === file) || fallback
244
+ }
245
+
246
+ const index = readNumber(payload, ["index", "currentIndex"])
247
+ if (index !== undefined && items[index]) {
248
+ return items[index]
249
+ }
250
+ return fallback
251
+ }
252
+
253
+ async function updateProgressHistory(
254
+ item: PreparedPlaylistItem,
255
+ payload: unknown,
256
+ ) {
257
+ const currentTime = readNumber(payload, [
258
+ "currentTime",
259
+ "timePos",
260
+ "time",
261
+ "position",
262
+ "positionSeconds",
263
+ "seconds",
264
+ ])
265
+ if (currentTime === undefined) {
266
+ return
267
+ }
268
+ const duration =
269
+ readNumber(payload, ["duration", "total", "totalTime"]) ||
270
+ Number(item.duration || item.sourceInfo.duration || 0)
271
+
272
+ const historyInfo = db.getCollection("historyInfo")
273
+ const history: VideoInfo = {
274
+ ...item.sourceInfo,
275
+ sourceUrl: item.sourceInfo.sourceUrl,
276
+ name: item.name || item.sourceInfo.name,
277
+ duration: Number.isFinite(duration) ? duration : 0,
278
+ currentTime,
279
+ invalid: false,
280
+ updateTime: Date.now(),
281
+ }
282
+ await historyInfo.upsertOrUpdate({ sourceUrl: history.sourceUrl }, history)
283
+ }
284
+
285
+ function subscribePlayerEvents(
286
+ subscribe: (listener: (event: NativeVideoPlayerEvent) => void) => () => void,
287
+ items: PreparedPlaylistItem[],
288
+ index: number,
289
+ ) {
290
+ let activeItem = items[index] || items[0]
291
+ let lastProgressAt = 0
292
+ let lastProgressEvent: NativeVideoPlayerEvent | undefined
293
+
294
+ const saveProgress = (event: NativeVideoPlayerEvent, force = false) => {
295
+ const now = Date.now()
296
+ if (!force && now - lastProgressAt < PROGRESS_UPDATE_INTERVAL) {
297
+ lastProgressEvent = event
298
+ return
299
+ }
300
+ lastProgressAt = now
301
+ lastProgressEvent = event
302
+ const item = findItemFromPayload(event.payload, items, activeItem)
303
+ void updateProgressHistory(item, event.payload)
304
+ }
305
+
306
+ const unsubscribe = subscribe((event) => {
307
+ if (event.name === "episode-switch") {
308
+ activeItem = findItemFromPayload(event.payload, items, activeItem)
309
+ lastProgressAt = 0
310
+ }
311
+ if (event.name === "progress") {
312
+ saveProgress(event)
313
+ }
314
+ if (event.name === "process-exit") {
315
+ if (lastProgressEvent) {
316
+ saveProgress(lastProgressEvent, true)
317
+ }
318
+ unsubscribe()
319
+ }
320
+ })
321
+ }
322
+
323
+ export async function openClientPlayer(
324
+ player: LzcPlayer,
325
+ allInfos: VideoInfo[],
326
+ ) {
327
+ if (!(await initClientPlayerCapability())) {
328
+ return false
329
+ }
330
+ const playlist = buildPlaylist(player, allInfos)
331
+ if (!playlist) {
332
+ return false
333
+ }
334
+
335
+ try {
336
+ const session = await AppCommon.OpenNativeVideoPlayer(playlist.payload)
337
+ player.pause()
338
+ subscribePlayerEvents(
339
+ (listener) => session.subscribe(listener),
340
+ playlist.items,
341
+ playlist.index,
342
+ )
343
+ return true
344
+ } catch (error) {
345
+ console.warn("[client-player] open failed", error)
346
+ return false
347
+ }
348
+ }
@@ -0,0 +1,57 @@
1
+ <script lang="ts" setup>
2
+ defineProps<{
3
+ items: any[]
4
+ isActive: (item: any, index: number) => boolean
5
+ }>()
6
+ defineEmits<{
7
+ (e: "select", item: any): void
8
+ }>()
9
+ </script>
10
+
11
+ <template>
12
+ <div class="simple-list">
13
+ <div
14
+ v-for="(item, index) in items"
15
+ :key="index"
16
+ class="item"
17
+ :class="isActive(item, index) ? 'active' : ''"
18
+ @click="$emit('select', { item, index })"
19
+ >
20
+ <slot :data="{ item, index }"></slot>
21
+ </div>
22
+ </div>
23
+ </template>
24
+
25
+ <style lang="scss" scoped>
26
+ .simple-list {
27
+ background: rgba(0, 0, 0, 0.5);
28
+ backdrop-filter: blur(20px);
29
+ -webkit-backdrop-filter: blur(20px);
30
+ border-radius: 4px;
31
+ padding: 8px;
32
+ font-size: 14px;
33
+ line-height: 20px;
34
+ border: 1px solid rgba(255, 255, 255, 0.3);
35
+ overflow-y: auto;
36
+ max-height: 400px;
37
+ @media (orientation: landscape) and (height < 600px) {
38
+ max-height: 200px;
39
+ }
40
+ }
41
+
42
+ .item {
43
+ padding: 8px 16px;
44
+
45
+ &:not(:last-child) {
46
+ margin-bottom: 4px;
47
+ }
48
+
49
+ &:hover,
50
+ &.active {
51
+ background: #ffffff33;
52
+ cursor: pointer;
53
+ border-radius: 4px;
54
+ color: #5f86ff;
55
+ }
56
+ }
57
+ </style>
@@ -0,0 +1,52 @@
1
+ <script lang="ts" setup>
2
+ import type { VideoInfo } from "@/model"
3
+ import Playlist from "@/components/PlayList/index.vue"
4
+ import type { LzcPlayer } from "@/components/Video/player"
5
+ import { isSourceEqual } from "@/use/useUtils";
6
+
7
+ const props = defineProps<{ player: LzcPlayer }>()
8
+ function isPlaying(info: VideoInfo): boolean {
9
+ const currentInfo = props.player.currentVideoInfo()
10
+ if (currentInfo?.sourceUrl) {
11
+ try {
12
+ if (isSourceEqual(info.sourceUrl, currentInfo)) {
13
+ return true
14
+ }
15
+ } catch {
16
+ // Fallback comparison for non-standard URLs.
17
+ }
18
+ if (currentInfo.sourceUrl === info.sourceUrl) {
19
+ return true
20
+ }
21
+ if (currentInfo.path && info.path && currentInfo.path === info.path) {
22
+ return true
23
+ }
24
+ }
25
+
26
+ const currentSrc = props.player.currentSrc()
27
+ if (!currentSrc) {
28
+ return false
29
+ }
30
+ try {
31
+ return isSourceEqual(currentSrc, info)
32
+ } catch {
33
+ return currentSrc === info.sourceUrl
34
+ }
35
+ }
36
+ function onBack() {
37
+ props.player.trigger({ type: "back" })
38
+ }
39
+ function onOpen(info: VideoInfo) {
40
+ props.player.trigger({ type: "openVideo", info })
41
+ }
42
+ </script>
43
+
44
+ <template>
45
+ <Playlist
46
+ class="modal-list"
47
+ :player="player"
48
+ :isPlaying="isPlaying"
49
+ @back="onBack"
50
+ @openVideo="onOpen"
51
+ ></Playlist>
52
+ </template>
@@ -0,0 +1,45 @@
1
+ <script lang="ts" setup>
2
+ import { ref } from "vue"
3
+ import SimpleList from "./components/simpleList.vue"
4
+ import { t } from "@/i18n"
5
+ import type { LzcPlayer } from "@/components/Video/player"
6
+ const props = defineProps<{ player: LzcPlayer }>()
7
+ const rates = [3.0, 2.0, 1.5, 1.25, 1.0, 0.75]
8
+ props.player.playbackRates(rates)
9
+
10
+ // player 不能响应式更新, 所以使用一个内部变量,而不是使用 computed
11
+ const currentRate = ref(props.player.playbackRate())
12
+ const onSelect = ({ item }: { item: number }) => {
13
+ props.player.playbackRate(item)
14
+ const modal = props.player.lzcModal?.()
15
+ modal?.close()
16
+ }
17
+ const isActive = (item: number, _: number) => {
18
+ return item === currentRate.value
19
+ }
20
+ props.player.on("ratechange", function () {
21
+ currentRate.value = props.player.playbackRate()
22
+ props.player.trigger({
23
+ type: "tipToast",
24
+ // eslint-disable-next-line no-undef
25
+ toastValue: t(
26
+ "src.components.video.components.lzc_modal.play_rate.toast_playback_rate",
27
+ "{{rate}}倍速播放中",
28
+ { rate: currentRate.value },
29
+ ),
30
+ toastIcon: "svguse:#icon-倍速.svg",
31
+ })
32
+ })
33
+ </script>
34
+
35
+ <template>
36
+ <SimpleList class="playrate" :items="rates" :isActive="isActive" @select="onSelect">
37
+ <template #default="{ data }"> {{ data.item }} x </template>
38
+ </SimpleList>
39
+ </template>
40
+
41
+ <style lang="scss" scoped>
42
+ .playrate {
43
+ width: 140px;
44
+ }
45
+ </style>
@@ -0,0 +1,117 @@
1
+ <script lang="ts" setup>
2
+ import { computed, ref } from "vue"
3
+ import type { LzcPlayer } from "@/components/Video/player"
4
+ import SimpleList from "./components/simpleList.vue"
5
+ import type { VideoQualityLevel } from "@/model"
6
+ import { t } from "@/i18n"
7
+
8
+ const props = defineProps<{ player: LzcPlayer }>()
9
+ const qualityVersion = ref(0)
10
+
11
+ const getResolutions = (): VideoQualityLevel[] => {
12
+ let resolutions = props.player.supportResolution()
13
+ if (props.player.logicalQualityLevels) {
14
+ const logicalLevels = props.player.logicalQualityLevels()
15
+ if (logicalLevels.length > 0) {
16
+ resolutions = logicalLevels
17
+ }
18
+ }
19
+ if (resolutions.length < 1 || resolutions == undefined) {
20
+ console.error(
21
+ "failed to get support resolutions",
22
+ props.player.supportResolution(),
23
+ )
24
+ resolutions = props.player.qualityLevels().levels_ as VideoQualityLevel[]
25
+ }
26
+ return resolutions
27
+ }
28
+
29
+ const resolutions = computed(() => {
30
+ void qualityVersion.value
31
+ return getResolutions()
32
+ })
33
+
34
+ // player 不能响应式更新,所以使用一个内部变量,而不是使用 computed
35
+ const currentRes = ref(props.player.currentResolution())
36
+
37
+ props.player.qualityLevels().on("change", function () {
38
+ qualityVersion.value += 1
39
+ currentRes.value = props.player.currentResolution()
40
+ if (currentRes.value == undefined) {
41
+ return
42
+ }
43
+ const tips = currentRes.value?.origin
44
+ ? t("src.components.video.components.lzc_modal.resolution.raw", "原始画质")
45
+ : `${currentRes.value?.res}P`
46
+ props.player.trigger({
47
+ type: "tipToast",
48
+ toastValue: t(
49
+ "src.components.video.components.lzc_modal.resolution.switched_to",
50
+ "已成功切换至 {{tips}} 清晰度",
51
+ { tips: tips },
52
+ ),
53
+ toastIcon: "",
54
+ })
55
+ })
56
+
57
+ props.player.qualityLevels().on("addqualitylevel", function () {
58
+ qualityVersion.value += 1
59
+ })
60
+
61
+ const onSelect = ({ item }: { item: VideoQualityLevel }) => {
62
+ const res = ref(props.player.currentResolution())
63
+ const modal = props.player.lzcModal?.()
64
+ res.value = props.player.currentResolution()
65
+
66
+ // 点击当前已选清晰度:只关闭弹框,不提示“已成功切换”。
67
+ if (res.value?.id === item.id) {
68
+ modal?.close()
69
+ return
70
+ }
71
+
72
+ props.player.changeResolution(item)
73
+ modal?.close()
74
+ props.player.trigger({
75
+ type: "tipToast",
76
+ toastValue: t(
77
+ "src.components.video.components.lzc_modal.resolution.switching",
78
+ "切换中,请稍等",
79
+ ),
80
+ toastIcon: "",
81
+ timeout: 5000,
82
+ //uncloseable: true,
83
+ })
84
+ }
85
+
86
+ const isActive = (item: VideoQualityLevel, index: number) => {
87
+ if (currentRes.value?.auto) {
88
+ return item.id == "auto"
89
+ } else if (currentRes.value?.origin) {
90
+ return item.label === "origin"
91
+ } else {
92
+ return item.id === currentRes.value?.id
93
+ }
94
+ }
95
+
96
+ const text = (item: VideoQualityLevel) => {
97
+ if (item.label === "origin") {
98
+ return t(
99
+ "src.components.video.components.lzc_modal.resolution.text_raw",
100
+ "原始画质",
101
+ )
102
+ }
103
+ if (item.id === "auto") {
104
+ return t(
105
+ "src.components.video.components.lzc_modal.resolution.auto",
106
+ "自动",
107
+ )
108
+ }
109
+ return item.label || `${Math.min(item.height, item.width)} P`
110
+ }
111
+ </script>
112
+
113
+ <template>
114
+ <SimpleList :items="resolutions" :isActive="isActive" @select="onSelect">
115
+ <template #default="{ data }"> {{ text(data.item) }} </template>
116
+ </SimpleList>
117
+ </template>