(一)WebRTC 通过 HTTP 交换信令的处理流程
1. WebRTC 基本介绍
WebRTC (Web Real-Time Communication) 是一种支持浏览器进行实时媒体通信的技术,允许音视频通话和数据共享,而不需要安装额外的插件。为了使 WebRTC 进行 P2P(点对点)通信,必须通过信令服务来交换 SDP
(Session Description Protocol)和 ICE(Interactive Connectivity Establishment)候选者信息。接下来介绍如何使用 HTTP 来交换 WebRTC 信令。
2. 创建端和接收端的概念
- 创建端(Offerer):发送
SDP offer
的一方。 - 接收端(Answerer):接受并回复
SDP answer
的一方。
在 WebRTC 中,创建端首先创建一个 offer
,通过 HTTP 请求发送到信令服务器,接收端接收到 offer
后,生成 answer
并返回给创建端,完成连接。
3. 创建端的代码示例
// 创建 RTCPeerConnection 对象
let pc = new RTCPeerConnection();
// 当接收到远端的媒体流时,设置到本地的 video 元素中
pc.ontrack = function(event) {
document.querySelector("video").srcObject = event.streams[0];
};
// 添加接收音频和视频的 Transceivers
pc.addTransceiver("audio", { direction: "recvonly" });
pc.addTransceiver("video", { direction: "recvonly" });
// 创建 SDP Offer
pc.createOffer().then(offer => {
return pc.setLocalDescription(offer).then(() => {
return offer;
});
}).then(offer => {
// 将 SDP Offer 通过 HTTP 发送到信令服务器
return new Promise((resolve, reject) => {
let url = "http://signaling-server-ip";
let data = {
api: url,
streamurl: "stream-url",
clientip: null,
sdp: offer.sdp
};
// 发送 HTTP POST 请求
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
}).then(response => response.json())
.then(res => resolve(res.sdp))
.catch(reject);
});
}).then(answerSdp => {
// 设置远端 SDP Answer
return pc.setRemoteDescription(new RTCSessionDescription({
type: "answer",
sdp: answerSdp
}));
}).catch(error => {
console.error("Error during WebRTC negotiation:", error);
});
4. 接收端的代码示例
接收端的处理逻辑包括接收 offer
,生成 answer
并发送回创建端。
// 接收端的 RTCPeerConnection 配置
let pc = new RTCPeerConnection();
// 当接收到远端的媒体流时,处理媒体流
pc.ontrack = function(event) {
document.querySelector("video").srcObject = event.streams[0];
};
// 接收 SDP Offer,并设置为远端描述
fetch('http://signaling-server-ip', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ streamurl: 'stream-url' })
}).then(response => response.json())
.then(offerSdp => {
return pc.setRemoteDescription(new RTCSessionDescription({
type: "offer",
sdp: offerSdp
}));
}).then(() => {
// 创建 SDP Answer 并设置本地描述
return pc.createAnswer();
}).then(answer => {
return pc.setLocalDescription(answer).then(() => {
return answer;
});
}).then(answer => {
// 发送 SDP Answer 回到创建端
return fetch('http://signaling-server-ip', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
sdp: answer.sdp,
streamurl: 'stream-url'
})
});
}).catch(error => {
console.error("Error during WebRTC negotiation:", error);
});
5. 说明
RTCPeerConnection
RTCPeerConnection
是 WebRTC 中核心的对象,用于创建、保持和监控 P2P 连接。- 在本例中,我们设置了
ontrack
事件监听器来接收远程媒体流,并通过srcObject
将其传输到 HTMLvideo
元素。
Transceivers
addTransceiver
用于在 P2P 连接中添加发送或接收音频和视频的功能。- 我们使用了
recvonly
模式,表示该端只接收媒体流。
Signaling Server
- 信令服务器在本例中通过 HTTP POST 请求来实现 SDP 的交换。
createOffer
和createAnswer
分别创建了offer
和answer
,通过 HTTP 发送和接收。
6. 完整流程示意图
7. 运行步骤
- 创建端:运行创建端的代码来生成
offer
并通过 HTTP 发送到信令服务器。 - 接收端:接收端的代码运行后,接收
offer
,生成answer
,并通过 HTTP 返回。 - 连接成功:创建端和接收端各自设置远程描述,成功建立 P2P 连接。
问题1:接收端如何知道来自于创建端的连接请求?
上面通过http来交换信令的方式存在一个很大的问题,在 WebRTC 信令过程中,接收端确实需要通过某种方式来知道何时接收到创建端发来的 SDP offer
。通常情况下,接收端并不会主动发送 HTTP 请求,而是通过信令服务器被动接收 SDP offer
。在我的代码示例中,接收端发送 HTTP 请求的设计并不准确,因为它应该由信令服务器来通知接收端,而不是接收端主动获取 SDP offer
。
(二)WebRTC 通过 WebSocket 交换信令的处理流程
正确的信令逻辑
接收端不应该主动发送 HTTP 请求来获取 offer
,而是由信令服务器通知接收端。例如,通过轮询、WebSocket 或其他实时通信机制,接收端会在信令服务器通知它收到 SDP offer
时,开始处理。然后,接收端在处理完 offer
后,将 SDP answer
发送回信令服务器。
1. 通过 WebSocket 实现更合适的信令
相较于 HTTP 请求,更推荐使用 WebSocket 来进行实时信令传输。WebSocket 能够实现双向通信,信令服务器可以实时推送 SDP offer
给接收端,接收端处理后将 SDP answer
发送回去。
修改后的信令流程
- 创建端:生成
SDP offer
,并通过信令服务器发送给接收端。 - 信令服务器:将
offer
转发给接收端。 - 接收端:接收到
offer
后,生成SDP answer
,并发送回信令服务器。 - 信令服务器:将
answer
转发给创建端。
2. 创建端的代码
以下代码展示了如何使用 WebSocket 来实现创建端(Offerer),生成 SDP offer
并发送给信令服务器。
// 创建 WebSocket 连接到信令服务器
const signalingSocket = new WebSocket("ws://signaling-server-ip");
const configuration = { iceServers: [{ urls: "stun:stun.l.google.com:19302" }] };
const pc = new RTCPeerConnection(configuration);
// 获取本地媒体流
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then(stream => {
document.querySelector("#localVideo").srcObject = stream;
stream.getTracks().forEach(track => pc.addTrack(track, stream));
}).catch(error => {
console.error("Error accessing media devices.", error);
});
// 处理 ICE 候选
pc.onicecandidate = (event) => {
if (event.candidate) {
// 发送 ICE 候选给远端
signalingSocket.send(JSON.stringify({
type: 'ice-candidate',
candidate: event.candidate
}));
}
};
// 当接收到远端的媒体流时,设置到本地的 video 元素中
pc.ontrack = function(event) {
document.querySelector("video").srcObject = event.streams[0];
};
// 添加音频和视频 Transceivers
pc.addTransceiver("audio", { direction: "recvonly" });
pc.addTransceiver("video", { direction: "recvonly" });
// WebSocket 连接成功后,生成 SDP offer 并发送到信令服务器
signalingSocket.onopen = function() {
pc.createOffer().then(offer => {
return pc.setLocalDescription(offer).then(() => {
// 通过 WebSocket 发送 SDP offer 到信令服务器
signalingSocket.send(JSON.stringify({
type: "offer",
sdp: offer.sdp
}));
});
}).catch(error => {
console.error("Error creating offer:", error);
});
};
// 接收来自信令服务器的 SDP answer,并设置为远端描述
signalingSocket.onmessage = function(event) {
let data = JSON.parse(event.data);
if (data.type === "answer") {
pc.setRemoteDescription(new RTCSessionDescription({
type: "answer",
sdp: data.sdp
})).then(() => {
console.log("Successfully set remote description.");
}).catch(error => {
console.error("Error setting remote description:", error);
});
}
// 处理来自创建端的 ICE 候选信息
else if (data.type === 'ice-candidate') {
pc.addIceCandidate(new RTCIceCandidate(data.candidate));
}
};
// 处理 WebSocket 连接关闭和错误
signalingSocket.onclose = function() {
console.log("WebSocket connection closed.");
};
signalingSocket.onerror = function(error) {
console.error("WebSocket error:", error);
};
2.1 创建端流程分析
- RTCPeerConnection:用于管理和协商点对点连接。
- createOffer():创建一个 SDP offer,并通过
setLocalDescription
设置本地描述。 - WebSocket 发送 offer:通过 WebSocket 连接将
offer
发送到信令服务器。 - 接收 SDP answer:通过 WebSocket 接收接收端生成的
SDP answer
,并将其设置为远端描述。
3. 接收端的代码
以下是接收端(Answerer)的代码,展示了如何通过 WebSocket 接收 SDP offer
,生成 SDP answer
,并返回给信令服务器。
// WebSocket 连接到信令服务器
const signalingSocket = new WebSocket("ws://signaling-server-ip");
const configuration = { iceServers: [{ urls: "stun:stun.l.google.com:19302" }] };
const pc = new RTCPeerConnection(configuration);
// 获取本地媒体流
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then(stream => {
document.querySelector("#localVideo").srcObject = stream;
stream.getTracks().forEach(track => pc.addTrack(track, stream));
}).catch(error => {
console.error("Error accessing media devices.", error);
});
// 处理 ICE 候选
pc.onicecandidate = (event) => {
if (event.candidate) {
signalingSocket.send(JSON.stringify({
type: 'ice-candidate',
candidate: event.candidate
}));
}
};
// 当接收到远端媒体流时,处理媒体流
pc.ontrack = function(event) {
document.querySelector("video").srcObject = event.streams[0];
};
// 当 WebSocket 接收到消息时,处理 SDP offer
signalingSocket.onmessage = function(event) {
let data = JSON.parse(event.data);
if (data.type === 'offer') {
// 设置远端 SDP offer
pc.setRemoteDescription(new RTCSessionDescription({
type: 'offer',
sdp: data.sdp
})).then(() => {
// 创建 SDP answer
return pc.createAnswer();
}).then(answer => {
// 设置本地 SDP answer
return pc.setLocalDescription(answer).then(() => {
return answer;
});
}).then(answer => {
// 通过 WebSocket 发送 SDP answer 回信令服务器
signalingSocket.send(JSON.stringify({
type: 'answer',
sdp: answer.sdp
}));
}).catch(error => {
console.error("Error during WebRTC negotiation:", error);
});
}
// 处理来自创建端的 ICE 候选信息
else if (data.type === 'ice-candidate') {
pc.addIceCandidate(new RTCIceCandidate(data.candidate));
}
};
3.1 接收端流程分析
- RTCPeerConnection:同样用于管理 P2P 连接。
- 接收 SDP offer:通过 WebSocket 接收来自创建端的 SDP offer,并将其设置为远端描述。
- createAnswer():生成一个 SDP answer 并通过 WebSocket 返回给信令服务器。
- WebSocket 发送 answer:将 answer 通过 WebSocket 发送回创建端。
4 说明
- 上面代码主要目的在说明基本的流程,具体的逻辑需要根据需要来调整,比如接收端的 pc 创建可以在 socket 接收到消息之后;比如 ice 候选项发送可以在 socket 创建完成之后 open 事件发生之后 等等,总而言之,具体逻辑需要自己调整
- 本文主要在于介绍网页端的创建接收实现代码,这是需要与信令服务器配合定协议的,本文也不涉及信令服务器的代码。
唯一标识符?
其实到这里也还是有问题的,创建端和接收端如何进行匹配的,简单说就是 clientA 找到 clientB,而不是找到 clientC,反之,clientB 怎么就知道 回复给 clientA?
这里的关键也还是信令服务器的中转,实现方法各异,下面提供一种常见思路;
创建端与指定接收端进行通信的关键在于使用唯一标识符(如房间号或用户 ID)来进行匹配和路由。简要流程如下:
-
创建端加入房间:创建端通过 WebSocket 向信令服务器发送一个请求,指定一个房间号或用户 ID,表示要加入这个房间。
-
信令服务器匹配房间:信令服务器根据房间号或用户 ID 来匹配已经加入的接收端。如果有接收端已经在这个房间,服务器会将创建端的信令(例如
SDP offer
)转发给该接收端。 -
接收端接收信令:接收端加入相同房间后,信令服务器将创建端的信令(如
SDP offer
)发送给接收端。 -
接收端发送响应:接收端生成
SDP answer
并通过信令服务器发送回创建端。
通过这种方式,信令服务器充当中间人角色,利用唯一标识符将创建端和接收端正确匹配并建立通信。
总结
通过 WebSocket 来实现 WebRTC 的信令交换,能有效提高实时性和减少通信延迟。信令服务器在这个过程中承担了创建端和接收端之间信令的中转角色,WebSocket 的全双工通信使得信令交换变得更加流畅。
拓展
通过 WebRTC
可以实现 A 与 B 建立音视频、数据的交换等基本操作。但随着对即时通讯
场景的发展与要求,比如音频、视频通话、即时消息、在线游戏和其他多媒体通信形式、视频会议的创建与管理等。这时候信令服务器就会越来越复杂,这时候就需要一种会话协议来进行管理与信令的处理。目前应用最多的是 SIP 协议
:
SIP(Session Initiation Protocol,会话发起协议)
是一种用于建立、修改和终止多媒体通信会话的应用层协议WebRTC
主要用于基于浏览器操纵音视频媒体,进行实时通信。SIP
是个信令协议,其本身并不实现实时音视频通信,它只是用于建立用于实时音视频通信的会话。
因此 SIP
与 WebRTC
常常配合使用,在浏览器中实现 SIP 协议,支持 WebRTC 的开源库有如 SipML5、sip.js等。。。