Merge pull request #7 from alfg/refactor/worker-module-calls

Refactor module calls into a Web Worker to prevent blocking main thread
This commit is contained in:
Alfred Gutierrez
2020-12-22 14:40:48 -08:00
committed by GitHub
12 changed files with 141 additions and 94 deletions

View File

@@ -1,4 +1,4 @@
FROM emscripten/emsdk as build FROM emscripten/emsdk:2.0.11 as build
ARG FFMPEG_VERSION=4.3.1 ARG FFMPEG_VERSION=4.3.1
ARG X264_VERSION=20170226-2245-stable ARG X264_VERSION=20170226-2245-stable
@@ -30,7 +30,7 @@ RUN cd /tmp/ && \
wget http://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.gz && \ wget http://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.gz && \
tar zxf ffmpeg-${FFMPEG_VERSION}.tar.gz && rm ffmpeg-${FFMPEG_VERSION}.tar.gz tar zxf ffmpeg-${FFMPEG_VERSION}.tar.gz && rm ffmpeg-${FFMPEG_VERSION}.tar.gz
ARG CFLAGS="-s USE_PTHREADS -O3 -I${PREFIX}/include" ARG CFLAGS="-s USE_PTHREADS=1 -O3 -I${PREFIX}/include"
ARG LDFLAGS="$CFLAGS -L${PREFIX}/lib -s INITIAL_MEMORY=33554432" ARG LDFLAGS="$CFLAGS -L${PREFIX}/lib -s INITIAL_MEMORY=33554432"
# Compile ffmpeg. # Compile ffmpeg.

View File

@@ -6,7 +6,8 @@ dist/ffprobe-wasm.js:
-I/opt/ffmpeg/include/ \ -I/opt/ffmpeg/include/ \
-s EXTRA_EXPORTED_RUNTIME_METHODS="[FS, cwrap, ccall, getValue, setValue, writeAsciiToMemory]" \ -s EXTRA_EXPORTED_RUNTIME_METHODS="[FS, cwrap, ccall, getValue, setValue, writeAsciiToMemory]" \
-s INITIAL_MEMORY=268435456 \ -s INITIAL_MEMORY=268435456 \
-lavcodec -lavformat -lavfilter -lavdevice -lswresample -lswscale -lavutil -lm -lx264 \
-pthread \ -pthread \
-lavcodec -lavformat -lavfilter -lavdevice -lswresample -lswscale -lavutil -lm -lx264 -pthread \ -lworkerfs.js \
-o dist/ffprobe-wasm.js \ -o dist/ffprobe-wasm.js \
src/ffprobe-wasm-wrapper.cpp src/ffprobe-wasm-wrapper.cpp

View File

@@ -1,5 +1,6 @@
#include <vector> #include <vector>
#include <string> #include <string>
#include <vector>
#include <inttypes.h> #include <inttypes.h>
#include <emscripten.h> #include <emscripten.h>
#include <emscripten/bind.h> #include <emscripten/bind.h>
@@ -65,8 +66,8 @@ typedef struct FramesResponse {
int nb_frames; int nb_frames;
} FramesResponse; } FramesResponse;
FileInfoResponse get_file_info() { FileInfoResponse get_file_info(std::string filename) {
FILE *file = fopen("file", "rb"); FILE *file = fopen(filename.c_str(), "rb");
if (!file) { if (!file) {
printf("cannot open file\n"); printf("cannot open file\n");
} }
@@ -79,9 +80,8 @@ FileInfoResponse get_file_info() {
// Open the file and read header. // Open the file and read header.
int ret; int ret;
if ((ret = avformat_open_input(&pFormatContext, "file", NULL, NULL)) < 0) { if ((ret = avformat_open_input(&pFormatContext, filename.c_str(), NULL, NULL)) < 0) {
printf("ERROR: could not open the file. Error: %d\n", ret); printf("ERROR: %s\n", av_err2str(ret));
printf("%s", av_err2str(ret));
} }
// Get stream info from format. // Get stream info from format.
@@ -135,10 +135,10 @@ FileInfoResponse get_file_info() {
return r; return r;
} }
FramesResponse get_frames(int offset) { FramesResponse get_frames(std::string filename, int offset) {
av_log_set_level(AV_LOG_QUIET); // No logging output for libav. av_log_set_level(AV_LOG_QUIET); // No logging output for libav.
FILE *file = fopen("file", "rb"); FILE *file = fopen(filename.c_str(), "rb");
if (!file) { if (!file) {
printf("cannot open file\n"); printf("cannot open file\n");
} }
@@ -151,9 +151,8 @@ FramesResponse get_frames(int offset) {
// Open the file and read header. // Open the file and read header.
int ret; int ret;
if ((ret = avformat_open_input(&pFormatContext, "file", NULL, NULL)) < 0) { if ((ret = avformat_open_input(&pFormatContext, filename.c_str(), NULL, NULL)) < 0) {
printf("ERROR: could not open the file. Error: %d\n", ret); printf("ERROR: %s\n", av_err2str(ret));
printf("%s", av_err2str(ret));
} }
// Get stream info from format. // Get stream info from format.
@@ -239,7 +238,6 @@ FramesResponse get_frames(int offset) {
return r; return r;
} }
EMSCRIPTEN_BINDINGS(constants) { EMSCRIPTEN_BINDINGS(constants) {
function("avformat_version", &c_avformat_version); function("avformat_version", &c_avformat_version);
function("avcodec_version", &c_avcodec_version); function("avcodec_version", &c_avcodec_version);

2
www/.gitignore vendored
View File

@@ -22,5 +22,5 @@ pnpm-debug.log*
*.sln *.sln
*.sw? *.sw?
public/*.js public/*-wasm*.js
public/*.wasm public/*.wasm

2
www/package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "ffprobe-wasm", "name": "ffprobe-wasm",
"version": "0.2.1", "version": "0.3.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "ffprobe-wasm", "name": "ffprobe-wasm",
"version": "0.2.1", "version": "0.3.0",
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
"build": "vue-cli-service build", "build": "vue-cli-service build",

View File

@@ -0,0 +1,72 @@
onmessage = (e) => {
const type = e.data[0];
const file = e.data[1];
let data;
switch (type) {
case 'get_file_info':
// Mount FS for files.
if (!FS.analyzePath('/work').exists) {
FS.mkdir('/work');
}
FS.mount(WORKERFS, { files: [file] }, '/work');
// Call the wasm module.
const info = Module.get_file_info('/work/' + file.name);
// Remap streams into collection.
const s = [];
for (let i = 0; i < info.streams.size(); i++) {
s.push(info.streams.get(i));
}
const versions = {
libavutil: Module.avutil_version(),
libavcodec: Module.avcodec_version(),
libavformat: Module.avformat_version(),
};
// Send back data response.
data = {
...info,
streams: s,
versions,
}
postMessage(data);
// Cleanup mount.
FS.unmount('/work');
break;
case 'get_frames':
if (!FS.analyzePath('/work').exists) {
FS.mkdir('/work');
}
FS.mount(WORKERFS, { files: [file] }, '/work');
const offset = e.data[2];
const frames = Module.get_frames('/work/' + file.name, offset);
// Remap frames into collection.
const f = [];
for (let i = 0; i < frames.frames.size(); i++) {
f.push(frames.frames.get(i));
}
data = {
...frames,
frames: f,
}
postMessage(data);
// Cleanup mount.
FS.unmount('/work');
break;
default:
break;
}
}
self.importScripts('ffprobe-wasm.js'); // Load ffprobe into worker context.

View File

@@ -12,7 +12,6 @@
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript> </noscript>
<div id="app"></div> <div id="app"></div>
<script src="ffprobe-wasm.js"></script>
<!-- built files will be auto injected --> <!-- built files will be auto injected -->
</body> </body>
</html> </html>

View File

@@ -12,23 +12,17 @@
></b-form-file> ></b-form-file>
</b-form-group> </b-form-group>
<b-progress <div v-if="info">
height="2px"
v-if="showProgress"
:value="progress"
max="100"></b-progress>
<div v-if="data">
<div class="mt-3">Selected file: {{ file ? `${file.name}: ${file.size} bytes` : '' }}</div> <div class="mt-3">Selected file: {{ file ? `${file.name}: ${file.size} bytes` : '' }}</div>
<b-tabs class="mt-4"> <b-tabs class="mt-4">
<b-tab title="Overview" class="mt-2"> <b-tab title="Overview" class="mt-2">
<div v-if="data"> <div v-if="info">
<Overview :info="info" /> <Overview :info="info" />
</div> </div>
</b-tab> </b-tab>
<b-tab title="Frames" class="mt-2" lazy> <b-tab title="Frames" class="mt-2" lazy>
<Frames /> <Frames :file="file" />
</b-tab> </b-tab>
</b-tabs> </b-tabs>
</div> </div>
@@ -49,37 +43,19 @@ export default {
return { return {
file: null, file: null,
data: null, data: null,
progress: 0, info: null,
showProgress: false,
} }
}, },
computed: { created() {
info() { this.$worker.onmessage = (e) => {
return this.data && window.Module.get_file_info(); this.info = e.data;
} }
}, },
methods: { methods: {
onFile(event) { onFile(event) {
this.data = null; this.data = null;
this.progress = 0;
this.showProgress = true;
const file = event.dataTransfer ? event.dataTransfer.files[0] : event.target.files[0]; const file = event.dataTransfer ? event.dataTransfer.files[0] : event.target.files[0];
const reader = new FileReader(); this.$worker.postMessage([ 'get_file_info', file ]);
// reader.onload = e => this.$emit("load", event.target.result);
reader.onload = (event) => {
this.progress = 100;
this.data = new Uint8Array(event.target.result);
window.Module.FS.writeFile('file', new Uint8Array(this.data));
setTimeout(() => { this.showProgress = false; }, 2000);
}
reader.onprogress = (event) => {
if (event.lengthComputable) {
this.progress = parseInt(((event.loaded / event.total) * 100), 10);
}
}
reader.readAsArrayBuffer(file);
} }
} }
} }

View File

@@ -1,6 +1,8 @@
<template> <template>
<div> <div>
<h4>Frames</h4> <h4>Frames</h4>
<div v-if="!data">Loading...</div>
<div v-if="data">
<p class="text-right">Total: {{ data.nb_frames }}</p> <p class="text-right">Total: {{ data.nb_frames }}</p>
<b-pagination <b-pagination
@@ -11,7 +13,12 @@
align="right" align="right"
></b-pagination> ></b-pagination>
<b-table striped hover :items="frames"> <b-table striped hover :items="data.frames" :busy="isBusy">
<template #table-busy>
<div class="text-center text-primary my-2">
<b-spinner class="align-middle"></b-spinner>
</div>
</template>
<template #cell(pict_type)="data"> <template #cell(pict_type)="data">
{{ String.fromCharCode(data.value) }} {{ String.fromCharCode(data.value) }}
</template> </template>
@@ -25,38 +32,38 @@
align="right" align="right"
></b-pagination> ></b-pagination>
</div> </div>
</div>
</template> </template>
<script> <script>
export default { export default {
name: 'Frames', name: 'Frames',
props: ['file'],
data() { data() {
return { return {
data: null, data: null,
currentPage: 1, currentPage: 1,
perPage: 48, perPage: 48,
isBusy: false,
}; };
}, },
computed: { computed: {
pages() { pages() {
return this.data.nb_frames; return this.data.nb_frames;
}, },
frames() {
if (!this.data) return [];
const s = [];
for (let i = 0; i < this.data.frames.size(); i++) {
s.push(this.data.frames.get(i));
}
return s;
},
}, },
created() { created() {
this.data = window.Module.get_frames(0); this.$worker.onmessage = (e) => {
this.data = e.data;
this.isBusy = false;
}
this.$worker.postMessage([ 'get_frames', this.file, 0 ]);
}, },
methods: { methods: {
onPageChanged(page) { onPageChanged(page) {
this.data = window.Module.get_frames(this.perPage * (page - 1)); this.isBusy = true;
this.$worker.postMessage([ 'get_frames', this.file, this.perPage * (page - 1) ]);
window.scrollTo(0, 0); window.scrollTo(0, 0);
}, },
} }

View File

@@ -7,7 +7,7 @@
<b-table stacked :items="items"></b-table> <b-table stacked :items="items"></b-table>
<h4>Streams</h4> <h4>Streams</h4>
<b-table striped hover :items="streams"></b-table> <b-table striped hover :items="info.streams"></b-table>
</div> </div>
</template> </template>
@@ -15,15 +15,6 @@
export default { export default {
name: 'Overview', name: 'Overview',
props: ['info'], props: ['info'],
computed: {
streams() {
const s = [];
for (let i = 0; i < this.info.streams.size(); i++) {
s.push(this.info.streams.get(i));
}
return s;
}
},
data() { data() {
return { return {
items: [ items: [
@@ -38,9 +29,9 @@ export default {
], ],
versions: [ versions: [
{ {
libavutil: window.Module.avutil_version(), libavutil: this.info.versions.libavutil,
libavcodec: window.Module.avcodec_version(), libavcodec: this.info.versions.libavcodec,
libavformat: window.Module.avformat_version(), libavformat: this.info.versions.libavformat,
} }
] ]
} }

View File

@@ -5,6 +5,9 @@ import App from './App.vue'
import 'bootstrap/dist/css/bootstrap.css'; import 'bootstrap/dist/css/bootstrap.css';
import 'bootstrap-vue/dist/bootstrap-vue.css'; import 'bootstrap-vue/dist/bootstrap-vue.css';
const worker = new Worker('ffprobe-worker.js');
Vue.prototype.$worker = worker;
Vue.use(BootstrapVue); Vue.use(BootstrapVue);
Vue.config.productionTip = false Vue.config.productionTip = false