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:
@@ -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')) {
|
||||
|
||||
Reference in New Issue
Block a user