概述
之前使用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。