ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

纯前端RTSP播放方案:WebSocket/Wasm/WebCodecs实战对比

纯前端RTSP播放方案:WebSocket/Wasm/WebCodecs实战对比 1. 项目概述为什么“不用FFmpeg”这件事值得认真对待RTSP直播在安防、工业监控、远程教育这些场景里早就不是新鲜事了。但凡做过前端音视频项目的人都知道想在浏览器里直接播RTSP流传统方案几乎等于“默认失败”。主流做法是后端用FFmpeg拉流、转封装成HLS或WebRTC再喂给前端——这中间多了一层服务、一次编解码、一个潜在的单点故障延迟动辄3~8秒还吃服务器CPU。我去年帮一家智能工厂做产线巡检系统时客户指着大屏上卡顿的机械臂画面说“你们这个‘实时’比我们车间里的对讲机还慢。”一句话让我回去啃了整整两周的浏览器音视频API文档。标题里那个“不用FFmpeg”的断言不是为了标新立异而是直指三个现实痛点第一是部署成本——嵌入式设备比如RK3588工控板根本跑不动FFmpeg第二是端到端延迟——从摄像头捕获到浏览器渲染必须压进500ms以内第三是架构轻量化——客户明确要求“前端自闭环”后端只管信令和权限不碰媒体流。这时候WebSocket/Wasm/WebCodecs这三套方案就不再是技术选型题而是生存题。你可能在热搜里刷到过“安卓缓存rtsp流”“海康威视摄像头rtsp地址”这类词它们背后其实是同一类人现场实施工程师、边缘计算开发者、IoT设备集成商。他们手里的设备五花八门——海康、大华的IPCESP32OV2640的DIY摄像头甚至树莓派接USB摄像头再用GStreamer推RTSP。这些人最怕两件事一是改后端代码要走审批流程二是现场没网只能靠本地局域网。所以当标题强调“纯前端”它真正承诺的是把解码器塞进浏览器沙箱让Chrome/Firefox/Safari变成一台分布式解码终端。这不是炫技是把音视频能力从服务端下沉到每个终端的必然选择。这三种方案里WebSocket是最容易被误解的——很多人以为它只是“传数据的管道”其实它能承载原始H.264 Annex B裸流Wasm常被当成“JS加速器”但它真正价值在于把C/C写的解码器比如libx264、dav1d安全地移植进浏览器而WebCodecs则是浏览器原生提供的底层音视频处理接口绕过了MediaSource API的黑盒封装。接下来我会拆开每一块告诉你它们在真实项目里怎么活下来、怎么扛住压力、又在哪种场景下会突然掉链子。2. 方案原理与适用边界别让技术选型变成埋雷现场2.1 WebSocket方案用“裸流管道”对抗协议鸿沟WebSocket方案的本质是把浏览器当成一个轻量级RTSP客户端。它不解析RTSP协议而是依赖后端比如Node.js rtsp-relay先完成RTSP握手、DESCRIBE、SETUP、PLAY这一整套流程然后把解复用后的H.264 NALU单元关键帧SPS/PPS I/P/B帧通过WebSocket二进制帧推送过来。前端收到后直接喂给HTMLVideoElement的MediaSource。这里的关键转折点在于RTSP协议本身不能被浏览器原生支持但它的有效载荷H.264流可以被浏览器消化。我见过太多团队卡在第一步——试图用JavaScript手写RTSP协议栈结果发现TCP粘包、RTP时间戳同步、RTCP反馈这些细节能把人逼疯。正确的做法是承认浏览器的能力边界让它专注解码和渲染把协议层交给更成熟的C/C库如live555、gstreamer。实际部署时我推荐用rtsp-simple-serverGo语言作为中继。它比FFmpeg轻量得多内存占用稳定在15MB左右启动速度不到300ms。配置文件里只需定义一个path映射paths: camera-001: source: rtsp://admin:password192.168.1.100:554/stream1 sourceOnDemand: true # 关键启用WebRTC和WebSocket双出口 sinks: - websocket这样前端就能用ws://your-server:8080/camera-001/ws直连。注意路径里的/ws后缀——这是rtsp-simple-server约定的WebSocket端点不是随便加的。提示很多初学者会忽略RTSP流的编码参数。海康/大华默认用H.264 High Profile但部分老旧Android设备尤其Chrome 109之前版本只支持Baseline Profile。实测发现当SPS中profile_idc100时某些低端平板会触发DOMException: Failed to execute addSourceBuffer on MediaSource。解决方案是在中继层用ffmpeg -vcodec copy -profile:v baseline做一次无损转码虽然增加10ms延迟但换来99%设备兼容性。2.2 WebAssembly方案把C语言解码器装进浏览器沙箱Wasm方案解决的是“浏览器没有原生H.264解码器”的硬伤。虽然Chrome支持H.264硬件解码但Firefox在Linux上默认禁用Safari对H.265支持有限。Wasm的价值在于用标准化的二进制格式把经过充分验证的C解码器如openh264、dav1d安全地运行在浏览器里。我参与过一个铁路轨旁监测项目客户要求所有终端包括国产麒麟OS360浏览器必须支持H.265 4K流。测试发现360浏览器的WebCodecs API尚未实现MediaSource对H.265支持也不稳定。最终方案是用Emscripten把dav1d编译成Wasm模块# 编译命令简化版 emcmake cmake -B build -DCMAKE_BUILD_TYPERelease \ -DEMSCRIPTENON \ -DENABLE_TESTSOFF \ -DENABLE_TOOLSOFF emmake make -C build -j4生成的dav1d.wasm只有1.2MB配合JavaScript胶水代码能实现每秒30帧的4K解码。关键技巧在于内存管理Wasm线性内存必须预先分配足够空间建议64MB否则解码过程中频繁grow_memory会导致卡顿。我在dav1d.js里加了这段保护const wasmMemory new WebAssembly.Memory({ initial: 1024, maximum: 4096 }); // 解码前检查剩余内存 if (wasmMemory.buffer.byteLength neededBytes) { throw new Error(Wasm memory overflow: need ${neededBytes}, have ${wasmMemory.buffer.byteLength}); }注意Wasm模块加载有冷启动问题。首次解码前需预热——我让页面初始化时调用一次空解码函数强制浏览器JIT编译。实测Chrome下预热耗时120ms但后续帧解码稳定在8ms/帧1080p30fps。这个“预热”步骤必须写进产品文档否则测试同事会误判为性能缺陷。2.3 WebCodecs方案浏览器原生API的终极解法WebCodecs是W3C标准也是目前唯一能绕过MediaSource黑盒的方案。它提供VideoDecoder、AudioDecoder、ImageEncoder等接口允许开发者完全控制解码管线。比如处理RTSP流时你可以用WebSocket接收NALU用VideoDecoder.configure()指定H.264解码器将NALU封装成EncodedVideoChunk对象调用decoder.decode(chunk)获取VideoFrame把VideoFrame绘制到OffscreenCanvas或video元素这套流程的优势在于零拷贝VideoFrame可以直接传递给WebGL纹理或者用transferToImageBitmap()转成图像。我在做AR巡检应用时需要把RTSP画面叠加设备三维模型用WebCodecs比MediaSource快47%因为省去了MediaSource内部的缓冲区复制。但它的坑也最深。首先是浏览器支持度Chrome 94、Edge 94、Opera 80支持完整API但Firefox仅支持解码无编码Safari至今未实现。其次是错误处理机制——VideoDecoder的onError回调不会告诉你具体哪一帧出错只会抛EncodingError。我的经验是在decode()前加一层NALU校验function validateNALU(nalu) { // 检查起始码 0x00000001 或 0x000001 if (!(nalu[0] 0 nalu[1] 0 nalu[2] 0 nalu[3] 1) !(nalu[0] 0 nalu[1] 0 nalu[2] 1)) { throw new Error(Invalid NALU start code); } // 检查NALU类型1I帧5IDR帧7SPS8PPS const nalType nalu[4] 0x1F; if (![1,5,7,8].includes(nalType)) { console.warn(Unexpected NALU type: ${nalType}); } }3. 实操全流程从环境搭建到生产部署的每一步3.1 WebSocket方案落地5分钟搭起低延迟直播通道WebSocket方案的实操核心在于流格式转换的精准控制。很多团队失败是因为后端中继输出的流格式与前端期望不匹配。下面以rtsp-simple-server为例给出可直接复制的配置和前端代码。首先确保RTSP流源可用。海康威视摄像头的典型地址是rtsp://admin:123456192.168.1.64:554/Streaming/Channels/101大华的是rtsp://admin:123456192.168.1.65:554/cam/realmonitor?channel1subtype0。注意两点密码不能含特殊字符如、/需URL编码端口必须开放554或8554。rtsp-simple-server.yml配置要点# 必须关闭RTMP避免端口冲突 rtmp: enable: false # WebSocket相关配置 webrtc: enable: false # 如果只用WebSocket关掉WebRTC节省资源 paths: # 定义一个路径对应一个摄像头 factory-cam: # 拉流地址支持用户名密码 source: rtsp://admin:123456192.168.1.64:554/Streaming/Channels/101 # 按需拉流没人看时自动断开 sourceOnDemand: true # 关键启用WebSocket输出 sinks: - websocket # 可选添加日志便于调试 logLevel: debug启动服务后前端HTML结构极简!DOCTYPE html html head titleWebSocket RTSP播放器/title style #video { width: 100%; max-width: 1280px; height: auto; } /style /head body video idvideo controls autoplay/video script srcplayer.js/script /body /htmlplayer.js的核心逻辑class WebSocketPlayer { constructor(videoElement, wsUrl) { this.video videoElement; this.wsUrl wsUrl; this.mediaSource null; this.sourceBuffer null; this.ws null; } async init() { // 1. 创建MediaSource this.mediaSource new MediaSource(); this.video.src URL.createObjectURL(this.mediaSource); // 2. 监听sourceopen事件 this.mediaSource.addEventListener(sourceopen, () { this.#initSourceBuffer(); }); // 3. 建立WebSocket连接 this.ws new WebSocket(this.wsUrl); this.ws.binaryType arraybuffer; this.ws.onopen () console.log(WebSocket connected); this.ws.onmessage (event) this.#handleMessage(event.data); this.ws.onerror (err) console.error(WebSocket error:, err); } #initSourceBuffer() { // H.264流必须指定 mimeType const mimeCodec video/mp4; codecsavc1.640028; this.sourceBuffer this.mediaSource.addSourceBuffer(mimeCodec); // 关键设置timestampOffset解决首帧PTS为0的问题 this.sourceBuffer.timestampOffset 0; } #handleMessage(data) { if (!this.sourceBuffer || this.sourceBuffer.updating) return; try { // 将ArrayBuffer转为Uint8Array const uint8Array new Uint8Array(data); // 构造MP4片段这里用最简化的fMP4格式 // 实际项目中建议用mux.js库生成标准fMP4 const mp4Fragment this.#createMP4Fragment(uint8Array); this.sourceBuffer.appendBuffer(mp4Fragment); } catch (e) { console.error(Append buffer failed:, e); } } #createMP4Fragment(nalu) { // 简化版只处理I帧SPS/PPS IDR // 真实项目需解析NALU类型分离SPS/PPS并插入moov const isKeyFrame nalu[4] 0x1F 5; // IDR帧 if (isKeyFrame) { // 插入SPS/PPS从RTSP DESCRIBE响应中提取 const sps new Uint8Array([0,0,0,1, 67, 64, 0, 28, 0, 96, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0]); const pps new Uint8Array([0,0,0,1, 68, 128, 0, 6]); return this.#nalusToMp4([sps, pps, nalu]); } else { return this.#nalusToMp4([nalu]); } } #nalusToMp4(nalus) { // 此处省略fMP4封装细节实际用mux.js的Transmuxer // 关键参数trackId1, timescale90000RTSP常用 } } // 使用示例 const player new WebSocketPlayer( document.getElementById(video), ws://localhost:8080/factory-cam/ws ); player.init();实操心得第一次调试时务必用Wireshark抓包确认WebSocket收到的数据确实是H.264 NALU起始码0x00000001。我曾遇到过因rtsp-simple-server版本bug导致发送H.265流的情况前端报错Failed to execute addSourceBuffer却找不到原因抓包后才定位到问题。3.2 Wasm方案实战从编译到解码的全链路控制Wasm方案的实操难点不在解码而在跨平台构建和内存协同。下面以openh264为例展示如何在Windows/macOS/Linux上统一构建并在前端安全使用。第一步环境准备Windows安装Emscripten SDKemsdk运行emsdk install latest和emsdk activate latestmacOS/Linux用brew install emscripten或apt-get install emscripten验证emcc --version应输出Emscripten 3.x第二步编译openh264# 克隆官方仓库 git clone https://github.com/cisco/openh264.git cd openh264 # 切换到稳定分支 git checkout v2.3.1 # 配置编译选项关键 emcmake cmake -B build-wasm -DCMAKE_BUILD_TYPERelease \ -DBUILD_SHARED_LIBSOFF \ -DENABLE_TESTOFF \ -DENABLE_LOGGINGOFF \ -DWASMON # 编译-j4表示4线程 emmake make -C build-wasm -j4 # 输出文件build-wasm/libopenh264.a 和 build-wasm/codec/console/dec/src/welsdec.js生成的welsdec.js是胶水代码welsdec.wasm是核心模块。但直接使用会有问题Wasm模块默认不导出解码函数。需要修改CMakeLists.txt在target_link_libraries后添加# 添加导出函数声明 set_target_properties(openh264 PROPERTIES EXPORT_SYMBOLS WelsDecGetDefaultParam;WelsDecCreate;WelsDecDestroy;WelsDecDecodeFrameNoDelay )第三步前端集成class WasmH264Decoder { constructor() { this.module null; this.decoder null; this.width 0; this.height 0; } async loadWasm() { // 加载Wasm模块注意必须用fetch不能用import const wasmBytes await fetch(welsdec.wasm).then(r r.arrayBuffer()); this.module await WebAssembly.instantiate(wasmBytes, { env: { // 实现必要的C运行时函数 abort: () { throw new Error(Wasm abort); }, memory: new WebAssembly.Memory({ initial: 256, maximum: 2048 }) } }); } initDecoder() { // 调用Wasm导出的创建函数 const createFunc this.module.instance.exports.WelsDecCreate; this.decoder createFunc(); // 设置解码参数 const param new this.module.instance.exports.DecodingParam(); param.uiTargetDqLayer 0; param.eOutputColorFormat 1; // YUV420P this.module.instance.exports.WelsDecSetParameter(this.decoder, param); } decode(nalu) { // 将NALU数据写入Wasm内存 const mem this.module.instance.exports.memory; const heap new Uint8Array(mem.buffer); const ptr 1024; // 预留内存地址 heap.set(nalu, ptr); // 调用解码函数 const result this.module.instance.exports.WelsDecDecodeFrameNoDelay( this.decoder, heap.subarray(ptr, ptr nalu.length), nalu.length, heap.subarray(0, 1024), // 输出缓冲区 1024 ); if (result 0) { // 解码成功返回YUV数据 return heap.subarray(0, this.width * this.height * 3 / 2); } } } // 使用示例 const decoder new WasmH264Decoder(); await decoder.loadWasm(); decoder.initDecoder(); // 从WebSocket接收NALU后解码 websocket.onmessage (e) { const yuvData decoder.decode(new Uint8Array(e.data)); // 将YUV转RGB并绘制到canvas drawYUVToCanvas(yuvData, canvas, width, height); };注意事项Wasm内存大小必须严格匹配。openh264解码1080p需要至少32MB线性内存如果initial: 256单位是64KB则初始内存为16MB不够用。我吃过亏——在RK3399设备上解码卡顿最后发现是内存不足触发了频繁GC。解决方案initial: 51232MBmaximum: 2048128MB。3.3 WebCodecs方案用原生API榨干浏览器性能WebCodecs方案的实操价值在于可控性。它不像MediaSource那样隐藏内部状态所有环节都暴露给你。下面给出一个生产环境可用的完整实现。第一步检测浏览器支持function checkWebCodecsSupport() { if (typeof VideoDecoder undefined) { throw new Error(WebCodecs not supported in this browser); } // 检查H.264支持 const h264Support VideoDecoder.isConfigSupported({ codec: avc1.640028, hardwareAcceleration: prefer-hardware, latencyMode: realtime }); if (!h264Support.supported) { console.warn(H.264 hardware acceleration not available, falling back to software); } return h264Support; }第二步构建解码器class WebCodecsPlayer { constructor(videoElement) { this.video videoElement; this.decoder null; this.frameQueue []; this.isDecoding false; } async initDecoder() { this.decoder new VideoDecoder({ output: (frame) this.#onFrameOutput(frame), error: (e) console.error(Decoder error:, e) }); // 配置解码器关键参数 await this.decoder.configure({ codec: avc1.640028, // H.264 High Profile Level 4.0 codedWidth: 1920, codedHeight: 1080, description: this.#getAVCCDescription(), // SPS/PPS数据 hardwareAcceleration: prefer-hardware, latencyMode: realtime }); } #getAVCCDescription() { // AVCC格式lengthSize4 SPS PPS // SPS和PPS从RTSP DESCRIBE响应中提取 const sps new Uint8Array([0,0,0,1, 67, 64, 0, 28, 0, 96, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0]); const pps new Uint8Array([0,0,0,1, 68, 128, 0, 6]); const avcc new Uint8Array(7 sps.length pps.length); avcc[0] 1; // version avcc[1] 100; // profile avcc[2] 0; // profile compat avcc[3] 40; // level avcc[4] 255; // lengthSizeMinusOne (4 bytes) avcc[5] 1; // numSPS avcc[6] sps.length; avcc.set(sps, 7); avcc.set(pps, 7 sps.length); return avcc; } #onFrameOutput(frame) { // 直接显示到video元素 this.video.srcObject frame; // 或者绘制到OffscreenCanvas用于滤镜处理 // this.offscreenCtx.drawImage(frame, 0, 0); frame.close(); // 必须手动释放否则内存泄漏 } async decodeChunk(nalu) { // 构建EncodedVideoChunk const chunk new EncodedVideoChunk({ type: nalu[4] 0x1F 5 ? key : delta, // IDR帧为key timestamp: performance.now() * 1000, // 时间戳单位为ns duration: 33333333, // 30fps的帧间隔 data: nalu }); // 解码非阻塞 await this.decoder.decode(chunk); } }第三步WebSocket数据流对接// 处理WebSocket消息的优化策略 class StreamProcessor { constructor(decoder) { this.decoder decoder; this.naluBuffer new Uint8Array(1024 * 1024); // 1MB缓冲区 this.bufferOffset 0; } handleMessage(data) { const arrayBuffer data instanceof ArrayBuffer ? data : data.arrayBuffer(); const uint8 new Uint8Array(arrayBuffer); // 合并NALU处理TCP粘包 this.#mergeNALU(uint8); // 解析完整NALU并解码 while (this.#hasCompleteNALU()) { const nalu this.#extractNALU(); this.decoder.decodeChunk(nalu); } } #mergeNALU(chunk) { // 将新数据追加到缓冲区 if (this.bufferOffset chunk.length this.naluBuffer.length) { // 缓冲区满丢弃旧数据生产环境应告警 this.bufferOffset 0; } this.naluBuffer.set(chunk, this.bufferOffset); this.bufferOffset chunk.length; } #hasCompleteNALU() { // 查找起始码 0x00000001 for (let i 0; i this.bufferOffset - 4; i) { if (this.naluBuffer[i] 0 this.naluBuffer[i1] 0 this.naluBuffer[i2] 0 this.naluBuffer[i3] 1) { return true; } } return false; } #extractNALU() { // 找到第一个起始码位置 let start 0; for (let i 0; i this.bufferOffset - 4; i) { if (this.naluBuffer[i] 0 this.naluBuffer[i1] 0 this.naluBuffer[i2] 0 this.naluBuffer[i3] 1) { start i; break; } } // 找到下一个起始码或缓冲区末尾 let end this.bufferOffset; for (let i start 4; i this.bufferOffset - 4; i) { if (this.naluBuffer[i] 0 this.naluBuffer[i1] 0 this.naluBuffer[i2] 0 this.naluBuffer[i3] 1) { end i; break; } } // 提取NALU包含起始码 const nalu this.naluBuffer.slice(start, end); // 移动缓冲区 this.naluBuffer.set(this.naluBuffer.slice(end), 0); this.bufferOffset - end - start; return nalu; } } // 使用 const player new WebCodecsPlayer(document.getElementById(video)); await player.initDecoder(); const processor new StreamProcessor(player); websocket.onmessage (e) processor.handleMessage(e.data);实操心得WebCodecs的VideoDecoder有隐式队列。如果连续调用decode()浏览器会自动排队。但当网络抖动导致NALU到达不均匀时队列会积压。我的解决方案是添加节流if (this.decoder.state configured) { this.decoder.decode(chunk); }避免向已满队列提交任务。4. 对比分析与避坑指南真实项目中的血泪教训4.1 三方案核心参数对比表维度WebSocket方案Wasm方案WebCodecs方案首帧延迟300~600ms含MediaSource初始化400~800msWasm加载解码器初始化150~300ms原生API无额外开销持续延迟200~400ms取决于fMP4封装效率100~250ms纯解码无封装80~180ms零拷贝GPU直通CPU占用1080p30Chrome: 12%~18%Firefox: 25%~35%Chrome: 20%~28%Firefox: 18%~25%Chrome: 8%~15%Firefox: 10%~20%内存占用40~60MBMediaSource缓冲区80~120MBWasm线性内存JS堆25~45MBVideoFrame池管理浏览器兼容性Chrome 50, Firefox 48, Safari 11Chrome 69, Firefox 70, Edge 79Chrome 94, Edge 94, Opera 80Firefox/Safari部分支持开发复杂度★★☆需熟悉fMP4格式★★★★需C/C/Wasm知识★★★☆需深入理解编解码原理维护成本低依赖成熟中继服务中Wasm模块需随浏览器更新高API变更频繁需紧跟W3C草案这张表不是纸上谈兵而是我在三个项目中实测的数据。比如在智慧园区项目中用WebSocket方案时发现Firefox CPU占用飙升到35%原因是其MediaSource实现对fMP4的索引解析效率低换成Wasm方案后CPU降到22%但首帧延迟增加了200ms——因为Wasm模块加载阻塞了主线程。最终上线版本采用混合策略Chrome用WebCodecsFirefox用WasmSafari回退到WebSocket。4.2 常见问题速查表与独家修复方案问题现象根本原因修复方案我的实测效果DOMException: Failed to execute addSourceBuffer on MediaSourceMIME类型不匹配或SPS/PPS缺失检查mediaSource.addSourceBuffer(video/mp4; codecsavc1.640028)中的codec值确保与RTSP流Profile一致用Wireshark抓包验证SPS/PPS是否正确发送100%解决耗时5分钟WebAssembly.instantiate(): CompileError: WebAssembly validation errorWasm模块编译目标与浏览器不兼容重新编译时添加-s STANDALONE_WASM1 -s EXPORTED_FUNCTIONS[_WelsDecCreate,_WelsDecDecodeFrameNoDelay]编译后体积增加15%但兼容性提升至Chrome 69VideoDecoder.configure() failed: NotSupportedError浏览器不支持指定codec或hardwareAcceleration动态降级先尝试prefer-hardware失败后重试no-preference用VideoDecoder.isConfigSupported()预检在Mac M1上硬件加速失败率从32%降至0%Stream disconnected before completion: failed to send websocket request: ioWebSocket心跳超时或Nginx代理超时在Nginx配置中添加proxy_read_timeout 300; proxy_send_timeout 300;前端实现心跳包每30秒发ping连接稳定性从87%提升至99.9%VideoFrame not rendering, black screenVideoFrame未正确绑定到video或OffscreenCanvas确保video.srcObject frame后立即调用video.play()若用OffscreenCanvas需在requestAnimationFrame中绘制解决90%的黑屏问题剩余10%需检查GPU驱动独家避坑技巧永远不要相信RTSP流的SPS/PPS是静态的。海康威视摄像头在码率自适应时会动态切换Profile导致SPS变化。我的方案是在WebSocket连接建立后先发送一个DESCRIBE请求用fetch调用后端API解析响应中的afmtp字段提取SPS/PPS再动态配置解码器。这个动作增加了200ms首帧延迟但换来全天候稳定播放。4.3 生产环境必做的5项加固措施网络抖动自适应在WebSocket层实现NACK否定确认。当检测到NALU丢失如连续收到P帧但无I帧主动向后端发送重传请求。我用Uint8Array的第0位标记重传标志后端rtsp-simple-server收到后从GOP头重新推流。内存泄漏防护WebCodecs的VideoFrame必须手动close()Wasm的解码器需destroy()。我在onbeforeunload事件中添加清理window.addEventListener(beforeunload, () { if (this.decoder) this.decoder.close(); if (this.wasmModule) this.wasmModule.destroy(); });降级策略兜底按浏览器能力分三级Level 1Chrome 94WebCodecsLevel 2Chrome 69 / Firefox 70WasmLevel 3Safari / 旧版ChromeWebSocket MediaSource 用navigator.userAgent和typeof VideoDecoder组合判断100ms内完成决策。**日志熔断机制
返回列表