LoginSignup
27
22

More than 5 years have passed since last update.

複数のストリーム同時送信やストリームの切り替え

Last updated at Posted at 2014-12-24

初めに、文中
* カメラの映像や音声などのストリームを単に"ストリーム"
* RTCPeerConnectionオブジェクトのことを"pc"
* offer/answerのやり取りのことを"ネゴシエーション"
と略させていただきます。
(2015-08-09)ものすごいいまさらですが、サンプルアプリを作成しました。詳しくはこちらを見てください。

WebRTCでストリームの切り替えってできないの?といった疑問を最近でもごくたまにみます。
意外とストリームの切り替え方法を知らない方がいらっしゃるようですので、アドベントカレンダーの場をお借りして説明させていただきます。

pcのremoveStream()/addStream()を実行するだけ(気分的に)

いきなり答を言っちゃいますとこれです。ただし、それにはコードが必要となります。

WebRTC(1.0)の一番シンプルと思われるコード

私が、WebRTCのコード解説で良く使わせていただいてるのがWebRTC 1.0ドラフト仕様の#Simple Peer-to-peer Exampleのコードです。
とてもシンプルにまとまっており、このコードのsignalingChannelの部分をWebSocketなどに置き換えて、configurationのところを実際のSTUN/TURNサーバーのURLに変更し、UI(HTML)とシグナリングチャンネルサーバーを用意すればビデオチャットができます。

サンプル
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>WebRTC サンプル</title>
</head>
<body>
  <input type="button" id="btnConnect" value="接続" onclick="start()" />
  <video id="selfView" width="200" height="150" autoplay></video>
  <video id="remoteView" width="200" height="150" autoplay></video>
  <span id="state"></span>
  <script>
    navigator.getUserMedia = navigator.getUserMedia || 
                             navigator.webkitGetUserMedia || 
                             navigator.mozGetUserMedia;
    window.RTCPeerConnection = window.RTCPeerConnection ||
                               window.webkitRTCPeerConnection ||
                               window.mozRTCPeerConnection;
    window.RTCSessionDescription = window.RTCSessionDescription ||
                                   window.mozRTCSessionDescription;
    window.RTCIceCandidate = window.RTCIceCandidate || 
                             window.mozRTCIceCandidate;

    var signalingChannel = new WebSocket('ws://192.168.0.2:9000');
    //var configuration = { "iceServers": [{ "url": "stun:stun.example.org" }] };
    var pc;

    // call start() to initiate
    function start() {
      pc = new RTCPeerConnection(null);

      // send any ice candidates to the other peer
      pc.onicecandidate = function (evt) {
        if (evt.candidate)
          signalingChannel.send(JSON.stringify({ "candidate": evt.candidate }));
      };

      // let the "negotiationneeded" event trigger offer generation
      pc.onnegotiationneeded = function () {
        pc.createOffer(localDescCreated, logError);
      }

      // once remote stream arrives, show it in the remote video element
      pc.onaddstream = function (evt) {
        remoteView.src = URL.createObjectURL(evt.stream);
      };

      // get a local stream, show it in a self-view and add it to be sent
      navigator.getUserMedia({ "audio": false, "video": true }, function (stream) {
        selfView.src = URL.createObjectURL(stream);
        pc.addStream(stream);
      }, logError);
    }

    function localDescCreated(desc) {
      pc.setLocalDescription(desc, function () {
        signalingChannel.send(JSON.stringify({ "sdp": pc.localDescription }));
      }, logError);
    }

    signalingChannel.onmessage = function (evt) {
      if (!pc)
        start();

      var message = JSON.parse(evt.data);
      if (message.sdp)
        pc.setRemoteDescription(new RTCSessionDescription(message.sdp), function () {
          // if we received an offer, we need to answer
          if (pc.remoteDescription.type == "offer")
            pc.createAnswer(localDescCreated, logError);
        }, logError);
      else
        pc.addIceCandidate(new RTCIceCandidate(message.candidate));
    };

    function logError(error) {
      log(error.name + ": " + error.message);
    }
  </script>
</body>
</html>

onnegotiationneededイベント

先ほど紹介しましたコードに

pc.onnegotiationneeded = function () {
  pc.createOffer(localDescCreated, logError);
}

という記述がみられます。これが今回の主役です。

ストリームを追加または削除したりした場合、それを相手に知らせなければなりません。
相手に知らせるにはSDPを使います。つまり、(再度)ネゴシエーションを行います。
pcにonaddstreamやonremovetreamというイベントがありますが、これはネゴシエーションで送られてきたSDPをもとに発生します。
試しに、createOffer()→addStream()→ネゴシエーションという順番で行ってみてください(onnegotiationneededイベントハンドラーは無しで)。
createOffer()実行した時点ではpcにストリームが登録されていませんので生成されるSDPにはストリームの情報が含まれていません。ですのでそのSDPを受け取っても相手側ではonaddstreamイベントが発生せずストリームを取得することができません。

ネゴシエーションは初回1回だけしか行えないまたは行わないと思われがちです(私も思ってました)。基本的には初回のみで十分ですので間違いではありませんが、接続が成立した後もネゴシエーションは何回でも行えます。
addStream()やremoveStream()を行ったら再度ネゴシエーションをしなければならないことがわかりましたが、これを簡単にしてくれるのがonnegotiationneededイベントです。
onnegotiationneededイベントは、addStream()やremoveStream()を行ったとき、つまり自明の通りネゴシエーションが必要となった時に発生します。
このonnegotiationneededイベントハンドラーにcreateOffer()→オファー送信のコードを記述すれば、後はaddStream()やremoveStream()を行うとこのイベントが発生し、ネゴシエーションが行われ相手のpcでonaddstream/onremovestreamイベントが発生します。

複数のストリームの同時送信やストリームの切り替え

もうほぼ説明しきったようなものですが、これでaddStream()でストリームを追加することで複数のストリームを同時に相手に送信したり、また、removeStream()→addStream()することでストリームを切り替えたりすることもできるようになります。

Firefoxではonnegotiationneededイベントが発生しません

このonnegotiationneededイベントですが、今のところChromeとOperaでしか発生しません。Firefoxのpcにはonnegotiationneededのメンバーがあるにはあるのですがそれにハンドラーを設定してもハンドラーが実行されません。この問題は1年以上も前に報告されているのですが(Bug 857115, Bug 1017888)、いまだに修正されていません。

あとがき

IEはORTCを実装し、ORTCを使用したSkypeを一部の地域(UKの地域に最初に公開し徐々に公開地域を広げるとのこと)に公開し始めました。
WebRTCの仕様を初めて見たときは複雑すぎ!!という第一印象を持ったのですが、ORTCの仕様はさらに複雑です。WebRTC1.0のような実装も可能なようですが。
あと、RTCSessionDescriptionオブジェクト。このまま機能が追加されないままだったら、もう廃止にしてほしいものです。

最後に

WebRTCアドベントカレンダーの最後を飾るには役不足な内容となってしまって申し訳ございませんでした。
WebRTCアドベントカレンダーに投稿してくださった皆さん。ありがとうございました!

27
22
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
27
22