この記事は何
最近Next.js上でffmpegを用いたBlobのデータのエンコードを行う処理を実装しました。
実装の方法があまり調べても出てこなかったので記事としてまとめます。
実装の流れ
Dockerの準備
まずはDockerで環境の準備を行います。
今回はNext.js上で実装をする想定なのでdocker-compose.ymlの設定はNext.jsの開発環境サーバーを立ち上げる前提にしています。
ポイントはDocker内でffmpeg
のインストールを行っているところです。
こちらでインストールしていたffmpeg
をこの後利用します。
FROM node:[versionを指定]-alpine
WORKDIR /app
COPY . /app
RUN apk add --update --no-cache ffmpeg
RUN npm install
services:
app:
build:
context: .
dockerfile: Dockerfile
tty: true
ports:
- "3000:3000"
command: npm run dev
volumes:
- type: bind
source: .
target: /app
- type: volume
source: node_modules
target: /app/node_modules
volumes:
node_modules:
Node.js上でffmpegを利用するための準備
次に、ffmpegをNode.js上で利用するための準備を行います。
今回はfluent-ffmpeg/node-fluent-ffmpeg: A fluent API to FFMPEG (http://www.ffmpeg.org) を用いて実装を行っていきます。
$ docker compose run --rm app npm install fluent-ffmpeg
$ docker compose run --rm app npm install -D @types/fluent-ffmpeg
処理の実装
最後に、受け取ったBlobデータのエンコード処理を実際に実装していきます。
まずはコードを紹介します。
今回は受け取った音声データを、mp3に変換して返す処理を実装します。
import fs from "fs";
import { setTimeout } from "timers/promises";
import Ffmpeg from "fluent-ffmpeg";
export const transcodeToMp3 = async (args: { blob: Blob; name: string }) => {
const inputFileName = `/tmp/${args.name}-input.${args.blob.type.split("/")[1]}`;
const outputFileName = `/tmp/${args.name}-output.mp3`;
fs.writeFileSync(inputFileName, Buffer.from(await args.blob.arrayBuffer()));
let isCompleted = false;
const deleteFiles = () => {
if (fs.existsSync(inputFileName)) {
fs.unlinkSync(inputFileName);
}
if (fs.existsSync(outputFileName)) {
fs.unlinkSync(outputFileName);
}
}
const command = Ffmpeg(inputFileName)
.on("error", (err) => {
deleteFiles();
})
.on("end", () => {
isCompleted = true;
})
.save(outputFileName)
for (let i = 0; i < 50; i++) {
if (isCompleted) {
break;
}
await setTimeout(100);
}
if (!isCompleted) {
command.kill('SIGSTOP');
deleteFiles();
throw new Error("Failed to transcode");
}
const output = fs.readFileSync(outputFileName);
deleteFiles();
return new Blob([output], { type: "audio/mp3" });
};
今回の実装のポイントは以下のとおりです。
ポイント1: 一時ファイルを用いて変換処理を行う
ffmpegはBlobデータをそのまま変換させるようなことはできないため、
データを一度音声ファイルとして書き出してffmpegによる変換を行なっています。
一時ファイルが残り続けるのを避けるため、変換が完了した後に一時ファイルを削除する処理を実装しています。
のように、fluent-ffmpegではエラーをイベントとして受け取れるので、エラーが起こった際もファイルの削除を行うようにしています。
何かロガーなどがあればここでエラーメッセージを送信するのもありだと思います。
ポイント2: 変換処理の完了をsetTimeout
を用いてポーリングして確認する
fluent-ffmpegはffmpegの操作は行えるものの、処理の状態はPromiseなどで管理されているわけではないので、完了待ちをするのにピーキーな実装が必要になりました。
今回はisCompleted
という変数を定義し、
のように処理の完了のイベントハンドラを用いてisCompleted
をtrue
に変換するという処理を実装しました。
そしてfor文と"timers/promises"
のsetTimeout
を用いて、
0.1秒ごとにisCompleted
のステータスを確認し、trueになったタイミングで次の処理で移動するような実装を行いました。
なお、今回は最大でも5秒までしか処理を待たないようにしたかったため、for文で50回ポーリング処理を行い、それでも完了していない場合はffmpegを止めエラーを投げるようにしました。
処理を組み込む
あとはバックエンド処理として今回実装したメソッドを組み込めば実装は完了です。
Next.jsであればサーバーサイドの処理(App RouterであればServer ComponentsやServer Actions)に組み込めば動作します。