こんにちは、デザイナーの @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 が実行され、
this.src = evt.target.result:base64エンコーディングされたdata: URLがthis.srcに入る。これにより画面に動画のプレビューが表示される。this.creaateThumbnails(this.src):サムネイル作成メソッドが実行される。
動画のプレビュー
<video controls v-if="src"> <source :src="src" type='video/mp4; codecs="avc1.42E01E, mp4a.40.2"'> </video>
this.src に data: URL が入ったタイミングで、vue の v-if="src" と :src="src"により、動画のプレビューが表示されます。簡単ですね。

サムネイルを作成
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() }
サムネイルを作成するために、新規で Video、Canvas、Context を作成します。
これらは画面に出てくることはありません。全てメモリ上で処理されます。
サムネイルを作成する大まかな流れとしては以下のとおりです。
videoに動画を読み込ませる。- 読み込みが完了したら、
canvasのサイズを指定して、video.currentTimeを変更(0を指定)。 video.currentTimeが変更されたら、動画からその時間の画像を作成し、配列に保管。video.currentTimeを変更。video.currentTimeがvideo.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.currentTimeがvideo.durationと同値以上になるまで、video.durationを必要枚数 - 1の数で割った数をvideo.currentTimeに足す。( =video.onseekedが実行される。)

その他
最後にスタイルとか UI/UX あたりをちょちょちょっと調整してあげればいい感じになります。
ご静読ありがとうございました。