diff --git a/public/js/components/VideoPlayer.js b/public/js/components/VideoPlayer.js index 16e22b8..3eaef0f 100644 --- a/public/js/components/VideoPlayer.js +++ b/public/js/components/VideoPlayer.js @@ -68,12 +68,16 @@ class VideoPlayer { maxBufferLength: 30, // Buffer up to 30 seconds of content maxMaxBufferLength: 60, // Absolute max buffer 60 seconds maxBufferSize: 60 * 1000 * 1000, // 60MB max buffer size + maxBufferHole: 0.5, // Allow 0.5s holes in buffer (helps with discontinuities) // Live stream settings - stay further from live edge for stability liveSyncDurationCount: 3, // Stay 3 segments behind live liveMaxLatencyDurationCount: 10, // Allow up to 10 segments behind before catching up - // Handle audio discontinuities (fixes garbled audio during ad transitions) + liveBackBufferLength: 30, // Keep 30s of back buffer for seeking + // Audio discontinuity handling (fixes garbled audio during ad transitions) stretchShortVideoTrack: true, // Stretch short segments to avoid gaps forceKeyFrameOnDiscontinuity: true, // Force keyframe sync on discontinuity + // More aggressive discontinuity recovery + maxAudioFramesDrift: 1, // Allow minimal audio drift before correction // Faster recovery from errors levelLoadingMaxRetry: 4, manifestLoadingMaxRetry: 4, @@ -101,9 +105,10 @@ class VideoPlayer { // Initialize HLS.js if supported if (Hls.isSupported()) { this.hls = new Hls(this.getHlsConfig()); + this.lastDiscontinuity = -1; // Track discontinuity changes this.hls.on(Hls.Events.ERROR, (event, data) => { - console.error('HLS error:', data); + console.error('HLS error:', data.type, data.details); if (data.fatal) { switch (data.type) { case Hls.ErrorTypes.NETWORK_ERROR: @@ -131,6 +136,10 @@ class VideoPlayer { // Non-fatal media error - try to recover (handles audio codec issues) console.log('Non-fatal media error, attempting recovery...'); this.hls.recoverMediaError(); + } else if (data.details === 'bufferAppendError' || data.details === 'fragParsingError') { + // Buffer/parsing errors during ad transitions - try recovery + console.log('Buffer/parsing error during segment, recovering...'); + this.hls.recoverMediaError(); } }); @@ -145,6 +154,27 @@ class VideoPlayer { this.hls.recoverMediaError(); }); + // Detect discontinuity changes (ad transitions) and help decoder reset + this.hls.on(Hls.Events.FRAG_CHANGED, (event, data) => { + const frag = data.frag; + // Debug: log every fragment change + console.log(`[HLS] FRAG_CHANGED: sn=${frag?.sn}, cc=${frag?.cc}, level=${frag?.level}`); + + if (frag && frag.sn !== 'initSegment') { + // Check if we crossed a discontinuity boundary using CC (Continuity Counter) + if (frag.cc !== undefined && frag.cc !== this.lastDiscontinuity) { + console.log(`[HLS] Discontinuity detected: CC ${this.lastDiscontinuity} -> ${frag.cc}`); + this.lastDiscontinuity = frag.cc; + + // Small nudge to help decoder sync (only if playing) + if (!this.video.paused && this.video.currentTime > 0) { + const nudgeAmount = 0.01; + this.video.currentTime += nudgeAmount; + } + } + } + }); + this.hls.on(Hls.Events.MANIFEST_PARSED, () => { this.video.play().catch(e => console.log('Autoplay prevented:', e)); }); @@ -265,6 +295,28 @@ class VideoPlayer { } else { console.error('Fatal HLS error:', data); } + } else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) { + // Non-fatal media error - just log it (auto-recovery can cause black screens) + console.log('Non-fatal media error:', data.details); + } + }); + + // Detect discontinuity changes (ad transitions) and help decoder reset + this.lastDiscontinuity = -1; + this.hls.on(Hls.Events.FRAG_CHANGED, (event, data) => { + const frag = data.frag; + if (frag && frag.sn !== 'initSegment') { + // Check if we crossed a discontinuity boundary using CC (Continuity Counter) + if (frag.cc !== undefined && frag.cc !== this.lastDiscontinuity) { + console.log(`[HLS] Discontinuity detected: CC ${this.lastDiscontinuity} -> ${frag.cc}`); + this.lastDiscontinuity = frag.cc; + + // Small nudge to help decoder sync (only if playing) + if (!this.video.paused && this.video.currentTime > 0) { + const nudgeAmount = 0.01; + this.video.currentTime += nudgeAmount; + } + } } }); } else if (this.video.canPlayType('application/vnd.apple.mpegurl') === 'probably' || diff --git a/server/routes/transcode.js b/server/routes/transcode.js index c59af3d..093ae39 100644 --- a/server/routes/transcode.js +++ b/server/routes/transcode.js @@ -20,15 +20,17 @@ router.get('/', (req, res) => { console.log(`[Transcode] Using binary: ${ffmpegPath}`); // FFmpeg arguments for transcoding - // Video: Copy (passthrough) - most sources are already H264/HEVC which browsers support - // Audio: Transcode to AAC - fixes Dolby/AC3/EAC3 that browsers can't play + // Optimized for VOD content with incompatible audio (Dolby/AC3/EAC3) + // NOT recommended for live streams - use HLS.js directly for those const args = [ '-hide_banner', - '-loglevel', 'info', + '-loglevel', 'warning', '-fflags', '+genpts', '-i', url, - '-c:v', 'copy', // Video passthrough (no re-encoding = fast!) - '-c:a', 'aac', // Transcode audio to browser-compatible AAC + // Video: passthrough (no re-encoding = fast!) + '-c:v', 'copy', + // Audio: Transcode to browser-compatible AAC with consistent parameters + '-c:a', 'aac', '-ac', '2', '-ar', '48000', '-b:a', '128k',