LoginSignup
4
6

More than 5 years have passed since last update.

WebRTCはじめました:コピペ編

Posted at

WebRTCでブラウザ間通信

「WebRTCはじめました:Webカメラ編」でWebカメラからストリームの取得ができたので,いよいよWebRTCの本番,ブラウザ間通信によってストリームの転送を実装.
普通はnodejsとか使って最初のシグナリングを行うけれど,まずはコピペによるシグナリングから始める.
基本的にWebRTCに関する実装はlibwebrtc.jsという形でまとめておく.
WebRTCの細かい内容については説明しないので適宜調べてください.

配信側:コネクションの作成

まず,呼び出す側の実装は下記のようになる.
カメラが取得できたらmainが呼び出される.カメラの取得方法等は「WebRTCはじめました:Webカメラ編」を参照.

CopyPasteCam.html
<!DOCTYPE html>
<html>

<head>
  <title>Camera</title>
  <script src="js/libwebrtc.js"></script>
</head>

<body>
  <div>
    <video id="video" autoplay="1" />
  </div>
  <form id="control"></form>
  <form>
    <textarea id="textarea" rows=5 cols=50></textarea>
    <input type="button" id="button" onclick="receive()" value="onAnswer"></input>
  </form>
  <script>
    var video = document.getElementById("video");
    var control = document.getElementById("control");
    var localStream = null;
    var connection = null;

    var msg_obj = [];
    var textarea = document.getElementById("textarea");

    // 指定されたメッセージをテキストエリアに表示する
    // 追記するためにリストを使っているけれど,直接今のvalueに足してもよい
    function appendMessage(msg) {
      msg_obj.push(msg);
      textarea.value = JSON.stringify(msg_obj);
    }

    getVideoSources(function(cam) {
      console.log("cam", cam);
      var b = document.createElement("input");
      b.type = "button";
      b.value = cam.name;
      b.onclick = getMain(cam.id);
      control.appendChild(b);
      console.log('add button');
    });

    function getMain(cam_id) {
      return function() {
        main(cam_id);
      };
    }

    function main(cam_id) {
      navigator.getUserMedia({
        audio: false,
        video: {  // とりあえず試す場合は「video : true」としてカメラの指定を行わなければよい
          optional: [{
            sourceId: cam_id
          }]
        }
      }, function(stream) { // success
        console.log("Start Video", stream);
        localStream = stream;
        video.src = URL.createObjectURL(stream);
        video.play();
        video.volume = 0;
        webrtc();
      }, function(e) { // error
        console.error("Error on start video: " + e.code);
      });
    };

    function webrtc() {
      var stop;
      msg_obj = [];
      // 相手に配信するストリーム,メッセージを伝えるメソッド,エラーコールバックを指定
      connection = start_webrtc(localStream, appendMessage, function() {
        console.log("Error");
      });
    }

    // Answerを受け取った時に呼び出されるメソッド
    function receive(){
      var msg = JSON.parse(textarea.value);
      msg.some(function(v, i){
        if(v.type==msg_type.answer){
          on_answer(connection, msg);
        }
      });
    }
  </script>
</body>

</html>

カメラ取得ができた後はライブラリ化されたstart_webrtc()メソッドを呼び出すだけにしている.
受信側に伝えるメッセージはコールバックを通してテキストエリアに表示される.
ユーザはテキストエリアの内容をコピーして受信側のテキストエリアにペーストする.
受信側で処理が終わってAnswerのテキストが取得できると,それをテキストエリアにペーストしてボタンを押す.

webrtc()メソッドは下記の通り.

libwebrtc.js
var msg_type = {
  offer: "offer",
  candidate: "candidate",
  answer: "answer"
}

// 配信側はこのメソッドを呼び出す.引数は,相手に配信するストリーム,受信側にメッセージを伝える場合のコールバック,エラーコールバック.
function start_webrtc(stream, sendMsg, error) {
  var connection = new RTCPeerConnection(ice_config);
  connection.addStream(stream);
  connection.createOffer(
    function(des) {
      // Offerに成功すると呼び出される.
      connection.setLocalDescription(des);
      // ローカルにDescriptionをセットし,次は受信側connectionのRemoteにセットする必要があるため,メッセージコールバックを呼び出す.
      sendMsg(des);
    },
    function() {
      // エラーの場合
      error();
    }, mediaC
  );

  connection.onicecandidate = function(evt) {
    if (evt.candidate) {
      // candidateの場合は受信側に伝える
      sendMsg({
        type: msg_type.candidate,
        sdpMLineIndex: evt.candidate.sdpMLineIndex,
        sdpMid: evt.candidate.sdpMid,
        candidate: evt.candidate.candidate
      });
    } else {
      // それ以外の場合は全てのcandidateが揃った場合なので,リストにcandidateを貯めてここで相手側に伝えても構わない
    }
  }

  return connection;
}

function on_answer(connection, msg) {
  // connectionにAnswerをセットして通信を開始する
  connection.setRemoteDescription(new RTCSessionDescription(msg));
}

onicecandidateは複数回呼ばれる点に注意.
例えばvideoの通信路とaudioの通信路といった形で別々に呼ばれる.まとめて送る方が合理的だけれど,とりあえずこのライブラリでは余計なことをせず,呼び出す側に任せている.

適当にtextareaを作っておいて,そこに相手に渡すメッセージを追記していく.
単純に追記して改行でパースしてもいいけれど,面倒なのでJSONのリストで管理している.

受信側:コネクションの作成

受信側の実装は下記の通り.
ちなみに受信側はカメラを取得しないのでSSLである必要は無い.

CopyPasteViewer.html
<!DOCTYPE html>
<html>

<head>
  <title>Viewer</title>
  <script src="js/libwebrtc.js"></script>
</head>

<body>
  <div>
    <video id="remote" autoplay="1"></video>
  </div>
  <form>
    <textarea id="textarea" rows=5 cols=50></textarea>
    <input type="button" id="button" onclick="receive()" value="onOffer"></input>
  </form>
  <script>
    var remote = document.getElementById("remote");
    var control = document.getElementById("control");

    var msg_obj = [];
    var textarea = document.getElementById("textarea");

    function appendMessage(msg) {
      msg_obj.push(msg);
      console.log("TEXT", msg_obj);
      textarea.value = JSON.stringify(msg_obj);
    }

    // メッセージを受け取ると呼び出される
    function receive() {
      msg_obj = [];
      var msg = JSON.parse(textarea.value);
      console.log("msg", msg);
      var connection = null;
      var candidate = [];
      // テキストエリアに入力されたメッセージはリストになっているのでtype別に分解する
      msg.some(function(v, i) {
          // offerの場合は1つしか無い(はず)なのでon_offerを呼び出す
        if (v.type == msg_type.offer) {
          // offerに成功するとAnswerを配信側に伝える必要があるため,テキストエリアを更新する
          connection = on_offer(v, remote, appendMessage, function() {
            console.log("Error");
          });
        } else if (v.type == msg_type.candidate) {
          // candidateは複数あるのでリストに追記する
          candidate.push(v);
        }
      });

      if (connection != null) {
        // 受け取ったcandidateを全てセットする.
        candidate.some(function(v, i) {
          on_icecandidate(connection, v);
        });
      }

    }
  </script>
</body>

</html>

受信側のテキストエリアにメッセージをペーストしたらonOfferのボタンを押下してreceive()
メソッドを呼び出す.
JSONのリストになっているはずなので,分解してon_offerとon_icecandidateを呼び出す.
複数のカメラを閲覧することを考えるとreceiveしてからvideoタグを作成して追加する方がスマートかもしれない.
Offerに成功したら今度はAnswerを配信側に伝えなければならないので,Answerをテキストエリアに表示させる.
ユーザは再度このメッセージをコピーし,配信側のテキストエリアにペースト,onAnswerのボタンを押下して完了する.

on_offerとon_icecandidateの実装は下記の通り.

libwebrtc.js
// offerを受け取ったら呼び出すメソッド.受け取ったメッセージ,ストリームを表示するvideo要素,相手にメッセージを渡すコールバックを引数に指定する.
function on_offer(offer_msg, viewer, sendMsg) {
  var connection = new RTCPeerConnection(ice_config);
  connection.addEventListener("addstream", function(evt) {
    // ストリームが追加されると呼び出される
    // 実際には接続の処理が完了し,ICEの通信路が確立されてからになる
    console.log("Add stream");
    // evt.streamに相手側のストリームが格納されているのでblob情報に変換してviewerのsrcに指定する
    // canvasでお絵かきする場合はフレーム毎にvideoをcanvasに書き出す
    viewer.src = URL.createObjectURL(evt.stream);
    console.log("stream src", viewer.src);
  }, false);

  connection.addEventListener("removestream", function(evt) {
    // ストリームが削除された場合に呼び出される.接続が切れた場合にも呼び出される(っぽい)
    viewer.src = "";
  }, false);

  // 受け取ったOfferをRemoteにセットする
  connection.setRemoteDescription(new RTCSessionDescription(offer_msg));
  // Answerを作成
  connection.createAnswer(
    function(des) {
      // 成功したらLocalにセットして配信側にAnswerを伝える
      connection.setLocalDescription(des);
      sendMsg(des);
    },
    function() {
      // error
    }, mediaC);

  return connection;
}

function on_icecandidate(connection, candidate) {
  connection.addIceCandidate(new RTCIceCandidate(candidate));
}

基本的には受け取った情報をセットして,相手に伝える情報をコールバックするだけなのでとても簡単.

まとめ

まずはコピペでシグナリングしてWebRTCの通信を開始.
コピペした部分をなんらかの方法で相手に伝えればいいので,nodejsなりなんなりを使えばいい.
で,次は予告した通りこれをQRコードでやってみる.

4
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
6