📽️

Insertable Streams for MediaStreamTrack APIを試してみた

2023/07/28に公開

はじめに

ブラウザ上でWEBカメラなどのMediaStreamを解析したり加工したいと思ったとき、昔ながらの方法だとvideoやcanvasなどを利用していたかと思います。
Chrome94以降であれば MediaStreamTrack を使うことで、より効率的に実現することができます。

下記のような事をブラウザ技術を使ってやりたい場合は参考になるかもしれません。

  • Webカメラ映像をAI解析したい
  • Webカメラ映像をリアルタイムに加工したい(顔認識してサングラスかけたり、エフェクトかけたり..etc)
  • マイク音声を認識してText to Speechにかけたい

勿論その他にも応用次第で色々なことができるかと思います。

デモについて

Typescriptで実装したデモアプリを元に解説したいと思います。
今回のデモではWEBカメラで写した映像内にQRコードを検出したら、映像を加工した上でWebRTCを使ってlocal上で送信/受信しています。

下記画像が動作時のスクリーンショットになりますが、上の映像は加工前の生カメラ映像で、下の映像はWebRTC経由で受信した加工映像になります。

イメージ画像

加工された映像では検出されたQRコードが赤枠で囲われ、下部に読み取ったテキストが表示されています。
このデモでは送信側で検出から加工までを行っていますが、勿論受信側で行うことも可能ですので用途に応じて使い分けることができます。

コード解説

順番に解説していきますが、記事の最後にコード全文があるGihubリポジトリへのリンクを掲載していますので、デモを動かしながら見たい方などはリンク先をご参照ください。

MediaStreamTrack利用箇所

下記のコード例ではまずWebカメラ映像のstreamを取得し、それを元にMediaStreamTrackを使って解析/加工しています。
そもそもこの MediaStreamTrack ですが背景として Streams APIと同じ思想で実装されており、データを小さなChunk(MediaStreamTrackではFrame)毎に処理していくイメージとなります。

MediaStreamTrackProcessor を使ってデータを順次読み出し、 TransformStream で変換し、MediaStreamTrackGenerator へ結果を書き出すという流れになっています。

今回のデモでは音声の解析/加工はしていませんが、同じようなコードで音声でも利用可能です。

  // Webカメラ映像を取得する
  const stream = await navigator.mediaDevices.getUserMedia({ video: true });
  const videoTrack = stream.getVideoTracks()[0];
  const transformStream = setupTransformer(videoTrack);
  // doSomething(transformStream);  

  // 引数で指定したVideoTrackをMediaStreamTrackを利用して解析/加工する
  setupTransformer(videoTrack: MediaStreamVideoTrack) {
    const trackProcessor = new MediaStreamTrackProcessor({ track: videoTrack });
    const trackGenerator = new MediaStreamTrackGenerator({ kind: 'video' });

    // 映像の加工を行うための処理
    const transformer = new TransformStream({
      async transform(videoFrame, controller) {
        // ココはこの下のセクションの「QRコードの検出と映像加工」で解説
        const newFrame = await processingDetectQR(videoFrame);
        controller.enqueue(newFrame);
      },
    });

    trackProcessor.readable
      .pipeThrough(transformer)
      .pipeTo(trackGenerator.writable);

    return new MediaStream([trackGenerator]);
  }

QRコードの検出と映像加工

transform() で渡されるパラメータはVideoTrackであれば VideoFrame という型のデータが渡されます。このVideoFrameから createImageBitmap()を使ってImageBitmapを作ることができます。

ImageBitmapができたらブラウザAPIのBarcodeDetectorを利用してQRコードがあるか調べます。
もしQRコードが見つかればcanvasへ描画した上で任意の加工を行い、最後にcanvasからVideoFrameを生成して返しています。

transform内でenqueue()されたVideoFrameはそのままtrackGeneratorに書き出しされていきますので、あとはそれを元に作ったMediaStreamを配信したり再生すれば加工された映像を見ることができます。

  async processingDetectQR(videoFrame: VideoFrame) {
    const timestamp = videoFrame.timestamp;
    const width = videoFrame.displayWidth;
    const height = videoFrame.displayHeight;
    const bitmap = await createImageBitmap(videoFrame);

    const detectedBarcodes = await barcodeDetector.detect(bitmap);
    // QRコードが検出されないときは元のVideoFrameをそのまま返す
    if (!detectedBarcodes.length) {
      bitmap.close();
      return videoFrame;
    }
    videoFrame.close();

    // videoFrameをCanvasへ描画する
    canvas.width = width;
    canvas.height = height;
    ctx.drawImage(bitmap, 0, 0, width, height);
    bitmap.close();

    await highlightBarcode(detectedBarcodes);

    const newBitmap = await createImageBitmap(canvas);
    return new VideoFrame(newBitmap, { timestamp });
  }

まとめ

今回のデモ実装では最低限の実装でしたがWebカメラ側でQRコードを激しく移動させても、検出マーカーはぬるぬる追従してくるのでパフォーマンスは悪くなさそうです。
とは言え毎フレーム検出にかける必要はないので、10フレームに1回だけ実施するなど工夫すれば更にパフォーマンスが高まるかと思います。

対応するブラウザに制約はあるもののcanvas+videoを利用した実装例よりも、パフォーマンスが高い印象で、その分重い処理でも実行できる余地が広がったように思います。

リポジトリ

https://github.com/eddybean/media-transform-demo

参考

https://zenn.dev/mganeko/articles/mediastreamtrackprocessor

Discussion