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

# はじめに

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

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

https://jsfiddle.net/buyoh/6on5tg98/108/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104336/880a4e10-9efa-7c35-1f55-f0497c8e22bb.png)

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


# WebCodecs とは？

[Chrome 94 以降から使える](https://developers-jp.googleblog.com/2021/09/chrome-94-webcodecswebgpu.html) API で、ブラウザ上でエンコードやデコードが出来ます。

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

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

# API

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

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

[^codec-api]: 引数や返値の型から、Decoder 単体や Encoder 単体で使うのはかなり難しいように見えます。

## Streams API

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

```javascript
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 に整数を流す実装例を示します。続くサンプルコードも、この実装例の一部になっています。


https://jsfiddle.net/buyoh/argjv63c/2

### [ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)

- ストリームを生成し続けます。
- コンストラクタ引数の `start` に渡した関数は、コントローラ `ReadableStreamDefaultController` を受け取ります。chunk を生成する度に、chunk をコントローラの `.queue` に渡せば良いです。
- 終端に到達したら、コントローラの `.close` を呼びます

```javascript
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](https://developer.mozilla.org/en-US/docs/Web/API/TransformStream)

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

```javascript
function createNumberConverter() {
  const transformStream = new TransformStream({
    start() {},
    transform(chunk, controller) {
      controller.enqueue(chunk * chunk);
    },
    flush() {},
  });
  return transformStream;
}
```

### [WritableStream](https://developer.mozilla.org/en-US/docs/Web/API/WritableStream)

- ストリームが最終的に行き着く先です。

```javascript
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](https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrackProcessor)

- MediaStreamTrack からデータをフレーム単位で取り出します。以下の例の場合、[VideoFrame](https://developer.mozilla.org/en-US/docs/Web/API/VideoFrame) が取り出されます。
  - audio も AudioFrame というものがありますが、今は audio は解説なし。
  - 使い終わった `VideoFrame` は `.close()` で閉じなければなりません。特に、`VideoEncoder` に渡す場合は閉じる必要があります[^close]。`.close` せずガベージコレクタに回収されると[^gc]、`A VideoFrame was garbage collected without being closed. Applications should call close() on frames when done with them to prevent stalls.` とログが出ます。 
- フレームデータは `.readable` プロパティから受け取ります。これは `ReadableStream` になっているので、先程の Stream API 同様、`.pipeTo` で `WritableStream` などを繋げることができます。

[^close]: エンコードが完了する（outputに指定したコールバックからデータを受けとる）前に閉じてしまって良いようです。
[^gc]: `frame = null` のようにインスタンスの関連をすべて外して数分放置すると、GC に回収されます。

```javascript
  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 からも簡単に作れます

```javascript
const canvas = document.getElementById('my-canvas');
const f = new VideoFrame(canvas, { timestamp: 0, duration: 50 * 1000 });
```

### [MediaStreamTrackGenerator](https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrackGenerator)

- フレームデータから、`MediaStreamTrack` を作ります。動画の場合、`VideoFrame` を受け取って動画のストリームを作ります。

```javascript
  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` と同じく、セキュリティの制限があります[^sec]。

[^sec]: 例えば、`0.0.0.0:8000` ではダメだけど `localhost:8000` なら使えるなど。

### [VideoEncoder](https://developer.mozilla.org/en-US/docs/Web/API/VideoEncoder)

- `.encode` メソッドから、`VideoFrame` を次々に渡していきます。
- エンコードされたデータは、 `output` に渡したコールバックから取得できます。このとき、`EncodedVideoChunk` と `EncodedVideoChunkMetadata` を受け取ります
  - `EncodedVideoChunk` は、エンコードされたデータそのものです。タイムスタンプや、キーフレームかどうか等の情報を持っています。
    - 生データにアクセスするには、`.copyTo` メソッドで `BufferSource` にコピーします。WebTransport で別端末に転送する場合のような、jsonやバイナリに変換しなければならない時は、これを使います。
  - `EncodedVideoChunkMetadata` は、使用したコーデックの情報等が格納されます。毎回値がセットされるとは限りません。特に、`meta.decoderConfig` は前回と値が異なった場合にのみセットされます。
    - `meta.decoderConfig` は、`VideoDecoder.configure` の引数にそのままセットできます。

```javascript
  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](https://developer.mozilla.org/en-US/docs/Web/API/VideoDecoder)

- インターフェースは `VideoEncoder` と大体同じです。デコードされたデータは、コールバック関数から受け取ります。
- `.decode` の引数は `EncodedVideoChunk` であり、`VideoEncoder` の `output` コールバック関数から受け取ったものと同じです。

```javascript
  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/