UV4L、ソケット通信、P2P通信、WebRTC 各々の関係が良く理解出来ていません。今回はこれらに付いて調べてみます。
そもそも、UV4Lとは
UV4LはUser space Video4Linuxの略です。Video4LinuxはLinuxで映像や音声を扱う時に使用するデバイスの1つです。Video4Linuxを元にそれに付加するプラグイン(例えば、UBSカメラを使う等)の集まり全体をUV4Lと言っているようです。
今回インストールしたのは、uv4l、 uv4l-webrtc、 uv4l-uvc の3つ。
- uv4l: Framework Core
- uv4l-webrtc: WebRTC用のプラグイン
- uv4l-uvc: USBカメラ用のプラグイン
UV4Lに関する資料ですが下記を見つけました。
シグナルサーバ、P2P通信、WebRTC とは
- WebRTC:”Web Real-Time Communication”
- Webブラウザやモバイルアプリでリアルタイム通信を実現するAPI。
- P2P通信:”Peer-to-Peer”
- サーバを介さずに、端末同士で直接データを通信する技術。
- WebRTCの通信方式として使用される。
- シグナルサーバ
- P2Pが確立する前にお互いの情報を交換する為に使用するサーバ。
シグナルサーバとP2P通信の準備
シグナルサーバはUV4L内に有り、今回の設定でアドレスは”raspberrypi.local:8090/stream/webrtc”になります。そことソケット通信しP2P通信を確立します。互いに交換される情報は、Session Description Protocol (SDP)とICE Candidateの2つです。
- Session Description Protocol (SDP)は以下の情報を含んでいます。
- 通信するメディアの種類(音声、映像)、メディアの形式(コーデック)
- IPアドレス、ポート番号
- 暗号化の鍵 等
- ICE Candidate
- P2P通信を行う際に使われる通信経路の候補
- どのような通信経路が使えるかは、お互いのネットワーク環境に依存。
- 通信相互で経路の候補を挙げ、通信が繋がった時にその経路を使う。
シグナルサーバの説明は、WebRTC signalingに有ります。サーバとの通信は下記のフォーマットが使用されます。
{
what: "call",
options: {
force_hw_vcodec: true,
vformat: 30,
trickle_ice: true
}
}
- what:リクエストの内容
- options: 設定パラメータ。
- force_hw_vcodec: ビデオコーデックの有無
- vformat: 画面の解像度
- trickle_ice: 候補の送信方法
true :見つかったら直ぐに送信。 false:まとめて送信
WebRTCの使用にはWebRTCに対応したWebブラウザが必要です。今回Raspberry PIはOSをCUIでインストールしているのでWebブラウザを使用出来ません。Webブラウザを持たないRaspberry PIとWebRTCを実行出来るのはUV4Lがブラウザの代わりを務めているからでしょうか。
HPのHTMLコードの簡略化
最終的にRaspberry PIを使ってインターフォン(もしくは監視カメラ)を作りたいので、その為に必要な部分を残して前回の最後で使用したRaspberry PIとのStreamingのHPのコードを簡略化して見ました。
- ”remote”, “local” と2つ有った画像の表示を、”remote”のみにする。
- ”Pause” , “Mute”, “Fullscreen”, “Recording” ボタンを廃止。
- ”Call”、”Hangup” ボタンはそのまま残す。
- 画面サイズの選択機能は廃止して、width=”640″ height=”480″に固定。
- Cast local Audio/Video sources機能を廃止。
- Data Channelsを廃止。
- Advanced optionsで、”Trickle ICE”をtrueに固定。
- その他コメントを廃止。
簡略化したコードをRaspberry PI 上では無くPCに”webrtc.html” として保存します。ますRaspberry PIでUV4Lを立ち上げ、その後PCに保存した”webrtc.html”をブラウザを使って表示すると以下の様にちゃんと立ち上がりました。

オリジナルのコードは約1250行でしたが変更後のコードは約216行となりました。このコードでRaspberry PIとの映像と音声のStreamingが出来ることを確認しています。
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 mediaConstraints = {
optional: [],
mandatory: {
OfferToReceiveAudio: true,
OfferToReceiveVideo: true
}
};
var iceCandidates = [];
function createPeerConnection() {
try {
pc = new RTCPeerConnection();
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 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>
コードの説明
コードの開始はHP上の、”CALL” ボタンを押した所から始めます。
- ”CALL” ボタンは213行で定義されており、これをクリックすると75行の”start()”へ飛びます。
- 75行: ”start()”関数はここから始まる。
- 76行: ”WebSocket”が使えるか判断。使えれば次は、使えなければ167行へ
- 82行: 新たなソケット通信を作成。Raspberry PIのシグナルサーバとの接続開始。
- 84行 : シグナルサーバとの接続が出来る(onopen)とここが実行される。
- 87行: P2P接続に必要な処理を開始。詳細は後ほど説明します。
- 88行: Raspberry PIのシグナルサーバに送るデータの製作
- 96行: ソケット通信でシグナルサーバにデータ(request)を送信。P2P接続の準備が開始。
送信したデータのWhatが”CALL”なのでPC側からRaspberry PI側にP2Pの準備を要求
データを受け取ったRaspberry PIは自信のSDPデータを”offer”とてPCへ送信 - 100行: シグナルサーバから送信されたメッセージはここで処理される。
- 108行: メッセージの解析はここから
Raspberry PI側から”offer”として送られるのでPC側で”answer”の処理は無し- 109行: Raspberry PI側から”offer”をここで処理
- 136行: Raspberry PI側から”iceCandidate”をここで処理
- 何回かICE Candidateのやり取りの後、お互いの条件が合えばP2P接続が開始される。
87行で実行した関数 createPeerConnection(); により以下が並行して行われる。
- 20行:createPeerConnection()関数。P2P接続に必要な処理を行う。
- 22行: 新しいP2P通信の作成とイベントに処理。
- 23行: icecandidateイベントが起きたら、onIceCandidate()を実行
- 24行: trackイベントが起きたら、onTrack()を実行
- 25行: removestreamイベントが起きたら、onRemoteStreamRemoved()を実行
- 22行: 新しいP2P通信の作成とイベントに処理。
- 32行:onIceCandidate(event)。ICE Candidateを相手(Raspberry PI)に送信
- 22行のRTCPeerConnection(); が実行されると自信のICE Candidateの作製が始まる。
- ICE Candidateが出来るとトリガーがかかりここが実行される。
- ”what”ラベルに、”addIceCandidate”。”data”ラベルに、ICE CandidateをセットしRaspberry PIに送信する。
- 49行: addIceCandidates() ICE Candidateの追加
- 63行: onTrack(event)。
- 66行:ここでRaspberry PIから送られて来たデータ(画像と音声)をHTML画面に接続。
その他の説明
- ”Hang up”ボタンは214行で定義。クリックすると、171行の”stop();”が起動。
- 171行: stop()。通信の停止作業を行う
- Streamの停止。
- ソケット通信の停止
- P2P通信の停止。
プログラムの実行
最後に実際にプログラムを実行して見ます。実行は以下の様に行いました。
- Raspberry PI側:
- UV4Lの起動が必要です。
- 前回の ”UV4Lを使う(1)” 通りにセットアップしていればRaspberry PIの起動と共にUV4Lも起動します。
- PC側:
- PCに保存されたHTMLのコードファイルを実行(ファイルをクリック)します。
- 今回ブラウザはFireFoxを使っています。
- 実行結果:
- FireFoxのURL欄にコードのPathを入力すると下記の画面が表示されます。

- 画面下の、”Call”ボタンをクリックするとStreamingが始まります。
- もちろん、音声もStreamingされています。
- ”Hang Up”ボタンをクリックするとStreamingが終了します。
次は
P2Pの確立する仕組がまだ良く理解出来ていません。ただ、P2P接続部、WebRTC部を残せばそれ以外のHTMLは編集出来る事が分かったのは大きな収穫です。
現時点で音と映像が送られてくる監視カメラレベルになっています。これでPCからRaspberry PIに音が送れれば立派なインターフォンになります。次回はこれに挑戦します。