news 2026/3/12 1:59:53

写一个最简单的 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/3/10 6:54:24

Java 日期格式化方法:SimpleDateFormat 和 DateTimeFormatter

一、SimpleDateFormat&#xff08;传统方式&#xff09; 1. 基本用法 import java.text.SimpleDateFormat; import java.util.Date;public class DateFormatExample {public static void main(String[] args) {// 创建 SimpleDateFormat 实例SimpleDateFormat sdf new SimpleD…

作者头像 李华
网站建设 2026/3/10 11:48:02

【dz-968】室内空气监测系统设计

摘要 随着人们对室内生活质量关注度的不断提升&#xff0c;室内空气质量的精准监测与及时调控对保障人体健康、提升居住舒适度具有重要意义。传统的室内空气管理多依赖人工观察和手动操作&#xff0c;不仅存在监测滞后、调控不及时的问题&#xff0c;还难以全面掌握多项空气参…

作者头像 李华
网站建设 2026/3/8 6:47:54

【接口测试】5_接口测试基础 _接口文档解析

文章目录一、接口文档1.1 接口文档介绍1.2 接口文档内容二、接口文档解析2.1 登录接口文档2.1.1 基本信息2.1.2 请求参数2.1.3 返回数据2.2 登录接口文档解析一、接口文档 1.1 接口文档介绍 接口文档&#xff1a;又称为API文档&#xff0c;一般是由后端开发人员所编写的&…

作者头像 李华
网站建设 2026/3/8 6:46:36

最近在搞风光储联合发电系统的仿真,发现直驱风机和光伏逆变器的配合特别有意思。今天就跟大伙儿唠唠这个模型搭建时遇到的坑,顺便分享几个关键模块的调参心得

风光储simulink仿真模型&#xff0c;风机采用直驱风机&#xff0c;光伏为经逆变器外送实现并网&#xff0c;运行稳定&#xff0c;先看整个系统的骨架结构&#xff08;顺手在Simulink里拖出三个大框&#xff09;&#xff1a; 直驱永磁同步发电机带着变流器光伏阵列连着双级式逆变…

作者头像 李华