WebRTC 全称为:Web Real-Time Communication。它是为了解决 Web 端无法捕获音视频的能力,并且提供了 peer-to-peer(就是浏览器间)的视频交互。实际上,细分看来,它包含三个部分:
MediaStream:捕获音视频流
RTCPeerConnection:传输音视频流(一般用在 peer-to-peer 的场景)
RTCDataChannel: 用来上传音视频二进制数据(一般用到流的上传)
MediaStream用于获取摄像头,麦克风,或屏幕图像。
var constraints = {
audio: true,
video: true
}
navigator.mediaDevices.getUserMedia(constraints).then(handleSuccess).catch(console.log())
这是一个异步方法,获取设备成功之后执行回调函数handleSuccess,这个函数的参数是一个音视频流,在回调函数内部实现将音视频流放进video标签播放。
function handleSuccess(stream) {
document.getElementById("video").srcObject = stream
}
这就是最基本的获取音视频图像在浏览器进行播放。
RTCPeerConnection 用于建立连接进行UDP音视频传输(在用STUN服务器的情况下),在STUN防火墙打洞失败的情况下就需要使用TCP传输(需要TURN服务器作为中继进行转发),当然这些操作不需要自己来进行,webtrc的API已经帮我们实现,那就是ICE。
ICE:说白了ICE就是帮你选出你能支持的传输方式(可能有多种,会全部发送给对端,由对端根据自己也支持的传输方式选择最优的传输方式,这个过程就叫做网络协商),但是需要配置ICE的STUN服务器地址,和TURN的服务器地址。
除了网络协商还有媒体协商SDP,媒体协商就是告诉双方自己所支持的视频编解码能力,媒体格式等信息。这个过程webrtc的API也已经进行了封装,拿点对点传输来说,A方获取自己的SDP信息,设置自己的本地SDP,然后发送给B端(这个过程A端作为发起方,发送的是offer),B端收到A端的SPD(offer),B端保存对端的SPD,然后B端获取自己的SDP,保存自己的本地SPD,然后B端对A端的offer进行回应(就是将B端自己的SDP告诉A端,这个回应就是answer,其实也就是SDP),这时A端收到B端的回应,A端就会保存B端的SDP。
至此媒体协商完成。
上述的两个过程不分先后,但是都需要websocket作为中间服务器进行对话传输,实现方式有很多种,我是用python实现django+channels实现websocket。
前端代码如下(没有写过多的逻辑控制,所以需要先打开接收端,保证websocket在传输发起方的信息时房间里有接收方。)
我的websocket服务会转发自己的消息到自己(用于聊天室),所以这里加了判断,如果不判断,webrtc协商时会造成自己和自己协商。
接收端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>接收端</title>
</head>
<body>
<video id="video" autoplay style="height: 600px;width: 800px" muted></video>
<video id="video2" autoplay style="height: 600px;width: 800px"></video>
<script>
//生成唯一id,用于websocket判断是否是自己
//生成唯一ID
function guid() {
return Number(Math.random().toString().substr(3, 3) + Date.now()).toString(36);
}
var id = guid();
//首先建立websocket连接
socket = new WebSocket("ws://这里就用自己的websocket服务器地址/");
socket.onopen = onOpen;
socket.onclose = onClose;
// 连接成功
function onOpen() {
console.log("websocket连接成功")
}
function onClose() {
console.log("websocket已经断开")
}
function sendCandidate(ICE) {
socket.send(JSON.stringify({
'message': {
'ICE': ICE, 'ID': id}
}))
console.log("B端发送ICE到服务器")
}
function sendAnswer(answer) {
socket.send(JSON.stringify({
'message': {
'SDP': answer, 'ID': id}
}))
console.log("B端收到A端offer(SDP),回发answer(SDP)成功")
}
socket.onmessage = function (e) {
const data = JSON.parse(e.data).message;
if (data.ID != id) {
try {
var ice = data.ICE
remoteConnection.addIceCandidate(new RTCIceCandidate(ice))
console.log("收到远端ICE候选,并添加")
} catch (e) {
var sdp = data.SDP
let desc = new RTCSessionDescription(sdp)
remoteConnection.setRemoteDescription(desc).then(function () {
remoteConnection.createAnswer().then((answer) => {
remoteConnection.setLocalDescription(answer).then(sendAnswer(answer))
})
})
}
}
}
var iceServer = {
iceServer: [
{
url: "stun:stun.l.google.com:19302"},//谷歌的公共服务器
]
}
var remoteConnection
var constraints = {
audio: true,
video: true
}
navigator.mediaDevices.getUserMedia(constraints).then(handleSuccess).catch(console.log())
function handleSuccess(stream) {
document.getElementById("video").srcObject = stream
remoteConnection = new RTCPeerConnection(iceServer)
remoteConnection.addStream(stream)
remoteConnection.onaddstream = function (e) {
console.log('获得发起方的视频流' + e.stream)
document.getElementById("video2").srcObject = e.stream
}
remoteConnection.onicecandidate = function (event) {
if (event.candidate) {
sendCandidate(event.candidate)
}
}
}
</script>
</body>
</html>
发起端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>发起端</title>
</head>
<body>
<video id="video" autoplay style="height: 600px;width: 800px" muted></video>
<video id="video2" autoplay style="height: 600px;width: 800px"></video>
<script>
//生成唯一id,用于websocket判断是否是自己
//生成唯一ID
function guid() {
return Number(Math.random().toString().substr(3, 3) + Date.now()).toString(36);
}
var id = guid();
//首先建立websocket连接
socket = new WebSocket("ws://这里就用自己的websocket服务器地址/");
socket.onopen = onOpen;
socket.onclose = onClose;
// 连接成功
function onOpen() {
console.log("websocket连接成功")
}
// 关闭连接
function onClose() {
console.log("websocket已经断开")
}
//发送ICE
function sendCandidate(ICE) {
//获取到本地ice之后发送到websocket
socket.send(JSON.stringify({
'message': {
'ICE': ICE, 'ID': id}
}))
console.log("A端发送ICE到服务器")
}
//发送SDP
function sendOffer(offer) {
//设置完本地的SDP之后发送到websocket
socket.send(JSON.stringify({
'message': {
'SDP': offer, 'ID': id}
}))
console.log("A端发送offer(SDP)到服务器")
}
// 收到消息,判断消息类型是ICE还是SDP
socket.onmessage = function (e) {
const data = JSON.parse(e.data).message;
if (data.ID != id) {
try {
var ice = data.ICE
localConnection.addIceCandidate(new RTCIceCandidate(ice))
} catch (e) {
var sdp = data.SDP
let desc = new RTCSessionDescription(sdp) //构建SDP对象
localConnection.setRemoteDescription(desc).then(() => {
//设置远端SDP
console.log('端对端连接成功')
})
}
}
}
var iceServer = {
iceServer: [
{
url: "stun:stun.l.google.com:19302"},//谷歌的公共服务器
]
}
var localConnection
var constraints = {
audio: true,
video: true
}
navigator.mediaDevices.getUserMedia(constraints).then(handleSuccess).catch(console.log())
function handleSuccess(stream) {
document.getElementById("video").srcObject = stream
localConnection = new RTCPeerConnection(iceServer) //获取连接对象 ,应填写stun服务器
localConnection.addStream(stream) //将本地流添加进连接对象
localConnection.onaddstream = function (e) {
//获得对方流触发函数
console.log('获得应答方的视频流' + e.stream)
document.getElementById("video2").srcObject = e.stream
}
localConnection.onicecandidate = function (event) {
// 用来寻找合适的ICE,获得合适的ICE时触发
if (event.candidate) {
sendCandidate(event.candidate) // 通过websocket将ICE发送给端,自己实现的函数
console.log(event.candidate)
}
}
localConnection.createOffer().then((offer) => {
//创建offer 成功之后更改与对端关联的本地描述(SDP),包括本端属性包括媒体格式
localConnection.setLocalDescription(offer).then(sendOffer(offer)) // 之后发送SDP信息到对端,sendOffer自己实现
})
}
</script>
</body>
</html>
这个过程用一张图来说明就很直观了
在我使用的例子中STUN服务器用的是免费的,如需要TURN服务器可以用coturn实现(coturn包含了STUN和TURN实现)。