2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Docker+ffmpegでWebアプリケーション上でBlobをエンコードする処理を実装する

Last updated at Posted at 2024-10-28

この記事は何

最近Next.js上でffmpegを用いたBlobのデータのエンコードを行う処理を実装しました。
実装の方法があまり調べても出てこなかったので記事としてまとめます。

実装の流れ

Dockerの準備

まずはDockerで環境の準備を行います。
今回はNext.js上で実装をする想定なのでdocker-compose.ymlの設定はNext.jsの開発環境サーバーを立ち上げる前提にしています。
ポイントはDocker内でffmpegのインストールを行っているところです。
こちらでインストールしていたffmpegをこの後利用します。

Dockerfile
FROM node:[versionを指定]-alpine
WORKDIR /app

COPY . /app
RUN apk add --update --no-cache ffmpeg
RUN npm install
docker-compose.yml
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) を用いて実装を行っていきます。

node-fluent-ffmpegのインストール
$ docker compose run --rm app npm install fluent-ffmpeg
$ docker compose run --rm app npm install -D @types/fluent-ffmpeg

処理の実装

最後に、受け取ったBlobデータのエンコード処理を実際に実装していきます。
まずはコードを紹介します。
今回は受け取った音声データを、mp3に変換して返す処理を実装します。

transcodeToMp3.ts
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という変数を定義し、

のように処理の完了のイベントハンドラを用いてisCompletedtrueに変換するという処理を実装しました。

そしてfor文と"timers/promises"setTimeoutを用いて、
0.1秒ごとにisCompletedのステータスを確認し、trueになったタイミングで次の処理で移動するような実装を行いました。
なお、今回は最大でも5秒までしか処理を待たないようにしたかったため、for文で50回ポーリング処理を行い、それでも完了していない場合はffmpegを止めエラーを投げるようにしました。

処理を組み込む

あとはバックエンド処理として今回実装したメソッドを組み込めば実装は完了です。
Next.jsであればサーバーサイドの処理(App RouterであればServer ComponentsやServer Actions)に組み込めば動作します。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?