【java script】mp4動画ファイルのエンコードを試みる( ffmpeg.wasm , openh264 , emscripten )

Logo - © Emscripten authors 2010-2014 license

はじめに

ブラウザ上において、複数画像からSNSに投稿可能な動画形式(MP4,Mov)に変換するwebアプリを作りたいと思い、技術的・ライセンス的に調査・実験した結果をメモしておく。

結果としては・・・

動作環境としてはホームページサービスで利用しやすいよう、php+java scriptでの実装を検討した。
以下のようなことを試みた結果、webアプリの公開は断念。

  1. ffmpeg.wasmライブラリ + java script
    ffmpeg.wasm」ライブラリを用いると、ブラウザ上でjava scriptのコーディングのみで動画ファイルを作成できることを確認。しかし、MPEG関連のライセンスが怖いのでwebアプリとしては公開できず。。。

  2. openh264ライブラリ + web assembly(動的リンク) + java script
    MPEG関連のライセンスをCiscoが肩代わりしてくれている「openh264」ライブラリをweb asseemblyで実行(動的リンク)後、javascriptで取得できないかとひらめく。

    しかし、資料を漁り動作検証した結果、c言語からweb assemblyに変換するツール「emscripten」では、既存のcライブラリ(dllやso等)をリコンパイルなしに実行することはできないことが判明。「openh264」はリコンパイル版だと、Ciscoのライセンス肩代わりの枠組みから外れるため、この方法も断念。

webアプリは公開できないが、作業時間の供養のために調査メモを以下に残す。
途中記載があるシェルコマンドは、ubuntu distributionであるzorin osで動作確認した。

1.ffmpeg.wasmライブラリの利用

ffmpegをブラウザ上で動作するweb assemblyに変換し、javascriptから操作できるようにしたライブラリ。web assemblyへの変換にはemscriptenというツールを用いて機械的に行っているため、機能やコマンドオプションなどはffmpegと全く同じとなっている便利ライブラリ。

利用方法

「ffmpeg.min.js」の取得

「ffmpeg.wasm」を利用する場合、以下の方法で「ffmpeg.min.js」を取得する。
ちなみに、取得時にNode.jsを利用するが、ブラウザ上で動作する際にはNode.jsは不要。

# Node.jsをインストール
sudo apt update
sudo apt-get install nodejs
sudo apt-get install npm
sudo npm install -g n
sudo n latest
sudo npm update -g npm

# Node.jsでライブラリをインストール
npm install @ffmpeg/ffmpeg @ffmpeg/core
find ffmpeg.min.js

サンプルコード

複数画像からmp4やmovなどの動画ファイルを作成・ダウンロードするjava scriptは以下の通り。対応するレイアウトファイルはこちら

 /*----------------定数----------------------*/
var files   = null;    // 選択したファイル
var ffmpeg  = null;

 /*----------------関数----------------------*/
// ページの初期化
window.onload = async function(){
  // 拡張子選択時の処理を追加
  document.getElementById('in_extension').addEventListener('change', selectExtension );
  selectExtension();

  // 作成ボタン押下時の処理を追加
  document.getElementById('make_button').addEventListener('click', make );
}

// 拡張子選択時の処理
selectExtension = function (e) {
  // 選択ファイルのリセット
  document.getElementById("file_button").value = "";

  // ファイル制限
  let extension = document.getElementById("in_extension").value;
  document.getElementById("file_button").accept = extension;
}

// 画像作成
make = async function (e) {
  // 変数取得
  let fps   = document.getElementById("fps").value;
  let files = document.getElementById("file_button").files;
  let inExtension   = document.getElementById("in_extension").value.replace("image/","");
  let outExtension  = document.getElementById("out_extension").value;

  // 変数確認
  if ( fps <= 0 ){ alert("FPSの値が不正です"); return; }
  if ( files == null || files.length <= 0 ) { alert("ファイルを選択していません"); return; }
  if ( outExtension == null ) { alert("出力ファイルの拡張子を選択していません"); return; }

  // 処理開始
  let btn = document.getElementById("make_button");
  btn.disabled = true;
  btn.value    = "画像作成中";

  // 非同期関数として実行
  return await new Promise((resolve, reject) => {
    setTimeout( () => resolve( download(files,inExtension,outExtension,fps) ) , 0 ); }
  );
}

// ダウンロード
// 非同期関数として実行するために、無名関数として処理を記載
download = async (files,inExtension,outExtension,fps) => {
  // ファイルの並び替え
  let comp = ( e1 , e2 ) => {
    if( e1.name <  e2.name ){ return -1; }
    if( e1.name >  e2.name ){ return +1; }
    return 0;
  }
  let array = Array.from(files).sort( comp );

  // ffmpeg.wasmの初期化
  try{
    // インスタンスの取得
    const { createFFmpeg, fetchFile } = FFmpeg;
    const ffmpeg = createFFmpeg({ log: true });

    // 変換関数
    const image2video = async () => {
      // 必要なライブラリのダウンロード
      await ffmpeg.load();

      // 画像ファイルをSharedArrayBufferへ書き込み
      for (let i = 0; i < array.length ; i += 1) {
        const num = `00${i}`.slice(-3);
        ffmpeg.FS('writeFile', 'tmp.'+ num + '.' + inExtension , await fetchFile( array[i] ) );
      }

      // ffmpegコマンドの実行
      await ffmpeg.run( '-framerate', fps ,               // フレームレート
                        '-r' , fps ,                      // フレームレート
                        '-pattern_type', 'glob',          // 画像から動画作成
                        '-i', 'tmp.*.' + inExtension,     // 入力ファイル
                        '-shortest',                      // サイズが短いものに合わせる
                        'out.' + outExtension );          // 出力ファイル名

      // 作成した動画をSharedArrayBufferから読み込み
      const data = ffmpeg.FS('readFile', 'out.' + outExtension );

      // 不要になった画像ファイルをSharedArrayBufferから削除
      for (let i = 0; i < array.length ; i += 1) {
        const num = `00${i}`.slice(-3);
        ffmpeg.FS('unlink', 'tmp.' + num + '.' + inExtension );
      }

      // ダウンロード
      var a = document.createElement('a');
      a.href = URL.createObjectURL(new Blob([data.buffer], { type: 'video/'+outExtension }));
      a.download = 'output.' + outExtension;
      a.click();

    }
    await image2video();

  } catch (error) {
     alert(error);
  } finally {
    // ボタンを活性に戻す
    let btn = document.getElementById("make_button");
    btn.disabled = false;
    btn.value    = "アニメーション作成";
  }
}

2.emscriptenの利用

emscriptenとは、c等の言語で記載されたソースコードを基にブラウザ上で動作するweb assemblyコードを作成するツール。過去のソフトウェア遺産はc言語のものが多い気がするので、本当にすごいツール。

webサイトを見ると、dllやsoなどのc言語ライブラリの動的リンクに対応しているように見える。しかし、よくよく読んでいくとc言語をweb assemblyに変換後、dllやsoという拡張子のサイドモジュールとして保存。それを動的リンクすることができるよと書いている。openh264ライブラリをそのまま動的リンクできるかと考えたが、できない模様。。。惜しい!

QAでもメインメンバーの方がc言語ライブラリを動的リンクする機能はないと述べていることや、実際にdlopen関数でc言語ライブラリを動的リンクしようとすると、それっぽいエラー「Error: need to see wasm magic number」が出てくるので、間違いはなさそう。

とはいえ、ライセンス回避という今回の目的にそぐわないだけで、個人利用や研究や開発等うちわ向けでの利用では使えそうなので、以下備忘録。

利用方法

emsdkをインストールするフォルダで、以下を実行する。

# python最新化(emsdkに必要)
sudo apt-get install python3

# cmakeインストール(emsdkに必要)
sudo apt-get install cmake

# emsdkインストール
wget --no-check-certificate https://github.com/emscripten-core/emsdk/archive/master.tar.gz
tar xpvf master.tar.gz
cd emsdk-master
sudo ./emsdk update
sudo ./emsdk install latest
sudo ./emsdk activate latest
sudo chmod 755 emsdk_env.sh

# 環境変数の設定(コンソールを立ち上げるたびに都度実行する)
. $PWD/emsdk_env.sh

# 動作確認
emcc --version

サンプルコード

MDNのサイトを参考に作成したサンプルコードを以下に示す。

hello world – 1

「Hello World」を出力するcプログラムをweb assemblyに変換・実行し、結果をjava scriptで受け取るプログラム。

# 入力ファイルの作成
cat << EOF > hello.c
#include <stdio.h>

int main(int argc, char ** argv) {
  printf("Hello World\n");
}
EOF

# emscriptenの実行
emcc hello.c -s WASM=1 -o hello.html

結果、以下の3ファイルが生成される。

hello.html
hello.js
hello.wasm

「hello.html」はemscriptenが自動生成した画面で、「hello.wasm」はc言語をweb assemblyに変換したファイル、「hello.js」はjava scriptとweb assemblyをつなぐためのコード(glue code)となる。

上記ファイルをwebサーバに配置して表示すると、以下のようになる。

hello.html画面

hello world – 2

「Hello World」を出力するcプログラムをweb assemblyに変換・実行し、結果を指定したファイル(html)とjava scriptで受け取るプログラム。

# 入力ファイルの作成
cat << EOF > hello2.c
#include <stdio.h>\

int main(int argc, char ** argv) {
  printf("Hello World\n");
}
EOF

cat << EOF > template.html
<html>
<head>
</head>
<body>
test
{{{ SCRIPT }}}
</body>
</html>
EOF

# emscriptenの実行
emcc -o hello2.html hello2.c -O3 -s WASM=1 --shell-file template.html

出力されたファイルをwebサーバ上に配置・表示すると以下のように表示される。

「–shell-file」を指定したファイルには「{{{ SCRIPT }}}」とう記述が必要で、この文言がjava script(glue code)の呼び出しタグに置換される。また、「–shell-file」を指定するとemscriptenのコンソールなどの自動生成がなくなる。それに伴い、cプログラムでstdinに出力した内容は、コンソール(console.logの出力先、開発者ツール[F12]で確認する)に出力されるようになる。

hello02.htmlの出力

hello world – 3

以下は、cプログラムの関数をjava scriptから呼び出すサンプル。

# 入力ファイルの作成
cat << EOF > hello3.c
#include <stdio.h>
#include <emscripten/emscripten.h>

int main(int argc, char ** argv) {
    printf("Hello World\n");
}

#ifdef __cplusplus
extern "C" {
#endif

void EMSCRIPTEN_KEEPALIVE myFunction(int argc, char ** argv) {
  printf("MyFunction Called\n");
}

#ifdef __cplusplus
}
#endif
EOF

cat << EOF > template.html
<html>
<head>
</head>
<body>
<button class="mybutton">Run myFunction</button>
{{{ SCRIPT }}}
<script type='text/javascript'>
document.querySelector('.mybutton').addEventListener('click', function(){
  alert('check console');
  var result = Module.ccall('myFunction', // name of C function
                             null, // return type
                             null, // argument types
                             null); // arguments
});
</script>
</body>
</html>
EOF

# emscriptenの実行
emcc -o hello3.html hello3.c -O3 -s WASM=1 --shell-file template.html -s NO_EXIT_RUNTIME=1 -s "EXPORTED_RUNTIME_METHODS=['ccall']"

参考

  1. ffmpeg.wasm : https://github.com/ffmpegwasm/ffmpeg.wasm
  2. openh264(github) : https://www.openh264.org/
  3. emscripten : https://emscripten.org/
  4. MDN web docs – Web Assembly : https://developer.mozilla.org/ja/docs/WebAssembly

2024/04/29 (修正)
・hello world – 3にemscriptenの実行コマンドが抜けていたので追加

タイトルとURLをコピーしました