กลับไปที่บล็อก
13 สิงหาคม 2569Sergei Solod15 นาทีในการอ่าน

ผมลดขนาดวิดีโอ H.264 ลงหลายเท่าโดยแทบไม่เห็นคุณภาพลดลงได้อย่างไร

Codec ยังเป็น H.264 เหมือนเดิม แต่ผมสร้างนโยบาย encoding ใหม่รอบ CRF 28, x264 veryslow, เพดานความละเอียดระดับ 720p และเก็บเฉพาะ frame rate ที่มีประโยชน์จริง ไฟล์เล็กลงหลายเท่าแต่ playback ปกติยังดูเป็นธรรมชาติ บทเรียนสำคัญคือค่า encoding จ่ายครั้งเดียว ส่วนค่า bandwidth จ่ายซ้ำทุกครั้งที่มีคนดู

H.264FFmpegการบีบอัดวิดีโอประสิทธิภาพเว็บx264

ผลลัพธ์ดูเหมือนสิ่งที่ปกติผมคาดว่าจะได้จากการเปลี่ยนไปใช้ codec ใหม่กว่า: video files ของผมเล็กลงหลายเท่า แต่ playback ปกติยังดูเป็นธรรมชาติ และในขนาดการรับชมทั่วไปผมไม่เห็น quality drop ที่ชัดเจน

แต่ผมไม่ได้ย้ายไป AV1, HEVC หรือ VP9 ผมยังใช้ H.264 ภายใน MP4 เหมือนเดิม

สิ่งที่เปลี่ยนคือทุกอย่างรอบ codec ผมสร้าง encoding policy ใหม่สำหรับ workload ที่เฉพาะมาก: คลิปภาพวาดและ animation สั้น ๆ, audience ที่ mobile-heavy, bandwidth เป็นค่าใช้จ่ายที่เกิดซ้ำที่สำคัญที่สุด และแทบไม่สนใจว่า offline encode ที่ทำเพียงครั้งเดียวจะใช้เวลานานแค่ไหน

Baseline ที่ผมลงเอยด้วยจงใจ conservative ฝั่ง playback และจงใจ expensive ฝั่ง encoding: H.264 Main Profile @ Level 3.1, avc1, 8-bit yuv420p, CRF 28, x264 veryslow, เพดาน resolution ระดับ 720p, frame rate ที่มีประโยชน์ซึ่งปกติไม่เกิน 30 fps, จำกัด references และ B-frames, และ faststart สำหรับ progressive MP4 delivery

Optimization ที่สำคัญที่สุดไม่ใช่ FFmpeg flag

การเปลี่ยนแปลงที่ใหญ่ที่สุดคือวิธีที่ผมคิดเรื่อง cost

Encoding เกิดขึ้นครั้งเดียว Delivery เกิดขึ้นทุกครั้งที่มีคน request file

สำหรับ real-time video การใช้ CPU มากขึ้นมากเพื่อประหยัด bitrate เล็กน้อยอาจเป็น trade ที่ไม่ดี แต่ files ของผม encode แบบ offline แล้วถูก serve ซ้ำ ๆ ในโมเดลนี้ การประหยัดเวลา encode สิบนาทีแทบไม่มีความหมายทางเศรษฐกิจ ถ้า encode ที่เร็วขึ้นทำให้ทุก request ในอนาคตมีขนาดใหญ่ขึ้น

นี่คือเหตุผลที่ -preset veryslow เหมาะกับผม ผมยอมจ่าย CPU ครั้งเดียวถ้า x264 ใช้มันเพื่อหา representation ที่มีประสิทธิภาพกว่าได้ Browser ไม่ได้ทำ search ของ encoder ซ้ำ มันแค่ decode bitstream ที่เสร็จแล้ว

กฎจึงเรียบง่าย: ใช้ computation กับขั้นตอนที่ทำครั้งเดียว และประหยัด byte กับขั้นตอนที่เกิดซ้ำ

ทำไมผมยังอยู่กับ H.264 แทนที่จะไล่ตาม codec ใหม่ล่าสุด

ผมไม่ได้อ้างว่า H.264 เป็น codec ที่ compression-efficient ที่สุด มันไม่ใช่ Codec ใหม่กว่าสามารถน่าสนใจมากเมื่อ delivery system เก็บหลาย rendition และเลือกอันที่เหมาะกับแต่ละ client ได้

Constraint ของผมต่างออกไป: หนึ่ง URL, หนึ่ง file, หนึ่ง codec และลด playback drama ให้เหลือน้อยที่สุดสำหรับ audience ที่ mobile-heavy

สำหรับโจทย์นี้ H.264 ใน MP4 ยังเป็น baseline ที่ปลอดภัยมาก Apple ปัจจุบันแนะนำ web developer ให้ใช้ ไฟล์ MP4 ที่ encode ด้วย H.264 สำหรับ static video ใน Safari เอกสาร Android ปัจจุบันก็ระบุ H.264 ใน MP4 และกำหนด Main Profile decoder ตั้งแต่ Android 6.0 ขึ้นไป พร้อมทั้งยก 1280×720 ที่ 30 fps เป็น configuration H.264 HD ในคำแนะนำ playback ดูได้ที่ Android supported media formats

นั่นไม่ได้แปลว่าอุปกรณ์สมัยใหม่จำกัดอยู่ที่ Main Profile หรือ Level 3.1 ตัวอย่างเช่น guidance HLS ของ Apple โดยทั่วไปชอบ High Profile มากกว่า Main หรือ Baseline ผมเลือก Main@3.1 เพราะต้องการ decoder envelope ที่ตั้งใจให้ modest สำหรับ static MP4 เดียว ไม่ใช่เพราะ Apple บังคับ

ผมหยุด encode pixel ที่ไม่จำเป็นต้องมี

Resolution เป็นหนึ่งใน lever ที่ใหญ่ที่สุด เพดานของผมอยู่ที่ประมาณ 1280×720 สำหรับ landscape, 720×1280 สำหรับ portrait และประมาณ 960×960 สำหรับเนื้อหา square หรือ mixed-orientation

กฎที่สำคัญกว่า: อย่า upscale เพียงเพื่อให้ถึงเพดาน

ถ้า source คือ 900×600 การทำให้เป็น 1280×720 ไม่ได้คืน detail มันแค่สร้าง sample เพิ่มให้ encoder ต้องอธิบาย Source 1920×1080 อาจลดลงมาเป็นระดับ 720p ได้ ส่วน source 900×600 สามารถอยู่ใกล้ 900×600 ต่อไป Ceiling คือ maximum ไม่ใช่ target

ฟังดูง่าย แต่การตัด pixel ที่ไม่จำเป็นอาจมีผลมากกว่า encoder tweak แปลก ๆ หลายตัวรวมกัน

ผมหยุดจ่าย bandwidth ให้ frame ที่ source ไม่ได้มีจริง

Frame rate เป็น multiplier อีกตัว ถ้า animation มี visual state ที่มีประโยชน์จริงประมาณ 16 ภาพต่อวินาที การเก็บเป็น 30 หรือ 60 fps ไม่ได้สร้าง motion ที่ดีขึ้นอัตโนมัติ ส่วนใหญ่แค่เพิ่ม temporal sample ที่ซ้ำหรือสร้างขึ้นมา และยังต้อง encode อยู่ดี

Policy ของผมคือรักษา useful cadence ของ source และปกติอยู่ที่ 30 fps หรือต่ำกว่า สำหรับ material แบบนี้ 12, 15, 16, 18, 20, 24, 25 หรือ 30 fps ล้วนสมเหตุผลได้ หากมันอธิบาย source จริง ๆ

ผมยังชอบ CFR ที่สะอาดใน generated output ด้วย VFR ไม่ได้ผิดโดยธรรมชาติ แต่ CFR ทำให้ timestamps, frame counts, duration checks, seeking และ validation ภายหลังง่ายขึ้นใน pipeline ของผม

หลักการทั่วไปสำคัญกว่าตัวเลข FPS ใดตัวเลขหนึ่ง: อย่าจ่าย bandwidth ให้ temporal information ที่ไม่มีอยู่ใน source

CRF 28 เป็นการเลือกตาม workload ไม่ใช่เลขมหัศจรรย์

ผมไม่ต้องการบังคับทุก clip ไปที่ target bitrate เดียวกัน Illustration ที่แทบจะนิ่งกับ scene ที่เคลื่อนไหวซับซ้อนไม่ต้องการจำนวน bit เท่ากันเพื่อให้ดูยอมรับได้

ดังนั้นผมใช้โหมด CRF ของ x264 และสำหรับ illustrated workload ที่ bandwidth-first นี้ ผมลงเอยแถว -crf 28 FFmpeg อธิบาย CRF ใน libx264 ว่าเป็น constant-quality rate control ดู FFmpeg codec documentation

CRF 28 จงใจ aggressive ผมจะไม่ copy มันแบบไม่คิดไปใช้กับ film grain, camera footage ที่ noisy, ตัวอักษรเล็กมากบนหน้าจอ หรือ workload ที่ fidelity สำคัญกว่า bandwidth

ผมก็ไม่มี perceptual score แบบ universal ที่พิสูจน์ว่า CRF 28 transparent สิ่งที่ผมพูดได้จาก workload ของตัวเองแคบกว่านั้น: files เล็กลงมากและยังดูปกติสำหรับผมใน playback ทั่วไป นี่คือ practical observation ไม่ใช่การอ้างว่า CRF 28 visually lossless

veryslow แพงสำหรับ encoder ไม่ได้แปลว่าแพงสำหรับ decoder อัตโนมัติ

Preset ของผมคือ -preset veryslow Preset ที่ช้ากว่าให้ x264 มีโอกาส search prediction และ coding decision ที่มีประสิทธิภาพมากขึ้น ราคาที่จ่ายคือ CPU และเวลา encoding

ความแตกต่างสำคัญคือ encoder effort กับ decoder complexity ไม่ใช่สิ่งเดียวกัน

ผมปล่อยให้ x264 ทำงานหนักมากได้ ขณะเดียวกันก็ constrain final stream แยกต่างหาก Output contract แบบ conservative ของผมคือ:

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 และ B-frame แยกกัน ผมก็คิดถึงมันแบบเดียวกัน: ให้ encoder search หนัก แต่ให้ฝั่ง playback ธรรมดา

GOP และ VBV เป็น guardrail ไม่ใช่ quality control หลัก

สำหรับ short progressive clips เหล่านี้ ผมใช้ maximum GOP ประมาณห้าวินาที: ราว -g 150 ที่ 30 fps, -g 120 ที่ 24 fps หรือ -g 80 ที่ 16 fps

นี่คือ workload choice ไม่ใช่ universal rule Adaptive streaming มี constraint ต่างกัน เช่น Apple HLS authoring guidance แนะนำ IDR ทุกสองวินาที ผมไม่เอา HLS rule นี้ไป copy แบบตรง ๆ ลง short static progressive MP4

ผมยังใช้ประมาณ:

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

ค่าพวกนี้เป็น ceiling ป้องกัน bitrate spike ผิดปกติ ไม่ได้หมายความว่า “encode ทุกอย่างที่ 4 Mbps” CRF ยังควบคุม normal rate allocation ดังนั้น clip ที่ encode ง่ายก็ยังเล็กมากได้

ผมทำให้ MP4 container ธรรมดาโดยตั้งใจด้วย

ผมระบุ avc1 ชัดเจน เอกสาร HLS ปัจจุบันของ Apple ชอบ sample format อย่าง avc1 มากกว่า avc3 นี่ไม่ใช่เหตุผลที่ทำให้ file เล็ก แต่สอดคล้องกับเป้าหมายในการสร้าง H.264-in-MP4 ที่ conventional

ผมใช้ -movflags +faststart ด้วย FFmpeg format documentation ระบุว่า faststart ย้าย index moov ของ MP4 ไปไว้ต้น file ข้อกำหนด HTTP streaming ของ Android ก็ระบุว่าสำหรับ MPEG-4 moov ต้องอยู่หลัง ftyp และก่อน mdat

ftyp
moov
mdat

faststart ไม่ได้ปรับ compression มันทำให้ progressive HTTP playback ง่ายขึ้น

สำหรับ SDR output ปกติ ผมใช้ 8-bit yuv420p และ signal BT.709 แบบ limited/video range ถ้า clip ไม่มี audio ผมก็ไม่สร้าง audio track สำหรับ illustrated content นี้ผมใช้ -tune animation ด้วย แต่ถือว่าเป็น content-specific choice ไม่ใช่ส่วนหนึ่งของ universal compatibility contract

FFmpeg profile หลัก

สำหรับ illustrated source ที่ 30 fps ส่วนหลักของ 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

ผมจงใจไม่ hard-code scaling และ frame-rate stage ไว้ตรงนี้ Source 900×600 ไม่ควรถูกขยายเพียงเพราะ ceiling คือ 1280×720 และ animation ที่มี natural frame rate ต่ำก็ไม่ควรถูกบังคับเป็น 30 fps เพียงเพราะตัวอย่างใช้ -g 150

Command คือ implementation ของ policy ไม่ใช่ policy เอง

ทำไม files ถึงเล็กลงหลายเท่า

ไม่มี magic flag ตัวเดียว

Reduction มาจากการซ้อนหลาย decision ซึ่งแต่ละตัวเอา waste คนละชนิดออก: pixel ที่ไม่จำเป็น, frame ที่ไม่จำเป็น, fixed-bitrate thinking, encoder setting ที่เน้นความเร็วเกินไป, keyframe ที่ถี่เกินความจำเป็น และ stream ที่ผมไม่ต้องใช้

ดังนั้นคำว่า “file นี้เป็น H.264” บอกเรื่องขนาดได้น้อยกว่าที่คิด H.264 encode สองชุดจาก source เดียวกันอาจมีขนาดต่างกันมาก เพราะชื่อ codec ไม่ได้บอก resolution, frame rate, rate control, preset, GOP structure, profile หรือ source preparation

ในกรณีของผม การเปลี่ยน decision รอบ codec สำคัญกว่าการเปลี่ยน codec

ผลลัพธ์นี้ไม่ได้พิสูจน์อะไรบ้าง

ผมไม่ได้ isolate ทุก setting ใน controlled experiment ดังนั้นผมไม่สามารถบอกอย่างซื่อสัตย์ได้ว่า savings กี่เปอร์เซ็นต์มาจาก veryslow, CRF 28, การลด resolution หรือการลด frame rate ทีละอย่าง

ผมก็ไม่สามารถอ้างว่า output CRF 28 ทุกอัน perceptually transparent ได้ “ไม่เห็น quality drop ชัดเจน” คือ observation ของผมสำหรับ illustrated workload นี้ในขนาดรับชมปกติ ไม่ใช่ scientific guarantee สำหรับวิดีโอทุกชนิด

และผมไม่ได้บอกว่า single H.264 file เป็น architecture ที่ถูกต้องสำหรับทุก site Multiple renditions, adaptive streaming, HDR, 4K และ codec negotiation เปลี่ยน trade-off

ข้อสรุปที่แคบกว่าและมีประโยชน์กว่าคือ สำหรับ library ของ short illustrated และ animated clips ที่ bandwidth-first, mobile-heavy, encoding time ราคาถูก และ predictable playback สำคัญ profile นี้ทำให้ files ของผมเล็กลงหลายเท่าโดยยังดูปกติใน playback ทั่วไป

กฎที่ผมใช้ตอนนี้

เมื่อก่อนผมมอง video optimization เป็นปัญหา encoder settings เป็นหลัก ตอนนี้ผมมองมันเป็นปัญหา lifetime cost

Encoder อาจทำงานครั้งเดียว Byte อาจเดินทางผ่าน network หลายพันหรือหลายล้านครั้ง

นั่นเปลี่ยนความหมายของคำว่า “แพง”

ผมยอมใช้ CPU เพิ่มครั้งเดียว แต่ไม่ค่อยยอมส่ง pixel ที่เกิดจาก upscale, frame ที่ไม่เพิ่ม useful motion หรือ bitrate ที่ content ไม่ต้องการในทุก request ต่อจากนั้น

Codec ยังคงธรรมดา: H.264 ใน MP4 Optimization เกิดขึ้นรอบมัน

สำหรับ workload นี้ บทเรียนชัดกว่า FFmpeg flag ตัวไหนตัวหนึ่ง: optimize ค่าใช้จ่ายที่ต้องจ่ายซ้ำ ๆ ไม่ใช่ค่าใช้จ่ายที่จ่ายครั้งเดียว