Quay lại blog
13 tháng 8, 2026Sergei Solod10 phút đọc

Tôi đã làm video H.264 nhỏ đi vài lần mà không thấy giảm chất lượng rõ rệt như thế nào

Codec vẫn là H.264, nhưng tôi xây lại chính sách encode quanh CRF 28, x264 veryslow, giới hạn độ phân giải lớp 720p và chỉ giữ frame rate thực sự hữu ích. File nhỏ đi vài lần trong khi playback thông thường vẫn nhìn tự nhiên. Bài học quan trọng nhất: chi phí encode chỉ trả một lần, còn bandwidth phải trả ở mỗi lượt xem.

H.264FFmpegNén videoHiệu năng webx264

Kết quả trông giống kiểu cải thiện mà bình thường tôi sẽ kỳ vọng khi chuyển sang một codec mới hơn: các file video nhỏ đi vài lần, trong khi playback thông thường vẫn tự nhiên và ở kích thước xem bình thường tôi không nhận thấy sự giảm chất lượng rõ rệt.

Nhưng tôi không chuyển sang AV1, HEVC hay VP9. Tôi vẫn dùng H.264 bên trong MP4.

Thứ thay đổi là mọi thứ xung quanh codec. Tôi xây lại chính sách encoding cho một workload rất cụ thể: clip minh họa và animation ngắn, phần lớn traffic đến từ mobile, bandwidth là chi phí lặp lại quan trọng nhất, còn thời gian cho một lần encode offline gần như không đáng bận tâm.

Baseline tôi chốt cố tình bảo thủ ở phía playback và cố tình tốn kém ở phía encoding: H.264 Main Profile @ Level 3.1, avc1, 8-bit yuv420p, CRF 28, x264 veryslow, trần độ phân giải lớp 720p, frame rate hữu ích thường không vượt 30 fps, giới hạn references và B-frames, cùng faststart cho progressive MP4 delivery.

Tối ưu quan trọng nhất không phải một flag của FFmpeg

Thay đổi lớn nhất là cách tôi nhìn chi phí.

Encoding diễn ra một lần. Delivery diễn ra mỗi lần có người request file.

Với video real-time, dùng nhiều CPU hơn rất nhiều chỉ để tiết kiệm một chút bitrate có thể là trade không tốt. File của tôi được encode offline rồi serve lặp đi lặp lại. Trong mô hình đó, tiết kiệm mười phút encode gần như vô nghĩa về kinh tế nếu encode nhanh hơn khiến mọi request về sau đều lớn hơn.

Đó là lý do -preset veryslow hợp với tôi. Tôi sẵn sàng trả chi phí CPU một lần nếu x264 có thể dùng nó để tìm representation hiệu quả hơn. Browser không lặp lại quá trình search của encoder; nó chỉ decode bitstream đã hoàn tất.

Quy tắc trở nên đơn giản: tiêu computation ở bước chỉ làm một lần, và tiết kiệm byte ở bước phải lặp lại.

Vì sao tôi ở lại với H.264 thay vì chạy theo codec mới nhất

Tôi không nói H.264 là codec nén hiệu quả nhất hiện có. Không phải. Codec mới có thể hấp dẫn khi hệ thống delivery giữ được nhiều rendition và chọn bản phù hợp nhất cho từng client.

Ràng buộc của tôi khác: một URL, một file, một codec, và càng ít rắc rối playback càng tốt với lượng người dùng mobile lớn.

Cho bài toán này, H.264 trong MP4 vẫn là baseline rất an toàn. Apple hiện khuyên web developer dùng file MP4 được encode H.264 cho static video trên Safari. Tài liệu Android hiện tại cũng liệt kê H.264 trong MP4 và yêu cầu Main Profile decoder từ Android 6.0 trở lên; khuyến nghị playback của Android còn có 1280×720 ở 30 fps như một cấu hình H.264 HD. Xem các định dạng media Android hỗ trợ.

Điều đó không có nghĩa thiết bị hiện đại bị giới hạn ở Main Profile hay Level 3.1. Ví dụ, hướng dẫn HLS của Apple thường ưu tiên High Profile hơn Main hoặc Baseline. Tôi chọn Main@3.1 vì muốn một decoder envelope cố tình vừa phải cho một static MP4 duy nhất, không phải vì Apple bắt buộc.

Tôi ngừng encode những pixel không cần tồn tại

Resolution là một trong những lever lớn nhất. Trần của tôi trở thành khoảng 1280×720 cho landscape, 720×1280 cho portrait và khoảng 960×960 cho nội dung square hoặc mixed-orientation.

Quy tắc quan trọng hơn: đừng upscale chỉ để chạm trần.

Nếu source là 900×600, biến nó thành 1280×720 không khôi phục detail. Nó chỉ tạo thêm sample mà encoder phải mô tả. Source 1920×1080 có thể giảm về lớp 720p, còn source 900×600 có thể giữ gần 900×600. Ceiling là maximum, không phải target.

Nghe đơn giản, nhưng bỏ pixel thừa có thể quan trọng hơn nhiều tweak encoder obscure.

Tôi ngừng trả bandwidth cho frame mà source thực ra không có

Frame rate là multiplier khác. Nếu một animation chỉ có khoảng 16 trạng thái hình ảnh hữu ích mỗi giây, lưu nó ở 30 hoặc 60 fps không tự động tạo motion tốt hơn. Thường nó chỉ tạo thêm temporal sample lặp hoặc tổng hợp, mà encoder vẫn phải biểu diễn.

Policy của tôi là giữ useful cadence của source và thường không vượt 30 fps. Với loại material này, 12, 15, 16, 18, 20, 24, 25 hoặc 30 fps đều có thể hợp lý nếu thực sự mô tả source.

Tôi cũng thích CFR sạch cho output được generate. VFR không hỏng về bản chất; CFR chỉ làm timestamps, frame counts, duration checks, seeking và validation sau đó dễ hơn trong pipeline của tôi.

Nguyên tắc chung hữu ích hơn một con số FPS cụ thể: đừng trả bandwidth cho thông tin thời gian mà source không có.

CRF 28 là lựa chọn theo workload, không phải con số thần kỳ

Tôi không muốn ép mọi clip về cùng một target bitrate. Một illustration gần như tĩnh và một scene chuyển động phức tạp không cần cùng số bit để nhìn chấp nhận được.

Vì vậy tôi dùng CRF mode của x264 và với workload illustrated ưu tiên bandwidth này, tôi chốt quanh -crf 28. FFmpeg mô tả CRF trong libx264 là constant-quality rate control; xem tài liệu codec FFmpeg.

CRF 28 cố tình aggressive. Tôi sẽ không copy mù quáng sang film grain, footage camera nhiều noise, chữ rất nhỏ trên màn hình, hoặc workload nơi fidelity quan trọng hơn bandwidth.

Tôi cũng không có perceptual score universal chứng minh CRF 28 transparent. Điều tôi có thể nói từ workload của mình hẹp hơn: file nhỏ đi rất nhiều nhưng với tôi vẫn nhìn bình thường trong playback thông thường. Đó là quan sát thực tế, không phải tuyên bố CRF 28 visually lossless.

veryslow đắt với encoder, không tự động đắt với decoder

Preset của tôi là -preset veryslow. Preset chậm hơn cho x264 nhiều cơ hội hơn để tìm prediction và coding decision hiệu quả. Cái giá là CPU và thời gian encoding.

Điểm phân biệt quan trọng: encoder effort và decoder complexity không phải một thứ.

Tôi có thể để x264 làm việc rất nặng trong khi giới hạn stream cuối riêng biệt. Output contract bảo thủ của tôi là:

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

FFmpeg expose CRF, preset, tuning, profile restriction, reference frame và B-frame riêng biệt. Tôi cũng nghĩ về chúng như vậy: để encoder search thật kỹ, nhưng giữ phía playback thật bình thường.

GOP và VBV là guardrail, không phải quality control chính

Với các progressive clip ngắn này, maximum GOP của tôi khoảng năm giây: khoảng -g 150 ở 30 fps, -g 120 ở 24 fps, hoặc -g 80 ở 16 fps.

Đây là lựa chọn theo workload, không phải universal rule. Adaptive streaming có constraint khác; hướng dẫn HLS authoring của Apple, chẳng hạn, khuyến nghị IDR mỗi hai giây. Tôi không copy mù quáng rule HLS đó sang static progressive MP4 ngắn.

Tôi cũng dùng khoảng:

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

Các giá trị này là ceiling chống bitrate spike bất thường. Chúng không có nghĩa “encode mọi thứ ở 4 Mbps”. CRF vẫn chịu trách nhiệm normal rate allocation, nên clip dễ vẫn có thể rất nhỏ.

Tôi cũng làm MP4 container cố tình nhàm chán

Tôi dùng rõ ràng avc1. Tài liệu HLS hiện tại của Apple ưu tiên sample format như avc1 thay vì avc3. Điều đó không làm file nhỏ hơn, nhưng phù hợp với mục tiêu tạo output H.264-in-MP4 thông thường.

Tôi cũng dùng -movflags +faststart. Tài liệu format FFmpeg nói faststart chuyển index moov của MP4 lên đầu file. Yêu cầu HTTP streaming của Android cũng nói với MPEG-4, moov phải đứng sau ftyp và trước mdat.

ftyp
moov
mdat

faststart không cải thiện compression. Nó làm progressive HTTP playback bớt khó chịu.

Với output SDR bình thường tôi dùng 8-bit yuv420p và signal BT.709 với limited/video range. Nếu clip không có audio, tôi không tạo audio track. Với illustrated content này tôi còn dùng -tune animation; tôi xem nó là lựa chọn theo content, không phải phần của universal compatibility contract.

FFmpeg profile cốt lõi

Với illustrated source 30 fps, phần chính của command gần như sau:

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

Các stage scaling và frame-rate cố tình không được hard-code. Source 900×600 không nên phóng to chỉ vì ceiling là 1280×720, và animation có frame rate tự nhiên thấp không nên bị ép lên 30 fps chỉ vì ví dụ dùng -g 150.

Command là implementation của policy, không phải policy.

Vì sao file nhỏ đi vài lần

Không có magic flag nào cả.

Reduction đến từ việc xếp chồng nhiều quyết định, mỗi quyết định bỏ một kiểu lãng phí khác nhau: pixel thừa, frame thừa, tư duy fixed-bitrate, encoder setting rẻ, keyframe quá thường xuyên và stream tôi không cần.

Vì vậy câu “file này là H.264” nói ít đáng ngạc nhiên về kích thước của nó. Hai encode H.264 của cùng source có thể khác nhau rất nhiều vì tên codec không mô tả resolution, frame rate, rate control, preset, GOP structure, profile hay source preparation.

Trong trường hợp của tôi, thay đổi những quyết định quanh codec quan trọng hơn đổi codec.

Kết quả này không chứng minh điều gì

Tôi không isolate từng setting trong controlled experiment, nên không thể trung thực gán một phần trăm chính xác của savings riêng cho veryslow, CRF 28, giảm resolution hay giảm frame rate.

Tôi cũng không thể tuyên bố mọi output CRF 28 đều perceptually transparent. “Không thấy giảm chất lượng rõ rệt” là quan sát của tôi với illustrated workload này ở kích thước xem bình thường, không phải bảo đảm khoa học cho mọi video.

Và tôi không lập luận rằng một file H.264 là kiến trúc đúng cho mọi site. Multiple renditions, adaptive streaming, HDR, 4K và codec negotiation làm thay đổi trade-off.

Kết quả hẹp hơn và hữu ích hơn: với một library short illustrated và animated clips ưu tiên bandwidth, mobile-heavy, nơi encoding time rẻ và predictable playback quan trọng, profile này làm file của tôi nhỏ đi vài lần nhưng vẫn nhìn bình thường trong playback thông thường.

Quy tắc tôi dùng bây giờ

Trước đây tôi nghĩ video optimization chủ yếu là bài toán encoder settings. Bây giờ tôi nghĩ nó là bài toán lifetime cost.

Encoder có thể chạy một lần. Byte có thể đi qua network hàng nghìn hoặc hàng triệu lần.

Điều đó thay đổi nghĩa của “đắt”.

Tôi sẵn sàng tiêu CPU một lần. Tôi ít sẵn sàng hơn nhiều với việc gửi pixel tạo ra bởi upscale, frame không thêm useful motion, hoặc bitrate content không cần ở mỗi request trong tương lai.

Codec vẫn nhàm chán: H.264 trong MP4. Tối ưu xảy ra xung quanh nó.

Với workload này, bài học rõ hơn bất kỳ flag FFmpeg riêng lẻ nào: hãy tối ưu chi phí bạn phải trả lặp đi lặp lại, không phải chi phí bạn chỉ trả một lần.