6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

SkyWayの公式チュートリアルでInsertable Streamsを試してみた

Last updated at Posted at 2020-10-26

10月6日にリリースされたChrome M86(安定版)から、WebRTC Insertable Streams APIが利用できるようになりました。

Insertable Streamsは簡単に言うと、音声や映像のフレーム毎のバイナリデータに任意の値を差し込む技術です。バイナリデータを触れるので、映像データを暗号化(E2EE)する等の用途に利用されている印象です。また、音声や映像とタイミングをズラさずにデータを届けられるという利点もあります。

さっそく、国内のWebRTC PaaSであるSkyWayで試してみました。

javascript SDKの公式チュートリアル(P2P)を少しだけ改造して、任意のデータをやり取りできるようにしてみます。

デモ

output.gif

CODEPEN
でお試しいただけます。

コード


<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Insertable Streams on SkyWay</title>
    <script src="https://cdn.webrtc.ecl.ntt.com/skyway-latest.js"></script>
  </head>
  <body>
    <video id="my-video" width="400px" autoplay muted></video>
    <video id="their-video" width="400px" autoplay></video>
    <p id="my-id"></p>
    <input id="their-id" placeholder="their peer id" />
    <button id="make-call">発信</button>
    <input id="num" placeholder="send number" />
    <p id="receive-num"></p>
    <script>
      const APIKEY = "YOUR APIKEY";

      const senderTransform = (pc) => {
        pc.getSenders().forEach((sender) => {
          const senderStreams = sender.createEncodedStreams();
          const readableStream = senderStreams.readable;
          const writableStream = senderStreams.writable;
          const transformStream = new TransformStream({
            transform: (encodedFrame, controller) => {
              const len = encodedFrame.data.byteLength;
              if (sender.track.kind === "audio") {
                const container = new Uint8Array(len + 1);
                container.set(new Uint8Array(encodedFrame.data), 0);
                const num = new Uint8Array(1);
                num[0] = document.getElementById("num").value;
                container.set(num, encodedFrame.data.byteLength);
                encodedFrame.data = container.buffer;
              }
              controller.enqueue(encodedFrame);
            },
          });
          readableStream.pipeThrough(transformStream).pipeTo(writableStream);
        });
      };

      const receiverTransform = (pc) => {
        pc.getReceivers().forEach((receiver) => {
          const receiverStreams = receiver.createEncodedStreams();
          const readableStream = receiverStreams.readable;
          const writableStream = receiverStreams.writable;
          const transformStream = new TransformStream({
            transform: (encodedFrame, controller) => {
              if (receiver.track.kind === "audio") {
                const mediaData = new Uint8Array(
                  encodedFrame.data.slice(0, -1)
                );
                const num = new Uint8Array(encodedFrame.data.slice(-1));
                document.getElementById("receive-num").textContent = num[0];
                encodedFrame.data = mediaData.buffer;
              }
              controller.enqueue(encodedFrame);
            },
          });
          readableStream.pipeThrough(transformStream).pipeTo(writableStream);
        });
      };

      navigator.mediaDevices
        .getUserMedia({ video: true, audio: true })
        .then((stream) => {
          const videoElm = document.getElementById("my-video");
          videoElm.srcObject = stream;
          videoElm.play();
          localStream = stream;
        })
        .catch((error) => {
          console.error("mediaDevice.getUserMedia() error:", error);
          return;
        });

      const peer = new Peer({
        key: APIKEY,
        debug: 2,
        config: {
          encodedInsertableStreams: true,
        },
      });

      peer.on("open", () => {
        document.getElementById("my-id").textContent = peer.id;
      });

      peer.on("call", (mediaConnection) => {
        mediaConnection.answer(localStream);
        mediaConnection.on("stream", (stream) => {
          document.getElementById("their-video").srcObject = stream;
          const pc = mediaConnection.getPeerConnection();
          receiverTransform(pc);
          senderTransform(pc);
        });
      });

      document.getElementById("make-call").onclick = async () => {
        const theirId = document.getElementById("their-id").value;
        const mediaConnection = peer.call(theirId, localStream);
        mediaConnection.on("stream", (stream) => {
          document.getElementById("their-video").srcObject = stream;
        });
        setTimeout(() => {
          const pc = mediaConnection.getPeerConnection();
          senderTransform(pc);
          receiverTransform(pc);
        }, 1000);
      };
    </script>
  </body>
</html>

解説

やっていることの概要は以下のとおりです。

  • 送信側:音声バイナリデータの先頭に、ある1byteのデータを差し込む
  • 受信側:音声バイナリデータの先頭から、1byte分のデータを取得すると同時に削除する

送信をsenderTransform、受信receiverTransform関数として、最初に用意しています。

では次に、通信が確率するまでの流れを追ってみます。

Peerの作成


      const peer = new Peer({
        key: APIKEY,
        debug: 2,
        config: {
          encodedInsertableStreams: true,
        },
      });

peerインスタンスを作成する際に、configというパラメータで、 encodedInsertableStreams: trueを与えます。
これにより、このpeerでInsertable Streams APIを使えるようになります。

発信

      // 発信ボタン押下
      document.getElementById("make-call").onclick = async () => {
        const theirId = document.getElementById("their-id").value;
        const mediaConnection = peer.call(theirId, localStream);
        // 相手映像が来たらDOMに適用
        mediaConnection.on("stream", (stream) => {
          document.getElementById("their-video").srcObject = stream;
        });
        // 1秒待つ
        setTimeout(() => {
          // RTCPeerConnectionを取得
          const pc = mediaConnection.getPeerConnection();
          senderTransform(pc); // 送信streamに任意データを差し込み
          receiverTransform(pc); // 受信streamから任意データを取得
        }, 1000);
      };

まず、発信ボタンを押した時に、peer.call()の戻り値としてMediaConnectionのインスタンスが取得できます。

MediaConnectionが確立する(openプロパティがtrueになる)まで、setTimeoutで1秒間待った上で、getPeerConnectionを実行し、RTCPeerConnectionを取得します。(Insertable Streamsを行うにはこのRTCPeerConnectionが必要です)

RTCPeerConnectionを先のsenderTransform, receiverTransform関数に渡してあげることで、流れているstreamに任意データを差し込み&取得します。

着信

      peer.on("call", (mediaConnection) => {
        mediaConnection.answer(localStream);
        mediaConnection.on("stream", (stream) => {
          document.getElementById("their-video").srcObject = stream;
          // RTCPeerConnectionを取得
          const pc = mediaConnection.getPeerConnection();
          senderTransform(pc); // 送信streamに任意データを差し込み
          receiverTransform(pc); // 受信streamから任意データを取得
        });
      });

発信側と同様に、getPeerConnection()でRTCPeerConnectionを取得し、sender/receiverTransform関数に渡してあげます。

任意データの差し込み&取得

sender/receiverTransform関数では、以下を実施しています。

  • sender:numという1バイトのUint8Array型の変数を用意し、DOMから取得した値を入れ、音声バイナリデータの先頭に足す

  • receiver:音声バイナリデータの先頭から値を削除 & 取得しDOMに適用

Insertable Streamsの詳細な使い方についてはここでは割愛します。

(以下に公式サイトや参考にした記事のリンクを貼りましたので、そちらをご参照ください。)

参考にしたInsertable Streams関連記事

6
0
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
6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?