【ffmpeg.wasm】ブラウザ上で動画を生成する
概要
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.write
やffmpeg.read
の扱いが特にそのような感じがする)
実装
ファイルの作成
index.html
とapp.js
を作成し、ライブラリの読み込みを行う。
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、Uint8Array
、File
(Blob
)の画像が使用できる。
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
}
使用API
ffmpeg.load
ffmpegのコアスクリプト(ffmpeg-core.js
)を読み込む。
ffmpeg.write
Emscripten file system内にデータを書き込む。
ffmpeg.run
ffmpegのコマンドを実行する。
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')
あとがき
砂糖が知らなかっただけかもしれないが、ブラウザ上で動画を作れることにびっくりした😲
Discussion