LoginSignup
4
1

More than 5 years have passed since last update.

emscriptenでpthreadが有効なlibvpx.jsを作る

Last updated at Posted at 2017-03-10

emscriptenでpthreadが有効なlibvpx.jsを作る

前回はemscriptenでlibvpxをasm.jsにしました。今回はpthreadに対応します。

configure へのパッチ

configure.patch
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 へのビルド

build.sh
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とのインターフェースを書く

decode.c
#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で書いておきます。

exported_functions.json
[
  "_init",
  "_decode",
  "_destroy",
  "_show_frame",
  "_width",
  "_height",
  "_mb_rows",
  "_mb_cols",
  "_mode_info_stride",
  "_mbmi_ref_frame",
  "_mbmi_mode"
]

emcc で asm.js を出力します。

build.sh
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 をデコードします。

index.html
<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 指定すれば動きます。

これで高解像度の動画も安心してデコードできますね!

作業レポジトリ

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