Skip to content

Commit 8273847

Browse files
authored
Merge pull request #478 from arabcoders/dev
[FIX] Chromium browser does not render subtitles after HLS attach
2 parents f4e5dfb + 8c8b932 commit 8273847

File tree

3 files changed

+82
-9
lines changed

3 files changed

+82
-9
lines changed

app/library/Playlist.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,6 @@ async def make(self, file: Path) -> str:
4242
)
4343

4444
playlist.append(f"#EXT-X-STREAM-INF:PROGRAM-ID=1{subs}")
45-
playlist.append(f"{self.url}api/player/m3u8/video/{quote(ref)}.m3u8")
45+
playlist.append(f"{self.url}api/player/m3u8/video/{quote(str(ref))}.m3u8")
4646

4747
return "\n".join(playlist)

ui/app/components/VideoPlayer.vue

Lines changed: 78 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@
8484
<template>
8585
<div v-if="infoLoaded">
8686
<div style="position: relative;">
87-
<video class="player" ref="video" :poster="uri(thumbnail)" playsinline controls
88-
crossorigin="anonymous" preload="auto" autoplay>
87+
<video class="player" ref="video" :poster="uri(thumbnail)" playsinline controls crossorigin="anonymous"
88+
preload="auto" autoplay>
8989
<source v-for="source in sources" :key="source.src" :src="source.src" @error="source.onerror"
9090
:type="source.type" />
9191
<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> => {
416416
applyMediaSessionMetadata()
417417
}
418418
419+
const restoreDefaultTextTrack = async () => {
420+
const el = video.value
421+
if (!el) {
422+
return
423+
}
424+
425+
try {
426+
const tracksList = el.textTracks
427+
if (!tracksList || 0 === tracksList.length) {
428+
return
429+
}
430+
431+
// Check if first track has cues - if not, we need to reload tracks
432+
const firstTrack = tracksList[0] as TextTrack | undefined
433+
const needsReload = firstTrack && (!firstTrack.cues || firstTrack.cues.length === 0)
434+
435+
if (needsReload) {
436+
const trackElements = el.querySelectorAll('track')
437+
438+
trackElements.forEach((trackEl, idx) => {
439+
const parent = trackEl.parentNode
440+
const clone = trackEl.cloneNode(true) as HTMLTrackElement
441+
trackEl.remove()
442+
setTimeout(() => {
443+
if (parent) {
444+
parent.appendChild(clone)
445+
// Set mode after cues load
446+
clone.addEventListener('load', () => {
447+
const trackObj = clone.track
448+
if (trackObj) {
449+
trackObj.mode = idx === 0 ? 'showing' : 'disabled'
450+
}
451+
}, { once: true })
452+
}
453+
}, idx * 10)
454+
})
455+
456+
return
457+
}
458+
459+
for (let i = 0; i < tracksList.length; i += 1) {
460+
const track = tracksList[i] as TextTrack | undefined
461+
if (track) {
462+
track.mode = 'disabled'
463+
}
464+
}
465+
466+
await new Promise(resolve => setTimeout(resolve, 50))
467+
468+
for (let i = 0; i < tracksList.length; i += 1) {
469+
const track = tracksList[i] as TextTrack | undefined
470+
if (!track) {
471+
continue
472+
}
473+
const newMode = 0 === i ? 'showing' : 'disabled'
474+
track.mode = newMode
475+
}
476+
} catch (error) {
477+
console.warn('Failed to restore subtitle track state', error)
478+
}
479+
}
480+
419481
onMounted(async () => {
420482
disableOpacity()
421483
const req = await request(makeDownload(config, props.item, 'api/file/info'))
@@ -452,15 +514,15 @@ onMounted(async () => {
452514
453515
if (isApple) {
454516
const allowedCodec = response.mimetype && response.mimetype.includes('video/mp4')
455-
const src = makeDownload(config, props.item, allowedCodec ? 'api/download' : 'm3u8')
517+
const src = makeDownload(config, props.item, allowedCodec ? 'api/download' : 'm3u8', allowedCodec ? false : true)
456518
sources.value.push({
457519
src,
458520
type: allowedCodec ? response.mimetype : 'application/x-mpegURL',
459521
onerror: (err: Event) => src_error(err),
460522
})
461523
usingHls.value = !allowedCodec
462524
} else {
463-
const src = makeDownload(config, props.item, 'api/download')
525+
const src = makeDownload(config, props.item, 'api/download', false)
464526
sources.value.push({ src, type: response.mimetype, onerror: (err: Event) => src_error(err), })
465527
usingHls.value = false
466528
}
@@ -597,6 +659,7 @@ const prepareVideoPlayer = () => {
597659
598660
video.value.volume = volume.value
599661
video.value.addEventListener('volumechange', volume_change_handler)
662+
restoreDefaultTextTrack()
600663
601664
if (hasVideoStream.value) {
602665
if ('requestVideoFrameCallback' in video.value) {
@@ -623,7 +686,7 @@ const src_error = async (e: any) => {
623686
}
624687
625688
console.warn('Source failed to load, attempting HLS fallback via hls.js...', e)
626-
attach_hls(makeDownload(config, props.item, 'm3u8'))
689+
attach_hls(makeDownload(config, props.item, 'm3u8', true))
627690
}
628691
629692
const attach_hls = (link: string) => {
@@ -640,6 +703,15 @@ const attach_hls = (link: string) => {
640703
})
641704
642705
hls.on(Hls.Events.MANIFEST_PARSED, () => applyMediaSessionMetadata())
706+
hls.on(Hls.Events.MANIFEST_PARSED, async () => {
707+
await new Promise(resolve => setTimeout(resolve, 100))
708+
await restoreDefaultTextTrack()
709+
})
710+
711+
hls.on(Hls.Events.MEDIA_ATTACHED, async () => {
712+
await new Promise(resolve => setTimeout(resolve, 200))
713+
await restoreDefaultTextTrack()
714+
})
643715
644716
hls.on(Hls.Events.LEVEL_LOADED, () => {
645717
if (video.value) {
@@ -667,6 +739,6 @@ const forceSwitchToHls = () => {
667739
return
668740
}
669741
670-
src_error(new Event('forceSwitch'))
742+
attach_hls(makeDownload(config, props.item, 'm3u8', true))
671743
}
672744
</script>

ui/app/utils/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -456,9 +456,10 @@ const getQueryParams = (url: string = window.location.search): Record<string, st
456456
* @param config - The application config object.
457457
* @param item - The item containing filename/folder.
458458
* @param base - The base endpoint type (default: 'api/download').
459+
* @param playlist - Whether to generate a playlist URL (default: false).
459460
* @returns The fully constructed download URI.
460461
*/
461-
const makeDownload = (config: any, item: StoreItem | { folder?: string; filename: string }, base: string = 'api/download'): string => {
462+
const makeDownload = (config: any, item: StoreItem | { folder?: string; filename: string }, base: string = 'api/download', playlist: boolean = false): string => {
462463
let baseDir = 'api/player/m3u8/video/'
463464
if ('m3u8' !== base) {
464465
baseDir = `${base}/`
@@ -474,7 +475,7 @@ const makeDownload = (config: any, item: StoreItem | { folder?: string; filename
474475
}
475476

476477
const url = `/${sTrim(baseDir, '/')}${encodePath(item.filename)}`
477-
return uri('m3u8' === base ? `${url}.m3u8` : url)
478+
return uri('m3u8' === base || true === playlist ? `${url}.m3u8` : url)
478479
}
479480

480481
/**

0 commit comments

Comments
 (0)