1
mirror of https://code.videolan.org/videolan/vlc synced 2024-09-04 09:11:33 +02:00
vlc/modules/access/emjsfile.c
Mehdi Sabwat 9d2f5d6125 emscripten: add js File access plugin
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>
```
2022-08-07 11:43:42 +00:00

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()