← Blog/Advanced Debugging

Fixing Silent Buffer Stalls in hls.js Caused by Audio/Video Drift

Published 2024-05-12·6 min read·By VideoLab

How to fix the issue where hls.js randomly pauses playback indefinitely due to microscopic audio/video track drift.

The "Silent Stall" in hls.js

I built a video player. My HLS streams played perfectly 99% of the time. But occasionally, I'd get a user report that the video just stopped. No spinner, no network failure, no obvious error in the console. The player was just stuck.

When I dug into the hls.js events, I could see a bufferStalledError, but when I checked the video.buffered ranges, there was clearly data ahead of the playhead. So why wasn't it playing?

The Root Cause: Microscopic Audio Drift

After days of debugging, I found that this issue boiled down to audio/video drift.

When a live encoder or transcoder creates an HLS stream, the video and audio tracks are packaged together in .ts segments. Sometimes, due to mismatched sample rates or encoder hiccups, the audio track might be slightly shorter than the video track inside a segment—we're talking milliseconds.

Over time, these microscopic differences accumulated in my stream. Eventually, the audio buffer and video buffer fell out of sync.

Browsers are notoriously strict about playback. If the video reaches a point where it has video frames to show, but the audio buffer has a tiny "hole" (e.g., a 0.2-second gap), the browser's native media engine will refuse to advance the playhead. It sits there, waiting for audio data that will never arrive.

The Fix: Tweaking hls.js Configs

By default, hls.js is configured to tolerate very small buffer holes, but for my streams with chronic audio drift, the default tolerances weren't enough.

I fixed this by loosening the maxBufferHole and maxAudioFramesDrift settings when initializing hls.js.

const hls = new Hls({
  // Increase the maximum allowed hole in the buffer before hls.js tries to jump it.
  // Default is usually 0.5 seconds. Bumping it to 1 second helps with severe drift.
  maxBufferHole: 1.0,

  // Allow a larger drift between the audio and video timeline before triggering a resync.
  maxAudioFramesDrift: 2, 

  // If the browser stalls, automatically try to "nudge" the playhead past the hole
  enableWorker: true,
});

The Simpler Option: Nudging the Playhead

When adjusting the config didn't work for one particularly stubborn stream, I implemented a manual nudge when hls.js detected a stall.

I set up a listener for the Hls.Events.ERROR event, checked if it was a bufferStalledError, and explicitly jumped the video forward by a fraction of a second to skip the hole:

hls.on(Hls.Events.ERROR, (event, data) => {
  if (data.details === Hls.ErrorDetails.BUFFER_STALLED_ERROR) {
    console.warn("Buffer stalled, attempting to nudge playhead...");
    video.currentTime += 0.1; // Nudge past the 100ms gap
  }
});

Pro Tip: Use VideoLab's debug panel. If you see the playback status switch from playing to stalled without a corresponding HTTP network failure, you are almost certainly dealing with an audio/video buffer gap.

Try VideoLab Free →

Put this knowledge to use. Test your video streams, debug errors, and export diagnostics — all in your browser.

Open VideoLab ▶