MediaRecorder APIで遊んだ話

これは株式会社POL テックカレンダーの20日目の記事です。前回の記事はこちらです。

こんにちは!株式会社POLでエンジニアをしている牛木と申します。今回のテックカレンダーでは、9日目の記事も書いています。

前置き

実は今回会社のテックカレンダーに参加するにあたって、最初はこの20日目にのみエントリーしていたため、9日目の記事のネタは本来は今回用でした。つまり、完全にネタ切れです。

で、ほんとどうするよと頭を抱えて考えた結果、そういえば入社前にMediaRecorder APIで遊んでたなーということを思い出したので、それについて書いてみます。

MediaRecorder APIとは

ざっくり言うとブラウザでメディアデータ(AudioとかVideoとか)をよしなに使えるようにさせてくれるAPIです。MDNの記事からの引用は次のとおりです。

MediaStream Recording API は、単に Media Recording API または MediaRecorder API と呼ばれることもありますが、Media Capture and Streams API および WebRTC API と密接に関係しています。 MediaStream Recording API を使用すると、MediaStream オブジェクトまたは HTMLMediaElement オブジェクトによって生成されたデータを分析、処理、またはディスクへの保存のためにキャプチャすることができます。 また、驚くほど簡単に作業できます。

https://developer.mozilla.org/ja/docs/Web/API/MediaStream_Recording_API

デバイスから取得したメディアデータを加工したり、WebRTCを利用して配信したりできるので、ブラウザの標準機能だけでここまでのことができるようになったかと胸が熱くなるものがあります。

これを利用して、簡単な録音アプリケーションを作ってみました。

録音アプリケーション

リポジトリはこちらです。

機能としてはMediaRecorder APIで録音し、IndexedDBにデータ保存して永続化・ファイルとしてダウンロードできるところまでやっています。

動いている様子はこちら。

録音アプリケーション

実装上のポイント

MediaRecorder APIを使う部分

基本的にMDNの資料のMediaRecorderのAPIにあるサンプルを参考にして、流れに沿って実装します。

はじめに navigator.mediaDevices.getUserMedia を利用してユーザーが利用しているデバイスの音声デバイスにアクセスするための許可を取得します(ブラウザからアクセス許可を聞かれるダイアログが表示されるやつです)。

navigator.mediaDevices.getUserMedia({audio: true})

実行結果はPromiseでMediaStreamとして取得できます。こちらをMediaRecorderのコンストラクタに渡してstartメソッドで取得を開始します。音声データは取得を開始するとイベントで渡されてきますので、そちらを溜め込んで使います。取得をやめるときはstopメソッドを実行します。stopを実行するとイベントが発生するので、終了処理をその中で行います。

今回のソースコード上はAudioRecorderInterfaceをアプリケーション用に定義して、それを実装したAudioRecorderクラスを作り、内部でMediaRecorderやそれに関連するAPIを使用しています。

録音を開始するときのコードは次のとおりです。

  start(): Promise<void> {
    this.chunks = [];
    return navigator.mediaDevices
      .getUserMedia({ audio: true })
      .then((stream) => {
        const audioCtx = new AudioContext();
        const source = audioCtx.createMediaStreamSource(stream);
        this.analyser = audioCtx.createAnalyser();
        this.analyser.fftSize = 2048;
        source.connect(this.analyser);

        this.mediaRecorder = new MediaRecorder(stream);
        this.mediaRecorder.ondataavailable = (e) => {
          this.chunks.push(e.data);
        };
        this.mediaRecorder.start();
      });
  }

navigator.mediaDevices.getUserMediaを利用してAudioインターフェイスからストリームデータを取得し、MediaRecorderのインスタンスに渡してdataavailableイベントが発生するごとにBlobデータをchunksにためます。

今回のコードではストリームをView上にて波形で可視化するために同時にAudioContextに食わせてAnalyserを作ることもしていますが、AudioContext周りについては今回は割愛します。

録音を停止するときのコードは次のとおりです。

  stop(): Promise<Blob> {
    return new Promise((resolve, reject) => {
      if (this.mediaRecorder == null) {
        throw new Error('Recorder does not started');
      }
      this.mediaRecorder.onstop = () => {
        try {
          const blob = new Blob(this.chunks, {
            type: 'audio/ogg; codecs=opus',
          });
          resolve(blob);
        } catch (err) {
          reject(err);
        }
      };
      this.mediaRecorder.stop();
    });
  }

MediarRecorderを停止して発生するstopイベント内で終了処理として、ためたchunksをBlobにまとめています。

データの保存部分

保存データはBlobのままIndexedDBに入れています。このBlobな音声データをダウンロードさせる場合は次のようにURL.createObjectURLでURLを作り、リンクのクリックを起こして保存させる方法を取っています。

          <IconButton
            aria-label="download"
            onClick={async () => {
              const r = await repository.fetchAudioRecord(id);
              const a = document.createElement('a');
              a.download = `${title}.ogg`;
              const url = URL.createObjectURL(r.data);
              a.href = url;
              a.click();
            }}
          >

ちなみに当時、MP3でダウンロードできるようにしたいなと思い、調べている途中で力尽きたようです。

まとめ

駆け足ですがMediaRecorder APIとそれを使った録音アプリケーションの実装について書いてみました。昔はブラウザだけではできなかったことがブラウザだけでできるようになるのは楽しいものです。

次回は同じチームで大変お世話になっているのやさんです。


この記事が気に入ったらサポートをしてみませんか?