前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >WebRTC 会话详解

WebRTC 会话详解

原创
作者头像
花落花相惜
发布2021-12-16 22:36:00
2.2K0
发布2021-12-16 22:36:00
举报

JavaScrpt 中用到的三个主要的对象有:

  • MediaStream 获取和渲染音频和视频流
  • RTCPeerConnection 支持音频和视频媒体数据通信
  • RTCDataChannel 支持应用级的数据通信

对于媒体传输层,WebRTC 规定了用 ICE/STUN/TURN 来连通,用 DTLS 来协商 SRTP 密钥,用 SRTP 来传输媒体数据, 用

SCTP 来传输应用数据。

而在信令层,WebRTC 并未指定,各个应用可以用自己喜欢的信令协议来进行媒体协商,一般都是用 SDP 来通过 HTTP, WebSocket 或 SIP

协议承载具体的媒体会话描述。

如果我们要进行视频聊天, 最基本的呼叫流程大致如下:

WebRTC flow

  1. 收集本地的媒体源(麦克风,摄像头)作为 MediaStream 媒体流
  2. 两个对端彼此创建信令通道,交换会话描述信息 SDP
  3. 通过信令通过来交换彼此的会话描述信息 SDP
  4. 通过 ICE/STUN/TURN 协议,协商出可连通的 Candidate Pair(候选者对) 来创建 PeerConnection
  5. PeerConnection 创建好后,通过SRTP来封装音视频数据进行传输

简单来说通信的双方需要了解两块信息

  1. ICE 候选者 ICE Candidates:包括可用来通信的地址信息
  2. 会话描述信息 Session Description: 包括媒体种类,编码,格式等等。

ICE的全称是" Interactive Connectivity Establishment " 即交互式连接的建立: 一个用于网络地址转换穿越的协议

大致的流程如下, Alice 想要和 Bob 在网上聊天(包括文字,语音和视频),需要经过这些步骤, 看起来很复杂,我们一步细细分解来说

call flow example

举两个例子

  1. 本地对等连接 Local Peer Connection
  2. 远程对等连接 Remote Peer Connection

1. 本地对等连接

代码语言:txt
复制
<!DOCTYPE html>
代码语言:txt
复制
<html xmlns="http://www.w3.org/1999/xhtml">
代码语言:txt
复制
<head>
代码语言:txt
复制
//... 省略引入的 css 和 js 文件
代码语言:txt
复制
</head>
代码语言:txt
复制
<body>
代码语言:txt
复制
<!-- <a href="https://github.com/walterfan/webrtc-primer"><img style="position: absolute; top: 0; left: 0; border: 0; z-index: 1001;" src="https://s3.amazonaws.com/github/ribbons/forkme_left_darkblue_121621.png" alt="Fork me on GitHub"></a>
代码语言:txt
复制
 -->
代码语言:txt
复制
<nav class="navbar navbar-default navbar-static-top">
代码语言:txt
复制
</nav>
代码语言:txt
复制
<div class="container">
代码语言:txt
复制
    <div class="row">
代码语言:txt
复制
        <div class="col-lg-12">
代码语言:txt
复制
            <div class="page-header">
代码语言:txt
复制
                <h1>WebRTC example of Peer Connection </h1>
代码语言:txt
复制
            </div>
代码语言:txt
复制
            <div class="container" id="details">
代码语言:txt
复制
          <div class="row">
代码语言:txt
复制
              <div class="col-lg-12">
代码语言:txt
复制
                <p>Click the button to open or close connection</p>
代码语言:txt
复制
                <div>
代码语言:txt
复制
                    <button class="btn btn-default" autocomplete="off" id="startButton">Start Video</button>
代码语言:txt
复制
                    <button class="btn btn-default" autocomplete="off" id="stopButton">Stop Video</button>
代码语言:txt
复制
                      <button class="btn btn-default" autocomplete="off" id="callButton">Call</button>
代码语言:txt
复制
                      <button class="btn btn-default" autocomplete="off" id="hangupButton">Hangup</button>
代码语言:txt
复制
                </div>
代码语言:txt
复制
                <br/>
代码语言:txt
复制
              </div>
代码语言:txt
复制
              <div class="col-lg-12">
代码语言:txt
复制
                    <div class="col-lg-6"><video id="localVideo" autoplay></video></div>
代码语言:txt
复制
                    <div class="col-lg-6"><video id="remoteVideo" autoplay></video></div>
代码语言:txt
复制
               </div>
代码语言:txt
复制
               <div>
代码语言:txt
复制
                    <div class="box">
代码语言:txt
复制
                        <span>SDP Semantics:</span>
代码语言:txt
复制
                        <select id="sdpSemantics">
代码语言:txt
复制
                            <option selected value="">Default</option>
代码语言:txt
复制
                            <option value="unified-plan">Unified Plan</option>
代码语言:txt
复制
                            <option value="plan-b">Plan B</option>
代码语言:txt
复制
                        </select>
代码语言:txt
复制
                    </div>
代码语言:txt
复制
                   <div>
代码语言:txt
复制
                        <button class="btn btn-default" autocomplete="off" id="sdpButton">Display SDP</button>
代码语言:txt
复制
                       <textarea id="output"></textarea>
代码语言:txt
复制
                       <code><pre>
代码语言:txt
复制
                           interface RTCOfferAnswerOptions {
代码语言:txt
复制
                                voiceActivityDetection?: boolean;
代码语言:txt
复制
                           }
代码语言:txt
复制
                           interface RTCOfferOptions extends RTCOfferAnswerOptions {
代码语言:txt
复制
                                iceRestart?: boolean;
代码语言:txt
复制
                                offerToReceiveAudio?: boolean;
代码语言:txt
复制
                                offerToReceiveVideo?: boolean;
代码语言:txt
复制
                            }
代码语言:txt
复制
                       </pre></code>
代码语言:txt
复制
                   </div>
代码语言:txt
复制
               </div>
代码语言:txt
复制
          </div>
代码语言:txt
复制
          <!-- 省略若干 HTML 片断 -->
代码语言:txt
复制
</div>
代码语言:txt
复制
<script type="text/javascript" src="js/local_peer_connection_demo.js"></script>
代码语言:txt
复制
</body>
代码语言:txt
复制
</html>
代码语言:txt
复制
'use strict';
代码语言:txt
复制
const startButton = document.getElementById('startButton');
代码语言:txt
复制
const stopButton = document.getElementById('stopButton');
代码语言:txt
复制
const callButton = document.getElementById('callButton');
代码语言:txt
复制
const hangupButton = document.getElementById('hangupButton');
代码语言:txt
复制
const sdpButton = document.getElementById('sdpButton');
代码语言:txt
复制
const outputTextarea = document.querySelector('textarea#output');
代码语言:txt
复制
stopButton.disabled = true;
代码语言:txt
复制
callButton.disabled = true;
代码语言:txt
复制
hangupButton.disabled = true;
代码语言:txt
复制
startButton.addEventListener('click', start);
代码语言:txt
复制
stopButton.addEventListener('click', stop);
代码语言:txt
复制
callButton.addEventListener('click', call);
代码语言:txt
复制
hangupButton.addEventListener('click', hangup);
代码语言:txt
复制
sdpButton.addEventListener('click', displaySdp);
代码语言:txt
复制
let startTime;
代码语言:txt
复制
const localVideo = document.getElementById('localVideo');
代码语言:txt
复制
const remoteVideo = document.getElementById('remoteVideo')
代码语言:txt
复制
localVideo.addEventListener('loadedmetadata', function() {
代码语言:txt
复制
  console.log(`Local video videoWidth: ${this.videoWidth}px,  videoHeight: ${this.videoHeight}px`);
代码语言:txt
复制
});
代码语言:txt
复制
remoteVideo.addEventListener('loadedmetadata', function() {
代码语言:txt
复制
  console.log(`Remote video videoWidth: ${this.videoWidth}px,  videoHeight: ${this.videoHeight}px`);
代码语言:txt
复制
});
代码语言:txt
复制
remoteVideo.addEventListener('resize', () => {
代码语言:txt
复制
  console.log(`Remote video size changed to ${remoteVideo.videoWidth}x${remoteVideo.videoHeight}`);
代码语言:txt
复制
  // We'll use the first onsize callback as an indication that video has started
代码语言:txt
复制
  // playing out.
代码语言:txt
复制
  if (startTime) {
代码语言:txt
复制
    const elapsedTime = window.performance.now() - startTime;
代码语言:txt
复制
    console.log('Setup time: ' + elapsedTime.toFixed(3) + 'ms');
代码语言:txt
复制
    startTime = null;
代码语言:txt
复制
  }
代码语言:txt
复制
});
代码语言:txt
复制
let localStream;
代码语言:txt
复制
let pc1;
代码语言:txt
复制
let pc2;
代码语言:txt
复制
const offerOptions = {
代码语言:txt
复制
  offerToReceiveAudio: 1,
代码语言:txt
复制
  offerToReceiveVideo: 1,
代码语言:txt
复制
  iceRestart:true,
代码语言:txt
复制
  voiceActivityDetection: true
代码语言:txt
复制
};
代码语言:txt
复制
function getName(pc) {
代码语言:txt
复制
  return (pc === pc1) ? 'pc1' : 'pc2';
代码语言:txt
复制
}
代码语言:txt
复制
function getOtherPc(pc) {
代码语言:txt
复制
  return (pc === pc1) ? pc2 : pc1;
代码语言:txt
复制
}
代码语言:txt
复制
//start the video stream
代码语言:txt
复制
async function start() {
代码语言:txt
复制
  console.log('Requesting local stream');
代码语言:txt
复制
  startButton.disabled = true;
代码语言:txt
复制
  stopButton.disabled = false;
代码语言:txt
复制
  try {
代码语言:txt
复制
    const stream = await navigator.mediaDevices.getUserMedia({audio: true, video: true});
代码语言:txt
复制
    weblog('Received local stream');
代码语言:txt
复制
    localVideo.srcObject = stream;
代码语言:txt
复制
    localStream = stream;
代码语言:txt
复制
    callButton.disabled = false;
代码语言:txt
复制
  } catch (e) {
代码语言:txt
复制
    alert(`getUserMedia() error: ${e.name}`);
代码语言:txt
复制
  }
代码语言:txt
复制
}
代码语言:txt
复制
  function stop(e) {
代码语言:txt
复制
        const stream = localVideo.srcObject;
代码语言:txt
复制
        const tracks = stream.getTracks();
代码语言:txt
复制
        e.target.disabled = true;
代码语言:txt
复制
        startButton.disabled = false;
代码语言:txt
复制
        callButton.disabled = true;
代码语言:txt
复制
        tracks.forEach(function(track) {
代码语言:txt
复制
            track.stop();
代码语言:txt
复制
        });
代码语言:txt
复制
        localVideo.srcObject = null;
代码语言:txt
复制
    }
代码语言:txt
复制
  function getSelectedSdpSemantics() {
代码语言:txt
复制
    const sdpSemanticsSelect = document.querySelector('#sdpSemantics');
代码语言:txt
复制
    const option = sdpSemanticsSelect.options[sdpSemanticsSelect.selectedIndex];
代码语言:txt
复制
    return option.value === '' ? {} : {sdpSemantics: option.value};
代码语言:txt
复制
  }
代码语言:txt
复制
  //call the remote peer
代码语言:txt
复制
  async function call() {
代码语言:txt
复制
    callButton.disabled = true;
代码语言:txt
复制
    hangupButton.disabled = false;
代码语言:txt
复制
    weblog('Starting call');
代码语言:txt
复制
    startTime = window.performance.now();
代码语言:txt
复制
    const videoTracks = localStream.getVideoTracks();
代码语言:txt
复制
    const audioTracks = localStream.getAudioTracks();
代码语言:txt
复制
    if (videoTracks.length > 0) {
代码语言:txt
复制
      weblog(`Using video device: ${videoTracks[0].label}`);
代码语言:txt
复制
    }
代码语言:txt
复制
    if (audioTracks.length > 0) {
代码语言:txt
复制
      weblog(`Using audio device: ${audioTracks[0].label}`);
代码语言:txt
复制
    }
代码语言:txt
复制
    const configuration = getSelectedSdpSemantics();
代码语言:txt
复制
    weblog('RTCPeerConnection configuration:', configuration);
代码语言:txt
复制
    pc1 = new RTCPeerConnection(configuration);
代码语言:txt
复制
    weblog('Created local peer connection object pc1');
代码语言:txt
复制
    pc1.addEventListener('icecandidate', e => onIceCandidate(pc1, e));
代码语言:txt
复制
    pc2 = new RTCPeerConnection(configuration);
代码语言:txt
复制
    weblog('Created remote peer connection object pc2');
代码语言:txt
复制
    pc2.addEventListener('icecandidate', e => onIceCandidate(pc2, e));
代码语言:txt
复制
    pc1.addEventListener('iceconnectionstatechange', e => onIceStateChange(pc1, e));
代码语言:txt
复制
    pc2.addEventListener('iceconnectionstatechange', e => onIceStateChange(pc2, e));
代码语言:txt
复制
    pc2.addEventListener('track', gotRemoteStream);
代码语言:txt
复制
    localStream.getTracks().forEach(track => pc1.addTrack(track, localStream));
代码语言:txt
复制
    weblog('Added local stream to pc1');
代码语言:txt
复制
    try {
代码语言:txt
复制
      weblog('pc1 createOffer start');
代码语言:txt
复制
      const offer = await pc1.createOffer(offerOptions);
代码语言:txt
复制
      await onCreateOfferSuccess(offer);
代码语言:txt
复制
    } catch (e) {
代码语言:txt
复制
      onCreateSessionDescriptionError(e);
代码语言:txt
复制
    }
代码语言:txt
复制
  }
代码语言:txt
复制
  function onCreateSessionDescriptionError(error) {
代码语言:txt
复制
    console.log(`Failed to create session description: ${error.toString()}`);
代码语言:txt
复制
  }
代码语言:txt
复制
  async function onCreateOfferSuccess(desc) {
代码语言:txt
复制
    weblog(`Offer from pc1\n${desc.sdp}`);
代码语言:txt
复制
    weblog('pc1 setLocalDescription start');
代码语言:txt
复制
    try {
代码语言:txt
复制
      await pc1.setLocalDescription(desc);
代码语言:txt
复制
      onSetLocalSuccess(pc1);
代码语言:txt
复制
    } catch (e) {
代码语言:txt
复制
      onSetSessionDescriptionError();
代码语言:txt
复制
    }
代码语言:txt
复制
    weblog('pc2 setRemoteDescription start');
代码语言:txt
复制
    try {
代码语言:txt
复制
      await pc2.setRemoteDescription(desc);
代码语言:txt
复制
      onSetRemoteSuccess(pc2);
代码语言:txt
复制
    } catch (e) {
代码语言:txt
复制
      onSetSessionDescriptionError();
代码语言:txt
复制
    }
代码语言:txt
复制
    weblog('pc2 createAnswer start');
代码语言:txt
复制
    // Since the 'remote' side has no media stream we need
代码语言:txt
复制
    // to pass in the right constraints in order for it to
代码语言:txt
复制
    // accept the incoming offer of audio and video.
代码语言:txt
复制
    try {
代码语言:txt
复制
      const answer = await pc2.createAnswer();
代码语言:txt
复制
      await onCreateAnswerSuccess(answer);
代码语言:txt
复制
    } catch (e) {
代码语言:txt
复制
      onCreateSessionDescriptionError(e);
代码语言:txt
复制
    }
代码语言:txt
复制
  }
代码语言:txt
复制
  function onSetLocalSuccess(pc) {
代码语言:txt
复制
    weblog(`${getName(pc)} setLocalDescription complete`);
代码语言:txt
复制
  }
代码语言:txt
复制
  function onSetRemoteSuccess(pc) {
代码语言:txt
复制
    weblog(`${getName(pc)} setRemoteDescription complete`);
代码语言:txt
复制
  }
代码语言:txt
复制
  function onSetSessionDescriptionError(error) {
代码语言:txt
复制
    weblog(`Failed to set session description: ${error.toString()}`);
代码语言:txt
复制
  }
代码语言:txt
复制
  function gotRemoteStream(e) {
代码语言:txt
复制
    if (remoteVideo.srcObject !== e.streams[0]) {
代码语言:txt
复制
      remoteVideo.srcObject = e.streams[0];
代码语言:txt
复制
      weblog('pc2 received remote stream');
代码语言:txt
复制
    }
代码语言:txt
复制
  }
代码语言:txt
复制
  async function onCreateAnswerSuccess(desc) {
代码语言:txt
复制
    weblog(`Answer from pc2:\n${desc.sdp}`);
代码语言:txt
复制
    weblog('pc2 setLocalDescription start');
代码语言:txt
复制
    try {
代码语言:txt
复制
      await pc2.setLocalDescription(desc);
代码语言:txt
复制
      onSetLocalSuccess(pc2);
代码语言:txt
复制
    } catch (e) {
代码语言:txt
复制
      onSetSessionDescriptionError(e);
代码语言:txt
复制
    }
代码语言:txt
复制
    console.log('pc1 setRemoteDescription start');
代码语言:txt
复制
    try {
代码语言:txt
复制
      await pc1.setRemoteDescription(desc);
代码语言:txt
复制
      onSetRemoteSuccess(pc1);
代码语言:txt
复制
    } catch (e) {
代码语言:txt
复制
      onSetSessionDescriptionError(e);
代码语言:txt
复制
    }
代码语言:txt
复制
  }
代码语言:txt
复制
  async function onIceCandidate(pc, event) {
代码语言:txt
复制
    try {
代码语言:txt
复制
      await (getOtherPc(pc).addIceCandidate(event.candidate));
代码语言:txt
复制
      onAddIceCandidateSuccess(pc);
代码语言:txt
复制
    } catch (e) {
代码语言:txt
复制
      onAddIceCandidateError(pc, e);
代码语言:txt
复制
    }
代码语言:txt
复制
    console.log(`${getName(pc)} ICE candidate:\n${event.candidate ? event.candidate.candidate : '(null)'}`);
代码语言:txt
复制
  }
代码语言:txt
复制
  function onAddIceCandidateSuccess(pc) {
代码语言:txt
复制
    weblog(`${getName(pc)} addIceCandidate success`);
代码语言:txt
复制
  }
代码语言:txt
复制
  function onAddIceCandidateError(pc, error) {
代码语言:txt
复制
    weblog(`${getName(pc)} failed to add ICE Candidate: ${error.toString()}`);
代码语言:txt
复制
  }
代码语言:txt
复制
  function onIceStateChange(pc, event) {
代码语言:txt
复制
    if (pc) {
代码语言:txt
复制
      weblog(`${getName(pc)} ICE state: ${pc.iceConnectionState}`);
代码语言:txt
复制
      weblog('ICE state change event: ', event);
代码语言:txt
复制
    }
代码语言:txt
复制
  }
代码语言:txt
复制
  function hangup() {
代码语言:txt
复制
    weblog('Ending call');
代码语言:txt
复制
    pc1.close();
代码语言:txt
复制
    pc2.close();
代码语言:txt
复制
    pc1 = null;
代码语言:txt
复制
    pc2 = null;
代码语言:txt
复制
    hangupButton.disabled = true;
代码语言:txt
复制
    callButton.disabled = false;
代码语言:txt
复制
  }
代码语言:txt
复制
  async function displaySdp() {
代码语言:txt
复制
    const configuration = getSelectedSdpSemantics();
代码语言:txt
复制
    let peerConnection = new RTCPeerConnection(configuration);
代码语言:txt
复制
    const offer = await peerConnection.createOffer(offerOptions);
代码语言:txt
复制
    await peerConnection.setLocalDescription(offer);
代码语言:txt
复制
    outputTextarea.value = offer.sdp;
代码语言:txt
复制
  }

整个程序实现可以在这里访问

https://www.fanyamin.com/webrtc/examples/local_peer_connection.html

我在页面了把整个连接的步骤打印了出来

代码语言:txt
复制
[49.461] Received local stream
代码语言:txt
复制
[72.743] Starting call
代码语言:txt
复制
[72.743] Using video device: USB Video Device (046d:081d)
代码语言:txt
复制
[72.743] Using audio device: 默认 - 麦克风 (USB Audio Device) (046d:081d)
代码语言:txt
复制
[72.743] RTCPeerConnection configuration:
代码语言:txt
复制
[72.745] Created local peer connection object pc1
代码语言:txt
复制
[72.746] Created remote peer connection object pc2
代码语言:txt
复制
[72.746] Added local stream to pc1
代码语言:txt
复制
[72.747] pc1 createOffer start
代码语言:txt
复制
/*
* 提示:该行代码过长,系统自动注释不进行高亮。一键复制会移除系统注释 
* [72.765] Offer from pc1 v=0 o=- 8212739043455445815 2 IN IP4 127.0.0.1 s=- t=0 0 a=group:BUNDLE 0 1 a=msid-semantic: WMS xlhFA5NOFj9VpQ7Z1ylg9jmfNytu6l7jTKhQ m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126 c=IN IP4 0.0.0.0 a=rtcp:9 IN IP4 0.0.0.0 a=ice-ufrag:nTn7 a=ice-pwd:1XyHlJ5xBTJ7NBuU5Y5mBqCn a=ice-options:trickle a=fingerprint:sha-256 51:62:BB:13:05:A7:38:05:47:78:BA:70:A6:A7:64:29:6C:45:00:AC:B3:7F:92:45:80:F5:5A:4B:10:7A:36:42 a=setup:actpass a=mid:0 a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id a=sendrecv a=msid:xlhFA5NOFj9VpQ7Z1ylg9jmfNytu6l7jTKhQ 2cbf5334-0e08-4985-817f-a666c35b633b a=rtcp-mux a=rtpmap:111 opus/48000/2 a=rtcp-fb:111 transport-cc a=fmtp:111 minptime=10;useinbandfec=1 a=rtpmap:103 ISAC/16000 a=rtpmap:104 ISAC/32000 a=rtpmap:9 G722/8000 a=rtpmap:0 PCMU/8000 a=rtpmap:8 PCMA/8000 a=rtpmap:106 CN/32000 a=rtpmap:105 CN/16000 a=rtpmap:13 CN/8000 a=rtpmap:110 telephone-event/48000 a=rtpmap:112 telephone-event/32000 a=rtpmap:113 telephone-event/16000 a=rtpmap:126 telephone-event/8000 a=ssrc:2931786518 cname:JVCgjci5cC/oQHwi a=ssrc:2931786518 msid:xlhFA5NOFj9VpQ7Z1ylg9jmfNytu6l7jTKhQ 2cbf5334-0e08-4985-817f-a666c35b633b a=ssrc:2931786518 mslabel:xlhFA5NOFj9VpQ7Z1ylg9jmfNytu6l7jTKhQ a=ssrc:2931786518 label:2cbf5334-0e08-4985-817f-a666c35b633b 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 c=IN IP4 0.0.0.0 a=rtcp:9 IN IP4 0.0.0.0 a=ice-ufrag:nTn7 a=ice-pwd:1XyHlJ5xBTJ7NBuU5Y5mBqCn a=ice-options:trickle a=fingerprint:sha-256 51:62:BB:13:05:A7:38:05:47:78:BA:70:A6:A7:64:29:6C:45:00:AC:B3:7F:92:45:80:F5:5A:4B:10:7A:36:42 a=setup:actpass a=mid:1 a=extmap:14 urn:ietf:params:rtp-hdrext:toffset a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time a=extmap:13 urn:3gpp:video-orientation a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 a=extmap:12 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay a=extmap:11 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id a=sendrecv a=msid:xlhFA5NOFj9VpQ7Z1ylg9jmfNytu6l7jTKhQ 906550e7-7a71-4c6a-a7ca-8f81fa0efe6c a=rtcp-mux a=rtcp-rsize a=rtpmap:96 VP8/90000 a=rtcp-fb:96 goog-remb a=rtcp-fb:96 transport-cc a=rtcp-fb:96 ccm fir a=rtcp-fb:96 nack a=rtcp-fb:96 nack pli a=rtpmap:97 rtx/90000 a=fmtp:97 apt=96 a=rtpmap:98 VP9/90000 a=rtcp-fb:98 goog-remb a=rtcp-fb:98 transport-cc a=rtcp-fb:98 ccm fir a=rtcp-fb:98 nack a=rtcp-fb:98 nack pli a=fmtp:98 profile-id=0 a=rtpmap:99 rtx/90000 a=fmtp:99 apt=98 a=rtpmap:100 VP9/90000 a=rtcp-fb:100 goog-remb a=rtcp-fb:100 transport-cc a=rtcp-fb:100 ccm fir a=rtcp-fb:100 nack a=rtcp-fb:100 nack pli a=fmtp:100 profile-id=2 a=rtpmap:101 rtx/90000 a=fmtp:101 apt=100 a=rtpmap:102 H264/90000 a=rtcp-fb:102 goog-remb a=rtcp-fb:102 transport-cc a=rtcp-fb:102 ccm fir a=rtcp-fb:102 nack a=rtcp-fb:102 nack pli a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f a=rtpmap:121 rtx/90000 a=fmtp:121 apt=102 a=rtpmap:127 H264/90000 a=rtcp-fb:127 goog-remb a=rtcp-fb:127 transport-cc a=rtcp-fb:127 ccm fir a=rtcp-fb:127 nack a=rtcp-fb:127 nack pli a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f a=rtpmap:120 rtx/90000 a=fmtp:120 apt=127 a=rtpmap:125 H264/90000 a=rtcp-fb:125 goog-remb a=rtcp-fb:125 transport-cc a=rtcp-fb:125 ccm fir a=rtcp-fb:125 nack a=rtcp-fb:125 nack pli a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f a=rtpmap:107 rtx/90000 a=fmtp:107 apt=125 a=rtpmap:108 H264/90000 a=rtcp-fb:108 goog-remb a=rtcp-fb:108 transport-cc a=rtcp-fb:108 ccm fir a=rtcp-fb:108 nack a=rtcp-fb:108 nack pli a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f a=rtpmap:109 rtx/90000 a=fmtp:109 apt=108 a=rtpmap:124 H264/90000 a=rtcp-fb:124 goog-remb a=rtcp-fb:124 transport-cc a=rtcp-fb:124 ccm fir a=rtcp-fb:124 nack a=rtcp-fb:124 nack pli a=fmtp:124 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d001f a=rtpmap:119 rtx/90000 a=fmtp:119 apt=124 a=rtpmap:123 H264/90000 a=rtcp-fb:123 goog-remb a=rtcp-fb:123 transport-cc a=rtcp-fb:123 ccm fir a=rtcp-fb:123 nack a=rtcp-fb:123 nack pli a=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=64001f a=rtpmap:118 rtx/90000 a=fmtp:118 apt=123 a=rtpmap:114 red/90000 a=rtpmap:115 rtx/90000 a=fmtp:115 apt=114 a=rtpmap:116 ulpfec/90000 a=ssrc-group:FID 818575976 4014440443 a=ssrc:818575976 cname:JVCgjci5cC/oQHwi a=ssrc:818575976 msid:xlhFA5NOFj9VpQ7Z1ylg9jmfNytu6l7jTKhQ 906550e7-7a71-4c6a-a7ca-8f81fa0efe6c a=ssrc:818575976 mslabel:xlhFA5NOFj9VpQ7Z1ylg9jmfNytu6l7jTKhQ a=ssrc:818575976 label:906550e7-7a71-4c6a-a7ca-8f81fa0efe6c a=ssrc:4014440443 cname:JVCgjci5cC/oQHwi a=ssrc:4014440443 msid:xlhFA5NOFj9VpQ7Z1ylg9jmfNytu6l7jTKhQ 906550e7-7a71-4c6a-a7ca-8f81fa0efe6c a=ssrc:4014440443 mslabel:xlhFA5NOFj9VpQ7Z1ylg9jmfNytu6l7jTKhQ a=ssrc:4014440443 label:906550e7-7a71-4c6a-a7ca-8f81fa0efe6c
*/
代码语言:txt
复制
[72.765] pc1 setLocalDescription start
代码语言:txt
复制
[72.772] pc1 setLocalDescription complete
代码语言:txt
复制
[72.772] pc2 setRemoteDescription start
代码语言:txt
复制
[72.937] pc2 received remote stream
代码语言:txt
复制
[72.937] pc2 setRemoteDescription complete
代码语言:txt
复制
[72.937] pc2 createAnswer start
代码语言:txt
复制
[72.959] Answer from pc2: v=0 o=- 1094912348166165889 2 IN IP4 127.0.0.1 s=- t=0 0 a=group:BUNDLE 0 1 a=msid-semantic: WMS m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126 c=IN IP4 0.0.0.0 a=rtcp:9 IN IP4 0.0.0.0 a=ice-ufrag:MIpG a=ice-pwd:yeUliWqTGocmk3MfGp8WnL4z a=ice-options:trickle a=fingerprint:sha-256 94:06:0A:3C:17:86:C7:D3:BB:3F:DE:D6:8D:4A:C6:FC:FE:08:69:86:74:22:B7:7A:58:2D:49:F0:3B:97:83:6B a=setup:active a=mid:0 a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id a=recvonly a=rtcp-mux a=rtpmap:111 opus/48000/2 a=rtcp-fb:111 transport-cc a=fmtp:111 minptime=10;useinbandfec=1 a=rtpmap:103 ISAC/16000 a=rtpmap:104 ISAC/32000 a=rtpmap:9 G722/8000 a=rtpmap:0 PCMU/8000 a=rtpmap:8 PCMA/8000 a=rtpmap:106 CN/32000 a=rtpmap:105 CN/16000 a=rtpmap:13 CN/8000 a=rtpmap:110 telephone-event/48000 a=rtpmap:112 telephone-event/32000 a=rtpmap:113 telephone-event/16000 a=rtpmap:126 telephone-event/8000 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 c=IN IP4 0.0.0.0 a=rtcp:9 IN IP4 0.0.0.0 a=ice-ufrag:MIpG a=ice-pwd:yeUliWqTGocmk3MfGp8WnL4z a=ice-options:trickle a=fingerprint:sha-256 94:06:0A:3C:17:86:C7:D3:BB:3F:DE:D6:8D:4A:C6:FC:FE:08:69:86:74:22:B7:7A:58:2D:49:F0:3B:97:83:6B a=setup:active a=mid:1 a=extmap:14 urn:ietf:params:rtp-hdrext:toffset a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time a=extmap:13 urn:3gpp:video-orientation a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 a=extmap:12 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay a=extmap:11 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id a=recvonly a=rtcp-mux a=rtcp-rsize a=rtpmap:96 VP8/90000 a=rtcp-fb:96 goog-remb a=rtcp-fb:96 transport-cc a=rtcp-fb:96 ccm fir a=rtcp-fb:96 nack a=rtcp-fb:96 nack pli a=rtpmap:97 rtx/90000 a=fmtp:97 apt=96 a=rtpmap:98 VP9/90000 a=rtcp-fb:98 goog-remb a=rtcp-fb:98 transport-cc a=rtcp-fb:98 ccm fir a=rtcp-fb:98 nack a=rtcp-fb:98 nack pli a=fmtp:98 profile-id=0 a=rtpmap:99 rtx/90000 a=fmtp:99 apt=98 a=rtpmap:100 VP9/90000 a=rtcp-fb:100 goog-remb a=rtcp-fb:100 transport-cc a=rtcp-fb:100 ccm fir a=rtcp-fb:100 nack a=rtcp-fb:100 nack pli a=fmtp:100 profile-id=2 a=rtpmap:101 rtx/90000 a=fmtp:101 apt=100 a=rtpmap:102 H264/90000 a=rtcp-fb:102 goog-remb a=rtcp-fb:102 transport-cc a=rtcp-fb:102 ccm fir a=rtcp-fb:102 nack a=rtcp-fb:102 nack pli a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f a=rtpmap:121 rtx/90000 a=fmtp:121 apt=102 a=rtpmap:127 H264/90000 a=rtcp-fb:127 goog-remb a=rtcp-fb:127 transport-cc a=rtcp-fb:127 ccm fir a=rtcp-fb:127 nack a=rtcp-fb:127 nack pli a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f a=rtpmap:120 rtx/90000 a=fmtp:120 apt=127 a=rtpmap:125 H264/90000 a=rtcp-fb:125 goog-remb a=rtcp-fb:125 transport-cc a=rtcp-fb:125 ccm fir a=rtcp-fb:125 nack a=rtcp-fb:125 nack pli a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f a=rtpmap:107 rtx/90000 a=fmtp:107 apt=125 a=rtpmap:108 H264/90000 a=rtcp-fb:108 goog-remb a=rtcp-fb:108 transport-cc a=rtcp-fb:108 ccm fir a=rtcp-fb:108 nack a=rtcp-fb:108 nack pli a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f a=rtpmap:109 rtx/90000 a=fmtp:109 apt=108 a=rtpmap:124 H264/90000 a=rtcp-fb:124 goog-remb a=rtcp-fb:124 transport-cc a=rtcp-fb:124 ccm fir a=rtcp-fb:124 nack a=rtcp-fb:124 nack pli a=fmtp:124 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d0015 a=rtpmap:119 rtx/90000 a=fmtp:119 apt=124 a=rtpmap:123 H264/90000 a=rtcp-fb:123 goog-remb a=rtcp-fb:123 transport-cc a=rtcp-fb:123 ccm fir a=rtcp-fb:123 nack a=rtcp-fb:123 nack pli a=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640015 a=rtpmap:118 rtx/90000 a=fmtp:118 apt=123 a=rtpmap:114 red/90000 a=rtpmap:115 rtx/90000 a=fmtp:115 apt=114 a=rtpmap:116 ulpfec/90000
代码语言:txt
复制
[72.959] pc2 setLocalDescription start
代码语言:txt
复制
[72.960] pc1 addIceCandidate success
代码语言:txt
复制
[72.961] pc1 addIceCandidate success
代码语言:txt
复制
[72.961] pc1 addIceCandidate success
代码语言:txt
复制
[72.964] pc1 addIceCandidate success
代码语言:txt
复制
[72.964] pc1 addIceCandidate success
代码语言:txt
复制
[72.964] pc1 addIceCandidate success
代码语言:txt
复制
[72.964] pc1 addIceCandidate success
代码语言:txt
复制
[72.965] pc1 addIceCandidate success
代码语言:txt
复制
[72.965] pc1 addIceCandidate success
代码语言:txt
复制
[72.965] pc1 addIceCandidate success
代码语言:txt
复制
[72.965] pc1 addIceCandidate success
代码语言:txt
复制
[72.965] pc1 addIceCandidate success
代码语言:txt
复制
[72.965] pc1 addIceCandidate success
代码语言:txt
复制
[73.292] pc2 setLocalDescription complete
代码语言:txt
复制
[73.294] pc2 ICE state: checking
代码语言:txt
复制
[73.294] ICE state change event:
代码语言:txt
复制
[73.295] pc1 ICE state: checking
代码语言:txt
复制
[73.295] ICE state change event:
代码语言:txt
复制
[73.295] pc2 ICE state: connected
代码语言:txt
复制
[73.295] ICE state change event:
代码语言:txt
复制
[73.297] pc1 ICE state: connected
代码语言:txt
复制
[73.297] ICE state change event:
代码语言:txt
复制
[73.297] pc1 setRemoteDescription complete
代码语言:txt
复制
[73.305] pc2 addIceCandidate success
代码语言:txt
复制
[73.305] pc2 addIceCandidate success
代码语言:txt
复制
[73.305] pc2 addIceCandidate success
代码语言:txt
复制
[73.306] pc2 addIceCandidate success

本地连接等于是自己连自己,这里的核心方法是 call(), 它创建两个 PeerConnection -- pc1 和 pc 2,

代码语言:txt
复制
 async function call() {
代码语言:txt
复制
    callButton.disabled = true;
代码语言:txt
复制
    hangupButton.disabled = false;
代码语言:txt
复制
    weblog('Starting call');
代码语言:txt
复制
    startTime = window.performance.now();
代码语言:txt
复制
    const videoTracks = localStream.getVideoTracks();
代码语言:txt
复制
    const audioTracks = localStream.getAudioTracks();
代码语言:txt
复制
    if (videoTracks.length > 0) {
代码语言:txt
复制
      weblog(`Using video device: ${videoTracks[0].label}`);
代码语言:txt
复制
    }
代码语言:txt
复制
    if (audioTracks.length > 0) {
代码语言:txt
复制
      weblog(`Using audio device: ${audioTracks[0].label}`);
代码语言:txt
复制
    }
代码语言:txt
复制
    const configuration = getSelectedSdpSemantics();
代码语言:txt
复制
    weblog('RTCPeerConnection configuration:', configuration);
代码语言:txt
复制
    pc1 = new RTCPeerConnection(configuration);
代码语言:txt
复制
    weblog('Created local peer connection object pc1');
代码语言:txt
复制
    pc1.addEventListener('icecandidate', e => onIceCandidate(pc1, e));
代码语言:txt
复制
    pc2 = new RTCPeerConnection(configuration);
代码语言:txt
复制
    weblog('Created remote peer connection object pc2');
代码语言:txt
复制
    pc2.addEventListener('icecandidate', e => onIceCandidate(pc2, e));
代码语言:txt
复制
    pc1.addEventListener('iceconnectionstatechange', e => onIceStateChange(pc1, e));
代码语言:txt
复制
    pc2.addEventListener('iceconnectionstatechange', e => onIceStateChange(pc2, e));
代码语言:txt
复制
    pc2.addEventListener('track', gotRemoteStream);
代码语言:txt
复制
    localStream.getTracks().forEach(track => pc1.addTrack(track, localStream));
代码语言:txt
复制
    weblog('Added local stream to pc1');
代码语言:txt
复制
    try {
代码语言:txt
复制
      weblog('pc1 createOffer start');
代码语言:txt
复制
      const offer = await pc1.createOffer(offerOptions);
代码语言:txt
复制
      await onCreateOfferSuccess(offer);
代码语言:txt
复制
    } catch (e) {
代码语言:txt
复制
      onCreateSessionDescriptionError(e);
代码语言:txt
复制
    }
代码语言:txt
复制
  }

代码中的主要流程分两条线

  1. SDP 协商
  2. ICE 检查

注意 WebRTC 支持 "Trickling ICE" , 针对多个 candidate pair , 这个过程有多次

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
作者已关闭评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 本地对等连接
相关产品与服务
对等连接
对等连接(Peering Connection,PC)是一种大带宽、高质量的云上资源互通服务,可以帮助您打通腾讯云上的资源通信链路。 对等连接具有多区域、多账户、多种网络异构互通等特点,轻松实现云上两地三中心、游戏同服等复杂网络场景;支持 VPC 网络与基础网络、黑石网络互通,满足您不同业务的部署需求。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档