VIVITABLOG

VIVITAで活躍するメンバーの情報発信サイト

デザイナーが Vue.js で作る、ローカル動画ファイルのプレビュー画面〜複数のサムネイル候補を添えて〜

こんにちは、デザイナーの @imatomix です。

今、新しいWebアプリ企画のプロトタイプを作ってます。
巷ではよく「スタートアップの武器はスピード」「プロトタイピングはスピードが命」なんて言われていますが、正直に開発スピードを追い求めていたら、手持ちのタスクの半分以上がエンジニアリングになりました。

こんにちは、デザイナーの @imatomix です。(2回目)

今回はそんな僕が、ちょうど今やってるタスクの1つをそのままブログにしてみます。

目次:

作る機能

Webアプリの企画自体は、ざっくりいうとYoutubeやInstagramのような動画SNS的なものです。 動画を取り扱う場合、

  • 動画のサムネイルをフロントエンド〜バックエンドのどこで作成するか
  • 動画のどの部分をサムネイルにするか

といった課題が上がるかと思います。
今回はプロトタイプということで、開発スピードを重視して、以下をフロントエンドのみで行います。

  • 選択した動画ファイルのプレビュー表示。
  • 動画から複数のサムネイルを自動作成。

できたもの

早速ですが、以下ができたものです。
適当にローカルの動画ファイルを選択してみてください。

※ ご安心ください。どこにもアップロードされません。

解説

では、各工程で要点を絞って解説します。

ファイルの選択

<input type="file" accept="video/mp4,video/x-m4v" @change="handleFileSelect">

基本通りに <input type="file" > でファイルを読み込みます。 その際、 accept="video/mp4"で選択可能なファイル形式を .mp4ムービーに絞っています。

ちなみにスマホの場合、これだけでカメラを起動して動画撮影することもできるようになっています。

便利ですねー。

最後に@change="handleFileSelect"で、選択しているファイルに変更があったときに実行するメソッドを指定しています。@change は vue.js のイベントハンドラで、ファイル変更時に発火してくれます。

では次に、選択ファイルの変更時に実行されるメソッドの中身を見ていきましょう。

ファイルのチェックと読み込み

handleFileSelect(event) {  
  // ファイルのチェック
  const file = event.target.files[0]
  if (!file || !file.type.match('video/*')) return

  // ファイルの読み込み
  const reader = new FileReader()
  reader.onload = (evt) => {
    this.src = evt.target.result
    this.createThumbnails(this.src)
  }
  reader.readAsDataURL(file)
}

選択したファイルの情報は、event.target.filesに、配列として格納されます。今回は1つしかファイルを選択できないので、ファイルの情報はevent.target.files[0] に格納されています。 if (!file || !file.type.match('video/*')) returnで、ファイルの存在と種類をチェックして、チェックを通ったファイルのみを読み込みます。

ファイルの読み込みには、以下の機能を持つ FileReader を使用します。

  • FileReader.onload :ファイルの読み込みが完了し利用可能になると発火するイベントで呼び出されるコールバック関数。
  • FileReader.readAsDataURL() :ファイルを読み込み、 base64 化したもの格納する。

reader.readAsDataURL(file) によるファイル読み込みが完了すると reader.onload が実行され、

動画のプレビュー

<video controls v-if="src">
  <source :src="src" type='video/mp4; codecs="avc1.42E01E, mp4a.40.2"'>
</video>

this.srcdata: URL が入ったタイミングで、vue の v-if="src":src="src"により、動画のプレビューが表示されます。簡単ですね。

f:id:imatomix:20190404191615p:plain

サムネイルを作成

createThumbnails(src) {
  const video = document.createElement('video')
  const canvas = document.createElement('canvas')
  const context = canvas.getContext('2d')

  //  読み込みが完了したらcanvas サイズを設定
  video.onloadeddata = () => {
    canvas.width = video.videoWidth
    canvas.height = video.videoHeight
    video.currentTime = 0
  }

  //  video.currentTime が変更されたらキャプチャ
  video.onseeked = () => {
    if(video.currentTime < video.duration ){
      context.drawImage(video, 0, 0, video.videoWidth, video.videoHeight)
      this.thumbnails.push(canvas.toDataURL('image/jpeg'))
      video.currentTime += Math.ceil(video.duration / 4) 
    } else {
      context.drawImage(video, 0, 0, video.videoWidth, video.videoHeight)
      this.thumbnails.push(canvas.toDataURL('image/jpeg'))
    }
  }
      
  // 動画を読み込む
  video.src = src
  video.load()
}

サムネイルを作成するために、新規で VideoCanvasContext を作成します。
これらは画面に出てくることはありません。全てメモリ上で処理されます。

サムネイルを作成する大まかな流れとしては以下のとおりです。

  1. video に動画を読み込ませる。
  2. 読み込みが完了したら、canvas のサイズを指定して、video.currentTimeを変更(0を指定)。
  3. video.currentTime が変更されたら、動画からその時間の画像を作成し、配列に保管。video.currentTimeを変更。
  4. video.currentTimevideo.duration (動画の総尺=最後) になるまで 3. を繰り返す。

Video は以下のイベントを持っています。これらを利用して 2. と 3. を実装します。

video.onloadeddata = () => {
  canvas.width = video.videoWidth
  canvas.height = video.videoHeight
  video.currentTime = 0
}

video.onloadeddata は、 video が動画データの読み込みを終えたときに実行されるコールバック関数です。この関数内で以下の処理を行います。

  • 動画データの読み込みにより取得した動画のサイズ情報を canvas に渡す。
  • video.currentTime に 値を代入し、video.onseeked を発火させる。

これにより、続けて以下の処理が実行されます。

video.onseeked = () => {
  if(video.currentTime < video.duration ){
    context.drawImage(video, 0, 0, video.videoWidth, video.videoHeight)
    this.thumbnails.push(canvas.toDataURL('image/jpeg'))
    video.currentTime += Math.ceil(video.duration / 4)
  } else {
    context.drawImage(video, 0, 0, video.videoWidth, video.videoHeight)
    this.thumbnails.push(canvas.toDataURL('image/jpeg'))
  }
}

video.onseeked は、シークが完了したときに実行されるコールバック関数です。つまり、video.currentTime に値を代入することで、発火させることができます。

ここで、サムネイル画像を作成します。

手順としては、以下の通りです。

  • context.drawImage を使用して、canvas にそのフレームの動画の画像を描画する。
  • canvas.toDataURL を使用して、canvas に描画された画像の data: URL を取得する。
  • 取得した data: URL<img src> に入れてあげれば画像が表示される。

あとは、これを以下の条件で繰り返せば、複数枚のサムネイルが作成できます。

  • video.currentTimevideo.duration と同値以上になるまで、video.duration必要枚数 - 1の数で割った数を video.currentTime に足す。( = video.onseeked が実行される。)

f:id:imatomix:20190404191729p:plain

その他

最後にスタイルとか UI/UX あたりをちょちょちょっと調整してあげればいい感じになります。

ご静読ありがとうございました。