1
0

Reactからffmpeg.wasmを使って動画にウォーターマークを埋め込んでみる

Posted at

概要

Reactでffmpeg.wasmを使って、選択した動画ファイルに対してウォーターマーク(透かし)を埋め込むツールを作ります。

ffmpeg.wasmとは

ffmpeg.wasmは、FFmpegの純粋なWebassembly/Javascript移植版です。ブラウザ上でビデオやオーディオの録画、変換、ストリーミングを行うことができます。Webassemblyなので従来はサーバサイドで行っていた動画リソースに対するいろいろな処理をクライアントで処理することが可能になります。

Reactのプロジェクトを用意する

予めNode.jsのv16以上を使えるようにしてください。

# バージョンをチェック
$ node -v

インストール

今回はViteを使います。

$ npm create vite@latest

Select a frameworkReactを選択します。


Select a variantはお好みですが今回はTypeScriptを選択しました。

プロジェクトが生成されたらプロジェクトディレクトリへ移動してffmpeg.wasmをインストールしてください。

$ npm install @ffmpeg/core @ffmpeg/ffmpeg

実装の前準備

メインの実装に取り掛かる前に、必要なアセットやスタイルシートなどを整備しておきます。

ウォーターマーク用の透過PNG画像を用意する

今回はフリーアイコンのPIN画像を使用します。
1.png

ファイルは /public ディレクトリに cat_line.png という名前で保存しておきます。

スタイルシートを修正する

index.css と App.css をそれぞれ以下のように修正します。

index.css
:root {
  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
  line-height: 1.5;
  font-weight: 400;

  color-scheme: light dark;
  color: rgba(255, 255, 255, 0.87);
  background-color: #242424;

  font-synthesis: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  display: flex;
  place-items: center;
  min-width: 320px;
  min-height: 100vh;
}

h1 {
  font-size: 3.2em;
  line-height: 1.1;
}

button {
  border-radius: 0.15rem;
  border: 1px solid transparent;
  padding: 0.6em 1.2em;
  font-size: 1em;
  font-weight: 500;
  font-family: inherit;
  background-color: #1a1a1a;
  cursor: pointer;
  transition: border-color 0.25s;
}
button:hover {
  border-color: #646cff;
}
button:focus,
button:focus-visible {
  outline: 4px auto -webkit-focus-ring-color;
}

App.css
#root {
  max-width: 1280px;
  margin: 0 auto;
  padding: 2rem;
  text-align: center;
}

.app {
  display: flex;
  flex-flow: column nowrap;
  align-items: center;
}

.video {
  margin-bottom: 2rem;;
}

.progress-bar {
  width: 300px;
  background-color: #444;
  height: 2rem;
  position:relative;
  border-radius: 0.15rem;
  overflow: hidden;
}

.progress {
  background-color: #1D9BF0;
  height: 2rem;
}

.progress-par {
  position: absolute;
  top: 0px;
  bottom: 0px;
  left:0px;
  right: 0px;
  text-align: center;
  line-height: 2rem;
}

実装

既存のApp.tsxを修正していきます。
完成したソースはこちら。

App.tsx
import { useState, Suspense, useCallback } from 'react';
import { createFFmpeg, fetchFile } from '@ffmpeg/ffmpeg';
import reactLogo from './assets/react.svg';
import './App.css';

function App() {
  const [videoSrc, setVideoSrc] = useState('');
  const [isLoading, setLoading] = useState(false);
  const [progress, setProgress] = useState(0);

  const ffmpeg = createFFmpeg({
    log: true,
    progress: (p: any) => {
      setProgress(Math.round(p.ratio * 100));
    },
  });

  const hanfleClear = () => {
    setVideoSrc('');
    setProgress(0);
  };

  const handleSelectFile = async ({ target: { files } }: React.ChangeEvent<HTMLInputElement>) => {
    if (!!!files || files.length === 0) return;

    try {
      setLoading(true);

      const file = files[0];
      const { name } = file;

      if (!ffmpeg.isLoaded()) await ffmpeg.load();

      ffmpeg.FS('writeFile', 'watermark.png', await fetchFile(`${window.location.origin}/cat_line.png`));
      ffmpeg.FS('writeFile', name, await fetchFile(file));

      await ffmpeg.run('-i', name, '-i', 'watermark.png', '-filter_complex', "[1:v]lut=a='val*0.4',[0:v]overlay", 'output.mp4');

      const data = ffmpeg.FS('readFile', 'output.mp4');
      setVideoSrc(URL.createObjectURL(new Blob([data.buffer], { type: 'video/mp4' })));
    } catch (error) {
      console.error(error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="app">
      <h1>Embed a Watermark</h1>
      {isLoading ? (
        <div className="progress-bar">
          <div className="progress" style={{ width: `${progress}%` }}></div>
          <div className="progress-par">Encoding... {progress}%</div>
        </div>
      ) : (
        <>
          {videoSrc ? (
            <div>
              <video className="video" src={videoSrc} controls autoPlay muted></video>
              <div>
                <button onClick={hanfleClear}>Clear</button>
              </div>
            </div>
          ) : (
            <div>
              <input type="file" id="uploader" onChange={handleSelectFile} />
            </div>
          )}
        </>
      )}
    </div>
  );
}

export default App;

ファイル選択など一般的なReactの実装は割愛して、ffmpeg.wasmを使用する部分に絞って解説していきます。

ffmpegインスタンスの生成

ffmpegのインスタンスはcreateFFmpegで生成します。


プログレスバーを表示するために進捗をuseStateで管理します。

/** 進捗 */
const [progress, setProgress] = useState(0);

const ffmpeg = createFFmpeg({
  log: true, // trueにするとcondoleにffmpegのログが表示されるようになる
  progress: (p: any) => { // 進捗のcallback
    setProgress(Math.round(p.ratio * 100));
  },
});

ffmpeg.wasmのcoreをロードする

ffmpeg.wasmのcoreをロードします。1度だけ実行すれば良いのでisLoaded()でロード状態を判定して実行します。

if (!ffmpeg.isLoaded()) await ffmpeg.load();

ウォーターマーク画像をMEMFSに保存する

ffmpeg.wasmで入出力するファイルはMEMFSで扱います。

/publicに配置したcat_line.pngをwatermark.pngというファイル名でMEMFSに保存します。

ffmpeg.FS('writeFile', 'watermark.png', await fetchFile(`${window.location.origin}/cat_line.png`));

選択された動画をMEMFSに保存する

選択された動画もウォーターマーク画像と同様にMEMFSへ書き込みます。

ffmpeg.FS('writeFile', name, await fetchFile(file));

ffmpegによるエンコード処理

ffmpegのオプションがそのまま指定できるので、filter_complexオプションを使用して透過度0.4でウォーターマークを左上に埋め込みます。

await ffmpeg.run('-i', name, '-i', 'watermark.png', '-filter_complex', "[1:v]lut=a='val*0.4',[0:v]overlay", 'output.mp4');

エンコードされた動画をvideoタグに読み込む

エンコードされた動画ファイルをMEMFSから取得して、ObjectURL形式に変換してvideoタグに読み込ませます。

videoタグにはautoPlaymutedを設定しているので自動的に動画が再生されます。

/** videoのsrc */
const [videoSrc, setVideoSrc] = useState('');

const data = ffmpeg.FS('readFile', 'output.mp4');
setVideoSrc(URL.createObjectURL(new Blob([data.buffer], { type: 'video/mp4' })));
<video className="video" src={videoSrc} controls autoPlay muted></video>

動かしてみる

早速作ったアプリを動かしてみます。

$ npm run dev

動画ファイルを選択します。
2.png

選択後すぐさまエンコード処理が実行されます。進捗はプログレスバーで表示されています。
3.png

エンコードが完了すると動画が再生されます。
ちゃんと左上にウォーターマークが埋め込まれていますね!

4.png

気になるエンコーディングのパフォーマンスですが、CLIのffmpegでエンコードした場合に比べて数倍遅くなります。そこはブラウザ内で実行しているので仕方がないですね。

まとめ

今回はWebAssemblyで実装されたffmpegを使ってみましたが、大きい動画でなければ十分実用的なパフォーマンスだと思いました。


使い方も簡単で、ffmpegのコマンドがそのまま使えるのもポイントだと思います。

参考

1
0
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
1
0