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,345 @@
|
|
|
1
|
+
import type { VideoInfo, VideoQualityLevel } from "@/model"
|
|
2
|
+
|
|
3
|
+
export interface DirectPlayProfile {
|
|
4
|
+
Container: string
|
|
5
|
+
Type: string
|
|
6
|
+
VideoCodec?: string
|
|
7
|
+
AudioCodec?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CodecProfileCondition {
|
|
11
|
+
Condition: string
|
|
12
|
+
Property: string
|
|
13
|
+
Value: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CodecProfile {
|
|
17
|
+
Type: string
|
|
18
|
+
Codec?: string
|
|
19
|
+
Conditions?: CodecProfileCondition[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface DeviceProfile {
|
|
23
|
+
Name: string
|
|
24
|
+
DirectPlayProfiles: DirectPlayProfile[]
|
|
25
|
+
CodecProfiles: CodecProfile[]
|
|
26
|
+
MaxStreamingBitrate?: number
|
|
27
|
+
MaxStaticBitrate?: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface OriginPlaybackDecision {
|
|
31
|
+
originMode: "direct" | "hls"
|
|
32
|
+
originDirectUrl: string
|
|
33
|
+
originHlsUrl: string
|
|
34
|
+
subtitleInfoUrl: string
|
|
35
|
+
reasons: string[]
|
|
36
|
+
mediaSource: {
|
|
37
|
+
container?: string
|
|
38
|
+
videoCodec?: string
|
|
39
|
+
audioCodec?: string
|
|
40
|
+
bitrate?: number
|
|
41
|
+
width?: number
|
|
42
|
+
height?: number
|
|
43
|
+
fps?: number
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function canPlayType(type: string): boolean {
|
|
48
|
+
if (typeof document === "undefined") {
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
const video = document.createElement("video")
|
|
52
|
+
return !!video.canPlayType?.(type).replace(/no/i, "")
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function canPlayAudioType(type: string): boolean {
|
|
56
|
+
if (typeof document === "undefined") {
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
const audio = document.createElement("audio")
|
|
60
|
+
return !!audio.canPlayType?.(type).replace(/no/i, "")
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function detectBrowserName() {
|
|
64
|
+
const ua = typeof navigator !== "undefined" ? navigator.userAgent : ""
|
|
65
|
+
if (/Firefox/i.test(ua)) return "Firefox"
|
|
66
|
+
if (/Safari/i.test(ua) && !/Chrome|Chromium|Edg/i.test(ua)) return "Safari"
|
|
67
|
+
return "Chromium"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function buildVideoDirectPlayProfiles(): DirectPlayProfile[] {
|
|
71
|
+
const profiles: DirectPlayProfile[] = []
|
|
72
|
+
const mp4VideoCodecs: string[] = []
|
|
73
|
+
const webmVideoCodecs: string[] = []
|
|
74
|
+
const mp4AudioCodecs: string[] = []
|
|
75
|
+
const webmAudioCodecs: string[] = []
|
|
76
|
+
|
|
77
|
+
if (
|
|
78
|
+
canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"') ||
|
|
79
|
+
canPlayType('video/mp4; codecs="avc1.64001F, mp4a.40.2"')
|
|
80
|
+
) {
|
|
81
|
+
mp4VideoCodecs.push("h264")
|
|
82
|
+
mp4AudioCodecs.push("aac")
|
|
83
|
+
}
|
|
84
|
+
if (
|
|
85
|
+
canPlayType('video/mp4; codecs="hvc1.1.L120"') ||
|
|
86
|
+
canPlayType('video/mp4; codecs="hev1.1.L120"')
|
|
87
|
+
) {
|
|
88
|
+
mp4VideoCodecs.push("hevc")
|
|
89
|
+
}
|
|
90
|
+
if (canPlayType('video/mp4; codecs="vp09.00.10.08, mp4a.40.2"')) {
|
|
91
|
+
mp4VideoCodecs.push("vp9")
|
|
92
|
+
}
|
|
93
|
+
if (canPlayType('video/mp4; codecs="av01.0.05M.08, mp4a.40.2"')) {
|
|
94
|
+
mp4VideoCodecs.push("av1")
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (canPlayType('video/webm; codecs="vp8, vorbis"')) {
|
|
98
|
+
webmVideoCodecs.push("vp8")
|
|
99
|
+
webmAudioCodecs.push("vorbis")
|
|
100
|
+
}
|
|
101
|
+
if (canPlayType('video/webm; codecs="vp9, opus"')) {
|
|
102
|
+
if (!webmVideoCodecs.includes("vp9")) {
|
|
103
|
+
webmVideoCodecs.push("vp9")
|
|
104
|
+
}
|
|
105
|
+
webmAudioCodecs.push("opus")
|
|
106
|
+
}
|
|
107
|
+
if (canPlayType('video/webm; codecs="av1, opus"')) {
|
|
108
|
+
if (!webmVideoCodecs.includes("av1")) {
|
|
109
|
+
webmVideoCodecs.push("av1")
|
|
110
|
+
}
|
|
111
|
+
if (!webmAudioCodecs.includes("opus")) {
|
|
112
|
+
webmAudioCodecs.push("opus")
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (canPlayAudioType('audio/mpeg; codecs="mp3"') && !mp4AudioCodecs.includes("mp3")) {
|
|
117
|
+
mp4AudioCodecs.push("mp3")
|
|
118
|
+
}
|
|
119
|
+
if (canPlayAudioType('audio/mp4; codecs="opus"') && !mp4AudioCodecs.includes("opus")) {
|
|
120
|
+
mp4AudioCodecs.push("opus")
|
|
121
|
+
}
|
|
122
|
+
if (canPlayAudioType('audio/mp4; codecs="ec-3"')) {
|
|
123
|
+
mp4AudioCodecs.push("eac3")
|
|
124
|
+
}
|
|
125
|
+
if (canPlayAudioType('audio/mp4; codecs="ac-3"')) {
|
|
126
|
+
mp4AudioCodecs.push("ac3")
|
|
127
|
+
}
|
|
128
|
+
if (canPlayAudioType('audio/webm; codecs="opus"') && !webmAudioCodecs.includes("opus")) {
|
|
129
|
+
webmAudioCodecs.push("opus")
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (mp4VideoCodecs.length) {
|
|
133
|
+
profiles.push({
|
|
134
|
+
Container: "mp4,m4v,mov",
|
|
135
|
+
Type: "Video",
|
|
136
|
+
VideoCodec: mp4VideoCodecs.join(","),
|
|
137
|
+
AudioCodec: Array.from(new Set(mp4AudioCodecs)).join(","),
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
if (webmVideoCodecs.length) {
|
|
141
|
+
profiles.push({
|
|
142
|
+
Container: "webm",
|
|
143
|
+
Type: "Video",
|
|
144
|
+
VideoCodec: Array.from(new Set(webmVideoCodecs)).join(","),
|
|
145
|
+
AudioCodec: Array.from(new Set(webmAudioCodecs)).join(","),
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
return profiles
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function buildDeviceProfile(): DeviceProfile {
|
|
152
|
+
const profile: DeviceProfile = {
|
|
153
|
+
Name: detectBrowserName(),
|
|
154
|
+
DirectPlayProfiles: buildVideoDirectPlayProfiles(),
|
|
155
|
+
CodecProfiles: [
|
|
156
|
+
{
|
|
157
|
+
Type: "Video",
|
|
158
|
+
Codec: "h264",
|
|
159
|
+
Conditions: [
|
|
160
|
+
{ Condition: "LessThanEqual", Property: "VideoLevel", Value: "51" },
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
Type: "Audio",
|
|
165
|
+
Codec: "aac,mp3,opus,vorbis,ac3,eac3",
|
|
166
|
+
Conditions: [
|
|
167
|
+
{ Condition: "LessThanEqual", Property: "AudioChannels", Value: "8" },
|
|
168
|
+
],
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
MaxStreamingBitrate: 120000000,
|
|
172
|
+
MaxStaticBitrate: 120000000,
|
|
173
|
+
}
|
|
174
|
+
return profile
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function normalizeMediaPrefix(mediaPrefix?: string): string {
|
|
178
|
+
if (!mediaPrefix) {
|
|
179
|
+
return "/_lzc/media"
|
|
180
|
+
}
|
|
181
|
+
return mediaPrefix.endsWith("/") ? mediaPrefix.slice(0, -1) : mediaPrefix
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function buildOwnerQueryFromSourceUrl(sourceUrl?: string): string {
|
|
185
|
+
if (!sourceUrl) {
|
|
186
|
+
return ""
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
const base =
|
|
190
|
+
typeof window !== "undefined" ? window.location.origin : "http://localhost"
|
|
191
|
+
const url = new URL(sourceUrl, base)
|
|
192
|
+
const owner = url.searchParams.get("X_LZCAPI_UID")
|
|
193
|
+
if (!owner) {
|
|
194
|
+
return ""
|
|
195
|
+
}
|
|
196
|
+
const params = new URLSearchParams()
|
|
197
|
+
params.set("X_LZCAPI_UID", owner)
|
|
198
|
+
return `?${params.toString()}`
|
|
199
|
+
} catch {
|
|
200
|
+
return ""
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function fetchOriginPlaybackDecision(
|
|
205
|
+
info: VideoInfo,
|
|
206
|
+
mediaPrefix?: string,
|
|
207
|
+
): Promise<OriginPlaybackDecision | undefined> {
|
|
208
|
+
if (!info.fromNetdisk || !info.path) {
|
|
209
|
+
return undefined
|
|
210
|
+
}
|
|
211
|
+
let path = info.path
|
|
212
|
+
if (path.startsWith("/")) {
|
|
213
|
+
path = path.slice(1)
|
|
214
|
+
}
|
|
215
|
+
const encodedPath = encodeURIComponent(path)
|
|
216
|
+
const prefix = normalizeMediaPrefix(mediaPrefix)
|
|
217
|
+
const url = `${prefix}/play/info/${encodedPath}${buildOwnerQueryFromSourceUrl(
|
|
218
|
+
info.sourceUrl,
|
|
219
|
+
)}`
|
|
220
|
+
const resp = await fetch(url, {
|
|
221
|
+
method: "POST",
|
|
222
|
+
headers: {
|
|
223
|
+
"Content-Type": "application/json",
|
|
224
|
+
},
|
|
225
|
+
body: JSON.stringify({
|
|
226
|
+
deviceProfile: buildDeviceProfile(),
|
|
227
|
+
}),
|
|
228
|
+
})
|
|
229
|
+
if (!resp.ok) {
|
|
230
|
+
throw new Error(`origin playback info failed: ${resp.status}`)
|
|
231
|
+
}
|
|
232
|
+
return (await resp.json()) as OriginPlaybackDecision
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function parseAttributeList(input: string): Record<string, string> {
|
|
236
|
+
const attrs: Record<string, string> = {}
|
|
237
|
+
const pattern = /([A-Z0-9-]+)=("(?:[^"\\]|\\.)*"|[^,]*)/gi
|
|
238
|
+
let match: RegExpExecArray | null
|
|
239
|
+
while ((match = pattern.exec(input))) {
|
|
240
|
+
const key = match[1]
|
|
241
|
+
const rawValue = match[2] || ""
|
|
242
|
+
attrs[key] =
|
|
243
|
+
rawValue.startsWith('"') && rawValue.endsWith('"')
|
|
244
|
+
? rawValue.slice(1, -1)
|
|
245
|
+
: rawValue
|
|
246
|
+
}
|
|
247
|
+
return attrs
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function parseResolutionFromUri(uri: string): { width: number; height: number } {
|
|
251
|
+
const match = uri.match(
|
|
252
|
+
/(?:^|[\/_-])(\d{3,4})[xX](\d{3,4})(?:[\/_.-]|$)|(?:quality-|res-|_)(\d{3,4})p?(?:[\/_.-]|$)/i,
|
|
253
|
+
)
|
|
254
|
+
if (!match) {
|
|
255
|
+
return { width: 0, height: 0 }
|
|
256
|
+
}
|
|
257
|
+
if (match[1] && match[2]) {
|
|
258
|
+
return {
|
|
259
|
+
width: Number(match[1]) || 0,
|
|
260
|
+
height: Number(match[2]) || 0,
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
width: 0,
|
|
265
|
+
height: Number(match[3]) || 0,
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function inferLevelLabel(
|
|
270
|
+
attrs: Record<string, string>,
|
|
271
|
+
height: number,
|
|
272
|
+
bitrate: number,
|
|
273
|
+
): string {
|
|
274
|
+
const name = (attrs.NAME || attrs["X-STREAM-NAME"] || "").trim()
|
|
275
|
+
if (/^origin$/i.test(name)) {
|
|
276
|
+
return "origin"
|
|
277
|
+
}
|
|
278
|
+
if (name) {
|
|
279
|
+
return /p$/i.test(name) ? name : `${name}P`
|
|
280
|
+
}
|
|
281
|
+
if (height > 0) {
|
|
282
|
+
return `${height}P`
|
|
283
|
+
}
|
|
284
|
+
if (bitrate > 0) {
|
|
285
|
+
return `${Math.round(bitrate / 1000)}K`
|
|
286
|
+
}
|
|
287
|
+
return ""
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export async function fetchOriginHlsLevels(
|
|
291
|
+
masterUrl: string,
|
|
292
|
+
): Promise<VideoQualityLevel[]> {
|
|
293
|
+
const resp = await fetch(masterUrl, { method: "GET" })
|
|
294
|
+
if (!resp.ok) {
|
|
295
|
+
throw new Error(`origin hls manifest failed: ${resp.status}`)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const text = await resp.text()
|
|
299
|
+
const lines = text
|
|
300
|
+
.split(/\r?\n/)
|
|
301
|
+
.map((line) => line.trim())
|
|
302
|
+
.filter((line) => line.length > 0)
|
|
303
|
+
|
|
304
|
+
const levels: VideoQualityLevel[] = []
|
|
305
|
+
let pendingAttrs: Record<string, string> | null = null
|
|
306
|
+
|
|
307
|
+
for (const line of lines) {
|
|
308
|
+
if (line.startsWith("#EXT-X-STREAM-INF:")) {
|
|
309
|
+
pendingAttrs = parseAttributeList(line.slice("#EXT-X-STREAM-INF:".length))
|
|
310
|
+
continue
|
|
311
|
+
}
|
|
312
|
+
if (line.startsWith("#")) {
|
|
313
|
+
continue
|
|
314
|
+
}
|
|
315
|
+
if (!pendingAttrs) {
|
|
316
|
+
continue
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const resolution = (pendingAttrs.RESOLUTION || "").match(/^(\d+)x(\d+)$/i)
|
|
320
|
+
const fallbackResolution = parseResolutionFromUri(line)
|
|
321
|
+
const width = resolution
|
|
322
|
+
? Number(resolution[1]) || 0
|
|
323
|
+
: fallbackResolution.width
|
|
324
|
+
const height = resolution
|
|
325
|
+
? Number(resolution[2]) || 0
|
|
326
|
+
: fallbackResolution.height
|
|
327
|
+
const bitrate =
|
|
328
|
+
Number(pendingAttrs["AVERAGE-BANDWIDTH"]) ||
|
|
329
|
+
Number(pendingAttrs.BANDWIDTH) ||
|
|
330
|
+
0
|
|
331
|
+
const label = inferLevelLabel(pendingAttrs, height, bitrate)
|
|
332
|
+
|
|
333
|
+
levels.push({
|
|
334
|
+
id: String(levels.length),
|
|
335
|
+
label,
|
|
336
|
+
height,
|
|
337
|
+
width,
|
|
338
|
+
bitrate,
|
|
339
|
+
enabled: true,
|
|
340
|
+
})
|
|
341
|
+
pendingAttrs = null
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return levels
|
|
345
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Subtitle } from "@/model"
|
|
2
|
+
|
|
3
|
+
type SubtitleInfoResp = {
|
|
4
|
+
len: number
|
|
5
|
+
data: Subtitle[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function basename(path: string): string {
|
|
9
|
+
return path.split('/').pop() ?? ''
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function transform(data: Subtitle[]) {
|
|
13
|
+
return data.map(i => {
|
|
14
|
+
if (i.is_external && i.path) {
|
|
15
|
+
return { ...i, name: i.name || basename(i.path) }
|
|
16
|
+
}
|
|
17
|
+
return i
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function getSubtitleInfo(
|
|
22
|
+
videoPath?: string,
|
|
23
|
+
): Promise<Subtitle[] | undefined> {
|
|
24
|
+
try {
|
|
25
|
+
if (!videoPath) {
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
const resp = await fetch(videoPath)
|
|
29
|
+
if (!resp.ok) {
|
|
30
|
+
throw new Error(`Http Status: ${resp.status}`)
|
|
31
|
+
}
|
|
32
|
+
const data: SubtitleInfoResp = await resp.json()
|
|
33
|
+
if (data.len == 0) {
|
|
34
|
+
return
|
|
35
|
+
} else {
|
|
36
|
+
return transform(data.data)
|
|
37
|
+
}
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error(`Failed to get subtitle info: ${error}`)
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 简单事件发射器,用于 NativePlayer 与 useSource 等对接
|
|
3
|
+
*/
|
|
4
|
+
type Listener = (e?: any) => void
|
|
5
|
+
|
|
6
|
+
export class EventEmitter {
|
|
7
|
+
private listeners: Record<string, Listener[]> = {}
|
|
8
|
+
private oneListeners: Record<string, Listener[]> = {}
|
|
9
|
+
|
|
10
|
+
on(event: string, fn: Listener) {
|
|
11
|
+
if (!this.listeners[event]) this.listeners[event] = []
|
|
12
|
+
this.listeners[event].push(fn)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
one(event: string, fn: Listener) {
|
|
16
|
+
if (!this.oneListeners[event]) this.oneListeners[event] = []
|
|
17
|
+
this.oneListeners[event].push(fn)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
off(event: string, fn?: Listener) {
|
|
21
|
+
if (fn) {
|
|
22
|
+
this.listeners[event] = (this.listeners[event] || []).filter(
|
|
23
|
+
(f) => f !== fn,
|
|
24
|
+
)
|
|
25
|
+
this.oneListeners[event] = (this.oneListeners[event] || []).filter(
|
|
26
|
+
(f) => f !== fn,
|
|
27
|
+
)
|
|
28
|
+
} else {
|
|
29
|
+
delete this.listeners[event]
|
|
30
|
+
delete this.oneListeners[event]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 支持两种用法:
|
|
36
|
+
* - trigger("eventName") 或 trigger("eventName", data)
|
|
37
|
+
* - trigger({ type: "eventName", ...data }) 与 video.js 兼容
|
|
38
|
+
*/
|
|
39
|
+
trigger(
|
|
40
|
+
eventOrName: string | { type: string; [key: string]: unknown },
|
|
41
|
+
data?: unknown,
|
|
42
|
+
) {
|
|
43
|
+
let eventName: string
|
|
44
|
+
let payload: unknown
|
|
45
|
+
if (typeof eventOrName === "string") {
|
|
46
|
+
eventName = eventOrName
|
|
47
|
+
payload = data
|
|
48
|
+
} else {
|
|
49
|
+
eventName = eventOrName.type
|
|
50
|
+
payload = eventOrName
|
|
51
|
+
}
|
|
52
|
+
const run = (fns: Listener[] = []) => fns.forEach((fn) => fn(payload))
|
|
53
|
+
run(this.oneListeners[eventName])
|
|
54
|
+
this.oneListeners[eventName] = []
|
|
55
|
+
run(this.listeners[eventName])
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
dispose() {
|
|
59
|
+
this.listeners = {}
|
|
60
|
+
this.oneListeners = {}
|
|
61
|
+
}
|
|
62
|
+
}
|