mediarecorder と webm で、オレオレ live streaming
TRANSCRIPT
MediaRecorder と WebM でオレオレ Live Streaming
2015.06.05WebRTC Meetup Tokyo #8インフォコム株式会社
がねこまさし@massie_g
1
今日のお話
• getUserMedia• RTCPeerConnection• MediaRecorder
• CODEC
• 「通信」ではなく、「配信」のお話– スケールするリアルタイム配信の仕組みを作り
たい2
Media Recorder API
• getUserMedia() で取得したストリームを記録する API– http://www.w3.org/TR/mediastream-recording/– http://html5experts.jp/mganeko/12475/
• Firefox で実装済
5
使い方var localStream; // getUserMedia() で取得した stream をセットしておくvar recorder = null;var video = document.getElement ById(‘video1’);
function startRecording() { recorder = new MediaRecorder(localStream); recorder.ondataavailable = function(evt) { // 録画が終了したタイミングで呼び出される var videoBlob = new Blob([evt.data], { type: evt.data.type }); video.src = window.URL.createObjectURL(videoBlob); } // 録画開始 recorder.start();} // 録画停止function stopRecording() { recorder.stop();}
6
使い方var localStream; // getUserMedia() で取得した stream をセットしておくvar recorder = null;var video = document.getElement ById(‘video1’);
function startRecording() { recorder = new MediaRecorder(localStream); recorder.ondataavailable = function(evt) { // 録画が終了したタイミングで呼び出される var videoBlob = new Blob([evt.data], { type: evt.data.type }); video.src = window.URL.createObjectURL(videoBlob); } // 録画開始 recorder.start();} // 録画停止function stopRecording() { recorder.stop();}
停止しないと、再生できない
(と、思い込んでた)
7
デモ• https://lab.infocom.co.jp/demo/webrtc-recorder.html
• 録画を止める → 再生できる
8
作戦 1MediaRecorder.start()
…MediaRecorder.stop()
MediaRecorder.start()…
MediaRecorder.stop()
MediaRecorder.start()…
MediaRecorder.stop()
MediaRecorder.start()…
MediaRecorder.stop()
Blob 1
Blob 2
Blob 3
Blob 4
video.src = url1;video.play();
video.src = url2;video.play();
video.src = url3;video.play();
video.src = url4;video.play();
…
11
作戦 1 :デモ• https://lab.infocom.co.jp/demo/webrtcfirefox-rec-split.html
12
作戦 2MediaRecorderstart()
…MediaRecorder.stop()
MediaRecorderstart()…
MediaRecorder.stop()
MediaRecorderstart()…
MediaRecorder.stop()
MediaRecorderstart()…
MediaRecorder.stop()
Blob 1
Blob 2
Blob 3
Blob 4
video1(preload)
video2(preload)
14
作戦 2 :デモ• https://lab.infocom.co.jp/demo/webrtcfirefox-rec-dual.html
15
作戦 3 :無理やり、映像配信
Firefox
XHR POST .webmXHR POST .webmXHR POST .webmXHR POST .webm
Firefox Chrome
HTTP GET .webm
HTTP GET .webmHTTP GET .webm
HTTP GET .webmHTTP GET .webm
HTTP GET .webm
HTTP GET .webm
HTTP GET .webm
17
video1
video2video1
video2
作戦 3 の結果• 配信できた!– デモは省略
• ちらつきも、ほとんど分からない– 良く見ると気が付くけど…
• 天の声– 「ダブルバッファーもどき、無理やりすぎ」– 「 MediaRecorder 、もっと良く見ろ」
• MediaRecorder.start(optional long timeslice);
– iOS で何とか見れないの?18
作戦 3 の結果• 配信できた!– デモは省略
• ちらつきも、ほとんど分からない– 良く見ると気が付くけど…
• 天の声– 「ダブルバッファーもどき、無理やりすぎ」– 「 MediaRecorder 、もっと良く見ろ」
• MediaRecorder.start(optional long timeslice);
– iOS で何とか見れないの?19
MediaRecorder.start(interval)var localStream; // getUserMedia() で取得した stream をセットしておくvar recorder = null;var video = document.getElement ById(‘video1’);
function startRecording() { recorder = new MediaRecorder(localStream); recorder.ondataavailable = function(evt) { // インターバルごとに呼び出される var videoBlob = new Blob([evt.data], { type: evt.data.type }); video.src = window.URL.createObjectURL(videoBlob); } // 録画開始 recorder.start( 5000 );} // 録画停止function stopRecording() { recorder.stop();}
停止しなくて呼ばれる
インターバルを指定できた! (ミリ秒)
20
MediaRecorder.start(interval)… しかしvar localStream; // getUserMedia() で取得した stream をセットしておくvar recorder = null;var video = document.getElement ById(‘video1’);
function startRecording() { recorder = new MediaRecorder(localStream); recorder.ondataavailable = function(evt) { // インターバルごとに呼び出される var videoBlob = new Blob([evt.data], { type: evt.data.type }); video.src = window.URL.createObjectURL(videoBlob); } // 録画開始 recorder.start( 5000 );} // 録画停止function stopRecording() { recorder.stop();}
停止しなくて呼ばれる
インターバルを指定できた! (ミリ秒)
2 個目以降は再生できない。 Blob の中身が違
う
21
Blob の中身は?• MediaRecorder で取得した Blob の中身– evt.data.type = "video/webm“– どうやら WebM らしい
• でも、最初の Blob と、 2 番目以降の Blobで違う??
• WebM の中身って、どうなっている??
22
WebM とは• Wikipedia より http://ja.wikipedia.org/wiki/WebM– 米 Google が開発している– オープンでロイヤリティフリーな– 動画コンテナフォーマット
• コーデック– 映像: VP8 / VP9– 音声: Vorbis
• コンテナ– Matroska のサブセット
Matroska• Wikipedia より http://ja.wikipedia.org/wiki/Matroska
– ロシアの入れ子人形マトリョーシカにちなむ– オープンソース( GNU LGPL )で開発中– EBML ( Extensible Binary Meta Language )採用– http://www.matroska.org/technical/specs/index.html– こちらの記事からたどり着きました。ありがとうございます!
• Media Source Extensions を使ってみた (WebM 編 ) @othersight• http://qiita.com/tomoyukilabs/items/57ba8a982ab372611669
• EBML ( Extensible Binary Meta Language )– XML を基に作られた、拡張性に優れたデータ格納方式
• 要素のみ、属性なし、入れ子あり• ※ むしろ YAML と言った方が近いかも
– 対応していない機能 ( タグ ) は無視する– テキストではなくバイナリで表現
EMBL のイメージ<EBML>
<EBMLVersion>1</EBMLVersion><DocType>webm</DocType>
</EBML><Segment> …
<Info> … </Info><Tracks> … </Tracks><Cluster> … </Cluster><Cluster> … </Cluster>…
</Segment>
これがバイナリで格納されている
WebM のおおまかな構造
ヘッダー 部分
映像 / 音声 部分
おまけ 部分
Header
Meta Seek Info
Segment InfoTracks
(Chapters)
ClusterClusterCluster
Cluster
(Cue Data)(Attachment)
(Tagging)
Matroska 的
EBML
Segment
WebRTC 的
タグ /Element のバイナリ表現
• 3 つのパートでタグ /Element は構成される
ID DataSize Data
1 ~ 4 バイト 1 ~ 8 バイト 0 ~ 0x00FFFFFFFFFFFFFE バイト ペタバイト級まで
可変長 可変長
可変長
実際のデータだけでなく、 ID も DataSize も可変長厄介なヤツ。マジ勘弁…
ID 部のバイナリ表現1 バイト目 (2 進 )
1xxx-xxxx
01xx-xxxx xxxx-xxxx
001x-xxxx xxxx-xxxx xxxx-xxxx
0001-xxxx xxxx-xxxx xxxx-xxxx xxxx-xxxx
0x80 ~ 0xFF
0x40 ~ 0x7F
1 バイト目 (16 進 )
0x20 ~ 0x3F
0x10 ~ 0x1F
※ 先頭ビットも、 ID の値に含める
DataSize 部のバイナリ表現
xxxx-xxxxxxxx-xxxx xxxx-xxxxxxxx-xxxx xxxx-xxxx xxxx-xxxxxxxx-xxxx0000-0001
xxxx-xxxxxxxx-xxxx xxxx-xxxxxxxx-xxxx xxxx-xxxx xxxx-xxxx0000-001x
xxxx-xxxxxxxx-xxxx xxxx-xxxxxxxx-xxxx xxxx-xxxx0000-01xx
xxxx-xxxxxxxx-xxxx xxxx-xxxxxxxx-xxxx0000-1xxx
xxxx-xxxxxxxx-xxxx xxxx-xxxx0001-xxxx
xxxx-xxxxxxxx-xxxx001x-xxxx
xxxx-xxxx01xx-xxxx
1xxx-xxxx
1 バイト目 (2 進 )
7bits
14bits
21bits
28bits
35bits
42bits
49bits
56bits
値は Big Endian の Unsigned Integer※ 先頭ビットは、 DataSize の値に含めない※ すべてのビットが 1 の値は予約済(多分)
Data 部分• DataSize で指定されたバイト数• タグ (ID) の種類によって、さまざま型– 整数: unsigned int, signed int (Big Endian)– 文字列 : ASCII string, UTF-8 string– 実数: float (Big Endian)– 日付 : date (Big Endian)– バイナリ : binary– 他の複数のタグ: master
• タグの入れ子の構造• 型を識別するルールは無い …みたい– タグの値と型を対応付ける辞書を持つしかなさそう– http://www.matroska.org/technical/specs/index.html
44
http://www.matroska.org/technical/specs/index.html より
ID 型
解析例 (1) EBMLVersion
• 16 進表記: 42 86 81 01• ID の最初のバイトの 2 進表記: 0100 0010– → ID は 2 バイトの [42][86]– 一覧表から、 EBMLVersion と判明
• DataSize の最初のバイトの 2 進表記: 1000 0001– → DataSize は 1 バイト– サイズは 0000 0001 = 1 バイト
• Data は 1 バイト。先ほどの一覧表から、型は Unsigend Int– → 値は 1
• 結果: EBMLVersion = 1
解析例 (2) DocType
• 16 進表記: 42 86 84 77 65 62 6D• ID の最初のバイトの 2 進表記: 0100 0010– → ID は 2 バイトの [42][84]– 一覧表から、 DocType と判明
• DataSize の最初のバイトの 2 進表記: 1000 1000– → DataSize は 1 バイト。サイズは 0000 1000 = 4 バイ
ト• Data は 4 バイト。先ほどの一覧表から、型は ASCII string– 77 65 62 6D → 値は "webm"
• 結果: DocType = "webm"
WebM のパース• 先頭から 1 バイトづつパースしていけば、タグの内容を解析可能
• とくに興味が無いタグや、理解できないタグが出現した場合– データー長はルールに従って算出可能 → スキップすることが可
能
• ※ もちろんライブラリもあります– libebml http://dl.matroska.org/downloads/libebml/– libmatroska http://dl.matroska.org/downloads/libmatroska/– yamka https://sourceforge.net/projects/yamka/※ 使ってはいません。詳細不明
• Node.js のサンプルコードを書いてみました– https://gist.github.com/mganeko/9ceee931ac5dde298e81– メモリ上に一括して読み込む、しょぼい実装ですが…
MediaRecorder の Blob と WebM の対応ヘッダー部
Cluster(映像 / 音声データ)
Cluster(映像 / 音声データ)
Cluster(映像 / 音声データ)
Cluster(映像 / 音声データ)
Cluster(映像 / 音声データ)
Blob 1
Blob 2
Blob 349
HeaderMeta Seek Info
Segment InfoTracks
ブラウザ側で何とかなる?• 次々と出現する Blob– 最初にはそろっていない
• 動的に連結しながらwindow.URL.createObjectURL() で取り出す
→ できない (多分)
50
ブラウザ側で何とかなる?• 次々と出現する Blob– 最初にはそろっていない
• 動的に連結しながらwindow.URL.createObjectURL() で取り出す
→ できない (多分)
なら、サーバーで無理やりの出番じゃない?
51
作戦 4 :やりたいこと
53
Firefox
XHR POST Blob1XHR POST Blob2XHR POST Blob3XHR POST Blob4
Firefox Chrome
HTTP GET
Blob 1 Blob 2 Blob 4Blob 3
Blob 1 Blob 2 Blob 4Blob 3
HTTP GET
サーバ側で順次連結し、1 つのレスポンスで返すサーバー側で順次連結し、1 つのレスポンスで返す
作戦 4 :やりたいこと
54
Firefox
XHR POST Blob1XHR POST Blob2XHR POST Blob3XHR POST Blob4
Firefox Chrome
HTTP GET
Blob 1 Blob 2 Blob 4Blob 3
Blob 1 Blob 2 Blob 4Blob 3
HTTP GET
サーバ側で順次連結し、1 つのレスポンスで返すサーバー側で順次連結し、1 つのレスポンスで返す
“WebM Live Streaming : WMLS”
Node.js
サーバー側の実装• Node.js + express で実装• Stream を理解していない … 苦戦
– 参考: Node.js の Stream API で「データの流れ」を扱う方法 @Jxck_• http://jxck.hatenablog.com/entry/20111204/1322966453
• 複数のファイルを連結して一つの Stream (http.ServerResponse) で返す– 最初 combined-stream を利用
• https://github.com/felixge/node-combined-stream– 後に自前の仕組みに変更
• 最初からすべてのファイルが揃っていない– 後から到着するファイルを待って、順々に連結する– 適切なやり方が分からない
• 仕方なく、 setTimeout() によるポーリングで実現
• GitHub で公開中– https://github.com/mganeko/wmls
• 詳細は別途、 Node.js系の場で?? 55
作戦 5 :「中継を中継する」 Part3
Firefox
XHR POST BLOB1
Firefox
Chrome
HTTP GET .webm
Part1: WebRTC Meetup Tokyo #2 http://www.slideshare.net/mganeko/meetup2-lt-audioPart2: WebRTC Meetup Tokyo #4 http://www.slideshare.net/mganeko/webrtc-meetup4-lt
HTTP POST
HTTP POST
Firefox Chrome
Firefox
Chrome
58
XHR POST BLOB2XHR POST BLOB3
HTTP POST
Node.js
Node.js
Node.js
Node.js
作戦 5 の結果• できた! 音も鳴った! (デモは省略)
– 同一マシン上の疑似環境だけど、原理的には OK– 当然遅延はあるけど、片方向なら許容できそう
• 良いところ(作戦 4, 5含めて)– サーバー側で再エンコード不要
• 悪いところ(作戦 4, 5含めて)– ファイルサイズが大きい
• 1.5MB / 5sec → 2.4Mbit / sec , 18MB / min → 1GB / hour
– Chrome / Firefox でのみ、再生可能– iOS はもう一歩
• Safari では NG• VLC for iOS で、 Video のみ再生可能。 Audio鳴らず…
• 天の声:「それ、 HLS で良くない?」 59
HLS: HTTP Live Streaming• HLS: HTTP Live Streaming
– Apple が開発した、動画配信の方式– iOS, OS X Safari で動画をストリーム再生– PC/Mac の Chrome, Firefox, IE は ×– Android は再生できるはず→試した範囲では、なぜか ×
• プレイリスト . m3u8• 複数の .ts ファイル (MPEG Transport Stream)
– H264 + AAC/MP3– ※ffmpeg で VP8 から変換できる
• H264 を有効にするには、自分でビルドする必要あり– 有償サービス /製品で利用するには、 H264 のライセンス料必要
• 実時間以内で終わらすには、そこそこ高スペックが必要• VP8→H264 変換、 TS 分割の 2 ステップが必要( 1 ステップでは失敗) 60
#EXTM3U#EXT-X-PLAYLIST-TYPE:VOD#EXT-X-TARGETDURATION:10#EXT-X-VERSION:3#EXT-X-MEDIA-SEQUENCE:0#EXTINF:10.0,http://example.com/movie1/sequence1.ts#EXTINF:10.0,http://example.com/movie1/sequence2.ts#EXTINF:10.0,http://example.com/movie1/sequence3.ts#EXTINF:9.0,http://example.com/movie1/sequence4.ts
#EXT-X-ENDLIST
sample.m3u8
sequence1.ts
sequence2.ts
sequence3.ts
sequence4.ts
H264 + AAC/MP3
終了の合図記述が無ければ、クライアントが繰り返し読みに
来る
プレイリストと TS ファイル
61
HLS vs. WMLS• HLS
– ○: 仕組みがシンプル• ただのファイルの集合体なので、 CDN などに置ける• キャッシュ ( ブラウザ、 Proxy) 、アンチウィルスとも相性が良い
– ○: クライアントが賢い。サーバー負荷低い– ○: 通信切断時に、再開しやすい
• クライアント側がどこから再開すれば良いか分かっている– △:再生環境が限られる (iOS, Mac OSX)– × :サーバー側で、再エンコードが必要なケースが多い
• WMLS– × :仕組みが面倒
• サーバーのロジックが必要• キャッシュ ( ブラウザ、 Proxy) 、アンチウィルスと相性が良くない
– × :クライアントは単純。サーバーが頑張る– × :通信切断時に、どこから再開すれば良いか分からない(?)– △:再生環境が限られる (Chrome, Firefox)– ○:サーバ側で、再エンコードが不要 62
キャッシュ問題
63
ブラウザ
.ts
.ts
.ts
ブラウザ
.webm
…
HLS
WMLS
http://server.com/channelA/0001.ts
http://server.com/channelA/0002.ts
http://server.com/channelA/1001.ts
http://server.com/channelA
.webm
…
http://server.com/channelA
必ずパーマネントリンクがあるので、
キャッシュが邪魔になることはない
不用意に URL を使いまわすとキャッシュが利いて、過去の映像が再生され
る
.ts http://server.com/channelA/1002.ts
アンチウィルス問題
64
アンチウィルスサービス ブラウザ
.ts
.ts
.ts
.ts
.ts
.ts
アンチウィルスサービス ブラウザ
.webm
…
…
…ウイルススキャンが終わらない…
(HTTPS なら除外されて OK)
HLS
WMLS
Media Source Extensions
• http://w3c.github.io/media-source/– JavaScript でメディア再生をコントロールする– 結構、低レベルな印象
• 参考: Media Source Extensions を使ってみた (WebM編 )– http://qiita.com/tomoyukilabs/items/57ba8a982ab372611669
• これを使えば、 1 つの video タグで、連続して再生できるのでは?– なんちゃってダブルバッファー不要– サーバーも頑張らなくて良い
67
作戦 6 :やりたいこと
68
[Headers]Header
InfoTracks
Blob 1
Firefox
XHR POST Blob1
XHR POST Blob2
XHR POST Blob3[Blob1’]ClusterCluster
Blob 2
[Blob2]ClusterCluster
Blob 3
[Blob3]ClusterCluster
作戦 6 :やりたいこと
69
[Headers]Header
InfoTracks
Blob 1
Firefox
XHR POST Blob1
XHR POST Blob2
XHR POST Blob3[Blob1’]ClusterCluster
Blob 2
[Blob2]ClusterCluster
Blob 3
[Blob3]ClusterCluster
Chrome
XHR GET Headers→ 初期化セグメントとして利用
XHR GET Blob2→ メディアセグメントとして利用
XHR GET Blob1’→ メディアセグメントとして利用
作戦 6 :再生側実装イメージ
70
var video = document.getElementById('video');var ms = new MediaSource();var sb = ms.addSourceBuffer('video/webm; codecs="vp8,vorbis" '); sb.addEventListener('updateend', appendMediaSegment, false);sb.appendBuffer(header_part); // <-- XHR で取得しておくvideo.src = URL.createObjectURL(ms);
function appendMediaSegment() { var segment = sliceNextCluster(current_media_blob); // <-- XHR で取得しておく if (segment) { sb.appendBuffer(segment); } else { requstNextMediaBlob(); // <-- XHR で取得をリクエストする setTimeout(appendMediaSegment, 1000); // <-- XHR で取得したころに再実行 }}
赤: MSE青:自分で
作戦 6 の結果
• Got a block with a timecode before the previous block• どうやら「 Audio Track のブロックの timecode が
変」と言われている• Cluster の内部構造を調べる必要あり
72
Cluster の内部
73
Cluster
SimpleBlock• Track Number 1:Video• Timecode some value
Timecode
SimpleBlock• Track Number 2:Audio• Timecode 0
Media Source Extensions は、ここに厳密※video で普通に再生する分には OK
作戦 6 :再生側実装イメージ
74
var video = document.getElementById('video');var ms = new MediaSource();var sb = ms.addSourceBuffer('video/webm; codecs="vp8" '); // NO Audiosb.addEventListener('updateend', appendMediaSegment, false);sb.appendBuffer(header_part); // <-- XHR で取得しておくvideo.src = URL.createObjectURL(ms);
function appendMediaSegment() { var segment = sliceNextCluster(current_media_blob); // <-- XHR で取得しておく if (segment) { sb.appendBuffer(segment); } else { requstNextMediaBlob(); // <-- XHR で取得をリクエストする setTimeout(appendMediaSegment, 1000); // <-- XHR で取得したころに再実行 }}
※getUserMedia() 側もAudio 無しで
まとめ• MediaRecorder に期待
– CODEC 指定や、ビットレート指定ができると嬉しい
• Media Source Extensions はまだ夢– Youtube では使われているらしい– 厳密すぎるのでは?
• 自己流で真似して、 HLS のシンプルさ / 優秀さを実感– 対応プラットフォーム / ブラウザが増えると良い
• DRM/著作権保護が入ってくると、がらりと変わりそう– ライブ配信の場合は、認証の仕組みで弾けば良さそう
• MPEG-DASH というのもあるらしい… 76