【ffmpeg.wasm】ブラウザ上で動画を生成する

公開:2020/10/17
更新:2020/10/18
6 min読了の目安(約5600字TECH技術記事

概要

ffmpeg.wasmを使用し、ブラウザ上で動画を生成する。

環境

Mac / Chrome 86

動作デモ

覚え書き

ffmpeg.wasm

ffmpegwasm/ffmpeg.wasm: FFmpeg for browser and node, powered by WebAssembly

WebAssemblyによりブラウザ上で動くFFmpeg。

WebAssembly(ウェブアセンブリ)

WebAssembly の概要 - WebAssembly | MDN

ブラウザでJavaScriptよりも高速に動く、JavaScriptとは違う種類のコード?🤔

サンドボックス化した実行環境上で動作するらしい。記事の中で「Emscripten file system内」という表現を使用しているが、その「Emscripten file system内」がサンドボックスした実行環境?🤔🤔(ffmpeg.writeffmpeg.readの扱いが特にそのような感じがする)

実装

ファイルの作成

index.htmlapp.jsを作成し、ライブラリの読み込みを行う。

ffmpeg.wasmのREADMEにはこの方法が「only works in Chrome」と書いてある。

index.html

表示する要素はJavaScriptで挿入する。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <script src="https://unpkg.com/@ffmpeg/ffmpeg@0.8.3/dist/ffmpeg.min.js"></script>
  </head>
  <body>
    <script src="app.js"></script>
  </body>
</html>

app.js

ライブラリの読み込みを行っている。

(async () => {
  const { createFFmpeg } = FFmpeg
})()

動画化する画像の用意

ここでは画像をbase64(データURL)で生成したが、この後行う処理ではbase64の他にもURL、Uint8ArrayFileBlob)の画像が使用できる。

function generateImages() {
  const canvas = document.createElement('canvas')
  canvas.width = 320
  canvas.height = 240

  const ctx = canvas.getContext('2d')
  ctx.textBaseline = 'middle'
  ctx.textAlign = 'center'
  ctx.font = '64px serif'

  const arr = []

  for (let i = 0; i < 4; i++) {
    ctx.clearRect(0, 0, canvas.width, canvas.height)
    ctx.fillStyle = '#fff'
    ctx.fillRect(0, 0, canvas.width, canvas.height)
    ctx.fillStyle = '#000'
    ctx.fillText(i + 1, canvas.width / 2, canvas.height / 2)

    const dataUrl = canvas.toDataURL()
    arr.push(dataUrl)
  }

  return arr
}

画像の動画化

ffmpegの機能で画像を動画化する。

async function generateVideo(images) {
  const ffmpeg = createFFmpeg({ log: true })
  await ffmpeg.load()

  images.forEach(async (image, i) => {
    await ffmpeg.write(`image${i}.png`, image)
  })

  await ffmpeg.run('-r 1 -i image%d.png -pix_fmt yuv420p output.mp4')
  const data = ffmpeg.read('output.mp4')
  return data
}

async functionは暗黙的にPromiseを返すので、呼び出し時は注意する。
参考:async function - JavaScript | MDN

使用API

ffmpeg.load

ffmpegのコアスクリプト(ffmpeg-core.js)を読み込む。

ffmpeg.write

Emscripten file system内にデータを書き込む。

ffmpeg.run

ffmpegのコマンドを実行する。

ffmpeg.wasmのREADMEにはffmpeg.transcode('test.avi', 'test.mp4')の例(aviをmp4に変換する)が記載されているが、インプットに対してオプションを指定する方法が分からなかったのでffmpeg.runを使用した(アウトプットに対してなら第3引数で指定できる?🤔)。

ffmpeg.read

Emscripten file system内のデータを読み込む。データはUint8Array型で返ってくる。

オブジェクトURLの作成

video要素で読み込むためのURLを作成する。

Uint8Array型で返ってきたデータをBlob型に変換してからオブジェクトURLを作成している。

function createObjectUrl(array, options) {
  const blob = new Blob(array, options)
  const objectUrl = URL.createObjectURL(blob)
  return objectUrl
}

video要素の挿入

video要素を作成し、srcの読込が完了したらbody要素に挿入する。

function insertVideo(src) {
  const video = document.createElement('video')
  video.controls = true

  video.onloadedmetadata = () => {
    document.body.appendChild(video)
  }

  video.src = src
}

完成ソースコード

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <script src="https://unpkg.com/@ffmpeg/ffmpeg@0.8.3/dist/ffmpeg.min.js"></script>
  </head>
  <body>
    <script src="app.js"></script>
  </body>
</html>

app.js

(async () => {
  const { createFFmpeg } = FFmpeg

  function generateImages() {
    const canvas = document.createElement('canvas')
    canvas.width = 320
    canvas.height = 240

    const ctx = canvas.getContext('2d')
    ctx.textBaseline = 'middle'
    ctx.textAlign = 'center'
    ctx.font = '64px serif'

    const arr = []

    for (let i = 0; i < 4; i++) {
      ctx.clearRect(0, 0, canvas.width, canvas.height)
      ctx.fillStyle = '#fff'
      ctx.fillRect(0, 0, canvas.width, canvas.height)
      ctx.fillStyle = '#000'
      ctx.fillText(i + 1, canvas.width / 2, canvas.height / 2)

      const dataUrl = canvas.toDataURL()
      arr.push(dataUrl)
    }

    return arr
  }

  async function generateVideo(images) {
    const ffmpeg = createFFmpeg({ log: true })
    await ffmpeg.load()

    images.forEach(async (image, i) => {
      await ffmpeg.write(`image${i}.png`, image)
    })

    await ffmpeg.run('-r 1 -i image%d.png -pix_fmt yuv420p output.mp4')
    const data = ffmpeg.read('output.mp4')
    return data
  }

  function createObjectUrl(array, options) {
    const blob = new Blob(array, options)
    const objectUrl = URL.createObjectURL(blob)
    return objectUrl
  }

  function insertVideo(src) {
    const video = document.createElement('video')
    video.controls = true

    video.onloadedmetadata = () => {
      document.body.appendChild(video)
    }

    video.src = src
  }

  const div = document.createElement('div')
  div.innerText = '動画生成中'
  document.body.appendChild(div)

  const images = generateImages()
  const video = await generateVideo(images)
  const objectUrl = createObjectUrl([video], { type: 'video/mp4' })
  insertVideo(objectUrl)

  document.body.removeChild(div)
})()

所感

ffmpegのコアスクリプトの読込時間

ffmpeg.loadにやや時間がかかるので、Node.jsが使える環境(ローカルとか)ならシェルコマンドの実行でもいいかもしれない?🤔

const { execSync } = require('child_process')
execSync('ffmpeg -r 1 -i image%d.png -pix_fmt yuv420p output.mp4')

あとがき

砂糖が知らなかっただけかもしれないが、ブラウザ上で動画を作れることにびっくりした😲