8.3 WebRTC 简介
8.3 WebRTC 简介
WebRTC(Web Real-Time Communication)是一套开放的实时通信技术标准,使浏览器和移动应用能够直接进行点对点(P2P)的音视频通信和数据交换,无需中间服务器转发媒体流。
核心架构与关键组件
1. 技术栈组成
| 组件 | 功能描述 |
|---|---|
| getUserMedia | 访问本地媒体设备(摄像头、麦克风) |
| RTCPeerConnection | 建立点对点连接,处理音视频传输和网络协商 |
| RTCDataChannel | 在已建立的连接上传输任意数据 |
| ICE/STUN/TURN | 穿透NAT和防火墙的网络地址转换技术 |
2. 典型通信流程
sequenceDiagram
participant A as 客户端A
participant S as 信令服务器
participant B as 客户端B
A->>S: 加入房间
B->>S: 加入同一房间
A->>A: 创建本地媒体流
A->>A: 创建RTCPeerConnection
A->>S: 发送offer
S->>B: 转发offer
B->>B: 创建RTCPeerConnection
B->>B: 设置远程描述(offer)
B->>B: 创建answer
B->>S: 发送answer
S->>A: 转发answer
A->>A: 设置远程描述(answer)
A->B: ICE候选交换(通过信令服务器)
A-->>B: 建立P2P连接
基础API使用
1. 媒体设备访问
// 获取用户媒体
async function getLocalStream() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
facingMode: 'user' // 或'environment'后置摄像头
}
});
document.getElementById('localVideo').srcObject = stream;
return stream;
} catch (err) {
console.error('获取媒体设备失败:', err);
throw err;
}
}
// 设备枚举
async function listDevices() {
const devices = await navigator.mediaDevices.enumerateDevices();
const cameras = devices.filter(d => d.kind === 'videoinput');
const mics = devices.filter(d => d.kind === 'audioinput');
console.log('可用摄像头:', cameras);
console.log('可用麦克风:', mics);
return { cameras, mics };
}
2. 建立P2P连接
// 创建并配置RTCPeerConnection
function createPeerConnection(config) {
const pcConfig = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{
urls: 'turn:turn.example.com',
username: 'your_username',
credential: 'your_password'
}
]
};
const pc = new RTCPeerConnection(pcConfig);
// ICE候选处理
pc.onicecandidate = (event) => {
if (event.candidate) {
// 通过信令服务器发送候选
signalingServer.send({
type: 'ice-candidate',
candidate: event.candidate
});
}
};
// 远程流到达时处理
pc.ontrack = (event) => {
document.getElementById('remoteVideo').srcObject = event.streams[0];
};
// 连接状态变化
pc.onconnectionstatechange = () => {
console.log('连接状态:', pc.connectionState);
};
return pc;
}
信令服务器实现
1. 基于WebSocket的信令
// 客户端信令处理
class SignalingClient {
constructor() {
this.socket = new WebSocket('wss://yourserver.com/signaling');
this.peerConnection = null;
this.roomId = null;
}
initialize() {
this.socket.onmessage = async (event) => {
const message = JSON.parse(event.data);
switch (message.type) {
case 'offer':
await this.handleOffer(message);
break;
case 'answer':
await this.handleAnswer(message);
break;
case 'ice-candidate':
await this.handleICECandidate(message);
break;
}
};
}
async joinRoom(roomId) {
this.roomId = roomId;
this.socket.send(JSON.stringify({
type: 'join',
roomId
}));
}
async createOffer() {
const offer = await this.peerConnection.createOffer();
await this.peerConnection.setLocalDescription(offer);
this.socket.send(JSON.stringify({
type: 'offer',
roomId: this.roomId,
sdp: offer.sdp
}));
}
async handleOffer(message) {
await this.peerConnection.setRemoteDescription(
new RTCSessionDescription({ type: 'offer', sdp: message.sdp })
);
const answer = await this.peerConnection.createAnswer();
await this.peerConnection.setLocalDescription(answer);
this.socket.send(JSON.stringify({
type: 'answer',
roomId: this.roomId,
sdp: answer.sdp
}));
}
async handleAnswer(message) {
await this.peerConnection.setRemoteDescription(
new RTCSessionDescription({ type: 'answer', sdp: message.sdp })
);
}
async handleICECandidate(message) {
try {
await this.peerConnection.addIceCandidate(
new RTCIceCandidate(message.candidate)
);
} catch (err) {
console.error('添加ICE候选失败:', err);
}
}
}
2. 完整连接示例
async function startCall() {
// 1. 获取本地媒体流
const localStream = await getLocalStream();
// 2. 创建信令客户端
const signaling = new SignalingClient();
signaling.initialize();
// 3. 加入房间
const roomId = 'demo-room';
await signaling.joinRoom(roomId);
// 4. 创建RTCPeerConnection
signaling.peerConnection = createPeerConnection();
// 5. 添加本地流
localStream.getTracks().forEach(track => {
signaling.peerConnection.addTrack(track, localStream);
});
// 6. 创建offer(如果是发起方)
if (isCaller) {
await signaling.createOffer();
}
}
// 处理挂断
function endCall() {
if (signaling.peerConnection) {
signaling.peerConnection.close();
signaling.peerConnection = null;
}
const localVideo = document.getElementById('localVideo');
if (localVideo.srcObject) {
localVideo.srcObject.getTracks().forEach(track => track.stop());
localVideo.srcObject = null;
}
}
数据通道(DataChannel)
1. 创建与使用
// 创建数据通道
function setupDataChannel(pc) {
const dataChannel = pc.createDataChannel('chat', {
ordered: true, // 保证消息顺序
maxRetransmits: 3 // 最大重传次数
});
dataChannel.onopen = () => {
console.log('数据通道已打开');
dataChannel.send('Hello!');
};
dataChannel.onmessage = (event) => {
console.log('收到消息:', event.data);
};
dataChannel.onclose = () => {
console.log('数据通道已关闭');
};
return dataChannel;
}
// 接收端处理
pc.ondatachannel = (event) => {
const dataChannel = event.channel;
dataChannel.onmessage = (event) => {
console.log('收到数据通道消息:', event.data);
};
};
2. 文件传输实现
// 发送文件
async function sendFile(dataChannel, file) {
return new Promise((resolve) => {
const reader = new FileReader();
const fileInfo = {
name: file.name,
type: file.type,
size: file.size,
lastModified: file.lastModified
};
// 发送文件元数据
dataChannel.send(JSON.stringify({
type: 'file-meta',
data: fileInfo
}));
let offset = 0;
const chunkSize = 16 * 1024; // 16KB
reader.onload = (e) => {
dataChannel.send(e.target.result);
offset += e.target.result.byteLength;
if (offset < file.size) {
readNextChunk();
} else {
resolve();
}
};
function readNextChunk() {
const slice = file.slice(offset, offset + chunkSize);
reader.readAsArrayBuffer(slice);
}
readNextChunk();
});
}
// 接收文件
function setupFileReceiver(dataChannel) {
let fileInfo, receivedSize = 0;
const chunks = [];
dataChannel.onmessage = (event) => {
if (typeof event.data === 'string') {
const msg = JSON.parse(event.data);
if (msg.type === 'file-meta') {
fileInfo = msg.data;
console.log(`开始接收文件: ${fileInfo.name}`);
}
} else {
chunks.push(event.data);
receivedSize += event.data.byteLength;
const progress = (receivedSize / fileInfo.size) * 100;
updateProgress(progress);
if (receivedSize === fileInfo.size) {
saveFile(chunks, fileInfo);
}
}
};
function saveFile(chunks, fileInfo) {
const blob = new Blob(chunks, { type: fileInfo.type });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileInfo.name;
a.click();
URL.revokeObjectURL(url);
}
}
高级主题与优化
1. 连接监控与统计
// 获取连接统计
async function getConnectionStats(pc) {
const stats = await pc.getStats();
let results = {};
stats.forEach(report => {
if (report.type === 'candidate-pair' && report.nominated) {
results.candidatePair = report;
} else if (report.type === 'inbound-rtp') {
results.inbound = report;
} else if (report.type === 'outbound-rtp') {
results.outbound = report;
}
});
return {
rtt: results.candidatePair?.currentRoundTripTime * 1000,
inboundBitrate: calculateBitrate(results.inbound),
outboundBitrate: calculateBitrate(results.outbound),
packetsLost: results.inbound?.packetsLost
};
}
function calculateBitrate(rtp) {
if (!rtp) return 0;
return (8 * rtp.bytesReceived) / (rtp.timestamp - rtp.lastPacketReceivedTimestamp);
}
// 定期监控
setInterval(async () => {
const stats = await getConnectionStats(pc);
console.log('当前连接质量:', stats);
}, 5000);
2. 自适应码率控制
// 基于网络条件调整视频质量
function setupBandwidthAdaptation(pc, sender) {
const MIN_BITRATE = 150000; // 150kbps
const MAX_BITRATE = 2000000; // 2Mbps
let currentBitrate = MAX_BITRATE;
setInterval(async () => {
const stats = await getConnectionStats(pc);
// 根据RTT和丢包调整码率
if (stats.rtt > 300 || stats.packetsLost > 5) {
currentBitrate = Math.max(currentBitrate * 0.8, MIN_BITRATE);
} else if (currentBitrate < MAX_BITRATE) {
currentBitrate = Math.min(currentBitrate * 1.2, MAX_BITRATE);
}
// 应用新参数
const parameters = sender.getParameters();
if (!parameters.encodings) {
parameters.encodings = [{}];
}
parameters.encodings[0].maxBitrate = currentBitrate;
sender.setParameters(parameters);
}, 5000);
}
// 使用示例
const videoSender = pc.getSenders().find(s => s.track.kind === 'video');
if (videoSender) {
setupBandwidthAdaptation(pc, videoSender);
}
实际应用案例
1. 视频会议系统
class VideoConference {
constructor() {
this.participants = new Map();
this.localStream = null;
this.signaling = new SignalingClient();
}
async start() {
try {
// 初始化本地媒体
this.localStream = await this.getUserMedia();
this.displayLocalStream();
// 连接信令服务器
await this.signaling.connect();
// 处理新参与者
this.signaling.on('new-peer', async (peerId) => {
const pc = this.createPeerConnection();
this.participants.set(peerId, pc);
// 添加本地轨道
this.localStream.getTracks().forEach(track => {
pc.addTrack(track, this.localStream);
});
// 创建offer
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
this.signaling.sendOffer(peerId, offer);
});
// 加入房间
this.signaling.joinRoom(this.roomId);
} catch (error) {
console.error('会议启动失败:', error);
}
}
createPeerConnection() {
const pc = new RTCPeerConnection(this.config);
pc.ontrack = (event) => {
this.displayRemoteStream(event.streams[0]);
};
pc.onicecandidate = (event) => {
if (event.candidate) {
this.signaling.sendICECandidate(event.candidate);
}
};
return pc;
}
displayRemoteStream(stream) {
const video = document.createElement('video');
video.autoplay = true;
video.playsInline = true;
video.srcObject = stream;
document.getElementById('remoteVideos').appendChild(video);
}
}
2. 远程桌面共享
// 屏幕捕获
async function startScreenShare() {
try {
const stream = await navigator.mediaDevices.getDisplayMedia({
video: {
width: { max: 1920 },
height: { max: 1080 },
frameRate: { ideal: 30 }
},
audio: false
});
const videoTrack = stream.getVideoTracks()[0];
videoTrack.onended = () => {
console.log('用户停止了屏幕共享');
stopScreenShare();
};
// 替换现有视频轨道
const sender = pc.getSenders().find(s => s.track.kind === 'video');
if (sender) {
sender.replaceTrack(videoTrack);
}
return stream;
} catch (err) {
console.error('屏幕共享失败:', err);
throw err;
}
}
// 鼠标键盘事件转发
function setupInputForwarding(dataChannel) {
document.addEventListener('mousemove', (e) => {
dataChannel.send(JSON.stringify({
type: 'mouse',
x: e.clientX,
y: e.clientY,
buttons: e.buttons
}));
});
document.addEventListener('keydown', (e) => {
dataChannel.send(JSON.stringify({
type: 'key',
key: e.key,
code: e.code,
ctrlKey: e.ctrlKey,
shiftKey: e.shiftKey
}));
});
}
安全与隐私考量
-
媒体访问权限:
- 明确向用户解释权限用途
- 处理权限拒绝情况
try { const stream = await navigator.mediaDevices.getUserMedia({ video: true }); } catch (err) { if (err.name === 'NotAllowedError') { showPermissionInstructions(); } } -
加密与安全:
- WebRTC强制使用SRTP加密媒体流
- 数据通道支持SCTP加密
- 始终使用HTTPS/WSS保护信令
-
隐私保护:
// 关闭摄像头/麦克风指示器 function stopMediaTracks(stream) { stream.getTracks().forEach(track => { track.enabled = false; track.stop(); }); }
WebRTC技术为Web平台带来了前所未有的实时通信能力,开发者应:
- 优先考虑用户体验和性能优化
- 实现完善的错误处理和回退机制
- 遵循安全最佳实践
- 针对移动设备进行特别优化
随着WebCodecs和WebTransport等新API的出现,WebRTC的能力边界仍在不断扩展,为开发更丰富的实时应用提供了可能。
#前端开发
分享于 2025-05-20