0%

webrtc学习笔记-使用js编写并调用摄像头同时进行码率统计


概述

之前使用pion的那个golang写的webrtc库的时候,总觉得有些问题,于是决定从原生的js接口学,毕竟这个成熟得多。

这里的代码同样很简单,主要展示了如下两点内容:

  • 如何调用摄像头,添加音视频track。
  • 如何调整音视频编码偏好。比如使用H264。
  • 如何统计当前实时的音视频码率。

代码:https://github.com/isadamu/webrtcstreamtest


如何调用摄像头,添加音视频track

有很多以前的博客中,使用的方法都是过时的,很多不能用,这里可以参考官方文档MediaDevices.getUserMedia()

可以使用如下语法来调用摄像头,并把stream添加到PeerConnection中:

1
2
3
4
5
6
7
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then(stream => {
document.getElementById('video1').srcObject = stream;

stream.getTracks().forEach(track => pc.addTrack(track, stream));

}).catch(log);

这里的video1是一个简单的html标签:

1
<video id="video1" width="320" height="240" autoplay muted></video>

这里使用addTrack将流添加到连接中,用法可以参考RTCPeerConnection.addTrack()。 它有两个参数trackstream,其中track我理解为轨道,可以是音轨,也可以是视频轨道。stream则代表这个轨道属于哪一条流, webrtc会自动的将同一个stream下的track绑定到一起,无论是本地还是远端。

另一端则使用ontrack来处理addTrack所添加的轨道:

1
2
3
4
5
6
let videoElem = document.getElementById('video2');
pc.ontrack = ev => {
if (ev.streams && ev.streams[0]) {
videoElem.srcObject = ev.streams[0];
}
}

每一次调用addTrack都会触发一次ontrack,其中的event结构参考文档RTCTrackEvent, 因为这里我们知道只有一个stream,所以判断stream[0]就可以了。


如何调整音视频编码偏好,比如使用H264

上面虽然添加了音视频,可是没有任何环节对编码进行了控制,显然我们不知道这里的音视频使用的都是什么编码。

查看建立连接时的SDP描述符,可以看到其中有如下两段:

1
2
3
4
5
6
7
8
9
10
11
12
13
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126
a=rtpmap:111 opus/48000/2
a=rtpmap:103 ISAC/16000
a=rtpmap:104 ISAC/32000
...

m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 121 127 120 125 107 108 109 124 119 123 118 114 115 116
a=rtpmap:96 VP8/90000
a=rtpmap:97 rtx/90000
a=rtpmap:98 VP9/90000
...
a=rtpmap:102 H264/90000
...

其中后面跟着的数字序列,其实就代变着编码格式,每一个数字代变着一种编码。

webrtc两端传递offer和answer的过程中,其实就对编码进行了协商,上面的编码序列就代表自己所支持的编码,顺序可以理解为优先级。 这里由于是本地测试,所以两端所支持的编码格式一样,所以肯定协商的结果就是音频使用opus,视频使用VP8

调整编码序列的方法参考文档Codecs used by WebRTC, 如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 用于修改视频的编码优先顺序
// 这里的mimeType可以通过 RTCRtpSender.getCapabilities().codecs 来查询
// 格式为 "video/H264" 形式的字符串
function changeVideoCodec(mimeType) {
const transceivers = pc.getTransceivers();

transceivers.forEach(transceiver => {
const kind = transceiver.sender.track.kind;
if (kind === "video") {

let sendCodecs = RTCRtpSender.getCapabilities(kind).codecs;
let recvCodecs = RTCRtpReceiver.getCapabilities(kind).codecs;

sendCodecs = preferCodec(sendCodecs, mimeType);
recvCodecs = preferCodec(recvCodecs, mimeType);

transceiver.setCodecPreferences([...sendCodecs, ...recvCodecs]);
}
});
}

// 将偏好的编码格式放到数组前,其它编码格式放到数组后
function preferCodec(codecs, mimeType) {
let otherCodecs = [];
let sortedCodecs = [];

codecs.forEach(codec => {
if (codec.mimeType === mimeType) {
sortedCodecs.push(codec);
} else {
otherCodecs.push(codec);
}
});

sortedCodecs = sortedCodecs.concat(otherCodecs)

// console.log(sortedCodecs);

return sortedCodecs;
}

只要在offer创建之前修改编码顺序即可(发送方在发出answer之前修改应该也是可以的),就能改变连接的默认编码偏好。

可以观察到SDP中的视频编码序列变为:

1
2
3
m=video 9 UDP/TLS/RTP/SAVPF 102 104 106 108 96 98 100 114 116 110 112 97 99 101 103 105 107 109 111 113 115
a=rtpmap:102 H264/90000
...

如何统计当前实时的音视频码率

很自然的,我们想知道当前实时的音视频码率是多少,这样可以大概判断一下带宽和CPU消耗,参考文档RTCPeerConnection.getStats()。 通过webrtc提供的统计接口getStats()来查询实时的信息,来完成对码率的统计。

直接将所有信息打印出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 每秒钟打印 stats 信息
window.setInterval(function() {
pc.getStats(null).then(stats => {
let statsOutput = "";

stats.forEach(report => {
statsOutput += `<h2>Report: ${report.type}</h3>\n<strong>ID:</strong> ${report.id}<br>\n` +
`<strong>Timestamp:</strong> ${report.timestamp}<br>\n`;

// Now the statistics for this report; we intentially drop the ones we
// sorted to the top above

Object.keys(report).forEach(statName => {
if (statName !== "id" && statName !== "timestamp" && statName !== "type") {
statsOutput += `<strong>${statName}:</strong> ${report[statName]}<br>\n`;
}
});
});

document.getElementById("stats-box").innerHTML = statsOutput;
});
}, 1000);

这样打印出来的信息非常多,很多都是我们不需要的,所以可以传入track,来过滤出我们需要的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// 处理远端 track
let videoElem = document.getElementById('video2');
let mediaTracks = []; // 用于统计
pc.ontrack = ev => {
if (ev.streams && ev.streams[0]) {
console.log("on track" + ev.streams[0]);
videoElem.srcObject = ev.streams[0];
mediaTracks.push(ev.track);
}
}

// 这里稍微过滤了一下,只统计输入的音视频流,并计算出实时码率(简单平滑了一下)
const statIntervalBase = 1000;
const statInterval = 1;
let lastRecvByteVideo = 0;
let lastRecvByteAudio = 0;
window.setInterval(function() {
if (mediaTracks.length <= 0) {
return;
}

mediaTracks.forEach(track => {

pc.getStats(track).then(stats => {
stats.forEach(report => {
if (report.type === "inbound-rtp") {
let statsOutput = "";
statsOutput += `<h2>Report: ${report.type}</h3>\n<strong>ID:</strong> ${report.id}<br>\n` +
`<strong>Timestamp:</strong> ${report.timestamp}<br>\n`;

Object.keys(report).forEach(statName => {
if (statName !== "id" && statName !== "timestamp" && statName !== "type") {
statsOutput += `<strong>${statName}:</strong> ${report[statName]}<br>\n`;
}
});

let RecvByte = report.bytesReceived;

if (report.mediaType === "video") {
document.getElementById("stats-video").innerHTML = statsOutput;

let bitRate = ((RecvByte - lastRecvByteVideo) * 8) / (statInterval * 1000);
lastRecvByteVideo = RecvByte;

document.getElementById("remote-stat-video-show").innerHTML = "video: " + bitRate + " kbps";

} else if (report.mediaType === "audio") {
document.getElementById("stats-audio").innerHTML = statsOutput;

let bitRate = ((RecvByte - lastRecvByteAudio) * 8) / (statInterval * 1000);
lastRecvByteAudio = RecvByte;

document.getElementById("remote-stat-audio-show").innerHTML = "audio: " + bitRate + " kbps";
}
}
});
});
});

}, statIntervalBase * statInterval);

上面的代码,在ontrack时将远端的track临时保存一下,然后传入到getStats()中,这样筛选出我们所需要的inbound-rtp信息,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
Report: inbound-rtp

ID: RTCInboundRTPVideoStream_3091018575
Timestamp: 1609225346268
ssrc: 3091018575
isRemote: false
mediaType: video
kind: video
trackId: RTCMediaStreamTrack_receiver_2
transportId: RTCTransport_0_1
codecId: RTCCodec_1_Inbound_102
firCount: 0
pliCount: 2
nackCount: 0
packetsReceived: 383830
bytesReceived: 323599556
headerBytesReceived: 9330284
packetsLost: 0
lastPacketReceivedTimestamp: 1663077.686
framesReceived: 45982
frameWidth: 640
frameHeight: 480
framesPerSecond: 29
framesDecoded: 45952
keyFramesDecoded: 768
framesDropped: 11
totalDecodeTime: 30.94
totalInterFrameDelay: 1533.7619999993074
totalSquaredInterFrameDelay: 53.32035400000804
estimatedPlayoutTimestamp: 3818214146152
decoderImplementation: ExternalDecoder

Report: inbound-rtp

ID: RTCInboundRTPAudioStream_3120593010
Timestamp: 1609225361268
ssrc: 3120593010
isRemote: false
mediaType: audio
kind: audio
trackId: RTCMediaStreamTrack_receiver_1
transportId: RTCTransport_0_1
codecId: RTCCodec_0_Inbound_111
packetsReceived: 77452
fecPacketsReceived: 0
fecPacketsDiscarded: 0
bytesReceived: 4679465
headerBytesReceived: 2168656
packetsLost: 0
lastPacketReceivedTimestamp: 1663092.678
jitter: 0.002
jitterBufferDelay: 3664896
jitterBufferEmittedCount: 74351040
totalSamplesReceived: 74406560
concealedSamples: 53120
silentConcealedSamples: 52600
concealmentEvents: 1
insertedSamplesForDeceleration: 4197
removedSamplesForAcceleration: 1663
audioLevel: 0
totalAudioEnergy: 0
totalSamplesDuration: 1552.3499999987332
estimatedPlayoutTimestamp: 3818214161171

首先可以看到视频的信息中的codecId: RTCCodec_1_Inbound_102说明当前视频编码为102,也就是a=rtpmap:102 H264/90000,即H264,说明上面对编码的修改成功生效。

然后很明显,我们可以通过bytesReceived: 323599556信息来对音视频的实时码率进行计算,正如上面代码中所展示的那样。

image

上图可以看到视频码率在1700kbps左右,音频在20kbps左右。本地连接之间的延迟大概在70ms。