MediaRecorder APIをつかってCanvas/WebAudioなゲーム画面を録画する

この記事は[『ドワンゴ AdventCalendar 2017』](https://qiita.com/advent-calendar/2017/dwango)5日目の記事です(๑˃̵ᴗ˂̵)و

昨日は @kinoppyd さんの[『Gitをバックエンドにしたタスク管理bot』](http://tolarian-academy.net/task-manage-bot-with-git/)でした。
普段使ってるgitの裏側の世界はとても興味深いですね。
僕は以前から自分用のメモアプリをつくりたいな〜と思っていろいろ考えていたのですが、
データ保存のバックエンドにgitを使うのも面白いかもなぁと思いました！

さて、5日目の今日は**『HTML5なブラウザゲームを録画したい話』**をします。

## どういうわけか：ゲームをつくったら録画したい

![record_shitai.jpg](https://qiita-image-store.s3.amazonaws.com/0/25109/c459be71-d3a4-f86a-2bd4-03c856508343.jpeg)

僕は昔からゲームをつくるのが好きで、個人でいろいろつくったりしています。
ここ数年はHTML5なブラウザゲームをメインでつくっています。
（といっても、その前はFlashゲームだったので、だいぶ前からブラウザなんですけどね）

ゲームが完成し、それをインターネットで公開する際、
見てくれた人にゲームの内容をわかりやすく伝えるたいため、
動画を撮影して[動画サイト](http://www.nicovideo.jp/video_top)とかにアップロードしたいな！と思うことがあります。

僕のPCは mac なので QuickTime などを使えば画面をキャプチャ保存できますが、
録画範囲の選択が難しかったり、間違って僕のハァハァ言ってる声が録音されてたりと、
正直、なかなか億劫です……＞＜；

そんなわけで **MediaRecorder API** を使って、
ブラウザゲームの画面をブラウザに直接録画してもらうことにします！

## MediaRecorder API

- MediaRecorder API - Web API インターフェイス | MDN
    - https://developer.mozilla.org/ja/docs/Web/API/MediaRecorder_API
- MediaRecorder API - Can I use
    - https://caniuse.com/#search=mediarecorder

MediaRecorder は WebRTC の一部で（たぶん）、録画・録音機能を司るAPIです。
ニコニコのサービスでは、プレゼン発表を録音して公開できる[『ニコナレ』](https://niconare.nicovideo.jp) というサービスで使っています。

例えば、以下のようにするとPCのカメラとマイクの映像・音声が録音できます。

```javascript
// getUserMedia でマイクの接続要求をする
// ※そのドメインで初めての場合はユーザに確認ダイアログがでます
navigator.getUserMedia({
    audio: true,
    video: true
}, (stream) => {
    // カメラ・音声のストリームを MediaRecorder に渡す
    const recorder = new MediaRecorder(stream);

    // 定期的に録画データが飛んでくるので貯めます
    let chunks = [];
    recorder.addEventListener('dataavailable', (e) => {
        chunks.push(e.data);
    });

    // 録画終了時の処理
    recorder.addEventListener('stop', () => {
        // 貯めていたデータをまるっとひとかたまりに
        const blob = new Blob(chunks, {type: 'video/webm'});

        // Videoタグにくっつけてあげれば再生できます
        const videoElement = document.querySelector('video');
        videoElement.src = window.URL.createObjectURL(blob);
        videoElement.play();
    });

    // 録画の開始
    // 今回は5秒に1回、データを出力するようにします
    // （＝5秒に1回、dataavailableが呼ばれます）
    recorder.start(5 * 1000);

    // 15秒後に録音終了しておきますね
    setTimeout(() => {
        recorder.stop();
    }, 15 * 1000);
}, (err) => {
    console.error(err);
});
```


## これをもとにゲーム画面と音声を録画してみる

上の例では入力ストリームにPCのカメラ・マイクを使っていますが、
ここのストリームを「ゲーム画面」と「ゲームの音声」に差し替えることができれば
MediaRecorderの力でゲームの録画ができそうです！

### ゲーム画面の取得

![canvas.jpg](https://qiita-image-store.s3.amazonaws.com/0/25109/5c22ed2e-2f02-25e8-b655-426fe0855555.jpeg)

ブラウザゲームだと、画面は通常 `<canvas>` に描いていることが多いかと思います。
canvas には captureStream という描画結果のストリームを生成する機能があるので、
それを使います。

- HTMLCanvasElement.captureStream() - Web API インターフェイス | MDN
    - https://developer.mozilla.org/ja/docs/Web/API/HTMLCanvasElement/captureStream

```javascript
const canvasStream = document.querySelector('canvas').captureStream();
const recorder = new MediaRecorder(canvasStream);
```

かんたんですね！

### ゲーム音声の取得

<img width="259" alt="audio" src="https://qiita-image-store.s3.amazonaws.com/0/25109/3f33e352-69ef-63b4-f20d-e4e9141dcfff.png">

ブラウザゲームの音声出力はいろいろな歴史の積み重ねがありましたが、
IE11亡き現在は WebAudio を使って鳴らすことが多いと思いますので、それに対応したいとおもいます。

…なーんて簡単に言いましたが、実はここはちょっぴり厄介です。
canvasタグはHTMLの中に見えているので外から簡単に探せましたが、
WebAudio の場合は JavaScript 内のどこかで使われている
以下の2つを見つける必要があります。

- AudioContext のインスタンス
- ↑に対して connect している AudioNode

例として CreateJS(SoundJS) が使われているゲームの場合は
`window.createjs.WebAudioPlugin.context` と `window.createjs.WebAudioPlugin.dynamicsCompressorNode` が、
RPGツクールMVだと `window.WebAudio._context` と
`window.WebAudio._masterGainNode` がそれに当たります。

以下のように録音用のストリームをつくることができます。

```javascript
// createJSの場合
const audioContext = window.createjs.WebAudioPlugin.context;
const audioNode = createjs.WebAudioPlugin.dynamicsCompressorNode;

// 出力用のストリームをつくる
const destination = audioContext.createMediaStreamDestination();

// ゲームの音声再生に使っているnodeをつなげる（これで音が奪える！）
audioNode.connect(destination);

// 後述
const oscillator = audioContext.createOscillator();
oscillator.connect(destination);

// このストリームを MediaRecorder に渡したい
const audioStream = destination.stream;
```

途中で `createOscillator` というよくわからない子がでてきました。

- AudioContext.createOscillator() - Web API インターフェイス | MDN
    - https://developer.mozilla.org/ja/docs/Web/API/AudioContext/createOscillator

サイン波や矩形波など、決まった波形を吐き出すストリームをつくる機能です。

なぜ余計な波形を混ぜているのかというと、ゲームには通常「無音になる時間」があるためです。
無音になっている間、ゲーム内で使われている音声ノードはストリームに一切のデータを流しません。
（もちろんゲームの実装にもよるのですが）

一見、特に問題が無いように感じますが、ストリームにデータが流れて来ないということは、
つまり時間が経過しないということになります。
そのため、録音後のデータを再生すると、
無音の部分が縮まって音が連続して再生されてしまったりすることがあります＞＜；

ゲームの映像と音声がズレてしまったら悲しすぎるため、
ここでは createOscillator を使い、0Hz（無音）の波形データを流し込むことで、
データが存在しない時間をつくらないようにしています。

### あれ、ストリームが2つある…！

MediaRecorder の引数は stream 1個なのに、画面と音声で別々のストリームをつくってしまいました＞＜
このままでは別々のデータに記録することになってしまうので、1個のストリームにまとめます。

ストリームの中にはトラックと呼ばれるものが入っているので、それを取り出して1つのストリームの中に混ぜます。

```javascript
const mediaStream = new MediaStream();

// それぞれのストリーム内にあるトラックを取り出して、同じストリームに混ぜる
[canvasStream, audioStream].forEach((stream) => {
  stream.getTracks().forEach((track) => mediaStream.addTrack(track));
});

// やったね！
const mediaRecorder = new MediaRecorder(mediaStream);
```

これでゲームの画面と音声が混ざったストリームが手に入ったので実質完成です！

## 実際にやってみた

<img src="https://qiita-image-store.s3.amazonaws.com/0/25109/2a7ea873-dba3-ebba-ee2f-eb399f0f0beb.jpeg" width="320">

- デモURL
    - https://rutan.github.io/media-recorder-demo/index.html
- コード
    - https://github.com/rutan/media-recorder-demo/blob/master/js/plugins/record.js

（Chromeで動かしてね！）

というわけで、実際にゲーム（？）に組み込んでみました。
一番右まで進む様子が動画として撮影されている様子が確認できるとおもいます。


## まとめ

MediaRecorder API をつかって、ブラウザゲームの画面と音声を記録してみました。
これでゲーム画面を録画するときに難しいソフトを使ったりせずに、
ブラウザだけで録画をすることができます。

今回の動機は「自分でつくったゲームを録画したい」でしたが、
ゲーム実況動画やSNSが全盛期のいま、プレイヤー側でも録画したい欲求は強いと思います。
（最近はPS4やSwtichなどもゲーム機自体が録画シェアをサポートしてますね！）

こういった機能がゲームの中に組み込まれていると、
スーパープレイの共有などもっと幅広く遊べるゲームがつくれるかもしれませんね！


明日は @lambdataro さんです٩(๑❛ᴗ❛๑)۶