news 2026/4/16 16:50:13

写一个最简单的 WebRTC Demo(实操篇)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
写一个最简单的 WebRTC Demo(实操篇)

写一个最简单的 WebRTC Demo(实操篇)

本文是 WebRTC 系列专栏的第三篇,我们将动手实践,从零开始构建一个完整的 WebRTC 音视频通话 Demo。通过这个实战项目,你将深入理解 WebRTC 的工作流程。


目录

  1. 项目概述
  2. 获取摄像头与麦克风
  3. 建立 RTCPeerConnection
  4. 实现完整的 P2P 音视频通话
  5. 运行与测试
  6. 常见问题与调试
  7. 总结

1. 项目概述

1.1 我们要做什么?

我们将构建一个1 对 1 的实时音视频通话应用,包含以下功能:

  • 获取本地摄像头和麦克风
  • 建立 P2P 连接
  • 实现双向音视频通话
  • 支持挂断功能

1.2 技术栈

组件技术选型
前端原生 HTML/CSS/JavaScript
信令服务器Node.js + WebSocket
WebRTC浏览器原生 API

1.3 项目结构

webrtc-demo/ ├── server/ │ ├── package.json │ └── server.js # 信令服务器 ├── client/ │ ├── index.html # 页面结构 │ ├── style.css # 样式 │ └── main.js # WebRTC 逻辑 └── README.md

2. 获取摄像头与麦克风

2.1 基础 API:getUserMedia

getUserMedia是获取媒体设备的核心 API。

// 最简单的用法asyncfunctiongetLocalStream(){try{conststream=awaitnavigator.mediaDevices.getUserMedia({video:true,audio:true});returnstream;}catch(error){console.error('获取媒体设备失败:',error);throwerror;}}

2.2 处理权限和错误

asyncfunctiongetLocalStream(){// 检查浏览器支持if(!navigator.mediaDevices||!navigator.mediaDevices.getUserMedia){thrownewError('浏览器不支持 getUserMedia');}try{conststream=awaitnavigator.mediaDevices.getUserMedia({video:true,audio:true});returnstream;}catch(error){// 处理不同类型的错误switch(error.name){case'NotAllowedError':thrownewError('用户拒绝了摄像头/麦克风权限');case'NotFoundError':thrownewError('找不到摄像头或麦克风设备');case'NotReadableError':thrownewError('设备被其他应用占用');case'OverconstrainedError':thrownewError('设备不满足指定的约束条件');default:throwerror;}}}

2.3 高级约束配置

constconstraints={video:{width:{min:640,ideal:1280,max:1920},height:{min:480,ideal:720,max:1080},frameRate:{ideal:30},facingMode:'user'// 前置摄像头},audio:{echoCancellation:true,// 回声消除noiseSuppression:true,// 噪声抑制autoGainControl:true// 自动增益}};conststream=awaitnavigator.mediaDevices.getUserMedia(constraints);

2.4 显示本地视频

<videoid="localVideo"autoplaymutedplaysinline></video>
constlocalVideo=document.getElementById('localVideo');conststream=awaitgetLocalStream();// 将媒体流绑定到 video 元素localVideo.srcObject=stream;

⚠️注意:本地视频需要设置muted属性,否则会产生回声。


3. 建立 RTCPeerConnection

3.1 创建 PeerConnection

constconfiguration={iceServers:[{urls:'stun:stun.l.google.com:19302'},{urls:'stun:stun1.l.google.com:19302'}]};constpeerConnection=newRTCPeerConnection(configuration);

3.2 添加本地媒体轨道

// 将本地媒体流的所有轨道添加到 PeerConnectionlocalStream.getTracks().forEach(track=>{peerConnection.addTrack(track,localStream);});

3.3 处理远端媒体流

constremoteVideo=document.getElementById('remoteVideo');peerConnection.ontrack=(event)=>{// 获取远端媒体流const[remoteStream]=event.streams;remoteVideo.srcObject=remoteStream;};

3.4 Offer/Answer 交换

// 发起方:创建 OfferasyncfunctioncreateOffer(){constoffer=awaitpeerConnection.createOffer();awaitpeerConnection.setLocalDescription(offer);// 通过信令服务器发送 OffersendToSignalingServer({type:'offer',sdp:offer.sdp});}// 接收方:处理 Offer 并创建 AnswerasyncfunctionhandleOffer(offer){awaitpeerConnection.setRemoteDescription(newRTCSessionDescription(offer));constanswer=awaitpeerConnection.createAnswer();awaitpeerConnection.setLocalDescription(answer);// 通过信令服务器发送 AnswersendToSignalingServer({type:'answer',sdp:answer.sdp});}// 发起方:处理 AnswerasyncfunctionhandleAnswer(answer){awaitpeerConnection.setRemoteDescription(newRTCSessionDescription(answer));}

3.5 ICE 候选交换

// 收集 ICE 候选peerConnection.onicecandidate=(event)=>{if(event.candidate){sendToSignalingServer({type:'candidate',candidate:event.candidate});}};// 添加远端 ICE 候选asyncfunctionhandleCandidate(candidate){awaitpeerConnection.addIceCandidate(newRTCIceCandidate(candidate));}

4. 实现完整的 P2P 音视频通话

现在让我们把所有部分组合起来,创建一个完整的项目。

4.1 信令服务器 (server/server.js)

constWebSocket=require('ws');consthttp=require('http');// 创建 HTTP 服务器constserver=http.createServer();// 创建 WebSocket 服务器constwss=newWebSocket.Server({server});// 存储所有连接的客户端constclients=newMap();letclientIdCounter=0;wss.on('connection',(ws)=>{// 为每个客户端分配唯一 IDconstclientId=++clientIdCounter;clients.set(clientId,ws);console.log(`客户端${clientId}已连接,当前在线:${clients.size}`);// 通知客户端其 IDws.send(JSON.stringify({type:'welcome',clientId:clientId,clientCount:clients.size}));// 通知其他客户端有新用户加入broadcastExcept(clientId,{type:'user-joined',clientId:clientId,clientCount:clients.size});ws.on('message',(message)=>{try{constdata=JSON.parse(message);console.log(`收到来自客户端${clientId}的消息:`,data.type);// 转发消息给目标客户端if(data.target){consttargetWs=clients.get(data.target);if(targetWs&&targetWs.readyState===WebSocket.OPEN){targetWs.send(JSON.stringify({...data,from:clientId}));}}else{// 广播给所有其他客户端broadcastExcept(clientId,{...data,from:clientId});}}catch(error){console.error('消息解析错误:',error);}});ws.on('close',()=>{clients.delete(clientId);console.log(`客户端${clientId}已断开,当前在线:${clients.size}`);// 通知其他客户端broadcastExcept(clientId,{type:'user-left',clientId:clientId,clientCount:clients.size});});ws.on('error',(error)=>{console.error(`客户端${clientId}错误:`,error);});});// 广播消息给除指定客户端外的所有客户端functionbroadcastExcept(excludeId,message){clients.forEach((ws,id)=>{if(id!==excludeId&&ws.readyState===WebSocket.OPEN){ws.send(JSON.stringify(message));}});}constPORT=process.env.PORT||8080;server.listen(PORT,()=>{console.log(`信令服务器运行在 ws://localhost:${PORT}`);});

4.2 package.json (server/package.json)

{"name":"webrtc-signaling-server","version":"1.0.0","description":"WebRTC 信令服务器","main":"server.js","scripts":{"start":"node server.js"},"dependencies":{"ws":"^8.14.2"}}

4.3 HTML 页面 (client/index.html)

<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>WebRTC 视频通话 Demo</title><linkrel="stylesheet"href="style.css"></head><body><divclass="container"><h1>WebRTC 视频通话</h1><!-- 状态显示 --><divclass="status-bar"><spanid="connectionStatus">未连接</span><spanid="clientInfo"></span></div><!-- 视频区域 --><divclass="video-container"><divclass="video-wrapper"><videoid="localVideo"autoplaymutedplaysinline></video><spanclass="video-label">本地视频</span></div><divclass="video-wrapper"><videoid="remoteVideo"autoplayplaysinline></video><spanclass="video-label">远端视频</span></div></div><!-- 控制按钮 --><divclass="controls"><buttonid="startBtn"class="btn btn-primary">开启摄像头</button><buttonid="callBtn"class="btn btn-success"disabled>发起通话</button><buttonid="hangupBtn"class="btn btn-danger"disabled>挂断</button></div><!-- 媒体控制 --><divclass="media-controls"><buttonid="toggleVideoBtn"class="btn btn-secondary"disabled>关闭视频</button><buttonid="toggleAudioBtn"class="btn btn-secondary"disabled>静音</button></div><!-- 日志区域 --><divclass="log-container"><h3>连接日志</h3><divid="logArea"></div></div></div><scriptsrc="main.js"></script></body></html>

4.4 CSS 样式 (client/style.css)

*{margin:0;padding:0;box-sizing:border-box;}body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,sans-serif;background:linear-gradient(135deg,#1a1a2e 0%,#16213e 100%);min-height:100vh;color:#fff;}.container{max-width:1200px;margin:0 auto;padding:20px;}h1{text-align:center;margin-bottom:20px;font-size:2rem;background:linear-gradient(90deg,#00d2ff,#3a7bd5);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;}/* 状态栏 */.status-bar{display:flex;justify-content:space-between;align-items:center;background:rgba(255,255,255,0.1);padding:10px 20px;border-radius:10px;margin-bottom:20px;}#connectionStatus{padding:5px 15px;border-radius:20px;background:#e74c3c;font-size:0.9rem;}#connectionStatus.connected{background:#27ae60;}#connectionStatus.calling{background:#f39c12;}/* 视频容器 */.video-container{display:flex;gap:20px;justify-content:center;flex-wrap:wrap;margin-bottom:20px;}.video-wrapper{position:relative;background:#000;border-radius:15px;overflow:hidden;box-shadow:0 10px 30pxrgba(0,0,0,0.3);}.video-wrapper video{width:480px;height:360px;object-fit:cover;display:block;}.video-label{position:absolute;bottom:10px;left:10px;background:rgba(0,0,0,0.7);padding:5px 15px;border-radius:20px;font-size:0.85rem;}/* 按钮样式 */.controls, .media-controls{display:flex;gap:15px;justify-content:center;margin-bottom:15px;}.btn{padding:12px 30px;border:none;border-radius:25px;font-size:1rem;cursor:pointer;transition:all 0.3s ease;font-weight:600;}.btn:disabled{opacity:0.5;cursor:not-allowed;}.btn-primary{background:linear-gradient(90deg,#00d2ff,#3a7bd5);color:#fff;}.btn-primary:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 5px 20pxrgba(0,210,255,0.4);}.btn-success{background:linear-gradient(90deg,#11998e,#38ef7d);color:#fff;}.btn-success:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 5px 20pxrgba(56,239,125,0.4);}.btn-danger{background:linear-gradient(90deg,#eb3349,#f45c43);color:#fff;}.btn-danger:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 5px 20pxrgba(235,51,73,0.4);}.btn-secondary{background:rgba(255,255,255,0.2);color:#fff;}.btn-secondary:hover:not(:disabled){background:rgba(255,255,255,0.3);}.btn-secondary.active{background:#e74c3c;}/* 日志区域 */.log-container{background:rgba(0,0,0,0.3);border-radius:15px;padding:20px;margin-top:20px;}.log-container h3{margin-bottom:15px;font-size:1.1rem;color:#aaa;}#logArea{height:200px;overflow-y:auto;font-family:'Monaco','Menlo',monospace;font-size:0.85rem;line-height:1.6;}#logArea .log-item{padding:3px 0;border-bottom:1px solidrgba(255,255,255,0.05);}#logArea .log-time{color:#888;margin-right:10px;}#logArea .log-info{color:#3498db;}#logArea .log-success{color:#27ae60;}#logArea .log-warning{color:#f39c12;}#logArea .log-error{color:#e74c3c;}/* 响应式设计 */@media(max-width:768px){.video-wrapper video{width:100%;height:auto;aspect-ratio:4/3;}.controls, .media-controls{flex-wrap:wrap;}.btn{flex:1;min-width:120px;}}

4.5 JavaScript 主逻辑 (client/main.js)

// ==================== 配置 ====================constSIGNALING_SERVER_URL='ws://localhost:8080';constICE_SERVERS={iceServers:[{urls:'stun:stun.l.google.com:19302'},{urls:'stun:stun1.l.google.com:19302'},{urls:'stun:stun2.l.google.com:19302'}]};// ==================== 全局变量 ====================letlocalStream=null;letpeerConnection=null;letsignalingSocket=null;letmyClientId=null;letremoteClientId=null;letisVideoEnabled=true;letisAudioEnabled=true;// ==================== DOM 元素 ====================constlocalVideo=document.getElementById('localVideo');constremoteVideo=document.getElementById('remoteVideo');conststartBtn=document.getElementById('startBtn');constcallBtn=document.getElementById('callBtn');consthangupBtn=document.getElementById('hangupBtn');consttoggleVideoBtn=document.getElementById('toggleVideoBtn');consttoggleAudioBtn=document.getElementById('toggleAudioBtn');constconnectionStatus=document.getElementById('connectionStatus');constclientInfo=document.getElementById('clientInfo');constlogArea=document.getElementById('logArea');// ==================== 日志函数 ====================functionlog(message,type='info'){consttime=newDate().toLocaleTimeString();constlogItem=document.createElement('div');logItem.className='log-item';logItem.innerHTML=`<span class="log-time">[${time}]</span><span class="log-${type}">${message}</span>`;logArea.appendChild(logItem);logArea.scrollTop=logArea.scrollHeight;console.log(`[${type.toUpperCase()}]${message}`);}// ==================== 状态更新 ====================functionupdateStatus(status,className=''){connectionStatus.textContent=status;connectionStatus.className=className;}// ==================== 信令服务器连接 ====================functionconnectSignalingServer(){log('正在连接信令服务器...');signalingSocket=newWebSocket(SIGNALING_SERVER_URL);signalingSocket.onopen=()=>{log('信令服务器连接成功','success');updateStatus('已连接','connected');};signalingSocket.onclose=()=>{log('信令服务器连接断开','warning');updateStatus('未连接');// 尝试重连setTimeout(connectSignalingServer,3000);};signalingSocket.onerror=(error)=>{log('信令服务器连接错误','error');};signalingSocket.onmessage=async(event)=>{constmessage=JSON.parse(event.data);awaithandleSignalingMessage(message);};}// ==================== 处理信令消息 ====================asyncfunctionhandleSignalingMessage(message){log(`收到信令消息:${message.type}`);switch(message.type){case'welcome':myClientId=message.clientId;clientInfo.textContent=`我的 ID:${myClientId}| 在线人数:${message.clientCount}`;log(`分配到客户端 ID:${myClientId}`,'success');break;case'user-joined':clientInfo.textContent=`我的 ID:${myClientId}| 在线人数:${message.clientCount}`;log(`用户${message.clientId}加入`,'info');if(localStream){callBtn.disabled=false;}break;case'user-left':clientInfo.textContent=`我的 ID:${myClientId}| 在线人数:${message.clientCount}`;log(`用户${message.clientId}离开`,'warning');if(message.clientId===remoteClientId){hangup();}break;case'offer':log(`收到来自用户${message.from}的通话请求`,'info');remoteClientId=message.from;awaithandleOffer(message);break;case'answer':log(`收到来自用户${message.from}的应答`,'success');awaithandleAnswer(message);break;case'candidate':awaithandleCandidate(message);break;case'hangup':log(`用户${message.from}挂断了通话`,'warning');hangup();break;}}// ==================== 发送信令消息 ====================functionsendSignalingMessage(message){if(signalingSocket&&signalingSocket.readyState===WebSocket.OPEN){signalingSocket.send(JSON.stringify(message));}}// ==================== 获取本地媒体流 ====================asyncfunctionstartLocalStream(){try{log('正在获取摄像头和麦克风...');localStream=awaitnavigator.mediaDevices.getUserMedia({video:{width:{ideal:1280},height:{ideal:720},frameRate:{ideal:30}},audio:{echoCancellation:true,noiseSuppression:true,autoGainControl:true}});localVideo.srcObject=localStream;log('摄像头和麦克风获取成功','success');// 更新按钮状态startBtn.disabled=true;callBtn.disabled=false;toggleVideoBtn.disabled=false;toggleAudioBtn.disabled=false;}catch(error){log(`获取媒体设备失败:${error.message}`,'error');}}// ==================== 创建 PeerConnection ====================functioncreatePeerConnection(){log('创建 PeerConnection...');peerConnection=newRTCPeerConnection(ICE_SERVERS);// 添加本地轨道localStream.getTracks().forEach(track=>{peerConnection.addTrack(track,localStream);log(`添加本地轨道:${track.kind}`);});// ICE 候选事件peerConnection.onicecandidate=(event)=>{if(event.candidate){log(`发送 ICE 候选:${event.candidate.type||'unknown'}`);sendSignalingMessage({type:'candidate',target:remoteClientId,candidate:event.candidate});}};// ICE 连接状态变化peerConnection.oniceconnectionstatechange=()=>{conststate=peerConnection.iceConnectionState;log(`ICE 连接状态:${state}`);switch(state){case'checking':updateStatus('正在连接...','calling');break;case'connected':case'completed':updateStatus('通话中','connected');log('P2P 连接建立成功!','success');break;case'failed':log('连接失败','error');hangup();break;case'disconnected':log('连接断开','warning');break;}};// 连接状态变化peerConnection.onconnectionstatechange=()=>{log(`连接状态:${peerConnection.connectionState}`);};// 收到远端轨道peerConnection.ontrack=(event)=>{log(`收到远端轨道:${event.track.kind}`,'success');const[remoteStream]=event.streams;remoteVideo.srcObject=remoteStream;};returnpeerConnection;}// ==================== 发起通话 ====================asyncfunctioncall(){log('发起通话...');createPeerConnection();try{constoffer=awaitpeerConnection.createOffer();awaitpeerConnection.setLocalDescription(offer);log('发送 Offer...');sendSignalingMessage({type:'offer',sdp:offer.sdp});updateStatus('等待应答...','calling');callBtn.disabled=true;hangupBtn.disabled=false;}catch(error){log(`创建 Offer 失败:${error.message}`,'error');}}// ==================== 处理 Offer ====================asyncfunctionhandleOffer(message){createPeerConnection();try{awaitpeerConnection.setRemoteDescription(newRTCSessionDescription({type:'offer',sdp:message.sdp}));constanswer=awaitpeerConnection.createAnswer();awaitpeerConnection.setLocalDescription(answer);log('发送 Answer...');sendSignalingMessage({type:'answer',target:remoteClientId,sdp:answer.sdp});callBtn.disabled=true;hangupBtn.disabled=false;}catch(error){log(`处理 Offer 失败:${error.message}`,'error');}}// ==================== 处理 Answer ====================asyncfunctionhandleAnswer(message){try{awaitpeerConnection.setRemoteDescription(newRTCSessionDescription({type:'answer',sdp:message.sdp}));log('Answer 处理完成','success');}catch(error){log(`处理 Answer 失败:${error.message}`,'error');}}// ==================== 处理 ICE 候选 ====================asyncfunctionhandleCandidate(message){try{if(peerConnection&&message.candidate){awaitpeerConnection.addIceCandidate(newRTCIceCandidate(message.candidate));log('添加 ICE 候选成功');}}catch(error){log(`添加 ICE 候选失败:${error.message}`,'error');}}// ==================== 挂断 ====================functionhangup(){log('挂断通话');// 通知对方if(remoteClientId){sendSignalingMessage({type:'hangup',target:remoteClientId});}// 关闭 PeerConnectionif(peerConnection){peerConnection.close();peerConnection=null;}// 清除远端视频remoteVideo.srcObject=null;remoteClientId=null;// 更新按钮状态callBtn.disabled=false;hangupBtn.disabled=true;updateStatus('已连接','connected');}// ==================== 切换视频 ====================functiontoggleVideo(){if(localStream){constvideoTrack=localStream.getVideoTracks()[0];if(videoTrack){isVideoEnabled=!isVideoEnabled;videoTrack.enabled=isVideoEnabled;toggleVideoBtn.textContent=isVideoEnabled?'关闭视频':'开启视频';toggleVideoBtn.classList.toggle('active',!isVideoEnabled);log(`视频已${isVideoEnabled?'开启':'关闭'}`);}}}// ==================== 切换音频 ====================functiontoggleAudio(){if(localStream){constaudioTrack=localStream.getAudioTracks()[0];if(audioTrack){isAudioEnabled=!isAudioEnabled;audioTrack.enabled=isAudioEnabled;toggleAudioBtn.textContent=isAudioEnabled?'静音':'取消静音';toggleAudioBtn.classList.toggle('active',!isAudioEnabled);log(`音频已${isAudioEnabled?'开启':'静音'}`);}}}// ==================== 事件绑定 ====================startBtn.addEventListener('click',startLocalStream);callBtn.addEventListener('click',call);hangupBtn.addEventListener('click',hangup);toggleVideoBtn.addEventListener('click',toggleVideo);toggleAudioBtn.addEventListener('click',toggleAudio);// ==================== 初始化 ====================window.addEventListener('load',()=>{log('WebRTC Demo 初始化...');connectSignalingServer();});// 页面关闭时清理window.addEventListener('beforeunload',()=>{if(localStream){localStream.getTracks().forEach(track=>track.stop());}if(peerConnection){peerConnection.close();}if(signalingSocket){signalingSocket.close();}});

5. 运行与测试

5.1 启动信令服务器

# 进入 server 目录cdserver# 安装依赖npminstall# 启动服务器npmstart

输出:

信令服务器运行在 ws://localhost:8080

5.2 启动客户端

由于需要访问摄像头,浏览器要求使用 HTTPS 或 localhost。我们可以使用简单的 HTTP 服务器:

# 进入 client 目录cdclient# 使用 Python 启动 HTTP 服务器python3 -m http.server3000# 或使用 Node.js 的 http-servernpx http-server -p3000

5.3 测试步骤

  1. 打开两个浏览器窗口(或两台设备)
  2. 访问http://localhost:3000
  3. 在两个窗口中分别点击「开启摄像头」
  4. 在其中一个窗口点击「发起通话」
  5. 观察连接建立过程和视频通话效果

5.4 测试检查清单

检查项预期结果
本地视频显示✅ 能看到自己的摄像头画面
信令连接✅ 状态显示「已连接」
发起通话✅ 状态变为「等待应答」
连接建立✅ 状态变为「通话中」
远端视频✅ 能看到对方的视频
音频通话✅ 能听到对方的声音
挂断功能✅ 能正常挂断并重新通话

6. 常见问题与调试

6.1 调试工具

Chrome WebRTC Internals

在 Chrome 浏览器中访问:

chrome://webrtc-internals

可以查看:

  • PeerConnection 状态
  • ICE 候选收集情况
  • SDP 内容
  • 媒体统计信息
获取连接统计
asyncfunctiongetStats(){if(peerConnection){conststats=awaitpeerConnection.getStats();stats.forEach(report=>{if(report.type==='inbound-rtp'&&report.kind==='video'){console.log('视频接收统计:',{packetsReceived:report.packetsReceived,bytesReceived:report.bytesReceived,packetsLost:report.packetsLost,framesDecoded:report.framesDecoded});}});}}

6.2 常见问题

问题 1:摄像头权限被拒绝

现象:点击「开启摄像头」后报错

解决方案

  1. 检查浏览器地址栏的权限图标
  2. 确保使用localhostHTTPS
  3. 在浏览器设置中重置摄像头权限
问题 2:ICE 连接失败

现象:状态一直显示「正在连接」

可能原因

  • 防火墙阻止 UDP 流量
  • NAT 类型不兼容
  • STUN 服务器不可用

解决方案

// 添加 TURN 服务器作为备选constICE_SERVERS={iceServers:[{urls:'stun:stun.l.google.com:19302'},{urls:'turn:your-turn-server.com:3478',username:'user',credential:'password'}]};
问题 3:只有单向视频

现象:一方能看到对方,但对方看不到自己

可能原因

  • 轨道未正确添加
  • ontrack事件未触发

调试方法

// 检查轨道状态console.log('发送器:',peerConnection.getSenders());console.log('接收器:',peerConnection.getReceivers());
问题 4:音频有回声

现象:通话时听到自己的声音

解决方案

  1. 确保本地视频设置了muted属性
  2. 使用耳机进行测试
  3. 检查echoCancellation是否启用
<videoid="localVideo"autoplaymutedplaysinline></video>

6.3 网络调试

// 监控 ICE 候选收集peerConnection.onicegatheringstatechange=()=>{console.log('ICE 收集状态:',peerConnection.iceGatheringState);};// 打印所有收集到的候选peerConnection.onicecandidate=(event)=>{if(event.candidate){console.log('ICE 候选:',{type:event.candidate.type,protocol:event.candidate.protocol,address:event.candidate.address,port:event.candidate.port});}else{console.log('ICE 候选收集完成');}};

7. 总结

本文要点回顾

步骤关键 API
获取媒体流navigator.mediaDevices.getUserMedia()
创建连接new RTCPeerConnection(config)
添加轨道pc.addTrack(track, stream)
创建 Offerpc.createOffer()
设置描述pc.setLocalDescription()/pc.setRemoteDescription()
ICE 候选pc.onicecandidate/pc.addIceCandidate()
接收媒体pc.ontrack

完整流程图

┌─────────────────────────────────────────────────────────────────┐ │ WebRTC 通话流程 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 1. getUserMedia() 获取本地媒体流 │ │ ↓ │ │ 2. new RTCPeerConnection() 创建连接对象 │ │ ↓ │ │ 3. addTrack() 添加本地轨道 │ │ ↓ │ │ 4. createOffer() 创建 Offer │ │ ↓ │ │ 5. setLocalDescription() 设置本地描述 │ │ ↓ │ │ 6. 信令服务器 交换 Offer/Answer/ICE │ │ ↓ │ │ 7. setRemoteDescription() 设置远端描述 │ │ ↓ │ │ 8. addIceCandidate() 添加 ICE 候选 │ │ ↓ │ │ 9. ontrack 接收远端媒体 │ │ ↓ │ │ 10. 通话建立! │ │ │ └─────────────────────────────────────────────────────────────────┘

下一篇预告

在下一篇文章中,我们将深入探讨WebRTC 的三个关键技术

  • NAT 穿透原理与 ICE 框架
  • 音视频实时传输协议(RTP/RTCP/SRTP)
  • 回声消除、抗抖动与带宽控制

参考资料

  1. MDN - WebRTC API
  2. WebRTC Samples
  3. Getting Started with WebRTC
  4. WebRTC for the Curious

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/15 20:25:37

Qt QtWebEngine 白屏的解决方案

公众号:cpp手艺人 Qt QtWebEngine 白屏的解决方案 最近在项目中有同事反馈,软件在开启的瞬间和长时间挂机之后,会出现白屏的现象。 先来看看白屏的常见原因和解决方案 1、QtWebEngine 白屏最常见的 5 大原因和解决方案: 主要原因 解决方式 GPU 加速问题 禁用 GPU、使用…

作者头像 李华
网站建设 2026/4/16 14:19:09

TCU变速箱控制器仿真模型:从代码到现实的传动艺术

TCU变速箱控制器仿真模型-含&#xff08;设计文档&#xff09; 乘用车AMTTCU变速箱控制器仿真模型算法模块&#xff0c;含&#xff0c;TCU应用层软件&#xff0c;驱动制动数学模型&#xff0c;电机传动数学模型&#xff0c;车辆数学模型等,在售产品已量产。 含有的功能模块包括…

作者头像 李华
网站建设 2026/4/16 15:44:18

QWebEngine 是什么?与 Chromium 的关系解析

公众号:cpp手艺人 QWebEngine 是什么?与 Chromium 的关系解析 1. 概述:QWebEngine 是什么? QWebEngine 是 Qt 框架中用于嵌入现代 Web 内容的核心模块,自 Qt 5.4(2014年)起正式引入,取代了旧版的 QtWebKit。它基于 Chromium 项目构建,为 Qt 应用程序提供高性能、安…

作者头像 李华
网站建设 2026/4/16 14:01:49

QWebEngine 常用 API 全面梳理

公众号:cpp手艺人 QWebEngine 常用 API 全面梳理(超全版本) Qt WebEngine 基于 Chromium,但提供了 Qt 风格的 API。本文对 QWebEngine 的常用类与 API 进行系统梳理,帮助你快速掌握其开发全景。 1. QWebEngineView(视图层) QWebEngineView 是最常用的 UI 控件,主要…

作者头像 李华
网站建设 2026/4/16 13:55:13

探索 COMSOL 光学与相场模拟的奇妙世界

COMSOL光学模型&#xff1a;随机分布颗粒散射&#xff0c;COMSOL光学仿真模型:光镊/光力模型&#xff08;包含三个模型&#xff0c;近似算法&#xff0c;张量算法&#xff09;相场模拟——合金&#xff0c;金属凝固模型&#xff0c;各向异性枝晶生长karma合金凝固模型&#xff…

作者头像 李华
网站建设 2026/4/15 15:10:02

【Linux网络编程】UDP Socket

前言:最近在复习 Linux 网络编程,重点梳理了 UDP 协议的实现细节。虽然 UDP 是无连接、不可靠的协议,但其简单高效的特性在很多场景下(如实时音视频、DNS)依然是首选。从最简单的 Echo Server 出发,逐步重构为支持业务解耦的字典服务器,最后实现一个支持多线程的全双工聊…

作者头像 李华