[FIX] Chromium browser does not render subtitles after HLS attach
This commit is contained in:
parent
1ced5b3314
commit
8c8b932d6b
3 changed files with 82 additions and 9 deletions
|
|
@ -42,6 +42,6 @@ class Playlist:
|
||||||
)
|
)
|
||||||
|
|
||||||
playlist.append(f"#EXT-X-STREAM-INF:PROGRAM-ID=1{subs}")
|
playlist.append(f"#EXT-X-STREAM-INF:PROGRAM-ID=1{subs}")
|
||||||
playlist.append(f"{self.url}api/player/m3u8/video/{quote(ref)}.m3u8")
|
playlist.append(f"{self.url}api/player/m3u8/video/{quote(str(ref))}.m3u8")
|
||||||
|
|
||||||
return "\n".join(playlist)
|
return "\n".join(playlist)
|
||||||
|
|
|
||||||
|
|
@ -84,8 +84,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="infoLoaded">
|
<div v-if="infoLoaded">
|
||||||
<div style="position: relative;">
|
<div style="position: relative;">
|
||||||
<video class="player" ref="video" :poster="uri(thumbnail)" playsinline controls
|
<video class="player" ref="video" :poster="uri(thumbnail)" playsinline controls crossorigin="anonymous"
|
||||||
crossorigin="anonymous" preload="auto" autoplay>
|
preload="auto" autoplay>
|
||||||
<source v-for="source in sources" :key="source.src" :src="source.src" @error="source.onerror"
|
<source v-for="source in sources" :key="source.src" :src="source.src" @error="source.onerror"
|
||||||
:type="source.type" />
|
:type="source.type" />
|
||||||
<track v-for="(track, i) in tracks" :key="track.file" :kind="track.kind" :label="track.label"
|
<track v-for="(track, i) in tracks" :key="track.file" :kind="track.kind" :label="track.label"
|
||||||
|
|
@ -416,6 +416,68 @@ const captureFirstFramePoster = async (el: HTMLVideoElement): Promise<void> => {
|
||||||
applyMediaSessionMetadata()
|
applyMediaSessionMetadata()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const restoreDefaultTextTrack = async () => {
|
||||||
|
const el = video.value
|
||||||
|
if (!el) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tracksList = el.textTracks
|
||||||
|
if (!tracksList || 0 === tracksList.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if first track has cues - if not, we need to reload tracks
|
||||||
|
const firstTrack = tracksList[0] as TextTrack | undefined
|
||||||
|
const needsReload = firstTrack && (!firstTrack.cues || firstTrack.cues.length === 0)
|
||||||
|
|
||||||
|
if (needsReload) {
|
||||||
|
const trackElements = el.querySelectorAll('track')
|
||||||
|
|
||||||
|
trackElements.forEach((trackEl, idx) => {
|
||||||
|
const parent = trackEl.parentNode
|
||||||
|
const clone = trackEl.cloneNode(true) as HTMLTrackElement
|
||||||
|
trackEl.remove()
|
||||||
|
setTimeout(() => {
|
||||||
|
if (parent) {
|
||||||
|
parent.appendChild(clone)
|
||||||
|
// Set mode after cues load
|
||||||
|
clone.addEventListener('load', () => {
|
||||||
|
const trackObj = clone.track
|
||||||
|
if (trackObj) {
|
||||||
|
trackObj.mode = idx === 0 ? 'showing' : 'disabled'
|
||||||
|
}
|
||||||
|
}, { once: true })
|
||||||
|
}
|
||||||
|
}, idx * 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < tracksList.length; i += 1) {
|
||||||
|
const track = tracksList[i] as TextTrack | undefined
|
||||||
|
if (track) {
|
||||||
|
track.mode = 'disabled'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50))
|
||||||
|
|
||||||
|
for (let i = 0; i < tracksList.length; i += 1) {
|
||||||
|
const track = tracksList[i] as TextTrack | undefined
|
||||||
|
if (!track) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const newMode = 0 === i ? 'showing' : 'disabled'
|
||||||
|
track.mode = newMode
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to restore subtitle track state', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
disableOpacity()
|
disableOpacity()
|
||||||
const req = await request(makeDownload(config, props.item, 'api/file/info'))
|
const req = await request(makeDownload(config, props.item, 'api/file/info'))
|
||||||
|
|
@ -452,7 +514,7 @@ onMounted(async () => {
|
||||||
|
|
||||||
if (isApple) {
|
if (isApple) {
|
||||||
const allowedCodec = response.mimetype && response.mimetype.includes('video/mp4')
|
const allowedCodec = response.mimetype && response.mimetype.includes('video/mp4')
|
||||||
const src = makeDownload(config, props.item, allowedCodec ? 'api/download' : 'm3u8')
|
const src = makeDownload(config, props.item, allowedCodec ? 'api/download' : 'm3u8', allowedCodec ? false : true)
|
||||||
sources.value.push({
|
sources.value.push({
|
||||||
src,
|
src,
|
||||||
type: allowedCodec ? response.mimetype : 'application/x-mpegURL',
|
type: allowedCodec ? response.mimetype : 'application/x-mpegURL',
|
||||||
|
|
@ -460,7 +522,7 @@ onMounted(async () => {
|
||||||
})
|
})
|
||||||
usingHls.value = !allowedCodec
|
usingHls.value = !allowedCodec
|
||||||
} else {
|
} else {
|
||||||
const src = makeDownload(config, props.item, 'api/download')
|
const src = makeDownload(config, props.item, 'api/download', false)
|
||||||
sources.value.push({ src, type: response.mimetype, onerror: (err: Event) => src_error(err), })
|
sources.value.push({ src, type: response.mimetype, onerror: (err: Event) => src_error(err), })
|
||||||
usingHls.value = false
|
usingHls.value = false
|
||||||
}
|
}
|
||||||
|
|
@ -597,6 +659,7 @@ const prepareVideoPlayer = () => {
|
||||||
|
|
||||||
video.value.volume = volume.value
|
video.value.volume = volume.value
|
||||||
video.value.addEventListener('volumechange', volume_change_handler)
|
video.value.addEventListener('volumechange', volume_change_handler)
|
||||||
|
restoreDefaultTextTrack()
|
||||||
|
|
||||||
if (hasVideoStream.value) {
|
if (hasVideoStream.value) {
|
||||||
if ('requestVideoFrameCallback' in video.value) {
|
if ('requestVideoFrameCallback' in video.value) {
|
||||||
|
|
@ -623,7 +686,7 @@ const src_error = async (e: any) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
console.warn('Source failed to load, attempting HLS fallback via hls.js...', e)
|
console.warn('Source failed to load, attempting HLS fallback via hls.js...', e)
|
||||||
attach_hls(makeDownload(config, props.item, 'm3u8'))
|
attach_hls(makeDownload(config, props.item, 'm3u8', true))
|
||||||
}
|
}
|
||||||
|
|
||||||
const attach_hls = (link: string) => {
|
const attach_hls = (link: string) => {
|
||||||
|
|
@ -640,6 +703,15 @@ const attach_hls = (link: string) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
hls.on(Hls.Events.MANIFEST_PARSED, () => applyMediaSessionMetadata())
|
hls.on(Hls.Events.MANIFEST_PARSED, () => applyMediaSessionMetadata())
|
||||||
|
hls.on(Hls.Events.MANIFEST_PARSED, async () => {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100))
|
||||||
|
await restoreDefaultTextTrack()
|
||||||
|
})
|
||||||
|
|
||||||
|
hls.on(Hls.Events.MEDIA_ATTACHED, async () => {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 200))
|
||||||
|
await restoreDefaultTextTrack()
|
||||||
|
})
|
||||||
|
|
||||||
hls.on(Hls.Events.LEVEL_LOADED, () => {
|
hls.on(Hls.Events.LEVEL_LOADED, () => {
|
||||||
if (video.value) {
|
if (video.value) {
|
||||||
|
|
@ -667,6 +739,6 @@ const forceSwitchToHls = () => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
src_error(new Event('forceSwitch'))
|
attach_hls(makeDownload(config, props.item, 'm3u8', true))
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -456,9 +456,10 @@ const getQueryParams = (url: string = window.location.search): Record<string, st
|
||||||
* @param config - The application config object.
|
* @param config - The application config object.
|
||||||
* @param item - The item containing filename/folder.
|
* @param item - The item containing filename/folder.
|
||||||
* @param base - The base endpoint type (default: 'api/download').
|
* @param base - The base endpoint type (default: 'api/download').
|
||||||
|
* @param playlist - Whether to generate a playlist URL (default: false).
|
||||||
* @returns The fully constructed download URI.
|
* @returns The fully constructed download URI.
|
||||||
*/
|
*/
|
||||||
const makeDownload = (config: any, item: StoreItem | { folder?: string; filename: string }, base: string = 'api/download'): string => {
|
const makeDownload = (config: any, item: StoreItem | { folder?: string; filename: string }, base: string = 'api/download', playlist: boolean = false): string => {
|
||||||
let baseDir = 'api/player/m3u8/video/'
|
let baseDir = 'api/player/m3u8/video/'
|
||||||
if ('m3u8' !== base) {
|
if ('m3u8' !== base) {
|
||||||
baseDir = `${base}/`
|
baseDir = `${base}/`
|
||||||
|
|
@ -474,7 +475,7 @@ const makeDownload = (config: any, item: StoreItem | { folder?: string; filename
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = `/${sTrim(baseDir, '/')}${encodePath(item.filename)}`
|
const url = `/${sTrim(baseDir, '/')}${encodePath(item.filename)}`
|
||||||
return uri('m3u8' === base ? `${url}.m3u8` : url)
|
return uri('m3u8' === base || true === playlist ? `${url}.m3u8` : url)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue