🎉

SvelteKitとffmpeg.wasmを使ったアプリの作り方

2022/06/28に公開約6,000字

SvelteKitffmpeg.wasmを使って簡単なアプリを作る。作ったものはこちらで、動画から好きなシーンを選択して保存するシンプルなアプリ

https://video-capture-web.web.app/
遊んでみてください

プロジェクト作成

こちらを参考にプロジェクトを作成する

$ npm create svelte video-capture-web

対話形式で選択していく

SPAモード

こちらを参考にadapter@sveltejs/adapter-staticを指定してSPAなアプリとする

$ npm install -D @sveltejs/adapter-static

adapter@sveltejs/adapter-staticに変えてfallbackを設定する

svelte.config.js
import adapter from '@sveltejs/adapter-static';
・・・
const config = {
  kit: {
    adapter: adapter({
      fallback: 'index.html'
    }),
  }
  ・・・
};

export default config;

npm run dev後、ブラウザでhttp://localhost:3000が表示できることを確認する

$ npm install

$ npm run dev
> video-capture-web@0.0.1 dev
> svelte-kit dev
  SvelteKit v1.0.0-next.355

  local:   http://localhost:3000

ffmpeg.wasm

こちらを参考にインストール

$ npm install @ffmpeg/ffmpeg @ffmpeg/core

ffmpeg.wasmが読み込めるかを確認

src/routes/index.svelte
<script lang="ts">
    import { createFFmpeg } from '@ffmpeg/ffmpeg';
    let ffmpeg = createFFmpeg({ log: true });
    ffmpeg.load();
</script>

ブラウザで使用する場合は以下の引用のように書かれているので、ローカルサーバーで起動したときにhttpヘッダーを追加する。また、サーバー上で動作させる場合にも同様なヘッダーが必要になる

SharedArrayBuffer is only available to pages that are cross-origin isolated. So you need to host your own server with Cross-Origin-Embedder-Policy: require-corp and Cross-Origin-Opener-Policy: same-origin headers to use ffmpeg.wasm.

svelte.config.js
const config = {
  ・・・
  kit: {
   ・・・
    vite: {
      server: {
        headers: {
          'Cross-Origin-Opener-Policy': 'same-origin',
          'Cross-Origin-Embedder-Policy': 'require-corp'
        }
      }
    }
  }
};

export default config;
hooks.ts
import type { Handle } from '@sveltejs/kit';
import config from '../svelte.config';
import { dev } from '$app/env';

const updateResponseHeadersInDevFactory = (): ((r: Response) => void) => {
    const doNothing = (r: Response): void => undefined;

    if (!dev) {
        return doNothing;
    }

    if (typeof config.kit?.vite === 'object' && config.kit?.vite?.server?.headers) {
        const headerArray = Object.entries(config.kit?.vite?.server?.headers)
        .filter(([k, v]) => v)
        .map(([k, v]) => [k, v?.toString() || '']);

        const updateHeaders = (r: Response): void => {
            headerArray.forEach(([k, v]) => r.headers.append(k, v));
        };
        return updateHeaders;
    }
    return doNothing;
};

const updateResponseHeadersInDev = updateResponseHeadersInDevFactory();

export const handle: Handle = async ({ event, resolve }) => {
    const response = await resolve(event, {});
    updateResponseHeadersInDev(response);
    return response;
};

ブラウザでページを表示してffmpegloadでエラーが出ていないことを確認する

フレーム画像生成

ブラウザから選択した動画から1秒ごとのフレーム画像のBlob URLのリストを作成して、Blob URLのリストからimgタグで表示する。ffmpeg.FSでファイルの読み込み、書き込み、削除をすることができ、ffmpeg.runffmpegコマンドを実行することができる。

<script lang="ts">
    import { createFFmpeg,fetchFile } from '@ffmpeg/ffmpeg';
    let ffmpeg = createFFmpeg({ log: true });
    ffmpeg.load();

    let images: string = [];

    const fileChangeHandler = async (e: Event) => {
        if (e.target instanceof HTMLInputElement && e.target.files) {
            let images: string[] = [];
            try {
                let file = e.target.files[0];

                // 動画ファイルを`video`というファイル名でメモリに読み込む
                ffmpeg.FS('writeFile', "video", await fetchFile(file));

                // `video`を入力して、1秒間隔でフレーム画像を`out%d.png`というファイル名でメモリに出力
                // %dには連番が振られる(out1.png, out2.png, out3.png, ・・・)
                await ffmpeg.run('-i', "video", '-vf', 'fps=1', "out%d.png");

                for (let i = 1; ; i++) {
                  // 生成された`out%d.png`ファイルのBlob URLを作ってリストに追加
                  const data = ffmpeg.FS('readFile', 'out' + i + '.png');
                  const blob = new Blob([data.buffer], { type: 'image/png' });
                  const url = URL.createObjectURL(blob);
                  images = [...images, url];
                }
            } catch (e) {
                console.log(e);
            }
        }
  };
</script>

<input type="file" accept=".mp4,.MOV" on:change={(e) => fileChangeHandler(e)}>

{#each $images as image}
  <img src={image}>
{/each}

今回作ったアプリでffmpeg.wasmを使ったプログラミング実装の大切な部分はほぼ上記のみで、あとはレイアウトなどを調整して作り込む。プロジェクト全体はリポジトリを参照のこと

Firebase Hostingにデプロイ

ビルドすると出来上がるbuildディレクトリが公開ディレクトリとなる

$ npm run build

ここではFirebase Hostingへの細かなデプロイ手順はかかないが、ffmpeg.wasmを使うためにhttpヘッダーの設定をして、publicにビルドで出来上がったbuildディレクトリを指定してfirebase deployを行う

firebase.json
{
  "hosting": {
    "public": "build",
    "headers": [
      {
        "source": "*",
        "headers": [
          {
            "key": "Cross-Origin-Opener-Policy",
            "value": "same-origin"
          },
          {
            "key": "Cross-Origin-Embedder-Policy",
            "value": "require-corp"
          }
        ]
      }
    ],
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ],
    ・・・
  }
}

また、ここではブラウザリロード時に404とならないようにrewritesを指定している。

その他

mac+chromeとandroidエミュレータ+chromeで動作確認済み

Unhandled Promise Rejection: RangeError: Out of Memory. And then the line of code that the error points to is "await ffmpeg.load();

iOSでは上記のエラーで動作確認できていない。issueによると、現バージョンでは既知の事象でパラメータを設定してビルドしなおせばiOSでも動くらしい

https://github.com/nrikiji/sveltekit-ffmpeg-example
今回作ったソースのリポジトリ

Discussion

ログインするとコメントできます