zoukankan      html  css  js  c++  java
  • WebRTC本地选择codec(web本地模拟)

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

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

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

    示例

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

    work

    开启视频后,建立连接前,我们可以选择设置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.videoWidth}x${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.videoWidth}px,  videoHeight: ${this.videoHeight}px`);
    });
    
    remoteVideo.addEventListener('loadedmetadata', function () {
      console.log(`Remote video videoWidth: ${this.videoWidth}px,  videoHeight: ${this.videoHeight}px`);
    });
    
    // 挂断
    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

    扩展阅读

    一个软件工程师的记录
  • 相关阅读:
    POJ 3252 Round Numbers
    HDU 1024 Max Sum Plus
    HDU 1024 Max Sum Plus Plus
    HDU 1698 Just a Hook
    HDU 1049 Climbing Worm
    HDU 3386 Reversi
    树状数组总结
    HDU 1556 Color the ball
    树形数组
    HDU 1188 敌兵布阵
  • 原文地址:https://www.cnblogs.com/rustfisher/p/15797587.html
Copyright © 2011-2022 走看看