Written by TSUYOSHI

【簡単】JavaScriptで動画を録画する機能を実装する方法

JavaScript PROGRAMMING

本記事では、ブラウザからJavaScriptでPCなどのデバイスカメラにアクセスして、動画の録画をする方法について解説します。

この記事を書いている僕は、エンジニア歴4年のフロントエンドエンジニアです。

この記事を読むことによって、ブラウザからPCやスマホのカメラやマイクを使って簡単に動画撮影する機能を実装することができるようになります。

JavaScriptで動画を録画する機能を実装する方法

GitHub

GitHubにコードを上げています。
▼GitHubコード
https://github.com/it-web-life/javascript_record_video_how_to

▼サンプルのdemoページ
https://it-web-life.github.io/javascript_record_video_how_to/index.html
※マイクとカメラを許可する必要があります

今回のコード

今回のコードは以下になります。メインはJavaScriptですが、ベースとなるHTMLも載せています。

▼HTML index.html

<!DOCTYPE html>
<html lang="ja">
<head></head>
<body>
  <div class="container">
    <div class="screen">
      <h2>カメラの映像</h2>
      <video
        class="js-video-record"
        data-video-player=".js-video-player"
        data-record-start=".js-record-start"
        data-record-stop=".js-record-stop"
        data-play-start=".js-play-start"
        data-download=".js-download"
      ></video>
      <div>
        <button class="js-record-start">録画開始</button>
        <button class="js-record-stop">録画停止</button>
        <button class="js-play-start">動画の再生</button>
        <button class="js-download">動画のダウンロード</button>
      </div>
    </div>
    
    <div class="screen">
      <h2>録画した動画の再生</h2>
      <video class="js-video-player"></video>
    </div>
  </div>
</body>
</html>

▼JavaScript videoRecord.js

/**
 * 動画の録画機能を付与する
 */
class VideoRecord {

  /**
   * @constractor
   * @param {Object} params 
   * @param {HTMLElement} params.$target カメラ映像をマウントする要素
   * @param {HTMLElement} params.$videoPlayer 録画した動画をマウントする要素
   * @param {HTMLElement} params.$recordStart 録画開始ボタン
   * @param {HTMLElement} params.$recordStop 録画停止ボタン
   * @param {HTMLElement} params.$playStart 再生ボタン
   * @param {HTMLElement} params.$download ダウンロードボタン
   */
  constructor({ $target, $videoPlayer, $recordStart, $recordStop, $playStart, $download }) {
    // 各要素
    this.$target = $target
    this.$videoPlayer = $videoPlayer
    this.$recordStart = $recordStart
    this.$recordStop = $recordStop
    this.$playStart = $playStart
    this.$download = $download

    this.initialize = this.initialize.bind(this)
    this.startRecording = this.startRecording.bind(this)
    this.startRecording = this.startRecording.bind(this)
    this.stopRecording = this.stopRecording.bind(this)
    this.startPlaying = this.startPlaying.bind(this)
    this.download = this.download.bind(this)

    // 設定の初期化処理
    this.initialize()

    // イベント設定
    this.$recordStart.addEventListener('click', this.startRecording)
    this.$recordStop.addEventListener('click', this.stopRecording)
    this.$playStart.addEventListener('click', this.startPlaying)
    this.$download.addEventListener('click', this.download)
  }

  /**
   * 録画関連の初期化処理
   */
  async initialize() {
    this.mediaStream = null
    this.mediaRecorder = null

    // Blob
    this.recordedChunks = []
    this.superBuffer = null

    this.$videoPlayer.src = null
    this.$videoPlayer.srcObject = null

    // ボタンの表示初期化
    this.$recordStart.disabled = false
    this.$recordStop.disabled = true
    this.$playStart.disabled = true
    this.$download.disabled = true

    // カメラ・音声の取得
    try {
      const mediaDevicesConstraints = {
        audio: true,
        video: { width: 1280, height: 720 }
      }

      // デバイスの動画・音声トラックを取得
      this.mediaStream = await navigator.mediaDevices.getUserMedia(mediaDevicesConstraints)

      // MediaStreamを設定して表示する
      this.$target.srcObject = this.mediaStream
      this.$target.play()
    } catch (err) {
      throw new Error(err)
    }
  }

  /**
   * 録画を開始する
   */
  startRecording() {
    // 録画機能の生成
    this.mediaRecorder = new MediaRecorder(this.mediaStream, { mimeType: 'video/webm; codecs=vp8' });
    // availableイベントでメディア記録を保持
    this.mediaRecorder.ondataavailable = event => this.recordedChunks.push(event.data)
    // 録画開始
    this.mediaRecorder.start()

    console.log('this.superBuffer', this.superBuffer)
    if (this.superBuffer) {
      // メモリ解放
      URL.revokeObjectURL(this.superBuffer)
    }

    // ボタンの表示更新 (動画停止を許可)
    this.$recordStop.disabled = false

    console.log('MediaRecorder start')
  }

  /**
   * 録画を停止する
   */
  stopRecording() {
    // 録画停止
    this.mediaRecorder.stop()

    // ボタンの表示更新 (動画再生・ダウンロードを許可)
    this.$playStart.disabled = false
    this.$download.disabled = false

    console.log('MediaRecorder stop')
  }

  /**
   * 動画を再生する
   */
  startPlaying() {
    // webm形式でBlobで取得
    this.superBuffer = new Blob(this.recordedChunks, { type: "video/webm" });

    // BlobをURLに変換して設定
    this.$videoPlayer.src = URL.createObjectURL(this.superBuffer)
    this.$videoPlayer.controls = true;

    // 動画の再生
    this.$videoPlayer.play()

    console.log('Video playing')
  }

  /**
   * ダウンロードする
   */
  download() {
    const blob = new Blob(this.recordedChunks, { type: "video/webm" });
    const url = URL.createObjectURL(blob)
    const a = document.createElement('a')
    document.body.appendChild(a)
    a.style = 'display: none'
    a.href = url
    a.download = 'video.webm'
    a.click()
    window.URL.revokeObjectURL(url)
    document.body.removeChild(a)

    console.log('Video download')
  }
}

/**
 * 動画の録画処理を生成する
 */
export const createVideoRecord = () => {
  // .js-video-recordの要素をすべて取得
  const targets = [...document.getElementsByClassName('js-video-record')]

  // 各要素の録画機能設定をする
  for (const target of targets) {
    /** トリガーとなるセレクター名を取得する */
    // 動画の再生プレイヤー
    const videoPlayer = target.getAttribute('data-video-player')
    if (!videoPlayer) {
      console.error('data-video-player is required.')
      continue;
    }

    // 録画の開始ボタン
    const recordStart = target.getAttribute('data-record-start')
    if (!recordStart) {
      console.error('data-record-start is required.')
      continue;
    }

    // 録画の停止ボタン
    const recordStop = target.getAttribute('data-record-stop')
    if (!recordStop) {
      console.error('data-record-stop is required.')
      continue;
    }

    // 動画の再生ボタン
    const playStart = target.getAttribute('data-play-start')
    if (!playStart) {
      console.error('data-play-start is required.')
      continue;
    }

    // 動画をダウンロードするボタン
    const download = target.getAttribute('data-download')
    if (!download) {
      console.error('data-download is required.')
      continue;
    }

    /** トリガーとなる各セレクターを取得する */
    // 動画の再生プレイヤー
    const $videoPlayer = document.querySelector(videoPlayer);
    if (!$videoPlayer) {
      console.error('videoPlayer Selector does not exist.')
      continue;
    }

    // 録画の開始ボタン
    const $recordStart = document.querySelector(recordStart);
    if (!$recordStart) {
      console.error('recordStart Selector does not exist.')
      continue;
    }

    // 録画の停止ボタン
    const $recordStop = document.querySelector(recordStop);
    if (!$recordStop) {
      console.error('recordStop Selector does not exist.')
      continue;
    }

    // 動画の再生ボタン
    const $playStart = document.querySelector(playStart);
    if (!$playStart) {
      console.error('playStart Selector does not exist.')
      continue;
    }

    // 動画をダウンロードするボタン
    const $download = document.querySelector(download);
    if (!$download) {
      console.error('download Selector does not exist.')
      continue;
    }

    // インスタンスの作成
    new VideoRecord({
      $target: target,
      $videoPlayer,
      $recordStart,
      $recordStop,
      $playStart,
      $download
    })
  }
}

JavaScriptで動画を録画するコードの詳細な解説

JavaScriptの処理を順番に解説していきます。

構成

.js-video-record」というclassをvideoタグにつけておき、data属性に動画再生や録画開始・停止ボタンなどを指定するような構成にしています。

createVideoRecord()という関数を実行することによって、すべての動作が始まるようになっています。

createVideoRecord()を実行すると、「.js-video-record」のデータを読み込み、VideoRecordクラスのインスタンスを生成して、動画の録画機能が付与されるようになっています。

export const createVideoRecord = () => {
  // .js-video-recordの要素をすべて取得
  const targets = [...document.getElementsByClassName('js-video-record')]

  // 各要素の録画機能設定をする
  for (const target of targets) {
    // 【中略】

    // インスタンスの作成
    new VideoRecord({
      $target: target,
      $videoPlayer,
      $recordStart,
      $recordStop,
      $playStart,
      $download
    })
  }

VideoRecord Class

Class定義をしており、「new VideoRecord()」で必要な要素を引数として渡すことによって、動画の録画・再生機能が付与されるようになっています。

class VideoRecord {
  constructor({ $target, $videoPlayer, $recordStart, $recordStop, $playStart, $download }) {
    // 【中略】
  }

  async initialize() {
    // 【中略】
  }

  startRecording() {
    // 【中略】
  }

  // 【中略】
}

constructorでイベント設定

    // イベント設定
    this.$recordStart.addEventListener('click', this.startRecording)
    this.$recordStop.addEventListener('click', this.stopRecording)
    this.$playStart.addEventListener('click', this.startPlaying)
    this.$download.addEventListener('click', this.download)

これらによって、各ボタンがクリックされた時に、それぞれの処理を呼ぶようにしています。

constructorでthisをbind

    this.initialize = this.initialize.bind(this)
    this.startRecording = this.startRecording.bind(this)
    this.startRecording = this.startRecording.bind(this)
    this.stopRecording = this.stopRecording.bind(this)
    this.startPlaying = this.startPlaying.bind(this)
    this.download = this.download.bind(this)

thisをそれぞれbindしています。

VideoRecord Classのconstractor内の初期化処理

  async initialize() {
    this.mediaStream = null
    this.mediaRecorder = null

    // Blob
    this.recordedChunks = []
    this.superBuffer = null

    this.$videoPlayer.src = null
    this.$videoPlayer.srcObject = null

    // ボタンの表示初期化
    this.$recordStart.disabled = false
    this.$recordStop.disabled = true
    this.$playStart.disabled = true
    this.$download.disabled = true

    // カメラ・音声の取得
    try {
      const mediaDevicesConstraints = {
        audio: true,
        video: { width: 1280, height: 720 }
      }

      // デバイスの動画・音声トラックを取得
      this.mediaStream = await navigator.mediaDevices.getUserMedia(mediaDevicesConstraints)

      // MediaStreamを設定して表示する
      this.$target.srcObject = this.mediaStream
      this.$target.play()
    } catch (err) {
      throw new Error(err)
    }
  }

いろいろな変数の初期化をおこなっていますが、注目すべきは、「await navigator.mediaDevices.getUserMedia()」の部分です。

this.mediaStream = await navigator.mediaDevices.getUserMedia(mediaDevicesConstraints)

» MediaDevices.getUserMedia()

これによって、ブラウザからデバイス(PCやスマホ)のカメラ・マイクにアクセスしてデバイスからの映像などをMediaStreamとして取得しています。

また取得したMediaStreamvideoタグのDOMにマウントしています。こうすることで、カメラから取得した映像を表示しています。

this.$target.srcObject = this.mediaStream
this.$target.play()

録画開始ボタンがクリックされた時の処理

  startRecording() {
    // 録画機能の生成
    this.mediaRecorder = new MediaRecorder(this.mediaStream, { mimeType: 'video/webm; codecs=vp8' });
    // availableイベントでメディア記録を保持
    this.mediaRecorder.ondataavailable = event => this.recordedChunks.push(event.data)
    // 録画開始
    this.mediaRecorder.start()

    console.log('this.superBuffer', this.superBuffer)
    if (this.superBuffer) {
      // メモリ解放
      URL.revokeObjectURL(this.superBuffer)
    }

    // ボタンの表示更新 (動画停止を許可)
    this.$recordStop.disabled = false

    console.log('MediaRecorder start')
  }

MediaRecorder」インスタンスを生成して、this.mediaRecorderに格納しています。

MediaRecorderの仕様を見るとわかりますが、dataavailableイベントが発生したら、データをthis.recordedChunks配列に格納しています。
this.mediaRecorder.start()」で録画を開始しています。

URL.revokeObjectURL(this.superBuffer)」は、後の動画再生処理「startPlaying()」で生成した「URL.createObjectURL(this.superBuffer)」が残っていたら「URL.revokeObjectURL()」でメモリを開放する処理です。(2回目の録画の時など)

this.$recordStop.disabled = false」で録画停止ボタンを押せるようにしています。

録画停止ボタンがクリックされた時の処理

  stopRecording() {
    // 録画停止
    this.mediaRecorder.stop()

    // ボタンの表示更新 (動画再生・ダウンロードを許可)
    this.$playStart.disabled = false
    this.$download.disabled = false

    console.log('MediaRecorder stop')
  }

this.mediaRecorder.stop()」で録画を停止しています。また録画停止ボタンを押したことで録画動画が取得できますので、動画の再生やダウンロードボタンを押せるようにしています。

動画の再生ボタンがクリックされた時の処理

  startPlaying() {
    // webm形式でBlobで取得
    this.superBuffer = new Blob(this.recordedChunks, { type: "video/webm" });

    // BlobをURLに変換して設定
    this.$videoPlayer.src = URL.createObjectURL(this.superBuffer)
    this.$videoPlayer.controls = true;

    // 動画の再生
    this.$videoPlayer.play()

    console.log('Video playing')
  }

new Blob(this.recordedChunks, { type: "video/webm" });」によって、録画したデータをwebmBlob形式で取得しています。

URL.createObjectURLにより、ブラウザのメモリに保存されたBlobにアクセス可能なURLを生成しています。ブラウザを閉じるまで有効になるのと同時に、「URL.createObjectURL()」で生成したデータは常駐するため、「URL.revokeObjectURL()」でメモリ解放する必要があります。

this.$videoPlayer.controls = true;」で「再生、音量、シーク、ポーズ」の各機能を制御するコントロールを表示しています。

this.$videoPlayer.play()」によって動画の再生が開始されます。

動画のダウンロード機能

  download() {
    const blob = new Blob(this.recordedChunks, { type: "video/webm" });
    const url = URL.createObjectURL(blob)
    const a = document.createElement('a')
    document.body.appendChild(a)
    a.style = 'display: none'
    a.href = url
    a.download = 'video.webm'
    a.click()
    URL.revokeObjectURL(url)
    document.body.removeChild(a)

    console.log('Video download')
  }

こちらはGoogleで公開されているサンプルを参考にして作成しています。

先ほどと同じくBlobで生成したデータを「URL.createObjectURL」でアクセスできるようにして、そのURLリンクをJavaScriptでクリックしてダウンロード、そして「URL.revokeObjectURL」でメモリを開放しているという内容です。

ダウンロードボタンがクリックされたらこの処理が実行されてwebm形式で録画した動画が「video.webm」という名前でダウンロードされます。

注意点など

今回のサンプルコードをローカルで動作させる時は、「yarn」でパッケージをインストールしてから、「yarn dev」を実行すると、「localhost:8080」でアクセスできるようになります。

localhostでスマホからは見れないかも

ちなみに、スマホからアクセスする時はサイトに上げるなどの工夫が必要です。ローカルIPアドレスからカメラにアクセスはできないと思います。localhosthttpsのオリジンのサイトからしかnavigator.mediaDevices.getUserMedia()はアクセスできないようです。

まとめ

今回はブラウザからカメラやマイクにアクセスして、簡単に録画し、その動画を再生・ダウンロードできる機能について解説しました。

GitHubのコード

https://github.com/it-web-life/javascript_record_video_how_to

GitHub Pages サンプルページ

https://it-web-life.github.io/javascript_record_video_how_to/index.html

ご参考になれば幸いです。

※当サイトでは一部のリンクについてアフィリエイトプログラムを利用して商品を紹介しています