영상 압축과 HLS 장애 분석 — I/P/B-frame, GOP, Segment
I·P·B frame과 GOP가 압축과 random access에 맡는 역할을 설명하고, frame 기반 keyint 설정이 실제 시간 길이로 어떻게 환산되는지 살펴봅니다. 입력 frame rate 저하, keyframe 간격, HLS segment duration이 함께 변하는 장애를 log와 ffprobe로 구분하는 절차도 다룹니다.
목차(36개 항목)
- 0. `#EXTINF:18`은 원인이 아니라 관찰값이다
- 1. 영상의 본질 — 사진 × N장
2. I-frame vs P-frame vs B-frame — 3종류의 프레임
3. GOP — Group Of Pictures
- 4. 정상 상태 — 30fps + keyint=60
- 5. 처리량 부족 가설 — fps가 무너지는 경우
- 6. 가능한 인과 사슬
6-1. 같은 증상, 다른 원인 — `#EXTINF:18` 대안 경로
7. HLS 세그먼트 경계와 랜덤 액세스
- 8. 플레이어 영향은 구현과 버퍼 정책에 따라 다르다
9. B-frame Deep Dive — 지연과 압축 효율의 교환
- 10. 실무 디버깅 역추적 🔍
- 11. 한 장 요약 — 최종 암기 카드
- 12. SA 의사결정 체크리스트
- 13. 한 줄 결론
- 관련 글
- 참고 자료
"라이브 송출 중에 화면이 멈춰요. 로그를 보니 #EXTINF:18이 찍혀 있습니다." EXTINF가 길다는 사실만으로 원인을 확정할 수는 없습니다. 실제 세그먼트 경계와 키프레임, 타임스탬프, 패키저 설정, 인코더 처리량을 함께 확인해야 합니다. 이 글은 I/P/B-frame, GOP, keyint을 바탕으로 그 진단 순서를 정리합니다.
0. #EXTINF:18은 원인이 아니라 관찰값이다
긴 HLS 세그먼트는 키프레임 간격, 입력 PTS, 패키저의 절단 정책, 처리 지연 가운데 하나 이상을 점검하라는 신호다.
keyint이 프레임 수로 지정되는 인코더도 있지만,EXTINF한 줄만으로 CPU 과부하나 GOP 이상을 확정해서는 안 된다.
흔한 오해와 정정:
| 오해 | 정확한 이해 |
|---|---|
keyint=2 = 2초마다 keyframe | 2 프레임마다 keyframe입니다. 시간 아니라 프레임 개수입니다. |
keyint=60 = 정확히 60프레임마다 I-frame | keyint은 최대값입니다. scenecut이 켜져 있으면 더 일찍 삽입될 수 있습니다. |
| HLS segment 길이는 플레이리스트 옵션만 정한다 | 패키저 목표 시간과 실제 키프레임·타임스탬프·절단 정책이 함께 결정 |
| 라이브에서는 B-frame을 반드시 꺼야 한다 | 지연·압축 효율·디코더 호환성을 보고 결정. Apple·YouTube 권장안도 B-frame을 일률적으로 금지하지 않음 |
#EXTINF:18이면 인코더가 문제다 | 관찰값만으로는 확정 불가. 패킷·프레임 타임스탬프와 패키저 설정부터 대조 |
1. 영상의 본질 — 사진 × N장
| 항목 | 수치 |
|---|---|
| 1초 | 30장 (30fps) |
| 1080p 8-bit YUV 4:2:0 한 프레임 | 약 3.11MB |
| 1초 원본 | 약 93MB |
| 1시간 원본 | 약 336GB |
결론: 압축 없이는 인터넷으로 영상을 보낼 방법이 없습니다. 영상 코덱(H.264/H.265/AV1)이 푸는 문제가 정확히 이것 — "사진 × N장을 어떻게 작게 만들 것인가".
2. I-frame vs P-frame vs B-frame — 3종류의 프레임
정의
| 종류 | 풀네임 | 저장 내용 | 참조 | 크기 |
|---|---|---|---|---|
| I-frame | Intra frame | 프레임 내부 정보로 부호화 | 다른 프레임 참조 없음 | 대체로 큼 |
| P-frame | Predicted frame | 앞 프레임과의 변화 | 앞만 참조 | 중간 |
| B-frame | Bidirectionally predicted frame | 다른 참조 프레임과의 예측 정보 | 과거·미래 참조 그림을 사용할 수 있음 | 대체로 작음 |
직관 — 공이 날아가는 장면
P-frame은 앞만 봐서 변화량을 적고, B-frame은 앞뒤를 다 봐서 더 작게 줄입니다.
압축 효율
B-frame이 가장 효율적이지만, 공짜는 아닙니다 (뒤에서 다룸).
3. GOP — Group Of Pictures
정의
GOP는 독립적으로 복호를 시작할 수 있는 그림과 그에 종속된 그림을 묶어 부르는 실무 용어다. 정확한 경계는 코덱의 랜덤 액세스 구조와 인코더 설정에 따라 달라진다.
B-frame을 포함한 전형적인 GOP는 이렇게 생깁니다:
keyint — 가장 헷갈리는 파라미터
흔한 착각:
keyint=60= "60초마다 keyframe" 실제: "60 프레임마다 keyframe (최대값)"
정확히 말하면 — keyint은 "최대" GOP 길이
x264에서 --keyint N은 maximum GOP length입니다. 두 가지 옵션이 함께 영향을 줍니다:
| 옵션 | 동작 |
|---|---|
--keyint | 최대 키프레임 간격 |
--min-keyint | 최소 키프레임 간격 |
--scenecut | 장면 전환에 따른 조기 키프레임 삽입 제어 |
--scenecut 0은 일부 FFmpeg/x264 파이프라인에서 경계를 고정하기 위한 선택이지 HLS 규격의 필수 조건은 아닙니다. keyint=60도 최대 간격이므로 다른 키프레임 강제 옵션과 함께 해석해야 합니다.
학교 출석부 비유 🏫
선생님이 말합니다: "60번째 학생마다 출석 불러".
근데 학생이 하루에 몇 명 오느냐에 따라 시간이 달라집니다.
- 하루에 30명씩 오면 → 2일 만에 60번째
- 하루에 5명씩 오면 → 12일 만에 60번째
keyint은 시간이 아니라 "몇 번째 프레임마다" 라는 카운트 규칙입니다.
fps에 따라 실제 시간이 달라진다
30fps 정상 케이스
🔴 fps 떨어진 케이스
택배 비유 📦
keyint=60= "60번째 택배마다 도장 찍어"정상: 하루에 30개 택배 옴 → 60개 = 2일 만에 도장 CPU 터짐: 하루에 5개밖에 못 처리 → 60개 = 12일 만에 도장
도장 찍는 규칙(
keyint=60)은 안 바뀝니다. 그냥 택배가 느리게 오는 것뿐.
시간이 아니라 프레임 개수. fps가 바뀌면 GOP의 실제 시간 길이도 같이 바뀝니다. 이게 뒤에 나올 "CPU 폭주 → segment 폭증"의 핵심 메커니즘 — 인코더에 설정된 규칙은 그대로지만, fps가 떨어지면 같은 keyint이 더 긴 시간으로 펼쳐집니다.
"GOP=2초"를 프레임 수로 바꾸는 구성
이 헷갈림의 진짜 원인은 사람이 설정하는 단위와 인코더 내부 단위가 다르다는 점입니다.
사람이 설정하는 값
사람 입장에서는 "2초마다 keyframe 넣어"가 가장 직관적입니다.
인코더 내부 변환
x264의 keyint처럼 프레임 수를 받는 옵션이 있는 반면, FFmpeg의 -force_key_frames처럼 타임스탬프 기반 표현식도 있습니다. 어떤 계층이 초를 프레임 수로 변환하는지 설정 파일과 실제 명령에서 확인해야 합니다.
정상일 땐 둘이 같아 보임
CPU 망하면 차이 발생
진실 한 장 정리
| 사람이 말하는 것 | 실제 인코더 내부 |
|---|---|
| GOP = 2초 | keyint = 60 |
| 시간 단위 | frame 개수 단위 |
한 줄: "2초 GOP는 설정값이고, 인코더는 종종 그걸 60 프레임으로 변환해서 처리한다."
평소엔 둘이 같아 보여서 헷갈리지만, fps가 흔들리는 순간 정체가 드러납니다.
Open GOP vs Closed GOP
| 종류 | B-frame이 이전 GOP 참조 가능? | 용도 |
|---|---|---|
| Open GOP | 가능 | VOD, 압축률 우선 |
| Closed GOP | 불가 (GOP 경계에서 차단) | 독립 세그먼트·랜덤 액세스에 유리 |
독립 세그먼트를 만들려면 경계에서 이전 세그먼트의 참조가 필요하지 않도록 구성해야 합니다. Closed GOP가 흔한 해법이지만, HLS RFC 자체가 인코더 옵션 이름인 Closed GOP를 의무화하지는 않습니다.
4. 정상 상태 — 30fps + keyint=60
깔끔하게 2초마다 I-frame이 박히고, HLS도 2초마다 자릅니다. #EXT-X-TARGETDURATION:2 약속과 실제가 일치.
5. 처리량 부족 가설 — fps가 무너지는 경우
아래는 인코더가 프레임을 드롭하지 않고 지연된 입력을 계속 처리하며, 출력 프레임률이 실제로 5fps까지 낮아진 경우의 계산 예입니다.
이 조건에서는 같은 keyint=60도 12초에 해당합니다. 실제 시스템이 프레임을 드롭하는지, 타임스탬프를 유지한 채 늦게 처리하는지에 따라 결과는 달라집니다.
6. 가능한 인과 사슬
이 흐름은 가능한 가설 중 하나입니다. 패키저와 타임스탬프를 확인하기 전에는 인코더를 원인으로 확정할 수 없습니다.
전제 조건: 이 인과 사슬은 인코더가 프레임을 드롭하지 않고 큐잉하는 경우에 발현됩니다. OBS의 "Frames missed due to rendering lag"이 늘어나는 모드, 또는 ffmpeg에서
-re없이 캡처 디바이스를 직접 받는 케이스. 인코더가 frame drop 정책이면 GOP 시간은 유지되지만 화질이 떨어집니다 (다음 섹션 참조).
6-1. 같은 증상, 다른 원인 — #EXTINF:18 대안 경로
동일한 관찰값이 다른 경로로도 발생합니다. 디버깅 시 함께 확인할 항목은 다음과 같습니다.
A. Frame drop 모드 — fps는 유지, 화질이 떨어짐
B. Packager 설정 오류 — 인코더는 무죄
C. 입력 소스 burst / PTS 불연속
D. Clock drift — 인코더와 시스템 시계 불일치
진단 우선순위
| 순서 | 확인 항목 | 명령 |
|---|---|---|
| 1 | Frame drop 카운터 | OBS UI, ffmpeg -progress drop_frames |
| 2 | CPU 사용률 | top, htop, nvidia-smi (HW 인코더) |
| 3 | 인코더 설정 vs packager 설정 | 양쪽 config diff |
| 4 | 입력 소스 PTS 연속성 | ffprobe -show_packets |
| 5 | 시스템 시계 | chronyc tracking, timedatectl |
#EXTINF:18을 보면 playlist만 보지 말고 실제 세그먼트의 첫 프레임, 키프레임 간격, PTS gap, frame drop, 패키저 설정을 같은 시간대에 대조합니다.
7. HLS 세그먼트 경계와 랜덤 액세스
HLS Split 규칙
HLS 비디오 세그먼트는 독립적으로 디코더를 초기화하고 재생을 시작할 수 있게 만드는 것이 권장된다. H.264 세그먼트는 IDR을 포함하는 것이 권장되지만, RFC의 표현은
MUST가 아니라SHOULD다.
이유는 디코딩 의존성 때문:
정상 vs 사고
| 상태 | I-frame 주기 | HLS segment 길이 | 로그 |
|---|---|---|---|
| 정상 | 2초 | 2초 | #EXTINF:2.0 |
| 🟡 살짝 부하 | 4~6초 | 4~6초 | #EXTINF:4.5 |
| 🔴 CPU 터짐 | 12~18초 | 12~18초 | #EXTINF:18 |
RFC 8216은 각 EXTINF 값을 가장 가까운 정수로 반올림했을 때 EXT-X-TARGETDURATION 이하가 되도록 요구합니다. 위반 여부는 원본 소수 값을 단순 비교하지 말고 이 규칙으로 판단합니다.
8. 플레이어 영향은 구현과 버퍼 정책에 따라 다르다
플레이어는 EXT-X-TARGETDURATION을 재요청 주기와 live edge 계산 등에 사용합니다. 잘못된 값은 재생 동작을 불안정하게 만들 수 있지만, 특정 버퍼 크기나 실패 형태는 구현별로 다릅니다.
| 증상 | 원인 |
|---|---|
| seek 이상 | 타임라인 계산이 segment 길이 기반인데 빗나감 |
| 버퍼 증가 | 긴 세그먼트의 다운로드·디코딩 시간이 늘어남 |
| 화면 멈춤 | 다음 segment 못 받음 (또는 받았는데 디코딩 지연) |
| AV sync 이상 | 오디오/비디오 PTS 계산 꼬임 (특히 B-frame 환경) |
RFC 8216:
EXT-X-TARGETDURATION검증은 각EXTINF를 가장 가까운 정수로 반올림해 수행합니다. 특정 iOS 내부 버퍼 동작은 공개 규격만으로 단정하지 않습니다.
9. B-frame Deep Dive — 지연과 압축 효율의 교환
동작 원리
P-frame은 과거만 봅니다:
B-frame은 과거 + 미래를 동시에 봅니다:
디코딩 순서 ≠ 재생 순서
B-frame의 핵심 함정: 저장/전송 순서와 화면 재생 순서가 다릅니다. 이유를 단계별로 풀어봅시다.
Step 1 — 문제 제기
B-frame은 미래 프레임을 참조합니다.
B(1번)을 디코딩하려면?
- 과거 I(0번) 필요 → 이미 있음
- 미래 P(3번) 필요 → 아직 안 왔음
B-frame을 재생하려면 미래의 P-frame을 먼저 디코딩해놔야 한다.
Step 2 — 그래서 저장 순서를 바꾼다
Step 3 — 디코더 내부 흐름
디코딩 시점과 화면 출력 시점이 분리되어 있다는 게 핵심. 디코더는 미래 프레임을 먼저 디코딩한 뒤 버퍼에 저장해두고, 재생 타이밍이 되면 꺼내서 출력합니다.
비유 — 영화 자막 번역가
영화 자막 번역가가 있다고 해봅시다. B-자막은 앞뒤 문맥을 모두 봐야 번역할 수 있습니다. 그래서 뒤 장면을 먼저 읽고, 다시 돌아와서 번역합니다.
→ 파일에는 "뒤 장면"이 먼저 저장되어 있어야 한다.
한 줄 요약
B-frame은 미래를 참조하므로, 그 미래(P)를 먼저 디코딩해야 한다. 그래서 파일 저장 순서 ≠ 화면 재생 순서가 된다.
이걸 처리하기 위해 디코더는 B-frame 직전의 P-frame을 미리 받아둘 만큼의 버퍼가 필요하고, 그 버퍼가 곧 지연(latency)으로 이어집니다. 실시간 통화에서 B-frame을 끄는 이유가 여기 있습니다.
PTS vs DTS — 컨테이너가 두 순서를 관리하는 법
디코딩 순서와 재생 순서가 다르다는 건 컨테이너 레벨에서 두 개의 타임스탬프를 따로 들고 있어야 한다는 뜻입니다.
| 타임스탬프 | 풀네임 | 의미 |
|---|---|---|
| PTS | Presentation Time Stamp | 화면에 보여줄 시각 |
| DTS | Decode Time Stamp | 디코더가 처리할 시각 |
실무 영향:
- 프레임 구조 확인: PTS/DTS 차이만으로 B-frame 유무를 단정하지 말고
ffprobe -show_frames의pict_type, 키프레임 플래그와 비트스트림을 함께 확인합니다. - 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 깨짐 | 손실 환경에서 화질 불안 |
실무 설정 — 서비스 목표에 따라 선택
B-frame을 줄이거나 끄는 경우
B-frame을 사용하는 경우
초저지연 서비스에서는 재정렬 지연 때문에 B-frame을 줄이거나 끌 수 있습니다. 반면 일반 라이브 HLS에서도 인코더·플랫폼 권장안에 따라 B-frame을 사용할 수 있습니다. YouTube의 공식 가이드는 라이브 H.264에 B-frame 2개를 권장합니다.
HLS Segment와 B-frame의 상호작용
라이브에서 B-frame을 쓸지는 목표 지연, 플랫폼 호환성, 압축 효율을 함께 보고 정합니다.
10. 실무 디버깅 역추적 🔍
#EXTINF:13 한 줄에서 시작해 CPU 모니터까지 거꾸로 따라갑니다.
이 순서는 가설을 좁히는 예시입니다. 한 줄 로그만으로 8단계의 인과를 확정할 수는 없습니다.
11. 한 장 요약 — 최종 암기 카드
| 개념 | 한 줄 요약 |
|---|---|
| I-frame | 다른 프레임을 참조하지 않는 intra-coded picture. 모든 I-frame이 랜덤 액세스 지점인 것은 아님 |
| P-frame | "앞이랑 이만큼 달라" (중간) |
| B-frame | "앞아 뒤야, 나 너네 사이 어딘가야" (가장 작음) |
| GOP | I-frame ~ 다음 I-frame 직전 묶음 |
| keyint=60 | 최대 키프레임 간격 60프레임(인코더별 의미 확인) |
| Closed GOP | 세그먼트 독립성과 랜덤 액세스에 유리 |
| HLS split | 패키저 목표와 랜덤 액세스 경계를 함께 확인 |
#EXTINF:18 | 긴 세그먼트라는 관찰값. 키프레임·PTS·패키저·처리량을 함께 점검 |
| B-frame | 지연·압축 효율·호환성에 따라 개수 결정 |
12. SA 의사결정 체크리스트
라이브 송출 셋업 전에 답해야 할 8가지:
| 질문 | 영향 |
|---|---|
| 1. 목표 fps와 keyint은? | GOP의 실제 시간 = keyint ÷ fps |
| 2. Closed GOP인가? | HLS segment 깨짐 방지 |
| 3. 렌디션 간 키프레임이 정렬되는가? | ABR 전환과 세그먼트 경계 안정성 |
| 4. rate control은 서비스 요구에 맞는가? | 대역폭 변동·품질·지연의 교환 |
| 5. 인코더 선택은? (SW/HW) | 품질·처리량·전력은 코덱과 기기별 실측 필요 |
| 6. 인코더 preset과 CPU 여유는? | preset이 너무 무거우면 fps 미달 |
| 7. B-frame 사용 여부? | 목표 지연·압축 효율·플랫폼 권장안 |
8. HLS EXT-X-TARGETDURATION 설정값? | 반올림한 모든 EXTINF가 이 값 이하인지 검증 |
이 항목을 사전에 확인하면 긴 세그먼트의 원인을 더 빨리 좁힐 수 있습니다.
13. 한 줄 결론
#EXTINF:18은 긴 세그먼트가 만들어졌다는 관찰값이다. 실제 키프레임 간격, PTS 연속성, 패키저 절단 정책, frame drop과 처리량을 같은 시간대에 대조해야 원인을 확정할 수 있다.
관련 글
- #37 GOP-세그먼트 정렬의 산수 — Agora Media Push에서 YouTube/Twitch까지 — GOP 설정의 산수
- #38 ABR Ladder의 Layer B 정렬 — 화질 전환 시 키프레임 동기화 — 멀티 ladder 정렬
- #40 라이브 스트리밍 프로토콜 선택 — HLS / LL-HLS / WebRTC 의사결정 — 프로토콜 비교
- #15 M3U8과 TS 파일의 모든 것 — HLS Deep Dive — HLS 컨테이너 구조
참고 자료
- RFC 8216 — HTTP Live Streaming —
EXT-X-TARGETDURATION규약 - FFmpeg Formats — HLS muxer —
hls_time,split_by_time등 HLS 절단 옵션 - Apple HLS Authoring Specification — Apple 권장 인코딩 가이드
- YouTube Live Encoder Settings — 키프레임과 B-frame 권장값
- ITU-T H.264 / ISO 14496-10 — I/P/B-frame, NAL, GOP 표준 정의