I/P/B Frames and GOP
프레임 종류와 GOP 구조
비디오는 모든 프레임을 똑같이 압축하지 않는다. I·P·B 세 종류와 그것들의 구조(GOP)가 스트리밍·랜덤 액세스·압축률을 결정한다.
Overview
지금까지 'P-프레임은 이전을 참조한다' 정도로만 다뤘다. 이번 장은 그 분류를 정확히 짚고, 그 프레임들이 모여 만드는 GOP(Group of Pictures) 구조와 그 구조가 실무에 미치는 영향을 본다.
GOP 설정 하나가 스트리밍 화질, 랜덤 액세스 속도, 라이브 지연을 동시에 결정한다. 이걸 모르면 라이브 방송 운영을 못 한다.
- I/P/B 세 프레임의 압축 방식과 크기 비교를 안다
- GOP의 정의와 구조(IBBP...IBBP)를 그릴 수 있다
- 키프레임 간격이 스트리밍·검색·랜덤 액세스에 미치는 영향을 안다
- B-프레임이 왜 가장 작고 가장 디코딩이 복잡한지 이해한다
- 라이브 방송과 VOD에서 GOP 설정이 어떻게 다른지 안다
Sections
6.1 세 종류의 프레임
| 타입 | 풀네임 | 압축 방식 | 평균 크기 | 자기완결 | |---|---|---|---|---| | I | Intra-coded | 자기 자신만(인트라 예측) | 1× (큼) | ✅ | | P | Predicted | 이전 I/P 참조 | ~1/5 | ❌ | | B | Bidirectional | 이전 + 이후 참조 | ~1/20 | ❌ |
I-프레임: JPEG 한 장과 비슷한 자기완결적 프레임. 인트라 예측 + DCT + 양자화. 가장 큰 압축률은 못 내지만 다른 프레임에 의존하지 않음 → 여기서 재생 시작·시킹·랜덤 액세스 가능.
P-프레임: 이전 I 또는 P 프레임 참조. 모션 보상으로 예측 + 잔차만 저장. I 크기의 1/5~1/10. 화면이 천천히 변하면 더 작아짐.
B-프레임: 이전 + 이후 둘 다 참조. "앞뒤 보면 가운데는 이렇게 됐을 거다"의 양방향 예측. 가장 작지만 가장 복잡 — 디코딩하려면 다음 P/I 프레임이 먼저 도착해야 함.
6.2 GOP (Group of Pictures) — 반복 단위
프레임 시퀀스의 반복 단위가 GOP. 전형적인 GOP:
I B B P B B P B B P B B I B B P ...
← --- GOP 1 (12프레임) --- → ← --- GOP 2 ... →
GOP 길이 = 12 (1초당 30fps이면 0.4초). 이 GOP 안의 모든 P·B는 앞쪽 I에 의존. 다음 I가 새 GOP 시작.
GOP 구조의 두 축: 1. GOP 길이(GOP size): I 프레임 사이 간격. 짧으면(예: 0.5초) 시킹 빠르고 에러 복구 쉽지만 압축률 손해. 길면(예: 5초) 압축률 좋지만 시킹 느리고 에러 누적 위험. 2. B-프레임 개수: 두 P/I 사이에 들어가는 B 개수. 0(B 없음)~3개가 일반적. 많을수록 압축 좋지만 디코더 복잡도·지연 증가.
GOP 길이는 도메인별로: - 일반 VOD: 2~5초 (60~150 프레임) - 라이브 스트리밍: 1~2초 (지연·복구 트레이드오프) - 화상회의: 0.5초 이하 또는 I-frame 없음(SVC) - Blu-ray: 0.5초 이하 (시킹·반응성 우선)
6.3 키프레임 간격과 실무
GOP의 시작 I 프레임을 키프레임(keyframe) 또는 IDR 프레임(Instantaneous Decoder Refresh) 이라고 부른다. IDR는 "이 프레임 이후로 이전 어떤 프레임도 참조 안 함"을 보장. 시킹의 진입점.
ffmpeg의 핵심 옵션:
``bash
# 키프레임 간격 60 프레임 (2초 @ 30fps)
ffmpeg -i input.mp4 -c:v libx264 -g 60 -keyint_min 60 -sc_threshold 0 ...
``
- -g 60: 최대 GOP 길이 60
- -keyint_min 60: 최소 GOP 길이 60 (강제)
- -sc_threshold 0: 장면 전환에서 자동 키프레임 삽입 안 함 (일정 간격 유지)
왜 일정 간격이 중요? 스트리밍(HLS·DASH)에선 세그먼트 경계가 키프레임에 맞춰야 함. 그래야 클라이언트가 세그먼트 단위로 받아 디코딩할 수 있음. 키프레임 간격이 들쭉날쭉하면 segmenter가 깨짐.
표준 패턴: HLS 세그먼트 2초 → 키프레임 간격 2초. DASH도 비슷.
6.4 B-프레임의 비용 — 인코딩·디코딩·지연
B-프레임은 압축률에선 최고지만 세 가지 비용이 있다.
인코딩 비용: 양방향 검색이라 모션 추정 2배. preset slow일수록 B 검색이 더 철저해짐.
디코딩 지연(reorder delay): B를 디코딩하려면 "다음 P"가 먼저 도착해야 한다. 즉 화면 출력 순서와 디코딩 순서가 다르다.
예시 GOP: I B B P (출력 순서)
- 디코딩 순서: I → P → B → B (P를 먼저 디코딩해야 B 디코딩 가능)
- 디코딩 후 화면에 표시할 때 다시 순서 재배열
라이브 방송에서 치명적: B 프레임 N개가 들어가면 출력이 N프레임만큼 지연. 30fps에서 B 2개면 약 67ms 추가 지연. 화상회의·게임 스트리밍에선 이 지연이 컴플레인 원인.
해결책:
- 라이브: -bf 0 (B 프레임 끄기) 또는 -bf 1 (최소)
- VOD: -bf 3 (기본) — 압축률 우선
- 화상회의: B 프레임 + I 프레임 둘 다 최소화, P만으로 가는 모드도 있음
6.5 GOP 길이와 에러 복구
스트리밍 도중 패킷이 손실됐다고 치자. 한 P 프레임이 깨지면 그 뒤의 P·B 프레임이 모두 영향. "drift"라고 한다. 다음 I 프레임이 와야 회복.
즉 GOP 길이가 에러 복구 시간을 결정. GOP 5초면 최악의 경우 5초간 화면이 깨진 채 유지. GOP 1초면 최대 1초.
라이브 스트리밍의 트레이드오프: - 짧은 GOP (1초): 에러 복구 빠름, 시킹 빠름, 압축률 손해 (~10%) - 긴 GOP (5초): 에러 복구 느림, 압축률 좋음 (~10% 향상)
현실의 선택: - YouTube Live: 2초 GOP - Twitch: 2초 GOP (HLS 세그먼트와 동기화) - Netflix VOD: 2~4초 GOP - 한국 IPTV: 0.5초 GOP (채널 전환 반응성)
GOP 길이는 "라이브 도메인의 reactivity vs 압축률" 트레이드오프 그 자체.
일기를 어떻게 쓸까? 세 가지 방식이 있다.
(I) 새 일기: 매일 모든 걸 처음부터 쓴다. 누가 봐도 그 날의 모든 게 다 적혀 있음. 그러나 길어진다.
(P) 어제 일기 + 변경분: "어제 일기와 같은데, 점심에 김치찌개 대신 된장찌개를 먹었다"만 쓴다. 짧지만 어제 일기를 같이 봐야 이해 가능.
(B) 앞뒤 일기 + 추측: "어제는 비, 오늘은 비, 그러니 오늘 점심 메뉴는 어제와 비슷했을 거다"라고 앞뒤 일기로 가운데를 추측한다. 가장 짧지만 앞뒤 둘 다 필요.
비디오 코덱의 I·P·B가 정확히 이렇다. I는 자기완결이라 크지만 "여기서 재생 시작 가능". P는 작지만 이전 의존. B는 가장 작지만 양방향 의존 → 디코딩 순서가 출력 순서와 다름.
GOP는 "몇 일에 한 번 새 일기를 쓸까"의 선택이다. 매일 새로 쓰면 일기장이 두꺼워지지만 어느 날을 펴도 그 날을 알 수 있다. 한 달에 한 번 새로 쓰면 일기장은 얇아지지만 중간을 보려면 처음부터 다 읽어야 한다.
ffprobe로 영상의 GOP 구조를 분석하고, GOP 길이가 다른 인코딩 결과를 비교해 보자.
import subprocess, json
def analyze_gop(path):
"""Extract frame types from a video and report GOP stats."""
out = subprocess.run([
'ffprobe', '-v', 'error',
'-select_streams', 'v:0',
'-show_entries', 'frame=pict_type',
'-of', 'json', path
], capture_output=True, text=True, check=True)
frames = json.loads(out.stdout)['frames']
types = [f['pict_type'] for f in frames]
counts = {t: types.count(t) for t in set(types)}
print(f'{path}: {counts}')
# Find GOP boundaries (I-frames)
i_positions = [i for i, t in enumerate(types) if t == 'I']
if len(i_positions) > 1:
gops = [i_positions[i+1] - i_positions[i] for i in range(len(i_positions)-1)]
print(f' 평균 GOP 길이: {sum(gops)/len(gops):.1f} 프레임')
print(f' 최소·최대 GOP: {min(gops)}~{max(gops)}')
return types
# Encode with different GOP settings
for gop in [30, 60, 120, 240]:
out = f'gop_{gop}.mp4'
subprocess.run([
'ffmpeg', '-y', '-i', 'sample.mp4',
'-c:v', 'libx264',
'-g', str(gop),
'-keyint_min', str(gop),
'-sc_threshold', '0',
'-bf', '2', # B-frames: 2 between I/P
'-preset', 'medium', '-crf', '23', '-an', out
], check=True, capture_output=True)
analyze_gop(out)
import os
print(f' 파일 크기: {os.path.getsize(out)/1e6:.2f} MB\n')
# 결과 예시 (1080p 30fps 10초 영상):
# GOP 30 (1초): {I: 11, P: 100, B: 189}, 파일 4.2 MB
# GOP 60 (2초): {I: 6, P: 98, B: 196}, 파일 3.8 MB
# GOP 120 (4초): {I: 3, P: 99, B: 198}, 파일 3.5 MB
# GOP 240 (8초): {I: 2, P: 100, B: 198}, 파일 3.4 MB
# → GOP 길수록 압축 좋지만 시킹·복구 느림
ffprobe가 프레임 타입 시퀀스를 그대로 보여준다. GOP 1초 vs 8초의 파일 크기 차이가 약 20%. 이 20%를 잃더라도 라이브 방송엔 GOP 짧은 게 필수 — 시킹·에러 복구가 즉각적이어야 함. VOD는 GOP 길게 가서 압축률 최대화. 도메인별로 다른 답이 나오는 대표적 트레이드오프.
✅ 시니어가 보는 것
- I/P/B의 크기·디코딩 순서·자기완결성을 안다
- GOP 길이의 도메인별 적정값을 안다
- HLS/DASH 세그먼트 길이와 키프레임 간격의 관계 안다
- B-frame 개수와 라이브 지연의 관계 안다
⚠️ 레드 플래그
- GOP 길이를 임의로 설정하고 세그먼트와 안 맞춤
- 라이브에서 B-frame 3개 쓰고 지연 컴플레인 받음
- 장면 전환에서 자동 키프레임을 끄지 않고 segmenter 깨뜨림
- I/P/B의 디코딩 순서가 출력 순서와 다르다는 사실 모름
🎤 예상 인터뷰 질문
- 라이브 방송에 적절한 GOP 길이는? 왜?
- B-프레임이 압축률에 좋은데 화상회의에서 안 쓰는 이유는?
- HLS 세그먼트 길이와 키프레임 간격이 다르면 어떤 문제가 생기나요?
Key Takeaways
I/P/B
자기완결 / 단방향 예측 / 양방향 예측.
크기 비교
I >> P >> B. 보통 1:1/5:1/20.
B는 양날의 검
최고 압축률, 최악 지연.
GOP
I 사이 간격. 압축·시킹·복구의 트레이드오프.
키프레임 = IDR
GOP 시작. 스트리밍 세그먼트 경계.
라이브엔 짧은 GOP
1~2초. 지연·복구 우선.
VOD엔 긴 GOP
2~5초. 압축률 우선.
세그먼트와 동기화
키프레임 간격이 세그먼트 길이와 같아야.