こんにちは、デザイナーの @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 あたりをちょちょちょっと調整してあげればいい感じになります。
ご静読ありがとうございました。