CBR, VBR, ABR, CRF: 비트레이트 제어 모드의 실제 차이
개요
인코딩 설정 화면에는 대개 레이트 컨트롤 항목이 있고, 거기에는 CBR, VBR, ABR, CRF 같은 약어가 나열되어 있습니다. CBR은 비트레이트를 고정한다는 뜻이고, VBR은 변동을 허용한다는 뜻이고, CRF는 비트레이트가 아니라 화질을 고정한다는 개념이라는 설명은 문서마다 비슷하게 반복됩니다.
문제는 이 설명이 "그래서 실제로 인코딩된 파일을 열어보면 초당 비트레이트가 어떤 모양으로 나오는가"라는 질문에는 답을 주지 않는다는 점입니다. CBR이라고 설정했는데 정말 프레임마다 비트가 균일하게 나가는지, VBR과 ABR은 이름이 다른데 실제로 얼마나 다르게 동작하는지는 직접 인코딩해서 프레임 단위로 뜯어보기 전에는 확인할 방법이 없습니다.
실무에서 이 선택은 구체적인 제약으로 다가옵니다. 방송 송출망처럼 고정 대역폭 회선에 물리는 스트림은 순간적으로도 대역폭을 넘기면 안 되니 CBR에 가까운 동작이 필요하고, VOD 저장용 파일은 화질을 우선하면서도 평균 용량을 예측 가능하게 맞추고 싶어서 VBR이나 ABR을 쓰고, 화질 자체가 목표라면 CRF를 씁니다.
그런데 "CBR로 설정하면 정말 순간 비트레이트가 안 넘어가는가", "VBR과 ABR의 차이가 실제 파일에서 얼마나 드러나는가" 같은 질문은 이름과 정의만 봐서는 답이 나오지 않습니다.
이 글에서는 같은 720p 5초 소스를 FFmpeg(libx264)로 CBR, VBR, ABR, CRF 네 가지 방식으로 인코딩하고, ffprobe로 프레임 단위 패킷 크기를 뽑아서 초당 비트레이트가 실제로 어떤 모양으로 나오는지 확인합니다.
이어서 Tencent Cloud의 미디어 트랜스코딩 서비스 MPS에 같은 소스를 올려 VideoTemplate의 Bitrate 필드 값을 바꿔가며, 원본 해상도와 다운스케일 해상도를 오가며 실제 출력 비트레이트가 어떻게 달라지는지 실측합니다.
테스트 환경과 소스
- FFmpeg 8.0 (Homebrew 빌드, libx264 포함), macOS Apple Silicon, 소프트웨어 인코더 기준
- 소스: 1280x720, h264, 24fps, 5.042초, 121프레임, 원본 비트레이트 약 3.30Mbps(2.08MB, ffprobe
bit_rate=3304142)
$ ffprobe -v error -show_format -show_streams src.mp4
...
width=1280
height=720
r_frame_rate=24/1
duration=5.041667
size=2082298
bit_rate=3304142실무 시나리오
VOD 플랫폼에서 720p 스트림의 평균 대역폭을 2Mbps 안팎으로 맞추라는 요구가 내려왔다고 가정하겠습니다. 인코딩 설정을 맡은 사람이 던질 수 있는 선택지는 최소 네 가지입니다. -b:v 2M에 minrate, maxrate, bufsize를 전부 2M로 묶어 CBR을 흉내 낼 것인지, maxrate만 여유 있게 풀어 VBR로 갈 것인지, 아무 제약 없이 -b:v 2M 하나만 줘서 ABR로 둘 것인지, 아니면 비트레이트 숫자를 아예 포기하고 CRF로 화질을 고정한 뒤 결과 용량을 받아들일 것인지입니다.
이 네 방식이 평균 비트레이트는 비슷하게 맞춰지더라도 초당 변동폭과 순간 피크가 얼마나 다른지 알아야, CDN 대역폭 설계나 스트리밍 버퍼 크기를 정할 때 어느 쪽이 안전한지 판단할 수 있습니다.
클라우드 트랜스코딩 서비스를 쓰는 경우라면 질문이 하나 더 붙습니다. MPS 같은 관리형 서비스에 Bitrate 값을 지정하면 그 숫자가 정말 상한으로 지켜지는지, 아니면 소스 복잡도나 해상도에 따라 실제 출력이 그 값을 넘거나 못 미칠 수 있는지입니다. 이 질문에 답하려면 같은 Bitrate 값을 다른 해상도에 줘보고 실제로 어떻게 나오는지 실측하는 수밖에 없습니다.
1. FFmpeg로 CBR, VBR, ABR, CRF 실제 인코딩하기
네 가지 방식을 아래 설정으로 인코딩했습니다. 오디오는 제외하고 비디오만 비교했습니다.
# CBR: minrate=maxrate=bufsize로 묶어 VBV 버퍼를 타이트하게 고정
ffmpeg -y -i src.mp4 -an -c:v libx264 -preset medium \
-b:v 2M -minrate 2M -maxrate 2M -bufsize 2M -pix_fmt yuv420p out_cbr.mp4
# VBR: maxrate/bufsize를 목표 비트레이트보다 여유 있게 풀어줌
ffmpeg -y -i src.mp4 -an -c:v libx264 -preset medium \
-b:v 2M -maxrate 3M -bufsize 4M -pix_fmt yuv420p out_vbr.mp4
# ABR: 목표 평균 비트레이트만 지정, VBV 제약 없음
ffmpeg -y -i src.mp4 -an -c:v libx264 -preset medium \
-b:v 2M -pix_fmt yuv420p out_abr.mp4
# CRF: 비트레이트가 아니라 화질(양자화 강도)을 고정
ffmpeg -y -i src.mp4 -an -c:v libx264 -preset medium \
-crf 23 -pix_fmt yuv420p out_crf.mp4결과물은 ffprobe로 파일 크기와 평균 비트레이트를 확인했습니다.
$ ffprobe -v error -select_streams v:0 \
-show_entries stream=codec_name,width,height,nb_frames \
-show_entries format=duration,size,bit_rate \
-of default=noprint_wrappers=1 out_cbr.mp4
codec_name=h264
width=1280
height=720
nb_frames=121
duration=5.041667
size=1134544
bit_rate=1800268실측 결과: 파일 크기와 평균 비트레이트
| 방식 | 설정 | 파일 크기 | 평균 비트레이트 |
|---|---|---|---|
| CBR | b:v/minrate/maxrate/bufsize=2M | 1.08MB | 1800kbps |
| VBR | b:v=2M, maxrate=3M, bufsize=4M | 1.08MB | 1801kbps |
| ABR | b:v=2M | 1.10MB | 1831kbps |
| CRF | crf=23 | 1.38MB | 2290kbps |
먼저 눈에 띄는 부분은 CBR, VBR, ABR 세 방식의 평균 비트레이트가 1800~1831kbps로 거의 같다는 점입니다. -b:v 2M는 세 설정 모두에 공통으로 들어 있는 "장기 평균 목표"이고, minrate/maxrate/bufsize는 그 목표를 얼마나 타이트하게 지킬지를 정하는 VBV 버퍼 제약일 뿐이라서, 평균값 자체는 세 방식 사이에 큰 차이가 나지 않습니다.
반면 CRF는 비트레이트 목표가 아예 없는 방식이라서, crf23이라는 화질을 유지하는 데 이 소스가 실제로 필요로 하는 비트레이트(2290kbps)가 다른 세 방식의 목표(2000kbps)보다 약 15% 높게 나왔습니다. 즉 CBR/VBR/ABR의 "평균이 비슷하다"는 결과는 레이트 컨트롤 방식의 우열이 아니라, 애초에 같은 -b:v 2M라는 목표를 공유했기 때문에 나온 당연한 결과입니다.
실제 결과물을 눈으로 비교하면 이렇습니다. 같은 소스를 CBR(1800kbps)과 CRF(2290kbps)로 각각 인코딩한 클립입니다.
CBR (b:v/minrate/maxrate/bufsize=2M, 1800kbps)
CRF 23 (2290kbps)
두 클립 모두 정지 장면에서는 육안으로 차이를 찾기 어렵지만, 앞서 표에서 본 것처럼 움직임이 많은 구간(0~1초)에서는 CBR이 1651kbps, CRF가 2368kbps로 43%나 차이가 났습니다. 이 정도 비트 차이는 고움직임 구간에서 블록킹이나 디테일 손실로 드러날 수 있는 폭이라는 걸 감안하고 봐야 합니다.
이 네 방식의 진짜 차이는 평균이 아니라 그 평균을 향해 가는 경로, 즉 초당 변동폭에서 드러납니다.
2. 프레임 단위로 비트레이트 변동 실측하기
각 결과물에서 프레임별 패킷 크기와 타임스탬프를 뽑았습니다.
$ ffprobe -v error -select_streams v:0 \
-show_entries frame=pkt_size,pts_time,pict_type \
-of csv=p=0 out_cbr.mp4 > frames_cbr.csv
$ head -5 frames_cbr.csv
0.000000,52545,I,
0.041667,1623,B
0.083333,3236,B
0.125000,1783,B
0.166667,9014,P이 CSV를 1초 단위(24프레임씩)로 묶어서 구간별 비트레이트를 합산했습니다. 소스가 5.042초라서 마지막 0.04초짜리 자투리 구간은 프레임 하나만 포함되어 통계를 왜곡하므로 제외하고, 정확히 24프레임씩 채워진 5개 구간(0~1초, 1~2초, ..., 4~5초)만 사용했습니다.
| 구간 | CBR | VBR | ABR | CRF |
|---|---|---|---|---|
| 0~1초 | 1651kbps | 1951kbps | 2006kbps | 2368kbps |
| 1~2초 | 2044kbps | 2043kbps | 2163kbps | 2760kbps |
| 2~3초 | 2035kbps | 2028kbps | 2010kbps | 2573kbps |
| 3~4초 | 1527kbps | 1471kbps | 1473kbps | 1923kbps |
| 4~5초 | 1691kbps | 1495kbps | 1487kbps | 1820kbps |
| 표준편차 | 211 | 259 | 290 | 364 |
| 변동계수(CV) | 11.8% | 14.4% | 15.9% | 15.9% |
변동계수(CV, 표준편차를 평균으로 나눈 값)로 보면 CBR이 11.8%로 가장 낮고, VBR이 14.4%, ABR이 15.9%로 CRF(15.9%)와 같은 수준까지 올라갑니다. 이 순서는 각 방식이 인코더에 걸어둔 VBV 버퍼 제약의 강도와 정확히 일치합니다.
CBR은 bufsize=2M로 버퍼 크기를 목표 비트레이트와 같은 1초 분량으로 좁혀놨기 때문에 인코더가 그 창(window) 안에서 비트를 강제로 평탄화해야 하고, VBR은 bufsize=4M로 2초 분량까지 버퍼를 늘려줘서 그만큼 순간적으로 몰아 쓸 여지가 커집니다.
ABR은 maxrate나 bufsize를 아예 지정하지 않아서 libx264가 기본값(사실상 무제약에 가까운 큰 버퍼)으로 동작하고, CRF는 애초에 비트레이트 제약이라는 개념 자체가 없으니 ABR과 비슷한 수준의 변동폭이 나오는 것도 자연스럽습니다.
여기서 실무적으로 중요한 함정이 하나 있습니다. 프레임 단위로 순간 비트레이트를 계산해보면, "CBR"로 인코딩한 파일에서도 I프레임 하나가 초당 10Mbps를 넘는 순간 전송률을 요구합니다.
$ python3 -c "print(52545*8*24/1000)"
10086.6CBR의 I프레임 크기는 52545바이트이고, 이 프레임 하나만 놓고 보면 초당 10Mbps가 넘는 순간 비트레이트에 해당합니다. maxrate 2M으로 상한을 걸었는데도 이런 스파이크가 나오는 이유는, x264의 maxrate/bufsize가 프레임 하나하나의 크기를 제한하는 게 아니라 "가상 버퍼(leaky bucket)가 넘치지 않는 한도 내에서" 평균을 제어하는 방식이기 때문입니다.
bufsize=2M는 대략 1초 분량의 버퍼 용량을 의미하는데, I프레임 하나가 그 버퍼의 상당 부분(약 21%)을 차지하더라도 같은 1초 구간 안의 나머지 P/B프레임이 훨씬 작기 때문에 구간 평균은 목표치 아래로 유지됩니다.
즉 "CBR"이라는 이름은 평균 전송률이 일정하다는 뜻이지, 프레임 단위로 데이터가 균일하게 나간다는 뜻이 아닙니다. 스트리밍 버퍼나 디코더 입력 버퍼를 설계할 때는 이 점을 감안해서, 목표 비트레이트가 아니라 bufsize로 지정한 버퍼 창 안에서 나올 수 있는 최악 케이스를 기준으로 여유를 잡아야 합니다.
3. Tencent Cloud MPS로 Bitrate 파라미터 동작 확인하기
로컬 FFmpeg 결과를 클라우드 트랜스코딩 서비스와 비교하기 위해, 같은 소스를 COS에 올리고 MPS의 ProcessMedia API로 VideoTemplate의 Bitrate 값을 바꿔가며 여러 케이스를 실행했습니다.
status, body = process_media(
"cbr-vbr-test/source-720p.mp4",
"cbr-vbr-test/out-t1-br800-720p/",
container="mp4",
video_template={"Codec": "libx264", "Bitrate": 800, "Fps": 24},
)여기서 실제로 겪은 함정이 하나 있었습니다. Definition: 0과 함께 OverrideParameter에 커스텀 값을 넣어 보냈더니, 태스크가 InvalidParameterValue.Container is null로 실패했습니다.
완료된 태스크 상세를 열어보니 RawParameter는 전부 빈 값으로 찍혀 있고 OverrideParameter는 null로 되돌아와 있었는데, 이는 Definition이 특정 템플릿 ID를 가리키지 않는 완전 커스텀 인코딩에서는 OverrideParameter가 아니라 RawParameter 필드에 Container와 VideoTemplate을 직접 넣어야 한다는 뜻이었습니다.
OverrideParameter는 이미 등록된 Definition 템플릿 위에 일부 필드만 덮어쓸 때 쓰는 용도이고, 템플릿 없이 처음부터 커스텀으로 인코딩하려면 RawParameter를 써야 합니다. 필드를 바꾸자 태스크가 정상적으로 처리됐습니다.
task = {
"OutputStorage": output_storage,
"OutputObjectPath": output_prefix + "{Definition}.{format}",
"RawParameter": {
"Container": "mp4",
"RemoveAudio": 1,
"VideoTemplate": {"Codec": "libx264", "Bitrate": 800, "Fps": 24},
},
}테스트 매트릭스와 실측 결과
같은 720p 소스를 기준으로, Bitrate 값과 해상도를 바꿔가며 다섯 개 태스크를 실행하고 DescribeTaskDetail의 Output.Bitrate를 ffprobe로 다시 검증했습니다.
| 테스트 | 설정 | 선언한 Bitrate | 실제 출력 비트레이트 | 비율 |
|---|---|---|---|---|
| T1 | Bitrate=800, 720p 그대로 | 800kbps | 909kbps | 114% |
| T2 | Bitrate=3000, 720p 그대로 | 3000kbps | 1870kbps | 62% |
| T3 | Bitrate=800, 480x270로 다운스케일 | 800kbps | 494kbps | 62% |
| T5 | Vcrf=23 + Bitrate=3000(느슨한 상한), 720p | 3000kbps | 2070kbps | 69% |
| T6 | Vcrf=23 + Bitrate=800(빡빡한 상한), 720p | 800kbps | 912kbps | 114% |
이 결과에서 확인할 수 있는 건, MPS의 Bitrate 필드가 "이 값을 넘지 않는 상한"으로 딱 떨어지게 동작하지는 않는다는 점입니다. T1에서는 800을 선언했는데 실제로는 909(114%)로 선언값을 넘어섰고, T2에서는 3000을 선언했는데 1870(62%)으로 한참 못 미쳤습니다. 같은 800이라는 숫자를 줘도 T3처럼 480x270으로 축소하면 494(62%)로 뚝 떨어집니다.
세 결과를 나란히 놓고 보면 패턴이 보입니다. Bitrate 값은 인코더가 참고하는 기준점에 가깝고, 실제 출력은 그 해상도와 소스 복잡도에서 해당 화질을 유지하는 데 필요한 비트 수에 맞춰 결정됩니다. 720p처럼 화면 정보량이 많은 해상도에서 800이라는 다소 빡빡한 숫자를 주면 인코더가 최소한의 화질을 지키기 위해 그 값을 넘기고, 480x270처럼 정보량이 적은 해상도에서는 같은 800을 줘도 그만큼 쓸 필요가 없어 낮은 값에서 끝납니다. 3000처럼 720p 기준으로도 여유 있는 상한을 주면, 인코더는 그 여유를 다 채우지 않고 화질에 필요한 만큼만 쓰고 멈춥니다.
Vcrf를 함께 준 T5, T6도 이 해석과 맞아떨어집니다. T5(Vcrf23 + 느슨한 상한 3000)는 화질 기준으로 필요한 만큼만 쓴 2070으로 끝났고, T6(Vcrf23 + 빡빡한 상한 800)는 Vcrf 없이 Bitrate만 준 T1(909)과 거의 같은 912로 나왔습니다. 이번 소스와 이 상한값 조합에서는 Bitrate가 사실상 지배적인 제약으로 작동했고, 함께 준 Vcrf가 결과를 크게 바꾸지는 못했습니다.
참고로 Vcrf만 단독으로 주고 Bitrate를 빼면 InvalidParameterValue.VideoBitrate is null로 거부되는데, 이는 FFmpeg의 -crf처럼 비트레이트 없이 화질만으로 인코딩을 완결하는 모드가 아니라 Bitrate를 반드시 함께 선언해야 하는 구조라는 뜻입니다.
출력물을 다시 프레임 단위로 뜯어보면, MPS의 Bitrate 기반 인코딩도 내부적으로는 로컬 ABR/VBR/CRF와 비슷한 폭으로 변동합니다.
| 구간 | T1(Bitrate=800) | T6(Vcrf23+Bitrate=800) |
|---|---|---|
| 0~1초 | 887kbps | 886kbps |
| 1~2초 | 1076kbps | 1093kbps |
| 2~3초 | 1017kbps | 1000kbps |
| 3~4초 | 668kbps | 675kbps |
| 4~5초 | 865kbps | 868kbps |
| 변동계수(CV) | 15.6% | 15.6% |
T1과 T6의 구간별 패턴이 거의 겹친다는 점도 앞서의 해석을 뒷받침합니다. CV 15.6%는 로컬에서 측정한 ABR(15.9%), CRF(15.9%)와 같은 구간에 있고, bufsize를 좁게 걸어 CV를 11.8%까지 낮춘 로컬 CBR보다는 변동폭이 큽니다.
정리하면 MPS의 VideoTemplate.Bitrate 하나만 지정하는 방식은 이름 그대로의 CBR이 아니라, 그 값을 기준점 삼아 움직이는 ABR에 가까운 동작으로 이해하는 게 정확합니다.
이 결과를 실무에 적용할 때 필요한 태도는 명확합니다. Bitrate 값을 CDN 대역폭 산정이나 저장 용량 예측의 근거로 쓰려면, 선언한 숫자를 그대로 믿지 말고 실제 소스와 해상도 조합으로 한 번 트랜스코딩을 돌려서 ffprobe로 확인하는 절차를 거쳐야 합니다. 특히 해상도를 낮추는 프리셋을 여러 단계로 운영한다면, 같은 Bitrate 값이라도 단계별로 실제 출력이 크게 벌어질 수 있다는 걸 이번 실측으로 확인했습니다.
만약 방송 송출망처럼 순간 대역폭도 엄격하게 지켜야 하는 요구사항이 있다면, 기본 Bitrate/Vcrf 조합만으로는 하드 캡을 보장하기 어려우니 담당 계정팀이나 기술지원팀에 문의해서 VBV 버퍼 제약을 반영한 커스텀 인코딩 프로파일을 구성할 수 있는지 논의하는 걸 권합니다. MPS는 HEVC 10bit HDR10 MOV나 ProRes 같은 특수 규격도 커스텀 트랜스코딩으로 지원하는 서비스이니, 표준 VideoTemplate 필드 범위를 벗어나는 요구사항이라면 계정팀에 커스텀 구성을 상담하는 게 합리적인 다음 단계입니다.
작업에 사용한 COS 오브젝트(cbr-vbr-test/ 아래 소스 파일과 다섯 개 트랜스코딩 결과물)는 확인이 끝난 뒤 전부 삭제했습니다.
정리
- CBR, VBR, ABR은
-b:v로 같은 평균 목표를 공유하면 평균 비트레이트 자체는 거의 같게 나옵니다. 이 세 방식의 진짜 차이는 평균이 아니라minrate/maxrate/bufsize가 정하는 VBV 버퍼 창의 크기이고, 그 크기가 좁을수록(CBR) 초당 변동계수가 낮고 넓을수록(ABR) 높아집니다. 실측한 변동계수는 CBR 11.8%, VBR 14.4%, ABR 15.9%였습니다. - CRF는 비트레이트 목표가 없는 화질 우선 방식이라서, 같은 소스에서 CBR/VBR/ABR의 목표(2000kbps)보다 실제로 필요한 비트레이트(2290kbps)가 더 높게 나왔고, 변동계수도 ABR과 같은 수준(15.9%)이었습니다.
- "CBR"로 인코딩해도 I프레임 같은 개별 프레임은 목표 비트레이트를 몇 배 넘는 순간 전송률을 요구할 수 있습니다.
maxrate/bufsize는 프레임 단위가 아니라 버퍼 창 단위로 평균을 제어하는 방식이기 때문이며, 스트리밍 버퍼를 설계할 때는 이 점을 감안해야 합니다. - Tencent Cloud MPS에서 커스텀 트랜스코딩을 요청할 때는
Definition: 0+OverrideParameter가 아니라RawParameter에Container와VideoTemplate을 직접 넣어야 합니다.OverrideParameter는 기존Definition템플릿 위에 얹는 용도입니다. - MPS의
VideoTemplate.Bitrate는 정확한 상한이나 정확한 목표가 아니라, 해상도와 소스 복잡도에 따라 실제 출력이 그 값을 넘거나 못 미칠 수 있는 기준점으로 동작했습니다. 같은 800이라는 값이 720p에서는 114%(909kbps)로 나오고 480p 다운스케일에서는 62%(494kbps)로 나온 것이 이를 보여줍니다.Vcrf를 함께 줘도Bitrate가 지배적인 제약으로 남았습니다. 순간 대역폭까지 엄격하게 지켜야 하는 요구사항이라면 계정팀이나 기술지원팀과 커스텀 프로파일을 논의하는 게 맞는 경로입니다.
레이트 컨트롤 방식을 고르는 문제는 결국 "평균을 맞출 것인가, 순간 상한을 지킬 것인가, 화질을 지킬 것인가" 중 무엇이 우선인지를 먼저 정하고, 그 우선순위에 맞는 버퍼 제약과 파라미터를 실제로 인코딩해서 프레임 단위로 확인하는 문제입니다.
이름이 CBR이라고 해서 프레임 단위로 균일하다고 가정하거나, 클라우드 서비스에 숫자 하나를 넣었다고 해서 그 숫자가 그대로 나온다고 가정하면, 이번 실측에서 본 것처럼 기대와 다른 결과를 받아들게 됩니다.