FFmpeg.wasm: Phân tích kỹ thuật WebAssembly chạy FFmpeg bên trong trình duyệt

FFmpeg.wasm Browser

FFmpeg.wasm là phiên bản của công cụ xử lý âm thanh và video mã nguồn mở nổi tiếng FFmpeg được biên dịch sang WebAssembly thông qua Emscripten. Nó cho phép trình duyệt thực hiện các thao tác phức tạp như giải mã, chuyển đổi định dạng, hợp nhất video trực tiếp phía máy khách mà không phụ thuộc vào máy chủ phụ trợ (back-end).

Liên kết dự án: github.com/ffmpegwasm/ffmpeg.wasm

Kiến trúc truyền thống vs Kiến trúc FFmpeg.wasm

Kiến trúc truyền thống (Chuyển đổi định dạng phía máy chủ):
Người dùng → Tải video lên → Máy chủ FFmpeg → Đầu ra → Người dùng tải về
Vấn đề: Tiêu tốn tài nguyên CPU máy chủ lớn, chi phí băng thông cao, lo ngại về quyền riêng tư.

Kiến trúc FFmpeg.wasm:
Người dùng → FFmpeg.wasm trong trình duyệt → Đầu ra Blob → Người dùng tải về
Ưu điểm: Không tốn tài nguyên máy chủ, đảm bảo quyền riêng tư, có thể sử dụng ngoại tuyến.

Lõi công nghệ: WebAssembly là gì?

WebAssembly (Wasm) là một định dạng nhị phân cấp thấp, mục tiêu thiết kế là cho phép mã nguồn của các ngôn ngữ biên dịch (C, C++, Rust, v.v.) chạy trong trình duyệt với tốc độ gần như nguyên bản.

Mã nguồn của FFmpeg được viết bằng ngôn ngữ C, chuỗi công cụ Emscripten có thể biên dịch mã C thành các mô-đun nhị phân .wasm, kết hợp với mã "keo" JavaScript (ffmpeg-core.js) để chạy trong môi trường trình duyệt.

Hai chế độ chạy của FFmpeg.wasm

Chế độ đơn luồng (Single-threaded)

import { FFmpeg } from '@ffmpeg/ffmpeg';
import { toBlobURL } from '@ffmpeg/util';

const ffmpeg = new FFmpeg();
await ffmpeg.load({
  coreURL: await toBlobURL(
    'https://unpkg.com/@ffmpeg/[email protected]/dist/esm/ffmpeg-core.js',
    'text/javascript'
  ),
  wasmURL: await toBlobURL(
    'https://unpkg.com/@ffmpeg/[email protected]/dist/esm/ffmpeg-core.wasm',
    'application/wasm'
  ),
});
  • Không cần SharedArrayBuffer
  • Không cần các HTTP Header đặc biệt (COOP/COEP)
  • Tất cả các thao tác diễn ra trong một luồng JS duy nhất (Worker)
  • Phù hợp cho hầu hết việc triển khai trang web tĩnh (Cloudflare Pages, GitHub Pages, v.v.)

Chế độ đa luồng (Multi-threaded)

await ffmpeg.load({
  coreURL: 'ffmpeg-core-mt.js',  // Phiên bản core đa luồng
  wasmURL: 'ffmpeg-core-mt.wasm',
  workerURL: 'ffmpeg-core-mt.worker.js',
});
  • Sử dụng SharedArrayBuffer để chia sẻ bộ nhớ giữa nhiều Web Worker.
  • Yêu cầu máy chủ thiết lập các HTTP Header sau:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
  • Tốc độ tăng đáng kể (có thể tận dụng CPU đa nhân).
  • Tuy nhiên việc thiết lập phức tạp hơn và một số CDN không hỗ trợ.

Hệ thống tệp ảo (Emscripten FS)

FFmpeg.wasm không có khả năng truy cập trực tiếp vào ổ đĩa cục bộ. Nó sử dụng Hệ thống tệp ảo (VFS) của Emscripten để mô phỏng các thao tác trên đĩa:

// Ghi ArrayBuffer vào VFS ảo
await ffmpeg.writeFile('input.ts', new Uint8Array(buffer));

// Chạy lệnh FFmpeg (thao tác trong VFS ảo)
await ffmpeg.exec(['-i', 'input.ts', '-c', 'copy', 'output.mp4']);

// Đọc đầu ra
const data = await ffmpeg.readFile('output.mp4');

// Dọn dẹp giải phóng bộ nhớ (Quan trọng!)
await ffmpeg.deleteFile('input.ts');
await ffmpeg.deleteFile('output.mp4');

Quan trọng: Dữ liệu trong VFS ảo tồn tại trong vùng nhớ Heap của JavaScript, phải được xóa thủ công sau khi sử dụng xong, nếu không nó sẽ chiếm bộ nhớ trình duyệt cho đến khi trang web bị đóng.

Giới hạn bộ nhớ và Thực tiễn tốt nhất

Đây là giới hạn bẩm sinh lớn nhất của FFmpeg.wasm:

Hạng mục Giới hạn
Giới hạn JS Heap Khoảng 1-2 GB (tùy thuộc trình duyệt và HĐH)
Mô hình bộ nhớ Wasm Định chỉ 32-bit, giới hạn mô-đun đơn là 4 GB
Khả dụng thực tế Thường từ 500 MB - 1.5 GB

Kịch bản sử dụng được khuyến nghị

  • ✅ Video dưới 720p, độ dài dưới 30 phút.
  • ✅ Chủ yếu là concat (hợp nhất các đoạn), không phải mã hóa lại.
  • ✅ Nghiên cứu kỹ thuật và thử nghiệm các liên kết công khai.
  • ❌ Video dài 4K/1080p (không đủ bộ nhớ).
  • ❌ Các bộ lọc phức tạp và mã hóa lại (tốc độ quá chậm).

Thực tiễn tốt nhất trong quản lý bộ nhớ

// 1. Tải xuống theo từng đợt, tránh giữ tất cả các ArrayBuffer cùng lúc
const BATCH = 5;
for (let i = 0; i < segments.length; i += BATCH) {
  const batch = await downloadBatch(segments.slice(i, i + BATCH));
  // Ghi ngay vào VFS, có thể thu hồi ArrayBuffer phía JS
  for (const [idx, buf] of batch.entries()) {
    await ffmpeg.writeFile(`seg${i + idx}.ts`, new Uint8Array(buf));
  }
}

// 2. Dọn dẹp VFS ngay sau khi chạy xong
await ffmpeg.deleteFile('output.mp4');

So sánh với API phụ trợ (Back-end)

Khía cạnh FFmpeg.wasm API phụ trợ
Chi phí máy chủ Không có Cao (Tận dụng CPU mạnh)
Quyền riêng tư 100% (Xử lý cục bộ) Cần tin tưởng máy chủ
Tốc độ xử lý Chậm (Wasm đơn nhân) Nhanh (Đa nhân + GPU)
Hỗ trợ tệp lớn Bị giới hạn bởi bộ nhớ trình duyệt Hầu như không giới hạn
Độ phức tạp triển khai Cực kỳ đơn giản (tài nguyên tĩnh) Cần bảo trì máy chủ
Kịch bản tối ưu Chuyển đổi nhỏ, công cụ nghiên cứu Xử lý hàng loạt quy mô sản xuất

Ứng dụng thực tế: Cách thức triển khai của trang web này

Công cụ nghiên cứu HLS của trang web này sử dụng sự kết hợp sau:

  • @ffmpeg/ffmpeg 0.12.x (Đơn luồng, không cần COOP/COEP).
  • @ffmpeg/core 0.12.x (Mô-đun Wasm Core tương ứng).
  • Tải động thông qua unpkg CDN, lần tải đầu tiên mất khoảng 15-30 giây (mô-đun Wasm khoảng 30MB).
  • Thực thi lệnh ffmpeg -f concat -safe 0 -i concat.txt -c copy output.mp4 để hoàn thành việc hợp nhất TS.

Xem thêm

Sẵn sàng kiểm tra luồng M3U8 của bạn?

🚀 Thử trình phát M3U8 trực tuyến