feat: enhance HLS error handling and key URL rewriting in VideoPlayer

- Introduced recovery attempts for HLS errors to prevent infinite loops.
- Added detailed logging for HLS errors, including fragment URLs and response statuses.
- Implemented a custom loader to rewrite AES-128 key URLs to resolve against the app backend.
- Improved user feedback for playback errors based on recovery attempts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Dorian
2026-02-14 10:08:14 +00:00
parent 276dab207c
commit 8aff9271a4

View File

@@ -260,6 +260,8 @@ let controlsTimeout: ReturnType<typeof setTimeout> | null = null
// ── HLS instance ────────────────────────────────────────────────────
let hls: Hls | null = null
let hlsRecoveryAttempts = 0
const MAX_HLS_RECOVERY_ATTEMPTS = 3
/**
* Build the YouTube embed URL for partner/YouTube content.
@@ -363,6 +365,7 @@ async function fetchStream() {
function initPlayer(url: string) {
destroyHls()
hlsRecoveryAttempts = 0
const video = videoEl.value
if (!video) return
@@ -370,9 +373,29 @@ function initPlayer(url: string) {
console.log(`[VideoPlayer] initPlayer — isHls=${isHls}, url=${url.substring(0, 120)}...`)
if (isHls && Hls.isSupported()) {
// Custom loader: rewrite AES-128 key URLs so they resolve
// against the app backend, not the CDN/S3 origin.
// The manifest references keys as "/api/contents/:id/key" which
// resolves against the manifest host (CDN). We redirect to the app.
const BaseLoader = Hls.DefaultConfig.loader as any
class KeyRewriteLoader extends BaseLoader {
load(context: any, config: any, callbacks: any) {
if (context.url?.includes('/contents/') && context.url?.includes('/key')) {
try {
const parsed = new URL(context.url)
const correctedUrl = `${window.location.origin}${parsed.pathname}`
console.log('[VideoPlayer] Rewriting key URL:', context.url, '→', correctedUrl)
context.url = correctedUrl
} catch { /* keep original URL */ }
}
super.load(context, config, callbacks)
}
}
hls = new Hls({
enableWorker: true,
lowLatencyMode: false,
loader: KeyRewriteLoader,
// Attach auth tokens to key requests so paid content can be decrypted
xhrSetup(xhr: XMLHttpRequest, xhrUrl: string) {
if (xhrUrl.includes('/key')) {
@@ -392,18 +415,43 @@ function initPlayer(url: string) {
})
})
hls.on(Hls.Events.ERROR, (_event, data) => {
console.error('[VideoPlayer] HLS error:', data.type, data.details, data.fatal ? '(FATAL)' : '')
if (data.fatal) {
if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
console.warn('[VideoPlayer] HLS network error, retrying...')
hls?.startLoad()
} else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
console.warn('[VideoPlayer] HLS media error, recovering...')
hls?.recoverMediaError()
// Log detailed error info including the fragment URL when available
const fragUrl = (data as any).frag?.url || (data as any).url || ''
console.error(
'[VideoPlayer] HLS error:', data.type, data.details,
data.fatal ? '(FATAL)' : '',
fragUrl ? `frag=${fragUrl.substring(0, 120)}` : '',
(data as any).response ? `status=${(data as any).response.code}` : '',
)
if (!data.fatal) return
// Track recovery attempts to avoid infinite loops
hlsRecoveryAttempts++
if (hlsRecoveryAttempts > MAX_HLS_RECOVERY_ATTEMPTS) {
console.error(`[VideoPlayer] Max recovery attempts (${MAX_HLS_RECOVERY_ATTEMPTS}) exceeded, giving up.`)
// fragParsingError usually means the segments are encrypted or
// the file is corrupt / served incorrectly
if (data.details === 'fragParsingError') {
streamError.value = 'Unable to play this video. The stream segments could not be decoded — they may be encrypted or the file may still be processing.'
} else {
streamError.value = 'Playback error. Please try again.'
destroyHls()
streamError.value = 'Playback failed after multiple attempts. Please try again later.'
}
destroyHls()
return
}
if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
console.warn(`[VideoPlayer] HLS network error, retrying... (attempt ${hlsRecoveryAttempts}/${MAX_HLS_RECOVERY_ATTEMPTS})`)
hls?.startLoad()
} else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
console.warn(`[VideoPlayer] HLS media error, recovering... (attempt ${hlsRecoveryAttempts}/${MAX_HLS_RECOVERY_ATTEMPTS})`)
hls?.recoverMediaError()
} else {
streamError.value = 'Playback error. Please try again.'
destroyHls()
}
})
} else if (isHls && video.canPlayType('application/vnd.apple.mpegurl')) {