webrtc网页代码分析二
- 前后端
- 2024-12-14
- 150热度
- 0评论
交互流程
上图是完整的处理流程。
获取URL参数
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const deviceId = urlParams.get('deviceId');
这段代码先获取当前页面 URL 中的查询字符串部分(window.location.search),然后使用 URLSearchParams 对象解析查询字符串,尝试获取名为 deviceId 的参数值,后续很多操作会根据这个 deviceId 的有无来进行不同的逻辑处理。
页面加载时的初始化操作
window.onload = function () {
if (deviceId == undefined) {
document.getElementById('video').style.display = 'none';
document.getElementById('getting-started').style.display = 'block';
}
const canvas = document.createElement('canvas')
canvas.width = 640
canvas.height = 480
const ctx = canvas.getContext('2d')
ctx.fillStyle = 'rgba(200, 200, 200, 100)'
ctx.fillRect(0, 0, 640, 480)
const img = new Image(640, 480)
img.src = canvas.toDataURL()
document.getElementById('imgStream').src = img.src;
}
window.onload是页面加载完成后的回调函数,上面使用的是匿名函数,即当页面所有内容加载完成后执行的逻辑。具体逻辑如下
这里判断如果deviceId未定义(也就是可能页面直接访问,没有带上对应的参数),则隐藏视频相关区域(id为video的div),显示初始引导内容区域(id 为 getting-started 的 div)。在函数内创建了一个 canvas 元素,绘制了一个半透明的矩形,将其转换为图片数据后设置给img id=\\\\"imgStream\\\\"元素作为初始显示内容。
MQTT操作
发起连接
var options = {
keepalive: 600,
username: 'xxx',
password: 'xxxx',
};
const client = mqtt.connect('wss://xxx.xxx.com:8084/mqtt', options);
定义了 MQTT连接的相关选项(如保活时间、用户名、密码等),然后尝试连接到指定的MQTT服务器地址(wss://xxx.xxx.xxx:8084/mqtt),后续通过client对象来处理 MQTT 的各种消息收发以及相关操作。
事件处理
connect事件
client.on('connect', function () {
console.log("connected")
client.subscribe('webrtc/' + deviceId + '/jsonrpc-reply', function (err) {
if (!err) {
}
});
console.log('publish to webrtc/' + deviceId + '/jsonrpc');
client.publish('webrtc/' + deviceId + '/jsonrpc', JSON.stringify({
jsonrpc: '2.0',
method: 'offer',
id: offerId,
}), { qos: 2 });
})
结构为client.on(\\\\'connect\\\\', function () {... }); client.on这里是mqtt对象的事件回调,这里将function(){...}作为回调函数注册进去,funciton实现是匿名函数的方式。当前收到connect事件时,就会执行function的内容。具体逻辑如下。
当成功连接到MQTT服务器后,先在控制台输出 connected 表示连接成功,然后订阅一个特定主题(webrtc/\\\\' + deviceId + \\\\'/jsonrpc-reply)用于接收回复消息,接着向另一个特定主题(webrtc/\\\\' + deviceId + \\\\'/jsonrpc)发布一条包含offer信息的 JSON 格式消息(设置了消息质量等级 qos 为 2),用于发起某种交互(可能与 WebRTC 相关的会话建立等操作)。
(1)订阅函数subscribe
client.subscribe(topic, options, callback);
- topic: 需要订阅的主题,可以是一个字符串或者一个主题数组。主题是 MQTT 中用于分类消息的方式。主题的命名规则通常是层级的,使用斜杠(/)分隔每一层级,例如 \\\\'home/livingroom/temperature\\\\'。
- options: 可选参数,配置订阅的一些选项,如 QoS(服务质量)级别。常见的选项包括:
-- qos: 服务质量(Quality of Service)级别,取值范围是:
-- 0 - 至多一次:消息最多发送一次,不做确认。
-- 1 - 至少一次:消息至少发送一次,可能会重复。
-- 2 - 只有一次:消息只能传递一次,不会丢失,也不会重复。 - callback: 可选的回调函数,当订阅成功时会被调用。回调函数接受一个错误对象作为参数,如果订阅成功,err 为 null;如果订阅失败,则返回相应的错误信息。
(2)发布函数publish
client.publish(topic, message, options, callback);
- topic: 需要发布消息的主题,可以是一个字符串,表示主题名称。
-
message: 需要发布的消息内容,可以是字符串、Buffer、对象等。一般情况下,消息会被自动转换为字符串或 Buffer(如果传入对象,会自动通过 JSON.stringify() 转换为字符串)。
-
options: 可选参数,配置发布消息的选项,常见的选项包括:
-- qos: 服务质量(Quality of Service)级别(如前述),默认是 0。
-- retain: 是否保留消息,若为 true,代理服务器会保存消息并在新订阅者连接时发送该消息。 - callback: 可选的回调函数,发布消息成功后会被调用。回调函数接受一个错误对象作为参数,如果发布成功,err 为 null;如果发布失败,则返回相应的错误信息。
messange事件
MQTT消息接收处理(client.on(\\\\'message\\\\', function (topic, message) {... })),语法与上面类似,逻辑如下。
client.on('message', function (topic, message) {
let msg = JSON.parse(message.toString());
if (msg.id == offerId) {
let sdp = msg.result;
let offer = { type: 'offer', sdp: sdp };
log(offer);
pc.setRemoteDescription(offer);
pc.createAnswer().then(d => pc.setLocalDescription(d)).catch(log);
} else if (msg.id == answerId) {
log('receive answer ok');
}
})
当接收到MQTT消息时,先将消息内容解析为 JSON 对象,然后根据消息中的 id 判断消息类型,如果 id 与之前定义的 offerId 相等,说明是对之前 offer 的回复,会提取其中的 sdp 信息设置为远程描述(setRemoteDescription),并创建一个本地回答(createAnswer)然后设置本地描述(setLocalDescription);如果 id 等于 answerId,则只是在控制台输出表示接收到了回答消息。
WebRTC操作
创建对象
var pc = new RTCPeerConnection({
iceServers: [
{
urls: "turn:rtpeer.allwinnertech.com:3478",
username: "pctest",
credential: "Aa123456",
},
],
});
创建了一个 RTCPeerConnection 对象用于建立 WebRTC 连接,配置了 iceServers,这里指定了一个 TURN 服务器的相关信息(地址、用户名、密码),有助于在复杂网络环境(如存在 NAT 等情况)下建立稳定的点对点连接,确保音视频数据能顺利传输。
获取PC录音
navigator.mediaDevices.getUserMedia({ video: false, audio: true })
.then(stream => {
stream.getTracks().forEach(track => pc.addTrack(track, stream));
}).catch(log);
通过 navigator.mediaDevices.getUserMedia 方法请求获取音频媒体流(这里明确只要音频,video: false, audio: true),如果获取成功,将音频轨道添加到 RTCPeerConnection 对象(pc)中,以便后续传输音频数据,如果出现错误则通过 log 函数(也就是在控制台输出错误信息)进行处理。
连接状态
pc.oniceconnectionstatechange = e => {
log(pc.iceConnectionState);
document.getElementById('status').innerHTML = pc.iceConnectionState;
if (pc.iceConnectionState == 'connected') {
// default to muted
if (!isMuted)
onMuted();
}
}
当 WebRTC 连接的 ICE 连接状态发生变化时,先在控制台输出当前连接状态(通过 log 函数),然后更新页面上 id 为 status 的元素文本内容为当前连接状态,并且当连接状态变为 connected(已连接)时,如果当前不是静音状态(!isMuted),则调用 onMuted 函数将音频静音。
ICE候选者
pc.onicecandidate = event => {
if (event.candidate === null) {
console.log(pc.localDescription.sdp)
let json = {
jsonrpc: \\'2.0\\',
method: \\'answer\\',
params: pc.localDescription.sdp,
id: answerId,
}
console.log(json)
client.publish(\\'webrtc/\\' + deviceId + \\'/jsonrpc\\', JSON.stringify(json))
setInterval(() => {....}, 1000);
}
}
当有 ICE 候选者信息产生并且当候选者为 null 时(意味着 ICE 收集过程结束),执行一系列操作,包括打印 RTCPeerConnection 的本地描述中的 sdp 信息,向 MQTT 特定主题发布包含本地描述 sdp 信息的 JSON 消息,并且每隔 1 秒获取连接的统计信息(通过 pc.getStats 方法),整理后更新到页面中 id 为 stats-box 的元素内展示出来。
当收到对端的offer候选信息后,本地会进行与stun/turn服务交互,交互获取到ICE信息后,就会触发回调这个函数,将自己SDP信息组装成json通过mqtt publish发送给对端。
接收音视频
pc.ontrack = function (event) {
if (event.track.kind == \\'video\\') {
var el = document.getElementById(\\'videoStream\\');
var newStream = new MediaStream();
newStream.addTrack(event.track);
el.srcObject = newStream;
el.autoplay = true
el.controls = false
el.muted = true
document.getElementById(\\'imgStream\\').style.display = \\'none\\';
document.getElementById(\\'videoStream\\').style.display = \\'block\\';
// 当视频流添加成功后,获取视频分辨率和码率信息并更新显示
el.addEventListener(\\'loadedmetadata\\', function () {
updateVideoInfo(event.track);
});
} else if (event.track.kind == \\'audio\\') {
var el = document.getElementById(\\'audioStream\\');
var newStream = new MediaStream();
newStream.addTrack(event.track);
el.srcObject = newStream;
el.controls = false
el.muted = false
}
}
当通过 RTCPeerConnection 接收到远端传来的媒体轨道(视频或音频轨道)时,根据轨道类型进行不同的处理。对于视频轨道,获取页面中的视频元素(id 为 videoStream),创建新的 MediaStream 对象添加轨道后设置为视频元素的播放源,同时设置自动播放、隐藏控制条、静音等属性,并在视频元数据加载完成后(通过 loadedmetadata 事件)调用 updateVideoInfo 函数更新视频分辨率和码率信息显示;对于音频轨道,类似地获取音频元素(id 为 audioStream),添加轨道到新的 MediaStream 对象并设置为音频元素播放源,设置不显示控制条且不静音。
数据通道
const datachannel = pc.createDataChannel(\\'pear\\')
datachannel.onclose = () => console.log(\\'datachannel has closed\\');
datachannel.onopen = () => {
console.log(\\'datachannel has opened\\');
console.log(\\'sending ping\\');
setInterval(() => {
console.log(\\'sending ping\\');
datachannel.send(\\'ping\\');
}, 1000);
}
datachannel.onmessage = e => {
if (e.data.byteLength === undefined) {
console.log(e.data);
} else {
// is binary data. mjpeg stream
// console.log(e.data.byteLength);
var arrayBufferView = new Uint8Array(e.data);
var blob = new Blob([arrayBufferView], { type: \\"image/jpeg\\" });
var urlCreator = window.URL || window.webkitURL;
var imageUrl = urlCreator.createObjectURL(blob);
var imageElement = document.getElementById(\\'imgStream\\');
imageElement.src = imageUrl;
}
}
创建了一个名为pear的数据通道(const datachannel = pc.createDataChannel(\\'pear\\')),并分别对数据通道的打开、关闭、接收消息等事件绑定了相应的回调函数。例如在数据通道打开时(datachannel.onopen),会定期(每隔 1 秒)发送 ping 消息用于保持连接或检测连接状态等;在接收到消息时(datachannel.onmessage),根据消息数据类型判断,如果是文本消息直接在控制台输出,如果是二进制数据(判断为 MJPEG 流),则将其转换为图片的 URL 并设置给页面中的图片元素(id 为 imgStream)用于显示。
所以这里支持mjpeg的方式,但是是通过数据通道来实现的。