各位好,我是老码,今天咱们来聊聊WebRTC这玩意儿,这可是前端领域里为数不多的硬骨头之一,啃下来那叫一个香!
今天咱们的目标是:搞明白WebRTC是怎么实现点对点音视频通信和数据传输的。别怕,咱们不搞那些深奥的理论,直接上干货,代码说话!
一、WebRTC是个啥? 简单来说就是个“媒婆”
WebRTC,全称Web Real-Time Communication,直译过来就是“网页实时通信”。 这玩意儿可不是让你在浏览器里写QQ, 而是让你在浏览器里直接搞音视频聊天,文件传输,甚至游戏!
你可以把WebRTC想象成一个超级媒婆,它不负责帮你谈恋爱,但它负责帮你找到对象,然后让你们俩直接对话,它自己就功成身退了。
WebRTC的核心功能主要有三个:
- 获取音视频流(getUserMedia): 允许浏览器访问用户的摄像头和麦克风。
- 点对点连接(Peer-to-Peer Connection): 在浏览器之间建立直接的连接,减少延迟。
- 数据通道(Data Channel): 在浏览器之间传输任意数据,比如文本,文件等。
二、getUserMedia:先露个脸,亮个嗓
要聊天,首先得让对方看到你,听到你。 getUserMedia
就是干这事的,它会弹出个窗口,问你愿不愿意让浏览器访问你的摄像头和麦克风。
// 先检查浏览器是否支持getUserMedia
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
// 定义请求的媒体类型(音视频)
const constraints = {
audio: true, // 允许访问麦克风
video: { width: 640, height: 480 } // 允许访问摄像头,并指定分辨率
};
navigator.mediaDevices.getUserMedia(constraints)
.then(function(stream) {
// 获取到媒体流,把它显示在video标签里
const video = document.querySelector('video');
video.srcObject = stream;
video.onloadedmetadata = function(e) {
video.play();
};
})
.catch(function(err) {
// 用户拒绝授权,或者发生其他错误
console.log("访问媒体设备失败: " + err.name + ": " + err.message);
});
} else {
console.log("getUserMedia is not supported");
}
这段代码做了几件事:
- 检查支持性: 看看浏览器是不是支持
getUserMedia
,不支持就直接退出,省的白费力气。 - 定义约束: 告诉浏览器你想访问哪些媒体设备,以及一些参数,比如分辨率。
- 请求授权: 调用
getUserMedia
方法,浏览器会弹窗询问用户是否授权。 - 处理成功: 如果用户授权成功,你会得到一个
MediaStream
对象,这个对象包含了音视频数据,你可以把它赋值给一个<video>
标签的srcObject
属性,让视频显示出来。 - 处理失败: 如果用户拒绝授权,或者发生了其他错误,
catch
块会捕获错误,并输出到控制台。
三、PeerConnection:连接你我,心连心
RTCPeerConnection
是WebRTC的核心,它负责建立浏览器之间的点对点连接。 这个过程比较复杂,涉及到信令交换(Signaling),NAT穿透(Network Address Translation Traversal),ICE协商(Interactive Connectivity Establishment)等技术。
别慌,咱们一步步来。
1. 信令交换(Signaling)
WebRTC本身并不负责信令交换,你需要自己搭建一个信令服务器,用来交换一些信息,比如:
- 会话描述(Session Description): 包括音视频编解码器,传输协议等信息。
- ICE候选者(ICE Candidates): 包含了客户端的网络地址信息,用于NAT穿透。
信令服务器可以使用任何技术,比如WebSocket, Socket.IO, HTTP长轮询等。
咱们先假设你已经有了一个信令服务器,并且已经实现了基本的消息传递功能。
2. 创建PeerConnection对象
// 创建PeerConnection对象
const configuration = {
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] // 使用Google的STUN服务器
};
const peerConnection = new RTCPeerConnection(configuration);
这里创建了一个 RTCPeerConnection
对象,并配置了一个STUN服务器。 STUN服务器的作用是帮助客户端发现自己的公网IP地址和端口,用于NAT穿透。
3. 创建Offer (发起方)
发起方需要创建一个 Offer,描述自己的音视频能力。
peerConnection.createOffer()
.then(function(offer) {
// 设置本地会话描述
return peerConnection.setLocalDescription(offer);
})
.then(function() {
// 将Offer发送给对方(通过信令服务器)
sendMessage({ type: 'offer', sdp: peerConnection.localDescription });
})
.catch(function(err) {
console.log("创建Offer失败: " + err.message);
});
createOffer()
方法会创建一个SDP (Session Description Protocol) 对象,描述了自己的音视频能力。setLocalDescription()
方法会将这个SDP设置为本地会话描述。- 然后你需要将这个SDP发送给对方,告诉对方你的能力。
4. 接收Answer (接收方)
接收方收到Offer后,需要设置远程会话描述,并创建Answer。
// 收到Offer消息
function handleOffer(message) {
peerConnection.setRemoteDescription(new RTCSessionDescription(message.sdp))
.then(function() {
// 创建Answer
return peerConnection.createAnswer();
})
.then(function(answer) {
// 设置本地会话描述
return peerConnection.setLocalDescription(answer);
})
.then(function() {
// 将Answer发送给对方(通过信令服务器)
sendMessage({ type: 'answer', sdp: peerConnection.localDescription });
})
.catch(function(err) {
console.log("处理Offer失败: " + err.message);
});
}
setRemoteDescription()
方法会将收到的Offer设置为远程会话描述。createAnswer()
方法会创建一个Answer,描述了自己的音视频能力,并和Offer进行协商。setLocalDescription()
方法会将这个Answer设置为本地会话描述。- 然后你需要将这个Answer发送给对方,告诉对方你的能力。
5. 设置远程会话描述 (发起方)
发起方收到Answer后,需要设置远程会话描述。
// 收到Answer消息
function handleAnswer(message) {
peerConnection.setRemoteDescription(new RTCSessionDescription(message.sdp))
.catch(function(err) {
console.log("处理Answer失败: " + err.message);
});
}
6. 交换ICE Candidates
在建立连接的过程中,双方需要交换ICE Candidates,用于NAT穿透。
// 监听icecandidate事件
peerConnection.onicecandidate = function(event) {
if (event.candidate) {
// 将ICE Candidate发送给对方(通过信令服务器)
sendMessage({ type: 'candidate', candidate: event.candidate });
}
};
// 收到ICE Candidate消息
function handleCandidate(message) {
const candidate = new RTCIceCandidate(message.candidate);
peerConnection.addIceCandidate(candidate)
.catch(function(err) {
console.log("添加ICE Candidate失败: " + err.message);
});
}
onicecandidate
事件会在收集到新的ICE Candidate时触发。- 你需要将ICE Candidate发送给对方,让对方添加到自己的PeerConnection对象中。
addIceCandidate()
方法会将ICE Candidate添加到PeerConnection对象中。
7. 添加媒体流
连接建立成功后,需要将媒体流添加到PeerConnection对象中。
// 将本地媒体流添加到PeerConnection对象
localStream.getTracks().forEach(track => {
peerConnection.addTrack(track, localStream);
});
// 监听ontrack事件,当收到远程媒体流时触发
peerConnection.ontrack = function(event) {
const remoteVideo = document.querySelector('#remoteVideo');
remoteVideo.srcObject = event.streams[0];
};
addTrack()
方法会将媒体流的轨道添加到PeerConnection对象中。ontrack
事件会在收到远程媒体流时触发。- 你可以将远程媒体流赋值给一个
<video>
标签的srcObject
属性,让视频显示出来。
四、Data Channel:不只是聊天,还能传文件!
WebRTC的Data Channel允许你在浏览器之间传输任意数据,比如文本,文件,游戏数据等。
1. 创建Data Channel
// 创建Data Channel (发起方)
const dataChannel = peerConnection.createDataChannel('myChannel', {
ordered: true, // 保证消息的顺序
maxRetransmits: 3 // 最大重传次数
});
dataChannel.onopen = function() {
console.log("Data Channel已打开");
};
dataChannel.onmessage = function(event) {
console.log("收到消息: " + event.data);
};
dataChannel.onerror = function(error) {
console.log("Data Channel发生错误: " + error);
};
dataChannel.onclose = function() {
console.log("Data Channel已关闭");
};
// 监听ondatachannel事件 (接收方)
peerConnection.ondatachannel = function(event) {
const dataChannel = event.channel;
dataChannel.onopen = function() {
console.log("Data Channel已打开");
};
dataChannel.onmessage = function(event) {
console.log("收到消息: " + event.data);
};
dataChannel.onerror = function(error) {
console.log("Data Channel发生错误: " + error);
};
dataChannel.onclose = function() {
console.log("Data Channel已关闭");
};
};
createDataChannel()
方法会创建一个Data Channel。ondatachannel
事件会在收到对方创建的Data Channel时触发。ordered
选项可以保证消息的顺序。maxRetransmits
选项可以设置最大重传次数。
2. 发送数据
// 发送消息
dataChannel.send("Hello, world!");
send()
方法可以发送数据。
五、 完整代码示例 (简化版)
为了方便理解,这里提供一个简化版的代码示例,只包含核心逻辑,省略了信令服务器的实现。
发起方 (Caller):
<!DOCTYPE html>
<html>
<head>
<title>WebRTC Demo (Caller)</title>
</head>
<body>
<h1>Caller</h1>
<video id="localVideo" autoplay muted></video>
<video id="remoteVideo" autoplay></video>
<button id="startButton">Start</button>
<button id="callButton">Call</button>
<button id="hangupButton">Hangup</button>
<script>
const startButton = document.querySelector('#startButton');
const callButton = document.querySelector('#callButton');
const hangupButton = document.querySelector('#hangupButton');
const localVideo = document.querySelector('#localVideo');
const remoteVideo = document.querySelector('#remoteVideo');
let localStream;
let peerConnection;
let dataChannel;
startButton.addEventListener('click', start);
callButton.addEventListener('click', call);
hangupButton.addEventListener('click', hangup);
async function start() {
try {
localStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
localVideo.srcObject = localStream;
} catch (e) {
console.error('getUserMedia() error:', e);
}
}
async function call() {
callButton.disabled = true;
startButton.disabled = true;
const configuration = {
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
};
peerConnection = new RTCPeerConnection(configuration);
peerConnection.onicecandidate = event => {
console.log('ICE candidate:', event.candidate);
// 在这里你需要通过信令服务器将candidate发送给Callee
// 这里为了简化,直接使用console.log模拟
};
peerConnection.ontrack = event => {
remoteVideo.srcObject = event.streams[0];
};
localStream.getTracks().forEach(track => {
peerConnection.addTrack(track, localStream);
});
// 创建Data Channel
dataChannel = peerConnection.createDataChannel('myChannel', {
ordered: true,
maxRetransmits: 3
});
dataChannel.onopen = () => {
console.log("Data Channel opened");
dataChannel.send("Hello from Caller!");
};
dataChannel.onmessage = event => {
console.log("Message from Data Channel: " + event.data);
};
dataChannel.onerror = error => {
console.error("Data Channel error:", error);
};
dataChannel.onclose = () => {
console.log("Data Channel closed");
};
try {
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
console.log('Created offer:', offer);
// 在这里你需要通过信令服务器将offer发送给Callee
// 这里为了简化,直接使用console.log模拟
} catch (e) {
console.error('Failed to create offer:', e);
}
}
function hangup() {
console.log('Ending call');
if (peerConnection) {
peerConnection.close();
}
hangupButton.disabled = true;
callButton.disabled = false;
startButton.disabled = false;
}
</script>
</body>
</html>
接收方 (Callee):
<!DOCTYPE html>
<html>
<head>
<title>WebRTC Demo (Callee)</title>
</head>
<body>
<h1>Callee</h1>
<video id="localVideo" autoplay muted></video>
<video id="remoteVideo" autoplay></video>
<button id="startButton">Start</button>
<button id="answerButton">Answer</button>
<button id="hangupButton">Hangup</button>
<script>
const startButton = document.querySelector('#startButton');
const answerButton = document.querySelector('#answerButton');
const hangupButton = document.querySelector('#hangupButton');
const localVideo = document.querySelector('#localVideo');
const remoteVideo = document.querySelector('#remoteVideo');
let localStream;
let peerConnection;
startButton.addEventListener('click', start);
answerButton.addEventListener('click', answer);
hangupButton.addEventListener('click', hangup);
async function start() {
try {
localStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
localVideo.srcObject = localStream;
} catch (e) {
console.error('getUserMedia() error:', e);
}
}
async function answer() {
answerButton.disabled = true;
startButton.disabled = true;
const configuration = {
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
};
peerConnection = new RTCPeerConnection(configuration);
peerConnection.onicecandidate = event => {
console.log('ICE candidate:', event.candidate);
// 在这里你需要通过信令服务器将candidate发送给Caller
// 这里为了简化,直接使用console.log模拟
};
peerConnection.ontrack = event => {
remoteVideo.srcObject = event.streams[0];
};
localStream.getTracks().forEach(track => {
peerConnection.addTrack(track, localStream);
});
peerConnection.ondatachannel = event => {
const dataChannel = event.channel;
dataChannel.onopen = () => {
console.log("Data Channel opened");
};
dataChannel.onmessage = event => {
console.log("Message from Data Channel: " + event.data);
};
dataChannel.onerror = error => {
console.error("Data Channel error:", error);
};
dataChannel.onclose = () => {
console.log("Data Channel closed");
};
};
// 在这里你需要通过信令服务器接收到Caller发送的offer
// 这里为了简化,直接使用硬编码模拟
const offer = {
type: 'offer',
sdp: '请将Caller控制台输出的offer.sdp复制到这里'
};
try {
await peerConnection.setRemoteDescription(offer);
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
console.log('Created answer:', answer);
// 在这里你需要通过信令服务器将answer发送给Caller
// 这里为了简化,直接使用console.log模拟
} catch (e) {
console.error('Failed to create answer:', e);
}
}
function hangup() {
console.log('Ending call');
if (peerConnection) {
peerConnection.close();
}
hangupButton.disabled = true;
answerButton.disabled = false;
startButton.disabled = false;
}
</script>
</body>
</html>
使用方法:
- 分别打开
Caller.html
和Callee.html
两个页面。 - 在两个页面分别点击 "Start" 按钮,允许访问摄像头和麦克风。
- 复制
Caller
控制台输出的offer.sdp
内容。 - 粘贴到
Callee
代码中offer
对象的sdp
属性值的位置。 - 在
Callee
页面点击 "Answer" 按钮。 - 如果一切顺利,你应该可以看到两个页面互相显示对方的视频了,并且在控制台中可以看到Data Channel的消息。
注意:
- 这个示例没有实现信令服务器,你需要自己搭建一个,或者使用一些现成的信令服务器解决方案,比如 Socket.IO, Firebase 等。
- 由于没有信令服务器,ICE Candidates也无法交换,所以NAT穿透可能会失败,导致无法建立连接。
六、WebRTC的优势与劣势
特性 | 优势 | 劣势 |
---|---|---|
点对点连接 | 延迟低,实时性高,适用于实时音视频通信和数据传输。 | NAT穿透复杂,需要STUN/TURN服务器辅助,成功率受网络环境影响。 |
浏览器支持 | 现代浏览器普遍支持,无需安装插件。 | 不同浏览器和版本可能存在兼容性问题,需要进行适配。 |
安全性 | 使用DTLS和SRTP等加密协议,保证通信安全。 | 需要注意信令服务器的安全,防止中间人攻击。 |
灵活性 | 可以自定义音视频编解码器,传输协议,以及数据通道的参数。 | 配置复杂,需要对WebRTC的底层原理有一定的了解。 |
扩展性 | 可以通过Data Channel实现各种应用,比如文件传输,游戏数据传输,远程控制等。 | 对网络带宽和设备性能有一定要求,在高并发场景下需要进行优化。 |
七、总结
WebRTC 是一项强大的技术,可以让你在浏览器中实现实时音视频通信和数据传输。 虽然学习曲线比较陡峭,但只要掌握了核心概念和API,就可以开发出各种有趣的应用。
希望今天的分享对你有所帮助,下次再见!