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’)」






