WebRTC本地选择codec(web本地模拟)

Posted dreamw

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了WebRTC本地选择codec(web本地模拟)相关的知识,希望对你有一定的参考价值。

codec:编码译码器,编解码器。它是一个程序,也可以是算法,或者设备,用于编码(encode)和解码(decode)数据流。

WebRTC能让两个web或者app之间建立音视频通信。通信过程中,数据流的格式必须被两边的设备支持。

WebRTC提供了接口查询支持的codec,并且可以设置要使用的codec。本文演示选择视频codec的过程。

示例#

用户可以在发送视频流之前选择codec。把支持的codec类型列出来,用户自行选择。

开启视频后,建立连接前,我们可以选择设置codec。如上图蓝色区域所示。

html#

先来准备页面。2个video控件分别显示收发视频。
按钮分别控制开始,呼叫(发起连接)和挂断。

select用来选择codec。获取支持的codec信息,放到下拉栏里让用户选择。

以下是index.html主要内容

<div id="container">
    <h1><a href="https://an.rustfisher.com/webrtc/peerconnection/change-codec/" title="WebRTC示例,修改codec">WebRTC示例,修改codec</a>
    </h1>

    <video id="localVideo" playsinline autoplay muted></video>
    <video id="remoteVideo" playsinline autoplay></video>

    <div class="box">
        <button id="startBtn">开始</button>
        <button id="callBtn">呼叫</button>
        <button id="hangupBtn">挂断</button>
    </div>

    <div class="box">
        <span>选择Codec:</span>
        <select id="codecPreferences" disabled>
            <option selected value="">Default</option>
        </select>
        <div id="actualCodec"></div>
    </div>
    <p>可以在控制台观察 <code>MediaStream</code>, <code>localStream</code>, 和 <code>RTCPeerConnection</code></p>
</div>

<script src="../../src/js/adapter-2021.js"></script>
<script src="js/main.js" async></script>

adapter-2021.js是存放在本地的文件。要使用最新的adapter,按以下地址引入

<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>

js#

main.js控制主要逻辑。从开启摄像头开始。建立连接前可以选择codec。

建立连接的流程与「WebRTC模拟传输视频流,video通过本地节点peer传输视频流」类似。

获取可用codec#

先判断浏览器是否有RTCRtpTransceiver,并且要能支持setCodecPreferences方法

const supportsSetCodecPreferences = window.RTCRtpTransceiver &&
  \'setCodecPreferences\' in window.RTCRtpTransceiver.prototype;

通过RTCRtpSender.getCapabilities(\'video\')获取可支持的codec。
然后把它们放进列表codecPreferences

  if (supportsSetCodecPreferences) 
    const  codecs  = RTCRtpSender.getCapabilities(\'video\');

    codecs.forEach(codec => 
      if ([\'video/red\', \'video/ulpfec\', \'video/rtx\'].includes(codec.mimeType)) 
        return;
      
      const option = document.createElement(\'option\');
      option.value = (codec.mimeType + \' \' + (codec.sdpFmtpLine || \'\')).trim();
      option.innerText = option.value;
      codecPreferences.appendChild(option);
    );
    codecPreferences.disabled = false;
  

配置codec#

呼叫之前,找到用户选择的codec。
调用transceiver.setCodecPreferences(codecs),把选中的codec交给transceiver

  if (supportsSetCodecPreferences) 
    // 获取选择的codec
    const preferredCodec = codecPreferences.options[codecPreferences.selectedIndex];
    if (preferredCodec.value !== \'\') 
      const [mimeType, sdpFmtpLine] = preferredCodec.value.split(\' \');
      const  codecs  = RTCRtpSender.getCapabilities(\'video\');
      const selectedCodecIndex = codecs.findIndex(c => c.mimeType === mimeType && c.sdpFmtpLine === sdpFmtpLine);
      const selectedCodec = codecs[selectedCodecIndex];
      codecs.splice(selectedCodecIndex, 1);
      codecs.unshift(selectedCodec);
      console.log(codecs);
      const transceiver = pc1.getTransceivers().find(t => t.sender && t.sender.track === localStream.getVideoTracks()[0]);
      transceiver.setCodecPreferences(codecs);
      console.log(\'选择的codec\', selectedCodec);
    
  

main.js完整代码如下


\'use strict\';

console.log(\'WebRTC示例,选择codec\');

// --------- ui准备 ---------
const startBtn = document.getElementById(\'startBtn\');
const callBtn = document.getElementById(\'callBtn\');
const hangupBtn = document.getElementById(\'hangupBtn\');
const localVideo = document.getElementById(\'localVideo\');
const remoteVideo = document.getElementById(\'remoteVideo\');

callBtn.disabled = true;
hangupBtn.disabled = true;
startBtn.addEventListener(\'click\', start);
callBtn.addEventListener(\'click\', call);
hangupBtn.addEventListener(\'click\', hangup);
// ---------------------------

// -------- codec 的配置 --------
const codecPreferences = document.querySelector(\'#codecPreferences\');
const supportsSetCodecPreferences = window.RTCRtpTransceiver &&
  \'setCodecPreferences\' in window.RTCRtpTransceiver.prototype;
// -----------------------------

let startTime;

remoteVideo.addEventListener(\'resize\', () => 
  console.log(`Remote video size changed to $remoteVideo.videoWidthx$remoteVideo.videoHeight`);
  if (startTime) 
    const elapsedTime = window.performance.now() - startTime;
    console.log(\'视频流连接耗时: \' + elapsedTime.toFixed(3) + \'ms\');
    startTime = null;
  
);

let localStream;
let pc1;
let pc2;
const offerOptions = 
  offerToReceiveAudio: 1,
  offerToReceiveVideo: 1
;

function getName(pc) 
  return (pc === pc1) ? \'pc1\' : \'pc2\';


function getOtherPc(pc) 
  return (pc === pc1) ? pc2 : pc1;


// 启动本地视频
async function start() 
  console.log(\'启动本地视频\');
  startBtn.disabled = true;
  try 
    const stream = await navigator.mediaDevices.getUserMedia( audio: true, video: true );
    console.log(\'获取到本地视频\');
    localVideo.srcObject = stream;
    localStream = stream;
    callBtn.disabled = false;
   catch (e) 
    alert(`getUserMedia() error: $e.name`);
  
  if (supportsSetCodecPreferences) 
    const  codecs  = RTCRtpSender.getCapabilities(\'video\');
    console.log(\'RTCRtpSender.getCapabilities(video):\\n\', codecs);
    codecs.forEach(codec => 
      if ([\'video/red\', \'video/ulpfec\', \'video/rtx\'].includes(codec.mimeType)) 
        return;
      
      const option = document.createElement(\'option\');
      option.value = (codec.mimeType + \' \' + (codec.sdpFmtpLine || \'\')).trim();
      option.innerText = option.value;
      codecPreferences.appendChild(option);
    );
    codecPreferences.disabled = false;
   else 
    console.warn(\'当前不支持更换codec\');
  


// 呼叫并建立连接
async function call() 
  callBtn.disabled = true;
  hangupBtn.disabled = false;
  console.log(\'开始呼叫\');
  startTime = window.performance.now();
  const videoTracks = localStream.getVideoTracks();
  const audioTracks = localStream.getAudioTracks();
  if (videoTracks.length > 0) 
    console.log(`使用的摄像头: $videoTracks[0].label`);
  
  if (audioTracks.length > 0) 
    console.log(`使用的麦克风: $audioTracks[0].label`);
  
  const configuration = ;
  pc1 = new RTCPeerConnection(configuration);
  pc1.addEventListener(\'icecandidate\', e => onIceCandidate(pc1, e));
  pc2 = new RTCPeerConnection(configuration);
  pc2.addEventListener(\'icecandidate\', e => onIceCandidate(pc2, e));
  pc2.addEventListener(\'track\', gotRemoteStream);

  localStream.getTracks().forEach(track => pc1.addTrack(track, localStream));
  if (supportsSetCodecPreferences) 
    // 获取选择的codec
    const preferredCodec = codecPreferences.options[codecPreferences.selectedIndex];
    if (preferredCodec.value !== \'\') 
      const [mimeType, sdpFmtpLine] = preferredCodec.value.split(\' \');
      const  codecs  = RTCRtpSender.getCapabilities(\'video\');
      const selectedCodecIndex = codecs.findIndex(c => c.mimeType === mimeType && c.sdpFmtpLine === sdpFmtpLine);
      const selectedCodec = codecs[selectedCodecIndex];
      codecs.splice(selectedCodecIndex, 1);
      codecs.unshift(selectedCodec);
      console.log(codecs);
      const transceiver = pc1.getTransceivers().find(t => t.sender && t.sender.track === localStream.getVideoTracks()[0]);
      transceiver.setCodecPreferences(codecs);
      console.log(\'选择的codec\', selectedCodec);
    
  
  codecPreferences.disabled = true;

  try 
    const offer = await pc1.createOffer(offerOptions);
    await onCreateOfferSuccess(offer);
   catch (e) 
    console.log(`Failed, pc1 createOffer: $e.toString()`);
  


async function onCreateOfferSuccess(desc) 
  try 
    await pc1.setLocalDescription(desc);
    console.log(\'pc1 setLocalDescription 成功\');
   catch (e) 
    console.error(\'pc1 setLocalDescription 出错\', e);
  
  try 
    await pc2.setRemoteDescription(desc);
    console.log(\'pc2 setRemoteDescription ok\');
   catch (e) 
    console.error(\'pc2 setRemoteDescription fail\', e);
  
  try 
    const answer = await pc2.createAnswer();
    await onCreateAnswerSuccess(answer);
   catch (e) 
    console.log(`pc2 create answer fail: $e.toString()`);
  


function gotRemoteStream(e) 
  if (remoteVideo.srcObject !== e.streams[0]) 
    remoteVideo.srcObject = e.streams[0];
    console.log(\'pc2 received remote stream\');
  


// 应答(接收)成功
async function onCreateAnswerSuccess(desc) 
  console.log(`Answer from pc2:\\n$desc.sdp`);
  console.log(\'pc2 setLocalDescription start\');
  try 
    await pc2.setLocalDescription(desc);
   catch (e) 
    console.error(\'pc2 set local d fail\', e);
  
  console.log(\'pc1 setRemoteDescription start\');
  try 
    await pc1.setRemoteDescription(desc);

    // Display the video codec that is actually used.
    setTimeout(async () => 
      const stats = await pc1.getStats();
      stats.forEach(stat => 
        if (!(stat.type === \'outbound-rtp\' && stat.kind === \'video\')) 
          return;
        
        const codec = stats.get(stat.codecId);
        document.getElementById(\'actualCodec\').innerText = \'Using \' + codec.mimeType +
          \' \' + (codec.sdpFmtpLine ? codec.sdpFmtpLine + \' \' : \'\') +
          \', payloadType=\' + codec.payloadType + \'. Encoder: \' + stat.encoderImplementation;
      );
    , 1000);
   catch (e) 
    console.error(e);
  


async function onIceCandidate(pc, event) 
  try 
    await (getOtherPc(pc).addIceCandidate(event.candidate));
    onAddIceCandidateSuccess(pc);
   catch (e) 
    onAddIceCandidateError(pc, e);
  
  console.log(`$getName(pc) ICE candidate:\\n$event.candidate ? event.candidate.candidate : \'(null)\'`);


function onAddIceCandidateSuccess(pc) 
  console.log(`$getName(pc) addIceCandidate success`);


function onAddIceCandidateError(pc, error) 
  console.log(`$getName(pc) failed to add ICE Candidate: $error.toString()`);


localVideo.addEventListener(\'loadedmetadata\', function () 
  console.log(`Local video videoWidth: $this.videoWidthpx,  videoHeight: $this.videoHeightpx`);
);
remoteVideo.addEventListener(\'loadedmetadata\', function () 
console.log(`Remote video videoWidth: $this.videoWidthpx,  videoHeight: $this.videoHeightpx`);
);
// 挂断
function hangup() 
console.log(\'挂断\');
  pc1.close();
  pc2.close();
  pc1 = null;
  pc2 = null;
  hangupBtn.disabled = true;
  callBtn.disabled = false;
  codecPreferences.disabled = false;

codec信息说明#

观察控制台,打印出了可用codec信息(Mac,97.0.4692.71(正式版本)x86_64)。主要关注下面3种

clockRate: 90000, mimeType: \'video/VP8\'
clockRate: 90000, mimeType: \'video/VP9\', sdpFmtpLine: \'profile-id=0\'
clockRate: 90000, mimeType: \'video/H264\', sdpFmtpLine: \'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f\'

clockRate 是codec的时钟频率,单位hz

sdpFmtpLine 是codec的SDP里a=fmtp的参数信息

mimeType 里说的是视频编码类型,常见的有VP8和H264等等

支持WebRTC的浏览器,必须要支持视频codec VP8和H264

VP8与VP9#

2010年5月Google收购了On2 Technologies,获得了VP8。
Opera,FireFox,Chrome和Chromium支持HTML5中的video播放VP8视频。

WebM作为一个容器格式,图像部分使用VP8,音频使用Vorbis和Opus。

VP9由Google开发,一个开放的无版权费的视频编码标准。开发初期曾用名“Next Gen Open Video”。VP9也被视为是VP8的下一代视频编码标准。

H264#

H.264,又称为MPEG-4第10部分,高级视频编码是一种面向块,基于运动补偿的视频编码标准。
到2014年,它已经成为高精度视频录制、压缩和发布的最常用格式之一。

优势:

  • 1)网络亲和性,即可适用于各种传输网络
  • 2)高的视频压缩比

目前我们用的比较多的还是H264。

效果预览#

网页效果请参考 选择codec

扩展阅读#

作者:AnRFDev

出处:https://www.cnblogs.com/rustfisher/p/15797587.html

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

-> 简单明了,上手快的Android合集 an.rustfisher.com
-> 更多文章 rustfisher.com
一家之言,仅当抛砖引玉。如有错漏,还请指出。

 
转 https://www.cnblogs.com/rustfisher/p/15797587.html

以上是关于WebRTC本地选择codec(web本地模拟)的主要内容,如果未能解决你的问题,请参考以下文章

Android WebRTC 入门教程 -- 模拟p2p本地视频传输

协商 远端codecs 和 本地codecs

协商 远端codecs 和 本地codecs

WebRTC - 如何静音本地音频输出

WebRTC/getUserMedia:如何正确静音本地视频?

webrtc codec