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.
- package/.dockerignore +1 -0
- package/.eslintrc.cjs +18 -0
- package/.prettierrc.json +5 -0
- package/AGENTS.md +31 -0
- package/README.md +38 -0
- package/build.sh +10 -0
- package/demo/.vscode/extensions.json +3 -0
- package/demo/README.md +40 -0
- package/demo/env.d.ts +1 -0
- package/demo/index.html +13 -0
- package/demo/package-lock.json +2037 -0
- package/demo/package.json +25 -0
- package/demo/public/favicon.ico +0 -0
- package/demo/src/App.vue +25 -0
- package/demo/src/assets/base.css +70 -0
- package/demo/src/assets/logo.svg +1 -0
- package/demo/src/assets/main.css +33 -0
- package/demo/src/main.ts +8 -0
- package/demo/tsconfig.config.json +8 -0
- package/demo/tsconfig.json +16 -0
- package/demo/vite.config.ts +14 -0
- package/docs/progress-bar-style-analysis.md +87 -0
- package/env.d.ts +1 -0
- package/error_pages/502.html.tpl +13 -0
- package/i18next-parser.config.mjs +147 -0
- package/index.html +54 -0
- package/lazycat.png +0 -0
- package/lib/README.md +48 -0
- package/lib/package.json +22 -0
- package/lzc-build.local.yml +65 -0
- package/lzc-build.yml +65 -0
- package/lzc-manifest.yml +53 -0
- package/makefile +15 -0
- package/package.json +69 -0
- package/postcss.config.js +6 -0
- package/public/512x512.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/languages/en/translation.json +125 -0
- package/public/languages/zh/translation.json +125 -0
- package/public/libass-wasm/4.1.0/default.woff2 +0 -0
- package/public/libass-wasm/4.1.0/subtitles-octopus-worker-legacy.js +40 -0
- package/public/libass-wasm/4.1.0/subtitles-octopus-worker.js +1 -0
- package/public/libass-wasm/4.1.0/subtitles-octopus-worker.wasm +0 -0
- package/public/libass-wasm/4.1.0/subtitles-octopus.js +1680 -0
- package/public/square-128x128.png +0 -0
- package/public/square-256x256.png +0 -0
- package/public/square-512x512.png +0 -0
- package/src/App.vue +18 -0
- package/src/assets/base.scss +104 -0
- package/src/assets/cloud.png +0 -0
- package/src/assets/logo.svg +1 -0
- package/src/components/Dialog/index.vue +96 -0
- package/src/components/MultipleEdit/choose.vue +39 -0
- package/src/components/PlayList/index.vue +521 -0
- package/src/components/Spectrum/index.vue +58 -0
- package/src/components/Video/NativeVideoPlayer.vue +748 -0
- package/src/components/Video/README.md +3 -0
- package/src/components/Video/clientPlayer.ts +348 -0
- package/src/components/Video/components/LzcModal/components/simpleList.vue +57 -0
- package/src/components/Video/components/LzcModal/list.vue +52 -0
- package/src/components/Video/components/LzcModal/playrate.vue +45 -0
- package/src/components/Video/components/LzcModal/resolution.vue +117 -0
- package/src/components/Video/components/LzcModal/subtitle.vue +499 -0
- package/src/components/Video/components/LzcModal/useModal.ts +18 -0
- package/src/components/Video/components/LzcOverlay/SubtitleLayer.vue +321 -0
- package/src/components/Video/components/LzcOverlay/cast.vue +253 -0
- package/src/components/Video/components/LzcOverlay/casting.vue +205 -0
- package/src/components/Video/components/LzcOverlay/error.vue +103 -0
- package/src/components/Video/components/LzcOverlay/helper.ts +81 -0
- package/src/components/Video/components/LzcOverlay/index.vue +99 -0
- package/src/components/Video/components/LzcOverlay/playing.vue +496 -0
- package/src/components/Video/components/LzcOverlay/playingButtons.vue +122 -0
- package/src/components/Video/components/LzcOverlay/playingLayout.vue +287 -0
- package/src/components/Video/components/LzcOverlay/useCast.ts +235 -0
- package/src/components/Video/components/LzcOverlay/useCommon.ts +41 -0
- package/src/components/Video/components/LzcOverlay/useOctopusRenderer.ts +230 -0
- package/src/components/Video/components/LzcOverlay/useSubtitleRenderEngine.ts +79 -0
- package/src/components/Video/components/LzcOverlay/useSubtitleTrack.ts +139 -0
- package/src/components/Video/components/useLzcCommon.ts +16 -0
- package/src/components/Video/directPlay.ts +345 -0
- package/src/components/Video/getSubtitleInfo.ts +42 -0
- package/src/components/Video/native/EventEmitter.ts +62 -0
- package/src/components/Video/native/NativeControls.vue +510 -0
- package/src/components/Video/native/NativeModal.vue +133 -0
- package/src/components/Video/native/NativePlayer.ts +913 -0
- package/src/components/Video/native/NativePlayer.vue +53 -0
- package/src/components/Video/native/index.ts +9 -0
- package/src/components/Video/native/native-player.css +183 -0
- package/src/components/Video/native/playerKey.ts +5 -0
- package/src/components/Video/native/useNativeCastMiddleware.ts +50 -0
- package/src/components/Video/native/useNativePlayer.ts +3 -0
- package/src/components/Video/native/useNativePlayerFullscreen.ts +44 -0
- package/src/components/Video/native/useNativePlayerHistory.ts +69 -0
- package/src/components/Video/native/useNativePlayerModal.ts +68 -0
- package/src/components/Video/native/useNativePlayerPlaylist.ts +67 -0
- package/src/components/Video/native/useNativePlayerState.ts +225 -0
- package/src/components/Video/player.ts +99 -0
- package/src/components/Video/theme/index.scss +291 -0
- package/src/components/Video/theme/videojs.css +1797 -0
- package/src/components/Video/useSource.ts +1431 -0
- package/src/components/Video/useSubtitlePreference.ts +66 -0
- package/src/components/Video/useWebview.ts +79 -0
- package/src/components/Video/videoFrame.ts +58 -0
- package/src/env.d.ts +3 -0
- package/src/i18n/README.md +392 -0
- package/src/i18n/index.ts +49 -0
- package/src/icons/Video_Player.svg +69 -0
- package/src/icons/box.svg +15 -0
- package/src/icons/client.svg +17 -0
- package/src/icons/logo.svg +28 -0
- package/src/icons//344/270/212/344/270/200/344/270/252.svg +6 -0
- package/src/icons//344/270/213/344/270/200/344/270/252.svg +4 -0
- package/src/icons//344/272/256/345/272/246.svg +13 -0
- package/src/icons//345/200/215/351/200/237.svg +14 -0
- package/src/icons//345/205/250/345/261/217.svg +16 -0
- package/src/icons//345/205/250/351/200/211_/345/267/262/351/200/211/344/270/255.svg +16 -0
- package/src/icons//345/205/250/351/200/211_/346/234/252/351/200/211/344/270/255.svg +15 -0
- package/src/icons//345/205/263/351/227/255/345/244/232/351/200/211.svg +14 -0
- package/src/icons//345/205/263/351/227/255/346/212/225/345/261/217.svg +11 -0
- package/src/icons//345/233/236/346/224/266/347/253/231.svg +15 -0
- package/src/icons//345/244/261/346/225/210.svg +17 -0
- package/src/icons//346/207/222/347/214/253/346/222/255/346/224/276/345/231/250-icon.png +0 -0
- package/src/icons//346/207/222/347/214/253/346/222/255/346/224/276/345/231/250.png +0 -0
- package/src/icons//346/212/225/345/261/217.svg +11 -0
- package/src/icons//346/212/225/351/200/201/344/270/255.jpg +0 -0
- package/src/icons//346/212/225/351/200/201/344/270/255.svg +21 -0
- package/src/icons//346/222/255/346/224/276.svg +3 -0
- package/src/icons//346/232/202/345/201/234.svg +4 -0
- package/src/icons//346/232/202/346/227/240.svg +21 -0
- package/src/icons//346/233/264/345/244/232/346/223/215/344/275/234.svg +11 -0
- package/src/icons//347/224/265/350/247/206.svg +18 -0
- package/src/icons//347/247/273/345/212/250/347/253/257_/350/203/214/346/231/257.webp +0 -0
- package/src/icons//350/203/214/346/231/257.png +0 -0
- package/src/icons//350/277/224/345/233/236.svg +13 -0
- package/src/icons//350/277/233/345/205/245/345/205/250/345/261/217.svg +13 -0
- package/src/icons//351/200/200/345/207/272/345/205/250/345/261/217.svg +15 -0
- package/src/icons//351/200/211/346/213/251.svg +15 -0
- package/src/icons//351/237/263/351/207/217.svg +13 -0
- package/src/index.d.ts +9 -0
- package/src/lzc-video-player.scss +7 -0
- package/src/lzc-video-player.ts +6 -0
- package/src/main.ts +62 -0
- package/src/model.ts +77 -0
- package/src/quasar-variables.sass +10 -0
- package/src/router/index.ts +74 -0
- package/src/stores/pinia.ts +3 -0
- package/src/stores/playlist.ts +146 -0
- package/src/use/useKeyBind.ts +61 -0
- package/src/use/useMultipleEdit.ts +60 -0
- package/src/use/useSdk.ts +5 -0
- package/src/use/useSubtitle.ts +39 -0
- package/src/use/useUtils.ts +22 -0
- package/src/use/useVideoFrame.ts +60 -0
- package/src/views/Home.ts +99 -0
- package/src/views/mobile/Home.vue +246 -0
- package/src/views/mobile/Player.vue +141 -0
- package/tailwind.config.js +15 -0
- package/tsconfig.config.json +8 -0
- package/tsconfig.json +20 -0
- package/vite.config.lib.ts +88 -0
- package/vite.config.ts +122 -0
- package/vue-shim.d.ts +4 -0
|
@@ -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>
|