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

Cách tôi chuyển WebP động, GIF và APNG sang H.264 MP4 mà không làm hỏng khung hình hay thời gian

Quy trình sản xuất của tôi tái tạo các khung hình đầy đủ mà người dùng thực sự thấy, giữ nguyên thời gian nguồn, chọn một CFR cho mỗi MP4 cuối, dùng khung chung nhỏ nhất mà không phóng to, mã hóa các đoạn H.264 tương thích, ghép chúng mà không qua lần mã hóa mất dữ liệu thứ hai, rồi xác thực cả tệp lẫn việc phân phối qua HTTP. Trong một lần chạy được đo, 217 WebP động với tổng dung lượng 1.49 GB trở thành một H.264 MP4 78.49 MB.

H.264FFmpegMP4WebP độngGIFAPNGNén videoXử lý phương tiện

Tôi không xây quy trình này để thử nghiệm các bộ mã hóa/giải mã khác nhau. Tôi xây nó vì ảnh động đã trở thành một cách quá tốn kém để phân phối thứ mà trên thực tế chỉ là một đoạn video ngắn không có âm thanh.

Phần lớn nội dung tôi xử lý là WebP động, GIF và APNG ngắn: thường chỉ vài giây, chỉ vài chục khung hình thực sự được hiển thị và có mức dư thừa theo thời gian rất lớn. Phần lớn lượt xem đến từ thiết bị di động, cùng một tệp có thể được yêu cầu nhiều lần, vì vậy số byte phải truyền về sau quan trọng với tôi hơn nhiều so với chi phí mã hóa chỉ trả một lần.

Phần khó không phải là gọi FFmpeg. Ảnh động không nhất thiết là một dãy ảnh đầy đủ có cùng kích thước và cùng tốc độ khung hình. Nó có thể chứa các hình chữ nhật cục bộ, quy tắc hòa trộn và xử lý sau khung hình, kênh alpha, độ trễ không đều, khung hình có thời lượng bằng 0, nhiều hướng ảnh khác nhau và dữ liệu thời gian mà công cụ thăm dò thông thường có thể tóm tắt sai.

Vì vậy tôi coi việc chuyển đổi là một chuỗi điều kiện bất biến cần được giữ nguyên, chứ không phải một câu lệnh duy nhất:

WebP động / GIF / APNG
        ↓
tái tạo trạng thái bề mặt đầy đủ mà người xem thực sự thấy
        ↓
khôi phục và chuẩn hóa thời gian nguồn
        ↓
phân tích mọi nguồn trong chuỗi cuối
        ↓
chọn một CFR cho MP4 cuối
        ↓
tính khung chung nhỏ nhất mà không phóng to
        ↓
mã hóa các đoạn H.264 tương thích
        ↓
kiểm tra hợp đồng luồng
        ↓
ghép bằng cách sao chép luồng
        ↓
chuẩn hóa và kiểm tra dòng thời gian gói
        ↓
kiểm tra phân phối HTTP
        ↓
xuất bản bằng một thao tác thay thế duy nhất

Bộ mã hóa/giải mã quan trọng, nhưng giữ đúng những gì hoạt ảnh thực sự hiển thị còn quan trọng hơn.

Kết quả sản xuất đã đo: 217 tệp WebP động trở thành một MP4 78.49 MB

Đầu vào không phải một video 1.49 GB. Đó là 217 tệp WebP động riêng biệt chứa tổng cộng 10,633 khung hình được hiển thị. Tổng dung lượng các hoạt ảnh nguồn khoảng 1.49 GB.

ĐẦU VÀO
217 tệp WebP động
tổng 1.49 GB
10,633 khung hình được hiển thị

ĐẦU RA
1 H.264 MP4
78.49 MB
0.98 Mbps
1264×720
30 fps CFR
Main@3.1 / CRF 28 / veryslow

Tệp H.264 cuối có dung lượng 78.49 MB ở khoảng 0.98 Mbps. So với tổng số byte của các hoạt ảnh nguồn, nó nhỏ hơn khoảng 19 lần, tức ít hơn khoảng 94.7% dữ liệu.

Đây là kết quả đầu-cuối của toàn bộ quy trình, không phải phép thử A/B sạch giữa “H.264 cũ” và “H.264 mới”. Cách biểu diễn đã đổi từ hàng trăm tệp ảnh động sang một video có nén theo thời gian, nên tôi không quy toàn bộ mức giảm 19 lần cho CRF 28, veryslow hay bất kỳ tùy chọn mã hóa đơn lẻ nào.

Trước tiên, tái tạo đúng bức ảnh đầy đủ mà người xem thực sự nhìn thấy

Lối tắt nguy hiểm nhất là giả định rằng mỗi khung hình được lưu trong ảnh động là một ảnh đầy đủ thay thế hoàn toàn ảnh trước đó.

Khung WebP động có thể mô tả một hình chữ nhật ở vị trí cụ thể kèm hành vi hòa trộn và xử lý sau khi hiển thị. APNG có độ lệch, kích thước, độ trễ, thao tác xử lý và hòa trộn. GIF cũng có thể giữ nguyên nền trước đó, xóa một vùng hoặc phục hồi trạng thái cũ hơn.

Vì vậy khung hình được lưu có thể chỉ là một mảnh nhỏ phụ thuộc vào bề mặt đã được các khung trước tạo ra. Nếu mã hóa các mảnh đó như ảnh đầy đủ, kết quả không phải bản nhỏ hơn của hoạt ảnh đúng mà là một hoạt ảnh sai.

Đơn vị tôi trích xuất là trạng thái bề mặt hiển thị đầy đủ: toàn bộ điểm ảnh đã được tổng hợp mà một trình xem đúng sẽ hiển thị sau khi áp dụng quy tắc xử lý của khung trước và quy tắc hòa trộn của khung hiện tại.

Đây là đảm bảo đúng đắn đầu tiên của toàn quy trình. Một khung cục bộ sai đã bị làm phẳng vào H.264 thì CRF, thiết lập sẵn hay tùy chọn đóng gói phía sau không thể sửa lại.

Thời gian khung hình là dữ liệu nguồn, không phải FPS để đoán

Các định dạng hoạt ảnh lưu thời gian khác nhau. WebP động dùng thời lượng riêng cho từng khung theo đơn vị 1 ms. GIF lưu độ trễ theo phần trăm giây. APNG lưu tử số và mẫu số cho độ trễ của từng khung; nếu mẫu số bằng 0, đặc tả PNG xử lý nó như 100.

Chính các độ trễ này là dòng thời gian thật. FPS nổi bật do công cụ thăm dò thông thường báo chỉ là một giá trị tóm tắt và có thể gây hiểu lầm.

Một WebP thật trong quy trình của tôi có kích thước 1264×720 với 49 khung hình được hiển thị. Độ trễ luân phiên 62 và 63 ms, tổng thời lượng 3.063 giây. Về thực chất đây là nhịp 16 fps vì một khung ở 16 fps kéo dài 62.5 ms.

Một công cụ thăm dò thông thường lại báo nguồn đó là 25 fps. Tin vào con số này sẽ làm thay đổi thời gian nguồn hoặc tạo các ảnh lặp không cần thiết.

Tôi cũng cần chính sách cho các độ trễ lỗi hoặc mơ hồ. WebP để việc diễn giải thời lượng khung bằng 0, và thường cả các thời lượng rất nhỏ, cho từng triển khai. GIF có thể có độ trễ 0. APNG cho phép tử số 0, nghĩa là khung tiếp theo nên được vẽ nhanh nhất có thể, dù trình xem vẫn có thể áp dụng một giới hạn dưới thực tế.

Chính sách chuẩn hóa của tôi giữ thời gian ở độ chính xác mili giây, dùng mức tối thiểu 10 ms cho thời lượng bằng 0 hoặc nhỏ bất hợp lý, và chỉ dùng 100 ms làm giá trị dự phòng khi thực sự không có thông tin thời gian hữu ích. Đây là chính sách vận hành của tôi chứ không phải tiêu chuẩn phổ quát.

Tôi chọn một CFR cho toàn bộ MP4 cuối thay vì mặc định 30 fps

Sau khi tái tạo các trạng thái hiển thị và thời lượng, tôi ánh xạ dòng thời gian nguồn sang dòng thời gian video. Tôi không mã hóa mù mọi thứ ở 30 fps.

Với mọi nguồn sẽ xuất hiện trong cùng một MP4 cuối, tôi đánh giá tập ứng viên nhỏ sau:

10, 12, 15, 16, 18, 20, 24, 25, 30 fps

Bộ chọn lấy CFR thấp nhất vẫn biểu diễn toàn bộ chuỗi cuối ở mức chấp nhận được. Các MP4 cuối khác nhau có thể chọn tốc độ khác nhau, nhưng mọi đoạn được mã hóa độc lập trong cùng một MP4 phải dùng cùng CFR đã chọn.

Ví dụ 3.063 giây cho thấy mức tiết kiệm rất rõ: ở 16 fps cần khoảng 49 khung hình đầu ra; ở 30 fps cần khoảng 92 khung hình. Nếu một nguồn khác trong cùng MP4 thực sự cần 30 fps thì toàn bộ bộ sưu tập dùng 30. Tôi không trộn tốc độ khung hình giữa các đoạn trong một luồng cuối.

Tôi dùng thang thời gian 90,000 Hz cho rãnh video vì mọi CFR được phép đều ánh xạ thành thời lượng khung nguyên chính xác:

10 fps → 9000 ticks
12 fps → 7500 ticks
15 fps → 6000 ticks
16 fps → 5625 ticks
18 fps → 5000 ticks
20 fps → 4500 ticks
24 fps → 3750 ticks
25 fps → 3600 ticks
30 fps → 3000 ticks

Chính thang thời gian của rãnh video tạo ra lưới khung chính xác này. Tôi cũng đặt thang thời gian chung của MP4 là 90.000 để giữ tính nhất quán, nhưng đó là một đồng hồ riêng ở cấp bộ chứa. Trình kiểm tra đối chiếu các gói video với lưới số nguyên chính xác, thay vì dựa vào thời lượng thập phân đã làm tròn.

Giới hạn độ phân giải là trần, không phải kích thước khung ảnh bắt buộc

Khung giới hạn phân phối của tôi khoảng 1280×720 cho ngang, 720×1280 cho dọc, và với nội dung trộn hướng thì cả chiều rộng lẫn chiều cao đều không vượt quá 960.

Quy tắc bắt buộc là không bao giờ phóng to. Nguồn 900×600 không tốt hơn khi biến thành 1280×720; nó chỉ tạo thêm điểm ảnh nội suy để bộ mã hóa phải mô tả.

Quy tắc thứ hai ít hiển nhiên hơn: 960×960 là giới hạn tối đa, không phải khung ảnh vuông bắt buộc.

Trước hết tôi tính kích thước vùng hoạt động của từng nguồn, chỉ cho phép thu nhỏ. Sau đó chuỗi cuối dùng khung chung có kích thước chẵn nhỏ nhất có thể chứa toàn bộ các vùng hoạt động đã được thu nhỏ.

Ví dụ, nếu chuỗi cuối cần ảnh ngang 960×540 và ảnh dọc 500×900, khung chung có thể là 960×900 thay vì 960×960. Mọi đoạn vẫn có kích thước mã hóa giống nhau nên ghép bằng sao chép luồng vẫn khả thi, nhưng không phải mã hóa vùng đen vô ích.

Tôi giữ tỷ lệ khung hình và đệm phần trống thay vì kéo giãn. Trong quy trình này phần đệm là đen. Vì H.264/yuv420p thông thường không giữ kênh alpha của nguồn, độ trong suốt được ghép phẳng có chủ ý lên nền đó thay vì mất ngẫu nhiên.

Vì sao H.264 MP4 phù hợp với bài toán phân phối này

GIF, APNG và WebP động không phải định dạng thô sơ. Chúng có thể tránh vẽ lại các vùng không đổi, nên khẳng định “video luôn nhỏ hơn” là sai.

Nhưng H.264 được thiết kế cho dự đoán theo thời gian giữa các hình. Các vòng lặp minh họa ngắn với nền tĩnh và vùng chuyển động nhỏ là loại nội dung thuận lợi cho ảnh tham chiếu, dự đoán liên khung, P-khung hình và B-khung hình.

Hướng dẫn Safari hiện tại của Apple khuyến nghị MP4 mã hóa H.264 cho video tĩnh và nói GIF động có thể tốn băng thông tới 12 lần và năng lượng khoảng 2 lần so với bộ mã hóa/giải mã video hiện đại. Con số 12× là ví dụ của Apple, không phải số đo của tôi.

Vì vậy kết quả giảm khoảng 19 lần mà tôi đo được là một quan sát thực tế hữu ích, nhưng tôi vẫn đo trực tiếp từng trường hợp. Tôi không mặc định rằng mọi WebP động vốn đã nhỏ và được tối ưu tốt đều sẽ lớn hơn MP4.

Mỗi hoạt ảnh được mã hóa thành một đoạn dưới cùng một hợp đồng luồng

Khi bộ mã hóa chạy, quy trình đã biết các khung hình được hiển thị, toàn bộ bộ sưu tập CFR, khung cuối và thời lượng dự kiến.

Phần cốt lõi của lệnh mã hóa từng đoạn trông xấp xỉ như sau:

ffmpeg -framerate "$COLLECTION_FPS" -i frame-%06d.png \
  -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 "$GOP_FRAMES" \
  -maxrate:v 4M \
  -bufsize:v 8M \
  -x264-params "stitchable=1:open-gop=0:b-pyramid=normal:nal-hrd=none" \
  -color_range tv \
  -color_primaries bt709 \
  -color_trc bt709 \
  -colorspace bt709 \
  -fps_mode passthrough \
  -map_metadata -1 \
  -map_chapters -1 \
  -an -sn -dn \
  -video_track_timescale 90000 \
  -movie_timescale 90000 \
  -t "$EXPECTED_DURATION" \
  segment.mp4

Giới hạn GOP khoảng năm giây, suy ra từ CFR đã chọn: 80 khung hình ở 16 fps, 120 ở 24 fps và 150 ở 30 fps.

-t "$EXPECTED_DURATION" không phải một lớp bảo vệ trang trí. Trong danh sách khung của tôi, ảnh cuối được lặp lại như một mốc kết thúc để thời lượng của khung thật cuối cùng được áp dụng đúng. Nếu không giới hạn rõ thời lượng, mốc kết thúc đó có thể tạo thêm một mẫu ở cuối.

Tôi đã tái hiện hiện tượng này với trường hợp 49 khung, dài 3.063 giây. Không có -t, kết quả là 50 khung ở 16 fps và 94 khung ở 30 fps. Với -t 3.063, số khung trở lại đúng như dự kiến: 49 ở 16 fps và 92 ở 30 fps.

Ghép bằng sao chép luồng chỉ an toàn sau khi kiểm tra tương thích nghiêm ngặt

Bộ tách ghép nối của FFmpeg yêu cầu các tệp có cùng cấu trúc luồng, bao gồm bộ mã hóa/giải mã và cơ sở thời gian, đồng thời dùng thời lượng của từng tệp để đặt vị trí tệp tiếp theo. Vì vậy siêu dữ liệu thời lượng sai có thể làm méo dòng thời gian.

Tôi không dùng bước ghép để biến các tệp không tương thích thành tương thích. Trước khi được chấp nhận, mỗi đoạn phải đáp ứng đầy đủ hợp đồng sau:

CFR của toàn bộ tập = giống nhau
cơ sở thời gian của luồng = giống nhau
thang thời gian rãnh video MP4 = giống nhau
kích thước khung chung / SAR = giống nhau
cấu hình / cấp / định dạng điểm ảnh = giống nhau
tín hiệu màu = giống nhau
avcC / dữ liệu bổ sung AVC = giống nhau từng byte

Tôi dùng stitchable=1 của x264 vì các đoạn được mã hóa độc lập, nhưng không coi tùy chọn đó là bằng chứng rằng cấu hình AVC thực sự giống nhau. Trước khi ghép, tôi vẫn so sánh các byte cấu hình thực tế.

Khi hợp đồng được giữ đúng, lần ghép cuối không tạo thêm một thế hệ nén mất dữ liệu ở cấp dòng bit video:

ffmpeg -f concat -safe 0 -i segments.ffconcat \
  -c:v copy \
  -an -sn -dn \
  -movflags +faststart \
  final.mp4

-c:v copy giúp các đoạn H.264 đã mã hóa không bị giải mã rồi nén lại lần thứ hai.

Kiểm tra bao phủ cả tệp MP4 lẫn cách tệp được phân phối qua mạng

Tôi không xuất bản tệp chỉ vì FFmpeg kết thúc với mã 0.

Trình kiểm tra từng bắt được các lỗi thời gian xác định trong thực tế: ở 16 fps xuất hiện 5580 tick trong khi hợp đồng yêu cầu 5625; về sau, một đầu ra 24 fps có 3751 thay vì đúng 3750. Phân tích sâu về sai lệch một tick là một chủ đề riêng; bài học ở đây chỉ là chạy lại cùng một phép mã hóa không sửa được lỗi dòng thời gian có tính xác định.

Với chính đối tượng phương tiện, tôi kiểm tra số luồng dự kiến, cấu hình và cấp H.264, định dạng điểm ảnh, kích thước chính xác đã tính, SAR, tín hiệu màu, thang thời gian 90 kHz của rãnh video, thời lượng gói, số khung và gói, tổng thời lượng, quan hệ PTS/DTS, cấu hình AVC giống nhau giữa các đoạn, việc moov nằm trước mdat, rồi giải mã toàn bộ với chế độ xử lý lỗi nghiêm ngặt.

ffprobe -v error \
  -select_streams v:0 \
  -show_streams \
  -show_packets \
  -of json \
  final.mp4

ffmpeg -v error -xerror -err_detect explode \
  -i final.mp4 -f null -

Nhưng một MP4 đúng trên đĩa vẫn có thể được máy chủ gửi sai. Vì vậy tôi kiểm tra cả đường phân phối HTTP: Content-Type đúng như dự kiến, Content-Length chính xác, hỗ trợ yêu cầu theo dải byte, phản hồi 206 Partial Content hợp lệ và Content-Range chính xác.

Khi thay đổi hợp đồng mã hóa, tôi còn chạy một thử nghiệm nhỏ trên thiết bị và trình duyệt thật thay vì coi ffprobe là bằng chứng về tương thích phần cứng. Tôi kiểm tra bắt đầu phát, tua, lặp, chuyển sang nền rồi tiếp tục, và phát theo dải trên iPhone/Safari hiện tại, một thiết bị Android phổ thông và các trình duyệt máy tính chính.

Chỉ khi cả tệp lẫn đường phân phối đều đạt hợp đồng, tôi mới thay tệp đang phục vụ bằng một thao tác thay thế duy nhất.

Những thông tin mà quy trình này chủ động chấp nhận mất đi

Đây là phép chuyển đổi để phân phối, không phải bản gốc lưu trữ. Kênh alpha được ghép phẳng. Thời gian nguồn không đều được lượng tử hóa lên một CFR của MP4 cuối. Nguồn quá lớn có thể bị thu nhỏ. WebP động vốn đã nén mất dữ liệu sẽ trải qua thêm một thế hệ nén mất dữ liệu. Âm thanh được chủ động loại bỏ.

Tôi không dùng chính quy trình này khi cần giữ độ trong suốt để ghép lên mọi nền tùy ý, khi thời gian không đều chính xác của từng khung là một phần ý nghĩa nội dung, khi đang tạo bản lưu trữ gốc, hoặc khi ứng dụng đã có hệ thống video đa bộ mã hóa thích ứng giải quyết bài toán phân phối theo cách khác.

Với WebP động rất nhỏ và đã được tối ưu tốt, tôi vẫn đo trực tiếp thay vì mặc định rằng MP4 chắc chắn sẽ thắng.

Trình tự dùng trong môi trường thực tế hiện nay

  1. Phát hiện định dạng có chuyển động và đọc siêu dữ liệu điều khiển khung thực tế.
  2. Tái tạo đầy đủ trạng thái bề mặt được hiển thị theo đúng quy tắc hòa trộn và xử lý của định dạng.
  3. Khôi phục thời lượng từng khung và sửa các giá trị không hợp lệ.
  4. Xây dựng dòng thời gian nguồn đáng tin cậy với độ chính xác mili giây.
  5. Phân tích mọi nguồn sẽ xuất hiện trong cùng một MP4 cuối.
  6. Chọn một CFR chung cho toàn bộ tập từ 10/12/15/16/18/20/24/25/30.
  7. Ánh xạ các trạng thái hiển thị lên dòng thời gian CFR đã chọn.
  8. Tính kích thước vùng hoạt động chỉ bằng cách thu nhỏ; tuyệt đối không phóng to.
  9. Tạo khung chung có kích thước chẵn nhỏ nhất mà chuỗi cuối thực sự cần.
  10. Đệm phần trống mà không kéo giãn hình và chủ động ghép phẳng kênh alpha.
  11. Mã hóa mỗi nguồn thành H.264 Main@3.1 / yuv420p / avc1 dưới cùng một hợp đồng luồng.
  12. Giới hạn mỗi đoạn bằng thời lượng dự kiến.
  13. Loại bỏ mọi đoạn có cấu hình AVC thực tế hoặc thời gian vi phạm hợp đồng.
  14. Ghép các đoạn đã chấp nhận bằng -c:v copy.
  15. Chuẩn hóa và kiểm tra dòng thời gian gói cuối.
  16. Giải mã toàn bộ kết quả.
  17. Kiểm tra các tiêu đề HTTP, dải byte và hành vi nội dung từng phần.
  18. Chạy thử nghiệm nhỏ trên thiết bị và trình duyệt sau mỗi thay đổi cấu hình bộ mã hóa.
  19. Chỉ xuất bản bằng một thao tác thay thế duy nhất sau khi mọi phép kiểm tra đều thành công.

Dòng thời gian, không phải phần mở rộng tệp, mới là nguồn dữ liệu chuẩn

WebP động, GIF hay APNG là một chuỗi theo thời gian của các trạng thái bề mặt hiển thị đầy đủ, không chỉ là một thư mục ảnh có phần mở rộng hình ảnh.

H.264 tận dụng dư thừa theo thời gian rất tốt, nhưng không thể sửa việc ghép hình sai, thời gian bị bịa ra hay siêu dữ liệu đoạn không tương thích. Phần lớn công việc kỹ thuật giúp quy trình này đáng tin cậy diễn ra trước và sau x264.

Nguyên tắc tôi giữ lại là: chỉ thay đổi cách biểu diễn khi có thể mô tả chính xác những gì bắt buộc phải giữ nguyên.

Tài liệu chính