2. コードを理解する (UV4L)

更新日: 2026.02.25

WebRTCのHTMLコードの簡略化

前回の WebRTCのHPのソースコードを以下の様に簡略化しました。

  • ”remote”, “local”の2画面を”remote”のみにする。
  • ”Pause” , “Mute”, “Fullscreen”, “Recording” ボタンを廃止。
  • 画面サイズはwidth=”640″ height=”480″に固定。
  • Advanced optionsで、”Trickle ICE”をtrueに固定。

簡略化したソースのファイル。(”webrtc.html")

webrtc.html 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>

ブラウザでこのファイルを読み込むと下記の画面が表示されます。 左下の”CALL”ボタンをクリックするとStreamingが開始。 ”Hang Up”ボタンをクリックでStreaming終了です。

Control Panel

ポート8090のサーバについて

ポート8090にアクセスするとコントロールパネルが表示されます。 今回はUSBカメラを使用しているでこのポートですがカメラがRaspberry PI専用の場合 ポートは8080になるそうです(試していませんが)。

このサーバの主な役割は以下の通り。

  • ストリーミングの開始・停止(P2P接続時のシグナルサーバ)。
  • 解像度やフレームレート(FPS)の変更
  • カメラパラメータの調整

ファイルがブラウザに読み込まれてStreamingが開始されるまで下記の様な流れになります。

Control Panel
  1. 初期状態。Raspberry PIとNoteBookは同一のLANの中にいる。
    • Raspberry PI側: UV4Lを立ち上げる
    • NoteBook側: ブラウザ(Firfox)を上げる
  2. HP左下の”CALL”ボタンをクリック
    • HPのコードがUV4LのSignal_ServerとWebsocketで接続。
    • ブラウザとRaspberry PIの間でP2P接続に必要な情報を交換する。
  3. 情報が整うとP2Pが開通しStreamingが始まる。

P2P接続に関して

P2P接続の為の情報の交換は以下の様に行われています。

Control Panel
  • HPの”CALL”ボタンをクリックすると”function start()”へ飛ぶ。
  • ws = new WebSocket(protocol +’//raspberrypi.local:8090/stream/webrtc’);
      UV4LのSignal_serverにwebsocketで接続。
  • ws.onopen:Signal_serverと接続したらここが実行。
    • createPeerConnection();:P2Pの準備を行う関数を実行
      • pc = new RTCPeerConnection();         P2P用インスタンスの作成
      • pc.onicecandidate = onIceCandidate;       イベント関数宣言
      • pc.ontrack = onTrack;              イベント関数の宣言
      • pc.onremovestream = onRemoteStreamRemoved; イベント関数の宣言
        • 各イベント関数の説明
          • function onIceCandidate(event)
            • IceCandidateが見つかったら発動する関数。
            • 相手にIceCandidateを送信し登録を依頼
          • function onTrack(event)
            • P2Pの接続が完了した発動する関数。
            • 画像データを接続する。
          • onRemoteStreamRemoved(event)
            • Streamingが削除された時に発動する関数。
    • ws.send(JSON.stringify(request));
      • 続いて”call”コマンドを送信して相手にP2Pの準備を依頼。
  • ws.onmessage = function (evt) 相手からのメッセージを処理する関数。
    • case “offer”: 相手メッセージが”offer”の場合
      • addIceCandidates(); 受信したIceCandidateを登録
      • pc.createAnswer(function (session ここで送信用IceCandidate(Answer)を作成
        • pc.setLocalDescription IceCandidateを検索
        • ws.send(JSON.stringify ここで相手に送信
    • case “iceCandidate”: 相手メッセージが”iceCandidate”の場合
      • ”iceCandidate”の登録
  • ws.onclose Signal_serverが切断されると実行。
  • ws.onerror Signal_serverとの接続でエラーが発生した時に実行。

Streamingの終了

Streamingの終了はstopt()関数が担当。Streaming、P2P、Signal_serverの順に終了します。

Control Panel

P2P通信とSignal_Server

P2P通信はサーバを介さずに端末同士で直接データを通信する技術ですが、 P2Pを確立する為にはお互いの情報を交換する必要が有ります。この情報交換をSignal_Serverと通して行います。 今回のコードでは”raspberrypi.local:8090/stream/webrtc”がSignal_Serverの役目を果たしています。 互いに交換するデータは主に下記の2つ。

  • Session Description Protocol (SDP)
    • 通信するメディアの種類(音声、映像)、メディアの形式(コーデック)
    • IPアドレス、ポート番号
    • 暗号化の鍵 等
  • ICE Candidate
    • P2P通信を行う際に使われる通信経路の候補
    • どのような通信経路が使えるかは、お互いのネットワーク環境に依存。
    • 通信相互で経路の候補を挙げ、通信が繋がった時にその経路を使う。

UV4LのSignal_Serverにはのアクセスフォーマットが有ります。 WebRTC signaling によるとフォーマットは下記。

{ what: "call", options: { force_hw_vcodec: true, vformat: 30, trickle_ice: true } }
  • what:リクエストの内容
    • call:  最初にサーバにアクセスする時に使用
    • offer: 自分の通信情報を相手に送りたい時に使用。
    • answer:相手からもらったデータに対する返答をする時に使用。
    • など…
  • options: 設定パラメータ。
    • force_hw_vcodec:  ビデオコーデックの有無
    • vformat:     画面の解像度
    • trickle_ice:      候補の送信方法

Signal_Serverの役目はP2Pが開通するまでの情報交換のみ。 開通するとP2PでStreamingを行い、Signal_ServerはStreamingに全く関与しません。

UV4Lに関する資料

UV4LとWebRTCの違いは

UV4L:User space Video4Linux。 WebRTC:Web Real-Time Communication。 どちらもP2PでStreamingを行うアプリ。簡単に両者を比較。

項目UV4LWebRTC
ブラウザ不要必要
MediaDevice使用許可許可無しでOK/td>必要
追加機能Web,Signalサーバ等ーーー

大きな違いは実行時のウザの有無とMediaDeviceの使用許可が必要無い点です。 監視用カメラには必要な条件です。

次回は

P2P、Signal_server、UV4Lの関係が大まかに理解出来ました。 相手側からの映像と音声のStreamingは出来る様になりました。 次回はこちら(PC側)から音声と映像の送信を試して見ます。

SINCE 2026