Xử lý hàng loạt video bằng một dòng lệnh, nhanh hơn nhờ card đồ hoạ

Nén, cắt, ghép, làm phụ đề và xuất bản dọc cho cả thư mục video bằng FFmpeg, nhanh hơn nhờ card NVIDIA. Kèm số đo thật, giới hạn AV1 và script chạy hàng loạt an toàn.

Phát LâmĐăng Cập nhật 8 phút đọcTrung cấp
Xử lý hàng loạt video bằng một dòng lệnh, nhanh hơn nhờ card đồ hoạ
Mục lục
  1. FFmpeg và NVENC là gì
  2. Nén video bằng GPU
  3. Cắt, ghép, ảnh nhỏ, phụ đề
  4. Script chạy hàng loạt mà không sợ tai nạn
  5. Lỗi hay gặp
  6. Làm gì tiếp theo

Cuối tháng, bạn có ba chục video giới thiệu sản phẩm cần đổi cỡ, nén nhẹ lại để đăng web, và cắt thêm một bản dọc cho mạng xã hội. Mở phần mềm dựng, kéo từng cái vào, bấm xuất, ngồi chờ. Làm lần đầu thì chịu được, lần thứ năm thì chán.

Có một công cụ miễn phí làm được tất cả những việc đó từ dòng lệnh, tên là FFmpeg. Gõ một lệnh, đi pha cà phê, quay lại là xong cả thư mục. Nếu máy có card đồ hoạ NVIDIA thì còn nhanh hơn nữa. Bài này là bộ công thức mình dùng, gồm cả những giới hạn mình đã đụng phải. Mọi lệnh mình chạy thử trên video mẫu tự tạo, bằng FFmpeg 6.1.1 và card RTX 3060 Ti, không đụng video thật nào.

FFmpeg và NVENC là gì

FFmpeg là chương trình dòng lệnh đọc, biến đổi và ghi video, âm thanh. NVENC là một khối phần cứng chuyên nén video nằm sẵn trong card NVIDIA. Khi bạn chọn bộ nén h264_nvenc hay hevc_nvenc, việc nén chạy trên khối đó thay vì trên CPU, nên CPU rảnh để làm việc khác.

Có thể hình dung CPU là một đầu bếp đa năng, còn NVENC là cái máy làm một món duy nhất, rất nhanh. Máy thì nhanh, nhưng món ra thường to hơn một chút, chuyện này mình nói ở phần số liệu.

Để thử mà không dùng dữ liệu của ai, tạo một video mẫu 30 giây có tiếng:

ffmpeg -f lavfi -i "testsrc2=size=1920x1080:rate=30" \
       -f lavfi -i "sine=frequency=440:sample_rate=48000" \
       -t 30 -c:v libx264 -preset veryfast -crf 20 -pix_fmt yuv420p -c:a aac -b:a 128k sample.mp4

Nén video bằng GPU

# H.264, quality-targeted (constant quality), audio copied
ffmpeg -nostdin -i sample.mp4 -c:v h264_nvenc -preset p5 -tune hq -rc vbr -cq 23 -b:v 0 -c:a copy out-h264.mp4

# H.265/HEVC: smaller files; -tag:v hvc1 makes Apple players accept it
ffmpeg -nostdin -i sample.mp4 -c:v hevc_nvenc -preset p5 -tune hq -rc vbr -cq 28 -b:v 0 -tag:v hvc1 -c:a copy out-hevc.mp4

Hai chỗ cần hiểu. -cq là mức chất lượng mong muốn, số càng nhỏ càng đẹp và càng nặng; đi kèm -b:v 0 để bộ nén tự chọn dung lượng. -preset chạy từ p1 tới p7, số lớn thì chậm hơn nhưng nén gọn hơn. Mình hay dùng p5.

Mình đo trên video mẫu 30 giây (900 khung hình), máy có 24 luồng CPU:

Bộ nén Thời gian Dung lượng
h264_nvenc p5, cq 23 3,6 s 38,7 MB
hevc_nvenc p5, cq 28 3,4 s 23,1 MB
libx264 medium, crf 23 (CPU) 8,7 s 21,3 MB
libx265 medium, crf 28 (CPU) 28,0 s 15,1 MB

Đọc bảng này cho kỹ. Video mẫu là hình tổng hợp nên số tuyệt đối không đại diện cho video thật. Mức cq của NVENC và crf của x264 cũng không cùng thước đo, nên dung lượng chỉ để tham khảo. Điều rút ra được là GPU nhanh hơn rõ (khoảng 2,4 lần so với x264 và 8 lần so với x265 ở đây), nhưng CPU thường nén gọn hơn ở cùng chất lượng cảm nhận. Cần tốc độ và số lượng thì dùng GPU. Cần file nhỏ nhất thì dùng CPU.

Một cuộn phim đi vào khối chip GPU phát sáng ở giữa, ba ô đầu ra xuất hiện bên phải gồm video ngang, video dọc và ảnh vuông
Một nguồn video, một khối NVENC, nhiều định dạng đầu ra.

Một giới hạn mình gặp: thử av1_nvenc trên RTX 3060 Ti báo lỗi thật No capable devices found và không tạo ra file nào (chạy lại hôm 21/10/2026 vẫn thế). Theo bảng hỗ trợ của NVIDIA, chỉ từ thế hệ RTX 40 trở đi mới nén được AV1; RTX 30 chỉ xem được. Cần AV1 trên máy này thì dùng libsvtav1 trên CPU, chấp nhận chậm hơn.

Cắt, ghép, ảnh nhỏ, phụ đề

Cắt có hai kiểu. Kiểu chính xác nén lại video nên cắt đúng từng khung. Kiểu nhanh chỉ cắt thẳng mà không nén lại, nhưng phải bắt đầu ở "khung chính" gần nhất nên có thể lệch vài khung.

# Accurate cut: -ss BEFORE -i is fast, and re-encoding makes it frame-accurate
ffmpeg -nostdin -ss 5.5 -i in.mp4 -t 4 -c:v h264_nvenc -preset p5 -c:a aac cut-accurate.mp4

# Instant cut with no re-encoding: only cuts on keyframes
ffmpeg -nostdin -ss 5.5 -i in.mp4 -t 4 -c copy cut-copy.mp4

Hôm 21/10/2026 mình đo lại: cắt chính xác ra đúng 4,000 s, cắt kiểu nhanh ra 4,067 s. Ghép các đoạn cùng định dạng thì dùng concat, không cần nén lại:

printf "file '%s'\n" "$PWD/a.mp4" "$PWD/b.mp4" > list.txt
ffmpeg -nostdin -f concat -safe 0 -i list.txt -c copy joined.mp4

Nếu các đoạn khác định dạng hay khác độ phân giải, phải nén lại bằng bộ lọc concat chứ không dùng cách trên. Ảnh nhỏ (thumbnail) và ảnh ghép nhiều khung thì như sau:

# one frame at 3 s, 640 px wide (-2 keeps the height even)
ffmpeg -nostdin -ss 3 -i in.mp4 -frames:v 1 -vf scale=640:-2 -q:v 3 thumb.jpg

# contact sheet: one frame every 3 s, tiled 2x2
ffmpeg -nostdin -i in.mp4 -vf "fps=1/3,scale=320:-2,tile=2x2" -frames:v 1 sheet.jpg

Phụ đề có hai kiểu: in cứng vào hình (luôn hiện) hoặc đính kèm để người xem bật tắt.

# burn-in (always visible); uses libass on CPU, then encode on GPU
ffmpeg -nostdin -i in.mp4 -vf "subtitles=sub.srt:force_style='FontSize=22,Outline=2'" \
       -c:v h264_nvenc -preset p5 -cq 23 -b:v 0 -c:a copy burned.mp4

# soft subtitles (viewer can toggle), no re-encode
ffmpeg -nostdin -i in.mp4 -i sub.srt -c copy -c:s mov_text -metadata:s:s:0 language=eng soft.mp4

Bản dọc 9:16 cho mạng xã hội, cắt ở giữa khung (nếu chủ thể lệch tâm, thêm toạ độ x vào crop):

ffmpeg -nostdin -i in.mp4 -vf "crop=ih*9/16:ih,scale=1080:1920" \
       -c:v h264_nvenc -preset p5 -cq 23 -b:v 0 -c:a copy vertical.mp4

Kiểm bằng ffprobe hôm 21/10/2026: bản dọc ra 1080x1920. Với video lớn, có thể giữ cả đường đi trong bộ nhớ GPU bằng -hwaccel cuda -hwaccel_output_format cuda và bộ lọc scale_cuda; mình chạy thử ra HEVC 1280x720. Lưu ý với cách này các bộ lọc chạy trên CPU như subtitles không dùng được.

Script chạy hàng loạt mà không sợ tai nạn

Chạy cả thư mục là lúc dễ xảy ra chuyện nhất: ghi đè file gốc, để lại file nửa vời khi bị ngắt giữa chừng, hoặc một file hỏng làm dừng cả mẻ. Script của mình tránh cả ba.

Băng chuyền các cuộn phim chạy sang phải, ở giữa một trạm ghi ra hộp tạm mờ rồi mũi tên đổi tên đưa hộp hoàn chỉnh lên kệ xanh, một vòm chặn tấm hỏng ở đầu vào
Ghi ra file tạm, kiểm tra, rồi đổi tên: không bao giờ có file nửa vời mang tên thật.

Ý tưởng là kết quả ghi ra một file tạm trước, kiểm xong mới đổi tên thành tên thật, nên không bao giờ có file dở dang mang tên thật. File gốc chỉ đọc, kết quả ra thư mục khác. File đã làm rồi thì bỏ qua, nên chạy lại sau lỗi không phải làm từ đầu. Và trước khi nhận kết quả, script so thời lượng đầu ra với đầu vào.

#!/usr/bin/env bash
set -euo pipefail
IN_DIR=${1:?input dir}; OUT_DIR=${2:?output dir}
LOCK=${GPU_LOCK:-/tmp/gpu.lock}   # one GPU job at a time (shared GPU)
mkdir -p "$OUT_DIR"
failed=0

duration() { ffprobe -v error -show_entries format=duration -of csv=p=0 "$1"; }

while IFS= read -r -d '' src; do
  name=$(basename "${src%.*}"); dst="$OUT_DIR/$name.mp4"; tmp="$OUT_DIR/.$name.part"
  [[ -e "$dst" ]] && { echo "skip   $name"; continue; }          # idempotent

  flock "$LOCK" ffmpeg -nostdin -hide_banner -loglevel error -y -i "$src" \
      -map 0:v:0 -map '0:a?' -c:v h264_nvenc -preset p5 -tune hq -rc vbr -cq 23 -b:v 0 \
      -c:a aac -b:a 160k -movflags +faststart -f mp4 "$tmp" \
    || { rm -f "$tmp"; echo "FAIL   $name"; failed=$((failed+1)); continue; }

  din=$(duration "$src"); dout=$(duration "$tmp" || echo 0)      # verify before publishing
  if awk -v a="$din" -v b="$dout" 'BEGIN{d=a-b; if(d<0)d=-d; exit !(d<=0.5)}'; then
    mv -f "$tmp" "$dst"; echo "ok     $name"
  else
    rm -f "$tmp"; echo "BADDUR $name"; failed=$((failed+1))
  fi
done < <(find "$IN_DIR" -maxdepth 1 -type f \( -iname '*.mp4' -o -iname '*.mov' -o -iname '*.mkv' \) -print0 | sort -z)

[[ $failed -eq 0 ]]                  # non-zero exit status if anything failed

Vài chi tiết đáng giải thích. -nostdin để FFmpeg không nuốt phần đầu vào còn lại của vòng lặp. -print0 và read -d '' để tên file có dấu cách không bị vỡ. flock để các việc dùng chung GPU chạy lần lượt thay vì tranh nhau; card phổ thông còn giới hạn số phiên nén cùng lúc.

Mình chạy lại hôm 21/10/2026. Thư mục có clip2.mp4 và sample one.mp4 (tên có dấu cách) ra ok cả hai, thời lượng 12,00 s thành 12,01 s. Chạy lần hai in skip cả hai. Thư mục có một bad.mp4 thực chất chỉ là chữ cho ra FAIL bad, nhưng good.mp4 bên cạnh vẫn được xử lý, mã thoát là 1 và không còn file .part nào sót lại.

Lỗi hay gặp

Lỗi dễ dính nhất là quên -nostdin trong vòng lặp: FFmpeg sẽ nuốt phần danh sách file còn lại và chỉ xử lý được vài video đầu. Một lỗi hay thấy khác là đặt -ss sau -i: lệnh chậm hẳn vì phải giải mã từ đầu, đặt trước -i thì nhảy thẳng tới chỗ cần. Ghép bằng concat các file khác định dạng sẽ ra hình hoặc tiếng hỏng. Xuất cho web hoặc điện thoại thì đừng quên -pix_fmt yuv420p và -movflags +faststart. Cuối cùng là đừng giả định card nào cũng nén được AV1.

Làm gì tiếp theo

Thử với một thư mục video mẫu trước, đừng chạy ngay lên dữ liệu thật. Tạo vài video bằng lệnh ở đầu bài, chạy script, kiểm file đầu ra bằng mắt và bằng ffprobe. Khi bạn tin nó, trỏ vào thư mục thật. Nếu bạn dựng cả một quy trình tự động nhiều bước như thế này, các nguyên tắc khoá tài nguyên, kiểm độc lập và đường lùi nằm ở Giao việc lớn cho nhiều AI cùng làm: cách chia việc để không hỏng.

FFmpegTruyền thôngffmpegnvencgpuvideoautomationbash

Nguồn tham khảo

  1. FFmpeg documentation
  2. FFmpeg filters documentation (subtitles, tile, scale_cuda)
  3. FFmpeg Wiki: Hardware acceleration introduction
  4. FFmpeg Wiki: Concatenate
  5. NVIDIA Video Encode and Decode GPU Support Matrix
  6. Linux man page: flock(1)