浏览器端播放 H.265:WebAssembly、FFmpeg 与 WebCodecs 的组合方案
背景与架构
<video src="video.h265.mp4"> 在 Chrome 里经常播不起来,问题不在代码写法,而在 H.265(HEVC)本身的支持现状。Safari 和 iOS 的原生支持相对完整,Edge 在有硬件条件时也能走通;Chrome 这类主流浏览器默认就没把它当成'开箱即用'的格式。
实际做法通常不是赌某一个方案,而是做一层分流:能走硬解就直接走 WebCodecs,不能走就退到 WebAssembly 版 FFmpeg 做软解。前者省 CPU,后者更稳,代价是吃性能。
解码流程
flowchart TD
A[视频流 H.265/HEVC] --> B{浏览器支持 WebCodecs?}
B -- Yes --> C[WebCodecs API VideoDecoder]
C --> D[GPU 解码 VideoFrame]
B -- No --> E[Wasm + FFmpeg 软解]
E --> F[CPU 解码 YUV420]
D --> G[Canvas WebGL 渲染]
F --> G
把 FFmpeg 编成 WebAssembly
这一步不算优雅,但很实用。Emscripten 能把 C 写的 FFmpeg 编成 .wasm,浏览器里直接跑。要想不至于太慢,多线程和 SIMD 基本都得开,不然 1080p 以上就容易露怯。
编译参数大致如下:
emcc \
-Llibavcodec -Llibavutil -Llibswscale \
-I. \
-o ffmpeg-decoder.js \
src/decoder.c \
-s WASM=1 \
-s USE_PTHREADS=1 \
-s PTHREAD_POOL_SIZE=4 \
-s SIMD=1 \
-s ALLOW_MEMORY_GROWTH=1 \
-O3
src/decoder.c 需要你自己补上,作用就是把 avcodec_send_packet 和 avcodec_receive_frame 这类接口包一层,给 JavaScript 调用。直接把 FFmpeg API 摆到 JS 面前并不现实,胶水层还是少不了。
在 Web Worker 里跑解码循环
解码是典型的 CPU 密集任务,放主线程基本等于主动制造卡顿。更稳妥的方式是把 Wasm 放进 Web Worker 里,让主线程只负责控制和渲染协作。
初始化解码器
importScripts('ffmpeg-decoder.js');
let decoderModule;
let codecContext;
// 初始化 Wasm 模块
Module().then(module => {
decoderModule = module;
// 调用 C 导出的初始化函数
codecContext = decoderModule._init_h265_decoder();
postMessage({ type: 'ready' });
});
self.onmessage = function(e) {
{ type, data } = e.;
(type === ) {
ptr = decoderModule.(data.);
decoderModule..(data, ptr);
ret = decoderModule.(codecContext, ptr, data.);
(ret === ) {
yuvData = ();
({ : , : yuvData }, [yuvData.]);
}
decoderModule.(ptr);
}
};


