mirror of
https://code.videolan.org/videolan/vlc
synced 2024-09-04 09:11:33 +02:00
![Mehdi Sabwat](/assets/img/avatar_default.png)
Emscripten currently does not support picking a file from the DOM with the element, and reading it from a webassembly application. This access plugin will be able to receive a File object handle in the input thread, and read into buffers in linear memory with the file content while keeping track of the offset. A basic setup could look like : ``` <input type="file" id="fpicker_btn">Select File</input> <script> let instance = _libvlc_new(); let media_player_ptr = _libvlc_media_player_new(instance); /* Module is the emscripten runtime object, it has the vlc_access_file array as a property */ let inputElement = document.getElementById("fpicker_btn"); function handleFile() { var name = this.files.item(0).name; console.log("opened file: ", name); // id starts at 1 let id = 1; let path_ptr = Module.allocateUTF8("emjsfile://" + id); let media_ptr = _libvlc_media_new_location(path_ptr); Module._free(path_ptr); _libvlc_media_player_set_media(media_player_ptr, media_ptr); Module['vlc_access_file'][id] = this.files.item(0); } inputElement.addEventListener("change", handleFile, false); _libvlc_media_player_play(media_player_ptr); </script> ```
298 lines
9.9 KiB
C
298 lines
9.9 KiB
C
/*****************************************************************************
|
|
* emjsfile.c: emscripten js file access plugin
|
|
*****************************************************************************
|
|
* Copyright (C) 2022 VLC authors Videolabs, and VideoLAN
|
|
*
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU Lesser General Public License as published by
|
|
* the Free Software Foundation; either version 2.1 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public License
|
|
* along with this program; if not, write to the Free Software Foundation,
|
|
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
|
|
*****************************************************************************/
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
# include "config.h"
|
|
#endif
|
|
|
|
#include <vlc_common.h>
|
|
#include <vlc_plugin.h>
|
|
#include <vlc_access.h>
|
|
#include <vlc_threads.h>
|
|
|
|
#include <emscripten.h>
|
|
|
|
typedef struct
|
|
{
|
|
uint64_t offset;
|
|
uint64_t js_file_size;
|
|
} access_sys_t;
|
|
|
|
static ssize_t Read (stream_t *p_access, void *buffer, size_t size) {
|
|
access_sys_t *p_sys = p_access->p_sys;
|
|
|
|
size_t offset = p_sys->offset;
|
|
size_t js_file_size = p_sys->js_file_size;
|
|
|
|
if (offset >= js_file_size)
|
|
return 0;
|
|
if (size > offset + js_file_size) {
|
|
size = js_file_size - offset;
|
|
}
|
|
EM_ASM({
|
|
const offset = $0;
|
|
const buffer = $1;
|
|
const size = $2;
|
|
const blob = Module.vlcAccess[$3].worker_js_file.slice(offset, offset + size);
|
|
HEAPU8.set(new Uint8Array(Module.vlcAccess[$3].reader.readAsArrayBuffer(blob)), buffer);
|
|
}, offset, buffer, size, p_access);
|
|
p_sys->offset += size;
|
|
return size;
|
|
}
|
|
|
|
static int Seek (stream_t *p_access, uint64_t offset) {
|
|
access_sys_t *p_sys = p_access->p_sys;
|
|
|
|
p_sys->offset = offset;
|
|
return VLC_SUCCESS;
|
|
}
|
|
|
|
static int get_js_file_size(stream_t *p_access, uint64_t *value) {
|
|
/*
|
|
to avoid RangeError on BigUint64 view creation,
|
|
the start offset must be a multiple of 8.
|
|
*/
|
|
if ((uintptr_t)value % 8 != 0) {
|
|
msg_Err(p_access, "error: value is not aligned in get_js_file_size!");
|
|
return VLC_EGENERIC;
|
|
}
|
|
return (EM_ASM_INT({
|
|
try {
|
|
var v = new BigUint64Array(wasmMemory.buffer, $0, 1);
|
|
v[0] = BigInt(Module.vlcAccess[$1].worker_js_file.size);
|
|
return 0;
|
|
}
|
|
catch (error) {
|
|
console.error("get_js_file_size error: " + error);
|
|
return 1;
|
|
}
|
|
}, value, p_access) == 0) ? VLC_SUCCESS: VLC_EGENERIC;
|
|
}
|
|
|
|
static int Control( stream_t *p_access, int i_query, va_list args )
|
|
{
|
|
bool *pb_bool;
|
|
vlc_tick_t *pi_64;
|
|
access_sys_t *p_sys = p_access->p_sys;
|
|
|
|
switch( i_query )
|
|
{
|
|
case STREAM_CAN_SEEK:
|
|
case STREAM_CAN_FASTSEEK:
|
|
pb_bool = va_arg( args, bool * );
|
|
*pb_bool = true;
|
|
break;
|
|
|
|
case STREAM_CAN_PAUSE:
|
|
case STREAM_CAN_CONTROL_PACE:
|
|
pb_bool = va_arg( args, bool * );
|
|
*pb_bool = true;
|
|
break;
|
|
|
|
case STREAM_GET_SIZE:
|
|
{
|
|
*va_arg( args, uint64_t * ) = p_sys->js_file_size;
|
|
break;
|
|
}
|
|
|
|
case STREAM_GET_PTS_DELAY:
|
|
pi_64 = va_arg( args, vlc_tick_t * );
|
|
*pi_64 = VLC_TICK_FROM_MS(
|
|
var_InheritInteger (p_access, "file-caching") );
|
|
break;
|
|
|
|
case STREAM_SET_PAUSE_STATE:
|
|
break;
|
|
|
|
default:
|
|
return VLC_EGENERIC;
|
|
|
|
}
|
|
return VLC_SUCCESS;
|
|
}
|
|
|
|
EM_ASYNC_JS(int, init_js_file, (stream_t *p_access, long id), {
|
|
let p = new Promise((resolve, reject) => {
|
|
function handleFileResult(e) {
|
|
const msg = e['data'];
|
|
if (msg.type === 'FileResult') {
|
|
self.removeEventListener('message', handleFileResult);
|
|
if (msg.file !== undefined) {
|
|
Module.vlcAccess[p_access].worker_js_file = msg.file;
|
|
Module.vlcAccess[p_access].reader = new FileReaderSync();
|
|
resolve();
|
|
}
|
|
else {
|
|
reject("error: sent an undefined File object from the main thread");
|
|
}
|
|
}
|
|
else if (msg.type === 'ErrorVLCAccessFileUndefined') {
|
|
reject("error: vlc_access_file object is not defined");
|
|
}
|
|
else if (msg.type === 'ErrorRequestFileMessageWithoutId') {
|
|
reject("error: request file message send without an id");
|
|
}
|
|
else if (msg.type === 'ErrorMissingFile') {
|
|
reject("error: missing file, bad id or vlc_access_file[id] is not defined");
|
|
}
|
|
}
|
|
self.addEventListener('message', handleFileResult);
|
|
});
|
|
let timer = undefined;
|
|
let timeout = new Promise(function (resolve, reject) {
|
|
timer = setTimeout(resolve, 1000, 'timeout')
|
|
});
|
|
let promises = [p, timeout];
|
|
/* id must be unique */
|
|
self.postMessage({ cmd: "customCmd", type: "requestFile", id: id});
|
|
let return_value = 0;
|
|
try {
|
|
let value = await Promise.race(promises);
|
|
if (value === 'timeout') {
|
|
console.error("vlc_access timeout: could not get file!");
|
|
return_value = 1;
|
|
}
|
|
}
|
|
catch(error) {
|
|
console.error("vlc_access error in init_js_file(): ", error);
|
|
return_value = 1;
|
|
}
|
|
clearTimeout(timer);
|
|
return return_value;
|
|
});
|
|
|
|
static int EmFileOpen( vlc_object_t *p_this ) {
|
|
stream_t *p_access = (stream_t*)p_this;
|
|
|
|
/* init per worker module.vlcAccess object */
|
|
EM_ASM({
|
|
if (Module.vlcAccess === undefined) {
|
|
Module.vlcAccess = {};
|
|
}
|
|
(Module.vlcAccess[$0] = {worker_js_file: undefined, reader: undefined});
|
|
}, p_access);
|
|
|
|
/*
|
|
This block will run in the main thread, to access the DOM.
|
|
When the user selects a file, it is assigned to the Module.vlc_access_file
|
|
array.
|
|
|
|
We listen to 'message' events so that when the file is requested by the
|
|
input thread, we can answer and send the File object from the main thread.
|
|
*/
|
|
MAIN_THREAD_EM_ASM({
|
|
const thread_id = $0;
|
|
let w = Module.PThread.pthreads[thread_id].worker;
|
|
function handleFileRequest(e) {
|
|
const msg = e.data;
|
|
if (msg.type === "requestFile") {
|
|
w.removeEventListener('message', handleFileRequest);
|
|
if (Module.vlc_access_file === undefined) {
|
|
console.error("vlc_access_file property missing!");
|
|
w.postMessage({ cmd: "customCmd",
|
|
type: "ErrorVLCAccessFileUndefined"
|
|
});
|
|
return ;
|
|
}
|
|
if (msg.id === undefined) {
|
|
console.error("id property missing in requestFile message!");
|
|
w.postMessage({ cmd: "customCmd",
|
|
type: "ErrorRequestFileMessageWithoutId"
|
|
});
|
|
return ;
|
|
}
|
|
if (Module.vlc_access_file[msg.id] === undefined) {
|
|
console.error("error file missing!");
|
|
w.postMessage({ cmd: "customCmd",
|
|
type: "ErrorMissingFile"
|
|
});
|
|
return ;
|
|
}
|
|
/*
|
|
keeping the File object in the main thread too,
|
|
in case we want to reopen the media, without re-selecting
|
|
the file.
|
|
*/
|
|
w.postMessage({ cmd: "customCmd",
|
|
type: "FileResult",
|
|
file: Module.vlc_access_file[msg.id]
|
|
});
|
|
}
|
|
}
|
|
w.addEventListener('message', handleFileRequest);
|
|
}, pthread_self());
|
|
|
|
char *endPtr;
|
|
long id = strtol(p_access->psz_location, &endPtr, 10);
|
|
if ((endPtr == p_access->psz_location) || (*endPtr != '\0')) {
|
|
msg_Err(p_access, "error: failed init uri has invalid id!");
|
|
return VLC_EGENERIC;
|
|
}
|
|
|
|
/*
|
|
Request the file from the main thread.
|
|
If it was not selected, it will return an error.
|
|
|
|
To open a file, we need to call libvlc_media_new_location with
|
|
the following uri : emjsfile://<id>
|
|
To avoid confusion with atoi() return error, id starts at 1.
|
|
*/
|
|
if (init_js_file(p_access, id)) {
|
|
msg_Err(p_access, "EMJsFile error: failed init!");
|
|
return VLC_EGENERIC;
|
|
}
|
|
|
|
access_sys_t *p_sys = vlc_obj_malloc(p_this, sizeof (*p_sys));
|
|
if (unlikely(p_sys == NULL))
|
|
return VLC_ENOMEM;
|
|
|
|
p_access->pf_read = Read;
|
|
p_access->pf_block = NULL;
|
|
p_access->pf_control = Control;
|
|
p_access->pf_seek = Seek;
|
|
p_access->p_sys = p_sys;
|
|
p_sys->js_file_size = 0;
|
|
p_sys->offset = 0;
|
|
if (get_js_file_size(p_access, &p_sys->js_file_size)) {
|
|
msg_Err(p_access, "EMJsFile error: could not get file size!");
|
|
return VLC_EGENERIC;
|
|
}
|
|
|
|
return VLC_SUCCESS;
|
|
}
|
|
|
|
static void EmFileClose (vlc_object_t * p_this) {
|
|
stream_t *p_access = (stream_t*)p_this;
|
|
EM_ASM({
|
|
Module.vlcAccess[$0].worker_js_file = undefined;
|
|
Module.vlcAccess[$0].reader = undefined;
|
|
}, p_access);
|
|
}
|
|
|
|
vlc_module_begin ()
|
|
set_description( N_("Emscripten module to allow reading local files from the DOM's <input>") )
|
|
set_shortname( N_("Emscripten Local File Input") )
|
|
set_subcategory( SUBCAT_INPUT_ACCESS )
|
|
set_capability( "access", 0 )
|
|
add_shortcut( "emjsfile" )
|
|
set_callbacks( EmFileOpen, EmFileClose )
|
|
vlc_module_end()
|