4
1

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 1 year has passed since last update.

WebCodecs と Insertable Streams for MediaStreamTrack API と Streams API ざっくり

Last updated at Posted at 2021-12-22

はじめに

つかみだけ実際の実装とともにざっくり説明します。各クラスは全て説明するわけではないので、詳細は規格書や https://developer.mozilla.org を見てください。

以下は、今回説明する API を一通り使ったデモです。canvas 要素に描画したものをストリームとして取り出して、フレームに切ったり、エンコードしたり、デコードしたり、ストリームに戻したりします。
2021/12/22 現在、Firefox は未実装のようです。Chrome 94 以降で開いてください。
また、autoplay がタイミング次第で機能しない様なので、必要に応じて video 要素の再生ボタンを押してください…。

image.png

2022/11/01: 指定 codec を avc1 から vp8 に変更しました。

WebCodecs とは?

Chrome 94 以降から使える API で、ブラウザ上でエンコードやデコードが出来ます。

そもそも、既にブラウザは様々なAPIによってコーデックを利用しています。例えば、 MediaRecorderWebRTC では MediaStream や URL を直接渡すので、コーデックを意識することなく実装できます。このように、殆どの場合で、WebCodec を使わずともコーデックが必要となる機能を実現できます。

WebCodecs は、プラットフォームが持つコーデックを利用して、メディアを高効率なバイナリデータに圧縮、または圧縮したデータの展開をすることができます。これにより、例えば、WebTransport と組み合わせて、柔軟性のあるビデオチャットアプリが設計できます。
…ありふれた説明ですが、他に使い道が思い浮かびませんでした。

API

本記事で紹介する API です。

  • Streams API
    • ストリームデータを取り扱うためのインターフェースを提供します。
    • ReadableStream
    • TransformStream
    • WritableStream
  • Insertable Streams for MediaStreamTrack API
    • メディアストリームからフレームを取り出したり、取り出したフレームからメディアストリームを作ったりします。
    • 取り出したフレームは、一旦 canvas に貼って加工したり、あるいは順番を入れ替えたりすることができ、自由です。
    • Streams API のインターフェースを持つため、Streams API と非常に相性が良いです
    • MediaStreamTrackProcessor
    • MediaStreamTrackGenerator
  • WebCodecs API
    • プラットフォームが持っているコーデックを使って、動画や音声を圧縮したり展開したりします。
    • Decoder と Encoder は2つ1組で使います。1
    • VideoEncoder
    • VideoDecoder
    • ほか

Streams API

以下のように、ReadableStream.pipeThrough.pipeToTransformStreamWritableStream を繋げていきます。ReadableStream が入力元、WritableStream が出力先に該当し、TransformStream で加工するイメージです。
各ストリームの挙動は、コンストラクタ引数のオプションに与えていきます。

const readableStream = new ReadableStream(aaaa);
const transformStream1 = new TransformStream(bbbb);
const transformStream2 = new TransformStream(bbbb);
const writableStream = new WritableStream(cccc);
readableStream
  .pipeThrough(transformStream1)
  .pipeThrough(transformStream2)
  .pipeTo(writableStream);

以下に、Streams API に整数を流す実装例を示します。続くサンプルコードも、この実装例の一部になっています。

ReadableStream

  • ストリームを生成し続けます。
  • コンストラクタ引数の start に渡した関数は、コントローラ ReadableStreamDefaultController を受け取ります。chunk を生成する度に、chunk をコントローラの .queue に渡せば良いです。
  • 終端に到達したら、コントローラの .close を呼びます
function createNumberSource() {
  let timer = null;
  const readableStream = new ReadableStream({
    start(controller) {
      // 定期的に乱数を送る
      const loop = (count) => {
        if (count < kMaxCount) {
          const n = Math.floor(Math.random() * 10);
          controller.enqueue(n);
          timer = setTimeout(loop, kInterval, count + 1);
        } else {
          controller.close();
        }
      };
      timer = setTimeout(loop, 0, 0);
    },
    cancel() {
      clearTimeout(timer);
    },
  });
  return readableStream;
}

TransformStream

  • ストリームを受け取ってストリームを返します。
  • transform で chunk とコントローラを受け取ります。コントローラ TransformStreamDefaultController は、ReadableStream と同様に、加工した chunk を返すために使います。
  • Array で言うところの .map に似ていますが、要素数は一致していなくてもよいです。
  • transform, flushasync (Promise を返す関数) に出来ます。async にすることで、成功か失敗かを報告できます。controller も .error メソッドを持っているので、こちらでも失敗を報告できます。
function createNumberConverter() {
  const transformStream = new TransformStream({
    start() {},
    transform(chunk, controller) {
      controller.enqueue(chunk * chunk);
    },
    flush() {},
  });
  return transformStream;
}

WritableStream

  • ストリームが最終的に行き着く先です。
function createNumberSink() {
  const writableStream = new WritableStream({
    start() {},
    write(chunk) {
      console.log(chunk);
    },
    close() {
      console.log("The stream has been clsoed");
    },
    abort(err) {
      console.warn(err);
    },
  });
  return writableStream;
}

Insertable Streams for MediaStreamTrack API

ストリームをフレーム単位に切り出す MediaStreamTrackProcessor と、フレーム単位の列をストリームに戻す MediaStreamTrackGenerator の 2 つを説明しています。

MediaStreamTrackProcessor

  • MediaStreamTrack からデータをフレーム単位で取り出します。以下の例の場合、VideoFrame が取り出されます。
    • audio も AudioFrame というものがありますが、今は audio は解説なし。
    • 使い終わった VideoFrame.close() で閉じなければなりません。特に、VideoEncoder に渡す場合は閉じる必要があります2.close せずガベージコレクタに回収されると3A VideoFrame was garbage collected without being closed. Applications should call close() on frames when done with them to prevent stalls. とログが出ます。
  • フレームデータは .readable プロパティから受け取ります。これは ReadableStream になっているので、先程の Stream API 同様、.pipeToWritableStream などを繋げることができます。
  const video = document.getElementById("my-src-video");
  video.addEventListener("loadeddata", () => {
    const stream = video.captureStream();
    const videoTrack = stream.getVideoTracks()[0];
    const processor = new MediaStreamTrackProcessor({ track: videoTrack });
    processor.readable.pipeTo(myNiceWritableStream);
  });
  • おまけ: VideoFrame は canvas からも簡単に作れます
const canvas = document.getElementById('my-canvas');
const f = new VideoFrame(canvas, { timestamp: 0, duration: 50 * 1000 });

MediaStreamTrackGenerator

  • フレームデータから、MediaStreamTrack を作ります。動画の場合、VideoFrame を受け取って動画のストリームを作ります。
  const trackGenerator = new MediaStreamTrackGenerator({ kind: "video" });
  myNiceReadableStream.pipeTo(trackGenerator.writable);

  // MediaStream を作る
  // MediaStreamTrackGenerator は MediaStreamTrack を継承するので、そのまま addTrack できる
  const outputMediaStream = new MediaStream();
  outputMediaStream.addTrack(trackGenerator);

  // 作った MediaStream を適当な video へセット
  const videoElem = document.getElementById("my-dst-video");
  videoElem.srcObject = outputMediaStream;

WebCodecs API

プラットフォームが持つコーデックを扱う API です。
ウェブカメラを取得する getUserMedia と同じく、セキュリティの制限があります4

VideoEncoder

  • .encode メソッドから、VideoFrame を次々に渡していきます。
  • エンコードされたデータは、 output に渡したコールバックから取得できます。このとき、EncodedVideoChunkEncodedVideoChunkMetadata を受け取ります
    • EncodedVideoChunk は、エンコードされたデータそのものです。タイムスタンプや、キーフレームかどうか等の情報を持っています。
      • 生データにアクセスするには、.copyTo メソッドで BufferSource にコピーします。WebTransport で別端末に転送する場合のような、jsonやバイナリに変換しなければならない時は、これを使います。
    • EncodedVideoChunkMetadata は、使用したコーデックの情報等が格納されます。毎回値がセットされるとは限りません。特に、meta.decoderConfig は前回と値が異なった場合にのみセットされます。
      • meta.decoderConfig は、VideoDecoder.configure の引数にそのままセットできます。
  const encoder = new VideoEncoder({
    output(chunk, meta) {
      console.log(chunk, meta);
    },
    error: console.error,
  });
  encoder.configure({
    codec: "avc1.42400a",
    width: 864,
    height: 360,
  });
  const encode = (frame) => {
    // frame: VideoFrame
    encoder.encode(frame);
  };

VideoDecoder

  • インターフェースは VideoEncoder と大体同じです。デコードされたデータは、コールバック関数から受け取ります。
  • .decode の引数は EncodedVideoChunk であり、VideoEncoderoutput コールバック関数から受け取ったものと同じです。
  const decoder = new VideoDecoder({
    output(videoFrame) {
      console.log(videoFrame);
    },
    error: console.error,
  });
  const decode = (chunk, decoderConfig) => {
    if (decoderConfig) decoder.configure(decoderConfig);
    decoder.decode(chunk);
  };

おわりに

WebCodecs に興味があって調べ始めたのですが、どうやら Insertable Streams for MediaStreamTrack API の方がなんだか面白そうです…
WebCodecs は、MediaStream の通信路の自由度を高めます。Web Transport 以外の通信路だと何があるでしょうか。Web Bluetooth 経由とか?

参考資料

https://developer.mozilla.org/en-US/docs/Web/API
https://streams.spec.whatwg.org/
https://w3c.github.io/webcodecs/

  1. 引数や返値の型から、Decoder 単体や Encoder 単体で使うのはかなり難しいように見えます。

  2. エンコードが完了する(outputに指定したコールバックからデータを受けとる)前に閉じてしまって良いようです。

  3. frame = null のようにインスタンスの関連をすべて外して数分放置すると、GC に回収されます。

  4. 例えば、0.0.0.0:8000 ではダメだけど localhost:8000 なら使えるなど。

4
1
1

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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?