Block Rockin’ Codes

back with another one of those block rockin' codes

ORTC が切り開く SVC サイマルキャストと WebRTC NV

intro

「ORTC って WebRTC がちょっと便利になるくらいなんでしょ?」くらいに思っている人が結構いるようだったので、現時点で予想される ORTC のもつ可能性 と、現状の WebRTC の問題点、そして WebRTC がこれからどうなっていきそうかについて、自分の理解している範囲で書いてみます。

ORTC については、ほとんど実装が無く(と書いてる間に Edge に入っちゃったんですが)まだドラフトやそこにある Example、 ML での議論などの公開情報を元に書いてるだけなので、間違っているものや、将来変わるところも有ると思います。よって内容は一切保証しません。 また、何か自分の理解がおかしいところなど有った場合は、コメントなどで指摘頂けると幸いです。

低レベル API 化へ

WebRTC は少なからず「ブラウザで P2P テレビ会議」をするというユースケースを中心として策定されました。 HTML5 の流れを組む API 群は、最初に「ユースケース」を大事にしていたのと同様に、WebRTC 1.0 もちょっとまじめに触ると「ユースケースを外れた途端の使いにくさ」が少なからずありました。

例えば、シグナリングで使用する情報が SDP 文字列であるために、変更したい場合は文字列処理が必要であったり、通信相手ごとに映像のクオリティを分けることができないなどの問題が指摘されています。 また、 WebRTC スタックは複数プロトコルの合わせ技でできているにも関わらず、高レベルな API でラップされているため、それら個々のプロトコルレベルで細かい制御ができないという問題も有ります。

最近では、こうした API は「低レベル API セット」の上で再構築という流れになるのは、このブログでも何度か言及した通りです。

それが MS 主導で提案された ORTC でした。今までの WebRTC API に輪をかけて色々と濃い感じになってます。

しかし ORTC は CG(community group) のものであり、 W3C が管理する API ではありません。したがって、これがこのまま W3C で勧告されることは基本的にはあり得ません。 W3C では、この ORTC の知見を WebRTC にどう取り込んで行くのか、そして WebRTC 1.0 の次のバージョンである、 WebRTC NV(next version) の方向性について議論が進んでいます。

今回は、特に話題となっている以下の2つを中心に、これから WebRTC に何が起こって行くのかを見てみたいと思います。

  • MultiStream
  • SVC Simulcast

MultiStream

相手と複数のストリーム(MediaStream)、例えば二つのカメラの映像をやり取りしたい場合、単純に二本の PeerConnection を張るという方法があります。 しかし、一本の PeerConnection の上に二本のストリームを多重化できればリソースを節約できます。

f:id:Jxck:20150923010536p:plain

実際に WebRTC 1.0 の API は、この MultiStream に対応した形で定義されています。 具体的には addStream()onnegotiationneeded イベントです。

addStream()

その名の通り、 PeerConnection に対してストリームを追加するメソッドです。 しかし、ただこのメソッドを呼んで終わりとはいきません。

シグナリングを実施して、 Peer に対してストリームの追加を通知する必要があります。 これは、 onnegotiationneeded の発火によって補足することができるため、そのコールバックで再度 SDP を取得し直してシグナリングしなおします。

peerConnection.onnegotiationneeded = () => {
  // ここでシグナリング
};

// 既存コネクションにストリームを追加
peerConnection.addStream(stream);

すると相手側で onaddstream が発火し、そこで追加のストリームを取得できるわけです。

peerConnection.onaddstream = (stream) => {
  // video タグに表示など
}

このフローを使うと、ストリームが追加されてない状態から順次ストリームを追加して行くこともできます。

Web にあるサンプルコードでは、あらかじめ addStream() を終えてから最初のシグナリングを行うことで onnegotiationneeded は無視されている場合もありますが、本来はこのイベントが「シグナリングを実行すべきタイミング」です。 ストリームの追加を途中で扱うためにはこのフローを行うことになります。

そして、そこで必ずハマるのは ChromeFirefox 間で、このフローがうまくいかないことがあるということです。

SDP plan B と unified plan

onnegtiationneeded のタイミングで createOffer() し直して取得する SDP には、複数ストリームの存在を示すラインが追加されます(m-line)。

しかし、この SDP の記述方式には現在 Chrome が採用している "plang B" というフォーマットと、 Firefox が採用している "Unified Plan" というフォーマットの二種類があります。

残念なことに、この二つのフォーマットには互換性が無いため、 multistream を Ch <-> FF 間で行おうとすると失敗する場合があります。

回避方法として、出力結果である文字列の SDP を書き換える方法があります。これを行うライブラリもすでに提供されています。

https://github.com/jitsi/sdp-interop

しかし、本質的な解決は plan の統一です。これは今後 Unified Plan での統一が決まっているため、基本的には Chrome の実装待ちとなるでしょう。

Track レベルの多重化

さて、 multi stream はストリーム(MediaStream)レベルの多重化な訳ですが、より細かく制御したい場合は、これだと粒度が大きすぎる場合があります。具体的には、ストリームは複数のトラック(MediaStreamTrack)からできているため、そのトラックレベルで制御をしたい場合です。

例えば getUserMedia(){ audio: true, video: true } を指定するおなじみのアレで取得されるストリームは Audio トラックと Video トラックが両方入ったストリームです。そしてストリームには addTrack() でより多くのトラックを追加できます。

PeerConnection は、このトラックレベルでのネットワーク上の扱いをそこまで細かく指定することができませんでした。

この問題は後述する、 ORTC で RTPSender/Receiver が追加されることで解決します。

SVC サイマルキャスト

多拠点で会議をして受信側が複数人いた場合、それぞれのネットワーク状況や受信している端末は同じとは限りません。

例えば送信者が送った動画が高画質・高フレームレートだったとしても、受信者が弱い Wi-Fi 上の画面の小さいモバイル端末だった場合は、あまり嬉しくありません。むしろ解像度とフレームレートを下げたものを送ってくれた方が快適な場合があります。

本来であれば、受信者の持つ表示能力やおかれている帯域状況に応じて、適切なクオリティの動画を配信したい。つまり、一つの動画を複数のクオリティで配信する。これがサイマルキャスト(Simulcast)です。

Wikipedia でサイマルキャスト(サイマル放送)を調べると全然違う意味で載っているように、一言でサイマルキャスと言ってもコンテキストによって意味が変わります。ここで言っているのは、テレビ会議などで使われるサイマルキャストです。

では、どのように自分に合ったクオリティの動画を要求するのでしょうか?

従来の AVC(Advanced Video Coding) であれば、一旦サーバで配信者の動画を受け取り、それを個々の受信者に合わせたパラメータに変えて、再エンコードをするという方法があります。

f:id:Jxck:20150923010628p:plain

しかし、これは再エンコードにかなりのリソースが必要な上、受信者によって必要なパラメータの組み合わせも非常に多くなってしまいます。遅延の問題も無視できません。

そこで注目されるのが SVC(Scalable Video Coding) という仕様です。

SVC(Scalable Video Coding)

SVC の基本的なアイデアは、動画を複数のレイヤに分けてエンコードすることです。

まず、元となる映像をベースとなる低解像度・低フレームレート・低ビットレートなパラメータでエンコードします。これを基本レイヤーと呼び、全員に配ります。

加えて、基本レイヤとの差分となる拡張レイヤをエンコードします。これはいわゆる diff のようなものだと考えるとわかりやすいでしょう。

サーバは、受け取った SVC の映像のレイヤを把握し、クライアントに配信する際に対象の表示能力や帯域状況に応じて、必要なレイヤだけを流してやります。

例えば受信者が低帯域にある画面の小さいモバイルだった場合は、基本レイヤ(解像度/フレームレート/ビットレート)のみを受けとります。

一方で、受信者が十分な帯域のある大画面のテレビ会議システムだった場合、基本レイヤに加えて二つの拡張レイヤを受けとり、デコード時にマージすることで高クオリティ(解像度/フレームレート/ビットレート)の映像が手に入るわけです。

もちろん、その中間のスペックの受信者は、基本レイヤに加えて一つ目の拡張レイヤだけを加えることで、中クオリティの映像を手に入れることができます。

f:id:Jxck:20150923010647p:plain

基本レイヤは、最低限会議は成り立つというレベルのものであり、クオリティが低い変わりにデータも小さくなっています。代わりにエラー訂正などについては他のレイヤよりも重視します。

拡張レイヤが壊れたり落ちたりしても基本レイヤがあれば最低限の映像は見えるため、そうした場合は基本レイヤにフォールバックしてリアルタイム性を確保します。

これは見方を変えると、単一レイヤに全てまとまっていた AVC ではロスによって画面がこわれていたものが、 SVC ではデータの少ない基本レイヤのリカバリにだけ気を使って入れば、ネットワークが不安定になっても、低クオリティへのフォールバックによって会議を続けることができるなどのメリットもあります。

また、映像を犠牲にしてオーディオに優先度を振ることで、会議を快適にすることができます。

なによりサーバ側で再エンコードが必要だった AVC と違い、エンコードを配信元クライアントが一回だけ行えばよく、サーバのリソースも節約できるため、効率のよいテレビ会議を構築するためには非常に有望な技術なのです。

SVCエンコードパラメータ

SVCエンコード時に注目するパラメータは大きく三つあります。

  • フレームレート(Temporal)
  • 解像度(Spatial)
  • 画質(Quarity)

フレームレート(Temporal)

フレームレートは一秒間に表示する画の数です。拡張レイヤを加えるとフレームが増え、動きが滑らかになります。

f:id:Jxck:20150923010659p:plain

解像度(Spatial)

解像度は単位面積あたりの画素数です。拡張レイヤを加えると大きな画面でもはっきり映ります。

f:id:Jxck:20150923010712p:plain

画質(Quality)

SNR と言われる場合もあるようです。いわゆる画の鮮明さです。

f:id:Jxck:20150923010725p:plain

ORTC の Low Level API セット

ORTC というと必ずと言っていいほど出てくる図があります。

ORTC

この図だけ見ても分かりにくいかもしれませんが、ざっくり言うと各ブロックがオブジェクトで、横の矢印が依存関係、縦がイベントとなっています。ここに出てないオブジェクトもまだあります。

ここまでの話を踏まえつつ少し細かく見てみます。

基本

これまでの WebRTC は RTCPeerConnection というオブジェクトが諸々よしなにやってくれるのという感じでした。ORTC では、その時ブラウザが内部で行っていた個々の操作をオブジェクトにしているイメージです。

たとえば DataChannel が欲しければ、これまでは peerconnection.createDataChannel() だったのですが、ORTC では こうなります。

let iceGatherer = new RTCIceGatherer(iceGatherOptions);
let iceTransport = new RTCIceTransport(iceGatherer);
let dtlsTransport = new RTCDtlsTransport(iceTransport);
let sctpTransport = new RTCSctpTransport(dtlsTransport);

let dataChannel = new RTCDataChannel(sctpTransport);

コードが増えてるだけに思えるかもしれませんが、実は WebRTC のスタックを理解できていると、レイヤリングされているプロトコルがオブジェクトとして抽象化されているだけだと言うことが良くわかるかと思います。

次に Video のトラックを一つだけ交換してみます。 DTLS までは作ったとして、そこから Sender/Receiver の生成は以下のような感じになります。

// DTLS Transport と MediaStreamTrack から Sender を生成
let rtpSender = new RTCRtpSender(mediaStreamTrack, dtlsTransport);

// DTLS Transport から Receiver を生成
let rtpReceiver = new RTCRtpReceiver(dtlsTransport);

// Capabilities を取得
let sendCapas = RTCRtpSender.getCapabilities('video');
let recvCapas = RTCRtpReceiver.getCapabilities('video');

// シグナリングで受信
signaller.on('capability', (remote) => {
  // Capability を変換
  let sendParameters = cap2param(sendCaps, remote.sendCapas);
  let recvParameters = cap2param(recvCaps, remote.recvCapas);

  // パラメータの設定
  rtpSender.send(sendParameters);
  rtpReceiver.receive(recvParameters);

  // これで sender/receiver がメディアを扱える
});

// シグナリングで送る
signaller.send("capability", {
  sendCapas,
  recvCapas,
});

この RTPSender/Receiver の存在も、初期の WebRTC では意識されていませんでした。 この API が今後どうなるかは後述するとして、レイヤが低いという意味はなんとなく感じて頂けたかと思います。

ここでは取得した capability を送信していますが、ここには WebRTC 1.0 で文字列化されていた SDP の情報の一部が含まれています。

dictionary RTCRtpCapabilities {
 sequence<RTCRtpCodecCapability> codecs;
 sequence<RTCRtpHeaderExtension> headerExtensions;
 sequence<DOMString>             fecMechanisms;
};

ここに track から取得した情報などを付加すると、 SDP 文字列を生成することができます。 SDP という文字列として表現されたものを、本来保有してるものをオブジェクトとして取得し、そこから情報を取得する低レベルな API が定義されたため、こうしたことが可能になった訳です。

実際にこれらのコードから SDP を生成する例が adapter.js のブランチに入ってたので、参考にしてみてください。

https://github.com/fippo/adapter/blob/41952910b199816de5a7323f866809560aa5088d/adapter.js#L1283

もちろん、面倒であればこれをラップしたライブラリを作れば良いでしょうし、それは従来の PeerConnection と同等になるでしょう。

MultiTrack(MultiStream) 対応

ORTC では、 track 単位で RTPSender/Receiver を作り、トランスポートに紐づけることができます。 そして、その RTPSender/Receiver 単位で capability を取得してシグナリングを行います。 トランスポートに複数の track を紐づけることが可能なため、その track を適宜 stream にまとめれば multistream 相当になります。

要するに先の例で、 track に合わせて Sender/Receive を増やしていけば良いという意味です。

// track を DTLS transport に紐づけて Sender/Receiver を作成
let audioSender = new RTCRtpSender(audioTrack, dtlsTransport);
let videoSender = new RTCRtpSender(videoTrack, dtlsTransport);
let audioReceiver = new RTCRtpReceiver(transport);
let videoReceiver = new RTCRtpReceiver(transport);

// capability を取得
let recvAudioCaps = RTCRtpReceiver.getCapabilities('audio');
let recvVideoCaps = RTCRtpReceiver.getCapabilities('video');
let sendAudioCaps = RTCRtpSender.getCapabilities('audio');
let sendVideoCaps = RTCRtpSender.getCapabilities('video');

// いわゆる createOffer でやっていたこと
signaller.send('capability', {
  recvAudioCaps,
  recvVideoCaps,
  sendAudioCaps,
  sendVideoCaps,
});

SVC Simulcast

ORTC では、ブラウザに SVC 対応のコーデックを載せ、そのレイヤパラメータを JS から細かく指定できるようにする方向で API の議論が進んでいます。

具体的には以下のような形で指定します。

// 2-layer spatial simulcast combined with 2-layer temporal scalability
// Solid arrows represent temporal prediction.
// 矢印がテンポラルな予測を表す
//
// I0:  base-layer I-frame
// P0:  base-layer P-frames
// EI0: enhanced resolution base-layer I-frame
// EP0: P-frames within the enhanced resolution base layer.
// P1:  first temporal enhancement layer,
// EP1: temporal enhancement to the enhanced resolution simulcast base-layer.
//
//          +----+                +----+
//          |EP1 |                |EP1 |
//          |    |                |    |
//          ++---+                ++---+
//           ^ +----+              ^ +----+
//           | |P1  |              | |P1  |
//           | |    |              | |    |
//           | ++---+              | ++---+
// +----+    |  ^        +----+    |  ^        +----+
// |EI0 +----+  |        |EP0 +----+  |        |EP0 |
// |    +--------------->+    +--------------->+    |
// +----+       |        +----+       |        +----+
//              |                     |
// +----+       |        +----+       |        +----+
// |I0  +-------+        |P0  +-------+        |P0  |
// |    +--------------->+    +--------------->+    |
// +----+                +----+                +----+
//
var encodings = [
  {
    // 低解像度ベースレイヤ(1/2 フレームレート 1/2 解像度)
    encodingId: "0",
    resolutionScale: 2.0,
    framerateScale: 2.0
  },
  {
    // 拡張解像度ベースレイヤ(1/2 フレームレート 1/1 解像度)
    encodingId: "E0",
    resolutionScale: 1.0,
    framerateScale: 2.0
  },
  {
    // 低解像度ベースへの拡張レイヤ (1/1 レート, 1/2 解像度)
    encodingId: "1",
    dependencyEncodingIds: ["0"],
    resolutionScale: 2.0,
    framerateScale: 1.0
  },
  {
    // 拡張解像度ベースへの拡張レイヤ (1/1 レート, 1/1 解像度)
    encodingId: "E1",
    dependencyEncodingIds: ["E0"],
    resolutionScale: 1.0,
    framerateScale: 1.0
  }
];

ざっくり言えば、個々のレイヤのパラメータと、そのレイヤの依存関係の指定になっています。 一般の開発者にはなかなか敷居が高いかもしれません、逆にコーデックやネットワークの状態の把握にノウハウがあると、その知見をここのパラメータに反映することができます。

ブラウザの場合は、コーデックがブラウザに載っているものだけに限定されているため、弄れる余地が少なかったわけですが、ORTC ではそうしたレベルにも干渉できるくらい低レベルな API が来る可能性があります。

ORTC の今と WebRTC の今後

ORTC と Edge

ORTC のドキュメントは正直言って Example がちょっと分かりにくすぎる(というか JS レベル、プログラミングレベルで間違ってる)ので PR しまくってる訳ですが、エディタがよくわからないフローで更新しているようで何とも言えません。。 (しかし、各 API や挙動の定義は割とキチンと書けているので、単にエディタがプログラミング苦手なだけだかなと思います。)

しかし、そんなドラフトでも実装は進んでおり、ついに先日 MS Edge に API の一部が実装されたと発表されました。 moder.ie のバーチャルマシンにはまだ更新されてないので、 Win10 を持って無い筆者はまだ試せていませんが、もし持っていたら是非試してみてください。

http://blogs.windows.com/msedgedev/2015/09/18/ortc-api-is-now-available-in-microsoft-edge/

ちなみに H.264/SVC についても、 H.264/UC として AVC/SVC 両方を包括した仕様として実装するそうなので、 ORTC レベルの APISVC を使わなければ AVC として使えるようになるのではないかと見ています。

WebRTC 1.0 の RC に向けて

今年は TPAC と IETF がどちらも日本で連続して開催されます。 TPAC は年に一度しかなく、そこで重要な問題にのみ時間を裂き、その結果を IETF にも持っていけるようにするため、 事前ミーティングとして WebRTC についての F2F(face to face meeting) が先日シアトルで開催されました。

minutes は以下です。

https://lists.w3.org/Archives/Public/public-webrtc/2015Sep/0075.html

また、そのすぐ後に kranky geek というカンファレンスで WebRTC がテーマになり、主要なエンジニアが発表をしています。 ORTC や Simulcast についての発表もありましたが、今回のエントリに関わる発表としては Google チームの発表が参考になります。

http://www.slideshare.net/webrtclive/kranky-geek-google-team

この辺の話を総合すると、今後の流れとして抑えておくべき点として以下が上げられると思います。

  • WebRTC 1.0 を 2015 年末に RC(勧告候補) として公開したい。
  • その後 ORTC の成果を汲んで WebRTC NV (next version) として次の仕様に取り組みたい。
  • ただし サイマルキャスト などについては WebRTC 1.0 にも入れたい。

WebRTC 1.0 を本当に年内に RC まで持って行くためには、かなり急いで準備(特にブラウザの実装)をすすめ、 TPAC/IETF である程度実現してから、残りの策定作業をする必要があります。 そんなスケジュール感でも、サイマルキャスト周りを 1.0 に入れてしまいたいというくらい、これらの API への要望が強く対応が急務になっていると見ることができそうです。

まとめ

ORTC はかなり複雑な API に見えますが、そもそも WebRTC でやろうとしていることが複雑なので、本気でやるとしたらこのくらいフックポイントがあっても良いだろうと思います。 全ての開発者がまんべんなく使いこなす必要は無いため、他の低レベル API (WebComponents 系、 ServiceWorker 系 etc) と同様、フレームワークなどが成熟することで、開発者が必要に応じて選ぶかたちになっていくでしょう。

そこでさらにサイマルキャストのような、さらに複雑なものが入ってくるのは、今 WebRTC に求められている重要な機能だということだと感じるし、じっさい標準化の場面でも非常に話題になっています。

前述の通り TPAC/IETF を経て、 WebRTC 1.0 が RC に向かうとすれば、今年の年末は WebRTC に取って一つの重要な転換期になるでしょう。 Edge に続いて、 Chrome/Firefox などの実装も進むことが予想されます。 そして、新たに策定が始まるかもしれない WebRTC NV は、当初 WebRTC 2.0 と呼ばれていたものに繋がって行くのではないかと思います。 すると、 ORTC 並みの低レベル API が揃い、それらを適切に使いこなせれば WebRTC を使ったサービスの品質向上につながるでしょう。

そして個人的には「想定と全く違う使い方」例えば、 ORTC で降りて来た低レベル API の一部を使い、これまで前提となっていた「テレビ会議」というユースケースに縛られない、全く予想外な使い方による何かが出てくると、面白いのではないかと思います。

低レベルで非常に複雑だけどスタックが分かっていると割と自然で、さらにもっと広い範囲でサイマルキャストもカバーし、ユースケースと外れた使い方もでき可能性があるかもしれない、そういう可能性が ORTC にはあるなぁという話でした。

総合格闘技を戦い抜くために

片手間じゃない WebRTC をちゃんとやろうとすると

  • RTP でのリアルタイムネットワークプロトコル
  • DTLS での暗号通信
  • ICE での Hole Punching
  • H.264-5/VP8-9 周りのコーデック
  • SVC でのサイマルキャスト
  • イベント/Promise を扱う片手間じゃない JS
  • IETF/TPAC 両方の標準化追従
  • クロスブラウザAPI の補完
  • TURN/STUN/MCU/SFU などのインフラ構築力

くらいの知識が平気で必要になります。

この総合格闘技を一人で戦い抜くのもいいけれど、正直それができる人を俺は知りません。

自分は Web はエコシステムで作るものだと思うので、全部一人で抱えるよりは、得意な人が得意な分野を開拓して、補いながら作り上げていく方が全体としては成長するんじゃないかと思います。 そのサイクルが回らないと WebRTC 自体が、己の自重で沈んでしまうでしょう。

こんなエントリでも、そのサイクルを回す一助となればと思います。

参考