MediaRecorder を使ってブラウザの(ほぼ)全てを記録しよう

Record almost everything in the browser with MediaRecorder の抄訳です。

MediaRecorder API は、動画や音声のようなメディアストリームを保存するための API です。保存した結果は、例えば ogg ファイルに出力するといった形で、楽しむことが可能です。

ブラウザでは、ストリームを様々な形で取得できます。よく見る例には次のように MediaDevices インタフェースを利用して Web カメラからのストリーム取得があります。

navigator.mediaDevices.getUserMedia({
  audio: true
}).then(function (stream) {
  // do something with the stream
}

ストリームを取得したら、次に MediaRecorder を作成します。

var recorder = new MediaRecorder(stream);

このインスタンスは、他の JavaScript オブジェクトと同様に操作できます。操作するためのメソッドも持っていますし、イベントハンドラを登録することも可能です。メソッドの中で最も重要なものは startstop で、イベントの中では dataavailable が重要です。このイベントは保存したストリームのエンコードが終わり、データが利用可能になった際に送出されます。

以上の点を踏まえて記述した、録音用のコードは以下のようになります:

recorder.addEventListener('dataavailable', function(e) {
// e.data contains the audio data! let's associate it to an

// start recording here...
recorder.start();

// and eventually call this to stop the recording, perhaps on the press of a button
recorder.stop();

こちらでライブデモをご覧になれます。

かなり短いコードではありますが、プラグインを必要しないブラウザ内で音声エンコードを実現しています。

追加された新機能を使ってみよう

基本を押さえたところで、追加された新機能、ブラウザでの録画についてみることにしましょう。

動画はとても複雑なものですが、MediaRecorder は簡単に利用できる API を提供しています。前掲のコードを変更して、録画できるようにしていきましょう。

最初にストリームの取得部分のコードを変更します。次のように音声だけでなく、動画も取得するように設定します。

navigator.mediaDevices.getUserMedia({
  audio: true,
  video: true // <-- new!
})

再生時に利用する要素を audio から video に変更した点以外、その他の部分は本質的に変更がありません。こちらで動作例を確認いただけます

すごくないですか?外部のライブラリをロードする必要も、C / C++ で記述されたソースコードを Emscripten のようなツールを使って asm.js に変換する必要も、PNaCl のような移植性の低い方法を取る必要もありません。たった 62 行の JavaScript を書くだけで、外部に依存することなくブラウザの機能だけを使ったプログラムを実現できるんです。大事なことなのでもう 1 度言います。たった 62 行ですよ!しかもコメントをいれて!

このプログラムはプラグインなどを使う方法と比べて、帯域も CPU の電力消費も抑えられます。なぜならビデオエンコーディングはネイティブコードが担った方が効率的だからです。また他のプラットフォームにコードをそのまま持っていくことも可能です。やったね!

さらなる改良

MediaRecorder は、ストリームの種類やそのソースを区別せず扱います。この特徴のおかげで、MediaRecorder に渡す前に Web AudioWebGL などを利用したストリームの操作・変更が可能です。これらは効率的かつ高速なデータ変換に向いているため、よく MediaRecorder と組み合わせて使われます。

メディア保存のための新しい API 群(この中に Media Recorder API も含まれます)には、canvas 要素や audio 要素、video 要素の拡張が定められています。これによりこれらの要素からもストリームを取得できるようになります。また新規ストリームの作成や、トラックの追加 / 取得 / 削除といったストリーム操作も可能になっています。

ステップ・バイ・ステップで、詳しく見ていきましょう。

DOM 要素からのストリーム取得

canvas に描画されるアニメーションの録画を例に説明してゆくことにしましょう。次のように <canvas>captureStream() メソッドを呼ぶだけで、ストリームを取得できます。

var canvasStream = canvas.captureStream();

次に、上述したように取得したストリームを対象とする MediaRecorder を作成します。

var recorder = new MediaRecorder(canvasStream);

こちらの例では requestAnimationFrame を利用してcanvas にホワイトノイズを描画しています。

「外部入力のない canvas を録画してるだけじゃね?」

という声も聞こえてきそうなので、入力画像を操作し、ストリームにして保存する例を作成しました。

// set the stream as src for a video element
video.src = URL.createObjectURL(stream)

// periodically draw the video into a canvas
ctx.drawImage(video, 0, 0, width, height);

// get the canvas content as image data
var imageData = ctx.getImageData(0, 0, width, height);

// apply your pixel magic to this bitmap
var data = imageData.data; // data is an array of pixels in RGBA

for (var p = 0; p < data.length; p+=4) { 
  var average = (data[p] + data[p+1 ] + data[p+2]) / 3; data[p] = average >= 128 ? 255 : 0; // red
  data[p+1] = average >= 128 ? 255 : 0; // green
  data[p+2] = average >= 128 ? 255 : 0; // blue
  // note: p+3 is the alpha channel, we are skipping that one
}

この例では動画に対して <canvas> を利用してフィルタを適用しています。この <canvas> に対して captureStream() を呼べばストリームが取得でき、それを録画するのは簡単です。録画を実現している例はこちらでご覧になれます。

<canvas> でのピクセル操作は効率の良い手段とは言えません。WebGL を利用する方が効率の面では優れていますが、設定も複雑で、この例には不向きなため今回は利用を見合わせています。

付記: 仕様では audio 要素や video 要素は captureStream メソッドを持つこととなっています。しかしブラウザによっては未実装の場合があります。

AudioContextからストリームへ、そしてAudioContext へ

適切なノードを利用することにより、ストリームは Web Audio の入力にも、そして出力にもなりえます。つまり AudioContext にストリームを入力として与え、オーディオグラフによって処理をすることできます。また AudioContext から他のストリームへと出力することで、さらなる操作や利用が可能になります。

stream とaudioContext が既に用意されているとすると、次のように MediaStreamAudioSourceNode オブジェクトを作成することで、音声ストリームをオーディオコンテキスト内で利用できるようになります:

var sourceNode = audioContext.createMediaStreamSource(stream);

このノードを直接 audioContext.destination に接続することで、入力の音声を聞くことができます:

sourceNode.connect(audioContext.destination);

またフィルタをかけることもできます。次のようにフィルタに接続し、フィルタを audioContext.destination へ接続することで、元の音声ではなく、フィルタされた結果を聞くことができます:

var filter = audioContext.createBiquadFilter();
filter.connect(audioContext.destination);
sourceNode.connect(filter);

フィルタした方を出力したければ、MediaStreamAudioDestination ノードを作成し、audioContext.destination の代わりにフィルタと接続します。

var streamDestination = audioContext.createMediaStreamDestination();
filter.connect(streamDestination);

streamDestination に接続されているものは全て、ストリームとしてオーディオグラフから出力されます。そのストリームは、streamDestinationstream 属性で参照できます。これを利用して、MediaRecorder のインスタンスを作成し、フィルタされた音声を録音します:

var filteredRecorder = new MediaRecorder(streamDestination.stream);

入力された音声にフィルタを適用し、その結果を録音するデモはこちらでご覧になれます。

結合しよう

ここまで動画と音声の処理方法について個別に解説してきましたが、その 2 つを同時に行うことを求められる場合もあります。

これまでの解説の通り、動画と音声を並行して処理することもできます。この場合、動画は再生されていなくてはならず、処理前の音声を聞くことになってしまいます。音声トラックなしのビデオストリームを取得していたため、上述の例ではこの問題が発生しませんでした。

「それなら、video 要素をミュートにすればよいのでは?」

と思われるかもしれませんが、それでは処理対象であるストリームの音声もミュートされてしまいます。

この問題は、新しく 2 つのストリームを作成することで解決できます。動画像のみを扱うストリームと、音声のみを扱うストリームをそれぞれ作成後、それぞれを並行して処理し、最後に 1 つのストリームにまとめます。

まずは新しいストリームを作りましょう。MediaStream コンストラクタを呼ぶことで、新しいストリームを作成できます。

var videoStream = new MediaStream();

getVideoTracks() を呼ぶことで、video トラックのリストを取得できます。それぞれのトラックを videoStream に追加します:

var videoTracks = inputStream.getVideoTracks();
videoTracks.forEach(function(track) {
    videoStream.addTrack(track);
});

同様に audioStream も作成します:

var audioStream = new MediaStream();
var audioTracks = inputStream.getAudioTracks();
audioTracks.forEach(function(track) {
    audioStream.addTrack(track);
});

これで動画処理用と音声処理用のストリームをそれぞれ作成し、並行して処理できるようになりました。

// Manipulate videoStream into a canvas, as shown above
// [...]
// Then get result from canvas stream into videoOutputStream
var videoOutputStream = videoCanvas.captureStream();

// Manipulate audio with an audio context, as shown above
// [...]
// Then get result from audio destination node into audioOutputStream
var audioOutputStream = streamDestination.stream;

この時点で、videoOutputStream と audioOutputStream、2 つのストリームができています。これからこの 2 つを 1 つのストリームにまとめます。getTracks() メソッドを利用することで、コードをより一般的なものにしています:

var outputStream = new MediaStream();
[audioOutputStream, videoOutputStream].forEach(function(s) {
    s.getTracks().forEach(function(t) {
        outputStream.addTrack(t);
    });
});

いつものように outputStream を MediaRecorder コンストラクタの引数に与えます:

var finalRecorder = new MediaRecorder(outputStream);

Boo でここまで解説したテクニックを全て利用したデモを見られます。このビデオブースデモは、音声と動画の処理、そしてエンコードも含め、完全にクライアントサイドで動作しています。

ブラウザの対応状況

ブラウザの対応状況は、優れているとは言える状況にはありません。しかしすぐに良くなることでしょう。

Firefox Developer Edition 47 以降のデスクトップ版 Firefox は、動画と音声の保存も含め、これまで解説した全てのテクニックに対応しています。

Android 版 Firefox では MediaRecorder を利用できませんが、Firefox 48 で対応される予定です。モバイルでのパフォーマンスは良いとは呼べませんが、今年のできるだけ早いうちにハードウェアエンコーディングを動作させられるように開発が続いています。

Chrome 47 以降、Opera 36 以降では、録画は WebRTC ストリームからのみ行えます。captureStream() による canvas からの動画ストリームや、Web Audio からのストリームには対応していません。また利用するためには、chrome://flags もしくは opera://flags から試験運用版のウェブ プラットフォームの機能を有効にする必要があります。Chrome 49 ではこの機能と、録音は標準で有効になります。Microsoft Edge はある時点で実装が開始されるようです。実はスペック作成者の 1 人は Microsoft で働いています。

Web サイトの機能を追加で向上させるためなら、上記の機能を使っても良いでしょう。

つまらない理由でプログラムが正しく動作しなくなることや、動作しない録音ボタンを表示してしまうことを防ぐために、MediaRecorder に対応していないブラウザでアクセスされた場合と同様に、まずは互換性のチェックを行いましょう。追加の機能は、それが利用可能である場合にのみ示されるべきです。

例えば、次のように window オブジェクトが MediaRecorder 属性を持つかどうかを確認することで、対応状況の確認ができます。

if(window.MediaRecorder !== undefined) {
    // great! show recording UI
}

詳細と関連情報

このポストを読んで MediaRecorder と、それに関連した API に興味を持ち、より深く学びたいと思っていただければ幸いです。

オンラインのシンプルな利用例を見ることで、それぞれのテクニックを個別に学べます。全部入りの大きなデモを見るよりもわかりやすいでしょう。コードはレポジトリごとクローンできます。この例は mediaDevices.getUserMedia を利用しているため、対応していないブラウザのためのポリフィルも用意されています。単純化のためコードにはポリフィルは含まれていないので、利用には注意が必要です。

上でも触れた Boo は、動画と音声に効果を適用し短いビデオクリップとして出力する、完全にブラウザ上で動作するビデオブースです。このソースコードも同様に公開されています。

以前に MediaRecorder について解説した時からの進捗について知りたい方は、2014 年 6 月の Chris Mills によるポストをごらんください。とても大きな大きな進捗がありました。

ステキなものを作られた際には、ぜひご連絡ください!

Soledad Penadés について

Mozilla の Developer Relations チーム所属。Web 上でステキで、リアルタイムに動作するものを作る人を助けるために活動中。irc.mozilla.org の #devrel でコンタクト可能。

Soledad Penadés による他の記事はこちら