PC上のブラウザ「Chrome」間で、WebRTCを使ってカメラ映像を配信します。

WebRTC で動画配信

Chrome上で動作するSenderと Receiver を作成し、Sender にUSBカメラを接続し、Receiver にカメラ映像を配信します。

Chrome間で P2P を使ってWebRTC するためには、次のような交換(ピアリング)を行う必要があります。このピアリングは今回は、手動(コピペ)で行います。今回は全てのICE candidateが出そろった後に、SDPとまとめて交換するVanilla ICEを使用します。

  • SDP で、最適な p2p 通信を確立するために、お互いのメディアストリームに関する情報などを交換します。SDPには通信を始める側(Offer)と、通信を受け入れる側(Answer)があります。必ずOffer → Answerの順番でやりとりする必要があります。
  • ICE candidateで、どのような通信経路が使えるかをお互いに確認しあって、通信経路を決めます。ICE candidateには、全てのICE candidateが出そろった後に、SDPとまとめて交換するVanilla ICE と 、初期のSDPを交換し、その後ICE Candidateを順次交換するTrickle ICEがあります。

動画配信アプリの作成

動画配信アプリはBootstrap 5とjquery 3で作成します。

動画配信アプリの受信側を次に示します。

receiver.html

<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"
    integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.0/font/bootstrap-icons.css">
  <link rel="stylesheet" href="css/style.css">

  <title>WebRTC Test 受信側</title>
</head>

<body>
  <div class="container">
    <div class="row justify-content-center mt-2 mb-2">

      <h2 class="text-center">WebRTC Test 受信側</h2>


      <div class="col-6 ">
        <video id="remote_video" autoplay></video>
      </div>

      <div class="col-6 ">
        <div class="form-group">
          <label for="text_for_send_sdp">SDP to send:</label>
          <textarea id="text_for_send_sdp" class="form-control" readonly></textarea>
        </div>

      </div>
    </div>
    <div class="row justify-content-center mt-2 mb-2">

      <div class="col-6 ">
        <div class="form-group">
          <label for="text_for_receive_sdp">SDP to send:</label>
          <textarea id="text_for_receive_sdp" class="form-control"></textarea>
        </div>

      </div>

      <div class="col-6 ">
        <button type="button" class="btn btn-primary" id="remote">Receive remote SDP</button>
      </div>

      <p>SDP to receive:&nbsp;
      </p>
    </div>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
    integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
    crossorigin="anonymous"></script>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

  <script src="js/main.js"></script>


</body>



</html>

動画配信アプリの送信側を次に示します。

sender.html

<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"
    integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.0/font/bootstrap-icons.css">
  <link rel="stylesheet" href="css/style.css">

  <title>WebRTC Test 送信側</title>
</head>

<body>
  <div class="container">
    <div class="row justify-content-center mt-2 mb-2">
      <h2 class="text-center">WebRTC Test 送信側</h2>

      <div class="col-6 ">
        <video id="local_video" autoplay></video>
      </div>

      <div class="col-6">
        <button type="button" class="btn btn-primary" id="play">Play</button>
        <button type="button" class="btn btn-primary" id="stop">Stop</button>
        <button type="button" class="btn btn-primary" id="connect">Connect</button>
        <button type="button" class="btn btn-primary" id="disconnect">Disconnect</button>
      </div>
    </div>
    <div class="row justify-content-center mt-2 mb-2">
      <div class="col-6 ">
        <div class="form-group">
          <label for="text_for_send_sdp">SDP to send:</label>
          <textarea id="text_for_send_sdp" class="form-control" readonly></textarea>
        </div>
      </div>
      <div class="col-6 ">
      </div>
    </div>
    <div class="row justify-content-center mt-2 mb-2">
      <div class="col-6 ">
        <div class="form-group">
          <label for="text_for_receive_sdp">SDP to receive:</label>
          <textarea id="text_for_receive_sdp" class="form-control"></textarea>
        </div>
      </div>
      <div class="col-6 ">
        <button type="button" class="btn btn-primary" id="remote">Receive remote SDP</button>
      </div>

    </div>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
    integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
    crossorigin="anonymous"></script>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

  <script src="js/main.js"></script>


</body>
</html>

使用するWeb APIのリファレンスは、「開発者向けのウェブ技術」に示します。

  1. 接続されているUSBカメラからの映像入力
    • 「Play」ボタンをクリックすると、126行目のnavigator.mediaDevices.getUserMedia() により、要求された種類のメディアを含むトラックを持つ MediaStream の使用許可をユーザーに求めます。
    • 成功すると、50行目のplayVideo()で再生を始めます。

  2. Offer SDPの生成
    • 「Connect」ボタンをクリックすると、174行目でRTCPeerConnection のオブジェクトを生成します。
    • 223行目のRTCPeerConnection.createOffer() で Offer SDPを生成します。
    • 226行目のRTCPeerConnection.setLocalDescription()でローカルSDPとして設定します。

  3. ICE candidateの収集
    • ICE candidate の収集が始まると、「ICE gathering state change」イベントが発生しします。193行目のRTCPeerConnection.onicecandidateにイベントハンドラを記述します。
    • このイベントは複数回発生し、全てのICE candidateを収集し終わると空のイベントが発生します。
    • 空のイベントが発生すると、204行目でlocalDescriptionをパラメータとしたsendSdp()の中で、テキストエリアにOffer SDPを表示します。

  4. Offser SDPの受信
    • 応答側にOffer SDPをペーストして「Receive remote SDP」ボタンをクリックすると、88行目でonSdpText() → 108行目でsetOffer() と呼び出されます。
    • 238行目のsetOffer()の中では、PeerConnectionのオブジェクトを生成し、受け取ったOffer SDPを setRemoteDescription()で設定します。
    • 成功すると、 252行目のmakeAnswer()を呼び出します。

  5. Answer SDPを生成・送信
    • 259行目のRTCPeerConnection.createAnswer() で Answer SDPを生成します。
    • 生成したAnswer SDPを、262行目のRTCPeerConnection.setLocalDescription()で設定します。
    • この後 、193行目のRTCPeerConnection.onicecandidate()でICE candidateを収集し、すべて揃ったらsendSdp()でOffer側に送り返します。

  6. Answer SDPの受信
    • 発信側にAnser SDPをペーストして「Receive remote SDP」ボタンをクリックすると、88行目でonSdpText() → 274行目でsetAnswer() と呼び出されます。
    • setAnswer()の中で、280行目でRTCPeerConnection.setRemoteDescription()で受け取ったSDPを設定します。

  7. 映像/音声の送受信
    • PeerConnectionのオブジェクトを生成した際に、送信する映像/音声ストリームを212行目のRTCPeerConnection.addStream()で指定します。
    • SDPの交換が終わると、P2P通信に相手の映像/音声が含まれていればイベントが発生するため、178行目のRTCPeerConnection.ontrack() にハンドラを記述します。

js/main.js

let localVideo = document.getElementById('local_video');
let remoteVideo = document.getElementById('remote_video');
let localStream = null;
let peerConnection = null;
let textForSendSdp = document.getElementById('text_for_send_sdp');
let textToReceiveSdp = document.getElementById('text_for_receive_sdp');

// --- prefix -----
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia ||
  navigator.mozGetUserMedia || navigator.msGetUserMedia;
RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection;
RTCSessionDescription = window.RTCSessionDescription || window.webkitRTCSessionDescription || window.mozRTCSessionDescription;

$(document).ready(function () {
  console.log('start:');

});

$('#play').on('click', function () {
  console.log('カメラキャプチャ開始\n');
  startVideo();
});

$('#stop').on('click', function () {
  console.log('カメラキャプチャ停止\n');
  stopVideo();
});
$('#connect').on('click', function () {
  console.log('ビデオストリーム開始\n');
  connect();
});
$('#disconnect').on('click', function () {
  console.log('ビデオストリーム停止\n');
  hangUp();
});

$('#remote').on('click', function () {
  console.log('シグナリング\n');
  onSdpText();
});



// ---------------------- media handling ----------------------- 
// start local video
function startVideo() {
  getDeviceStream({ video: true, audio: false })
    .then(function (stream) { // success
      localStream = stream;
      playVideo(localVideo, stream);
    }).catch(function (error) { // error
      console.error('getUserMedia error:', error);
      return;
    });
}

// stop local video
function stopVideo() {
  pauseVideo(localVideo);
  stopLocalStream(localStream);
}

// start PeerConnection
function connect() {
  if (!peerConnection) {
    console.log('make Offer');
    makeOffer();
  }
  else {
    console.warn('peer already exist.');
  }
}

// close PeerConnection
function hangUp() {
  if (peerConnection) {
    console.log('Hang up.');
    peerConnection.close();
    peerConnection = null;
    pauseVideo(remoteVideo);
  }
  else {
    console.warn('peer NOT exist.');
  }
}

// ----- hand signaling ----
function onSdpText() {
  let text = textToReceiveSdp.value;
  if (peerConnection) {
    console.log('Received answer text...');
    let answer = new RTCSessionDescription({
      type: 'answer',
      sdp: text,
    });
    setAnswer(answer);
  }
  else {
    console.log('Received offer text...');
    let offer = new RTCSessionDescription({
      type: 'offer',
      sdp: text,
    });
    setOffer(offer);
  }
  textToReceiveSdp.value = '';
}

//***************************

function stopLocalStream(stream) {
  let tracks = stream.getTracks();
  if (!tracks) {
    console.warn('NO tracks');
    return;
  }

  for (let track of tracks) {
    track.stop();
  }
}

function getDeviceStream(option) {
  if ('getUserMedia' in navigator.mediaDevices) {
    console.log('navigator.mediaDevices.getUserMadia');
    return navigator.mediaDevices.getUserMedia(option);
  }
  else {
    console.log('wrap navigator.getUserMadia with Promise');
    return new Promise(function (resolve, reject) {
      navigator.getUserMedia(option,
        resolve,
        reject
      );
    });
  }
}

function playVideo(element, stream) {
  if ('srcObject' in element) {
    element.srcObject = stream;
  }
  else {
    element.src = window.URL.createObjectURL(stream);
  }
  element.play();
  element.volume = 0;
}

function pauseVideo(element) {
  element.pause();
  if ('srcObject' in element) {
    element.srcObject = null;
  }
  else {
    if (element.src && (element.src !== '')) {
      window.URL.revokeObjectURL(element.src);
    }
    element.src = '';
  }
}


function sendSdp(sessionDescription) {
  console.log('---sending sdp ---');
  textForSendSdp.value = sessionDescription.sdp;
  textForSendSdp.focus();
  textForSendSdp.select();
}

// ---------------------- connection handling -----------------------
function prepareNewConnection() {
  let pc_config = { "iceServers": [] };
  let peer = new RTCPeerConnection(pc_config);

  // --- on get remote stream ---
  if ('ontrack' in peer) {
    peer.ontrack = function (event) {
      console.log('-- peer.ontrack()');
      let stream = event.streams[0];
      playVideo(remoteVideo, stream);
    };
  }
  else {
    peer.onaddstream = function (event) {
      console.log('-- peer.onaddstream()');
      let stream = event.stream;
      playVideo(remoteVideo, stream);
    };
  }

  // --- on get local ICE candidate
  peer.onicecandidate = function (evt) {
    if (evt.candidate) {
      console.log(evt.candidate);

      // Trickle ICE の場合は、ICE candidateを相手に送る
      // Vanilla ICE の場合には、何もしない
    } else {
      console.log('empty ice event');

      // Trickle ICE の場合は、何もしない
      // Vanilla ICE の場合には、ICE candidateを含んだSDPを相手に送る
      sendSdp(peer.localDescription);
    }
  };


  // -- add local stream --
  if (localStream) {
    console.log('Adding local stream...');
    peer.addStream(localStream);
  }
  else {
    console.warn('no local stream, but continue.');
  }

  return peer;
}

function makeOffer() {
  peerConnection = prepareNewConnection();
  peerConnection.createOffer()
    .then(function (sessionDescription) {
      console.log('createOffer() succsess in promise');
      return peerConnection.setLocalDescription(sessionDescription);
    }).then(function () {
      console.log('setLocalDescription() succsess in promise');

      // -- Trickle ICE の場合は、初期SDPを相手に送る -- 
      // -- Vanilla ICE の場合には、まだSDPは送らない --
      //sendSdp(peerConnection.localDescription);
    }).catch(function (err) {
      console.error(err);
    });
}

function setOffer(sessionDescription) {
  if (peerConnection) {
    console.error('peerConnection alreay exist!');
  }
  peerConnection = prepareNewConnection();
  peerConnection.setRemoteDescription(sessionDescription)
    .then(function () {
      console.log('setRemoteDescription(offer) succsess in promise');
      makeAnswer();
    }).catch(function (err) {
      console.error('setRemoteDescription(offer) ERROR: ', err);
    });
}

function makeAnswer() {
  console.log('sending Answer. Creating remote session description...');
  if (!peerConnection) {
    console.error('peerConnection NOT exist!');
    return;
  }

  peerConnection.createAnswer()
    .then(function (sessionDescription) {
      console.log('createAnswer() succsess in promise');
      return peerConnection.setLocalDescription(sessionDescription);
    }).then(function () {
      console.log('setLocalDescription() succsess in promise');

      // -- Trickle ICE の場合は、初期SDPを相手に送る -- 
      // -- Vanilla ICE の場合には、まだSDPは送らない --
      //sendSdp(peerConnection.localDescription);
    }).catch(function (err) {
      console.error(err);
    });
}

function setAnswer(sessionDescription) {
  if (!peerConnection) {
    console.error('peerConnection NOT exist!');
    return;
  }

  peerConnection.setRemoteDescription(sessionDescription)
    .then(function () {
      console.log('setRemoteDescription(answer) succsess in promise');
    }).catch(function (err) {
      console.error('setRemoteDescription(answer) ERROR: ', err);
    });
}

css/style.css

#local_video,
#remote_video {
    width: 320px;
    height: 240px;
    border: 1px solid rgb(219, 217, 217);
}

動画配信アプリの実行

USBカメラ「Logitech, Inc. HD Pro Webcam C920」をPCに接続し、PC上で構築したローカルのアプリケーションの開発環境「xampp」で、作成した動画配信アプリを実行します。

  1. Chromeから送信側「http://localhost/webrtc-test/sender.html」をアクセスします。
  2. Chromeから受信側「http://localhost/webrtc-test/receiver.html」をアクセスします。
  3. 送信側で「Play」ボタンをクリックすると、USBカメラの撮影動画が表示されます。
  4. 送信側で「Connect」ボタンをクリックすると、「SDP to send」テキストにOfferデータが表示されます。
  5. 受信側で、「SDP to receive」テキストエリアに送信側のOfferデータをコピーして、「Receive remote SDP」ボタンをクリックすると、「SDP to send」テキストエリアにAnswerデータが表示されます。
  6. 送信側で、「SDP to receive」テキストエリアに受信側のAnswerデータをコピーして、「Receive remote SDP」ボタンをクリックします。
  7. 受信側で、送信側から配信されたUSBカメラの撮影動画が表示されます。

Offerデータ

v=0
o=- 5835688284266751874 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0
a=extmap-allow-mixed
a=msid-semantic: WMS 1cfe8d2c-3481-415a-b1e8-b7c279fb5ade
m=video 56516 UDP/TLS/RTP/SAVPF 96 97 102 103 104 105 106 107 108 109 127 125 39 40 45 46 98 99 100 101 116 117 118
c=IN IP4 192.168.10.105
a=rtcp:9 IN IP4 0.0.0.0
a=candidate:2852683139 1 udp 2122129151 192.168.10.105 56516 typ host generation 0 network-id 1
a=candidate:1778395675 1 udp 2122063615 192.168.135.1 56517 typ host generation 0 network-id 4
a=candidate:3960311980 1 udp 2121998079 192.168.16.1 56518 typ host generation 0 network-id 5
a=candidate:2134324098 1 udp 2122262783 240b:11:1c80:3a00:9799:7da9:a9e4:299b 56519 typ host generation 0 network-id 2
a=candidate:3058385008 1 udp 2122197247 240b:11:1c80:3a00:cd58:73d7:d29:4ee2 56520 typ host generation 0 network-id 3
a=candidate:3569852187 1 tcp 1518149375 192.168.10.105 9 typ host tcptype active generation 0 network-id 1
a=candidate:349161603 1 tcp 1518083839 192.168.135.1 9 typ host tcptype active generation 0 network-id 4
a=candidate:2462216756 1 tcp 1518018303 192.168.16.1 9 typ host tcptype active generation 0 network-id 5
a=candidate:33083674 1 tcp 1518283007 240b:11:1c80:3a00:9799:7da9:a9e4:299b 9 typ host tcptype active generation 0 network-id 2
a=candidate:3364135656 1 tcp 1518217471 240b:11:1c80:3a00:cd58:73d7:d29:4ee2 9 typ host tcptype active generation 0 network-id 3
a=ice-ufrag:AKYh
a=ice-pwd:FBsbxYTq++oGs8aUYcYtEvV/
a=ice-options:trickle
a=fingerprint:sha-256 AB:FB:5E:21:E1:90:49:E8:9B:EC:56:C1:A7:C3:F6:6C:16:34:1A:C8:28:EB:D3:8F:5D:2B:30:F9:BB:43:82:EA
a=setup:actpass
a=mid:0
a=extmap:1 urn:ietf:params:rtp-hdrext:toffset
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 urn:3gpp:video-orientation
a=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=extmap:6 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:9 urn:ietf:params:rtp-hdrext:sdes:mid
a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
a=sendrecv
a=msid:1cfe8d2c-3481-415a-b1e8-b7c279fb5ade c3391f16-5fb1-4364-85e0-5166132615a2
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: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:103 rtx/90000
a=fmtp:103 apt=102
a=rtpmap:104 H264/90000
a=rtcp-fb:104 goog-remb
a=rtcp-fb:104 transport-cc
a=rtcp-fb:104 ccm fir
a=rtcp-fb:104 nack
a=rtcp-fb:104 nack pli
a=fmtp:104 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f
a=rtpmap:105 rtx/90000
a=fmtp:105 apt=104
a=rtpmap:106 H264/90000
a=rtcp-fb:106 goog-remb
a=rtcp-fb:106 transport-cc
a=rtcp-fb:106 ccm fir
a=rtcp-fb:106 nack
a=rtcp-fb:106 nack pli
a=fmtp:106 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=rtpmap:107 rtx/90000
a=fmtp:107 apt=106
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: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=1;profile-level-id=4d001f
a=rtpmap:125 rtx/90000
a=fmtp:125 apt=127
a=rtpmap:39 H264/90000
a=rtcp-fb:39 goog-remb
a=rtcp-fb:39 transport-cc
a=rtcp-fb:39 ccm fir
a=rtcp-fb:39 nack
a=rtcp-fb:39 nack pli
a=fmtp:39 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=4d001f
a=rtpmap:40 rtx/90000
a=fmtp:40 apt=39
a=rtpmap:45 AV1/90000
a=rtcp-fb:45 goog-remb
a=rtcp-fb:45 transport-cc
a=rtcp-fb:45 ccm fir
a=rtcp-fb:45 nack
a=rtcp-fb:45 nack pli
a=rtpmap:46 rtx/90000
a=fmtp:46 apt=45
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:116 red/90000
a=rtpmap:117 rtx/90000
a=fmtp:117 apt=116
a=rtpmap:118 ulpfec/90000
a=ssrc-group:FID 1015839969 1236739797
a=ssrc:1015839969 cname:5pzbKKS36iC32H5x
a=ssrc:1015839969 msid:1cfe8d2c-3481-415a-b1e8-b7c279fb5ade c3391f16-5fb1-4364-85e0-5166132615a2
a=ssrc:1236739797 cname:5pzbKKS36iC32H5x
a=ssrc:1236739797 msid:1cfe8d2c-3481-415a-b1e8-b7c279fb5ade c3391f16-5fb1-4364-85e0-5166132615a2

Answerデータ

v=0
o=- 773571970845784342 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0
a=extmap-allow-mixed
a=msid-semantic: WMS
m=video 56915 UDP/TLS/RTP/SAVPF 96 97 102 103 104 105 106 107 108 109 127 125 39 40 45 46 98 99 100 101 116 117 118
c=IN IP4 192.168.10.105
a=rtcp:9 IN IP4 0.0.0.0
a=candidate:2502815815 1 udp 2122129151 192.168.10.105 56915 typ host generation 0 network-id 1
a=candidate:655803704 1 udp 2122063615 192.168.135.1 56916 typ host generation 0 network-id 4
a=candidate:4185827048 1 udp 2121998079 192.168.16.1 56917 typ host generation 0 network-id 5
a=candidate:3391231333 1 udp 2122262783 240b:11:1c80:3a00:9799:7da9:a9e4:299b 56918 typ host generation 0 network-id 2
a=candidate:3127992598 1 udp 2122197247 240b:11:1c80:3a00:cd58:73d7:d29:4ee2 56919 typ host generation 0 network-id 3
a=ice-ufrag:rX3B
a=ice-pwd:3DIwNghHuw5UICfHpJgRdbIf
a=ice-options:trickle
a=fingerprint:sha-256 B9:DE:42:6F:0D:F9:B2:02:E2:82:1D:5E:85:7A:81:64:F6:2A:3D:06:EE:1A:B2:C3:41:2A:25:07:B5:57:D7:FE
a=setup:active
a=mid:0
a=extmap:1 urn:ietf:params:rtp-hdrext:toffset
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 urn:3gpp:video-orientation
a=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=extmap:6 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:9 urn:ietf:params:rtp-hdrext:sdes:mid
a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
a=extmap:11 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: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:103 rtx/90000
a=fmtp:103 apt=102
a=rtpmap:104 H264/90000
a=rtcp-fb:104 goog-remb
a=rtcp-fb:104 transport-cc
a=rtcp-fb:104 ccm fir
a=rtcp-fb:104 nack
a=rtcp-fb:104 nack pli
a=fmtp:104 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f
a=rtpmap:105 rtx/90000
a=fmtp:105 apt=104
a=rtpmap:106 H264/90000
a=rtcp-fb:106 goog-remb
a=rtcp-fb:106 transport-cc
a=rtcp-fb:106 ccm fir
a=rtcp-fb:106 nack
a=rtcp-fb:106 nack pli
a=fmtp:106 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=rtpmap:107 rtx/90000
a=fmtp:107 apt=106
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: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=1;profile-level-id=4d001f
a=rtpmap:125 rtx/90000
a=fmtp:125 apt=127
a=rtpmap:39 H264/90000
a=rtcp-fb:39 goog-remb
a=rtcp-fb:39 transport-cc
a=rtcp-fb:39 ccm fir
a=rtcp-fb:39 nack
a=rtcp-fb:39 nack pli
a=fmtp:39 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=4d001f
a=rtpmap:40 rtx/90000
a=fmtp:40 apt=39
a=rtpmap:45 AV1/90000
a=rtcp-fb:45 goog-remb
a=rtcp-fb:45 transport-cc
a=rtcp-fb:45 ccm fir
a=rtcp-fb:45 nack
a=rtcp-fb:45 nack pli
a=rtpmap:46 rtx/90000
a=fmtp:46 apt=45
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:116 red/90000
a=rtpmap:117 rtx/90000
a=fmtp:117 apt=116
a=rtpmap:118 ulpfec/90000

参考:Raspberry pi とAndroid携帯でのWebRTC配信

  • Raspberry pi に同じUSBカメラを接続して、ブラウザ「Chromium」を用いて接続を試みたところ、次のエラーが発生してUSBカメラが接続できなかった。
  • 「Uncaught TypeError: Cannot read properties of undefined (reading ‘getUserMedia’)」

  • Android携帯(Android7.0)のカメラを、ブラウザ「Chrome」を用いて接続を試みたところ、カメラ映像が表示されなかった。consoleのログは確認しなかった