blob: e3b73d16c0b83479a869bf7b402316dad891a3a6 [file] [edit]
const gpac = require('..');
const filesys = require('fs');
//run in blocking mode
const RUN_SYNC=0;
//run in step mode but blocking node eventloop
const RUN_STEP=1;
//run in step mode and not blocking node eventloop
const RUN_PROMISE=2;
//test simple session, no custom filters
const TEST_SIMPLE=0;
//test sink
const TEST_SINK=1;
//test forward
const TEST_FORWARD=2;
let run_mode=RUN_SYNC;
let test_task=false;
let run_task_dur=0;
let test_type=TEST_SIMPLE;
let FILTERS=[];
let SINKS=[];
let SRCS=[];
let memtrack=0;
let is_verbose=false;
let start_time=0;
let custom_dash=false;
let custom_httpout=false;
let fileio_sources=false;
let fileio_sinks=false;
const PCKFWD_REF=0;
const PCKFWD_ALLOC=1;
const PCKFWD_SHARED=2;
const PCKFWD_CLONE=3;
const PCKFWD_COPY=4;
const PCKFWD_FWD=5;
const PCKFWD_ALL=6;
let pck_fwd_mode=PCKFWD_FWD;
print_usage = () => {
console.log("node gpac.js [options]");
console.log();
console.log("This scripts tests usage of GPAC NodeJS bindings.\n\nPossible options are:");
console.log("-run=MODE: runs in given MODE:");
console.log("\tsync (default): blocking filter session, Node EventLoop blocked");
console.log("\tstep: non-blocking filter session, Node EventLoop blocked");
console.log("\tasync: non-blocking filter session as promise, Node EventLoop active");
console.log("-cust=MODE: custom JS filter test mode:");
console.log("\tsimple (default): runs source(s), sink(s) and filters");
console.log("\tsink: runs source(s) into custom JS filter sink");
console.log("\tfwd: runs source(s) into custom JS filter forwarding to sink(s) (-dst=) or to filter(s) (-f=)");
console.log("-fwd=MODE: packet forward mode for forward test mode:");
console.log("\tfwd (default): uses packet forwarding");
console.log("\tref: uses packet reference");
console.log("\talloc: copies data");
console.log("\tshared: use shared JS data and JS destruction callbacks");
console.log("\tclone: uses packet cloning");
console.log("\tcopy: uses packet copy");
console.log("\tall: cycles through all above modes");
console.log("-src=URL: adds a source URL. Can be set multiple times for multiple sources");
console.log("-dst=URL: adds a destination URL. Can be set multiple times for multiple sinks");
console.log("-f=FILTER: adds a filter (e.g. 'avgen'). Can be set multiple times for filters, cf `gpac -h doc` for link options");
console.log("-mt: enables GPAC memory tracking");
console.log("-p=PROF: sets profile to PROF");
console.log("-ib: enables FileIO wrapper for source(s)");
console.log("-ob: enables FileIO wrapper for sink(s)");
console.log("-task=N: runs user test task every second for N seconds (0 means until end)");
console.log("-play=N: overrides PLAY event in forward mode to the given start time in seconds");
console.log("-cdash: test custom DASH algo");
console.log("-chttp: test custom HTTP request handler");
console.log("-v: enables info dump, property dump for PIDs and packets");
console.log("-h: prints help");
console.log();
console.log("Other options will be passed to GPAC");
console.log();
console.log("Examples");
console.log("- Play MyURL to aout and vout:\nnode gpac.js -src=MyURL -f=aout -f=vout\n");
console.log("- Remux MyURL to DASH and HLS (assumes directory dash exists):\nnode gpac.js -src=MyURL -dst=dash/live.mpd:dual\n");
console.log("- Run avgen to aout and vout:\nnode gpac.js -f=avgen -f=aout -f=vout\n");
console.log("- Play MyURL into custom node filter then to aout and vout:\nnode gpac.js -cust=fwd -src=MyURL -f=aout -f=vout\n");
console.log("- Play MyURL into custom node filter, get input file from NodeJS, keep node eventloop active and use 2 extra threads:\nnode gpac.js -run=async -threads=2 -cust=fwd -ib -src=MyURL -f=aout -f=vout\n");
console.log();
console.log('GPAC NodeJS version ' + gpac.version + ' libgpac ' + gpac.abi_major + ':' + gpac.abi_minor + '.' + gpac.abi_micro);
console.log('' + gpac.copyright_cite);
process.exit(0);
}
bad_param = (v) => {
console.log(v);
process.exit(1);
};
let other_args=['nodegpac'];
if (is_verbose)
console.log('GPAC NodeJS version ' + gpac.version + ' libgpac ' + gpac.abi_major + ':' + gpac.abi_minor + '.' + gpac.abi_micro);
let fio_factory = {
open: function(url, mode) {
this.file = null;
this.size = 0;
this.position = 0;
this.read_mode = false;
this.is_eof = false;
this.url = url;
mode = mode.replace('b', '');
mode = mode.replace('t', '');
try {
this.file = filesys.openSync(url, mode);
} catch (e) {
console.log('Fail to open ' + url + ' in mode ' + mode + ': ' + e);
return false;
}
if (mode.indexOf('w')<0) {
let stats = filesys.fstatSync(this.file);
this.size = stats.size;
if (mode.indexOf('a+')>=0) {
this.position = this.size;
}
this.read_mode = true;
}
return true;
},
close: function() {
filesys.closeSync(this.file);
this.file = null;
},
read: function(buf) {
let nb_bytes = 0;
try {
nb_bytes = filesys.readSync(this.file, buf, 0, buf.length, this.position);
} catch (e) {
console.log('read error: ' + e);
return 0;
}
if (!nb_bytes) this.is_eof = true;
this.position += nb_bytes;
return nb_bytes;
},
write: function(buf) {
let nb_bytes = filesys.writeSync(this.file, buf, 0, buf.length, this.position);
if (this.position == this.size) {
this.size += nb_bytes;
}
this.position += nb_bytes;
return nb_bytes;
},
seek: function(pos, whence) {
this.is_eof = false;
if (pos<0) return -1;
//seek set
if (whence==0) {
this.position = pos;
}
//seek cur
else if (whence==1) {
this.position += pos;
}
//seek end
else if (whence==2) {
if (this.size < pos) return -1;
this.position = this.size - pos;
} else {
return -1;
}
return 0;
},
tell: function() {
return this.position;
},
eof: function() {
return this.is_eof;
},
exists: function(url) {
try {
filesys.accessSync(url);
} catch (err) {
return false;
}
return true;
}
};
function push_source(url)
{
if (fileio_sources) {
let fio = new gpac.FileIO(url, fio_factory);
url = fio.url;
}
SRCS.push(url);
}
function push_sink(url)
{
if (fileio_sinks) {
let fio = new gpac.FileIO(url, fio_factory);
url = fio.url;
}
SINKS.push(url);
}
//first pass, init libgpac mem track and profile if set
//this must be done before any new constructor (FileIO, FilterSession) otherwise it is ignored
let profile=null;
let args = process.argv.splice(1);
args.forEach( val => {
if (val=='-h') print_usage();
else if (val == '-mt') memtrack = gpac.GF_MemTrackerSimple;
else if (val.startsWith('-p=')) {
profile = val.substring(3);
}
});
if (memtrack || profile)
gpac.init(memtrack, profile);
//second pass, parse all other args
args.forEach( val => {
if (val=='-h') { }
else if (val=='-mt') { }
else if (val.startsWith('-p=')) { }
else if (val=='-run=sync') run_mode=RUN_SYNC;
else if (val=='-run=step') run_mode=RUN_STEP;
else if (val=='-run=async') run_mode=RUN_PROMISE;
else if (val.startsWith('-run=')) { bad_param("Unknown run mode "+val); }
else if (val=='-cust=simple') test_type=TEST_SIMPLE;
else if (val=='-cust=sink') test_type=TEST_SINK;
else if (val=='-cust=fwd') test_type=TEST_FORWARD;
else if (val.startsWith('-cust=')) { bad_param("Unknown custom test mode "+val); }
else if (val=='-fwd=fwd') pck_fwd_mode=PCKFWD_FWD;
else if (val=='-fwd=ref') pck_fwd_mode=PCKFWD_REF;
else if (val=='-fwd=alloc') pck_fwd_mode=PCKFWD_ALLOC;
else if (val=='-fwd=shared') pck_fwd_mode=PCKFWD_SHARED;
else if (val=='-fwd=copy') pck_fwd_mode=PCKFWD_COPY;
else if (val=='-fwd=clone') pck_fwd_mode=PCKFWD_CLONE;
else if (val=='-fwd=all') pck_fwd_mode=PCKFWD_ALL;
else if (val.startsWith('-fwd=')) { bad_param("Unknown packet forward mode "+val); }
else if (val=='-task') { test_task = true; run_task_dur = -1; }
else if (val.startsWith('-task=')) {
run_task_dur = parseInt(val.substring(6));
if (run_task_dur == 0) run_task_dur = -1;
test_task = true;
}
else if (val.startsWith('-src=')) { push_source( val.substring(5) ); }
else if (val.startsWith('-dst=')) { push_sink( val.substring(5) ); }
else if (val.startsWith('-f=')) { FILTERS.push( val.substring(3) ); }
else if (val == '-v') is_verbose = true;
else if (val.startsWith('-play=')) {
start_time = parseFloat(val.substring(6));
}
else if (val == '-ib') fileio_sources = true;
else if (val == '-ob') fileio_sinks = true;
else if (val == '-cdash') custom_dash = true;
else if (val == '-chttp') custom_httpout = true;
else if (val.startsWith('-')) {
other_args.push(val);
}
});
if (other_args.length > 1)
gpac.set_args(other_args);
if (!SRCS.length && !SINKS.length && !FILTERS.length) {
console.log("No source, destination or filter specified, check usage (-h)");
process.exit(1);
}
let dash_algo = null;
if (custom_dash) {
dash_algo = {
on_period_reset: function(type) {
console.log('period reset type ' + type);
},
on_new_group: function(group) {
console.log('Got new group ' + JSON.stringify(group) );
},
on_rate_adaptation: function(group, base_group, force_lower_complexity, stats) {
console.log('Rate adaptation on group ' + group.idx + ' - stats ' + JSON.stringify(stats) );
return 0;
},
on_download_monitor: function(group, stats) {
console.log('Download monitor on group ' + group.idx + ' - stats ' + JSON.stringify(stats) );
return -1;
}
};
}
let http_req = null;
if (custom_httpout) {
http_req = {
on_request: function (req) {
console.log('got resquest ' + JSON.stringify(req));
req.headers_out = [{name: "x-GPAC", value: "foo"}];
req.close = function(error) {
console.log("Closing: " + gpac.e2s(error));
}
req.replys=0;
req.send();
}
};
}
let fs;
if (run_mode==RUN_SYNC) {
fs = new gpac.FilterSession();
} else {
fs = new gpac.FilterSession(gpac.GF_FS_FLAG_NON_BLOCKING);
}
fs.on_filter_new = function(f)
{
if (is_verbose)
console.log('New filter created: ' + f.name);
if (dash_algo && (f.name=="dashin")) {
try {
f.bind(dash_algo);
} catch (e) {
console.log('Failed to bind dash algo: ' + e);
}
}
else if (http_req && (f.name=="httpout")) {
try {
f.bind(http_req);
} catch (e) {
console.log('Failed to bind http request handler: ' + e);
}
}
}
fs.on_filter_del = function(f) {
if (is_verbose)
console.log('Filter deleted: ' + f.name);
}
if (is_verbose) {
console.log('Error code GF_URL_ERROR means ' + gpac.e2s(gpac.GF_URL_ERROR) );
console.log('Supported mime video/mp4: ' + fs.is_supported_mime('video/mp4') );
if (SRCS.length)
console.log('Supported file ' + SRCS[0] + ': ' + fs.is_supported_source(SRCS[0]) );
}
if (test_task) {
t = {};
t.nb_tasks = 0;
t.execute = function() {
if (fs.last_task) {
return false;
}
this.nb_tasks++;
if (run_task_dur > 0) {
run_task_dur -= 1;
if (run_task_dur <= 0) {
console.log('Last task run #' + this.nb_tasks);
return false;
}
}
console.log('Task run #' + this.nb_tasks);
return 1000;
}
fs.post_task(t);
}
fs.on_event = function(evt)
{
//console.log('FS event ' + JSON.stringify(evt) );
if (evt.ui_type == gpac.GF_EVENT_QUIT) {
fs.abort(gpac.GF_FS_FLUSH_NONE);
return true;
}
return false;
}
//load sources
SRCS.forEach( src => {
console.log('Loading source URL ' + src);
src_f = fs.load_src(src);
});
const dump_prop = (type, val, ptype) => {
if (ptype==gpac.GF_PROP_DATA)
console.log('\t' + type + ': raw data');
else if (typeof val == 'object')
console.log('\t' + type + ': ' + JSON.stringify(val) );
else
console.log('\t' + type + ': ' + val + ' (' + (typeof val) + ')');
}
//load custom filter
let cust = null;
if (test_type != TEST_SIMPLE) {
cust = fs.new_filter("NodeJS_Test");
//we accept any number of input pids
cust.set_max_pids(-1);
cust.pids = [];
if (test_type == TEST_FORWARD) {
cust.push_cap('StreamType', 'Visual', gpac.GF_CAPS_INPUT_OUTPUT);
cust.push_cap('StreamType', 'Audio', gpac.GF_CAPS_INPUT_OUTPUT);
} else {
cust.push_cap('StreamType', 'Visual', gpac.GF_CAPS_INPUT);
cust.push_cap('StreamType', 'Audio', gpac.GF_CAPS_INPUT);
}
cust.configure_pid = function(pid, is_remove)
{
if (this.pids.indexOf(pid) < 0) {
this.pids.push(pid);
pid.nb_pck = 0;
if (test_type == TEST_SINK) {
let evt = new gpac.FilterEvent(gpac.GF_FEVT_PLAY);
pid.send_event(evt);
if (is_verbose) {
console.log('Input PID Props: ');
pid.enum_props( dump_prop );
}
pid.opid = null;
}
else {
//create output pid
pid.opid = this.new_pid();
pid.opid.copy_props(pid);
if (is_verbose) {
console.log('Output PID props: ');
pid.opid.enum_props( dump_prop );
}
}
} else if (is_remove) {
console.log('PID remove !');
} else {
console.log('PID reconfigure !');
pid.opid.copy_props(pid);
}
return gpac.GF_OK;
}
cust.pck_clone_cache = null;
if (pck_fwd_mode==PCKFWD_ALL) {
cust.fwd_mode = 0;
cust.fwd_loop = true;
} else {
cust.fwd_mode = pck_fwd_mode;
cust.fwd_loop = false;
}
cust.process = function() {
let nb_eos=0;
this.pids.forEach(pid =>{
if (pid.eos) {
nb_eos++;
if (pid.opid) pid.opid.eos = true;
return;
}
//begin of packet fetch loop
while (1) {
let pck = pid.get_packet();
if (!pck) return;
if (is_verbose) {
console.log('PCK props: ' + JSON.stringify(pck) );
pck.enum_props( dump_prop );
}
pid.nb_pck ++;
if (test_type == TEST_SINK) {
pid.drop_packet();
continue;
}
//send by reference
if (this.fwd_mode==PCKFWD_REF) {
let dst = pid.opid.new_pck_ref(pck);
dst.copy_props(pck);
dst.send();
}
//full copy mode
else if (this.fwd_mode==PCKFWD_ALLOC) {
let dst = pid.opid.new_pck(pck.size);
dst.copy_props(pck);
new Uint8Array(dst.data).set(new Uint8Array(pck.data));
dst.send();
}
//keep ref to packet and send new packet using shared data
else if (this.fwd_mode==PCKFWD_SHARED) {
/*
Ultimately we would like to test ab = pck.data + pck.ref() and pck.unref() in destructor
however this crashes node:
https://github.com/node-ffi-napi/ref-napi/issues/54
*/
let ab = new ArrayBuffer(pck.size);
new Uint8Array(ab).set(new Uint8Array(pck.data));
//keep ref
pck.ref();
let dst = pid.opid.new_pck_shared(ab, () => { pck.unref();});
dst.copy_props(pck);
dst.send();
}
//pck_clone
else if (this.fwd_mode==PCKFWD_CLONE) {
let dst = pid.opid.new_pck_clone(pck);
dst.send();
}
//pck_copy
else if (this.fwd_mode==PCKFWD_COPY) {
let dst = pid.opid.new_pck_copy(pck);
dst.send();
} else {
pid.opid.forward(pck);
}
if (this.fwd_loop) {
this.fwd_mode++;
if (this.fwd_mode==6) this.fwd_mode=0;
}
pid.drop_packet();
} //end of packet fetch loop
});
if (nb_eos == this.pids.length)
return gpac.GF_EOS;
return gpac.GF_OK;
}
cust.process_event = function(evt)
{
//shortcut play event, canceling it and sending a play event with a different start
if ((evt.type==gpac.GF_FEVT_PLAY) && start_time) {
let anevt = new gpac.FilterEvent(gpac.GF_FEVT_PLAY);
anevt.start_range = start_time;
this.pids.forEach(pid => pid.send_event(anevt));
return true;
}
return false;
}
}
//load sinks
if (test_type != TEST_SINK) {
SINKS.forEach( sink => {
console.log('Loading dest ' + sink);
let sink_f = fs.load_dst(sink);
//set sink source to be our custom filter
if (cust && !FILTERS.length)
sink_f.set_source(cust);
});
}
//load filters
FILTERS.forEach( fname => {
console.log('Loading filter ' + fname);
let f = fs.load(fname);
if (test_type != TEST_SINK) {
//set source to be our custom filter
if (cust && !SINKS.length)
f.set_source(cust);
}
});
//print custom filter connections
if (cust && is_verbose) {
console.log('CustomFilter connections ');
cust.print_connections();
}
console.log('');
const session_done = () => {
console.log('Session done running, graph: ');
fs.print_graph();
};
//runin sync mode
if (run_mode==RUN_SYNC) {
console.log('Running in blocking mode');
fs.run();
session_done();
return;
}
//runin step mode
if (run_mode==RUN_STEP) {
console.log('Running in non-blocking mode');
while (true) {
fs.run();
if (fs.last_task) break;
}
session_done();
return;
}
//Run session step as Promise
const FilterSessionPromise = (fs_run_task) => {
var fsrun_promise = () => {
return (fs.last_task==false) ? fs_run_task().then(fsrun_promise) : Promise.resolve();
}
return fsrun_promise();
};
const run_task = () => {
return new Promise((resolve, reject) => {
resolve( fs.run() );
});
}
FilterSessionPromise(run_task).then( ).finally( session_done );
console.log('Entering NodeJS EventLoop');