概述
之前使用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()。
它有两个参数track
和stream
,其中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
|
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)
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
| 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`;
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
| 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
信息来对音视频的实时码率进行计算,正如上面代码中所展示的那样。
上图可以看到视频码率在1700kbps左右,音频在20kbps左右。本地连接之间的延迟大概在70ms。