UV4Lを使う(2)

前回、”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を使用
      • 112行: ソケット通信でRaspberry PI にデータを送信
    • 116行: ws.onmessage Raspberry PIからソケット通信でデータを受信すると実行される。
      • 125行: メッセージが、”offer”の場合。
        • 126行: SDPデータの登録
        • 131行: ”answer”データの作成。
        • 137行: ”answer”データをソケット通信でRaspberry PIに送信。
      • 152行: メッセージが、””iceCandidate”の場合。
        • 161行: IceCandidateの追加
    • 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からの音の送信にチャレンジしてみたいと思います。