Startup GOP, 스트림 초반만 다르게 잡아야 하는 이유
개요
GOP 길이는 보통 스트림 전체에 한 값으로 고정해서 씁니다. 그런데 라이브 스트리밍에서는 "전체 GOP 길이"와 별개로 "맨 처음 GOP만 얼마나 짧게 잡을지"가 따로 문제가 되는 구간이 있습니다.
뷰어가 방송에 접속한 순간 플레이어가 재생을 시작하려면 키프레임 하나를 받아야 하는데, GOP가 길면 그 키프레임이 올 때까지 기다리는 시간이 그만큼 늘어나기 때문입니다.
이 문제를 푸는 방법 중 하나가 스트림 시작 지점의 GOP 한두 개만 짧게 잡고, 그 뒤부터는 원래 쓰려던 긴 GOP로 돌아가는 구조입니다. Tencent Cloud MPS 콘솔에서는 이런 개념을 "GOP Duration at Startup"이라는 이름으로 부릅니다.
이 글에서는 이 구조를 FFmpeg에서 -force_key_frames로 그대로 만들어보고, ffprobe로 키프레임이 지정한 시점에 정확히 찍혔는지, 그리고 이 구조 때문에 파일 크기나 비트레이트가 얼마나 늘어나는지 실측합니다.
이어서 같은 소스를 Tencent Cloud MPS로 트랜스코딩해서, 이 서비스의 VideoTemplate 스키마에 같은 개념(스트림 앞부분과 뒷부분의 GOP를 다르게 잡는 파라미터)이 실제로 노출돼 있는지 API 응답으로 직접 확인합니다.
실무 시나리오
라이브 방송 서비스를 운영하고 있는데, "방송 시작 직후 첫 화면이 늦게 뜬다"는 리포트가 들어왔다고 가정하겠습니다. 인코딩 설정을 보니 GOP가 5초로 잡혀 있었습니다.
인코더는 5초에 한 번씩만 키프레임을 찍고, HLS 세그먼트도 이 키프레임 경계에 맞춰 잘립니다. 뷰어가 방송 시작 시점 근처에 접속하면 플레이어는 재생 가능한 첫 세그먼트(키프레임으로 시작하는 세그먼트)가 만들어질 때까지 기다려야 하고, 운이 나쁘면 이 대기가 GOP 길이인 5초 가까이 걸립니다.
가장 단순한 해결책은 GOP를 스트림 전체에서 짧게(예: 1초) 줄이는 것이지만, 이러면 방송 내내 키프레임이 5배 더 자주 찍혀서 대역폭 비용이 계속 늘어납니다.
실제로 필요한 건 "스트림이 막 시작된 처음 몇 초 동안만" 짧은 GOP를 쓰고, 재생이 정상적으로 시작된 뒤에는 원래대로 5초 GOP로 돌아가는 구조입니다. 이렇게 하면 첫 화면이 늦게 뜨는 문제는 시작 구간에서만 해결하고, 방송이 안정적으로 진행되는 대부분의 시간에는 압축 효율을 그대로 유지할 수 있습니다.
이 구조를 실제로 만들 수 있는지, 만들면 초반 구간의 비용이 얼마나 늘어나는지, 그리고 지금 쓰고 있는 클라우드 트랜스코딩 서비스에서 이 설정을 그대로 쓸 수 있는지를 확인해야 팀에 이 방향을 제안할 수 있습니다.
테스트 환경과 소스
- FFmpeg 8.0 (Homebrew 빌드, libx264 포함), macOS, Apple Silicon
- 소스: 1280x720, 24fps, H.264, 오디오 트랙 없음
$ ffprobe -v error -show_entries format=duration,size,bit_rate \
-show_entries stream=codec_name,width,height,r_frame_rate,pix_fmt \
-of default=noprint_wrappers=0 06011027e7e345fe.mp4
[STREAM]
codec_name=h264
width=1280
height=720
pix_fmt=yuv420p
r_frame_rate=24/1
[/STREAM]
[FORMAT]
duration=5.041667
size=2082298
bit_rate=3304142
[/FORMAT]원본은 5.04초(121프레임)뿐이라서 GOP 전환 구간(짧은 GOP에서 긴 GOP로 넘어가는 지점)이 여러 번 나오도록 -stream_loop 5로 6번 이어붙여 30.25초(726프레임) 분량으로 늘렸습니다.
모든 테스트에서 -sc_threshold 0을 걸어 libx264의 장면전환 감지 키프레임을 껐고, B-frame은 0으로 고정해서 GOP 구조 자체에만 집중했습니다.
1. FFmpeg에서 Startup GOP 구조 만들기
키프레임을 0초, 0.5초, 1초에 한 번씩(짧은 시작 구간)에 이어 6초, 11초, 16초, 21초, 26초(5초 간격의 일반 구간)로 지정했습니다.
$ ffmpeg -y -stream_loop 5 -i src.mp4 -an -c:v libx264 -preset medium -crf 21 \
-g 999999 -keyint_min 0 -sc_threshold 0 -bf 0 -pix_fmt yuv420p \
-force_key_frames "0,0.5,1,6,11,16,21,26" \
startup_gop.mp4-force_key_frames에 시점 목록을 넘기면 그 시점마다 키프레임을 강제로 찍긴 하지만, 이 옵션만으로는 인코더의 주기적 키프레임 삽입(-g)이 꺼지지 않습니다. -g를 기본값이나 작은 값으로 두면 지정한 시점 사이사이에도 인코더가 자체적으로 키프레임을 추가로 찍어서, 원하는 "짧게-길게" 구조가 뭉개집니다.
그래서 -g 999999 -keyint_min 0으로 인코더의 자동 키프레임 삽입을 사실상 무력화하고, 키프레임 위치를 전적으로 -force_key_frames 목록에만 맡겼습니다.
비교 대상으로 GOP를 5초(120프레임)로 처음부터 끝까지 고정한 파일도 만들었습니다.
$ ffmpeg -y -stream_loop 5 -i src.mp4 -an -c:v libx264 -preset medium -crf 21 \
-g 120 -keyint_min 120 -sc_threshold 0 -bf 0 -pix_fmt yuv420p \
uniform_gop.mp4두 파일의 키프레임 위치를 ffprobe로 확인했습니다.
$ ffprobe -v error -select_streams v:0 -show_entries frame=key_frame,pts_time,pict_type \
-of csv=p=0 startup_gop.mp4 | awk -F, '$1==1{print $2, $3}'
0.000000 I
0.500000 I
1.000000 I
6.000000 I
11.000000 I
16.000000 I
21.000000 I
26.000000 I
$ ffprobe -v error -select_streams v:0 -show_entries frame=key_frame,pts_time,pict_type \
-of csv=p=0 uniform_gop.mp4 | awk -F, '$1==1{print $2, $3}'
0.000000 I
5.000000 I
10.000000 I
15.000000 I
20.000000 I
25.000000 I
30.000000 Istartup_gop.mp4는 요청한 대로 0초, 0.5초, 1초까지는 0.5초 간격으로, 그 뒤부터는 5초 간격으로 정확히 키프레임이 찍혔습니다. 지정한 시점과 실측 결과가 소수점까지 정확히 일치합니다. uniform_gop.mp4는 처음부터 끝까지 5초 간격을 그대로 유지했습니다. 두 파일 모두 전체 프레임 수는 726개로 같습니다.
2. 초반 구간 파일 크기와 비트레이트 실측
키프레임을 촘촘히 찍을수록 그 구간의 압축 효율은 떨어집니다. I-frame은 앞뒤 프레임을 참조하지 않고 통째로 압축해야 해서 같은 화질 기준으로 P-frame보다 크기가 훨씬 크기 때문입니다. Startup GOP 구조는 이 비용을 시작 구간에만 짊어지는 셈인데, 실제로 얼마나 늘어나는지 초 단위로 잘라서 확인했습니다.
def get_packets(path):
out = subprocess.run(
["ffprobe","-v","error","-select_streams","v","-show_entries","packet=pts_time,size",
"-of","json", path], capture_output=True, text=True, check=True
).stdout
data = json.loads(out)
return [(float(p["pts_time"]), int(p["size"])) for p in data["packets"]]
def window_bitrate(pkts, window=1.0, total=30.0):
buckets = defaultdict(int)
for t, size in pkts:
buckets[int(t // window)] += size
return [(i*window, buckets.get(i,0)*8/1000/window, buckets.get(i,0)) for i in range(int(total//window))]$ python3 bitrate_windows.py startup_gop.mp4 uniform_gop.mp4
=== startup_gop.mp4 ===
t= 0.0s 3537.7 kbps (442212 bytes)
t= 1.0s 3948.0 kbps (493500 bytes)
t= 2.0s 3407.4 kbps (425929 bytes)
t= 3.0s 2525.3 kbps (315668 bytes)
t= 4.0s 2350.5 kbps (293809 bytes)
t= 5.0s 3139.2 kbps (392394 bytes)
...
=== uniform_gop.mp4 ===
t= 0.0s 3196.4 kbps (399549 bytes)
t= 1.0s 3676.5 kbps (459560 bytes)
t= 2.0s 3414.0 kbps (426747 bytes)
t= 3.0s 2530.8 kbps (316348 bytes)
t= 4.0s 2350.8 kbps (293845 bytes)
t= 5.0s 3399.5 kbps (424932 bytes)
...구간별로 누적해서 비교하면 다음과 같습니다.
| 구간 | Startup GOP | 고정 GOP(5s) | 차이 |
|---|---|---|---|
| 첫 1초 | 442,212 bytes | 399,549 bytes | +10.68% |
| 첫 5초 | 1,971,118 bytes | 1,896,049 bytes | +3.96% |
| 전체 30.25초 | 11,686,769 bytes | 11,681,277 bytes | +0.047% |
첫 1초 구간은 키프레임이 두 개(0초, 0.5초)나 몰려 있어서 비트레이트가 10.68% 더 높게 나왔습니다. 첫 5초로 넓혀서 보면 이 차이는 3.96%로 줄어드는데, 5초 구간 안에 짧은 GOP 두 개와 첫 번째 긴 GOP 하나가 섞여 있어서 초반 비용이 상대적으로 희석되기 때문입니다.
그리고 전체 30.25초로 보면 두 파일의 크기 차이는 0.047%까지 줄어서 사실상 같은 크기입니다. Startup GOP 구조가 늘리는 비용은 스트림 시작 부분 1~2초에만 집중돼 있고, 그 이후로는 전체 스트림 크기에 거의 영향을 주지 않는다는 걸 실측으로 확인한 셈입니다. 첫 화면 지연을 줄이는 대가로 지불하는 비용치고는 상당히 저렴합니다.
3. Tencent Cloud MPS에서 같은 구조 확인하기
로컬에서 확인한 구조를 클라우드 트랜스코딩 서비스에서도 그대로 쓸 수 있는지 확인하려면, 우선 MPS의 VideoTemplate 스키마에 GOP를 구간별로 다르게 지정하는 필드가 있는지부터 봐야 합니다.
DescribeTranscodeTemplates로 등록된 프리셋 템플릿 100개를 조회해서 VideoTemplate 객체가 갖는 필드 전체를 뽑아봤습니다.
$ python3 mps_smoke_test.py DescribeTranscodeTemplates '{"Limit":100}'keys = set
for t in templates:
keys.update(t["VideoTemplate"].keys)
print(sorted(keys))['Bframes', 'BitDepth', 'Bitrate', 'Codec', 'CompressType', 'ContentAdaptStream',
'FillType', 'Fps', 'FpsDenominator', 'Gop', 'Height', 'HlsTime', 'Mode', 'NoScenecut',
'RawPts', 'ResolutionAdaptive', 'Sar', 'ScenarioBased', 'SceneType', 'SegmentType',
'Stereo3dType', 'Subjective', 'Vcrf', 'VideoLevel', 'VideoProfile', 'Width']Gop 필드는 스트림 전체에 적용되는 단일 값 하나뿐입니다. "시작 구간 GOP"와 "안정화 구간 GOP"를 따로 받는 필드는 이 목록에 없습니다.
혹시 몰라서 gop라는 문자열이 들어간 다른 흔적도 응답 전체에서 훑어봤는데, 일부 VOD용 압축 프리셋의 StdExtInfo에 {"video_info":{"keep_gop":0}}라는 값이 붙어 있는 걸 찾았습니다. 다만 이건 이름 그대로 트랜스코딩 시 소스의 GOP 구조를 유지할지 여부를 정하는 플래그로 보이고, 스트림 앞부분만 다르게 잡는 것과는 다른 용도입니다.
이 결과를 근거로 정리하면, GOP 앞뒤 구간을 다르게 잡는 기능은 현재 VideoTemplate 스키마의 기본 필드로는 노출돼 있지 않습니다. 콘솔에서 이 개념에 "GOP Duration at Startup"이라는 이름이 붙어 있다는 걸 감안하면, 라이브 트랜스코딩 파이프라인 안쪽에는 관련 로직이 이미 있을 가능성이 높습니다.
다만 이번에 확인한 범위(DescribeTranscodeTemplates가 돌려주는 표준 템플릿 스키마와 RawParameter로 즉석 지정 가능한 필드) 안에서는 API로 직접 값을 넣을 수 있는 자리가 없었습니다. 이런 스트림 시작 구간 전용 파라미터가 꼭 필요하다면, 담당 계정팀이나 기술지원팀에 문의해서 라이브 트랜스코딩 템플릿 쪽에 커스텀 지원이 가능한지 논의하는 걸 권합니다.
Gop 하나만으로 실제 동작을 재현해보기
스키마에 시작 구간 전용 필드는 없지만, 로컬 FFmpeg에서 확인한 "GOP 길이 하나가 키프레임 간격을 그대로 결정한다"는 규칙이 MPS에서도 똑같이 지켜지는지는 확인할 가치가 있습니다. looped_input.mp4(30.25초로 늘린 소스)를 COS에 올리고, Gop: 120(24fps 기준 5초)으로 즉석 트랜스코딩을 요청했습니다.
Definition을 0으로 두고 즉석 파라미터를 넘길 때는 RawParameter 필드를 써야 합니다(OverrideParameter는 기존 템플릿 ID의 일부 값만 덮어쓰는 용도라 Definition=0과 같이 쓰면 Container is null 에러가 납니다). 소스에 오디오 트랙이 없으므로 RemoveAudio: 1도 명시했습니다.
video_template = {
"Codec": "libx264",
"Fps": 24,
"Bitrate": 3200,
"ResolutionAdaptive": "close",
"Width": 1280,
"Height": 720,
"Gop": 120,
}
task = {
"OutputStorage": output_storage,
"OutputObjectPath": "startup-gop-test/out-gop120/{Definition}.{format}",
"Definition": 0,
"RawParameter": {
"Container": "mp4",
"RemoveAudio": 1,
"VideoTemplate": video_template,
},
}$ python3 run_mps.py gop120 startup-gop-test/looped_input.mp4
200 {"Response": {"TaskId": "2600012260-WorkflowTask-e945631d2fc6764d0eb672e1dea7c727tt26", ...}}
$ python3 mps_pipeline.py poll 2600012260-WorkflowTask-e945631d2fc6764d0eb672e1dea7c727tt26
poll: TaskStatus=PROCESSING
poll: TaskStatus=FINISHDescribeTaskDetail 응답의 Output.Path는 /startup-gop-test/startup-gop-test/out-gop120/0.mp4처럼 경로 앞부분이 중복돼서 찍혔습니다. OutputObjectPath에 이미 startup-gop-test/를 붙였는데, 버킷 쪽 출력 스토리지 설정에도 같은 접두어가 한 번 더 붙는 것으로 보입니다.
다운로드할 때는 OutputObjectPath를 조합해서 유추하지 말고 이 Output.Path 값을 그대로 쓰는 게 안전합니다.
$ python3 mps_pipeline.py download "/startup-gop-test/startup-gop-test/out-gop120/0.mp4" mps_gop120.mp4
downloaded mps_gop120.mp4
$ ffprobe -v error -select_streams v:0 -show_entries frame=key_frame,pts_time,pict_type \
-of csv=p=0 mps_gop120.mp4 | awk -F, '$1==1{print $2, $3}'
0.000000 I
5.000000 I
10.000000 I
15.000000 I
20.000000 I
25.000000 I
30.000000 I키프레임이 정확히 5초 간격(0, 5, 10, 15, 20, 25, 30초)으로 찍혀서 로컬 FFmpeg의 uniform_gop.mp4와 완전히 같은 패턴이 나왔습니다. Gop 값 하나가 스트림 전체에 균일하게 적용된다는 걸 클라우드 트랜스코딩 결과로도 다시 확인했습니다.
파일 크기와 비트레이트도 함께 봤습니다.
| 구분 | 요청값 | 결과 |
|---|---|---|
| 파일 크기 | - | 7,122,150 bytes (6.79MB) |
| 비트레이트 | Bitrate: 3200kbps 상한 | 1,881,006 bps (약 1.88Mbps) |
Bitrate: 3200으로 상한을 걸었는데 실제로는 그 절반을 조금 넘는 수준에서 결과가 나왔습니다. 이전 GOP/B-frame 실측 글에서 확인했던 것과 같은 패턴으로, MPS의 Bitrate 파라미터는 고정 비트레이트를 강제하는 값이 아니라 레이트 컨트롤이 참고하는 상한에 가깝게 동작합니다.
이 소스처럼 반복 움직임이 많고 짧은 클립에서는 상한까지 채우지 않고도 화질 목표를 만족할 수 있었던 것으로 보입니다.
작업에 쓴 COS 오브젝트(startup-gop-test/ 아래 소스 파일과 트랜스코딩 결과물)는 검증이 끝난 뒤 모두 삭제했습니다.
4. 실무에 적용할 때 고려할 점
이번 실측을 종합하면, Startup GOP 구조는 인코더 쪽에서 직접 만들 수 있는 기법이고, 그 비용은 스트림 시작 1~2초 구간에만 집중되며 전체 스트림 크기에는 거의 영향을 주지 않습니다. 문제는 이 구조를 어느 지점에서 만드느냐입니다.
라이브 방송 파이프라인에서 소스 인코더(OBS나 자체 RTMP 푸셔)를 직접 통제할 수 있다면, 이번에 확인한 -force_key_frames 방식을 인제스트 단계에 그대로 적용해서 스트림 시작 지점부터 짧은 GOP로 방송을 시작하게 만들 수 있습니다.
이렇게 만든 스트림을 MPS로 넘겨서 트랜스코딩할 때는, 앞서 찾은 keep_gop처럼 소스의 GOP 구조를 보존하는 옵션이 있다면 이 값을 함께 확인해볼 만합니다. 소스에서 이미 만들어둔 시작 구간 GOP 경계를 트랜스코딩 과정에서 그대로 유지할 수 있는지에 따라, 인제스트 단계의 작업이 결과물까지 이어지는지가 갈리기 때문입니다.
반대로 인제스트 단계를 통제할 수 없고 MPS의 트랜스코딩 템플릿 설정만으로 이 구조를 만들어야 하는 상황이라면, 지금 확인한 범위에서는 표준 API로는 어렵습니다. 이런 요구사항이 있다면 감으로 우회 방법을 찾기보다, 담당 계정팀이나 기술지원팀에 라이브 트랜스코딩 템플릿의 커스텀 지원 여부를 먼저 문의하는 편이 빠릅니다.
정리
- Startup GOP는 스트림 시작 부분의 GOP 한두 개만 짧게 잡고 이후엔 원래 GOP 길이로 돌아가는 구조입니다. FFmpeg에서는
-force_key_frames에 원하는 시점을 직접 나열하고-g를 매우 큰 값으로 올려 자동 키프레임 삽입을 무력화하면 정확히 지정한 시점에만 키프레임이 찍힙니다. - 실측 결과 지정한 8개 시점(0, 0.5, 1, 6, 11, 16, 21, 26초)에 소수점까지 정확히 키프레임이 생성됐습니다.
- 이 구조 때문에 늘어나는 비용은 첫 1초 구간에서 +10.68%, 첫 5초 구간에서 +3.96%였고, 전체 30.25초로 보면 +0.047%로 사실상 사라졌습니다. 초반 비용이 스트림 전체로 희석되면서, 첫 화면 지연을 줄이는 대가치고는 저렴한 기법이라는 게 실측으로 확인됩니다.
- Tencent Cloud MPS의
DescribeTranscodeTemplates응답에서VideoTemplate스키마 전체를 확인한 결과,Gop는 스트림 전체에 적용되는 단일 값이고 시작 구간과 안정화 구간을 따로 지정하는 필드는 없습니다. 이 기능은 현재 API 기본 스키마로는 노출돼 있지 않으므로, 필요하면 담당 계정팀이나 기술지원팀에 커스텀 지원 여부를 문의하는 걸 권합니다. Gop: 120으로 즉석 트랜스코딩을 요청한 결과는 로컬 FFmpeg의 고정 GOP 결과와 키프레임 간격이 정확히 일치했고,Bitrate파라미터는 여기서도 고정값이 아니라 레이트 컨트롤 상한처럼 동작했습니다.- Startup GOP를 클라우드 트랜스코딩 파이프라인에서 쓰려면, 인제스트 단계(소스 인코더)에서 이 구조를 미리 만들어 넣고 트랜스코딩 쪽에서 그 구조를 보존하는 방식이 현실적인 대안입니다.
GOP 길이는 스트림 전체에 한 값으로 고정해야 한다는 전제 자체가 라이브 스트리밍의 첫 화면 지연 문제 앞에서는 깨집니다. 스트림 시작 지점만 따로 떼어 짧은 GOP를 쓰는 건 압축 효율을 거의 희생하지 않으면서 체감 반응성을 크게 개선할 수 있는, 비용 대비 효과가 뚜렷한 조정입니다.