在本文中,我们将编写一个可在网络浏览器中运行的简单、功能齐全的 WebRTC 会议应用程序。我们将通过创建对等连接、添加媒体轨道、信令、SDP 交换和 ICE 协商来设置会议应用程序。
在本系列的第 1 部分《搭建WebRTC视频会议应用系列1:WebRTC架构》,我们讨论了 WebRTC 架构及其组件。我们还了解了基本的应用程序工作流程。我们将借鉴文章中的概念,实现一个正常运行的会议应用程序。这个示例应用程序的屏幕截图如下:
开始编码
我们将按照以下步骤创建会议应用程序。本节其余部分将对每个步骤进行更深入的讨论。
- 为应用程序创建布局
- 创建 RTCPeerConnection 对象
- 添加视频和音频轨道
- 信令
- SDP 交换
- ICE 协商
1. 创建布局
让我们来想想需要什么样的界面。至少,需要一个窗口来看到对方。此外,如果在视频通话时也能看到自己就更好了。此外,还需要两个参与者共享一个会议 ID。除此以外,我们还将为 SDP 角色添加另一个下拉菜单。这些信息有助于稍后描述的 SDP 协商过程。
因此,我们将在用户界面中使用以下组件:
- 远程视频 – 显示对方视频的视频元素
- 本地视频 – 一个视频元素,用于显示从我们的摄像头捕获的视频
- 会议 ID – 用于共享 ID 的文本输入,两人可使用该 ID 加入通话。
- 角色 – 一个下拉菜单,用于帮助发送信号以建立 WebRTC 连接(稍后将深入讨论)。
以下是将这些元素添加到用户界面的代码:
<input type="text" id="meetingId">
<select id="role">
<option value="initiator">WebRTC SDP Initiator</option>
<option value="receiver">WebRTC SDP Receiver</option>
</select>
<button id="joinButton">Join Conference</button>
<div class="video-container">
<div class="local-video">
<h2>Local Video</h2>
<video id="localVideo" autoplay muted></video>
</div>
<div class="remote-video">
<h2>Remote Video</h2>
<video id="remoteVideo" autoplay></video>
</div>
</div>
接下来,我们应该应用 CSS 规则来组织用户界面,使其看起来更美观。
2. 创建 RTCPeerConnection 对象
现在布局已经准备就绪,我们将开始实际使用 WebRTC API。RTCPeerConnection 是其中的第一个,它提供了在两个对等方之间流式传输媒体的核心 API。将有两个 RTCPeerConnection 对象,每个对等设备中都有一个,它们将在它们之间建立连接,并将媒体轨迹从一个对等设备流向另一个对等设备。
创建对等连接对象非常简单,可以使用以下代码完成:
const configuration = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] };
let rtcConnection = new RTCPeerConnection(configuration);
在这里,在初始化 RTCPeerConnection 时传递一个 STUN 服务器列表作为配置。STUN 服务器有助于在对等连接对象之间建立连接,这将在本文后面讨论。在初始化 RTCPeerConnection 对象时,还可以传递更多配置选项。我们将在以后的文章中讨论这些选项。
3. 将媒体轨道添加到 RTCPeerConnection
接下来,我们将在连接中添加音频和视频,以便对方可以接收。MediaDevices 接口提供了一个简单的应用程序接口,用于请求用户对摄像头/麦克风的使用权限,一旦获得许可,就会返回一个 MediaStream 对象。一个 MediaStream 对象由一个或多个 MediaStreamTrack 对象组成,这些对象是同步的,并与同一个信号源相关。例如,会议应用程序的每个对等设备的媒体流中都会有一个音频和一个视频 MediaStreamTrack。获得 MediaStream 后,我们需要将各个轨道添加到 RTCPeerConnection 对象中。最后一步,我们还要将媒体流添加到 localVideo HTML 元素中,以便渲染本地视频。
// Request access to user's camera and microphone
localStream = await navigator.mediaDevices.getUserMedia(
{ audio: true, video: true }
);
// Add local media stream tracks to the connection
localStream.getTracks().forEach((track) => {
rtcConnection.addTrack(track, localStream);
});
// Display local video stream in the local video element
const localVideo = document.getElementById('localVideo');
localVideo.srcObject = localStream;
我们已在连接对象中添加了本地轨道,但如果要显示其他参与者的视频,还需要接收远程对等对象的轨道信息。RTCPeerConnection 对象通过轨道事件提供这些信息。让我们通过为 track 事件附加事件监听器来捕捉这些远程轨道,并将轨道添加到 remoteVideo HTML 元素中。
// Set remote video stream as the source for the remote video element
rtcConnection.addEventListener('track', (event) => {
const remoteVideo = document.getElementById('remoteVideo');
if (event.track.kind === 'video') {
remoteVideo.srcObject = event.streams[0];
}
});
4. 信令
在设置会议应用程序的下两个步骤中,我们需要在两个对等方之间交换设置信息。这种交换连接相关信息的过程在 WebRTC 领域称为信令。由于对等方之间的连接尚未建立,我们需要在带外进行信令处理。在进入 SDP 和 ICE 之前,让我们先简单讨论一下信令架构。
本文将使用 MQTT 协议进行信令传送。但我们也可以使用任何其他协议/机制来代替 MQTT。信令所需的一切就是能够快速、可靠地向对方发送信息。我之所以选择 MQTT,是因为它是一个简单轻量级的发布-子协议,而且有一个免费托管的 HiveMQ 服务器,我可以毫不费力地使用它。
MQTT 使用发布子模型在客户端之间交换消息。客户端使用的主题是在发布或订阅后即时创建的。首先,在 HTML 文件中加入 MQTT 库,如下所示:
<script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>
加入程序库后,我们与 MQTT 代理建立连接,并订阅主题,如下所示:
const mqttBroker = 'wss://mqtt-dashboard.com:8884/mqtt';
const mqttTopicPrefix = 'webrtc/conference/'; // Replace with your MQTT topic
let mqttClient;
const meetingId = document.getElementById('meetingId').value;
// Connect to the MQTT broker
mqttClient = mqtt.connect(mqttBroker);
mqttTopic = mqttTopicPrefix + meetingId
// Handle MQTT connection
mqttClient.on('connect', () => {
console.log('Connected to MQTT broker');
// Subscribe to the MQTT topic
mqttClient.subscribe(mqttTopic);
});
这里我们在主题名称中包含会议 ID,以便使用会议 ID 正确连接两个与会者。让我们看看在 MQTT 主题上发布消息的代码段。
// Function to send MQTT message
function sendMqttMessage(payload) {
const meetingId = document.getElementById('meetingId').value;
const role = document.getElementById('role').value;
const mqttTopic = mqttTopicPrefix + meetingId
payload.role = role;
mqttClient.publish(mqttTopic, JSON.stringify(payload));
}
如果你注意到,我们实际上是在使用同一个主题来发布会议两个参与者的信息。由于两个同行都发布和订阅同一个主题,因此区分来自另一个同行的消息至关重要。为此,我们为每条 MQTT 消息附加了 self 角色。接收消息时,我们会检查消息中的角色是否与 self 角色匹配,如果匹配则忽略消息。
// message event is triggered on every new message on the subscrived topic
mqttClient.on('message', handleMqttMessage);
function handleMqttMessage(topic, message) {
const payload = JSON.parse(message);
const role = document.getElementById('role').value
// Ignore self messages
if(payload.role === role) return;
if (payload.type === 'sdp') {
handleSdpMessage(payload.sdp);
} else if (payload.type === 'ice') {
handleIceCandidate(payload.candidate);
}
}
5. SDP 交换
SDP 交换是连接建立过程的第一步。SDP 交换过程包括从发起方对等端生成一个要约,接收方对等端接收该要约并生成一个应答,然后将应答发送回发起方对等端。由于这种要约-应答协议,我们需要使用角色下拉元素来区分发起方和接收方。
negotiationneeded 事件是 RTCPeerConnection 发出的指示,表明我们可以继续进行 SDP 交换。我们将为该事件附加一个事件监听器,如果角色是发起方,则发送 SDP 要约。这里需要注意的一点是,每个对等端都会将自己的 SDP 设置为本地描述,而将从另一个对等端收到的 SDP 设置为远程描述。这是根据对等方的能力协商媒体协议所必需的。
// Send SDP offer to the receiver
rtcConnection.addEventListener('negotiationneeded', async () => {
const role = document.getElementById('role').value;
if (role === 'receiver') return;
try {
// Create offer and set local description
const offer = await rtcConnection.createOffer();
await rtcConnection.setLocalDescription(offer);
console.log('SDP offer created and set as local description');
// Send offer to the receiver
sendMqttMessage({
meetingId: document.getElementById('meetingId').value,
type: 'sdp',
sdp: rtcConnection.localDescription.sdp
});
} catch (error) {
console.error('Error creating SDP offer:', error);
}
});
SDP 信息包含有关添加了哪些媒体轨道以及支持哪些协议来传输媒体的信息。例如,SRTP 用于传输音频和视频,而 SCTP 用于传输其他数据。SDP 还包含进一步配置这些协议的各种参数。
接收方对等体收到 SDP 消息后,会将 SDP 设置为远程描述,并生成 SDP 应答。生成的 SDP 答案被设置为接收方对等体的本地描述并传回给发起方对等体,发起方对等体将答案设置为远程描述。以下代码通过启动程序和接收程序中的信令处理 SDP 消息。
role = document.getElementById('role').value
if (role === 'initiator') {
await rtcConnection.setRemoteDescription({ type: 'answer', sdp });
console.log('SDP answer set as remote description');
} else if (role === 'receiver') {
await rtcConnection.setRemoteDescription({ type: 'offer', sdp });
console.log('SDP offer set as remote description');
// Create answer and set local description
const answer = await rtcConnection.createAnswer();
await rtcConnection.setLocalDescription(answer);
console.log('SDP answer created and set as local description');
// Send answer to the initiator
sendMqttMessage({
meetingId: document.getElementById('meetingId').value,
type: 'sdp',
role: document.getElementById('role').value,
sdp: rtcConnection.localDescription.sdp
});
}
这里需要注意的一点是,由于使用 MQTT 作为信令传输,接收方应首先启动并订阅 MQTT 主题,然后等待启动方的消息。如果启动者先启动,由于接收者仍未连接,SDP 消息就会丢失,导致 SDP 协商失败。有一种交换 SDP 的算法没有这个缺点,我们将在以后的文章中讨论。
6. ICE 协商和连接建立
添加本地描述后,连接对象开始收集 ICE 候选者。候选 ICE 包含节点如何在网络中到达连接对象的信息。每个 ICE 候选对象都是一种可能的连接方式,但并不保证一定能建立连接。候选对象在两个对等节点上收集,并应与另一个对等节点交换。交换后,将对所有候选配对(每个对等端各一个)进行检查,以找到两个对等端都能相互连接的配对。
RTCPeerConnection 提供了一个 icecandidate
事件,每当它收集到一个 ICE 候选对时就会发出提示。让我们添加一个事件监听器并向另一个对等点发送消息。
// Handle ICE candidates
rtcConnection.addEventListener('icecandidate', (event) => {
if (event.candidate) {
// Send ICE candidate to the other participant
sendMqttMessage({
meetingId: document.getElementById('meetingId').value,
type: 'ice',
candidate: event.candidate
});
}
});
从其他对等点收到的 ICE 候选者应添加到对等连接中,如下所示:
rtcConnection.addIceCandidate(candidate);
就是这样。我们完成了设置一个正常运行的会议应用程序所需的所有步骤。
将所有步骤整合在一起
现在我们已经完成了所有步骤,让我们打开浏览器看看我们取得了什么成果。在两个浏览器中打开应用程序,然后按照本文介绍中的步骤开始会议。现在,我们有了自己的会议应用程序,它是安全的、端到端加密的。继续与你的同事和爱人分享吧。
以上源代码链接:https://github.com/bharath-kotha/WebRTC-Articles/tree/master/WebRTC-web-1
下一步是什么?
在本文中,我们有意忽略了一些概念,如配置 RTCPeerConnection、对称 SDP 交换等。我们将在以后的文章中继续介绍这些概念以及在 Android 中实现会议应用程序。
作者:Bharath Kotha
本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/webrtc/32630.html