一、核心 API 简述
navigator.mediaDevices.getUserMedia:获取本地摄像头 / 麦克风媒体流RTCPeerConnection:WebRTC 核心,建立 P2P 连接、传输音视频- SDP:会话描述协议(信令交换:Offer / Answer)
- ICE:网络穿透,自动协商直连地址
WebRTC 本身不含信令服务,需自行实现信令(WebSocket/Socket.IO 交换 SDP、ICE 候选)。
二、最简可运行 Demo(本地模拟双人通话,无后端)
该示例单页面模拟两端,不用后端,直接浏览器打开即可测试。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>WebRTC 本地调用演示</title>
<style>
video { width: 400px; border: 1px solid #ccc; }
</style>
</head>
<body>
<h3>本地视频</h3>
<video id="localVideo" autoplay muted playsinline></video>
<h3>远端视频</h3>
<video id="remoteVideo" autoplay playsinline></video>
<script>
// 1. 获取 DOM
const localVideo = document.getElementById('localVideo');
const remoteVideo = document.getElementById('remoteVideo');
// 配置:音视频轨道 + ICE 服务器(STUN 用于内网穿透)
const pcConfig = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' }
]
};
let localStream;
let pc1, pc2; // 模拟两个 Peer
// 启动
start();
async function start() {
try {
// 步骤1:采集本地音视频流
localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
localVideo.srcObject = localStream;
// 步骤2:创建两个 PeerConnection(模拟A、B两端)
pc1 = new RTCPeerConnection(pcConfig);
pc2 = new RTCPeerConnection(pcConfig);
// 将本地流添加到 Peer
localStream.getTracks().forEach(track => {
pc1.addTrack(track, localStream);
});
// 远端收到轨道,渲染视频
pc2.ontrack = e => {
remoteVideo.srcObject = e.streams[0];
};
// ICE 候选互相交换(模拟信令转发)
pc1.onicecandidate = e => e.candidate && pc2.addIceCandidate(e.candidate);
pc2.onicecandidate = e => e.candidate && pc1.addIceCandidate(e.candidate);
// 步骤3:创建 Offer
const offer = await pc1.createOffer();
await pc1.setLocalDescription(offer);
// 步骤4:B 端接收 Offer,创建 Answer
await pc2.setRemoteDescription(offer);
const answer = await pc2.createAnswer();
await pc2.setLocalDescription(answer);
// 步骤5:A 端接收 Answer
await pc1.setRemoteDescription(answer);
} catch (err) {
console.error('WebRTC 调用失败:', err);
alert('请允许摄像头/麦克风权限,且使用 HTTPS / localhost');
}
}
</script>
</body>
</html>
运行要求
- 必须
localhost/ HTTPS 环境(浏览器安全策略,HTTP 公网无法调用摄像头) - 浏览器授权摄像头、麦克风权限
- 现代浏览器(Chrome / Edge / Firefox)
三、标准 P2P 通话完整流程(真实线上架构)
整体链路
客户端A ↔ 信令服务 (WebSocket/Socket.IO) ↔ 客户端B ↔ WebRTC P2P
3.1. 标准调用步骤(时序)
1,本地媒体采集
js
运行
javascript
const stream = await navigator.mediaDevices.getUserMedia({video:true,audio:true})
2,创建 RTCPeerConnection
3,本地轨道 addTrack 进 Peer
- A 发起 Offer
pc.createOffer()→setLocalDescription(offer)- 通过信令把 Offer 发给 B
- B 收到 Offer
setRemoteDescription(offer)createAnswer()→setLocalDescription(answer)- 通过信令把 Answer 发回 A
- A 收到 Answer
setRemoteDescription(answer)
- ICE 候选交换(网络穿透)
- 两端
onicecandidate拿到候选,通过信令互发 - 对方
addIceCandidate
- 两端
- 连接成功 →
ontrack收到远端流,播放
3.2. 纯客户端核心代码(分离两端)
发起方(A)
javascript
// 拿到本地流后
const pc = new RTCPeerConnection(pcConfig);
localStream.getTracks().forEach(t => pc.addTrack(t, localStream));
// 收到远端流
pc.ontrack = e => remoteVideo.srcObject = e.streams[0];
// ICE 候选发送给对方
pc.onicecandidate = e => {
if (e.candidate) socket.emit('ice', e.candidate);
};
// 创建并发送 Offer
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
socket.emit('offer', offer);
// 接收对方 Answer
socket.on('answer', async answer => {
await pc.setRemoteDescription(answer);
});
// 接收对方 ICE 候选
socket.on('ice', candidate => {
pc.addIceCandidate(candidate);
});
接收方(B)
javascript
const pc = new RTCPeerConnection(pcConfig);
localStream.getTracks().forEach(t => pc.addTrack(t, localStream));
pc.ontrack = e => remoteVideo.srcObject = e.streams[0];
pc.onicecandidate = e => e.candidate && socket.emit('ice', e.candidate);
// 接收 Offer
socket.on('offer', async offer => {
await pc.setRemoteDescription(offer);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
socket.emit('answer', answer);
});
// 接收 ICE
socket.on('ice', candidate => pc.addIceCandidate(candidate));
四、常用配置 & 功能扩展
1. 只开音频 / 只开视频
js
运行
javascript
// 仅语音
navigator.mediaDevices.getUserMedia({ audio: true, video: false })
// 仅画面
navigator.mediaDevices.getUserMedia({ audio: false, video: true })
2. 关闭音视频轨道(静音 / 关摄像头)
js
运行
javascript
// 关闭摄像头
localStream.getVideoTracks()[0].enabled = false;
// 静音
localStream.getAudioTracks()[0].enabled = false;
3. 挂断通话
js
运行
javascript
// 停止媒体流
localStream.getTracks().forEach(track => track.stop());
// 关闭连接
pc.close();
4. 多路流 / 屏幕共享
屏幕共享 API:
js
运行
javascript
const screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true });
五、常见报错 & 排查
-
NotAllowedError- 原因:未授权权限 / 非
localhost/HTTPS - 解决:本地用 localhost,线上部署 HTTPS
- 原因:未授权权限 / 非
-
RTCPeerConnection不存在- 原因:浏览器过低 / 禁用 WebRTC
- 解决:升级 Chrome/Edge
-
能发信令但看不到画面
- 大概率 ICE/STUN 穿透失败,更换可用 STUN/TURN 服务器
六、后端信令选型(极简)
前端 WebRTC 只负责媒体连接,信令必须单独做:
- 小型应用:
Socket.IO(Node.js 最简) - 大型 / 分布式:原生 WebSocket、MQTT
- 商用:直接用 Janus / SFU 服务(支持多人、转码、录制)