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,1431 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Subtitle,
|
|
3
|
+
VideoInfo,
|
|
4
|
+
VideoQualityLevel,
|
|
5
|
+
VideoResolution,
|
|
6
|
+
} from "@/model"
|
|
7
|
+
import { ref } from "vue"
|
|
8
|
+
import throttle from "lodash.throttle"
|
|
9
|
+
import { getFileLinkWithDefault } from "@/use/useVideoFrame"
|
|
10
|
+
import { LzcApp } from "@/use/useSdk"
|
|
11
|
+
import { getSubtitleInfo } from "./getSubtitleInfo"
|
|
12
|
+
import { join } from "path-browserify"
|
|
13
|
+
import type { LzcPlayer, SourceObject } from "./player"
|
|
14
|
+
import { subtitleDB } from "@/use/useSubtitle"
|
|
15
|
+
import { useHistoryInfo } from "@/stores/playlist"
|
|
16
|
+
import { isSourceEqual } from "@/use/useUtils"
|
|
17
|
+
import {
|
|
18
|
+
fetchOriginPlaybackDecision,
|
|
19
|
+
fetchOriginHlsLevels,
|
|
20
|
+
type OriginPlaybackDecision,
|
|
21
|
+
} from "./directPlay"
|
|
22
|
+
|
|
23
|
+
// 后端定义好的参数
|
|
24
|
+
const previewConfig = {
|
|
25
|
+
width: 120,
|
|
26
|
+
height: 96,
|
|
27
|
+
interval: 5, // 缩略图的时间间隔
|
|
28
|
+
tile: 4, // 缩略图的个数 16, tile=4x4
|
|
29
|
+
len: 16,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const LEGACY_SUBTITLE_TYPE_PARAM = "sub_type"
|
|
33
|
+
const LEGACY_SUBTITLE_INDEX_PARAM = "subtitle"
|
|
34
|
+
const LEGACY_SUBTITLE_CUSTOM_PARAM = "custom_sub"
|
|
35
|
+
|
|
36
|
+
export function useVideoPreview(baseUrlPrefix: string, player: LzcPlayer) {
|
|
37
|
+
const cache: Map<string, HTMLImageElement> = new Map()
|
|
38
|
+
let loading: boolean = false
|
|
39
|
+
|
|
40
|
+
function currentPage() {
|
|
41
|
+
return Math.trunc(
|
|
42
|
+
player.currentTime()! / (previewConfig.len * previewConfig.interval),
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function genPreviewUrl(info: VideoInfo, page = 0): string | undefined {
|
|
47
|
+
const pageStart = page * previewConfig.len * previewConfig.interval
|
|
48
|
+
const filename = info.path
|
|
49
|
+
if (filename == undefined) {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
const uri = `${baseUrlPrefix}/screenshot/${encodeURIComponent(
|
|
53
|
+
filename,
|
|
54
|
+
)}?start=${pageStart}`
|
|
55
|
+
uri.replace(/\/\//, "/")
|
|
56
|
+
return uri
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function _loadPreview(page: number): any {
|
|
60
|
+
if (loading) {
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
const totalPage = Math.ceil(
|
|
64
|
+
player.duration()! / (previewConfig.len * previewConfig.interval),
|
|
65
|
+
)
|
|
66
|
+
if (page > totalPage) {
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const info = player.currentVideoInfo()
|
|
71
|
+
if (!info) {
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
if (!info.fromNetdisk && !info.path) {
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const url = genPreviewUrl(info, page)
|
|
79
|
+
if (url == undefined) {
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (cache.has(url)) {
|
|
84
|
+
return _loadPreview(page + 1)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const img = new Image()
|
|
88
|
+
img.src = url
|
|
89
|
+
|
|
90
|
+
loading = true
|
|
91
|
+
img.onload = () => {
|
|
92
|
+
cache.set(url, img)
|
|
93
|
+
loading = false
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function loadPreview() {
|
|
98
|
+
const page = currentPage()
|
|
99
|
+
return _loadPreview(page)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getPreview() {
|
|
103
|
+
const page = currentPage()
|
|
104
|
+
const info = player.currentVideoInfo()
|
|
105
|
+
if (!info) {
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
if (!info.fromNetdisk && !info.path) {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
const url = genPreviewUrl(info, page)
|
|
112
|
+
if (url == undefined) {
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
return cache.get(url)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { loadPreview, getPreview, currentPage }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface UseSourceInternalOptions {
|
|
122
|
+
externalPrevNextControl?: {
|
|
123
|
+
prev?: boolean
|
|
124
|
+
next?: boolean
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function useSource(
|
|
129
|
+
player: LzcPlayer,
|
|
130
|
+
options: UseSourceInternalOptions = {},
|
|
131
|
+
) {
|
|
132
|
+
const isDirectPlayerMode = player.options_.playMode === "direct"
|
|
133
|
+
const store = useHistoryInfo()
|
|
134
|
+
const externalPrevNextControl = {
|
|
135
|
+
prev: options.externalPrevNextControl?.prev === true,
|
|
136
|
+
next: options.externalPrevNextControl?.next === true,
|
|
137
|
+
}
|
|
138
|
+
let openVideoSeq = 0
|
|
139
|
+
let loadedMetadataSeekSeq = 0
|
|
140
|
+
const safePlay = () => {
|
|
141
|
+
void Promise.resolve(player.play()).catch((err: any) => {
|
|
142
|
+
if (err?.name !== "AbortError") {
|
|
143
|
+
console.error("play failed after source update", err)
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
const invalidateLoadedMetadataSeek = () => {
|
|
148
|
+
loadedMetadataSeekSeq += 1
|
|
149
|
+
}
|
|
150
|
+
const isSameVideo = (a?: VideoInfo | null, b?: VideoInfo | null) => {
|
|
151
|
+
if (!a || !b) return false
|
|
152
|
+
if (a.path && b.path) {
|
|
153
|
+
return a.path === b.path
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
return isSourceEqual(a.sourceUrl, b)
|
|
157
|
+
} catch {
|
|
158
|
+
return a.sourceUrl === b.sourceUrl
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const mergeHistoryInfo = (
|
|
162
|
+
incoming: VideoInfo,
|
|
163
|
+
history?: VideoInfo | null,
|
|
164
|
+
): VideoInfo => {
|
|
165
|
+
if (!history) {
|
|
166
|
+
return incoming
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
...history,
|
|
170
|
+
...incoming,
|
|
171
|
+
_id: history._id,
|
|
172
|
+
updateTime: history.updateTime,
|
|
173
|
+
currentTime:
|
|
174
|
+
typeof incoming.currentTime === "number" && incoming.currentTime > 0
|
|
175
|
+
? incoming.currentTime
|
|
176
|
+
: history.currentTime,
|
|
177
|
+
duration:
|
|
178
|
+
typeof incoming.duration === "number" && incoming.duration > 0
|
|
179
|
+
? incoming.duration
|
|
180
|
+
: history.duration,
|
|
181
|
+
invalid: history.invalid,
|
|
182
|
+
subtitles: history.subtitles ?? incoming.subtitles,
|
|
183
|
+
beforeHiddenSubtitle:
|
|
184
|
+
history.beforeHiddenSubtitle ?? incoming.beforeHiddenSubtitle,
|
|
185
|
+
path: incoming.path || history.path,
|
|
186
|
+
fromNetdisk: incoming.fromNetdisk || history.fromNetdisk,
|
|
187
|
+
name: incoming.name || history.name,
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const scheduleLoadedMetadataSeek = (
|
|
191
|
+
seekTo: number,
|
|
192
|
+
expectedInfo?: VideoInfo | null,
|
|
193
|
+
afterLoadedMetadata?: () => void,
|
|
194
|
+
) => {
|
|
195
|
+
invalidateLoadedMetadataSeek()
|
|
196
|
+
const seq = loadedMetadataSeekSeq
|
|
197
|
+
player.one("loadedmetadata", () => {
|
|
198
|
+
if (seq !== loadedMetadataSeekSeq) return
|
|
199
|
+
if (expectedInfo) {
|
|
200
|
+
const currentInfo = player.currentVideoInfo?.()
|
|
201
|
+
if (!isSameVideo(currentInfo, expectedInfo)) {
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
player.currentTime(seekTo)
|
|
206
|
+
afterLoadedMetadata?.()
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
const normalizeQualityToMaster = (url: string) => {
|
|
210
|
+
if (!/\.m3u8(\?|$)/i.test(url)) return url
|
|
211
|
+
const [base, search] = url.split("?")
|
|
212
|
+
const replaced = base.replace(/\/quality-[^/]*\.m3u8$/i, "/master.m3u8")
|
|
213
|
+
if (!search) return replaced
|
|
214
|
+
return `${replaced}?${search}`
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let videoDisplayName: string = ""
|
|
218
|
+
let autoSwitchResolution: boolean = false
|
|
219
|
+
let subtitleLoading = false
|
|
220
|
+
let baseUrlPrefix = "/_lzc/media"
|
|
221
|
+
if (player.options_.mediaPrefix) {
|
|
222
|
+
if (player.options_.mediaPrefix.endsWith("/")) {
|
|
223
|
+
baseUrlPrefix = player.options_.mediaPrefix.substring(
|
|
224
|
+
0,
|
|
225
|
+
player.options_.mediaPrefix.length - 1,
|
|
226
|
+
)
|
|
227
|
+
} else {
|
|
228
|
+
baseUrlPrefix = player.options_.mediaPrefix
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const previewImageCache = useVideoPreview(baseUrlPrefix, player)
|
|
233
|
+
const qualityLevels = player.qualityLevels()
|
|
234
|
+
|
|
235
|
+
function autoEnableAndDisableResolution(
|
|
236
|
+
index?: number,
|
|
237
|
+
options: { trigger?: boolean } = {},
|
|
238
|
+
) {
|
|
239
|
+
if (index !== undefined) {
|
|
240
|
+
autoSwitchResolution = false
|
|
241
|
+
} else {
|
|
242
|
+
// autoSwitchResolution = true;
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
for (let i = 0; i < qualityLevels.levels_.length; i++) {
|
|
246
|
+
if (autoSwitchResolution) {
|
|
247
|
+
qualityLevels.levels_[i].enabled = true
|
|
248
|
+
} else {
|
|
249
|
+
if (i == index) {
|
|
250
|
+
qualityLevels.levels_[i].enabled = true
|
|
251
|
+
} else {
|
|
252
|
+
qualityLevels.levels_[i].enabled = false
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (options.trigger !== false) {
|
|
257
|
+
qualityLevels.trigger({ type: "change", selectedIndex: index })
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// 注意 player 中的函数重新保存到其他变量上需要 bind 函数的上下文
|
|
262
|
+
function init(info: VideoInfo) {
|
|
263
|
+
getFileLinkWithDefault(info._id).then((link) => {
|
|
264
|
+
player.poster(link)
|
|
265
|
+
})
|
|
266
|
+
const normalizedSrc =
|
|
267
|
+
isDirectPlayerMode
|
|
268
|
+
? (info.resolvedSourceUrl || "").trim() ||
|
|
269
|
+
normalizeQualityToMaster(info.sourceUrl)
|
|
270
|
+
: (info.resolvedSourceUrl || "").trim() ||
|
|
271
|
+
(originDecision?.originMode === "direct"
|
|
272
|
+
? originDecision.originDirectUrl
|
|
273
|
+
: originDecision?.originHlsUrl) ||
|
|
274
|
+
normalizeQualityToMaster(info.sourceUrl)
|
|
275
|
+
currentTransportMode = /\.m3u8(\?|$)/i.test(normalizedSrc)
|
|
276
|
+
? "hls"
|
|
277
|
+
: "direct"
|
|
278
|
+
restorePlaybackRateAfterSourceReload()
|
|
279
|
+
player.src(normalizedSrc)
|
|
280
|
+
|
|
281
|
+
store.ready.then(() => {
|
|
282
|
+
const currentIndex = store.infos.findIndex((i: VideoInfo) =>
|
|
283
|
+
isSourceEqual(i.sourceUrl, info),
|
|
284
|
+
)
|
|
285
|
+
const total = store.infos.length
|
|
286
|
+
if (!externalPrevNextControl.prev) {
|
|
287
|
+
player.enablePrev(currentIndex !== 0)
|
|
288
|
+
}
|
|
289
|
+
if (!externalPrevNextControl.next) {
|
|
290
|
+
player.enableNext(currentIndex !== total - 1)
|
|
291
|
+
}
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
if (info.name) {
|
|
295
|
+
videoDisplayName = info.name
|
|
296
|
+
}
|
|
297
|
+
if (info.fromNetdisk && !LzcApp.isTvOsWebShell()) {
|
|
298
|
+
player.on(
|
|
299
|
+
"timeupdate",
|
|
300
|
+
throttle(() => {
|
|
301
|
+
// HaveEnoughData = 4, Idle = 1, Loading = 2
|
|
302
|
+
if (player.networkState() == 1 || player.networkState() == 2) {
|
|
303
|
+
previewImageCache.loadPreview()
|
|
304
|
+
}
|
|
305
|
+
}, 2000),
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function videoSrc() {
|
|
311
|
+
const src = player.currentSrc()
|
|
312
|
+
return src.split("?")[0]
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function videoName() {
|
|
316
|
+
if (videoDisplayName !== "") {
|
|
317
|
+
return videoDisplayName
|
|
318
|
+
}
|
|
319
|
+
const currSrc = videoSrc()
|
|
320
|
+
return currSrc?.split("/").slice(-1)[0]
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function currentPreview() {
|
|
324
|
+
const image = previewImageCache.getPreview()
|
|
325
|
+
if (!image) {
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
const page = previewImageCache.currentPage()
|
|
329
|
+
return {
|
|
330
|
+
...previewConfig,
|
|
331
|
+
page,
|
|
332
|
+
image,
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const defaultAvaliableSub = ref<Subtitle | undefined>()
|
|
337
|
+
let subtitleHidden = false
|
|
338
|
+
let lastVisibleSubtitle: Subtitle | undefined = undefined
|
|
339
|
+
let netdiskPath: string | undefined = undefined
|
|
340
|
+
let resolvedPlayableSource: string | undefined = undefined
|
|
341
|
+
let subtitleSourceOverride: string | undefined = undefined
|
|
342
|
+
let resolutionDisplayLock: VideoResolution | null = null
|
|
343
|
+
let resolutionDisplayLockTimer: ReturnType<typeof setTimeout> | null = null
|
|
344
|
+
let originDecision: OriginPlaybackDecision | undefined = undefined
|
|
345
|
+
let currentTransportMode: "hls" | "direct" = "hls"
|
|
346
|
+
let logicalResolutionLevels: VideoQualityLevel[] = []
|
|
347
|
+
let manifestResolutionLevels: VideoQualityLevel[] = []
|
|
348
|
+
let directOriginFallbackPending = false
|
|
349
|
+
|
|
350
|
+
const setSubtitleLoading = (value: boolean) => {
|
|
351
|
+
if (subtitleLoading === value) {
|
|
352
|
+
return
|
|
353
|
+
}
|
|
354
|
+
subtitleLoading = value
|
|
355
|
+
player.trigger("subtitleloadingchange")
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
player.isSubtitleLoading = () => subtitleLoading
|
|
359
|
+
player.logicalQualityLevels = () => logicalResolutionLevels
|
|
360
|
+
|
|
361
|
+
const refreshLogicalResolutionLevels = () => {
|
|
362
|
+
if (isDirectPlayerMode) {
|
|
363
|
+
logicalResolutionLevels = []
|
|
364
|
+
return
|
|
365
|
+
}
|
|
366
|
+
const rawLevels =
|
|
367
|
+
qualityLevels.levels_.length > 0 ? qualityLevels.levels_ : manifestResolutionLevels
|
|
368
|
+
|
|
369
|
+
const hlsLevels: VideoQualityLevel[] = [...rawLevels]
|
|
370
|
+
.map((level) => ({
|
|
371
|
+
...level,
|
|
372
|
+
transport: "hls" as "hls" | "direct",
|
|
373
|
+
}))
|
|
374
|
+
.sort((a, b) => {
|
|
375
|
+
if (b.height === a.height) {
|
|
376
|
+
return b.bitrate - a.bitrate
|
|
377
|
+
}
|
|
378
|
+
return b.height - a.height
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
if (originDecision) {
|
|
382
|
+
const hasOrigin = hlsLevels.some((level) => level.label === "origin")
|
|
383
|
+
if (!hasOrigin) {
|
|
384
|
+
hlsLevels.unshift({
|
|
385
|
+
id: "origin",
|
|
386
|
+
label: "origin",
|
|
387
|
+
height: originDecision.mediaSource.height || 0,
|
|
388
|
+
width: originDecision.mediaSource.width || 0,
|
|
389
|
+
bitrate: originDecision.mediaSource.bitrate || 0,
|
|
390
|
+
transport: originDecision.originMode,
|
|
391
|
+
})
|
|
392
|
+
} else {
|
|
393
|
+
const originLevel = hlsLevels.find((level) => level.label === "origin")
|
|
394
|
+
if (originLevel) {
|
|
395
|
+
originLevel.transport = originDecision.originMode
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
logicalResolutionLevels = hlsLevels
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const syncManifestResolutionLevels = (levels: VideoQualityLevel[]) => {
|
|
403
|
+
manifestResolutionLevels = levels.map((level, index) => ({
|
|
404
|
+
...level,
|
|
405
|
+
id: `manifest-${level.label || "level"}-${level.height}-${level.width}-${level.bitrate}-${index}`,
|
|
406
|
+
enabled: level.enabled ?? true,
|
|
407
|
+
}))
|
|
408
|
+
refreshLogicalResolutionLevels()
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const clearResolutionDisplayLockTimer = () => {
|
|
412
|
+
if (resolutionDisplayLockTimer) {
|
|
413
|
+
clearTimeout(resolutionDisplayLockTimer)
|
|
414
|
+
resolutionDisplayLockTimer = null
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const setResolutionDisplayLock = (snapshot?: VideoResolution) => {
|
|
419
|
+
clearResolutionDisplayLockTimer()
|
|
420
|
+
if (!snapshot) {
|
|
421
|
+
resolutionDisplayLock = null
|
|
422
|
+
return
|
|
423
|
+
}
|
|
424
|
+
resolutionDisplayLock = { ...snapshot }
|
|
425
|
+
// Fallback unlock to avoid stale lock if source reload fails unexpectedly.
|
|
426
|
+
resolutionDisplayLockTimer = setTimeout(() => {
|
|
427
|
+
resolutionDisplayLock = null
|
|
428
|
+
resolutionDisplayLockTimer = null
|
|
429
|
+
}, 8000)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const releaseResolutionDisplayLock = () => {
|
|
433
|
+
clearResolutionDisplayLockTimer()
|
|
434
|
+
resolutionDisplayLock = null
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const buildSubtitleInfoUrl = (
|
|
438
|
+
path: string | undefined,
|
|
439
|
+
search = "",
|
|
440
|
+
): string | undefined => {
|
|
441
|
+
if (!path) {
|
|
442
|
+
return undefined
|
|
443
|
+
}
|
|
444
|
+
const base = join(baseUrlPrefix, "hls", path, "subtitle-info")
|
|
445
|
+
if (!search) {
|
|
446
|
+
return base
|
|
447
|
+
}
|
|
448
|
+
return `${base}${search.startsWith("?") ? search : `?${search}`}`
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const escapeRegExp = (value: string) => {
|
|
452
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const normalizeSubtitleMediaUrl = (url?: string): string | undefined => {
|
|
456
|
+
const raw = (url || "").trim()
|
|
457
|
+
if (!raw) {
|
|
458
|
+
return raw
|
|
459
|
+
}
|
|
460
|
+
if (/^(https?:)?\/\//i.test(raw) || /^(blob:|data:)/i.test(raw)) {
|
|
461
|
+
return raw
|
|
462
|
+
}
|
|
463
|
+
const mediaHlsPrefix = `${baseUrlPrefix}/hls/`
|
|
464
|
+
if (raw.startsWith(mediaHlsPrefix)) {
|
|
465
|
+
return raw
|
|
466
|
+
}
|
|
467
|
+
if (raw.startsWith("/hls/")) {
|
|
468
|
+
return `${baseUrlPrefix}${raw}`
|
|
469
|
+
}
|
|
470
|
+
if (raw.startsWith("hls/")) {
|
|
471
|
+
return `${baseUrlPrefix}/${raw}`
|
|
472
|
+
}
|
|
473
|
+
return raw
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const normalizeSubtitle = (subtitle: Subtitle): Subtitle => {
|
|
477
|
+
return {
|
|
478
|
+
...subtitle,
|
|
479
|
+
vtt_url: normalizeSubtitleMediaUrl(subtitle.vtt_url),
|
|
480
|
+
ass_url: normalizeSubtitleMediaUrl(subtitle.ass_url),
|
|
481
|
+
ass_fonts: (subtitle.ass_fonts || [])
|
|
482
|
+
.map((item) => normalizeSubtitleMediaUrl(item))
|
|
483
|
+
.filter((item): item is string => !!item),
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const hasRenderableSubtitleSource = (subtitle?: Subtitle): boolean => {
|
|
488
|
+
return !!((subtitle?.vtt_url || subtitle?.ass_url || "").trim())
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const isSelectableSubtitle = (subtitle?: Subtitle): subtitle is Subtitle => {
|
|
492
|
+
if (!subtitle) {
|
|
493
|
+
return false
|
|
494
|
+
}
|
|
495
|
+
return (
|
|
496
|
+
hasRenderableSubtitleSource(subtitle) ||
|
|
497
|
+
canUseLegacySubtitleProtocol(subtitle)
|
|
498
|
+
)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const canUseLegacySubtitleProtocol = (subtitle?: Subtitle): boolean => {
|
|
502
|
+
if (!subtitle) {
|
|
503
|
+
return false
|
|
504
|
+
}
|
|
505
|
+
if (hasRenderableSubtitleSource(subtitle)) {
|
|
506
|
+
return false
|
|
507
|
+
}
|
|
508
|
+
return Number.isInteger(subtitle.stream_index) && subtitle.stream_index >= 0
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const getLegacySubtitleTypeValue = (subtitle: Subtitle): string => {
|
|
512
|
+
// media legacy protocol: 1 => internal, 2 => external
|
|
513
|
+
return subtitle.is_external ? "2" : "1"
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const getLegacyProbeSubtitle = () => {
|
|
517
|
+
if (subtitleHidden) {
|
|
518
|
+
return lastVisibleSubtitle ?? defaultAvaliableSub.value
|
|
519
|
+
}
|
|
520
|
+
return defaultAvaliableSub.value
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
player.isUsingNativeSubtitleFallback = () => {
|
|
524
|
+
return canUseLegacySubtitleProtocol(getLegacyProbeSubtitle())
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* 修改字幕初始化时间,防止changeSubtitle中loadedmetadata在配置未加载前改变播放时间
|
|
529
|
+
*/
|
|
530
|
+
function initSubtitles() {
|
|
531
|
+
const seq = openVideoSeq
|
|
532
|
+
const info = player.currentVideoInfo()
|
|
533
|
+
if (!info) {
|
|
534
|
+
setSubtitleLoading(false)
|
|
535
|
+
return
|
|
536
|
+
}
|
|
537
|
+
let SubtitlePath: string | undefined = undefined
|
|
538
|
+
|
|
539
|
+
if (info.subtitleInfoUrl) {
|
|
540
|
+
SubtitlePath = info.subtitleInfoUrl
|
|
541
|
+
if (info.path) {
|
|
542
|
+
netdiskPath = info.path
|
|
543
|
+
if (netdiskPath?.startsWith("/")) {
|
|
544
|
+
netdiskPath = encodeURIComponent(netdiskPath.substring(1))
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
} else if (info.path) {
|
|
548
|
+
netdiskPath = info.path
|
|
549
|
+
if (netdiskPath?.startsWith("/")) {
|
|
550
|
+
netdiskPath = encodeURIComponent(netdiskPath.substring(1))
|
|
551
|
+
}
|
|
552
|
+
SubtitlePath = buildSubtitleInfoUrl(netdiskPath)
|
|
553
|
+
} else {
|
|
554
|
+
const originPath = new URL(info.sourceUrl)
|
|
555
|
+
const url = originPath.origin + originPath.pathname
|
|
556
|
+
const searchPath = originPath.search || ""
|
|
557
|
+
const mediaHlsPrefixPattern = new RegExp(
|
|
558
|
+
`^https?:\\/\\/[^/]+${escapeRegExp(`${baseUrlPrefix}/hls`)}`,
|
|
559
|
+
)
|
|
560
|
+
netdiskPath = decodeURIComponent(url)
|
|
561
|
+
.replace(mediaHlsPrefixPattern, "")
|
|
562
|
+
.replace(/\/[^/]*\.m3u8$/, "")
|
|
563
|
+
if (netdiskPath?.startsWith("/")) {
|
|
564
|
+
netdiskPath = encodeURIComponent(netdiskPath.substring(1))
|
|
565
|
+
}
|
|
566
|
+
SubtitlePath = buildSubtitleInfoUrl(netdiskPath, searchPath)
|
|
567
|
+
}
|
|
568
|
+
const subtitleHistory = subtitleDB.findOne(netdiskPath || "")
|
|
569
|
+
const lastSelectedSubtitle = subtitleDB.findLastSelected()
|
|
570
|
+
|
|
571
|
+
player.getAvaliableSubtitles = () => []
|
|
572
|
+
|
|
573
|
+
player.currentSubtitle = () => undefined
|
|
574
|
+
|
|
575
|
+
setSubtitleLoading(true)
|
|
576
|
+
|
|
577
|
+
void getSubtitleInfo(SubtitlePath)
|
|
578
|
+
.then(async (subtitles?) => {
|
|
579
|
+
if (seq !== openVideoSeq) {
|
|
580
|
+
return
|
|
581
|
+
}
|
|
582
|
+
if (subtitles === undefined || subtitles.length === 0) {
|
|
583
|
+
console.info("No subtitles found")
|
|
584
|
+
return
|
|
585
|
+
}
|
|
586
|
+
const normalizedSubtitles = subtitles.map(normalizeSubtitle)
|
|
587
|
+
|
|
588
|
+
player.getAvaliableSubtitles = () => {
|
|
589
|
+
return normalizedSubtitles
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const fallbackSubtitle =
|
|
593
|
+
normalizedSubtitles.find((i) => isSelectableSubtitle(i)) ??
|
|
594
|
+
normalizedSubtitles[0]
|
|
595
|
+
if (!fallbackSubtitle) {
|
|
596
|
+
return
|
|
597
|
+
}
|
|
598
|
+
const [subtitleHistoryResult, lastSelectedSubtitleResult] =
|
|
599
|
+
await Promise.all([subtitleHistory, lastSelectedSubtitle])
|
|
600
|
+
if (seq !== openVideoSeq) {
|
|
601
|
+
return
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const subtitleByHistory = normalizedSubtitles.find(
|
|
605
|
+
(i) =>
|
|
606
|
+
i.name === subtitleHistoryResult?.name &&
|
|
607
|
+
i.stream_index === subtitleHistoryResult?.index,
|
|
608
|
+
)
|
|
609
|
+
const subtitleByLastName = subtitleByHistory
|
|
610
|
+
? undefined
|
|
611
|
+
: normalizedSubtitles.find(
|
|
612
|
+
(i) =>
|
|
613
|
+
isSelectableSubtitle(i) &&
|
|
614
|
+
i.name === lastSelectedSubtitleResult?.name,
|
|
615
|
+
)
|
|
616
|
+
const subtitleByLastType =
|
|
617
|
+
subtitleByHistory || subtitleByLastName
|
|
618
|
+
? undefined
|
|
619
|
+
: typeof lastSelectedSubtitleResult?.is_external === "boolean"
|
|
620
|
+
? normalizedSubtitles.find(
|
|
621
|
+
(i) =>
|
|
622
|
+
isSelectableSubtitle(i) &&
|
|
623
|
+
i.is_external === lastSelectedSubtitleResult.is_external,
|
|
624
|
+
)
|
|
625
|
+
: undefined
|
|
626
|
+
|
|
627
|
+
const defaultSubtitle =
|
|
628
|
+
subtitleByHistory ??
|
|
629
|
+
subtitleByLastName ??
|
|
630
|
+
subtitleByLastType ??
|
|
631
|
+
fallbackSubtitle
|
|
632
|
+
defaultAvaliableSub.value = defaultSubtitle
|
|
633
|
+
lastVisibleSubtitle = defaultSubtitle
|
|
634
|
+
|
|
635
|
+
player.currentSubtitle = () => {
|
|
636
|
+
const current = defaultAvaliableSub.value
|
|
637
|
+
if (!current) return undefined
|
|
638
|
+
return {
|
|
639
|
+
name: current.name,
|
|
640
|
+
language: current.language,
|
|
641
|
+
stream_index: current.stream_index,
|
|
642
|
+
codec_name: current.codec_name,
|
|
643
|
+
is_external: current.is_external,
|
|
644
|
+
path: current.path,
|
|
645
|
+
vtt_url: normalizeSubtitleMediaUrl(current.vtt_url),
|
|
646
|
+
track_id: current.track_id,
|
|
647
|
+
ass_url: normalizeSubtitleMediaUrl(current.ass_url),
|
|
648
|
+
ass_fonts: (current.ass_fonts || [])
|
|
649
|
+
.map((item) => normalizeSubtitleMediaUrl(item))
|
|
650
|
+
.filter((item): item is string => !!item),
|
|
651
|
+
ass_renderable: current.ass_renderable,
|
|
652
|
+
ass_unavailable_reason: current.ass_unavailable_reason,
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
setSubtitleLoading(false)
|
|
657
|
+
player.trigger("subtitleReady")
|
|
658
|
+
|
|
659
|
+
if (defaultAvaliableSub.value && !player.isSubtitleHidden?.()) {
|
|
660
|
+
player.changeSubtitle(defaultAvaliableSub.value)
|
|
661
|
+
}
|
|
662
|
+
})
|
|
663
|
+
.finally(() => {
|
|
664
|
+
if (seq !== openVideoSeq) {
|
|
665
|
+
return
|
|
666
|
+
}
|
|
667
|
+
setSubtitleLoading(false)
|
|
668
|
+
})
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const _src = player.src.bind(player)
|
|
672
|
+
|
|
673
|
+
const getOriginPreferredSource = () => {
|
|
674
|
+
if (!originDecision) {
|
|
675
|
+
return undefined
|
|
676
|
+
}
|
|
677
|
+
return originDecision.originMode === "direct"
|
|
678
|
+
? originDecision.originDirectUrl
|
|
679
|
+
: originDecision.originHlsUrl
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const forceOriginFallbackToHLS = () => {
|
|
683
|
+
if (isDirectPlayerMode) {
|
|
684
|
+
return
|
|
685
|
+
}
|
|
686
|
+
if (!originDecision) {
|
|
687
|
+
return
|
|
688
|
+
}
|
|
689
|
+
originDecision = {
|
|
690
|
+
...originDecision,
|
|
691
|
+
originMode: "hls",
|
|
692
|
+
}
|
|
693
|
+
const current = player.currentVideoInfo?.()
|
|
694
|
+
if (current) {
|
|
695
|
+
current.playMode = "hls"
|
|
696
|
+
current.resolvedSourceUrl = originDecision.originHlsUrl
|
|
697
|
+
}
|
|
698
|
+
currentTransportMode = "hls"
|
|
699
|
+
refreshLogicalResolutionLevels()
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const getCurrentPlayableSourceUrl = () => {
|
|
703
|
+
const preferred = (subtitleSourceOverride || resolvedPlayableSource || "").trim()
|
|
704
|
+
if (preferred) {
|
|
705
|
+
return preferred
|
|
706
|
+
}
|
|
707
|
+
const source = (player.currentSource?.().src || player.currentSrc() || "").trim()
|
|
708
|
+
if (source && !/^blob:/i.test(source)) {
|
|
709
|
+
return source
|
|
710
|
+
}
|
|
711
|
+
const infoSource = (player.currentVideoInfo?.()?.sourceUrl || "").trim()
|
|
712
|
+
if (infoSource) {
|
|
713
|
+
return normalizeQualityToMaster(infoSource)
|
|
714
|
+
}
|
|
715
|
+
return ""
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const parseSourceUrl = (raw: string) => {
|
|
719
|
+
if (!raw) return undefined
|
|
720
|
+
try {
|
|
721
|
+
const baseHref =
|
|
722
|
+
typeof window !== "undefined"
|
|
723
|
+
? window.location.href
|
|
724
|
+
: "http://localhost/"
|
|
725
|
+
return new URL(raw, baseHref)
|
|
726
|
+
} catch (err) {
|
|
727
|
+
console.error("Failed to parse current source url", { raw, err })
|
|
728
|
+
return undefined
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const toOutputSourceUrl = (parsed: URL, raw: string) => {
|
|
733
|
+
if (/^(https?:)?\/\//i.test(raw)) {
|
|
734
|
+
return parsed.toString()
|
|
735
|
+
}
|
|
736
|
+
if (raw.startsWith("//")) {
|
|
737
|
+
return parsed.toString().replace(/^[^:]+:/, "")
|
|
738
|
+
}
|
|
739
|
+
return `${parsed.pathname}${parsed.search}${parsed.hash}`
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const hasLegacySubtitleParamsInSource = () => {
|
|
743
|
+
const raw = getCurrentPlayableSourceUrl()
|
|
744
|
+
const parsed = parseSourceUrl(raw)
|
|
745
|
+
if (!parsed) {
|
|
746
|
+
return false
|
|
747
|
+
}
|
|
748
|
+
return (
|
|
749
|
+
parsed.searchParams.has(LEGACY_SUBTITLE_TYPE_PARAM) ||
|
|
750
|
+
parsed.searchParams.has(LEGACY_SUBTITLE_INDEX_PARAM) ||
|
|
751
|
+
parsed.searchParams.has(LEGACY_SUBTITLE_CUSTOM_PARAM)
|
|
752
|
+
)
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const buildLegacySubtitleSwitchUrl = (
|
|
756
|
+
mode: "select" | "clear",
|
|
757
|
+
subtitle?: Subtitle,
|
|
758
|
+
): string | undefined => {
|
|
759
|
+
const raw = getCurrentPlayableSourceUrl()
|
|
760
|
+
const parsed = parseSourceUrl(raw)
|
|
761
|
+
if (!parsed) {
|
|
762
|
+
return undefined
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
parsed.searchParams.delete(LEGACY_SUBTITLE_CUSTOM_PARAM)
|
|
766
|
+
|
|
767
|
+
if (mode === "select") {
|
|
768
|
+
if (!subtitle) {
|
|
769
|
+
return undefined
|
|
770
|
+
}
|
|
771
|
+
parsed.searchParams.set(
|
|
772
|
+
LEGACY_SUBTITLE_TYPE_PARAM,
|
|
773
|
+
getLegacySubtitleTypeValue(subtitle),
|
|
774
|
+
)
|
|
775
|
+
parsed.searchParams.set(
|
|
776
|
+
LEGACY_SUBTITLE_INDEX_PARAM,
|
|
777
|
+
`${subtitle.stream_index}`,
|
|
778
|
+
)
|
|
779
|
+
} else {
|
|
780
|
+
parsed.searchParams.delete(LEGACY_SUBTITLE_TYPE_PARAM)
|
|
781
|
+
parsed.searchParams.delete(LEGACY_SUBTITLE_INDEX_PARAM)
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
return toOutputSourceUrl(parsed, raw)
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const getPreservedPlaybackRate = () => {
|
|
788
|
+
const playbackRateValue = player.playbackRate?.()
|
|
789
|
+
const preservedPlaybackRate =
|
|
790
|
+
typeof playbackRateValue === "number" && Number.isFinite(playbackRateValue)
|
|
791
|
+
? playbackRateValue
|
|
792
|
+
: undefined
|
|
793
|
+
if (preservedPlaybackRate == null || preservedPlaybackRate <= 0) {
|
|
794
|
+
return undefined
|
|
795
|
+
}
|
|
796
|
+
return preservedPlaybackRate
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const restorePlaybackRateAfterSourceReload = () => {
|
|
800
|
+
const preservedPlaybackRate = getPreservedPlaybackRate()
|
|
801
|
+
if (preservedPlaybackRate === undefined || preservedPlaybackRate === 1) {
|
|
802
|
+
return
|
|
803
|
+
}
|
|
804
|
+
player.one("loadedmetadata", () => {
|
|
805
|
+
player.playbackRate?.(preservedPlaybackRate)
|
|
806
|
+
})
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const reloadSourceWithSeek = (nextSrc: string) => {
|
|
810
|
+
const seekTo = (player.duration() !== Infinity && player.currentTime()) || 0
|
|
811
|
+
const expectedInfo = player.currentVideoInfo?.()
|
|
812
|
+
scheduleLoadedMetadataSeek(seekTo, expectedInfo)
|
|
813
|
+
restorePlaybackRateAfterSourceReload()
|
|
814
|
+
resolvedPlayableSource = nextSrc
|
|
815
|
+
subtitleSourceOverride = nextSrc
|
|
816
|
+
currentTransportMode = /\.m3u8(\?|$)/i.test(nextSrc) ? "hls" : "direct"
|
|
817
|
+
_src({
|
|
818
|
+
src: nextSrc,
|
|
819
|
+
type: currentTransportMode === "hls" ? "application/x-mpegURL" : "",
|
|
820
|
+
})
|
|
821
|
+
safePlay()
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const reloadSourceWithSeekAndResolution = (
|
|
825
|
+
nextSrc: string,
|
|
826
|
+
level: VideoQualityLevel,
|
|
827
|
+
) => {
|
|
828
|
+
const seekTo = (player.duration() !== Infinity && player.currentTime()) || 0
|
|
829
|
+
const expectedInfo = player.currentVideoInfo?.()
|
|
830
|
+
scheduleLoadedMetadataSeek(seekTo, expectedInfo, () => {
|
|
831
|
+
const index = findCurrentQualityLevelIndex(level)
|
|
832
|
+
if (index >= 0) {
|
|
833
|
+
autoEnableAndDisableResolution(index, { trigger: false })
|
|
834
|
+
baseChangeResolution(qualityLevels.levels_[index] || level)
|
|
835
|
+
} else {
|
|
836
|
+
baseChangeResolution(level)
|
|
837
|
+
}
|
|
838
|
+
releaseResolutionDisplayLock()
|
|
839
|
+
})
|
|
840
|
+
restorePlaybackRateAfterSourceReload()
|
|
841
|
+
resolvedPlayableSource = nextSrc
|
|
842
|
+
subtitleSourceOverride = nextSrc
|
|
843
|
+
currentTransportMode = /\.m3u8(\?|$)/i.test(nextSrc) ? "hls" : "direct"
|
|
844
|
+
_src({
|
|
845
|
+
src: nextSrc,
|
|
846
|
+
type: currentTransportMode === "hls" ? "application/x-mpegURL" : "",
|
|
847
|
+
})
|
|
848
|
+
if (currentTransportMode === "hls") {
|
|
849
|
+
baseChangeResolution(level)
|
|
850
|
+
}
|
|
851
|
+
safePlay()
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const applyLegacySubtitleSelection = (subtitle: Subtitle) => {
|
|
855
|
+
if (!canUseLegacySubtitleProtocol(subtitle)) {
|
|
856
|
+
return false
|
|
857
|
+
}
|
|
858
|
+
const nextSrc = buildLegacySubtitleSwitchUrl("select", subtitle)
|
|
859
|
+
if (!nextSrc) {
|
|
860
|
+
return false
|
|
861
|
+
}
|
|
862
|
+
const currentSrc = getCurrentPlayableSourceUrl()
|
|
863
|
+
if (currentSrc === nextSrc) {
|
|
864
|
+
return true
|
|
865
|
+
}
|
|
866
|
+
reloadSourceWithSeek(nextSrc)
|
|
867
|
+
return true
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const clearLegacySubtitleSelection = () => {
|
|
871
|
+
if (
|
|
872
|
+
!canUseLegacySubtitleProtocol(getLegacyProbeSubtitle()) &&
|
|
873
|
+
!hasLegacySubtitleParamsInSource()
|
|
874
|
+
) {
|
|
875
|
+
return false
|
|
876
|
+
}
|
|
877
|
+
const nextSrc = buildLegacySubtitleSwitchUrl("clear")
|
|
878
|
+
if (!nextSrc) {
|
|
879
|
+
return false
|
|
880
|
+
}
|
|
881
|
+
const currentSrc = getCurrentPlayableSourceUrl()
|
|
882
|
+
if (currentSrc === nextSrc) {
|
|
883
|
+
return true
|
|
884
|
+
}
|
|
885
|
+
reloadSourceWithSeek(nextSrc)
|
|
886
|
+
return true
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
player.changeSubtitle = (sub: Subtitle | number) => {
|
|
890
|
+
if (isDirectPlayerMode) {
|
|
891
|
+
return
|
|
892
|
+
}
|
|
893
|
+
const subs = player.getAvaliableSubtitles()
|
|
894
|
+
if (subs === undefined) {
|
|
895
|
+
return
|
|
896
|
+
}
|
|
897
|
+
let selectedSubtitle: Subtitle | undefined
|
|
898
|
+
const updateSelection = (sub: Subtitle) => {
|
|
899
|
+
if (netdiskPath) {
|
|
900
|
+
subtitleDB.update(netdiskPath, {
|
|
901
|
+
name: sub.name,
|
|
902
|
+
index: sub.stream_index,
|
|
903
|
+
})
|
|
904
|
+
}
|
|
905
|
+
subtitleDB.updateLastSelected({
|
|
906
|
+
name: sub.name,
|
|
907
|
+
is_external: sub.is_external,
|
|
908
|
+
})
|
|
909
|
+
defaultAvaliableSub.value = sub
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
if (typeof sub == "number") {
|
|
913
|
+
if (sub >= subs.length) {
|
|
914
|
+
return
|
|
915
|
+
}
|
|
916
|
+
selectedSubtitle = subs[sub]
|
|
917
|
+
if (selectedSubtitle) {
|
|
918
|
+
updateSelection(selectedSubtitle)
|
|
919
|
+
}
|
|
920
|
+
} else {
|
|
921
|
+
const canonicalSubtitle = subs.find(
|
|
922
|
+
(item) =>
|
|
923
|
+
item.name === sub.name && item.stream_index === sub.stream_index,
|
|
924
|
+
)
|
|
925
|
+
if (canonicalSubtitle) {
|
|
926
|
+
selectedSubtitle = canonicalSubtitle
|
|
927
|
+
updateSelection(canonicalSubtitle)
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
if (selectedSubtitle) {
|
|
931
|
+
subtitleHidden = false
|
|
932
|
+
lastVisibleSubtitle = selectedSubtitle
|
|
933
|
+
if (!applyLegacySubtitleSelection(selectedSubtitle)) {
|
|
934
|
+
if (hasRenderableSubtitleSource(selectedSubtitle)) {
|
|
935
|
+
clearLegacySubtitleSelection()
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
player.trigger("subtitlechange")
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
player.clearSubtitle = () => {
|
|
943
|
+
if (isDirectPlayerMode) {
|
|
944
|
+
return
|
|
945
|
+
}
|
|
946
|
+
if (netdiskPath) {
|
|
947
|
+
subtitleDB.update(netdiskPath, {})
|
|
948
|
+
}
|
|
949
|
+
clearLegacySubtitleSelection()
|
|
950
|
+
player.trigger("subtitlechange")
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
player.isSubtitleHidden = () => subtitleHidden
|
|
954
|
+
|
|
955
|
+
player.setSubtitleHidden = (value: boolean) => {
|
|
956
|
+
if (isDirectPlayerMode) return
|
|
957
|
+
if (subtitleHidden === value) return
|
|
958
|
+
subtitleHidden = value
|
|
959
|
+
if (value) {
|
|
960
|
+
const current = player.currentSubtitle?.()
|
|
961
|
+
if (current) {
|
|
962
|
+
lastVisibleSubtitle = current
|
|
963
|
+
}
|
|
964
|
+
player.clearSubtitle()
|
|
965
|
+
}
|
|
966
|
+
player.trigger("subtitlevisibilitychange")
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
player.toggleSubtitleVisibility = () => {
|
|
970
|
+
if (isDirectPlayerMode) return
|
|
971
|
+
if (subtitleHidden) {
|
|
972
|
+
subtitleHidden = false
|
|
973
|
+
player.trigger("subtitlevisibilitychange")
|
|
974
|
+
const next = lastVisibleSubtitle ?? defaultAvaliableSub.value
|
|
975
|
+
if (next) {
|
|
976
|
+
player.changeSubtitle(next)
|
|
977
|
+
}
|
|
978
|
+
return
|
|
979
|
+
}
|
|
980
|
+
player.setSubtitleHidden(true)
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function src2(): string
|
|
984
|
+
function src2(sources: string | SourceObject | SourceObject[]): void
|
|
985
|
+
function src2(sources?: any): any {
|
|
986
|
+
if (!sources || sources == null || sources == undefined) {
|
|
987
|
+
return player.currentSrc()
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
const format = (url: string, stype = "") => {
|
|
991
|
+
const info = player.currentVideoInfo()
|
|
992
|
+
if (!info) {
|
|
993
|
+
return {
|
|
994
|
+
src: normalizeQualityToMaster(url),
|
|
995
|
+
type: stype,
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
if (
|
|
999
|
+
isDirectPlayerMode ||
|
|
1000
|
+
(info.fromNetdisk && info.path) ||
|
|
1001
|
+
(url.startsWith("/") && !url.startsWith(baseUrlPrefix)) // 兼容代码
|
|
1002
|
+
) {
|
|
1003
|
+
const resolved = (
|
|
1004
|
+
isDirectPlayerMode ? info.sourceUrl : info.resolvedSourceUrl || ""
|
|
1005
|
+
).trim()
|
|
1006
|
+
if (resolved) {
|
|
1007
|
+
return {
|
|
1008
|
+
src: resolved,
|
|
1009
|
+
type: /\.m3u8(\?|$)/i.test(resolved) ? "application/x-mpegURL" : "",
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
let path = info.path
|
|
1013
|
+
if (path?.startsWith("/")) {
|
|
1014
|
+
path = path.substring(1)
|
|
1015
|
+
}
|
|
1016
|
+
if (path != null) {
|
|
1017
|
+
path = encodeURIComponent(path)
|
|
1018
|
+
}
|
|
1019
|
+
return {
|
|
1020
|
+
src: `${baseUrlPrefix}/hls/${path || url}/master.m3u8`.replace(
|
|
1021
|
+
/\/\//,
|
|
1022
|
+
"/",
|
|
1023
|
+
),
|
|
1024
|
+
type: "application/x-mpegURL",
|
|
1025
|
+
}
|
|
1026
|
+
} else {
|
|
1027
|
+
return {
|
|
1028
|
+
src: normalizeQualityToMaster(url),
|
|
1029
|
+
type: stype,
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
let newSources: SourceObject[] = []
|
|
1035
|
+
if (Array.isArray(sources)) {
|
|
1036
|
+
newSources = sources.map((s: SourceObject) => {
|
|
1037
|
+
return format(s.src, s.type)
|
|
1038
|
+
})
|
|
1039
|
+
} else if (typeof sources == "string") {
|
|
1040
|
+
newSources.push(format(sources, ""))
|
|
1041
|
+
} else {
|
|
1042
|
+
newSources.push(format(sources.src, sources.type))
|
|
1043
|
+
}
|
|
1044
|
+
if (newSources[0]?.src) {
|
|
1045
|
+
resolvedPlayableSource = `${newSources[0].src}`.trim()
|
|
1046
|
+
currentTransportMode = /\.m3u8(\?|$)/i.test(resolvedPlayableSource)
|
|
1047
|
+
? "hls"
|
|
1048
|
+
: "direct"
|
|
1049
|
+
}
|
|
1050
|
+
_src(newSources)
|
|
1051
|
+
}
|
|
1052
|
+
player.src = src2
|
|
1053
|
+
|
|
1054
|
+
player.currentResolution = () => {
|
|
1055
|
+
if (resolutionDisplayLock) {
|
|
1056
|
+
return resolutionDisplayLock
|
|
1057
|
+
}
|
|
1058
|
+
if (currentTransportMode === "direct" && originDecision) {
|
|
1059
|
+
return {
|
|
1060
|
+
id: "origin",
|
|
1061
|
+
res: Math.min(
|
|
1062
|
+
originDecision.mediaSource.height || 0,
|
|
1063
|
+
originDecision.mediaSource.width || 0,
|
|
1064
|
+
),
|
|
1065
|
+
auto: false,
|
|
1066
|
+
origin: true,
|
|
1067
|
+
width: originDecision.mediaSource.width || 0,
|
|
1068
|
+
height: originDecision.mediaSource.height || 0,
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
const quality = player.qualityLevels()
|
|
1072
|
+
const idx = quality.selectedIndex_
|
|
1073
|
+
if (idx === -1) {
|
|
1074
|
+
const en = quality.levels_.find((level) => level.enabled === true)
|
|
1075
|
+
if (!en) {
|
|
1076
|
+
return undefined
|
|
1077
|
+
}
|
|
1078
|
+
return {
|
|
1079
|
+
id: en.id,
|
|
1080
|
+
res: Math.min(en.height, en.width),
|
|
1081
|
+
auto: autoSwitchResolution,
|
|
1082
|
+
origin: en.label === "origin",
|
|
1083
|
+
width: en.width,
|
|
1084
|
+
height: en.height,
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
const res = quality.levels_[idx]
|
|
1088
|
+
if (res) {
|
|
1089
|
+
return {
|
|
1090
|
+
id: res.id,
|
|
1091
|
+
res: Math.min(res.height, res.width),
|
|
1092
|
+
auto: autoSwitchResolution,
|
|
1093
|
+
origin: res.label === "origin",
|
|
1094
|
+
width: res.width,
|
|
1095
|
+
height: res.height,
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
const fallback = quality.levels_.find((level) => level.enabled === true)
|
|
1099
|
+
if (!fallback) {
|
|
1100
|
+
return undefined
|
|
1101
|
+
}
|
|
1102
|
+
return {
|
|
1103
|
+
id: fallback.id,
|
|
1104
|
+
res: Math.min(fallback.height, fallback.width),
|
|
1105
|
+
auto: autoSwitchResolution,
|
|
1106
|
+
origin: fallback.label === "origin",
|
|
1107
|
+
width: fallback.width,
|
|
1108
|
+
height: fallback.height,
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
const baseChangeResolution = player.changeResolution.bind(player)
|
|
1113
|
+
|
|
1114
|
+
const findCurrentQualityLevelIndex = (level: VideoQualityLevel) => {
|
|
1115
|
+
const levels = player.qualityLevels().levels_
|
|
1116
|
+
if (level.label === "origin" || level.id === "origin") {
|
|
1117
|
+
const originIndex = levels.findIndex((item) => item.label === "origin")
|
|
1118
|
+
if (originIndex >= 0) {
|
|
1119
|
+
return originIndex
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
const exactMatchIndex = levels.findIndex(
|
|
1124
|
+
(l: VideoQualityLevel) =>
|
|
1125
|
+
l.label === level.label &&
|
|
1126
|
+
l.height == level.height &&
|
|
1127
|
+
l.width == level.width &&
|
|
1128
|
+
l.bitrate == level.bitrate,
|
|
1129
|
+
)
|
|
1130
|
+
if (exactMatchIndex >= 0) {
|
|
1131
|
+
return exactMatchIndex
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
const shapeMatchIndex = levels.findIndex(
|
|
1135
|
+
(l: VideoQualityLevel) =>
|
|
1136
|
+
l.label === level.label &&
|
|
1137
|
+
l.height == level.height &&
|
|
1138
|
+
l.width == level.width,
|
|
1139
|
+
)
|
|
1140
|
+
if (shapeMatchIndex >= 0) {
|
|
1141
|
+
return shapeMatchIndex
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
const bitrateMatchIndex = levels.findIndex(
|
|
1145
|
+
(l: VideoQualityLevel) =>
|
|
1146
|
+
l.height == level.height &&
|
|
1147
|
+
l.width == level.width &&
|
|
1148
|
+
l.bitrate == level.bitrate,
|
|
1149
|
+
)
|
|
1150
|
+
if (bitrateMatchIndex >= 0) {
|
|
1151
|
+
return bitrateMatchIndex
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
return levels.findIndex((l: VideoQualityLevel) => l.id == level.id)
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
if (autoSwitchResolution == false) {
|
|
1158
|
+
qualityLevels.on("addqualitylevel", (event: any) => {
|
|
1159
|
+
const level = event.qualityLevel
|
|
1160
|
+
if (Math.min(level.height, level.width) > 720) {
|
|
1161
|
+
level.enabled = false
|
|
1162
|
+
} else {
|
|
1163
|
+
level.enabled = true
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
const selectedIndex = qualityLevels.levels_.findIndex(
|
|
1167
|
+
(l) => l.enabled === true,
|
|
1168
|
+
)
|
|
1169
|
+
qualityLevels.selectedIndex_ = selectedIndex
|
|
1170
|
+
if (selectedIndex !== -1) {
|
|
1171
|
+
const selectedLevel = qualityLevels.levels_[selectedIndex]
|
|
1172
|
+
if (selectedLevel) {
|
|
1173
|
+
baseChangeResolution(selectedLevel)
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
refreshLogicalResolutionLevels()
|
|
1177
|
+
})
|
|
1178
|
+
}
|
|
1179
|
+
qualityLevels.on("change", () => {
|
|
1180
|
+
refreshLogicalResolutionLevels()
|
|
1181
|
+
})
|
|
1182
|
+
player.supportResolution = () => {
|
|
1183
|
+
if (isDirectPlayerMode) {
|
|
1184
|
+
return []
|
|
1185
|
+
}
|
|
1186
|
+
refreshLogicalResolutionLevels()
|
|
1187
|
+
let arr: VideoQualityLevel[] = autoSwitchResolution
|
|
1188
|
+
? [
|
|
1189
|
+
...logicalResolutionLevels,
|
|
1190
|
+
{ id: "auto", label: "auto", height: 0, width: 0, bitrate: 0 },
|
|
1191
|
+
]
|
|
1192
|
+
: [...logicalResolutionLevels]
|
|
1193
|
+
arr.sort((a, b) => {
|
|
1194
|
+
if (b.height == a.height) {
|
|
1195
|
+
return b.bitrate - a.bitrate
|
|
1196
|
+
}
|
|
1197
|
+
return b.height - a.height
|
|
1198
|
+
})
|
|
1199
|
+
if (arr.length === 0) {
|
|
1200
|
+
arr = [
|
|
1201
|
+
{
|
|
1202
|
+
id: "auto",
|
|
1203
|
+
label: "auto",
|
|
1204
|
+
height: 0,
|
|
1205
|
+
width: 0,
|
|
1206
|
+
bitrate: 0,
|
|
1207
|
+
},
|
|
1208
|
+
]
|
|
1209
|
+
}
|
|
1210
|
+
return arr
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
player.changeResolution = function (level: VideoQualityLevel) {
|
|
1214
|
+
if (isDirectPlayerMode) {
|
|
1215
|
+
return
|
|
1216
|
+
}
|
|
1217
|
+
if (level.id == "auto") {
|
|
1218
|
+
autoSwitchResolution = true
|
|
1219
|
+
baseChangeResolution(level)
|
|
1220
|
+
return
|
|
1221
|
+
} else if (level.label === "origin" || level.id === "origin") {
|
|
1222
|
+
autoSwitchResolution = false
|
|
1223
|
+
const originSrc = getOriginPreferredSource()
|
|
1224
|
+
if (originSrc && originDecision?.originMode === "direct") {
|
|
1225
|
+
setResolutionDisplayLock({
|
|
1226
|
+
id: "origin",
|
|
1227
|
+
res: Math.min(level.height, level.width),
|
|
1228
|
+
auto: false,
|
|
1229
|
+
origin: true,
|
|
1230
|
+
width: level.width,
|
|
1231
|
+
height: level.height,
|
|
1232
|
+
})
|
|
1233
|
+
reloadSourceWithSeek(originSrc)
|
|
1234
|
+
return
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
const originIndex = findCurrentQualityLevelIndex(level)
|
|
1238
|
+
if (originIndex >= 0) {
|
|
1239
|
+
autoEnableAndDisableResolution(originIndex, { trigger: false })
|
|
1240
|
+
baseChangeResolution(qualityLevels.levels_[originIndex] || level)
|
|
1241
|
+
releaseResolutionDisplayLock()
|
|
1242
|
+
return
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
if (originSrc) {
|
|
1246
|
+
setResolutionDisplayLock({
|
|
1247
|
+
id: "origin",
|
|
1248
|
+
res: Math.min(level.height, level.width),
|
|
1249
|
+
auto: false,
|
|
1250
|
+
origin: true,
|
|
1251
|
+
width: level.width,
|
|
1252
|
+
height: level.height,
|
|
1253
|
+
})
|
|
1254
|
+
reloadSourceWithSeekAndResolution(originSrc, level)
|
|
1255
|
+
return
|
|
1256
|
+
}
|
|
1257
|
+
} else {
|
|
1258
|
+
autoSwitchResolution = false
|
|
1259
|
+
const index = findCurrentQualityLevelIndex(level)
|
|
1260
|
+
if (currentTransportMode === "direct" && originDecision?.originHlsUrl) {
|
|
1261
|
+
setResolutionDisplayLock({
|
|
1262
|
+
id: level.id,
|
|
1263
|
+
res: Math.min(level.height, level.width),
|
|
1264
|
+
auto: false,
|
|
1265
|
+
origin: false,
|
|
1266
|
+
width: level.width,
|
|
1267
|
+
height: level.height,
|
|
1268
|
+
})
|
|
1269
|
+
reloadSourceWithSeekAndResolution(originDecision.originHlsUrl, level)
|
|
1270
|
+
return
|
|
1271
|
+
}
|
|
1272
|
+
autoEnableAndDisableResolution(index, { trigger: false })
|
|
1273
|
+
baseChangeResolution(level)
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
player.on("error", () => {
|
|
1278
|
+
if (isDirectPlayerMode) {
|
|
1279
|
+
return
|
|
1280
|
+
}
|
|
1281
|
+
const currentResolution = player.currentResolution()
|
|
1282
|
+
if (
|
|
1283
|
+
currentTransportMode === "direct" &&
|
|
1284
|
+
currentResolution?.origin &&
|
|
1285
|
+
originDecision?.originHlsUrl &&
|
|
1286
|
+
!directOriginFallbackPending
|
|
1287
|
+
) {
|
|
1288
|
+
directOriginFallbackPending = true
|
|
1289
|
+
forceOriginFallbackToHLS()
|
|
1290
|
+
reloadSourceWithSeekAndResolution(originDecision.originHlsUrl, {
|
|
1291
|
+
id: "origin",
|
|
1292
|
+
label: "origin",
|
|
1293
|
+
height: originDecision.mediaSource.height || 0,
|
|
1294
|
+
width: originDecision.mediaSource.width || 0,
|
|
1295
|
+
bitrate: originDecision.mediaSource.bitrate || 0,
|
|
1296
|
+
})
|
|
1297
|
+
setTimeout(() => {
|
|
1298
|
+
directOriginFallbackPending = false
|
|
1299
|
+
}, 0)
|
|
1300
|
+
}
|
|
1301
|
+
})
|
|
1302
|
+
// player 加载视频唯一的入口
|
|
1303
|
+
player.on("openVideo", async (e: any) => {
|
|
1304
|
+
openVideoSeq += 1
|
|
1305
|
+
const seq = openVideoSeq
|
|
1306
|
+
invalidateLoadedMetadataSeek()
|
|
1307
|
+
let info = e.info as VideoInfo
|
|
1308
|
+
|
|
1309
|
+
subtitleHidden = false
|
|
1310
|
+
lastVisibleSubtitle = undefined
|
|
1311
|
+
defaultAvaliableSub.value = undefined
|
|
1312
|
+
netdiskPath = undefined
|
|
1313
|
+
resolvedPlayableSource = undefined
|
|
1314
|
+
subtitleSourceOverride = undefined
|
|
1315
|
+
originDecision = undefined
|
|
1316
|
+
currentTransportMode = "direct"
|
|
1317
|
+
logicalResolutionLevels = []
|
|
1318
|
+
manifestResolutionLevels = []
|
|
1319
|
+
directOriginFallbackPending = false
|
|
1320
|
+
player.getAvaliableSubtitles = () => []
|
|
1321
|
+
player.currentSubtitle = () => undefined
|
|
1322
|
+
setSubtitleLoading(false)
|
|
1323
|
+
releaseResolutionDisplayLock()
|
|
1324
|
+
|
|
1325
|
+
if (info.requestHistory) {
|
|
1326
|
+
await store.ready
|
|
1327
|
+
const result = store.getHistoryInfo(info.sourceUrl)
|
|
1328
|
+
if (result) {
|
|
1329
|
+
info = mergeHistoryInfo(info, result)
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
videoDisplayName = info.name
|
|
1333
|
+
|
|
1334
|
+
player.currentVideoInfo = () => {
|
|
1335
|
+
return info
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
if (!player.isCastMode()) {
|
|
1339
|
+
if (isDirectPlayerMode) {
|
|
1340
|
+
info.playMode = "direct"
|
|
1341
|
+
info.originDirectUrl = (info.sourceUrl || "").trim()
|
|
1342
|
+
info.resolvedSourceUrl = info.originDirectUrl
|
|
1343
|
+
} else if (info.fromNetdisk && info.path) {
|
|
1344
|
+
try {
|
|
1345
|
+
originDecision = await fetchOriginPlaybackDecision(
|
|
1346
|
+
info,
|
|
1347
|
+
player.options_.mediaPrefix,
|
|
1348
|
+
)
|
|
1349
|
+
if (originDecision) {
|
|
1350
|
+
info.playMode = originDecision.originMode
|
|
1351
|
+
info.resolvedSourceUrl =
|
|
1352
|
+
originDecision.originMode === "direct"
|
|
1353
|
+
? originDecision.originDirectUrl
|
|
1354
|
+
: originDecision.originHlsUrl
|
|
1355
|
+
info.originDirectUrl = originDecision.originDirectUrl
|
|
1356
|
+
info.originHlsUrl = originDecision.originHlsUrl
|
|
1357
|
+
info.subtitleInfoUrl = originDecision.subtitleInfoUrl
|
|
1358
|
+
try {
|
|
1359
|
+
const manifestLevels = await fetchOriginHlsLevels(
|
|
1360
|
+
originDecision.originHlsUrl,
|
|
1361
|
+
)
|
|
1362
|
+
if (seq === openVideoSeq) {
|
|
1363
|
+
syncManifestResolutionLevels(manifestLevels)
|
|
1364
|
+
}
|
|
1365
|
+
} catch (err) {
|
|
1366
|
+
console.error("Failed to load origin hls levels", err)
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
} catch (err) {
|
|
1370
|
+
console.error("Failed to resolve origin playback decision", err)
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
init(info)
|
|
1374
|
+
refreshLogicalResolutionLevels()
|
|
1375
|
+
|
|
1376
|
+
player.isNetdiskSource = () => !!info.fromNetdisk
|
|
1377
|
+
|
|
1378
|
+
if (info.duration - info.currentTime >= 3) {
|
|
1379
|
+
player.one("canplay", function () {
|
|
1380
|
+
if (seq !== openVideoSeq) return
|
|
1381
|
+
player.currentTime(info.currentTime)
|
|
1382
|
+
if (!isDirectPlayerMode) {
|
|
1383
|
+
initSubtitles()
|
|
1384
|
+
}
|
|
1385
|
+
player.currentVideoInfo = () => {
|
|
1386
|
+
return {
|
|
1387
|
+
...info,
|
|
1388
|
+
duration: player.duration() ?? info.duration,
|
|
1389
|
+
currentTime: player.currentTime() ?? info.currentTime,
|
|
1390
|
+
name: player.currentVideoName() ?? info.name,
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
})
|
|
1394
|
+
} else {
|
|
1395
|
+
player.one("canplay", () => {
|
|
1396
|
+
if (seq !== openVideoSeq) return
|
|
1397
|
+
if (!isDirectPlayerMode) {
|
|
1398
|
+
initSubtitles()
|
|
1399
|
+
}
|
|
1400
|
+
player.currentVideoInfo = () => {
|
|
1401
|
+
return {
|
|
1402
|
+
...info,
|
|
1403
|
+
duration: player.duration() ?? info.duration,
|
|
1404
|
+
currentTime: player.currentTime() ?? info.currentTime,
|
|
1405
|
+
name: player.currentVideoName() ?? info.name,
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
})
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
})
|
|
1412
|
+
|
|
1413
|
+
player.currentVideoName = videoName
|
|
1414
|
+
player.currentPreview = currentPreview
|
|
1415
|
+
player.isCastMode = function () {
|
|
1416
|
+
return player.currentType() === "video/lzc-cast"
|
|
1417
|
+
}
|
|
1418
|
+
player.reloadSource = function (index?: number) {
|
|
1419
|
+
const seekTo = (player.duration() !== Infinity && player.currentTime()) || 0
|
|
1420
|
+
const expectedInfo = player.currentVideoInfo?.()
|
|
1421
|
+
scheduleLoadedMetadataSeek(seekTo, expectedInfo)
|
|
1422
|
+
restorePlaybackRateAfterSourceReload()
|
|
1423
|
+
player.one("loadedmetadata", function () {
|
|
1424
|
+
if (index != undefined) {
|
|
1425
|
+
autoEnableAndDisableResolution(index)
|
|
1426
|
+
}
|
|
1427
|
+
})
|
|
1428
|
+
player.src(player.currentSource())
|
|
1429
|
+
safePlay()
|
|
1430
|
+
}
|
|
1431
|
+
}
|