블로그 목록
Media20분 읽기

영상 압축의 본질 — I/P/B-frame, GOP, 그리고 HLS가 같이 죽는 이유

라이브 송출 중 화면이 멈추고 로그에 EXTINF:18이 찍혔다면, 진짜 범인은 HLS 패키저가 아니라 인코딩 파이프라인 위쪽일 가능성이 큽니다. 이 글은 영상이 왜 압축되어야 하는지부터 I·P·B 프레임의 역할, keyint이 시간이 아니라 프레임 개수라는 사실, 그리고 CPU 과부하로 fps가 무너지면 같은 설정인데도 GOP가 늘어나 HLS까지 같이 죽는 인과 사슬을 설명합니다. 끝까지 읽으면 segment 길이 한 줄만 보고 어디가 깨졌는지 거꾸로 추적하는 법을 알게 됩니다.

영상 압축I-frameP-frameB-frameGOPkeyintHLSsegmentFFmpegx264라이브 스트리밍디버깅
목차(36개 항목)
  1. 0. 핵심 명제 — `#EXTINF:18`은 인코더 파이프라인이 보낸 비명이다
  2. 1. 영상의 본질 — 사진 × N장
  3. 2. I-frame vs P-frame vs B-frame — 3종류의 프레임
    1. 정의
    2. 직관 — 공이 날아가는 장면
    3. 압축 효율
  4. 3. GOP — Group Of Pictures
    1. 정의
    2. keyint — 가장 헷갈리는 파라미터
    3. "GOP=2초"는 사람 표현, 인코더는 프레임으로 처리
    4. Open GOP vs Closed GOP
  5. 4. 정상 상태 🟢 — 30fps + keyint=60
  6. 5. CPU 터짐 🔴 — fps가 무너지는 순간
  7. 6. 사고 연쇄 반응 💥 — 5단계 인과 사슬
  8. 6-1. 같은 증상, 다른 원인 — `#EXTINF:18` 대안 경로
    1. A. Frame drop 모드 — fps는 유지, 화질이 떨어짐
    2. B. Packager 설정 오류 — 인코더는 무죄
    3. C. 입력 소스 burst / PTS 불연속
    4. D. Clock drift — 인코더와 시스템 시계 불일치
    5. 진단 우선순위
  9. 7. HLS가 같이 죽는 이유 — I-frame 위치 강제
    1. HLS Split 규칙
    2. 정상 vs 사고
  10. 8. iOS 플레이어가 더 터지는 이유
  11. 9. B-frame Deep Dive — 왜 라이브에서는 끄는가
    1. 동작 원리
    2. 디코딩 순서 ≠ 재생 순서
    3. 단점 3가지
    4. 실무 설정 — 라이브 OFF, VOD ON
    5. HLS Segment와 B-frame의 상호작용
  12. 10. 실무 디버깅 역추적 🔍
  13. 11. 한 장 요약 — 최종 암기 카드
  14. 12. SA 의사결정 체크리스트
  15. 13. 한 줄 결론
  16. 관련 글
  17. 참고 자료

"라이브 송출 중에 화면이 멈춰요. 로그 보니 #EXTINF:18이 찍혀있는데, 이게 뭔가요?". HLS로 송출하다 보면 종종 마주치는 증상입니다. 표면적으로는 "HLS segment가 18초"인데, 진짜 원인은 인코딩 파이프라인 한참 위에 있습니다. 이 글은 영상 압축의 본질부터, I/P/B-frame의 역할, GOP과 keyint, 그리고 CPU 과부하가 HLS까지 어떻게 끌고 내려가는지를 한 번에 정리합니다.


0. 핵심 명제 — #EXTINF:18은 인코더 파이프라인이 보낸 비명이다

HLS segment가 이상하게 길다는 건 인코더가 입력 페이스를 못 따라가거나 GOP 정책이 깨졌다는 신호다. 가장 흔한 원인은 CPU 과부하지만 유일한 원인은 아니다 (frame drop, packager 설정 오류, 입력 burst, clock drift 등). 영상의 압축 구조(I/P/B-frame)와 GOP, 그리고 HLS의 split 규칙(I-frame 위치에서만 자름)을 알면 segment 길이 한 줄로 인코딩 파이프라인 어디가 깨졌는지 역추적할 수 있다. keyint은 시간이 아니라 프레임 개수다.

흔한 오해와 정정:

오해정확한 이해
keyint=2 = 2초마다 keyframe❌ "2 프레임마다 keyframe". 시간 아니라 프레임 개수
keyint=60 = 정확히 60프레임마다 I-framekeyint최대값. scenecut이 켜져 있으면 더 일찍 삽입 가능
HLS segment 길이는 플레이리스트 옵션이 정한다❌ I-frame이 어디 박히느냐에 따라 결정 — 인코더가 결정
B-frame은 무조건 켜야 한다❌ 실시간/라이브는 끄는 게 표준. VOD에서만 켬
#EXTINF:18은 HLS 패키저 버그⚠️ 대개는 인코더 단의 GOP 폭증이지만, packager 자체 설정 오류 가능성도 배제하면 안 됨

1. 영상의 본질 — 사진 × N장

영상 = 사진 × N장
항목수치
1초30장 (30fps)
사진 1장 (raw)~2MB
1초 원본60MB → 💀 미침
1시간 원본216GB → 💀💀

결론: 압축 없이는 인터넷으로 영상을 보낼 방법이 없습니다. 영상 코덱(H.264/H.265/AV1)이 푸는 문제가 정확히 이것 — "사진 × N장을 어떻게 작게 만들 것인가".


2. I-frame vs P-frame vs B-frame — 3종류의 프레임

정의

종류풀네임저장 내용참조크기
I-frameIntra frame (Keyframe)전체 사진 통째로없음 (독립)
P-framePredicted frame앞 프레임과의 변화앞만 참조중간
B-frameBidirectional frame앞뒤 양방향 보간앞 + 뒤 모두 참조가장 작음

직관 — 공이 날아가는 장면

Frame1 (I):  공이 왼쪽에 있음     ●
Frame2 (B):  공이 중간에 있음        ●
Frame3 (P):  공이 오른쪽에 있음         ●

I-frame 저장: "공이 왼쪽 (x=100, y=50)에 있다" — 전체 픽셀 기록
P-frame 저장: "앞 프레임 대비 공이 +200px 이동" — 차분만 기록
B-frame 저장: "앞(I)이랑 뒤(P) 평균쯤" — 보간 정보만 기록

P-frame은 앞만 봐서 변화량을 적고, B-frame은 앞뒤를 다 봐서 더 작게 줄입니다.

압축 효율

같은 화질 기준 파일 크기:   I  >  P  >  B
같은 파일 크기 기준 화질:   B  >  P  >  I

B-frame이 가장 효율적이지만, 공짜는 아닙니다 (뒤에서 다룸).


3. GOP — Group Of Pictures

정의

GOP = I-frame부터 다음 I-frame 직전까지의 묶음.

I P P P P P P P P I P P P P P P P P I
|<--- GOP1 --->|  |<--- GOP2 --->|

B-frame을 포함한 전형적인 GOP는 이렇게 생깁니다:

I B B P B B P B B P B B P B B P I
|<------------- GOP ------------->|

keyint — 가장 헷갈리는 파라미터

⚠️ 흔한 착각: keyint=60 = "60초마다 keyframe" 실제: "60 프레임마다 keyframe (최대값)"

정확히 말하면 — keyint은 "최대" GOP 길이

x264에서 --keyint Nmaximum GOP length입니다. 두 가지 옵션이 함께 영향을 줍니다:

옵션기본값 (x264)동작
--keyint250최대 GOP 길이 — 이 값 도달 시 무조건 I-frame
--min-keyintauto최소 GOP 길이 — 이 값 이전엔 I-frame 안 박힘
--scenecut40장면 전환 감지 임계값 — 감지 시 keyint 도달 전에도 I-frame 삽입
일반 영상:  --scenecut 40 켜짐
  장면 전환 감지 → keyint 도달 전에도 I-frame 삽입
  → GOP 길이가 가변적

라이브/HLS: --scenecut 0 (OFF)
  장면 전환 무시, keyint으로만 카운트
  → GOP 길이가 균일 (HLS segment 정렬 가능)
  → 이 글의 모든 GOP 계산은 이 전제를 따름

이 글의 시나리오는 --scenecut 0 전제입니다. 라이브 HLS에서는 segment 정렬을 위해 scenecut을 끄는 것이 표준이며 (#37 참조), 이 전제 위에서만 "keyint=60 = 정확히 60프레임마다 I-frame"이 성립합니다.

학교 출석부 비유 🏫

선생님이 말합니다: "60번째 학생마다 출석 불러".

근데 학생이 하루에 몇 명 오느냐에 따라 시간이 달라집니다.

  • 하루에 30명씩 오면 → 2일 만에 60번째
  • 하루에 5명씩 오면 → 12일 만에 60번째

keyint은 시간이 아니라 "몇 번째 프레임마다" 라는 카운트 규칙입니다.

keyint=60 의 진짜 뜻:
"60번째 프레임이 올 때마다 I-frame을 찍어라"

프레임 번호:
1  2  3  ...  59  60  61  ...  119  120  ...
                   ↑                    ↑
              I-frame              I-frame

fps에 따라 실제 시간이 달라진다

🟢 30fps 정상 케이스

1초: [1][2][3]...[30]
2초: [31][32]...[60]  ← 60번째 도달 → I-frame!
3초: [61]...[90]
4초: [91]...[120]     ← 120번째 도달 → I-frame!

결과: keyint=60, fps=30 → 2초마다 I-frame

🔴 fps 떨어진 케이스

keyint=60, fps=30: 60 ÷ 30 = 2초마다 I-frame
keyint=60, fps=60: 60 ÷ 60 = 1초마다 I-frame
keyint=60, fps=15: 60 ÷ 15 = 4초마다 I-frame
keyint=60, fps=5:  60 ÷ 5  = 12초마다 I-frame ← GOP 폭증!

택배 비유 📦

keyint=60 = "60번째 택배마다 도장 찍어"

정상: 하루에 30개 택배 옴 → 60개 = 2일 만에 도장 CPU 터짐: 하루에 5개밖에 못 처리 → 60개 = 12일 만에 도장

도장 찍는 규칙(keyint=60)은 안 바뀝니다. 그냥 택배가 느리게 오는 것뿐.

시간이 아니라 프레임 개수. fps가 바뀌면 GOP의 실제 시간 길이도 같이 바뀝니다. 이게 뒤에 나올 "CPU 폭주 → segment 폭증"의 핵심 메커니즘 — 인코더에 설정된 규칙은 그대로지만, fps가 떨어지면 같은 keyint이 더 긴 시간으로 펼쳐집니다.

"GOP=2초"는 사람 표현, 인코더는 프레임으로 처리

이 헷갈림의 진짜 원인은 사람이 설정하는 단위와 인코더 내부 단위가 다르다는 점입니다.

사람이 설정하는 값

GOP = 2초
fps = 30

사람 입장에서는 "2초마다 keyframe 넣어"가 가장 직관적입니다.

인코더 내부 변환

인코더: "음... 난 frame만 처리하는데?"
       → 2초 × 30fps = 60 frame
       → 내부 파라미터: keyint = 60
       → 뜻: 60 frame마다 I-frame

x264/x265 같은 인코더는 시간 개념이 없습니다. frame 카운터로만 동작합니다. 그래서 사람이 입력한 "2초"를 자동으로 "60 frame"으로 변환.

정상일 땐 둘이 같아 보임

30fps 유지 시:
60 frame = 2초

I─────────I
   2초

→ 사람들은 "GOP=2초"라고 부름
→ 인코더는 keyint=60으로 작동
→ 결과 동일 → 차이를 인지 못함

CPU 망하면 차이 발생

원래: 30fps
망함: 5fps

근데 인코더 규칙은 안 바뀜:
"60 frame 모이면 I-frame"

60개 모으려면? 60 ÷ 5 = 12초

→ "GOP=2초"로 설정했지만 실제로는 12초
→ 사람의 표현(시간)과 실제 동작(frame)이 어긋남

진실 한 장 정리

사람이 말하는 것실제 인코더 내부
GOP = 2초keyint = 60
시간 단위frame 개수 단위

한 줄: "2초 GOP는 설정값이고, 인코더는 종종 그걸 60 프레임으로 변환해서 처리한다."

평소엔 둘이 같아 보여서 헷갈리지만, fps가 흔들리는 순간 정체가 드러납니다.

Open GOP vs Closed GOP

종류B-frame이 이전 GOP 참조 가능?용도
Open GOP가능VOD, 압축률 우선
Closed GOP불가 (GOP 경계에서 차단)라이브/HLS 필수

HLS segment를 깨끗하게 자르려면 반드시 Closed GOP. Open GOP에서는 segment 경계의 B-frame이 이전 segment의 프레임을 참조해서 깨집니다 (#37 참조).


4. 정상 상태 🟢 — 30fps + keyint=60

30fps 정상 동작

1초 = 30 frame
2초 = 60 frame  ← keyint=60

타임라인:
0s   I ──────────── 2s   I ──────────── 4s   I
     |<-- GOP 2초 -->|    |<-- GOP 2초 -->|

HLS segment:
[seg1: 2초][seg2: 2초][seg3: 2초]...
#EXTINF:2.0
#EXTINF:2.0
#EXTINF:2.0

깔끔하게 2초마다 I-frame이 박히고, HLS도 2초마다 자릅니다. #EXT-X-TARGETDURATION:2 약속과 실제가 일치.


5. CPU 터짐 🔴 — fps가 무너지는 순간

CPU 부하가 100%에 도달하면 인코더가 fps를 못 채웁니다.

원래 fps: 30
실제 fps: 5   ← 6배 느림 (인코더 헉헉)

keyint=60을 채우려면?
60 frame ÷ 5 fps = 12초 걸림

타임라인:
0s   I ──────────────────────── 12s   I
     |<-------- GOP 12초 ------->|

같은 keyint=60 설정인데도, fps가 떨어지면 GOP의 실제 시간이 6배로 늘어납니다. 이게 사고의 시작점.


6. 사고 연쇄 반응 💥 — 5단계 인과 사슬

1. CPU 과부하 (100%)
       ↓
2. fps 하락 (30 → 5)
       ↓
3. 프레임 처리 지연
       ↓
4. I-frame 생성 지연 (keyint=60을 채우는 데 12초)
       ↓
5. GOP 길어짐 (2초 → 12~18초)
       ↓
6. HLS: I-frame 기다리며 못 자름
       ↓
7. segment 18초짜리 생성
       ↓
8. #EXTINF:11 / :13 / :18 로그 출현

#EXTINF:18이 찍힌 시점에는 이미 5단계 전부터 무너진 상태. 패키저는 무죄, 인코더가 범인.

⚠️ 전제 조건: 이 인과 사슬은 인코더가 프레임을 드롭하지 않고 큐잉하는 경우에 발현됩니다. OBS의 "Frames missed due to rendering lag"이 늘어나는 모드, 또는 ffmpeg에서 -re 없이 캡처 디바이스를 직접 받는 케이스. 인코더가 frame drop 정책이면 GOP 시간은 유지되지만 화질이 떨어집니다 (다음 섹션 참조).


6-1. 같은 증상, 다른 원인 — #EXTINF:18 대안 경로

CPU 과부하가 가장 흔한 원인이지만, 동일 증상이 다른 경로로도 발생합니다. 디버깅 시 반드시 함께 확인해야 할 4가지:

A. Frame drop 모드 — fps는 유지, 화질이 떨어짐

Real-time 인코더(x264 + zerolatency, OBS) 기본 동작:
  CPU 부족 → 프레임을 큐에 쌓는 게 아니라 드롭함
  → 출력 fps는 30 유지
  → 입력 30프레임 중 5프레임만 인코딩, 25프레임 버림
  → GOP 시간은 정상 (2초)
  → 화질만 급격히 저하

증상: #EXTINF는 정상, 화면이 뚝뚝 끊김, "blocking" 증가
진단: ffmpeg -progress의 drop_frames, OBS의 "missed frames"

B. Packager 설정 오류 — 인코더는 무죄

ffmpeg -hls_time 2 인데 인코더 GOP가 5초:
  Packager는 다음 keyframe까지 기다림
  → #EXTINF:5 발생

Bento4/Shaka Packager의 segment_duration이 GOP와 불일치:
  → #EXTINF가 들쭉날쭉

진단: 인코더 설정 vs packager 설정 양쪽 동시 확인
처방: segment_length가 GOP_length의 정수배여야 함 ([#37](/frank/blog/37))

C. 입력 소스 burst / PTS 불연속

RTMP push 소스가 burst로 도착:
  잠시 멈췄다가 한 번에 여러 프레임 전송
  → 인코더 timestamp 기준 GOP가 늘어남

PTS discontinuity (캡처 디바이스 reconnect, 멀티 소스 전환):
  → packager가 segment boundary 오판
  → #EXTINF 비정상

진단: ffprobe -show_packets로 PTS gap 확인

D. Clock drift — 인코더와 시스템 시계 불일치

NTP sync 실패 또는 CPU steal로 인한 시계 jitter:
  → wall-clock 기반 keyframe 삽입 로직(예: -force_key_frames "expr:gte(t,n_forced*2)")이 빗나감
  → GOP 길이가 점진적으로 어긋남

진단: 시스템 NTP 상태, VM이라면 hypervisor clock drift 확인

진단 우선순위

순서확인 항목명령
1Frame drop 카운터OBS UI, ffmpeg -progress drop_frames
2CPU 사용률top, htop, nvidia-smi (HW 인코더)
3인코더 설정 vs packager 설정양쪽 config diff
4입력 소스 PTS 연속성ffprobe -show_packets
5시스템 시계chronyc tracking, timedatectl

핵심: #EXTINF:18을 보면 frame drop 카운터를 먼저 보세요. drop이 많으면 §6-1 A 시나리오 (Frame drop → fps 유지, 화질 저하), drop이 적은데 segment가 길면 §6 본문 시나리오 (CPU/큐잉 → GOP 폭증).


7. HLS가 같이 죽는 이유 — I-frame 위치 강제

HLS Split 규칙

HLS segment는 반드시 I-frame 위치에서만 split 가능.

이유는 디코딩 의존성 때문:

I → P → P → P → P → P → P → P
         ↑
         여기서 자르면? → 앞 I가 없음 → 💀 재생 불가

P-frame은 앞 frame이 있어야 복구 가능.
B-frame은 앞뒤 둘 다 있어야 복구 가능.
→ 새 segment의 시작은 항상 I-frame이어야 함.

정상 vs 사고

상태I-frame 주기HLS segment 길이로그
🟢 정상2초2초#EXTINF:2.0
🟡 살짝 부하4~6초4~6초#EXTINF:4.5
🔴 CPU 터짐12~18초12~18초#EXTINF:18

#EXTINF의 값이 #EXT-X-TARGETDURATION보다 크면 RFC 8216 위반이며, 거의 대부분 인코더/파이프라인 단의 문제입니다.


8. iOS 플레이어가 더 터지는 이유

iOS HLS 플레이어는 EXT-X-TARGETDURATION을 신뢰합니다. 그게 어긋나면 줄줄이 무너집니다.

HLS playlist:
#EXT-X-TARGETDURATION:2  ← "2초씩 올게요" 약속

실제 도착:
#EXTINF:18  ← 18초짜리 옴

iOS 플레이어 내부: ???
→ 버퍼 사이즈 예상 빗나감
→ 타임라인 계산 꼬임
→ 다음 segment 도착 예측 실패
증상원인
seek 이상타임라인 계산이 segment 길이 기반인데 빗나감
버퍼 꼬임예상 크기의 9배 segment 도착 → 버퍼 overflow/underflow
화면 멈춤다음 segment 못 받음 (또는 받았는데 디코딩 지연)
AV sync 이상오디오/비디오 PTS 계산 꼬임 (특히 B-frame 환경)

RFC 8216: EXT-X-TARGETDURATION은 모든 segment의 최대 길이 상한값이어야 함. 위반 시 클라이언트 동작은 implementation-defined → iOS는 보수적으로 처리하다 멈춤.


9. B-frame Deep Dive — 왜 라이브에서는 끄는가

동작 원리

P-frame은 과거만 봅니다:

[I] → [P] → [P]
             ↑
          앞만 참조

B-frame은 과거 + 미래를 동시에 봅니다:

[I] → [B] → [P]
       ↑
   앞뒤 모두 참조

디코딩 순서 ≠ 재생 순서

B-frame의 핵심 함정: 저장/전송 순서와 화면 재생 순서가 다릅니다. 이유를 단계별로 풀어봅시다.

Step 1 — 문제 제기

B-frame은 미래 프레임을 참조합니다.

재생 순서:  I  B  B  P  B  B  P
시간 →      0  1  2  3  4  5  6

B(1번)을 디코딩하려면?

  • ⬅️ 과거 I(0번) 필요 → ✅ 이미 있음
  • ➡️ 미래 P(3번) 필요 → ❌ 아직 안 왔음

💡 B-frame을 재생하려면 미래의 P-frame을 먼저 디코딩해놔야 한다.

Step 2 — 그래서 저장 순서를 바꾼다

재생 순서 (Display order):  I  B  B  P  B  B  P
                            0  1  2  3  4  5  6

디코딩 순서 (Decode order): I  P  B  B  P  B  B
                            0  3  1  2  6  4  5
                            ↑  ↑
                       I 다음에 P가 먼저 옴 (B보다 앞)

Step 3 — 디코더 내부 흐름

① I(0) 디코딩 → 버퍼에 저장
② P(3) 디코딩 → I(0) 참조 → 버퍼에 저장 (아직 화면 출력 X)
③ B(1) 디코딩 → I(0) + P(3) 둘 다 준비됨 → 화면 출력 (t=1)
④ B(2) 디코딩 → I(0) + P(3) 참조 → 화면 출력 (t=2)
⑤ P(3) 화면 출력 (t=3)  ← 이미 ②에서 디코딩만 해둠
⑥ P(6) 디코딩 → P(3) 참조 → 버퍼에 저장
⑦ B(4), B(5) 디코딩 → P(3) + P(6) 참조 → 차례로 화면 출력

디코딩 시점과 화면 출력 시점이 분리되어 있다는 게 핵심. 디코더는 미래 프레임을 먼저 디코딩한 뒤 버퍼에 저장해두고, 재생 타이밍이 되면 꺼내서 출력합니다.

비유 — 영화 자막 번역가 🎯

영화 자막 번역가가 있다고 해봅시다. B-자막은 앞뒤 문맥을 모두 봐야 번역할 수 있습니다. 그래서 뒤 장면을 먼저 읽고, 다시 돌아와서 번역합니다.

→ 파일에는 "뒤 장면"이 먼저 저장되어 있어야 한다.

한 줄 요약

B-frame은 미래를 참조하므로, 그 미래(P)를 먼저 디코딩해야 한다. 그래서 파일 저장 순서 ≠ 화면 재생 순서가 된다.

이걸 처리하기 위해 디코더는 B-frame 직전의 P-frame을 미리 받아둘 만큼의 버퍼가 필요하고, 그 버퍼가 곧 지연(latency)으로 이어집니다. 실시간 통화에서 B-frame을 끄는 이유가 여기 있습니다.

PTS vs DTS — 컨테이너가 두 순서를 관리하는 법

디코딩 순서와 재생 순서가 다르다는 건 컨테이너 레벨에서 두 개의 타임스탬프를 따로 들고 있어야 한다는 뜻입니다.

타임스탬프풀네임의미
PTSPresentation Time Stamp화면에 보여줄 시각
DTSDecode Time Stamp디코더가 처리할 시각
B-frame 없는 경우:   PTS = DTS  (한 줄로 흐름)
B-frame 있는 경우:   DTS < PTS  (디코딩이 먼저, 재생은 나중)

예: I B B P
PTS 순서:  I(0)  B(1)  B(2)  P(3)
DTS 순서:  I(0)  P(3)  B(1)  B(2)
            ↑    ↑
        같음   P가 B보다 먼저 디코딩되어야 함

실무 영향:

  • AV sync 디버깅: ffprobe -show_frames -select_streams v 출력에서 ptspkt_dts가 같으면 B-frame 없음, 다르면 B-frame 있음 (FFmpeg 5.0 미만 구버전은 pkt_pts 필드 사용). PTS/DTS 불연속 발견 시 segment 경계 이슈 의심.
  • HLS segment 경계: B-frame이 포함된 GOP를 segment로 자를 때, 마지막 B-frame의 DTS가 다음 segment의 첫 프레임 DTS보다 클 수 있어 sample dependency가 꼬일 위험이 있음 → Closed GOP 필수의 또 다른 이유.
  • iOS AV sync 증상: 비정상 segment에서 PTS/DTS 정렬이 깨지면 오디오 PTS와 비디오 PTS 매칭이 어긋남 → 입술 동기 오차.

단점 3가지

단점메커니즘영향
디코더 지연뒤 P-frame 먼저 받아 버퍼링실시간 스트리밍에서 치명적
디코딩 복잡도 ↑앞뒤 둘 다 참조해서 합성CPU/GPU 부하 증가
에러 전파앞뒤 frame 중 하나만 손상돼도 B 깨짐손실 환경에서 화질 불안

실무 설정 — 라이브 OFF, VOD ON

🔴 B-frame 끄는 경우 (라이브)

- 실시간 방송 (OBS, 라이브 스트리밍)
- 초저지연 화상통화 (WebRTC, SIP)
- 인코딩 속도 최우선

ffmpeg 설정:
  -bf 0
x264 직접:
  --bframes 0

🟢 B-frame 켜는 경우 (VOD)

- VOD (다시보기, 영화, 강의 녹화)
- 파일 크기 최소화 우선
- 화질 최대화 (같은 비트레이트에서)
- 인코딩 시간 여유 있음

ffmpeg 설정:
  -bf 2   ← B-frame 2개 연속 허용
  -bf 3   ← 더 높은 압축, 더 큰 지연

WebRTC는 사양상 B-frame 미사용이 일반적. LL-HLS도 part 단위가 워낙 짧아서 B-frame 효용이 적습니다 (#40).

HLS Segment와 B-frame의 상호작용

HLS split 규칙: I-frame 위치에서만 자름.
B-frame 있어도 규칙 동일.

I B B P B B P ... I
↑                 ↑
여기서만 자름     여기서만 자름

추가 주의:
B-frame 때문에 디코딩 순서 ≠ 재생 순서
→ segment 경계의 PTS(재생시간) 계산 꼬일 위험
→ iOS 플레이어에서 AV sync 어긋나는 원인

라이브 + HLS + B-frame 조합은 디버깅이 가장 어렵습니다. 그래서 라이브에서는 끄는 게 표준.


10. 실무 디버깅 역추적 🔍

#EXTINF:13 한 줄에서 시작해 CPU 모니터까지 거꾸로 따라갑니다.

1. HLS 로그에서 #EXTINF:13 발견
        ↓
2. "EXT-X-TARGETDURATION:2 인데 왜 13?"
        ↓
3. segment 왜 못 잘랐지?
   → HLS는 I-frame 위치에서만 자름
        ↓
4. I-frame이 늦게 왔나?
   → keyint=60인데 13초가 걸렸다 = fps가 ~5
        ↓
5. fps가 왜 떨어졌나?
   → 인코더 출력 fps 확인 (ffmpeg -progress)
        ↓
6. 인코더가 왜 느린가?
   → 입력 fps OK? 캡처 카드 정상?
   → CPU/GPU 사용률 확인 (top / htop / nvidia-smi)
        ↓
7. CPU 100% 확인 → 인코더 thread starvation
        ↓
8. 처방:
   - preset 다운 (slow → fast / veryfast)
   - 해상도/비트레이트 다운
   - HW 인코더 사용 (NVENC/QSV/VideoToolbox)
   - B-frame 끄기 (-bf 0)
   - 인코더 노드 스케일아웃

한 줄 로그에서 8단계 인과 추적이 가능하다는 게 핵심. 영상 압축 구조를 모르면 "HLS 버그"로 오인하고 패키저만 뒤집니다.


11. 한 장 요약 — 최종 암기 카드

개념한 줄 요약
I-frame완전한 사진 한 장 (큼, 독립)
P-frame"앞이랑 이만큼 달라" (중간)
B-frame"앞아 뒤야, 나 너네 사이 어딘가야" (가장 작음)
GOPI-frame ~ 다음 I-frame 직전 묶음
keyint=6060 프레임마다 I-frame (≠ 60초!)
Closed GOPHLS 필수 — GOP 경계에서 참조 차단
HLS split반드시 I-frame 위치에서만
#EXTINF:18인코딩 파이프라인 이상 증거 (주로 CPU, §6-1 대안 참조)
B-frame 라이브OFF (-bf 0)
B-frame VODON (-bf 2)
압축 효율:      B  >  P  >  I
디코딩 난이도:   B  >  P  >  I
실시간 지연:    B  >  P  >  I

12. SA 의사결정 체크리스트

라이브 송출 셋업 전에 답해야 할 8가지:

질문영향
1. 목표 fps와 keyint은?GOP의 실제 시간 = keyint ÷ fps
2. Closed GOP인가?HLS segment 깨짐 방지
3. scene-cut detection은 OFF인가?GOP 경계 무작위 삽입 방지 (#37)
4. CBR인가 VBR인가?라이브는 CBR (#37)
5. 인코더 선택은? (SW/HW)SW(x264): 화질↑ CPU↑ / HW(NVENC/QSV/VideoToolbox): 화질△ CPU↓↓ — CPU 부하 자체를 차단
6. 인코더 preset과 CPU 여유는?preset이 너무 무거우면 fps 미달
7. B-frame 사용 여부?라이브 OFF / VOD ON
8. HLS EXT-X-TARGETDURATION 설정값?segment 길이 ÷ GOP의 정수배여야 깔끔

이 8가지를 사전에 확정하면 #EXTINF:18 로그를 만날 일이 거의 없습니다.


13. 한 줄 결론

HLS segment 길이는 인코더의 I-frame 박힘에 의해 결정되고, I-frame은 GOP 설정과 fps에 의해 결정되며, fps는 주로 CPU 여유에 의해 결정된다. 그래서 #EXTINF:18 한 줄로 인코딩 파이프라인 전체를 역추적할 수 있다 (단, frame drop / packager 설정 / PTS 불연속 / clock drift 같은 대안 경로도 함께 점검 — §6-1 참조). keyint은 시간이 아니라 프레임 개수다.


관련 글

참고 자료

© 2026 Frank Kim. All rights reserved.