はじめに
つかみだけ実際の実装とともにざっくり説明します。各クラスは全て説明するわけではないので、詳細は規格書や https://developer.mozilla.org を見てください。
以下は、今回説明する API を一通り使ったデモです。canvas 要素に描画したものをストリームとして取り出して、フレームに切ったり、エンコードしたり、デコードしたり、ストリームに戻したりします。
2021/12/22 現在、Firefox は未実装のようです。Chrome 94 以降で開いてください。
また、autoplay がタイミング次第で機能しない様なので、必要に応じて video 要素の再生ボタンを押してください…。
2022/11/01: 指定 codec を avc1 から vp8 に変更しました。
WebCodecs とは?
Chrome 94 以降から使える API で、ブラウザ上でエンコードやデコードが出来ます。
そもそも、既にブラウザは様々なAPIによってコーデックを利用しています。例えば、 MediaRecorder
や WebRTC
では 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
や .pipeTo
で TransformStream
や WritableStream
を繋げていきます。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
,flush
はasync
(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 が取り出されます。
- フレームデータは
.readable
プロパティから受け取ります。これはReadableStream
になっているので、先程の Stream API 同様、.pipeTo
でWritableStream
などを繋げることができます。
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
に渡したコールバックから取得できます。このとき、EncodedVideoChunk
とEncodedVideoChunkMetadata
を受け取ります-
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
であり、VideoEncoder
のoutput
コールバック関数から受け取ったものと同じです。
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/