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: </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のリファレンスは、「開発者向けのウェブ技術」に示します。
- 接続されているUSBカメラからの映像入力
- 「Play」ボタンをクリックすると、126行目のnavigator.mediaDevices.getUserMedia() により、要求された種類のメディアを含むトラックを持つ MediaStream の使用許可をユーザーに求めます。
- 成功すると、50行目のplayVideo()で再生を始めます。
- Offer SDPの生成
- 「Connect」ボタンをクリックすると、174行目でRTCPeerConnection のオブジェクトを生成します。
- 223行目のRTCPeerConnection.createOffer() で Offer SDPを生成します。
- 226行目のRTCPeerConnection.setLocalDescription()でローカルSDPとして設定します。
- ICE candidateの収集
- ICE candidate の収集が始まると、「ICE gathering state change」イベントが発生しします。193行目のRTCPeerConnection.onicecandidateにイベントハンドラを記述します。
- このイベントは複数回発生し、全てのICE candidateを収集し終わると空のイベントが発生します。
- 空のイベントが発生すると、204行目でlocalDescriptionをパラメータとしたsendSdp()の中で、テキストエリアにOffer SDPを表示します。
- Offser SDPの受信
- 応答側にOffer SDPをペーストして「Receive remote SDP」ボタンをクリックすると、88行目でonSdpText() → 108行目でsetOffer() と呼び出されます。
- 238行目のsetOffer()の中では、PeerConnectionのオブジェクトを生成し、受け取ったOffer SDPを setRemoteDescription()で設定します。
- 成功すると、 252行目のmakeAnswer()を呼び出します。
- Answer SDPを生成・送信
- 259行目のRTCPeerConnection.createAnswer() で Answer SDPを生成します。
- 生成したAnswer SDPを、262行目のRTCPeerConnection.setLocalDescription()で設定します。
- この後 、193行目のRTCPeerConnection.onicecandidate()でICE candidateを収集し、すべて揃ったらsendSdp()でOffer側に送り返します。
- Answer SDPの受信
- 発信側にAnser SDPをペーストして「Receive remote SDP」ボタンをクリックすると、88行目でonSdpText() → 274行目でsetAnswer() と呼び出されます。
- setAnswer()の中で、280行目でRTCPeerConnection.setRemoteDescription()で受け取ったSDPを設定します。
- 映像/音声の送受信
- 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」で、作成した動画配信アプリを実行します。
- Chromeから送信側「http://localhost/webrtc-test/sender.html」をアクセスします。
- Chromeから受信側「http://localhost/webrtc-test/receiver.html」をアクセスします。
- 送信側で「Play」ボタンをクリックすると、USBカメラの撮影動画が表示されます。
- 送信側で「Connect」ボタンをクリックすると、「SDP to send」テキストにOfferデータが表示されます。
- 受信側で、「SDP to receive」テキストエリアに送信側のOfferデータをコピーして、「Receive remote SDP」ボタンをクリックすると、「SDP to send」テキストエリアにAnswerデータが表示されます。
- 送信側で、「SDP to receive」テキストエリアに受信側のAnswerデータをコピーして、「Receive remote SDP」ボタンをクリックします。
- 受信側で、送信側から配信された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カメラが接続できなかった。
- Android携帯(Android7.0)のカメラを、ブラウザ「Chrome」を用いて接続を試みたところ、カメラ映像が表示されなかった。consoleのログは確認しなかった
「Uncaught TypeError: Cannot read properties of undefined (reading ‘getUserMedia’)」