emscriptenでpthreadが有効なlibvpx.jsを作る
前回はemscriptenでlibvpxをasm.jsにしました。今回はpthreadに対応します。
configure へのパッチ
diff --git a/build/make/configure.sh b/build/make/configure.sh
index ac60f50..72f6db9 100644
--- a/build/make/configure.sh
+++ b/build/make/configure.sh
@@ -456,7 +456,7 @@ NM=${NM}
CFLAGS = ${CFLAGS}
CXXFLAGS = ${CXXFLAGS}
-ARFLAGS = -crs\$(if \$(quiet),,v)
+ARFLAGS = crs\$(if \$(quiet),,v)
LDFLAGS = ${LDFLAGS}
ASFLAGS = ${ASFLAGS}
extralibs = ${extralibs}
@@ -1337,6 +1337,21 @@ EOF
;;
esac
;;
+ asmjs-unknown-emscripten)
+ # echo "@ " $toolchain " = asmjs-unknown-emscripten""
+ # echo "@ " $tgt_isa "-" $tgt_os "-" $tgt_cc
+ CC=emcc
+ LD=llvm-link
+ AR=llvm-ar
+ AS=llvm-as
+ NM=llvm-nm
+ tune_cflags=""
+ tune_asflags=""
+ add_cflags -emit-llvm
+ #add_ldflags
+ #add_asflags
+ HAVE_GNU_STRIP=no
+ ;;
*-gcc|generic-gnu)
link_with_cc=gcc
enable_feature gcc
diff --git a/configure b/configure
index 379c2f4..deb0965 100755
--- a/configure
+++ b/configure
@@ -155,6 +155,7 @@ all_platforms="${all_platforms} x86_64-win64-vs11"
all_platforms="${all_platforms} x86_64-win64-vs12"
all_platforms="${all_platforms} x86_64-win64-vs14"
all_platforms="${all_platforms} generic-gnu"
+all_platforms="${all_platforms} asmjs-unknown-emscripten"
# all_targets is a list of all targets that can be configured
# note that these should be in dependency order for now.
diff --git a/vpx/src/vpx_codec.c b/vpx/src/vpx_codec.c
index 10331aa..15b81c1 100644
--- a/vpx/src/vpx_codec.c
+++ b/vpx/src/vpx_codec.c
@@ -124,6 +124,7 @@ void vpx_internal_error(struct vpx_internal_error_info *info,
info->has_detail = 1;
va_start(ap, fmt);
+ printf("vpx_internal_error: %s\n", fmt);
vsnprintf(info->detail, sz - 1, fmt, ap);
va_end(ap);
info->detail[sz - 1] = '\0';
llvm bitcode へのビルド
emconfigure ./configure \
--disable-runtime-cpu-detect \
--disable-examples \
--disable-docs \
--disable-vp9 \
--disable-vp8-encoder \
--disable-unit_tests \
--disable-install_bins \
--disable-install_libs \
--disable-encode_perf_tests \
--disable-decode_perf_tests \
--target=asmjs-unknown-emscripten \
--extra-cflags="-O2"
emmake make
ここまでは前回とほとんど同じで libvpx.a
なる llvm bitcode が生成されます。
違いは --disable-multithread
していないだけです。
JSとのインターフェースを書く
# include "../libvpx/vpx/vpx_decoder.h"
# include "../libvpx/vpx/internal/vpx_codec_internal.h"
# include "../libvpx/vp8/decoder/onyxd_int.h"
# include "../libvpx/vp8/vp8_dx_iface.c"
# include <stdio.h>
vpx_codec_ctx_t codec;
vpx_codec_dec_cfg_t cfg;
void die_codec(vpx_codec_ctx_t *ctx, const char *s) {
const char *detail = vpx_codec_error_detail(ctx);
printf("%s: %s\n", s, vpx_codec_error(ctx));
if (detail) printf(" %s\n", detail);
exit(1);
}
int init(int argc, char **argv) {
printf("Using %s\n", vpx_codec_iface_name(vpx_codec_vp8_dx()));
cfg.threads = 4;
if (vpx_codec_dec_init(&codec, vpx_codec_vp8_dx(), &cfg, 0)){
die_codec(&codec, "Failed to initialize decoder.");
}
return 0;
}
int decode(const unsigned char *frame, size_t frame_size){
printf("frame_size: %d \n",(unsigned int)frame_size);
if (vpx_codec_decode(&codec, frame, (unsigned int)frame_size, NULL, 0)){
die_codec(&codec, "Failed to decode frame.");
}
return 0;
}
int destroy(void){
if (vpx_codec_destroy(&codec)){
die_codec(&codec, "Failed to destroy codec");
}
return 0;
}
# define CM ((vpx_codec_alg_priv_t *)codec.priv)->yv12_frame_buffers.pbi[0]->common
int show_frame(){ return CM.show_frame; }
int width(){ return CM.Width; }
int height(){ return CM.Height; }
int mb_rows(){ return CM.mb_rows; }
int mb_cols(){ return CM.mb_cols; }
int mode_info_stride(){ return CM.mode_info_stride; }
# define MI(NAME, OP) \
NAME (int row, int col){ \
const int idx = row * CM.mode_info_stride + col; \
MODE_INFO *mi = &CM.mi[idx]; \
return (OP); \
}
int MI(mbmi_ref_frame, mi->mbmi.ref_frame)
int MI(mbmi_mode, mi->mbmi.mode)
init
でデコーダを初期化し、 decode
でVP8 bitstream の毎フレームをデコードし、
各種アクセサ関数を定義してフレーム情報の入った構造体へアクセスします。
今回はフレームごとの各マクロブロックの情報を取得します。
cfg.threads = 4;
でデコーダが使うスレッドを4つに指定しています。
pthreadを有効にした asm.jsへのビルド
JSからアクセスする関数をJSONで書いておきます。
[
"_init",
"_decode",
"_destroy",
"_show_frame",
"_width",
"_height",
"_mb_rows",
"_mb_cols",
"_mode_info_stride",
"_mbmi_ref_frame",
"_mbmi_mode"
]
emcc で asm.js を出力します。
exported_functions=$(node --eval "console.log(JSON.stringify(require('./exported_functions.json')).split('\"').join('\''))
emcc \
-s USE_PTHREADS=1 -s PTHREAD_POOL_SIZE=4 \
-s TOTAL_MEMORY=67108864 \
-s SIMD=1 \
-O2 --llvm-lto 3 \
-s EXPORTED_FUNCTIONS="$exported_functions" \
-I. \
./decode.c libvpx.a \
-o ./decode.js
pthread を使うには -s USE_PTHREADS=1
します。
-s PTHREAD_POOL_SIZE=4
でスレッドの最大数も指定できます。
ビデオのデコードは解像度に比例してヒープを消費するので -s TOTAL_MEMORY
に大きめのヒープサイズを指定しています。
さらなる最適化を期待して llvm bitcode オブジェクトファイル結合後に最適化をかけ、その上で linktime optimization をする --llvm-lto 3
を指定します。
これで decode.js
decode.mem
が生成されます。
JSから呼び出す
MediaRecorder の作る WebM をデコードします。
<script src="decode.js"></script>
<script src="EBML.js"></script>
<script src="EBMLReader.js"></script>
<script>
//window.onload = (()=>{ // .mem がないとき
Module.addOnPostRun(()=>{ // .mem があるとき
// デコーダの準備
const decoder = new EBML.Decoder(); // webm コンテナデコーダ
const reader = new EBMLReader();
Module.ccall("init", "number", ["number", "*"], []); // vpx デコーダ
navigator.mediaDevices.getUserMedia({
audio: true,
video: true
}).then((stream)=>{
const rec = new MediaRecorder(stream, { mimeType: 'video/webm; codecs="vp8, opus"' });
let task = Promise.resolve();
rec.ondataavailable = (ev)=>{
task.then(()=> readAsArrayBuffer(ev.data).then((buffer)=>{
decoder.decode(buffer).forEach((elm)=>{
reader.read(elm);
});
}) );
};
reader.addListener("simpleblock_video", ({elm, data})=>{
// video の入った Simple Block のとき
// SimpleBlock には複数の frame が入っている場合がある
data.frames.forEach((frame)=>{
// malloc してvp8 bitstream を asm.js のヒープへ書き込む
const size = frame.length;
const buf = Module._malloc(size);
Module.HEAP8.set(frame, buf);
// 配列ポインタ渡しでデコード
console.time("decode");
Module.ccall("decode", "number", ["*", "number"], [buf, size]);
console.timeEnd("decode");
// メモリ解放
Module._free(buf);
// フレーム情報を得る
const width = Module.ccall("width", "number", [], []);
const height = Module.ccall("height", "number", [], []);
const rows = Module.ccall("mb_rows", "number", [], []);
const cols = Module.ccall("mb_cols", "number", [], []);
for (let row = 0; row < rows; ++row) {
for (let col = 0; col < cols; ++col) {
// マクロブロックごとの予測モード
const mode = Module.ccall("_mbmi_mode", "number", ["number", "number"], [row, col]);
}
}
});
});
rec.start(100);
setTimeout(()=>{
// 録画停止
rec.stop();
rec.ondataavailable = undefined;
rec.stream.getTracks().map((track) => { track.stop(); });
}, 10 * 1000);
});
});
function readAsArrayBuffer(blob) {
return new Promise((resolve, reject)=>{
const reader = new FileReader();
reader.readAsArrayBuffer(blob);
reader.onloadend = ()=>{ resolve(reader.result); };
reader.onerror = (ev)=>{ reject(ev.error); };
});
}
</script>
EBML.js, EBMLReader.js は 拙作WebMコンテナパーサhttps://github.com/legokichi/ts-ebml/ です。
注意点として *.mem
があるときと --memory-init-file 0
でメモリファイルないときでJS側のエントリポイントが異なります。
メモリファイルがある場合、asm.jsは非同期に実行されるため、Module.addOnPostRun(()=>{});
でイベントを登録します。
メモリファイルがない場合は <script />
が上から順番に同期的に実行されるため、 とりあえずbodyが読み終わる window.onload
をエントリポイントにしています。
ブラウザで動かす
Chrome でも SharedArrayBuffer を使えるようになりました。chrome:flags#shared-array-buffer
拡張を有効にしてください。
現在の emscripten で生成されるpthreadを使用したコードをchromeで動かすとUncaught DOMException: Failed to execute 'postMessage' on 'Worker': A SharedArrayBuffer could not be cloned.
エラーが発生します。
chrome では shared array buffer を postMessage で webworker に渡すときに 暗黙に transferable に指定されないバグ?のためです。
decode.js の worker.postMessage({cmd:"load",..., buffer:HEAPU8.buffer,...});
を worker.postMessage({cmd:"load",buffer:HEAPU8.buffer,...}, [HEAPU8.buffer]);
として SharedArrayBuffer である HEAP のバッファを transferable 指定すれば動きます。
これで高解像度の動画も安心してデコードできますね!