DEV Community

Robert
Robert

Posted on

How I Fixed My HLS Seeking Lag with One Keyframe Trick (GOP explained)

Introduction

I’ve been building a self‑hosted HLS streaming system from scratch – Node.js + TypeScript on the backend, React and hls.js on the frontend, and FFmpeg doing the heavy lifting. Adaptive bitrate, multiple qualities, audio tracks, subtitles, the whole thing.

Everything worked perfectly until I clicked anywhere on the progress bar.

The audio resumed instantly. But the video? It took 5 to 7 seconds to catch up.

After a deep dive, I discovered a concept I should have understood from day one: GOP (Group of Pictures).

The Problem

My HLS segments were 2 seconds long. But I hadn’t set the GOP size explicitly in my FFmpeg command. FFmpeg’s default GOP is usually 250 frames (around 10 seconds for 25 fps). This meant that keyframes were far apart, and most segments didn’t start with one.

When you click on the progress bar, the player requests a new segment at that timestamp. If that segment doesn’t begin with a keyframe, the player cannot decode it immediately. It has to wait for the next keyframe inside that segment – which could be several seconds away.

The audio is independent, so it plays right away. The video waits.

What is GOP?

GOP = Group of Pictures. It’s the distance between two keyframes (I‑frames) in a video stream.

A GOP contains three types of frames:

Frame What it is?
I‑frame (Intra) Complete, standalone image
P‑frame (Predictive) Only changes from previous frame
B‑frame (Bi‑directional) Changes from both past and future frames

The smaller the GOP, the more I‑frames you have. More I‑frames = larger file size, but better seeking and faster quality switches.

How GOP affects HLS streaming?

In HLS, segments are independent files. For a segment to be playable from scratch, it must start with an I‑frame.

If your GOP is larger than your segment duration:

  • Segments often start with a P‑frame or B‑frame.
  • The player must wait until the next I‑frame within that segment.
  • This introduces seeking delay in adaptive bitrate streaming.

The Fix

I changed two settings in my FFmpeg pipeline:

  1. Set GOP size = 2 seconds (matching my segment duration)
  2. Forced keyframes at segment boundaries (using -force_key_frames)

This ensures that every segment starts with an I‑frame.

ffmpeg -i input.mp4 \
  -g 48 \     # 48 frames at 24 fps = 2 seconds
  -force_key_frames "expr:gte(t,n_forced*2)" \
  -hls_time 2 \
  -hls_segment_type fmp4 \
  -hls_flags independent_segments+program_date_time \
  output.m3u8
Enter fullscreen mode Exit fullscreen mode

After those changes:

  • Seeking is now instant and smooth
  • Quality changes in adaptive bitrate are fluid (no more stutter)

What I learned

  • In HLS, the relationship between segment duration and GOP size is critical.
  • Always align your GOP with your segment duration for VOD content.

Conclusion

Building a streaming system from scratch is a fantastic way of learning video fundamentals. If you’re working with HLS, take a moment to check your GOP settings – it could save you hours of debugging later.

Have you ever struggled with seeking or adaptive bitrate switching? I’d like to hear(read) your experience

Github Repo

Top comments (0)