WebCodecs と WebTransport でビデオチャット
Intro
ブラウザの持つ Video/Audio コーデック実装へアクセスする API として WebCodecs の仕様策定と実装が進んでいる。
これにより、映像や音声の変換などといったユースケースへの応用も可能だ。
本来なら WebCodecs 単体の API について解説するところだが、筆者がこの API を待っていた理由であるところの「WebRTC の代替」としての WebCodecs/WebTransport の応用に注目し、背景も踏まえて解説する。
WebRTC
WebRTC は UDP 上に DTLS で交換した鍵を用いて、RTP を SRTP で流し、そのシグナリングに SDP を、ホールパンチに ICE(STUN/TURN) を用いることで、P2P ビデオチャットといったユースケースを可能にした API だ。
しかし、最初から「P2P ビデオチャット」というユースケースに寄せていることもあり、従来使われていたプロトコルスタックを応用して実現しており、P2P ならではの問題を解決するために、全体は割と複雑な構成になっていた。(真面目にフルで実装すると 3 桁近い RFC が絡んでくる)
ところが、実際 WebRTC を用いたサービスを提供する際には、P2P でクライアント同士を会話させるなどということはあまりなく、サービス側で持ったサーバ(SFU/MCU)で、ルーティングや QoS の調整といった様々なサービスを提供することになることが多い。
また、ブラウザ API の抽象度も高く、内部で行われる動作の機微をコントロールするには、新しい API が必要になり、クロスブラウザの切り分けが難しい場合もある。
特にコーデック周りの制御も容易ではなく、実際の映像バイナリを一切触ることなく、その中身を制御するための命令を行うといった雰囲気の実装になる。
これは、チュートリアルにあるような典型的ビデオチャットを提供するだけならうまく動くが、少し特別な実装を入れたり、その UDP トランスポートを使って別のメディア(ゲームなど)を流そうとすると、途端に扱いにくい部分が目立ってくる。
もし P2P ではなく、Server/Client 型のデプロイを前提とし、コーデックもブラウザが持っている実装を直接触るような API があり、データは生のバイナリでやり取りできれば、その方が柔軟性が高く多くのユースケースへ応用できる。そうした発想から生まれたのが WebTransport と WebCodecs だ。
そのあたりのモチベーションは以前にも書いている。
WebTransport
QUIC/HTTP3 の実装が進んでから、このトランスポートを WebSocket のように直接触れる API があり、任意のバイナリを送れるようにしようという発想から始まったのが WebTransport だ。
仕様上は Http3Transport と QuicTransport の両方が検討されているが、現状 Chrome は QuicTransport のみ実装を進めている。
単純に Uint8Array を自由に送ることができるため、バイナリが手元にあれば送るだけだ。
ビデオチャットの場合、問題はカメラの出力をどうバイナリで取得するかだった。
WebCodecs
WebRTC の場合は、取得した MediaStream を RTCPeerConnection に addStream/addTrack すると、そのメディアに合わせてシグナリングを行い、自動でエンコード/デコードし、そのバイナリを自動的に送受信してくれるという、抽象度の高い設計になっていた。
// 接続されたカメラを抽象化した MediaStream を取得
const stream = await navigator.mediaDevices.getUserMedia({video:true, audio:true})
// WebRTC の通信を抽象化した RTCPeerConnection を取得
const connection = new RTCPeerConnection(config)
// 両者を繋ぐ
stream.getTracks().forEach((track) => {
connection.addTrack(track, stream)
})
この流れを見てもわかるように、従来の getUserMedia()
で取得した MediaStream は、そこから直接カメラの映像をバイナリで取得できるような作りにはなってない。
<video>
や RTCPeerConnection などの対応した API に繋ぐことで、あとは中で やってくれる/やってしまう 、ため、外からそこに手を加えることができないのだ。
もし手を加えたい場合は、一旦 Canvas に描画する、Insertable Stream で横取りするなどのワークアラウンドが必要だった。
カメラから取得したデータを、ブラウザが内部で持っている VP8 や H.264 などのコーデック実装でエンコード/デコードを行い、その結果をバイナリで取得できれば様々なユースケースに応用できる。
これが WebCodecs のモチベーションだ。
今回は、まずカメラから取得したビデオストリームに注目して解説する。音声や画面キャプチャもほぼ同じように可能だ。
ビデオの場合は VideoStreamTrack を取得するところから始まる。
ここには、カメラからの生のストリームがあるようなイメージだ。そのままでは大きいため、多くの場合は圧縮を行う。その方式が VP8 や H.264 のようなコーデックになる。
VideoTrack をエンコードするには VideoEncoder を用いる。
初期化時に output コールバックを指定し、初期化後に 後から動的にエンコードパラメータを変えられるようにするために、このような API になっていると思われる。
(将来 Simulcast などに対応する場合はここが拡張されるだろう)
VideoTrackReader を用いて MediaStream からビデオのデータを取り出す。
ここでの単位はフレームで、この VideoFrame を VideoEncoder の 結果は VideoEncoder の初期化時に指定した output コールバックに渡り、これが vp8 でエンコードした結果の ArrayBuffer だ。
デコードもほぼ同じ、まずは初期化し ちなみに VideoTrackWriter は無いため、戻した結果を 音声も基本的に WebAudio を用いるため、音声も映像も自分で表示し、コントローラも自分で作ることになるだろう。
(現状の WebRTC でもそうしていることは多いので、あまり変わらない)
API
const stream = await navigator.mediaDevices.getUserMedia({video:true, audio:true})
const [videoTrack] = mediaStream.getVideoTracks()
configure()
でエンコードの仕様を設定する。
// Encoder
const videoEncoder = new VideoEncoder({
output: function(chunk) {
console.log(chunk)
},
error: function() {
console.error(arguments)
}
})
await videoEncoder.configure({
codec: 'vp8'
width: 640
height: 480
framerate: 30
})
encode()
に渡すとエンコードされる。
const videoReader = new VideoTrackReader(videoTrack)
videoReader.start((videoFrame) => {
videoEncoder.encode(videoFrame)
})
configure()
を呼ぶ。
encode()
に vp8 の chunk を渡せばデコードしたフレームが取り出せる。
createImageBitmap()
でビットマップに変換すれば Canvas に描画できる。
const ctx = $canvas.getContext('2d')
// Decoder
const videoDecoder = new VideoDecoder({
output: async function(frame) {
const imageBitmap = await frame.createImageBitmap()
// canvas に描画
ctx.drawImage(imageBitmap, 0, 0)
},
error: function() {
console.error(arguments)
}
})
videoDecoder.configure({ codec: 'vp8' })
// encoder で作った vp8 の chunk
videoDecoder.decode(chunk)
<video>
に流すことはできない(と思われる)。
ビデオチャット
エンコードしたバイナリを送り、受け取ってデコードすれば、ビデオ会議が可能になる。
そこで WebCodecs + WebTransport を用いたビデオチャットを簡単に作ってみた。
多人数にすると少し面倒なため、自分の映像をサーバがエコーして自分で表示する作りにしてある。
デモは最後に貼るため、以下は特に WebRTC と比較しての部分について解説する。
エンコードした Chunk は以下のような形をしている。
したがって、 転送は基本的に Uint8Array 単位で行うため、今回は CBOR を採用した。しかし後述のサイズの問題があるため、少し工夫している。
QUIC はパケットサイズに上限があるため、QuicTransport でも大きいバイナリが送れず、分割される。
なので v4 だと PMTUD でサイズを確認しない限り 1252 以上は送れない
すると、キーフレームは確実にフラグメントするため、全て揃うまでバッファしないと CBOR でデコードできない。
今回は、暫定的に CBOR エンコードしたバイナリの先頭に Length をつけるようにした。
Bidirectional Stream で送れば自動で分割してくれるため、最初に Length を読んでから、長さ分のパケットが揃うまでバッファに貯めている。
このあたりを上手く転送制御する上では、やはり RTP のような既存の仕組みが欲しくなる。
CBOR はストリーミングっぽい仕組みがあった気がするため、もっとうまく書けたかもしれない。
デフォルトでは最初に key frame を送ったら残りは delta のみがエンコードされる。
もしどこかで絵が壊れた場合は、明示的に つまり、キーフレームを得るタイミングも自分で制御でき、逆を言えば今まで RTCP などでいい感じにやってくれていたことも自分でやらないといけない。
また、絵が壊れたところで Canvas がエラーを出してくれるわけではなく、帯域が狭くなったことやロスが増えたことも誰も教えてくれないため、フィードバック制御やリカバリを自分で考える必要がある。
今回はとりあえず 10 回に一回 KeyFrame を送るようにしてみた。
これまでは 一方 そうした目的からあえて 並行している SIMD+WASM は、そうした画像処理に必要な計算を高速化することが可能なため、今回は触れなかったが、今後はそちらも試しておきたい。
WebCodecs の出力バイナリは、従来の WebRTC でいえば RTC 内の body 部分のみにあたる。
WebTransport の転送は、WebRTC でいうと DTLS-SRTP の部分のみだ。
WebRTC よりも軽いスタックでビデオ会議が実現可能になったが、逆を言えば前述のようなメディアの転送制御に関わる部分は別途自分で用意する必要がある。
「自分で用意する必要がある」は、ちょっと遊んでみたい素人にとっては負荷かもしれないが、WebRTC で困っていたサービス提供者にとっては「自分で提供することができる」と取れる場合もある。
バイナリさえあればソースがカメラである必要も、出力先が Canvas である必要もないため、その間には無限のユースケースがある。
ユースケースに応じて適切な中間ロジックを選定でき、なければ自分で JS や WASM で書くこともできるという点は、自由度をかなり上げている。
単純なビデオ会議を実現する程度なら、軽量なフレームワークをサクッと作ることは可能だし、おそらくそうしたものは多く出てくるだろう。どうしても足りない部分はまた別途標準かもあるかもしれない。
それを除けば、WebCodecs / WebTransport の 2 つさえ実装されたブラウザでは、残りを自分たちの要件に合わせて実装していけるのは、開発者としてもサービスに個性を出しやすく、実装に多様性が生まれると期待している。
シリアライズ
interface EncodedVideoChunk {
constructor(EncodedVideoChunkInit init);
readonly attribute EncodedVideoChunkType type;
readonly attribute unsigned long long timestamp; // microseconds
readonly attribute unsigned long long? duration; // microseconds
readonly attribute ArrayBuffer data;
};
{type, timestamp, duration, data}
のようなオブジェクトをシリアライズして送り、復元する必要がある。
転送制御
メディア制御
encode({keyFrame: true})
するとキーフレームが取得できる。
表示制御
<video>
に表示するのが基本だったが、<video>
は表示をイジるのに多少の面倒臭さがあった。
<canvas>
は周知の通り、表示に関する操作の方法が多く知られており、フィルタ適用、物体/顔検出、合成、傾き etc 手法やライブラリも潤沢にある。
<video>
ではなく <canvas>
を選ぶサービスがもあったくらいなので、このメリットはデカイだろう。
WebCodecs と WebTransport の繋ぎ
Outro
WebCodecs と WebTransport を用いて WebRTC のようなビデオ会議が、よりシンプルなスタックで可能になった。
バイナリをエンコードし、それを送る、その間に必要なロジックは、標準ではなく要件に合わせて開発者が用意するという世界観は、Extensible Web Manifest 以来進められてきた API の低レイヤ化の流れを組んでいるといえる。
そうして、ユースケースに特化し膨らんだ WebRTC の解体は、ブラウザが内側にもつスタックを、ArrayBuffer を取り回すという低レベル API として開発者に提供したことによって、可能性を広げたと考えられる。
本ブログでは、ビデオ会議の側面から API を比較するため、全体の一部しか触れてないが、並行して進められている SIMD+WASM は ArrayBuffer の加工に最適なため、今後はそちらも含めて色々と試しつつ、フィードバックにつなげていきたい。
DEMO
動作するデモを以下に用意した。
Resources
- Spec
- Explainer
- Requirements Doc
- Mozilla Standard Position
- Webkit Position
- TAG Design Review
-
Intents
- Intent to Experiment: WebCodecs
- Intent to Implement WebCodecs
- Intent to Prototype: ImageDecoder API extension for WebCodecs
- Chrome Platform Status
- DEMO
- Blog
- Presentation
- Issues
- Other