如何通过HTML5经WebSocket传输音视频流并重建?WebRTC与MediaRecorder存问题
基于WebSocket的HTML5音视频流传输与重建方案
针对你的需求,这里提供一个通过WebSocket中转、使用原生Media Encoder/Decoder API实现的音视频流传输方案,可解决WebRTC的隐私暴露问题,以及MediaRecorder的卡顿、延迟和丢包依赖问题。
核心思路
放弃WebRTC的P2P模式,改用WebSocket通过服务器中转音视频数据(客户端仅与服务器建立连接,无需暴露彼此公网IP);同时绕过MediaRecorder的分段机制,直接对MediaStream的音视频轨道进行实时帧编码,逐帧发送编码后数据,接收端实时解码重建流。
具体实现步骤
1. 音视频采集
使用getUserMedia获取摄像头和麦克风的MediaStream,同时支持本地预览:
async function getMediaStream() { const constraints = { video: { width: 1280, height: 720, frameRate: 30 }, audio: { sampleRate: 48000, channelCount: 2 } }; try { const stream = await navigator.mediaDevices.getUserMedia(constraints); document.getElementById('local-preview').srcObject = stream; return stream; } catch (err) { console.error('音视频采集失败:', err); } }
2. 发送端:实时编码音视频帧
使用浏览器原生的VideoEncoder和AudioEncoder对音视频轨道的每一帧进行编码,直接输出编码后的块数据,避免MediaRecorder的分段问题:
视频编码与发送
async function setupVideoEncoder(stream, ws) { const videoTrack = stream.getVideoTracks()[0]; const videoProcessor = new MediaStreamTrackProcessor({ track: videoTrack }); const reader = videoProcessor.readable.getReader(); const videoEncoder = new VideoEncoder({ output: (chunk) => { // 将编码块序列化为可传输格式,携带类型和时间戳 const payload = { type: 'video', timestamp: chunk.timestamp, chunkType: chunk.type, data: new Uint8Array(chunk.data) }; // 以二进制形式发送(比JSON高效) ws.send(serializePayload(payload)); }, error: (err) => console.error('视频编码错误:', err) }); // 配置H.264编码参数 await videoEncoder.configure({ codec: 'avc1.42E01E', width: 1280, height: 720, bitrate: 2_000_000, // 2Mbps,可根据网络调整 framerate: 30, latencyMode: 'low-latency' // 低延迟模式 }); // 循环读取并编码视频帧 while (true) { const { done, value } = await reader.read(); if (done) break; videoEncoder.encode(value); value.close(); } }
音频编码与发送
async function setupAudioEncoder(stream, ws) { const audioTrack = stream.getAudioTracks()[0]; const audioProcessor = new MediaStreamTrackProcessor({ track: audioTrack }); const reader = audioProcessor.readable.getReader(); const audioEncoder = new AudioEncoder({ output: (chunk) => { const payload = { type: 'audio', timestamp: chunk.timestamp, chunkType: chunk.type, data: new Uint8Array(chunk.data) }; ws.send(serializePayload(payload)); }, error: (err) => console.error('音频编码错误:', err) }); // 配置Opus编码(适合实时音频传输) await audioEncoder.configure({ codec: 'opus', sampleRate: 48000, numberOfChannels: 2, bitrate: 128000, // 128kbps latencyMode: 'low-latency' }); while (true) { const { done, value } = await reader.read(); if (done) break; audioEncoder.encode(value); value.close(); } }
WebSocket连接与序列化工具
// 建立WebSocket连接(服务器仅做中转,无需处理音视频数据) function createWebSocket() { const ws = new WebSocket('wss://your-server-domain/stream-relay'); ws.onclose = () => { console.log('连接断开,3秒后重连'); setTimeout(createWebSocket, 3000); }; ws.onerror = (err) => console.error('WebSocket错误:', err); return ws; } // 序列化payload为ArrayBuffer(比JSON.stringify更高效) function serializePayload(payload) { // 简单序列化示例,可根据需求优化 const encoder = new TextEncoder(); const meta = encoder.encode(JSON.stringify({ type: payload.type, timestamp: payload.timestamp, chunkType: payload.chunkType })); const data = payload.data; // 先发送元数据长度,再发送元数据,最后发送二进制数据 const buffer = new ArrayBuffer(4 + meta.length + data.length); const view = new DataView(buffer); view.setUint32(0, meta.length, true); new Uint8Array(buffer, 4).set(meta); new Uint8Array(buffer, 4 + meta.length).set(data); return buffer; }
3. 接收端:解码与流重建
接收WebSocket传输的编码块,使用VideoDecoder和AudioDecoder解码,再通过MediaStreamTrackGenerator生成音视频轨道,合并为MediaStream后播放:
async function setupVideoDecoder() { const videoGenerator = new MediaStreamTrackGenerator({ kind: 'video' }); const writer = videoGenerator.writable.getWriter(); const videoDecoder = new VideoDecoder({ output: (frame) => { writer.write(frame).catch(err => console.error('写入视频帧失败:', err)); frame.close(); }, error: (err) => console.error('视频解码错误:', err) }); await videoDecoder.configure({ codec: 'avc1.42E01E', width: 1280, height: 720 }); return { videoDecoder, videoGenerator }; } async function setupAudioDecoder() { const audioGenerator = new MediaStreamTrackGenerator({ kind: 'audio' }); const writer = audioGenerator.writable.getWriter(); const audioDecoder = new AudioDecoder({ output: (frame) => { writer.write(frame).catch(err => console.error('写入音频帧失败:', err)); frame.close(); }, error: (err) => console.error('音频解码错误:', err) }); await audioDecoder.configure({ codec: 'opus', sampleRate: 48000, numberOfChannels: 2 }); return { audioDecoder, audioGenerator }; } // 处理WebSocket接收的数据 async function handleWebSocketMessage(event, videoDecoder, audioDecoder) { const buffer = event.data; const view = new DataView(buffer); // 解析元数据 const metaLength = view.getUint32(0, true); const metaBuffer = buffer.slice(4, 4 + metaLength); const meta = JSON.parse(new TextDecoder().decode(metaBuffer)); // 解析二进制数据 const dataBuffer = buffer.slice(4 + metaLength); if (meta.type === 'video') { const chunk = new EncodedVideoChunk({ type: meta.chunkType, timestamp: meta.timestamp, data: dataBuffer }); await videoDecoder.decode(chunk); } else if (meta.type === 'audio') { const chunk = new EncodedAudioChunk({ type: meta.chunkType, timestamp: meta.timestamp, data: dataBuffer }); await audioDecoder.decode(chunk); } } // 初始化接收端 async function initReceiver() { const { videoDecoder, videoGenerator } = await setupVideoDecoder(); const { audioDecoder, audioGenerator } = await setupAudioDecoder(); const remoteStream = new MediaStream([videoGenerator, audioGenerator]); document.getElementById('remote-player').srcObject = remoteStream; const ws = createWebSocket(); ws.onmessage = (event) => handleWebSocketMessage(event, videoDecoder, audioDecoder); }
关键问题解决
- 隐私保护:所有数据通过WebSocket服务器中转,客户端仅与服务器建立连接,无需暴露彼此公网IP。
- 卡顿与延迟:采用逐帧编码发送的方式,避免MediaRecorder的分段等待,同时开启低延迟编码模式,平衡延迟与质量。
- 丢包处理:可在发送端为每个帧块添加序号,接收端检测到序号断层时,通过WebSocket向服务器请求重传对应帧块;也可在编码时加入简单的FEC(前向纠错)数据,降低丢包对播放的影响。
注意事项
- 浏览器兼容性:Media Encoder/Decoder API目前在Chrome、Edge 94+支持良好,Firefox需开启
media.webrtc.hw.h264.enabled等配置,Safari 16.4+开始支持。 - 服务器要求:中转服务器需支持WebSocket协议,且具备足够的带宽和CPU资源处理二进制数据转发。
- 音视频同步:依赖编码帧的时间戳,接收端解码时会自动根据时间戳对齐,无需额外处理。
内容的提问来源于stack exchange,提问作者z3db0y
相关产品推荐
相关产品推荐

