前回、”UV4Lを使う(1)” でUV4Lを使って画像と音声のStreamingが出来る事が分かりました。今回はそのHTMLのコードからWebRTC部分のみを取り出し動作の理解を目的にします。
コードの簡略化と解析
以下の様に設定しコードを簡略化しました。
- ”remote”, “local” と2つ有った画像の表示を、”remote”のみにする。
- ”Pause” , “Mute”, “Fullscreen”, “Recording” ボタンを廃止。
- 画面サイズの選択機能は廃止して、width=”640″ height=”480″に固定。
- Cast local Audio/Video sources機能を廃止。
- Data Channelsを廃止。
- Advanced optionsで、”Trickle ICE” を true に固定。
- その他コメントを廃止。
- ”Call”、”Hangup” ボタンはそのまま残す。
簡略化したコードは、Raspberry PI 上では無くPCのホームディレクトリに、”webrtc.html” として保存しています。”webrtc.html” をブラウザ(FireFox)を使って表示すると以下の様になります。

下記がコードです。オリジナルは約1250行でしたが、WebRTC関係のみを取り出したので約230行となりました。
webrtc.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>UV4L WebRTC</title>
<script type="text/javascript">
var ws = null;
var pc;
var audio_video_stream;
var pcConfig = {/*sdpSemantics : "plan-b"*,*/ "iceServers": [
{"urls": ["stun:stun.l.google.com:19302", "stun:" + location.hostname + ":3478"]}
]};
var mediaConstraints = {
optional: [],
mandatory: {
OfferToReceiveAudio: true,
OfferToReceiveVideo: true
}
};
var iceCandidates = [];
const constraints = {
audio: true,
video: false
};
function createPeerConnection() {
try {
var pcConfig_ = pcConfig;
console.log(JSON.stringify(pcConfig_));
pc = new RTCPeerConnection(pcConfig_);
pc.onicecandidate = onIceCandidate;
pc.ontrack = onTrack;
pc.onremovestream = onRemoteStreamRemoved;
console.log("peer connection successfully created!");
} catch (e) {
console.error("createPeerConnection() failed");
}
}
function onIceCandidate(event) {
if (event.candidate && event.candidate.candidate) {
var candidate = {
sdpMLineIndex: event.candidate.sdpMLineIndex,
sdpMid: event.candidate.sdpMid,
candidate: event.candidate.candidate
};
var request = {
what: "addIceCandidate",
data: JSON.stringify(candidate)
};
ws.send(JSON.stringify(request));
} else {
console.log("End of candidates.");
}
}
function addIceCandidates() {
iceCandidates.forEach(function (candidate) {
pc.addIceCandidate(candidate,
function () {
console.log("IceCandidate added: " + JSON.stringify(candidate));
},
function (error) {
console.error("addIceCandidate error: " + error);
}
);
});
iceCandidates = [];
}
function onRemoteStreamAdded(event) {
console.log("Remote stream added:", event.stream);
var remoteVideoElement = document.getElementById('remote-video');
remoteVideoElement.srcObect = event.stream;
}
function onTrack(event) {
console.log("Remote track!");
var remoteVideoElement = document.getElementById('remote-video');
remoteVideoElement.srcObject = event.streams[0];
}
function onRemoteStreamRemoved(event) {
var remoteVideoElement = document.getElementById('remote-video');
remoteVideoElement.srcObject = null;
remoteVideoElement.src = ''; // TODO: remove
}
function start() {
if ("WebSocket" in window) {
document.getElementById("stop").disabled = false;
document.getElementById("start").disabled = true;
document.documentElement.style.cursor = 'wait';
var protocol = location.protocol === "https:" ? "wss:" : "ws:";
ws = new WebSocket(protocol + '//raspberrypi.local:8090/stream/webrtc');
ws.onopen = function () {
iceCandidates = [];
remoteDesc = false;
createPeerConnection();
var request = {
what: "call",
options: {
force_hw_vcodec: false,
vformat: "30",
trickle_ice: true
}
};
ws.send(JSON.stringify(request));
console.log("call(), request=" + JSON.stringify(request));
};
ws.onmessage = function (evt) {
var msg = JSON.parse(evt.data);
if (msg.what !== 'undefined') {
var what = msg.what;
var data = msg.data;
}
console.log("message =" + what);
switch (what) {
case "offer":
pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(data)),
function onRemoteSdpSuccess() {
remoteDesc = true;
addIceCandidates();
console.log('onRemoteSdpSucces()');
pc.createAnswer(function (sessionDescription) {
pc.setLocalDescription(sessionDescription);
var request = {
what: "answer",
data: JSON.stringify(sessionDescription)
};
ws.send(JSON.stringify(request));
console.log(request);
}, function (error) {
alert("Failed to createAnswer: " + error);
}, mediaConstraints);
},
function onRemoteSdpError(event) {
alert('Failed to set remote description (unsupported codec on this browser?): ' + event);
stop();
}
);
break;
case "iceCandidate": // when trickle is enabled
if (!msg.data) {
console.log("Ice Gathering Complete");
break;
}
var elt = JSON.parse(msg.data);
let candidate = new RTCIceCandidate({sdpMLineIndex: elt.sdpMLineIndex, candidate: elt.candidate});
iceCandidates.push(candidate);
if (remoteDesc)
addIceCandidates();
document.documentElement.style.cursor = 'default';
break;
}
};
ws.onclose = function (evt) {
if (pc) {
pc.close();
pc = null;
}
document.getElementById("stop").disabled = true;
document.getElementById("start").disabled = false;
document.documentElement.style.cursor = 'default';
};
ws.onerror = function (evt) {
alert("An error has occurred!");
ws.close();
};
} else {
alert("Sorry, this browser does not support WebSockets.");
}
}
function stop() {
if (audio_video_stream) {
try {
if (audio_video_stream.getVideoTracks().length)
audio_video_stream.getVideoTracks()[0].stop();
if (audio_video_stream.getAudioTracks().length)
audio_video_stream.getAudioTracks()[0].stop();
audio_video_stream.stop(); // deprecated
} catch (e) {
for (var i = 0; i < audio_video_stream.getTracks().length; i++)
audio_video_stream.getTracks()[i].stop();
}
audio_video_stream = null;
}
document.getElementById('remote-video').srcObject = null;
document.getElementById('remote-video').src = ''; // TODO; remove
if (pc) {
pc.close();
pc = null;
}
if (ws) {
ws.close();
ws = null;
}
document.getElementById("stop").disabled = true;
document.getElementById("start").disabled = false;
document.documentElement.style.cursor = 'default';
}
</script>
<style>
video {
background: #eee none repeat scroll 0 0;
border: 1px solid #aaa;
}
</style>
</head>
<body>
<h1><span>WebRTC two-way Audio/Video Intercom</span></h1>
<video id="remote-video" autoplay="" width="640" height="480">
Your browser does not support the video tag.
</video><br>
<button id="start" style="background-color: green; color: white" onclick="start();">Call!</button>
<button disabled id="stop" style="background-color: red; color: white" onclick="stop();">Hang up</button>
</body>
</html>
コードの説明
簡略化した時に必要な部分を省略した可能性は有りますが、このコードでRaspberry PI からの映像と音声のStreamingは出来ています。説明は、HP上の、”CALL” ボタンを押した所から始めます。
- ”CALL” ボタンは229行で定義されておりクリックすると、”start()” が実行される。
- 91行: ”start()”関数はここから始まる。
- 92行: ”WebSocket”が使えるか判断
- 98行: ws = new WebSocket(protocol 新たなソケット通信を作成。
- 100行 : ws.onopen ソケット通信が確立した時に実行される。
- 103行:createPeerConnection()の呼び出し。
- 104行:requestに値を設定してUV4Lの状態を設定
- request変数の
- ラベルwhatに”call”
- ラベルoptionsに
- force_hw_vcodec: false -> ハードウェアデコード無し
- vformat: “30” -> 解像度とフレームレート 640×480 30 fps
- “5” -> 320×240 15 fps / “10”-> 320×240 30 fps / “20”-> 352×288 30 fps
- “25”-> 640×480 15 fps / “30”-> 640×480 30 fps 等
- trickle_ice: true -> ICE CandidateにTrickleを使用
- request変数の
- 112行: ソケット通信でRaspberry PI にデータを送信
- 116行: ws.onmessage Raspberry PIからソケット通信でデータを受信すると実行される。
- 125行: メッセージが、”offer”の場合。
- 126行: SDPデータの登録
- 131行: ”answer”データの作成。
- 137行: ”answer”データをソケット通信でRaspberry PIに送信。
- 152行: メッセージが、””iceCandidate”の場合。
- 161行: IceCandidateの追加
- 125行: メッセージが、”offer”の場合。
- 167行: ws.onclose ソケット通信がCloseされる時に実行。
- 28行:createPeerConnection()関数。P2P接続に必要な処理を行う。
- 32行: 新しいP2P通信、PCを作成。
- 33行: icecandidateイベントが起きたら、onIceCandidate()を実行
- 34行: trackイベントが起きたら、onTrack()を実行
- 35行: removestreamイベントが起きたら、onRemoteStreamRemoved()を実行
- 42行:onIceCandidate(event)。ICE Candidateを相手(Raspberry PI)に送信
- ソケット通信通信 ws を使ってICE Candidateを相手(Raspberry PI)に送信する。
- request変数の、”what”ラベルに、”addIceCandidate”。”data”ラベルに、ICE Candidateをセット
- 53行:ws.send(JSON.stringify(request));でRaspberry PIに送信される。
- ICE Candidateの収集作業には見つかり次第送信する、Trickle ICE方式が使われている。
- 59行: addIceCandidates() ICE Candidateの追加
- 79行: onTrack(event)。
- 82行:ここでRaspberry PIから送られて来たデータ(画像と音声)をHTML画面に接続。
- ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
- ”Hang up”ボタンは230行で定義。クリックすると、”stop();”が起動。
- 187行: stop()。通信の停止作業を行う
- Streamの停止。
- ソケット通信(ws)の停止
- P2P通信(pc)の停止
プログラムの実行
- Raspberry PI側:
- 前回の ”UV4Lを使う(1)” 通りにセットアップしていれば変更は有りません。Raspberry PIを立ち上げればUV4Lが起動します。
- PC側:
- 今回書いたHTMLのコードはPCのホームディレクトリに保存しています。
- FireFoxを使って実行しています。
- 実行:
- FireFoxのURL欄にコードのPathを入力すると下記の画面が表示されます。
- 画面したの、”Call”ボタンをクリックするとStreamingが始まります。
- もちろん、音声もStreamingされています。
- ”Hang Up”ボタンをクリックするとStreamingが終了します。

次はこちらの音声をRaspberry PIに送信
これで外の音もStreaming出来ると感動してアプリを触っていたのですが、ふと気づくとこちらの音がRaspberry PIに送れません。インターフォンを目指しているので、こちらの画像は送る必要は無いのですが音は送りたいです。WebRTCは、”Web Real-Time Communication”の略です。きっと遅れるはずです。
感覚的は分かった様な気がしますが、まだソケット通信、P2P通信、WebRTCの動作を良く理解していません。次はこれらの理解とRaspberry PIにスピーカーを付けてPCからの音の送信にチャレンジしてみたいと思います。