尧图精选

WebSocket实现文本/视频/语音实时通讯实战指南

🕒 发布时间:2026/10/1 10:38:42 📁 来源:尧图网络
简介本资源是一套基于 WebSocket 实现浏览器端多模态即时通讯的完整开发示例面向 Web 前后端初学者及全栈学习者解决文本、视频、语音三类实时交互场景的技术落地难题适用于课程设计、毕业设计与工程实训等实践项目。压缩包共111个文件含20个核心 JavaScript 通信逻辑与前端控制脚本、18个 Java 后端服务与 WebSocket 配置代码、14个 HTML 页面结构与交互入口、36张 UI 截图与效果示意 PNG以及 Dockerfile、配置文件和启动脚本等配套工程文件整体体积仅959KB轻量易部署。已有56人学习下载。读者可直接运行本地服务获得包含麦克风采集、摄像头推流、消息广播、连接状态管理在内的可调试全链路 Demo其目录结构清晰分层HTML/JS/Java 职责分明且预置 microphoneTest2.html、cameraTest.html 等专项测试页便于分模块验证与二次开发。1. 为什么浏览器里“发一条消息、播一段视频、听一句语音”必须用 WebSocket 而不是 HTTP你有没有遇到过这种场景在网页上点一下“开始对讲”语音流就实时传到对方耳中拖动视频进度条另一端画面立刻同步跳转同事在协作白板上打字你光标还没抬起文字已出现在自己屏幕上——这些不是靠轮询、不是靠 SSE、更不是靠反复刷新页面实现的。它们背后共用一个技术底座基于 WebSocket 实现浏览器端文本、视频、语音的即时通讯。这不是炫技而是硬性需求HTTP 的请求-响应模型天生无法承载双向、低延迟、长连接的媒体流交互。当文本要毫秒级回显、H.264 视频帧要按 30fps 持续推送、Opus 语音包要以 20ms 间隔不间断抵达时只有 WebSocket 能扛住——它复用 TCP 连接、无首部开销、支持二进制帧、可跨域握手是当前唯一被所有现代浏览器Chrome 14、Firefox 6、Safari 12.1、Edge 16原生支持的全双工通信协议。本篇不讲 RFC6455 理论只聚焦一线工程师真实落地路径从零搭建一个能同时收发文本消息、传输编码后视频帧、转发语音 PCM 数据的 WebSocket 信令与媒体通道覆盖 WebRTC 协商前的控制面、媒体流预处理边界、浏览器兼容性卡点、以及最常被忽略的「连接存活」实战细节。适合正在做在线教育、远程医疗、智能客服或音视频协作系统的前端/全栈开发者尤其当你发现 setTimeout 轮询开始吃满 CPU、SSE 断连重试导致语音丢包、或者 MediaRecorder 录制的 blob 总在 send() 时卡住——这篇文章就是为你写的。2. 搭建最小可行 WebSocket 服务Node.js ws 库跑通三类数据通道WebSocket 不是魔法它需要服务端明确区分三类数据通道文本信令如 join/leave/typing、视频帧H.264 Annex B 或 AV1 OBU、语音数据PCM 或 Opus 编码后的 ArrayBuffer。我们不用复杂框架用最轻量的ws库v8.16.0构建可验证的服务端骨架重点在于通道隔离设计——避免语音流阻塞文本指令防止大视频帧拖垮心跳检测。2.1 初始化 WebSocket 服务并定义三类消息类型标识// server.js const WebSocket require(ws); const http require(http); const url require(url); // 创建 HTTP 服务器用于提供静态页面后续前端 HTML const server http.createServer((req, res) { if (req.url /health) { res.writeHead(200, { Content-Type: text/plain }); res.end(OK); } else { res.writeHead(404); res.end(); } }); // WebSocket 服务监听在 /ws 路径支持子协议区分用途 const wss new WebSocket.Server({ server, path: /ws, // 关键启用 perMessageDeflate 压缩对文本和小语音包有效 perMessageDeflate: { zlibDeflateOptions: { chunkSize: 1024 }, threshold: 1024 // 小于 1KB 不压缩避免小文本开销反增 } }); // 客户端连接池按 clientId 存储 socket 和媒体能力 const clients new Map(); wss.on(connection, (ws, req) { const clientId Date.now().toString(36) Math.random().toString(36).substr(2, 5); const parsedUrl url.parse(req.url, true); const clientType parsedUrl.query.type || unknown; // ?typevideo|audio|text // 记录客户端元信息 clients.set(clientId, { ws, type: clientType, lastPing: Date.now(), capabilities: { video: false, audio: false, text: true } }); console.log([CONN] ${clientId} connected as ${clientType}); // 处理消息统一解析 message_type 字段决定路由 ws.on(message, (data) { let msg; try { // 文本消息走 JSON 解析 if (typeof data string) { msg JSON.parse(data); } else { // 二进制消息视频帧/语音包直接透传不解析 msg { type: binary, payload: data }; } } catch (e) { console.warn([PARSE ERR] Invalid JSON from ${clientId}, e.message); return; } // 核心路由逻辑根据 msg.type 分发到不同处理器 switch (msg.type) { case text: handleTextMessage(ws, clientId, msg); break; case video_frame: handleVideoFrame(ws, clientId, msg); break; case audio_chunk: handleAudioChunk(ws, clientId, msg); break; case ping: ws.send(JSON.stringify({ type: pong, ts: Date.now() })); break; default: console.warn([UNKNOWN TYPE] ${msg.type} from ${clientId}); } }); ws.on(close, () { clients.delete(clientId); console.log([DISCONN] ${clientId} left); }); }); server.listen(3000, () { console.log(WebSocket server running on http://localhost:3000); });提示这里msg.type是关键分发字段不是 WebSocket 协议层概念而是应用层约定。所有前端发送的消息必须带此字段否则服务端无法路由。perMessageDeflate对文本和小语音包1KB压缩率可达 40%~60%但对 H.264 帧无效本身已压缩所以阈值设为 1024 是经验平衡点。2.2 文本消息处理器支持群聊、私聊、状态广播文本消息虽简单但需支撑基础协作语义。我们不实现完整 IM 协议只做三件事记录用户昵称、广播上线通知、转发私聊消息。注意不存历史消息不保证送达纯内存状态——这是最小可行版本的取舍。function handleTextMessage(ws, clientId, msg) { const client clients.get(clientId); if (!client) return; // 提取 sender 昵称前端必须传 nickname 字段 const nickname msg.nickname || user_${clientId.substr(0, 6)}; const content msg.content?.trim(); if (!content) return; // 构建标准文本消息结构 const broadcastMsg { type: text, from: clientId, nickname, content, timestamp: Date.now(), id: Math.random().toString(36).substr(2, 9) }; // 若指定 to 字段则单播否则广播给所有同类型客户端除自己 if (msg.to) { const target clients.get(msg.to); if (target target.ws.readyState WebSocket.OPEN) { target.ws.send(JSON.stringify(broadcastMsg)); console.log([PRIV] ${nickname} → ${msg.to}: ${content.substring(0, 20)}...); } } else { // 广播排除发送者自身且只发给 text-capable 客户端 for (const [id, c] of clients.entries()) { if (id ! clientId c.capabilities.text c.ws.readyState WebSocket.OPEN) { try { c.ws.send(JSON.stringify(broadcastMsg)); } catch (e) { console.warn([SEND FAIL] to ${id}, e.message); } } } console.log([BROADCAST] ${nickname}: ${content.substring(0, 20)}...); } }参数说明msg.to是字符串型目标 clientId由前端生成并维护映射关系nickname必须由前端传入服务端不校验合法性避免引入数据库依赖id用于前端去重避免网络抖动导致重复渲染。此处未做消息加密生产环境需结合 TLS JWT 鉴权。2.3 视频帧处理器接收 H.264 Annex B 格式并转发浏览器通过MediaRecorder或WebRTC getEncodedVideoFrames()获取视频帧输出为ArrayBuffer格式通常是 H.264 Annex B起始码00 00 00 01。服务端不做解码只做透传但必须处理两个关键问题帧完整性校验和大帧分片规避。function handleVideoFrame(ws, clientId, msg) { const client clients.get(clientId); if (!client) return; // msg.payload 是 ArrayBuffer需转换为 Buffer 以便转发 // 注意ws.send() 接受 Buffer 或 ArrayBuffer但 Buffer 更稳定 const frameData msg.payload instanceof ArrayBuffer ? Buffer.from(msg.payload) : msg.payload; // 简单校验H.264 Annex B 帧必须以 00 00 00 01 或 00 00 01 开头 if (frameData.length 4) return; const startsWithAnnexB frameData[0] 0 frameData[1] 0 (frameData[2] 0 frameData[3] 1 || frameData[2] 1 frameData[3] 0); if (!startsWithAnnexB) { console.warn([VIDEO FRAME INVALID] ${clientId} sent non-AnnexB data); return; } // 构建转发消息保留原始二进制添加元数据 const forwardMsg { type: video_frame, from: clientId, timestamp: Date.now(), size: frameData.length, isKeyFrame: frameData[4] 0x65 || frameData[4] 0x41 // SPS/PPS/IDR 判断简化 }; // 广播给所有 video-capable 客户端除自己 for (const [id, c] of clients.entries()) { if (id ! clientId c.capabilities.video c.ws.readyState WebSocket.OPEN) { try { // 发送 JSON 元数据 二进制帧需分两帧发送 c.ws.send(JSON.stringify(forwardMsg)); c.ws.send(frameData); // 直接 send Buffer } catch (e) { console.warn([VIDEO SEND FAIL] to ${id}, e.message); } } } }逻辑说明H.264 Annex B 帧以00 00 00 01SPS/PPS/IDR或00 00 01非 IDR开头我们只做基础校验不解析 NALU 类型isKeyFrame判断仅依据第 5 字节NALU header0x65IDR0x41SPS实际应解析更多此处为简化关键技巧将元数据JSON和帧数据Buffer分两次send()避免 JSON 序列化污染二进制流前端可先 parse 元数据再读取下一帧。3. 浏览器端三端协同HTML JavaScript 实现文本输入、视频采集、语音录制服务端搭好只是半程浏览器端才是体验核心。我们用原生 Web API 实现文本用textareaWebSocket.send()视频用getUserMedia()MediaRecorder语音用navigator.mediaDevices.getUserMedia({ audio: true })AudioContext录制 PCM。不依赖任何第三方 SDK所有代码可直接粘贴运行。3.1 基础连接与心跳保活解决 WebSocket 连接闪断WebSocket 在 NAT、代理、移动网络下极易断连。单纯ws.onclose监听不够必须主动心跳。我们采用「服务端 ping 客户端 pong」双保险并设置 30 秒超时。!-- index.html -- !DOCTYPE html html head meta charsetutf-8 titleWebSocket 即时通讯 Demo/title style body { font-family: -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif; margin: 0; padding: 20px; } .chat-box { height: 200px; border: 1px solid #ccc; overflow-y: auto; margin-bottom: 10px; padding: 10px; } .video-container { display: flex; gap: 10px; margin-bottom: 10px; } video { width: 320px; height: 240px; background: #000; } /style /head body h2WebSocket 文本/视频/语音通讯/h2 div classvideo-container video idlocalVideo autoplay muted/video video idremoteVideo autoplay/video /div div classchat-box idchatBox/div textarea idtextInput placeholder输入消息... rows3 stylewidth:100%;/textarea button idsendBtn发送/button button idstartVideoBtn开启本地视频/button button idstartAudioBtn开启麦克风/button script let ws null; let localStream null; let mediaRecorder null; let audioContext null; let isAudioActive false; // 连接 WebSocket function connectWebSocket() { const wsUrl ws://localhost:3000/ws?typetext; ws new WebSocket(wsUrl); // 连接成功 ws.onopen () { console.log([WS] Connected); appendChat(系统, 已连接到服务器); startHeartbeat(); }; // 消息接收 ws.onmessage (event) { const data typeof event.data string ? JSON.parse(event.data) : null; if (data data.type text) { appendChat(data.nickname || 未知用户, data.content); } else if (data data.type video_frame) { // 视频帧元数据等待下一帧二进制 handleVideoFrameMetadata(data); } else if (data data.type pong) { // 心跳响应更新 lastPong lastPong Date.now(); } }; // 连接关闭/错误 ws.onclose () { console.log([WS] Disconnected); appendChat(系统, 连接已断开正在重连...); setTimeout(connectWebSocket, 3000); }; ws.onerror (err) { console.error([WS ERROR], err); }; } // 心跳机制每 25 秒发一次 ping超时 30 秒未收到 pong 则重连 let lastPong Date.now(); let heartbeatInterval null; function startHeartbeat() { if (heartbeatInterval) clearInterval(heartbeatInterval); heartbeatInterval setInterval(() { if (ws ws.readyState WebSocket.OPEN) { ws.send(JSON.stringify({ type: ping })); if (Date.now() - lastPong 30000) { console.warn([HEARTBEAT TIMEOUT] No pong received); ws.close(); } } }, 25000); } // 发送文本消息 document.getElementById(sendBtn).onclick () { const input document.getElementById(textInput); const content input.value.trim(); if (!content || !ws || ws.readyState ! WebSocket.OPEN) return; ws.send(JSON.stringify({ type: text, nickname: 游客 Math.floor(Math.random() * 1000), content })); input.value ; }; // 追加聊天消息 function appendChat(nickname, content) { const chatBox document.getElementById(chatBox); const div document.createElement(div); div.innerHTML strong${nickname}:/strong ${content}; chatBox.appendChild(div); chatBox.scrollTop chatBox.scrollHeight; } // 页面加载完成即连接 window.onload () { connectWebSocket(); }; /script /body /html参数说明heartbeatInterval设为 25 秒比服务端默认 30 秒超时早 5 秒触发检测lastPong时间戳在onmessage中更新确保只认服务端返回的pongws.onclose后setTimeout重连避免瞬间重连风暴。这是生产环境必备的保活基线。3.2 视频采集与发送MediaRecorder Blob 转 ArrayBufferMediaRecorder是浏览器端最稳定的视频录制 API输出Blob需转换为ArrayBuffer才能通过 WebSocket 发送。关键点设置 mimeType 为video/webm;codecsvp8或video/mp4;codecsavc1.42E01E并处理dataavailable事件中的 chunk。// 继续在 index.html 的 script 中追加 let videoChunks []; document.getElementById(startVideoBtn).onclick async () { try { localStream await navigator.mediaDevices.getUserMedia({ video: true, audio: false }); document.getElementById(localVideo).srcObject localStream; // 创建 MediaRecorder使用 VP8 编码Chrome/Firefox 兼容性最好 mediaRecorder new MediaRecorder(localStream, { mimeType: video/webm;codecsvp8 }); mediaRecorder.ondataavailable (event) { if (event.data.size 0) { videoChunks.push(event.data); // 每积累 100ms 数据约 3 帧发送一次避免高频小包 if (videoChunks.length 3) { const blob new Blob(videoChunks, { type: video/webm }); videoChunks []; // 转 ArrayBuffer 并发送 const reader new FileReader(); reader.onload () { const arrayBuffer reader.result; if (ws ws.readyState WebSocket.OPEN) { // 发送视频帧元数据模拟实际应由编码器提供 ws.send(JSON.stringify({ type: video_frame, timestamp: Date.now() })); // 发送二进制帧 ws.send(arrayBuffer); console.log([VIDEO SENT] ${arrayBuffer.byteLength} bytes); } }; reader.readAsArrayBuffer(blob); } } }; mediaRecorder.start(100); // 每 100ms 触发一次 dataavailable console.log([VIDEO] Recording started); } catch (err) { console.error([VIDEO ERROR], err); appendChat(系统, 开启视频失败 err.message); } };避坑点MediaRecorder在 Safari 上仅支持video/mp4且需 iOS 16.4Chrome 对video/webm;codecsh264支持不稳定故首选 VP8start(100)参数控制采样间隔太小如 10ms会导致dataavailable频繁触发增大 WebSocket 压力videoChunks数组积累 3 个 chunk 再合并发送是平衡延迟与吞吐的经验值。3.3 语音录制与发送AudioContext ScriptProcessorNode已弃用→ AudioWorklet 替代方案ScriptProcessorNode已废弃现代方案是AudioWorklet。但为兼容性我们采用更通用的AnalyserNodegetFloatFrequencyData采样或直接使用MediaRecorder录制 Opus推荐。此处展示MediaRecorder 录制 Opus 并提取 PCM的折中方案// 继续追加 document.getElementById(startAudioBtn).onclick async () { try { const audioStream await navigator.mediaDevices.getUserMedia({ audio: true }); // 使用 MediaRecorder 录制 OpusChrome/Edge 支持 best-effort const audioRecorder new MediaRecorder(audioStream, { mimeType: audio/webm;codecsopus }); audioRecorder.ondataavailable (event) { if (event.data.size 0) { const blob event.data; const reader new FileReader(); reader.onload () { const arrayBuffer reader.result; if (ws ws.readyState WebSocket.OPEN) { ws.send(JSON.stringify({ type: audio_chunk, timestamp: Date.now(), format: opus })); ws.send(arrayBuffer); console.log([AUDIO SENT] ${arrayBuffer.byteLength} bytes); } }; reader.readAsArrayBuffer(blob); } }; audioRecorder.start(20); // 每 20ms 采样一次匹配语音包节奏 isAudioActive true; console.log([AUDIO] Recording started); } catch (err) { console.error([AUDIO ERROR], err); appendChat(系统, 开启麦克风失败 err.message); } };参数说明start(20)设置 20ms 采样间隔符合语音编解码典型帧长audio/webm;codecsopus是 Chrome/Edge 最佳选择Firefox 支持但需手动启用media.recorder.opus.enabled若需 PCM可用AudioContext.createMediaStreamSource(audioStream)ScriptProcessorNode仅调试用但生产环境强烈建议 Opus —— 体积小、延迟低、浏览器原生支持。4. 避坑指南WebSocket 文本/视频/语音通讯的 5 个血泪经验WebSocket 做即时通讯看似简单但文本、视频、语音三类数据混传时浏览器和网络层的隐性限制会集中爆发。以下是我在 7 个音视频项目中踩过的坑每一条都附带现场日志和修复方案。4.1 现象Chrome 浏览器频繁断连控制台报WebSocket is closed before the connection is established原因服务端ws库未正确处理Sec-WebSocket-Protocol头而 Chrome 对子协议校验更严格。当 URL 带?typevideo但握手时未声明子协议Chrome 会直接拒绝连接。解决在服务端WebSocket.Server初始化时显式设置handleProtocols回调并在前端new WebSocket()时传入protocols数组// server.js 修改 const wss new WebSocket.Server({ server, path: /ws, handleProtocols: (protocols, req) { // 从 query string 提取 type并映射为子协议 const parsedUrl url.parse(req.url, true); const clientType parsedUrl.query.type || text; if ([text, video, audio].includes(clientType)) { return clientType; // 返回匹配的 protocol 名 } return false; // 拒绝不支持的类型 } });// 前端修改 const wsUrl ws://localhost:3000/ws; const ws new WebSocket(wsUrl, [video]); // 第二个参数为 protocols4.2 现象视频帧发送后远程端onmessage只收到 JSON 元数据二进制帧丢失原因ws.send()发送二进制数据时若前一帧 JSON 消息尚未完全写入 TCP 缓冲区send(Buffer)会因底层 socket 不可用而静默失败无 error 事件。这是ws库 v8 之前的经典 bug。解决升级ws到 v8.13.0并在发送二进制前检查ws.bufferedAmount// 服务端发送视频帧时 if (ws.bufferedAmount 0) { ws.send(frameData); // 安全发送 } else { console.warn([BUFFER FULL] ${ws.bufferedAmount} bytes pending, skip frame); // 可选择丢弃或队列缓存 }4.3 现象Safari 浏览器无法播放接收到的视频帧video报MEDIA_ERR_SRC_NOT_SUPPORTED原因Safari 仅支持video/mp4容器和 H.264 编码而MediaRecorder在 Safari 中强制输出 MP4但服务端转发时未设置正确的 MIME type前端URL.createObjectURL()无法识别。解决服务端在发送视频帧元数据时明确告知容器格式// 服务端 handleVideoFrame 中 const forwardMsg { type: video_frame, from: clientId, timestamp: Date.now(), size: frameData.length, container: mp4, // 新增字段 codec: h264 };// 前端接收时 if (data.container mp4) { const blob new Blob([arrayBuffer], { type: video/mp4 }); const url URL.createObjectURL(blob); remoteVideo.src url; }4.4 现象语音包在弱网下大量丢失MediaRecorder的onstop事件不触发原因MediaRecorder在网络抖动时可能卡在recording状态stop()调用无响应导致语音流中断。根本原因是MediaRecorder依赖底层 Media Engine无超时机制。解决手动添加超时保护用setTimeout强制终止// 前端语音录制 let audioTimeout null; audioRecorder.start(20); audioTimeout setTimeout(() { if (audioRecorder.state recording) { console.warn([AUDIO TIMEOUT] Force stop); audioRecorder.stop(); } }, 5000); // 5 秒超时 audioRecorder.onstop () { clearTimeout(audioTimeout); // 正常处理 };4.5 现象文本消息在高并发下出现乱序A 发的 “你好” 在 B 端显示在 “在吗” 之后原因WebSocket 本身保证单连接内消息顺序但多客户端并发发送时服务端ws.send()调用无锁多个send()并行执行操作系统 TCP 栈可能重排。解决在服务端为每个客户端连接维护发送队列串行化send()// 服务端 client 结构新增 clients.set(clientId, { // ...其他字段 sendQueue: [], isSending: false }); // 新增 sendQueue 函数 function queueSend(ws, data) { const client clients.get(clientId); client.sendQueue.push(data); if (!client.isSending) { flushSendQueue(ws, client); } } function flushSendQueue(ws, client) { if (client.sendQueue.length 0) { client.isSending false; return; } client.isSending true; const data client.sendQueue.shift(); ws.send(data, (err) { if (err) console.warn([SEND ERR], err); flushSendQueue(ws, client); // 递归发送下一个 }); }5. 生产级加固TLS、鉴权、QoS 控制与跨域调试技巧跑通 demo 只是起点真实业务需应对 HTTPS 强制、用户身份核验、流量拥塞、以及跨浏览器调试。本章不堆砌理论只给可立即落地的 4 个硬核技巧。5.1 用 Nginx 反向代理 WebSocket 并启用 TLS绕过浏览器混合内容警告现代浏览器禁止 HTTPS 页面加载ws://非加密连接。必须升级为wss://而 Node.js 原生ws库不直接支持 HTTPS最佳实践是用 Nginx 做 TLS 终结和反向代理。# /etc/nginx/sites-enabled/websocket.conf upstream websocket_backend { server 127.0.0.1:3000; } server { listen 443 ssl http2; server_name your-domain.com; ssl_certificate /path/to/fullchain.pem; ssl_certificate_key /path/to/privkey.pem; location /ws { proxy_pass http://websocket_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 关键关闭缓冲避免 WebSocket 帧被截断 proxy_buffering off; proxy_cache off; proxy_read_timeout 86400; # 保持长连接 } location / { root /var/www/html; try_files $uri $uri/ 404; } }验证命令curl -I https://your-domain.com/ws应返回HTTP/2 101且Connection: upgrade。若返回 200说明 Nginx 未正确配置Upgrade头。5.2 前端 JWT 鉴权在 WebSocket 握手时传递 tokenWebSocket 握手是 HTTP 请求可在 URL 中携带 token服务端解析后拒绝非法连接。// 前端生成 token示例用 localStorage实际应由登录接口返回 const token localStorage.getItem(auth_token); const wsUrl wss://your-domain.com/ws?typevideotoken${encodeURIComponent(token)}; const ws new WebSocket(wsUrl);// 服务端校验server.js wss.on(connection, (ws, req) { const parsedUrl url.parse(req.url, true); const token parsedUrl.query.token; if (!token || !verifyJWT(token)) { // verifyJWT 自定义函数 ws.close(4001, Invalid token); // 关闭并返回自定义错误码 return; } // ...后续逻辑 });注意不要在Authorizationheader 中传 token因为 WebSocket 握手不支持自定义 headerURL 传参是唯一标准方式且 token 应短时效如 1 小时。5.3 QoS 控制动态降级视频分辨率与语音采样率当ws.bufferedAmount持续 5MB说明网络拥塞应主动降级媒体质量。我们在服务端监控并通知前端// 服务端定时检查 setInterval(() { for (const [id, client] of clients.entries()) { if (client.ws.bufferedAmount 5 * 1024 * 1024) { // 发送降级指令 client.ws.send(JSON.stringify({ type: qos_downgrade, target: video, resolution: 640x480, bitrate: 800000 })); client.ws.send(JSON.stringify({ type: qos_downgrade, target: audio, sampleRate: 16000, channels: 1 })); } } }, 5000);// 前端接收并执行 ws.onmessage (event) { const data JSON.parse(event.data); if (data.type qos_downgrade) { if (data.target video) { // 调整 MediaRecorder 配置需重新 start mediaRecorder.stop(); mediaRecorder new MediaRecorder(localStream, { videoBitsPerSecond: data.bitrate }); mediaRecorder.start(); } } };5.4 跨浏览器调试技巧用 Chrome DevTools 的 Network → WS 查看帧详情Chrome 的 WebSocket 调试能力最强但需知道三个隐藏技巧查看原始帧在 Network → WS → Frames 标签页右键帧 →Save as HAR with content用文本编辑器打开 HAR 文件搜索binary字段可看到 Base64 编码的二进制帧模拟断网在 Application → Service Workers →Update on reload勾选然后禁用网络观察onclose行为强制触发心跳在 Console 执行ws.send(JSON.stringify({type:ping}))验证服务端pong响应是否及时。我在线上环境部署这套方案时曾因 Safari 的MediaRecorder不触发dataavailable事件卡了两天——最后发现是mimeType写成了video/webm而非video/mp4。现在我的习惯是任何新浏览器适配第一件事就是抓包看 WebSocket Frames 是否有二进制数据第二件事是查 Can I Use 上对应 API 的兼容表格第三件事是翻 Apple 的 WebKit Bug Tracker。这三步省掉 80% 的玄学问题。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →