블로그로 돌아가기
2026년 8월 13일Sergei Solod11 분 읽기

H.264를 그대로 쓰면서 눈에 띄는 화질 저하 없이 영상 용량을 몇 배 줄인 방법

Codec은 H.264 그대로 두고 CRF 28, x264 veryslow, 720p급 해상도 상한, 실제로 필요한 frame rate만 유지하는 방향으로 encoding 정책을 다시 만들었습니다. 일반 playback에서는 자연스럽게 보이는 상태를 유지하면서 파일은 몇 배 작아졌습니다. 가장 큰 교훈은 encoding 비용은 한 번, bandwidth 비용은 view마다 반복된다는 점이었습니다.

H.264FFmpeg비디오 압축웹 성능x264

결과만 보면 더 최신 codec으로 바꾼 것처럼 보였습니다. Video file은 몇 배 작아졌는데, 일반적인 playback은 여전히 자연스러웠고 평소 보는 크기에서는 눈에 띄는 화질 저하를 느끼지 못했습니다.

하지만 AV1, HEVC, VP9으로 바꾼 것이 아닙니다. 여전히 MP4 안의 H.264를 사용했습니다.

바뀐 것은 codec 주변의 모든 결정이었습니다. 아주 구체적인 workload에 맞춰 encoding 정책을 다시 만들었습니다. 짧은 illustration/animation clip이 중심이고, audience는 mobile-heavy이며, 반복해서 발생하는 가장 큰 비용은 bandwidth입니다. 반대로 offline encode는 한 번만 하므로 시간이 오래 걸리는 것은 거의 문제가 되지 않았습니다.

최종 baseline은 playback 측에서는 의도적으로 보수적이고, encoding 측에서는 의도적으로 비싼 구성입니다. H.264 Main Profile @ Level 3.1, avc1, 8-bit yuv420p, CRF 28, x264 veryslow, 720p-class resolution ceiling, 보통 30 fps를 넘지 않는 유효 frame rate, 제한된 reference/B-frame 구조, progressive MP4 delivery를 위한 faststart를 사용합니다.

가장 중요한 최적화는 FFmpeg flag 하나가 아니었다

가장 크게 바뀐 것은 비용을 보는 방식이었습니다.

Encoding은 한 번 일어납니다. Delivery는 누군가 파일을 요청할 때마다 반복됩니다.

Real-time video라면 약간의 bitrate를 줄이기 위해 CPU를 훨씬 더 쓰는 것이 나쁜 trade일 수 있습니다. 제 파일은 offline에서 encode한 뒤 계속 반복해서 serve합니다. 이런 모델에서는 encoding 시간을 10분 줄여도, 그 때문에 앞으로의 모든 request가 더 커진다면 경제적으로 거의 의미가 없습니다.

그래서 저에게는 -preset veryslow가 맞습니다. x264가 더 효율적인 표현을 찾는 데 CPU를 더 쓸 수 있다면 그 비용은 한 번 내면 됩니다. Browser는 encoder의 search를 다시 수행하지 않습니다. 완성된 bitstream을 decode할 뿐입니다.

원칙은 단순해졌습니다. 한 번만 하는 단계에는 계산량을 쓰고, 반복되는 단계에서는 byte를 아낀다.

최신 codec을 쫓지 않고 H.264를 유지한 이유

H.264가 가장 압축 효율이 높은 codec이라고 주장하는 것은 아닙니다. 그렇지 않습니다. Delivery system이 여러 rendition을 보관하고 client마다 가장 적합한 것을 선택할 수 있다면 더 최신 codec은 충분히 매력적입니다.

제 조건은 달랐습니다. URL 하나, file 하나, codec 하나, 그리고 mobile-heavy audience에서 실제 playback 문제를 가능한 한 줄이는 것이었습니다.

이 조건에서는 MP4 안의 H.264가 여전히 매우 안전한 baseline입니다. Apple은 현재 Safari에서 static video를 제공할 때 Web developer에게 H.264로 encode한 MP4 file을 사용하도록 안내합니다. 현재 Android 문서도 MP4의 H.264를 지원 대상으로 명시하고 있으며 Android 6.0 이상에서는 Main Profile decoder를 필수로 요구합니다. 또한 H.264 HD playback 구성 예시로 1280×720, 30 fps를 제시합니다. 자세한 내용은 Android supported media formats에서 확인할 수 있습니다.

그렇다고 최신 기기가 Main Profile이나 Level 3.1까지만 지원한다는 뜻은 아닙니다. 예를 들어 Apple HLS 가이드는 일반적으로 Main이나 Baseline보다 High Profile을 선호합니다. 제가 Main@3.1을 선택한 이유는 single static MP4에서 의도적으로 modest한 decoder envelope을 원했기 때문이지, Apple이 그것을 요구하기 때문이 아닙니다.

존재할 필요가 없는 pixel을 encode하지 않았다

Resolution은 가장 큰 lever 중 하나였습니다. 상한은 landscape에서 약 1280×720, portrait에서 720×1280, square나 mixed-orientation material에서 약 960×960으로 정했습니다.

더 중요한 규칙은 상한에 맞추기 위해 일부러 upscale하지 않는다는 것입니다.

Source가 900×600이라면 1280×720으로 키워도 detail이 되살아나지 않습니다. Encoder가 표현해야 할 sample만 늘어납니다. 1920×1080 source는 720p class로 줄일 수 있지만, 900×600 source는 약 900×600 그대로 두면 됩니다. Ceiling은 maximum이지 target이 아닙니다.

단순해 보이지만 불필요한 pixel을 제거하는 것이 작은 encoder tweak 여러 개보다 더 큰 효과를 줄 수 있습니다.

source에 실제로 없는 frame에 bandwidth를 쓰지 않았다

Frame rate도 multiplier입니다. Animation에 초당 유효한 visual state가 약 16개뿐이라면 30 fps나 60 fps로 저장한다고 자동으로 움직임이 좋아지지 않습니다. 오히려 반복되거나 합성된 temporal sample이 늘고, 그것도 결국 표현해야 합니다.

제 정책은 source의 유효한 cadence를 유지하고 보통 30 fps 이하에 머무는 것입니다. 이런 material에서는 12, 15, 16, 18, 20, 24, 25, 30 fps 모두 source를 실제로 잘 표현한다면 합리적일 수 있습니다.

생성 output에서는 깔끔한 CFR도 선호합니다. VFR 자체가 잘못된 것은 아닙니다. 제 pipeline에서는 CFR이 timestamps, frame counts, duration checks, seeking, 이후 validation을 더 다루기 쉽게 만들어 줍니다.

특정 FPS 값보다 더 중요한 원칙은 이것입니다. source에 없는 시간 정보에 bandwidth를 지불하지 않는다.

CRF 28은 workload 선택이지 마법의 숫자가 아니다

모든 clip을 같은 target bitrate로 밀어 넣고 싶지 않았습니다. 거의 정지한 illustration과 복잡하게 움직이는 scene은 자연스럽게 보이는 데 필요한 bit 수가 다릅니다.

그래서 x264의 CRF mode를 사용하고, 이 bandwidth-first illustration workload에서는 -crf 28 정도로 정착했습니다. FFmpeg는 libx264의 CRF를 constant-quality rate control로 설명합니다. FFmpeg codec documentation에서 확인할 수 있습니다.

CRF 28은 의도적으로 공격적인 값입니다. Film grain이 많은 영상, noise가 많은 camera footage, 매우 작은 화면 글자, 또는 bandwidth보다 fidelity가 더 중요한 workload에 그대로 복사할 생각은 없습니다.

CRF 28이 항상 perceptually transparent하다는 universal perceptual score도 가지고 있지 않습니다. 제 workload에서 말할 수 있는 것은 더 제한적입니다. 파일은 크게 작아졌고, 일반적인 playback에서는 제 눈에 여전히 자연스럽게 보였습니다. 이것은 실사용 관찰이지, CRF 28이 visually lossless라는 주장과는 다릅니다.

veryslow가 비싼 것은 encoder 쪽이지 decoder 쪽이라고 단정할 수 없다

Preset은 -preset veryslow입니다. 느린 preset은 x264가 더 효율적인 prediction과 coding decision을 찾을 시간을 늘려 줍니다. 그 대가는 encoding CPU와 시간입니다.

여기서 중요한 구분은 encoder effort와 decoder complexity가 같은 것이 아니다라는 점입니다.

x264에는 많은 계산을 시키면서도, 완성되는 stream은 별도로 제한할 수 있습니다. 제가 사용하는 보수적인 output contract는 다음과 같습니다.

H.264 Main Profile
Level 3.1
8-bit yuv420p
avc1
refs = 4
B-frames = 5
B-pyramid = normal
open GOP = disabled

FFmpeg는 CRF, preset, tune, profile restriction, reference frame, B-frame을 각각 분리해서 설정할 수 있습니다. 저도 똑같이 생각합니다. Encoder는 충분히 탐색하게 두되, playback 쪽은 평범하게 유지합니다.

GOP와 VBV는 품질의 핵심이 아니라 guardrail이다

이 짧은 progressive clip에서는 maximum GOP를 약 5초로 사용합니다. 30 fps에서는 대략 -g 150, 24 fps에서는 -g 120, 16 fps에서는 -g 80 정도입니다.

이 값은 workload 선택이지 universal rule이 아닙니다. Adaptive streaming에는 다른 constraint가 있습니다. 예를 들어 Apple HLS authoring guidance는 2초마다 IDR을 권장합니다. 저는 이 HLS rule을 짧은 static progressive MP4에 그대로 복사하지 않습니다.

대략 다음 값도 사용합니다.

-maxrate:v 4M
-bufsize:v 8M

이 값들은 비정상적인 bitrate spike를 막는 ceiling입니다. “모든 것을 4 Mbps로 encode한다”는 뜻이 아닙니다. Normal rate allocation은 여전히 CRF가 담당하기 때문에 압축하기 쉬운 clip은 아주 작아질 수 있습니다.

MP4 container도 의도적으로 평범하게 만들었다

avc1을 명시적으로 사용합니다. Apple의 현재 HLS 문서는 avc3보다 avc1 같은 sample format을 권장합니다. 이것이 파일 크기를 줄이는 것은 아니지만, 일반적인 H.264-in-MP4 output을 만들려는 목표에는 맞습니다.

-movflags +faststart도 사용합니다. FFmpeg의 format documentation에 따르면 faststart는 MP4의 moov index를 file 앞쪽으로 옮깁니다. Android의 HTTP streaming requirement도 MPEG-4에서는 ftyp 다음, mdat 이전에 moov가 와야 한다고 설명합니다.

ftyp
moov
mdat

faststart는 compression 자체를 개선하지 않습니다. Progressive HTTP playback을 더 다루기 쉽게 만드는 설정입니다.

일반 SDR output에는 8-bit yuv420p를 쓰고 BT.709, limited/video range를 signal합니다. Audio가 필요 없는 clip에는 audio track을 만들지 않습니다. Illustration content에는 -tune animation도 사용하지만, 이것은 content-specific한 선택이지 universal compatibility contract의 일부는 아닙니다.

핵심 FFmpeg profile

30 fps illustration source라면 command의 중심 부분은 대략 다음과 같습니다.

ffmpeg -i input \
  -c:v libx264 \
  -preset veryslow \
  -tune animation \
  -crf 28 \
  -profile:v main \
  -level:v 3.1 \
  -pix_fmt yuv420p \
  -tag:v avc1 \
  -refs 4 \
  -bf 5 \
  -g 150 \
  -maxrate:v 4M \
  -bufsize:v 8M \
  -x264-params "open-gop=0:b-pyramid=normal:nal-hrd=none" \
  -color_range tv \
  -color_primaries bt709 \
  -color_trc bt709 \
  -colorspace bt709 \
  -an \
  -movflags +faststart \
  output.mp4

Scaling과 frame-rate stage는 여기서 일부러 hard-code하지 않았습니다. Source가 900×600이면 ceiling이 1280×720이라는 이유만으로 확대할 필요가 없습니다. 원래 낮은 frame rate의 animation도 예시가 -g 150이라는 이유만으로 30 fps로 만들 필요가 없습니다.

Command는 policy의 구현이지, policy 자체가 아닙니다.

파일이 몇 배 작아진 이유

Magic flag 하나가 있었던 것은 아닙니다.

서로 다른 낭비를 제거하는 여러 결정을 쌓은 결과입니다. 필요 없는 pixel, 필요 없는 frame, fixed-bitrate 사고방식, 가벼운 encoder 설정, 너무 잦은 keyframe, 필요 없는 stream을 각각 줄였습니다.

그래서 “이 파일은 H.264다”라는 말만으로는 실제 크기를 거의 알 수 없습니다. 같은 source의 H.264 encode라도 resolution, frame rate, rate control, preset, GOP structure, profile, source preparation이 다르면 크기는 크게 달라질 수 있습니다.

제 경우에는 codec 자체를 바꾸는 것보다 codec 주변의 이런 결정을 바꾸는 것이 더 중요했습니다.

이 결과가 증명하지 않는 것

각 setting을 controlled experiment로 하나씩 분리한 것은 아닙니다. 따라서 전체 savings 중 정확히 몇 퍼센트가 veryslow, CRF 28, resolution reduction, frame-rate reduction 각각에서 나왔는지는 정직하게 말할 수 없습니다.

모든 CRF 28 output이 perceptually transparent하다고도 말할 수 없습니다. “눈에 띄는 화질 저하가 없었다”는 것은 일반적인 viewing size에서 이 illustration workload를 본 제 관찰입니다. 임의의 video에 대한 과학적 보장은 아닙니다.

Single H.264 file이 모든 site에 올바른 architecture라고 주장하는 것도 아닙니다. Multiple renditions, adaptive streaming, HDR, 4K, codec negotiation이 필요하면 trade-off가 달라집니다.

제가 말할 수 있는 결과는 더 좁고, 그래서 더 실용적입니다. 짧은 illustration/animation clip을 다루는 bandwidth-first, mobile-heavy library에서 encoding time은 싸고 predictable playback은 중요했습니다. 이 조건에서 이 profile은 파일을 몇 배 작게 만들었고 일반 playback에서는 자연스러운 상태를 유지했습니다.

지금은 이렇게 생각한다

예전에는 video optimization을 주로 encoder setting 문제로 봤습니다. 지금은 lifetime cost 문제로 봅니다.

Encoder는 한 번 실행될 수 있습니다. Byte는 network를 수천 번, 수백만 번 지나갈 수 있습니다.

그러면 “비싸다”의 의미가 달라집니다.

CPU를 한 번 더 쓰는 것은 괜찮습니다. 그보다 upscale로 만들어진 pixel, 실제 motion을 늘리지 않는 frame, content가 필요로 하지 않는 bitrate를 이후의 모든 request에서 계속 보내는 쪽이 더 아깝습니다.

Codec은 평범한 상태로 남았습니다. MP4 안의 H.264입니다. 최적화는 그 주변에서 일어났습니다.

이 workload에서 얻은 교훈은 어떤 FFmpeg flag 하나보다 명확합니다. 한 번만 지불하는 비용보다, 계속 반복해서 지불하는 비용을 먼저 최적화한다.