はじめに
ブラウザ上において、複数画像からSNSに投稿可能な動画形式(MP4,Mov)に変換するwebアプリを作りたいと思い、技術的・ライセンス的に調査・実験した結果をメモしておく。
結果としては・・・
動作環境としてはホームページサービスで利用しやすいよう、php+java scriptでの実装を検討した。
以下のようなことを試みた結果、webアプリの公開は断念。
-
ffmpeg.wasmライブラリ + java script
「ffmpeg.wasm」ライブラリを用いると、ブラウザ上でjava scriptのコーディングのみで動画ファイルを作成できることを確認。しかし、MPEG関連のライセンスが怖いのでwebアプリとしては公開できず。。。 -
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 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]で確認する)に出力されるようになる。
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']"
参考
- ffmpeg.wasm : https://github.com/ffmpegwasm/ffmpeg.wasm
- openh264(github) : https://www.openh264.org/
- emscripten : https://emscripten.org/
- MDN web docs – Web Assembly : https://developer.mozilla.org/ja/docs/WebAssembly
2024/04/29 (修正)
・hello world – 3にemscriptenの実行コマンドが抜けていたので追加