blob: 598e874ca21f04a2a92d88244df5ee7782edc48d [file] [edit]
import { Sys as sys } from 'gpaccore'
import {XMLHttpRequest} from 'xhr'
globalThis.sys = sys;
let server_ports=0;
let do_quit = false;
let force_quit = false;
let all_requests = [];
const MANI_DASH = 1;
const MANI_HLS = 2;
const HLS_MASTER = 1;
const HLS_VARIANT = 2;
const LIVE_EDGE_MAX_DIST = 5;
const DEACTIVATE_TIMEOUT_MS = 5000;
//safety on how long we will make the client wait for the MABR file to be available
//the overall timeout depends on repair parameters
const MABR_TIMEOUT_SAFETY = 200;
//timeout before MABR self-deactivates if no request on a PID
const MABR_PID_DEACTIVATION_TIMEOUT = 10000;
const CACHE_TYPE_HTTP = 0;
const CACHE_TYPE_MOD = 1;
const CACHE_TYPE_MABR = 2;
const DEFAULT_MABR_UNLOAD_SEC = 4;
const DEFAULT_KEEPALIVE_SEC = 4;
const DEFAULT_ACTIVATE_CLIENTS = 1;
const DEFAULT_MCACHE = false;
const DEFAULT_REPAIR = 'false'; // 'false', 'true' or 'auto'
const DEFAULT_TIMESHIFT = 10;
const DEFAULT_GCACHE = false;
const DEFAULT_CHECKIP = false;
//metadata
filter.set_name("mediaserver");
filter.set_class_hint(GF_FS_CLASS_NETWORK_IO);
filter.set_desc("Media Server");
filter.set_version("1.0");
filter.set_author("GPAC team - (c) Telecom Paris 2024 - license LGPL v2");
let filter_help = `This filter is an HTTP server and proxy for GET and HEAD requests supporting Multicast ABR sources.
This filter does not produce PIDs, it attaches to HTTP output filter.
If no HTTP output filter is specified in the session, the filter will create one.
The file \`.gpac_auth\`, if present in current working directory, will be used for authentication unless \`--rdirs\` is set.
If more options need to be specified for the HTTP output filter, they can be passed as local options or using global options:
EX gpac mediaserver:cors=on --user_agent=MyUser
Although not recommended, a server may be specified explicitly:
EX gpac mediaserver httpout:OPTS
In this case, the first \`httpout\` filter created will be used.
Default request handling of \`httpout\` filter through read / write directories is disabled.
# Services Configuration
The service configuration is set using [-scfg](). It shall be a JSON file containing a single array of services.
Each service is a JSON object with one or more of the following properties:
- id: (string, default null) Service identifier used for logs
- active: (boolean, default true) service is ignored if false
- http: (string, default null) URL of remote service to proxy (either resource name or server path)
- gcache: (boolean, default ${DEFAULT_GCACHE}) use gpac local disk cache when fetching media from HTTP for this service
- local: (string, default null) local mount point of this service
- keepalive: (number, default ${DEFAULT_KEEPALIVE_SEC}) remove the service if no request received for the indicated delay in seconds (0 force service to stay in memory forever)
- mabr: (string, default null) address of multicast ABR source for this service
- timeshift: (number, default ${DEFAULT_TIMESHIFT}) time in seconds a cached file remains in memory
- unload: (number, default ${DEFAULT_MABR_UNLOAD_SEC}) multicast unload policy
- activate: (number, default ${DEFAULT_ACTIVATE_CLIENTS}) multicast activation policy
- mcache: (boolean, default ${DEFAULT_MCACHE}) cache manifest files
- repair: (string, default ${DEFAULT_REPAIR}) enable unicast repair in MABR stack
- false: disable repair
- true: enable repair using source URL or repair servers indicated in MABR
- auto: enable repair only from repair servers indicated in MABR
- check_ip: (boolean, , default ${DEFAULT_CHECKIP}) monitor IP address and port rather than connection when tracking active clients
- noproxy: (boolean) disable proxy for service when local mount point is set. Default is \`true\` if both \`local\` and \`http\` are set, \`false\` otherwise
- sources: (array, default null) list of sources objects for file-only services. Each source has the following property:
- name: name as used in resource path,
- url: local or remote URL to use for this resource.
- js: (string, default null) built-in or custom request resolver
Any JSON object with the property \`comment\` set will be ignored.
Not all properties are used for each type of service and other properties can be defined by custom request resolvers.
# Proxy versus Server
All services using \`http\` option can be exposed by the server without exposing the origin URL, rather than being proxied. To enable this, the \`local\` service configuration option must be set to:
- the exposed server path, in which case manifest names are not rewritten
- or the exposed manifest path, in which case manifest names are rewritten, but only one manifest can be exposed (does not work with dual MPD and M3U8 services)
EX { "http": "https://test.com/live/dash/live.mpd", "local": "/service1/"}
The server will translate any request \`/service1/foo/bar.ext\` into \`https://test.com/live/dash/foo/bar.ext\`.
EX { "http": "https://test.com/live/dash/live.mpd", "local": "/service1/manifest.mpd"}
The server will translate:
- request \`/service1/manifest.mpd\` into \`https://test.com/live/dash/live.mpd\`
- any request \`/service1/foo/bar.ext\` into \`https://test.com/live/dash/foo/bar.ext\`
Note: The URL must point to a self-contained subdirectory of the remote site. Any URLs outside this directory will either fail or be resolved as absolute path on the remote site.
When \`local\` is not set, these services are always acting as proxies for the \`http\` URL.
When \`noproxy\` is explicitly set to false for the services with both \`http\` and \`local\`, the remote URL will be available as a proxy service as well.
# HTTP Proxy and Relay
The server can act as a proxy for HTTP requests, either for any requests or by domain or resource name.
__Service configuration parameters used :__ \`http\` (mandatory), \`gcache\`, \`local\`.
Configuration for activating proxy for a specific network path:
EX { "http": "https://test.com/video/"}
Configuration for activating proxy for any network path:
EX { "http": "*"}
Configuration for a relay on a given path:
EX { "http": "https://test.com/some/path/to/video/", "local": "/myvids/"}
This will resolve any request \`http://localhost/myvids/*\` to \`https://test.com/some/path/to/video/*\`
Note: The requests are never cached in memory in this mode, but can be cached on disk if \`gcache\` is set.
# HTTP Streaming Cache
The server can act as a cache for live HTTP streaming sessions. The live edge can be cached in memory for a given duration.
__Service configuration parameters used :__ \`http\` ( mandatory), \`timeshift\`, \`mcache\`, \`gcache\`, \`keepalive\` and \`local\`.
Configuration for proxying while caching a live HTTP streaming service:
EX { "http": "https://test.com/dash/live.mpd", "timeshift": 30 }
Configuration for relay caching a live HTTP streaming service:
EX { "http": "https://test.com/dash/live.mpd", "timeshift": 30, "local": "/myservice/test.mpd"}
The \`local\` service configuration option can be set to:
- the exposed server path, in which case manifest names are not rewritten
- or the exposed manifest path, in which case manifest names are rewritten, but only one manifest can be exposed (does not work with dual MPD and M3U8 services)
# Multicast ABR Gateway
The server can be configured to use a multicast ABR source for an HTTP streaming service, without any HTTP source.
__Service configuration parameters used :__ \`mabr\` (mandatory), \`local\` (mandatory), \`timeshift\` and \`keepalive\`.
The multicast source can be DVB-MABR (e.g. \`mabr://235.0.0.1:1234/\`), ATSC3.0 (e.g. \`atsc://\`) or ROUTE (e.g. \`route://235.0.0.1:1234/\`).
- If the multicast is replayed from a file, netcap ID shall be set in this multicast URL (e.g. \`:NCID=N\`).
- If a specific IP interface is used, it can also be set in multicast URL (e.g. \`:ifce=IP\`).
For example, with \`local\` set to \`/service/live.mpd\` with \`mabr\` set, the server will expose the multicast service as \`http://localhost/service/live.mpd\`.
The manifest name can be omitted, in which case the exact manifest name used in the broadcast shall be used (and known to the client).
Configuration for exposing a MABR session:
EX { "mabr": "mabr://234.0.0.1:1234", "local": "/service1", "timeshift": 30 }
# Multicast ABR Gateway with HTTP cache
The server can be configured to use a multicast source as an alternate data source of a given HTTP streaming service.
__Service configuration parameters used :__ \`http\` (mandatory), \`mabr\` (mandatory), \`local\`, \`timeshift\`, \`repair\`, \`gcache\`, \`mcache\`, \`unload\`, \`activate\`, \`keepalive\` and \`js\`.
The multicast service can be dynamically loaded at run-time using the \`unload\` service configuration option:
- if 0, the multicast is started when loading the server and never ended,
- otherwise, the multicast is started dynamically and ended \`unload\` seconds after last deactivation.
The qualities in the multicast service can be dynamically activated or deactivated using the \`activate\` service configuration option:
- if 0, multicast streams are never deactivated,
- otherwise, a multicast representation is activated only if at least \`activate\` clients are consuming it, and deactivated otherwise.
The multicast service can use repair options of the MABR stack using \`repair\` service configuration option:
- if false, the file will not be sent until completely received (this increases latency),
- otherwise, file data will be pushed as soon as available in order (after reception or repair).
The number of active clients on a given quality is computed using the client connection state: any disconnect/reconnect from a client for the same quality will trigger a deactivate+activate sequence.
If \`check_ip\` is set to true, the remote IP address+port are used instead of the connection. This however assumes that each client has a unique IP/port which may not always be true (NATs).
If \`timeshift\` is 0 for the service, multicast segments will be trashed as soon as not in use (potentially before the client request).
Note: Manifest files coming from multicast are currently never cached.
Configuration for caching a live HTTP streaming service with MABR backup:
EX { "http": "https://test.com/dash/live.mpd", "mabr": "mabr://234.0.0.1:1234", "timeshift": 30}
For such services, the custom HTTP header \`X-From-MABR\` is defined:
- for client request, a value of \`no\` will disable MABR cache for this request; if absent or value is \`yes\`, MABR cache will be used if available
- for client response, a value of \`yes\` indicates the content comes from the MABR cache; if absent or value is \`no\` or \`off-edge\`, the content comes from HTTP (\`off-edge\` indicates a request outside of the timeshift buffer)
The dedicated root endpoint \`/stats\` returns, the response content type's count (\`yes\`, \`no\`, \`off-edge\`) served by the gateway.
The \`js\` option can be set to a JS module exporting the following functions:
- init : (mandatory) The function is called once at the start of the server. Parameters:
- scfg: the service configuration object
- return value: must be true if configuration and initialization are successful, false otherwise.
- service_activation : (optional) The function is called when the service is activated or deactivated. Parameters:
- do_activate (boolean): if true, service is being loaded otherwise it is being unloaded
- return value: none
- quality_activation : (optional) The function is called when the given quality is to be activated or deactivated. If not present, (de)activation always happens. Parameters (in order):
- do_activate (boolean): if true, quality is being activated otherwise it is being deactivated
- service_id (integer): ID of the service as announced in the multicast
- period_id (string): ID of the DASH Period, ignored (empty) for HLS
- adaptationSet_ID (integer): ID of the DASH AdaptationSet, ignored (-1) for HLS
- representation_ID (string): ID of the DASH representation or name of the HLS variant playlist
- return value: shall be true if activation/deactivation shall proceed and false if activation/deactivation shall be canceled.
- get_mcast_address : (optional) The function is called when the service is activated. Parameters:
- service_url (string): URL of service for which the multicast adress is queried
- return value: shall be the multicast address to use for the service or null if no multicast is used (active multicast wil then be deactivated).
# File Services
A file system directory can be exposed as a service.
__Service configuration parameters used :__ \`local\` (mandatory), \`sources\` (mandatory), \`gcache\` and \`keepalive\`.
The \`local\` service configuration option must be set to the desired service path, and the \`sources\` service configuration option must one or more valid sources.
Each source is either a file, a directory or a remote URL.
Configuration for exposing a directory:
EX { "local": "/dserv/", "sources": [ { "name": "foo/", "url": "my_dir/" } ] }
This service will expose the content of directory \`my_dir/*\` as \`http://localhost/dserv/foo/*\`.
In this mode, file serving is handled directly by httpout filter and no memory caching is used.
If the source is a remote HTTP one, the \`gcache\` option will indicate if GPAC local cache shall be used.
# Module development
A JS module can be specified using the \`js\` option in the service configuration. The module export functions are:
## init (mandatory)
The function is called once at the start of the server
Parameter: the service configuration object
return value must be true if configuration and initialization are successful, false otherwise
## resolve (mandatory)
Parameter: an HTTP request object from GPAC
The function returns an array of two values \`[result, delay]\`:
- result: null if error, a resolved string indicating either a local file or the reply body, or an object
- delay: if true, the reply is being delayed by the module for later processing
When an object is returned, the request is handled by the JS module in charge of sending the reply and reading the data. The object shall have the following properties:
- read: same semantics as the request read method
- on_close: optional function called when the request is closed
# Built-in modules
`;
filter.set_arg({ name: "scfg", desc: "service configuration file", type: GF_PROP_STRING} );
filter.set_arg({ name: "quit", desc: "exit server once last service has been deactivated", type: GF_PROP_BOOL, def: "false"} );
let services_defs = [];
let all_services = [];
function do_log(level, msg)
{
if (arguments.length==3) print(level, `${arguments[2]}: ${msg}`);
else print(level, msg);
}
function setup_representation(rep)
{
rep.mabr_active = false;
rep.mabr_deactivate_timeout = 0;
rep.nb_active = 0;
rep.first_mabr_tune = 0;
rep.seg_id = null;
rep.seg_dur = 0;
rep.hls_seq_start = 0;
rep.live_edge = false;
rep.radical = null;
rep.suffix = null;
rep.last_edge_compute = 0;
if (rep.template) {
let idx = rep.template.indexOf('$');
rep.radical = rep.template.substring(0, idx);
let remain = rep.template.substring(idx+1);
idx = remain.indexOf('$');
rep.suffix = remain.substring(idx+1);
}
}
function update_manifest(manifest_ab, target_url, mabr_service, cache_file)
{
let mani = sys.mpd_parse(manifest_ab);
if (!mani) return;
let hls_active_rep = null;
let service = mabr_service ? mabr_service : all_services.find(s => {
if (target_url.indexOf(s.url)>=0) return true;
if (!s.manifest || !s.manifest.m3u8) return false;
if (!s.manifest) return false;
let period = s.manifest.periods[0];
//for HLS, locate rep for this variant URL
let rep = period.reps.find(e => {
if (!e.xlink) return false;
//absolute URLs
if (e.xlink == target_url) return true;
let idx = target_url.indexOf(e.xlink);
if (idx<0) return false;
if (target_url.startsWith(s.base_url)) return true;
return false;
});
if (rep) {
hls_active_rep = rep;
return true;
}
return false;
});
if (!service) {
if (mani.m3u8 == HLS_VARIANT) {
do_log(GF_LOG_ERROR, `Failed to locate service for HLS variant ${target_url}`);
return;
}
service = create_service(target_url, true, null);
}
//HLS master playlist
if (mani.m3u8 == HLS_MASTER) {
//we assume master playlist is static
if (service.manifest) return;
let period = mani.periods[0];
period.reps = period.reps.sort((a, b) => a.bandwidth - b.bandwidth);
for (let r=0; r<period.reps.length; r++) {
setup_representation( period.reps[r] );
}
service.manifest = mani;
return;
}
//HLS variant playlist
if (mani.m3u8 == HLS_VARIANT) {
if (!service.manifest) return;
let period = service.manifest.periods[0];
if (!hls_active_rep) {
do_log(GF_LOG_ERROR, `Service ${service.id} cannot find rep for target ${target_url}`);
return;
}
hls_active_rep.segments = mani.segments;
hls_active_rep.hls_seq_start = mani.seq_start;
hls_active_rep.live_seg_num = mani.live_seg_num;
service.manifest.live = mani.live;
if (cache_file) {
cache_file.manifest_min_update = mani.min_update;
}
do_log(GF_LOG_DEBUG, `Service ${service.id} updated HLS manifest`); // + JSON.stringify(service.manifest));
return;
}
for (let p=0; p<mani.periods.length; p++) {
let period = mani.periods[p];
period.reps = period.reps.sort((a, b) => a.bandwidth - b.bandwidth);
for (let r=0; r<period.reps.length; r++) {
setup_representation( period.reps[r] );
}
}
let updated = false;
if (!service.manifest) {
service.manifest = mani;
} else {
updated = true;
service.manifest.live_utc = mani.live_utc;
//update existing manifest
//remove old periods
for (let p=0; p<service.manifest.periods.length; p++) {
let period = service.manifest.periods[p];
let new_p = mani.periods.find(e => e.id == period.id);
if (new_p) continue;
service.manifest.periods.splice(p);
p--;
continue;
}
//push new periods
for (let p=0; p<mani.periods.length; p++) {
let rep_inserted = false;
let period = mani.periods[p];
let old_p = service.manifest.periods.find(e => e.id == period.id);
if (!old_p) {
service.manifest.periods.push(period);
continue;
}
//same period, update reps
for (let r=0; r<period.reps.length; r++) {
let rep = period.reps[r];
let old_r = old_p.reps.find(e => (e.ID == rep.ID || e.bandwidth == rep.bandwidth));
//rep injection is not prohibited, rep removal is
if (!old_r) {
setup_representation( rep);
old_p.reps.push(rep);
rep_inserted = true;
continue;
}
//same rep, update segments and live edge info
old_p.bandwidth = rep.bandwidth;
old_r.segments = rep.segments;
old_r.live_seg_num = rep.live_seg_num;
}
if (rep_inserted) {
old_p.reps = old_p.reps.sort((a, b) => a.bandwidth - b.bandwidth);
}
}
}
if (cache_file) {
cache_file.manifest_min_update = mani.min_update;
}
do_log(GF_LOG_DEBUG, `Service ${service.id} manifest ${ (updated ? 'updated' : 'received')} for ${target_url}`); // + ': ' + JSON.stringify(service.manifest));
}
function locate_service(target_url)
{
let service = null;
for (let i=0; i<all_services.length; i++) {
service = all_services[i];
if (service.url == target_url) break;
if (! service.manifest) {
service=null;
continue;
}
//todo, deal with base urls ?
let base_url = service.base_url;
if (target_url.startsWith(base_url)) break;
service=null;
}
return service;
}
function locate_service_quality(target_url, in_service)
{
let service = in_service;
let with_base_url = null;
if (!service) {
for (let i=0; i<all_services.length; i++) {
service = all_services[i];
if (service.url == target_url) break;
if (! service.manifest) {
service=null;
continue;
}
//todo, deal with base urls ?
let base_url = service.base_url;
if (target_url.startsWith(base_url)) break;
service=null;
}
if (!service) return [null,null,null];
}
if (!service.manifest) return [service, null,null];
if (!with_base_url) with_base_url = service.base_url;
//extract seg name
let seg_name = target_url.substring(with_base_url.length);
let period = null;
let rep = null;
let now = sys.get_utc();
//find segment
for (let p=0; p<service.manifest.periods.length; p++) {
period = service.manifest.periods[p];
for (let r=0; r<period.reps.length; r++) {
rep = period.reps[r];
rep.seg_id = null;
rep.seg_dur = 0;
rep.live_edge = false;
rep.seg_num = 0;
if (rep.xlink && (seg_name.indexOf(rep.xlink)>=0)) {
rep=null;
continue;
}
if (rep.radical && ! seg_name.startsWith(rep.radical)) {
rep=null;
continue;
}
//m3u8 or SegmentTimeline
if (rep.segments.length) {
let found_rep = false;
for (let si=0; si<rep.segments.length; si++) {
let s_idx = rep.segments.length - 1 - si;
let seg = rep.segments[s_idx];
if (seg.url == seg_name) {
rep.seg_dur = seg.duration * 1000 / rep.timescale;
if (rep.radical) {
let num_time = seg_name.substring(rep.radical.length);
let idx = num_time.indexOf(rep.suffix);
if (idx>=0) {
rep.seg_id = num_time.substring(0, idx);
}
} else if (service.manifest.m3u8) {
rep.seg_id = '' + (rep.hls_seq_start + s_idx);
}
if (si<=LIVE_EDGE_MAX_DIST) {
rep.live_edge = true;
}
found_rep = true;
break;
}
}
if (found_rep) break;
}
if (rep.template) {
let num_time = seg_name.substring(rep.radical.length);
let idx = num_time.indexOf(rep.suffix);
if (idx>=0) {
rep.seg_id = num_time.substring(0, idx);
}
if (!rep.seg_dur)
rep.seg_dur = rep.duration * 1000 / rep.timescale;
//compute live edge time divide then multiply to avoid overflow
if (rep.last_edge_compute + rep.seg_dur < now) {
let live_edge_num = now;
live_edge_num -= service.manifest.ast;
live_edge_num -= period.start;
live_edge_num /= 1000;
live_edge_num /= rep.duration;
live_edge_num *= rep.timescale;
live_edge_num = Math.ceil(live_edge_num) + 1;
rep.live_seg_num = live_edge_num;
rep.last_edge_compute = now;
}
//todo in live, figure out if we are on the live edge
if (rep.live_seg_num && rep.seg_id) {
rep.seg_num = parseInt(rep.seg_id);
let seg_num = rep.seg_num - rep.live_seg_num;
if (Math.abs(seg_num) <= LIVE_EDGE_MAX_DIST) {
rep.live_edge = true;
}
}
break;
}
rep = null;
}
if (rep) {
rep.period_id = period.ID;
break;
}
period = null;
}
do_log(GF_LOG_DEBUG, `Located service ${service.id} for URL ${seg_name}${(rep ? (' rep: ' + rep.ID ) : '' )}`);
return [service, rep, period];
}
function cat_buffer(dst_ab, src_ab)
{
let tmp = new Uint8Array(dst_ab ? dst_ab.byteLength + src_ab.byteLength : src_ab.byteLength);
if (dst_ab)
tmp.set(new Uint8Array(dst_ab), 0);
tmp.set(new Uint8Array(src_ab), dst_ab ? dst_ab.byteLength : 0);
return tmp.buffer;
}
globalThis.cat_buffer = cat_buffer;
function get_mediaserver_stats(){
return all_services.reduce((r,s) => {
r[s.id] = s.stats;
return r;
}, {});
}
globalThis.get_mediaserver_stats = get_mediaserver_stats;
//custom HTTPout request handler
let httpout = {};
httpout.on_request = (req) =>
{
//use pre-authentication done by server
if (req.auth_code!=200) {
req.reply = req.auth_code;
req.send();
return;
}
//not yet supported
if ((req.method !== 'GET') && (req.method !== 'HEAD')) {
req.reply = 405;
req.send('Method not allowed by server');
return;
}
do_log(GF_LOG_DEBUG, `Got request ${req.method} for ${req.url}`);
if (req.url.startsWith('/stats')){
req.reply = 200;
req.body = JSON.stringify(get_mediaserver_stats());
req.send();
return;
}
//setup request
req.target_url = null;
req.is_head = (req.method === 'HEAD');
req.start_time = sys.clock_ms();
req.bytes_in_req = 0;
req.cache_file = null;
req.ab_offset = 0;
req.ab_queue = null;
req.ab_total_bytes = 0;
req.data = null;
req.waiting_mabr = 0;
req.xhr_done = false;
req.activate_rep_timeout = null;
req.xhr = null;
req.br_start=0;
req.br_end=0;
req.from_cache=false;
req.no_cache=false;
req.do_cache = false;
req.jsmod=null;
req.up_rate=0;
req.service = null;
all_requests.push(req);
req.get_down_rate = function() {
let bitrate = this.bytes_in_req * 8; // /1000 for kbits but we then need to divide by time/1000 for secs
let time = sys.clock_ms() - this.start_time;
if (!time) time=1;
return [Math.floor(1000*bitrate/time), time];
}
req.get_up_rate = function() {
return this.up_rate;
}
req.on_done = function() {
let bitrate, time;
[bitrate, time] = this.get_down_rate();
bitrate = Math.floor(bitrate/1000);
let orig = 'HTTP'
if (this.from_cache) {
orig = 'local cache';
} else if (this.cache_file) {
orig = ((this.cache_file.cache_type==CACHE_TYPE_MABR) ? 'MABR' : 'HTTP') +' cache';
} else if (this.jsmod) {
orig = 'JS module';
}
do_log(GF_LOG_INFO, `Done sending ${this.url} (${this.reply}) from ${orig} in ${time} ms at ${bitrate} kbps (${this.bytes_in_req} bytes)`);
};
//used when reading from JS module
req._read_from_mod = function(ab) {
let res = this.jsmod.read(ab);
if (res==0) {
this.on_done();
return 0;
}
if (res<0) return res;
this.bytes_in_req += res;
return res;
}
//used when reading from XHR without cache
req._read_from_queue = function(ab) {
if (!this.ab_queue.length) {
if (this.xhr_done) {
this.on_done();
if (this.activate_rep_timeout) this.activate_rep_timeout.update();
return 0;
}
return -1;
}
let q_ab=this.ab_queue[0];
let to_read = q_ab.byteLength - this.ab_offset;
if (to_read > ab.byteLength) to_read = ab.byteLength;
new Uint8Array(ab, 0, to_read).set(new Uint8Array(q_ab, this.ab_offset, to_read) );
this.ab_offset += to_read;
if (this.ab_offset == q_ab.byteLength) {
this.ab_offset = 0;
this.ab_queue.shift();
}
this.bytes_in_req += to_read;
return to_read;
}
//used when reading from MABR service or cached file
req._read_from_buffer = function(ab) {
if (this.is_head || !this.cache_file) return 0;
let src_ab = this.cache_file.data;
if (src_ab.byteLength <= this.ab_offset) {
if (this.cache_file.done) {
this.on_done();
this.cache_file.release(this);
this.cache_file = null;
if (this.activate_rep_timeout) this.activate_rep_timeout.update();
return 0;
}
//waiting for data
return -1;
}
let to_read = src_ab.byteLength - this.ab_offset;
if (to_read > ab.byteLength) to_read = ab.byteLength;
new Uint8Array(ab, 0, to_read).set(new Uint8Array(src_ab, this.ab_offset, to_read) );
this.ab_offset += to_read;
this.bytes_in_req += to_read;
return to_read;
};
req._read_abort = function(ab) {
return 0;
};
//end of session
req.close = function(code) {
if (code<0)
do_log(GF_LOG_DEBUG, `Client session ${this.url} is closed: ${sys.error_string(code)}`);
let ridx = all_requests.indexOf(this);
if (ridx>=0) all_requests.splice(ridx, 1);
if (do_quit && !all_requests.length && !all_services.length) force_quit = true;
if (this.jsmod && typeof this.jsmod.on_close == 'function') this.jsmod.on_close();
this.jsmod = null;
this.ab_queue = null;
if (this.cache_file) this.cache_file.release(this);
if (this.activate_rep_timeout) this.activate_rep_timeout.update();
else if (req.service && req.service.keepalive && !req.service.mabr) {
let timeout = (code<GF_OK) ? 100 : 1000*req.service.keepalive;
req.service.unload_timeout = sys.clock_ms() + timeout;
}
//we have an XHR, abort it if needed and move to XHR cache for later reuse
if (this.xhr) {
if (this.xhr && (this.xhr.readyState < 4)) this.xhr.abort();
if (this.service) {
this.xhr.last_used = sys.clock_ms();
this.service.xhr_cache.push(this.xhr);
this.xhr = null;
}
}
};
req.set_cache_file = function(file) {
if (this.cache_file) return;
this.cache_file = file;
file.nb_users ++;
this.read = this._read_from_buffer;
if (file.xhr_status) {
this.flush_request();
} else {
if (!file.pending_reqs) file.pending_reqs = [];
file.pending_reqs.push(this);
}
if (file.pid) file.pid.deactivate_timeout = 0;
};
req.flush_request = function() {
this.reply = this.cache_file.xhr_status;
this.headers_out.push( { "name" : 'Content-Type', "value": this.cache_file.mime} );
if (this.cache_file.headers)
this.cache_file.headers.forEach( hdr => this.headers_out.push(hdr) );
if (this.cache_file.range)
this.headers_out.push( { "name" : 'Content-Range', "value": this.cache_file.range} );
if (this.cache_file.aborted) {
} else if (this.cache_file.done) {
this.headers_out.push( { "name" : 'Content-Length', "value": this.cache_file.data.byteLength} );
} else {
this.headers_out.push( { "name" : 'Transfer-Encoding', "value": 'chunked'} );
}
if (this.cache_file.cache_type==CACHE_TYPE_MABR) {
this.service.update_mabr_stats('yes');
this.headers_out.push( { "name" : 'X-From-MABR', "value": 'yes'} );
}
else if (this.service && this.service.mabr) {
const val = this.live_edge ? 'no' : 'off-edge';
this.service.update_mabr_stats(val);
this.headers_out.push( { "name" : 'X-From-MABR', "value": val} );
}
this.send();
};
req.fetch_unicast = function() {
//already setup through set_cache_file - typically happens when N requests are pending on MABR file that is later cancelled
//if source can still be cached, a cache file will be created and assigned to all pending requests for the same url
if (this.cache_file) return;
//MABR without origin server
if (this.service && !this.service.url && !this.service.mabr_repair_url) {
do_log(GF_LOG_DEBUG, `No URL for base repair, cannot fetch ${this.target_url}`);
this.reply = 404;
this.send();
return;
}
this.ab_queue = [];
this.manifest_ab = null;
this.read = this._read_from_queue;
//DASH/HLS rep, create a cache file for media
if (active_rep && !this.no_cache) {
this.cache_file = this.service.create_cache_file(this.target_url, "video/mp4", this.br_start, this.br_end, CACHE_TYPE_HTTP);
this.cache_file.nb_users ++;
this.read = this._read_from_buffer;
}
//DASH/HLS manifest, create a cache file for media
else if (this.service && this.manifest_type && this.service.mani_cache) {
this.cache_file = this.service.create_cache_file(this.target_url,(this.manifest_type==MANI_DASH) ? "application/dash+xml" : "application/vnd.apple", this.br_start, this.br_end, CACHE_TYPE_HTTP);
this.cache_file.nb_users ++;
this.read = this._read_from_buffer;
}
//MABR without origin server but repair URL signaled from mabr, update target url
if (this.service && !this.service.url) {
this.target_url = sys.url_cat(this.service.mabr_repair_url, this.target_url);
}
this.xhr = null;
if (this.service && this.service.xhr_cache.length) {
this.xhr = this.service.xhr_cache.shift();
}
if (!this.xhr)
this.xhr = new XMLHttpRequest();
this.xhr.last_used = sys.clock_ms();
let do_cache = false;
if (!this.cache_file) {
do_cache = req.do_cache;
}
this.xhr.cache = do_cache ? "normal" : "none";
this.xhr.responseType = "push";
this.xhr.onprogress = function (evt) {
if (!evt) return;
if (evt.buffer.byteLength) {
let new_bytes = evt.buffer.slice(0);
req.ab_queue.push(new_bytes);
req.ab_total_bytes += new_bytes.byteLength;
req.up_rate = evt.bps;
//gather manifest
if (req.manifest_type) {
req.manifest_ab = cat_buffer(req.manifest_ab, new_bytes);
}
//gather cache (can be media or manifest)
if (req.cache_file) {
req.cache_file.data = cat_buffer(req.cache_file.data, new_bytes);
}
let client_rate, req_time;
[client_rate, req_time] = req.get_down_rate();
let buf_size = req.ab_total_bytes - req.bytes_in_req;
//do_log(GF_LOG_DEBUG, `Up rate ${evt.bps} bps - down rate ${client_rate} bps ${buf_size} queued bytes`);
if (buf_size > 1000000) return 10000;
else if (buf_size > 200000) return client_rate;
return 0;
}
};
this.xhr.onerror = function() {
do_log(GF_LOG_ERROR, `Request failed for ${req.target_url}`);
if (req.reply) return;
req.reply = 502;
req.send();
req.read = req._read_abort;
if (req.cache_file) {
req.cache_file.xhr_status = req.reply;
req.cache_file.aborted = true;
req.cache_file.done = true;
//flush all pending requests that were waiting for XHR status
if (req.cache_file.pending_reqs) {
req.cache_file.pending_reqs.forEach(r => r.flush_request() );
req.cache_file.pending_reqs = null;
}
}
};
this.xhr.onreadystatechange = function() {
if (this.readyState == 4) {
do_log(GF_LOG_DEBUG, `${req.target_url} received`);
if (req.manifest_type) {
update_manifest(req.manifest_ab, req.target_url, req.service, req.cache_file);
req.manifest_ab = null;
}
//cache can be set for manifests or media
if (req.cache_file) {
do_log(GF_LOG_INFO, `${req.target_url} closing cache file`);
req.cache_file.done = true;
//purge delay applies on received file
req.cache_file.received = sys.clock_ms();
}
req.xhr_done = true;
//and move to XHR cache
if (req.service) {
req.xhr.last_used = sys.clock_ms();
req.service.xhr_cache.push(req.xhr);
req.xhr = null;
}
return;
}
if (this.readyState != 2) return;
let content_range = null;
if (req.cache_file) req.cache_file.headers = [];
//we got our headers
let headers = this.getAllResponseHeaders().split('\r\n');
headers.forEach(n => {
let h = n.split(': ');
let hdr = { "name" : h[0], "value": h[1]};
//filter headers
let h_name = h[0].toLowerCase();
if (h_name === 'upgrade') return;
if (h_name === ':status') return;
if (h_name === 'content-range') content_range = h[1];
req.headers_out.push(hdr);
if (req.cache_file) {
//cache some headers
if ((h_name !== 'content-length')
&& (h_name !== 'transfer-encoding')
&& (h_name !== 'server')
&& (h_name !== 'date')
) {
req.cache_file.headers.push(hdr);
}
}
if (h_name == 'content-type') {
if ((h[1].indexOf('application/vnd.apple')>=0) || (h[1].indexOf('x-mpegurl')>=0)) {
req.manifest_type = MANI_HLS;
}
else if (h[1].indexOf('dash+xml')>=0) {
req.manifest_type = MANI_DASH;
}
if (req.cache_file) req.cache_file.mime = h[1];
}
});
do_log(GF_LOG_DEBUG, `Sending reply to ${req.url} code ${req.xhr.status}`);
req.reply = req.xhr.status;
if (req.service && req.service.mabr) {
const val = req.live_edge ? 'no' : 'off-edge';
req.service.update_mabr_stats(val);
req.headers_out.push( { "name" : 'X-From-MABR', "value": val} );
}
req.send();
if (req.cache_file) {
req.cache_file.xhr_status = req.reply;
if (req.reply == 206) req.cache_file.range = content_range;
//prune if error
if (req.reply>=400) {
req.cache_file.headers = null;
req.cache_file.aborted = true;
req.xhr_done = true;
}
//flush all pending requests that were waiting for XHR status
if (req.cache_file.pending_reqs) {
req.cache_file.pending_reqs.forEach(r => r.flush_request() );
req.cache_file.pending_reqs = null;
}
}
};
this.xhr.open(this.method, this.target_url);
this.xhr.send();
};
//preprocess headers to locate host and extract range
let disable_mabr_cache = false;
let host = null;
let referer = null;
req.headers_in.forEach(h => {
let hlwr = h.name.toLowerCase();
if (hlwr == "host") host = h.value;
if (hlwr == "referer") referer = h.value;
else if (hlwr == ":authority") host = h.value;
else if ((hlwr == "x-from-mabr") && (h.value.toLowerCase() == 'no')) disable_mabr_cache = true;
else if (hlwr == "range") {
let hdr_range = h.value.split('=');
let range = hdr_range[1];
if (range.indexOf(',')>=0) {
req.no_cache = true;
} else {
let idx = range.indexOf('-');
req.br_start = parseInt(range.substring(0, idx));
req.br_end = parseInt(range.substring(idx+1));
//only cache closed ranges
if (!idx || !req.br_end) req.no_cache = true;
}
}
});
let check_any_allowed = true;
//support http://myhost/http://url - not really standard
if (!req.service &&
(req.url.startsWith('/http://')
|| req.url.startsWith('/http%3A%2F%2F')
|| req.url.startsWith('/https://')
|| req.url.startsWith('/https%3A%2F%2F')
)) {
req.target_url = req.url.substring(1);
if ((target_url.indexOf("127.0.0.1")>=0) || (target_url.indexOf("localhost")>=0)) {
req.reply = 405;
req.send('Must use Host for localhost proxying');
return;
}
}
//look in mount points
if (!req.target_url) {
let host = null;
let full_name = true;
let service_def = services_defs.find( s => (s.local && (req.url.indexOf(s.local)==0) ) );
if (!service_def) {
service_def = services_defs.find( s => (s.local_base && (req.url.indexOf(s.local_base)==0) ) );
full_name = false;
}
//special case for MABR services, check we are loaded
if (service_def && service_def.mabr) {
req.service = locate_service(req.url);
//if http source, only start once we fetch the manifest, otherwise start now
if (!req.service && !service_def.http)
req.service = create_service(null, true, service_def);
req.target_url = req.url;
//fall through
}
//for other services, format target url
if (service_def && !req.service) {
check_any_allowed = false;
req.do_cache = service_def.gcache;
if (service_def.http) {
if (full_name) {
req.target_url = req.url.replace(service_def.local, service_def.http);
} else {
req.target_url = req.url.replace(service_def.local_base, service_def.http_proto + '://' + service_def.http_host + service_def.http_path);
}
} else {
req.target_url = req.url;
}
}
if (!service_def && referer) {
let src = referer.split('://');
src = src[(src.length>1) ? 1 : 0];
src = src.split('/');
src.shift();
src = '/'+src.join('/');
service_def = services_defs.find( s => (s.local_base && (src.indexOf(s.local_base)==0) ) );
if (service_def) {
check_any_allowed = false;
req.do_cache = service_def.gcache;
req.target_url = service_def.http_proto + '://' + service_def.http_host + req.url;
}
}
//local dir or custom JS
if (service_def && !service_def.http && !service_def.mabr) {
//custom JS handler
if (service_def.js_mod) {
let resolved, delayed;
[resolved, delayed ] = service_def.js_mod.resolve(req);
if (!resolved) {
if (!req.reply) {
do_log(GF_LOG_DEBUG, `Invalid JS handler for ${req.url}`);
req.reply = 404;
req.send();
}
return;
}
if (delayed) return;
if (typeof resolved == 'string') {
if (req.from_cache) {
req.read = req._read_from_buffer;
req.target_url = resolved;
} else if (resolved.indexOf('http')>=0) {
req.target_url = resolved;
} else if (typeof resolved == 'string') {
req.body = resolved;
req.send();
return;
}
} else if (typeof resolved == 'object') {
req.jsmod = resolved;
if (typeof resolved.read == 'function') {
req.read = req._read_from_mod;
return;
}
//otherwise we read from cache and module will populate it
req.read = req._read_from_buffer;
req.from_cache = true;
} else {
req.reply = 501;
req.send('Invalid module resolution');
return;
}
}
//custom mount point
else if (service_def.sources.length && service_def.local_base) {
let my_url = req.url.replace(service_def.local_base, '');
let e = service_def.sources.find( a => (a.name == my_url) );
if (!e) e = service_def.sources.find( a => my_url.startsWith(a.name) );
if (!e) {
do_log(GF_LOG_DEBUG, `Invalid service definition for ${req.url}`);
req.reply = 404;
req.send();
return;
}
if (e.url.startsWith('http')) {
req.target_url = e.url;
} else if (e.url.indexOf('://')>=0) {
req.reply = 501;
req.send('Protocol not supported');
return;
} else {
my_url = my_url.replace(e.name, e.url);
if (sys.file_exists(my_url)) {
req.body = my_url;
req.send();
return;
} else {
do_log(GF_LOG_DEBUG, `Invalid local path for ${req.url}`);
req.reply = 404;
req.send();
return;
}
}
}
}
if (req.target_url && !req.from_cache && check_any_allowed) {
//rewrite request url
req.url = req.target_url.substring(req.target_url.indexOf('/', 8));
}
}
//running as real proxy, check host
if (!req.target_url) {
if (!host) {
req.reply = 405;
req.send('Invalid host name');
return;
}
let hport = req.tls ? 443 : 80;
let splith = host.split(':');
if (splith.length>1) {
hport = parseInt(splith[splith.length-1]);
}
if ((host.indexOf("127.0.0.1")>=0) || (host.indexOf("localhost")>=0)) {
let e = server_ports.find( e => e == hport);
if (e) {
req.reply = 405;
req.send('Invalid host pointing to this proxy');
return;
}
}
let use_tls=false;
if (hport!==443) {
let url_no_proto = host + req.url;
let service = all_services.find( s => (url_no_proto.indexOf(s.base_url_no_proto)>=0) );
if (service && service.force_tls) use_tls = true;
} else {
use_tls = true;
}
//remove default ports in host
if (use_tls && (hport == 443)) host = host.replace(":443", "");
else if (!use_tls && (hport == 80)) host = host.replace(":80", "");
req.target_url = (use_tls ? 'https://' : 'http://') + host + req.url;
}
if (! req.target_url) {
do_log(GF_LOG_DEBUG, `No service found for ${req.url}`);
req.reply = 404;
req.send('No such service');
return;
}
do_log(GF_LOG_INFO, `Processing request ${req.method} for ${req.url}`);
do_log(GF_LOG_DEBUG, `Resolved request URL ${req.target_url}`);
let url_lwr = req.target_url.toLowerCase();
if (url_lwr.indexOf('.m3u8')>=0) req.manifest_type = MANI_HLS;
else if (url_lwr.indexOf('.mpd')>=0) req.manifest_type = MANI_DASH;
else req.manifest_type = 0;
//locate service
let period, active_rep;
[req.service, active_rep, period] = locate_service_quality(req.target_url, req.service);
let purge_delay = req.service ? req.service.purge_delay : 0;
if (!purge_delay) req.no_cache = true;
//example of how to force highest bandwidth - this assumes segment alignment across all reps and bitstream switching
//not working for HLS as we would need to extract the segment name from the target variant playlist which we may not have
if (0 && active_rep && !req.service.m3u8) {
let reps = period.reps.filter(r => (active_rep.as_idx== r.as_idx) && (r!==active_rep) );
if (reps.length) {
let force_rep = reps[ reps.length-1];
let idx = req.target_url.indexOf(active_rep.radical);
let new_url = req.target_url.substring(0, idx) + force_rep.radical + active_rep.seg_id + active_rep.suffix;
req.target_url = new_url;
force_rep.seg_id = active_rep.seg_id;
force_rep.seg_dur = active_rep.seg_dur;
active_rep = force_rep;
}
}
if (!req.service && check_any_allowed) {
//pure proxy, check allowed domains
let service_def = services_defs.find( s => (!s.noproxy && (host === s.http_host) && req.url.startsWith(s.http_path)) );
if (!service_def) service_def = services_defs.find( s => (s.http === '*') );
if (!service_def) {
do_log(GF_LOG_ERROR, `Proxying of ${req.url} not allowed`);
req.reply = 404;
req.send('Requested service is not exposed by server');
return;
}
req.do_cache = service_def.gcache;
//little opt: create mabr service for dash before sending the request, so that we can signal X-From-MABR=no in the answer
if (req.manifest_type && service_def.mabr) {
req.service = create_service(req.target_url, true, null);
}
}
if (req.service) {
req.do_cache = req.service.gcache;
//deactivate timeout, will be reactivated when removing request
req.service.unload_timeout = 0;
if (req.manifest_type && !req.service.mani_cache && !req.from_cache) req.no_cache = true;
//check mcast
if (req.service.dyn_mabr) {
req.service.check_dyn_mabr();
}
}
//look in mem cache
if (req.service && !req.no_cache) {
let f = req.service.get_file(req.from_cache ? req.target_url : req.url, req.br_start, req.br_end);
//only cache manifest if not too old - we use more than the min update due to transfer times in mabr
if (f && !f.sticky && (f.cache_type == CACHE_TYPE_MABR) && req.manifest_type && (req.service.url || req.service.mabr_repair_url)) {
if (f.manifest_min_update && (f.received + 3*f.manifest_min_update/2 < sys.clock_ms())) {
//trash
req.service.mem_cache.splice(req.service.mem_cache.indexOf(f) , 1);
do_log(GF_LOG_WARNING, `Cached manifest ${req.url} too old by ${sys.clock_ms() - f.received - f.manifest_min_update} ms - removing and refreshing (${f.received} - ${f.url})`);
f = null;
}
}
if (f) {
do_log(GF_LOG_INFO, `File ${req.url} present in cache ${ f.done ? '(completed)' : '(transfer in progress)'} (${f.received} - ${f.url})`);
req.service.mark_active_rep(active_rep, req);
return req.set_cache_file(f);
}
}
if (!req.service) {
if (req.target_url.startsWith('http')<0) {
req.reply = 404;
req.send('invalid path');
return;
}
do_log(GF_LOG_INFO, `No associated service configured, going for HTTP`);
req.fetch_unicast();
return;
}
//load MABR service if present
if (req.service.mabr && !req.service.mabr_loaded && !disable_mabr_cache) {
req.service.load_mabr();
}
//multicast service only
if (req.service.mabr && !req.service.url && !active_rep && !disable_mabr_cache) {
req.waiting_mabr_start = sys.clock_ms();
req.waiting_mabr = req.waiting_mabr_start + MABR_TIMEOUT_SAFETY;
req.service.pending_reqs.push(req);
return;
}
if (req.from_cache) {
req.service.pending_reqs.push(req);
return;
}
if (active_rep) {
//mark rep active - this will trigger mcast joining so do this before checking mcast
req.service.mark_active_rep(active_rep, req);
if (disable_mabr_cache) {
do_log(GF_LOG_INFO, `Rep ${active_rep.ID} seg ${req.url} disbaled MABR for this request, going for HTTP`);
}
else if (req.service && req.service.mabr_failure) {
do_log(GF_LOG_INFO, `Rep ${active_rep.ID} seg ${req.url} MABR setup failed, going for HTTP`);
}
//check if we have mabr active
else if (active_rep.live_edge && active_rep.mabr_active) {
if (req.service && req.service.last_mabr_active && (req.service.last_mabr_active + 3*active_rep.seg_dur/2 < req.start_time)) {
do_log(GF_LOG_WARNING, `Rep ${active_rep.ID} MABR is down since ${req.start_time - req.service.last_mabr_active} ms, going for HTTP`);
}
else if (active_rep.first_mabr_tune) {
do_log(GF_LOG_INFO, `Rep ${active_rep.ID} seg ${req.url} first segment request and no low-latency, going for HTTP`);
active_rep.first_mabr_tune = 0;
} else {
//set a timeout for the request in case MABR fails
req.waiting_mabr_start = sys.clock_ms();
//we use 3 x segment duration since we don't know if the source was low-latency or not so in worst case we have:
//- one seg_dur delay for multicaster to send the file
//- one seg_dur delay for the file to be received
//- repair delay
req.waiting_mabr = req.waiting_mabr_start + 3*active_rep.seg_dur + MABR_TIMEOUT_SAFETY;
do_log(GF_LOG_INFO, `Waiting for file ${req.url} from MABR (ID ${req.service.mabr_service_id}) - will wait max ${req.waiting_mabr-req.waiting_mabr_start} ms for seg dur ${active_rep.seg_dur}`);
req.service.pending_reqs.push(req);
return;
}
} else if (active_rep.mabr_active) {
do_log(GF_LOG_INFO, `Rep ${active_rep.ID} seg ${req.url} num ${active_rep.seg_num} is not on live edge ${active_rep.live_seg_num}, going for HTTP`);
} else if (req.service.mabr_service_id) {
do_log(GF_LOG_INFO, `Rep ${active_rep.ID} seg ${req.url} has no active multicast, going for HTTP`);
} else if (req.service.mabr) {
do_log(GF_LOG_INFO, `Rep ${active_rep.ID} seg ${req.url} waiting for multicast bootstrap, going for HTTP`);
}
}
//fetch from unicast
req.fetch_unicast();
}
function create_service(http_url, force_mcast_activate, forced_sdesc)
{
//mabr service is null by default
let s = {};
s.url = http_url;
s.mabr = null;
s.local = null;
s.mabr_loaded = false;
s.mabr_failure = false;
s.mem_cache = [];
s.id = all_services.length+1;
s.mabr_service_id = 0;
s.source = null;
s.sink = null;
s.pending_reqs=[];
s.active_reps_timeouts=[];
s.nb_mabr_active = 0;
s.mabr_unload_timeout = 0;
s.dyn_mabr = false;
s.last_mabr_active = 0;
s.unload_timeout = 0;
s.purge_delay = 0;
s.mani_cache = false;
s.gcache = false;
s.check_ip = DEFAULT_CHECKIP;
s.keepalive = DEFAULT_KEEPALIVE_SEC;
s.xhr_cache = [];
s.pending_deactivate = [];
s.mabr_repair_url = null;
if (http_url && http_url.toLowerCase().startsWith('https://')) s.force_tls = true;
else s.force_tls = false;
s.hdlr = null;
s.manifest = null;
//do we have a mcast service for this ?
let serv_cfg = forced_sdesc;
if (!forced_sdesc) {
let url_noport = http_url.replace(/([^\/\:]+):\/\/([^\/]+):([0-9]+)\//, "$1://$2/");
serv_cfg = services_defs.find(e => e.http == url_noport);
if (!serv_cfg) serv_cfg = services_defs.find(e => e.http == http_url);
}
if (serv_cfg) {
do_log(GF_LOG_DEBUG, `Service ${forced_sdesc ? forced_sdesc.local_base : http_url} has custom config${ (serv_cfg.mabr ? ' and MABR' : '')}`);
s.mabr = serv_cfg.mabr;
s.unload = serv_cfg.unload;
s.mabr_min_active = serv_cfg.activate;
s.purge_delay = 1000 * serv_cfg.timeshift;
s.mani_cache = serv_cfg.mcache;
s.repair = serv_cfg.repair;
s.local = serv_cfg.local;
s.keepalive = serv_cfg.keepalive;
s.gcache = serv_cfg.gcache;
s.check_ip = serv_cfg.check_ip;
if (serv_cfg.id) s.id = serv_cfg.id;
s.dyn_mabr = serv_cfg.dyn_mabr;
}
if (!http_url) {
s.base_url = serv_cfg.local_base;
s.base_url_no_proto = serv_cfg.local_base;
} else {
//remove cgi and remove resource name
let str = http_url.split('?');
str = str[0].split('/')
str.pop();
s.base_url = str.join('/') + '/';
//remove protocol scheme
s.base_url_no_proto = s.base_url.split('://')[1];
}
s.get_file = function(url, br_start, br_end)
{
let file = null;
for (let i=0; i<this.mem_cache.length; i++) {
let f = this.mem_cache[i];
if (f.aborted) continue;
if (f.url.indexOf(url)>=0) {}
//in case the whole server path was not sent in mabr
else if (f.cache_type && (url.indexOf(f.url)>=0)) {}
else continue;
//only deal with exact byte ranges - we could optimize to allow sub-ranges
if (f.br_start==br_start && f.br_end==br_end) return f;
}
return null;
};
s.unqueue_pending_request = function(url, from_mabr)
{
let ext=null;
let pending = this.pending_reqs.find (r =>{
if (url.indexOf(r.url)>=0) return true;
//in case the whole server path was not sent in mabr
if (from_mabr && (r.url.indexOf(url)>=0) ) return true;
if (r.from_cache && (r.url.indexOf(url)>=0) ) return true;
return false;
});
//if no match and direct multicast, locate manifests by extension
if (!pending && from_mabr && !s.url) {
let ext = url.split('.').pop().toLowerCase();
if ((ext==="mpd") || (ext==="m3u8")) {
pending = this.pending_reqs.find (r => {
if (r.url.indexOf(ext)>=0) return true;
return false;
});
}
}
if (pending)
this.pending_reqs.splice(this.pending_reqs.indexOf(pending), 1);
return pending;
};
s.create_cache_file = function(url, mime, br_start, br_end, cache_type=CACHE_TYPE_MOD, pid=null)
{
let file = {};
file.url = url;
file.mime = mime || 'video/mp4';
file.received = sys.clock_ms();
file.data = new ArrayBuffer();
file.data_tmp = null;
file.data_tmp_done = false;
this.mem_cache.push(file);
file.done = false;
file.aborted = false;
file.xhr_status = 0;
file.pending_reqs = null;
file.headers = null;
file.nb_users = 0;
file.sticky = 0;
file.br_start = br_start;
file.br_end = br_end;
file.cache_type = cache_type;
file.range = null;
file.pid = pid;
file.manifest_min_update = 0;
if (cache_type) {
file.xhr_status = 200;
}
file.release = function(req=null) {
this.nb_users --;
if (!this.nb_users && this.data_tmp) {
this.data = this.data_tmp;
this.data_tmp = null;
this.done = this.data_tmp_done;
this.data_tmp_done = false;
}
//special case for files created for out of date cached manifests, revert cache type to MABR
if (req && !this.nb_users && req.service && req.manifest_type && req.service.mani_cache) {
this.cache_type = CACHE_TYPE_MABR;
this.xhr_status = 200;
}
};
//get any pending request(s) for this file
while (true) {
//we use req.url , eg server path, and not target_url which includes server name
let pending = this.unqueue_pending_request(url, (cache_type==CACHE_TYPE_MABR));
if (!pending) break;
pending.waiting_mabr = 0;
pending.set_cache_file(file);
let ellapsed = pending.waiting_mabr_start ? (sys.clock_ms() - pending.waiting_mabr_start) : 0;
do_log(GF_LOG_DEBUG, `Found pending request (${url} ${pending.target_url}) for ${file.url}, canceling timeout (${ellapsed} ms since request)`);
if (pid) pid.deactivate_timeout=0;
}
do_log(GF_LOG_DEBUG, `Start ${(cache_type==CACHE_TYPE_MABR) ? 'MABR ' : ''}reception of ${file.url} mime ${file.mime} - ${this.mem_cache.length} files in service cache`);
return file;
};
s.mark_active_rep = function(rep, request)
{
if (!rep || !request) return;
let use_same_conn = false;
let connection_id = s.check_ip ? (request.ip + ':'+request.port) : request.netid;
let idx = s.pending_deactivate.indexOf(rep);
if (idx>=0) s.pending_deactivate.splice(idx, 1);
//mark number of active requests for this quality
if (!rep.nb_active) {
do_log(GF_LOG_INFO, `Service ${this.id} Rep ${rep.ID} is now active`);
if (!this.repair)
rep.first_mabr_tune = 1;
} else {
//check if we have active requests on the same connection to be deactivated. If so, extend timeout of last found
//and do not change nb_active
//this avoids activate/deactivate on same connection which would trigger spurious mcast joins/leaves
let same_conn = this.active_reps_timeouts.filter(e => e.rep.ID == rep.ID && e.cid == connection_id);
if (same_conn.length) {
let rep_to = same_conn[same_conn.length-1];
rep_to.update();
do_log(GF_LOG_DEBUG, `Service ${this.id} same connection used for same quality ${rep.ID}, extending inactive timeout`);
use_same_conn = true;
}
}
if (!use_same_conn) {
rep.nb_active++;
request.activate_rep_timeout = {"rep": rep, "timeout": 0, "cid": connection_id, "mabr_canceled": false};
request.activate_rep_timeout.update = function() {
//timeout for active quality is segment duration + safety
this.timeout = sys.clock_ms() + this.rep.seg_dur + DEACTIVATE_TIMEOUT_MS;
};
request.activate_rep_timeout.update();
this.active_reps_timeouts.push( request.activate_rep_timeout );
}
//check if we need to activate multicast
if (this.mabr_loaded && !rep.mabr_active
&& (!this.mabr_min_active || (rep.nb_active>=this.mabr_min_active))
) {
this.activate_mabr(rep, true);
}
};
s.activate_mabr = function(rep, do_activate)
{
if (!this.mabr_service_id || !this.source || !rep) return;
if (serv_cfg && serv_cfg.js_mod && (serv_cfg.js_mod.quality_activation != null)) {
let req_ok = serv_cfg.js_mod.quality_activation(do_activate, this.mabr_service_id, rep.period_id, rep.AS_ID, rep.ID);
if (!req_ok) return;
}
if (do_activate) {
this.nb_mabr_active++;
this.mabr_unload_timeout = 0;
do_log(GF_LOG_INFO, `Service ${this.id} MABR activated for Rep ${rep.ID} - active in service ${this.nb_mabr_active}`);
} else if (this.nb_mabr_active) {
this.nb_mabr_active--;
do_log(GF_LOG_INFO, `Service ${this.id} MABR deactivated for Rep ${rep.ID} - active in service ${this.nb_mabr_active}`);
if (!this.nb_mabr_active && this.unload) {
this.mabr_unload_timeout = sys.clock_ms() + 1000*this.unload;
}
//no deactivation of multicast channels
if (this.mabr_min_active==0) {
rep.mabr_active = true;
return;
}
}
let evt = new FilterEvent(GF_FEVT_DASH_QUALITY_SELECT);
evt.service_id = this.mabr_service_id;
evt.period_id = rep.period_id;
evt.as_id = rep.AS_ID;
evt.rep_id = rep.ID;
evt.select = do_activate ? 0 : 1;
rep.mabr_active = do_activate;
session.fire_event(evt, this.source, false, true);
};
s.load_mabr = function()
{
if (!this.mabr || this.mabr_loaded) return;
//load routein
let args = 'src=' + this.mabr;
if (this.mabr.indexOf('gpac:')<0) args += ':gpac';
//routein config:
// - no-cache mode to fetch files directly
// - single pid per TSI to have files for a given quality on a single PID
// - keep alive to disable timeout when multicast is down
// - require source ID in case we use a capture file, we could get the first PID before returning from the add_filter function
args += ':gcache=0:stsi:ka:RSID';
// - multicast is dynamically enabled/disabled, start with all services tuned but disabled
// only do this if HTTP mirror - otherwise, activate everything to make sure we fetch the manifests and init segments
if (this.url && this.mabr_min_active>0) args += ':tunein=-3';
//add repair option last
if (!this.repair) { // false (default) - disable unicast repair
args += ':repair=strict';
} else {
args += ':repair=full';
if (this.repair == 1 && this.url) {
args += '::repair_urls='+this.url;
}
}
this.source = session.add_filter(args);
if (!this.source) {
do_log(GF_LOG_ERROR, `Service ${this.id} Failed to load MABR demux for ${this.mabr}`);
this.mabr_failure = true;
return;
}
this.source.require_source_id();
this.mabr_loaded = true;
this.nb_mabr_active = 0;
this.mabr_unload_timeout = 0;
this.last_mabr_active = 0;
//create custom sink accepting files
this.sink = session.new_filter("RouteSink"+this.id);
this.sink.set_cap({id: "StreamType", value: "File", in: true} );
this.sink.max_pids=-1;
this.sink.pids=[];
this.sink.on_setup_error = function(srcf, err) {
do_log(GF_LOG_WARNING, `Service ${s.id} MABR setup error ${sys.error_string(err)}`);
s.mabr_failure = true;
if (s.sink) session.remove_filter(s.sink);
if (s.source) session.remove_filter(s.source);
s.sink = null;
s.source = null;
};
this.sink.watch_setup_failure(this.source);
this.sink.configure_pid = function(pid)
{
//reconfigure (new file), get previous file and mar as done
if (this.pids.indexOf(pid)>=0) {
//get previous file
if (pid.url) {
let file = s.mem_cache.find(f => f.url===pid.url);
if (file && !file.done) {
do_log(GF_LOG_INFO, `${pid.url} closing cache file as new file from MABR starts ${pid.get_prop('URL')}`);
file.done = true;
}
}
} else {
//initial declaration
this.pids.push(pid);
//send play
let evt = new FilterEvent(GF_FEVT_PLAY);
evt.start_range = 0.0;
pid.send_event(evt);
//no repair, we must dispatch full files
if (!s.repair) pid.framing = true;
//auto-deactivate unless unload is set
if (s.mabr_min_active) {
pid.deactivate_timeout = MABR_PID_DEACTIVATION_TIMEOUT + sys.clock_ms();
}
else
pid.deactivate_timeout = 0;
}
if (!s.mabr_service_id) {
s.mabr_service_id = pid.get_prop('ServiceID');
do_log(GF_LOG_INFO, `MABR configured for service ${s.mabr_service_id}`);
}
s.last_mabr_active = sys.clock_ms();
//get new URL for this pid - can be null when service is just being announced
pid.url = pid.get_prop('URL');
if (pid.url && (pid.url.charAt(0) != '/')) pid.url = '/' + pid.url;
pid.mime = pid.get_prop('MIMEType');
pid.corrupted = false;
let is_manifest=false;
if (pid.url) {
let purl = pid.url.toLowerCase();
//do not cache HLS/DASH manifests
if ((purl.indexOf('.m3u8')>=0)|| (purl.indexOf('.mpd')>=0)) is_manifest = true;
else is_manifest = false;
}
pid.do_skip = false;
pid.is_manifest = false;
if (is_manifest) {
pid.is_manifest = true;
//we want full files for the manifest
pid.framing = true;
if ((s.url !== null) && !s.mani_cache)
pid.do_skip = true;
pid.deactivate_timeout = 0;
}
if (!s.url) {
let urls = pid.get_prop('MABRBaseURLs');
s.mabr_repair_url = urls ? urls[0] : null;
}
};
this.push_to_cache = function (pid, pck) {
let file = this.mem_cache.find(f => f.url===pid.url);
let corrupted = pck.corrupted ? 1 : 0;
if (corrupted && pck.get_prop('PartialRepair')) {
corrupted = 2;
}
//file corrupted and no repair, move to HTTP
if (!s.repair && pck.corrupted) {
let log_done = false;
//cache file shall never be created at this point since we only aggregate full files when no repair
if (file) {
do_log(GF_LOG_ERROR, `Service ${this.id} receiving corrupted MABR packet and cache file was already setup, bug in code !`);
file.data.byteLength = 0;
}
//get any pending request(s) for this file
while (true) {
let pending = this.unqueue_pending_request(pid.url, true);
if (!pending) break;
//signal mabr was canceled
pending.waiting_mabr = 0;
if (pending.activate_rep_timeout) {
pending.activate_rep_timeout.update();
pending.activate_rep_timeout.mabr_canceled = true;
}
if (!log_done) {
do_log(GF_LOG_INFO, `Corrupted MABR packet for ${pid.url} (valid container ${corrupted==2}), switching to HTTP`);
log_done = true;
}
//the first one will create a cache file if possible, associated it to the other pending requests and potentially removing them
pending.fetch_unicast();
}
if (!log_done) {
do_log(GF_LOG_INFO, `Corrupted MABR packet for ${pid.url} (valid container ${corrupted==2}), removing`);
}
return;
} else if (corrupted) {
do_log(GF_LOG_WARNING, `MABR Repair failed for ${pid.url} (valid container ${corrupted==2}), broken data sent to client`);
}
if (!file) {
file = this.create_cache_file(pid.url, pid.mime, 0, 0, CACHE_TYPE_MABR, pid);
}
//cache file exists, it was created from a pending request or an old request
else if (pck.start) {
//update to a file being sent, we need to create a temp buffer
//which will be reassigned once no more users are on the file
if (file.nb_users) {
file.data_tmp = new ArrayBuffer();
file.data_tmp_done = false;
} else {
file.data = new ArrayBuffer();
file.data_tmp = null;
file.data_tmp_done = false;
file.done = false;
}
}
do_log(GF_LOG_DEBUG, `Service ${this.id} receiving MABR packet for ${pid.url} size ${pck.size} end ${pck.end}`);
//reagregate packet
if (pck.size) {
if (file.data_tmp)
file.data_tmp = cat_buffer(file.data_tmp, pck.data);
else
file.data = cat_buffer(file.data, pck.data);
}
if (pck.start && pck.end) {
do_log(GF_LOG_INFO, `Service ${this.id} got MABR file ${pid.url} in one packet`);
}
else if (pck.start) {
do_log(GF_LOG_INFO, `Service ${this.id} start receiving MABR file ${pid.url}`);
}
else if (pck.end) {
do_log(GF_LOG_INFO, `Service ${this.id} received MABR file ${pid.url}`);
}
if (pck.end) {
if (file.data_tmp) {
if (!file.nb_users) {
file.data = file.data_tmp;
file.data_tmp = null;
file.data_tmp_done = false;
file.done = true;
} else {
file.data_tmp_done = true;
}
} else {
file.done = true;
}
file.received = sys.clock_ms();
if (pid.is_manifest) {
update_manifest(file.data_tmp ? file.data_tmp : file.data, file.url, s, file);
}
}
};
this.sink.process = function(pid)
{
let has_pck=false;
this.pids.forEach(function(pid) {
while (1) {
let pck = pid.get_packet();
if (!pck) break;
if (!pid.do_skip && !pid.corrupted)
s.push_to_cache(pid, pck);
pid.drop_packet();
has_pck=true;
}
if (pid.deactivate_timeout && (pid.deactivate_timeout<sys.clock_ms())) {
print(GF_LOG_INFO, `PID ${pid.url} without active requests - deactivating multicast`);
let evt = new FilterEvent(GF_FEVT_DASH_QUALITY_SELECT);
evt.service_id = s.mabr_service_id;
evt.period_id = pid.get_prop('Period');
evt.as_id = pid.get_prop('ASID');
evt.rep_id = pid.get_prop('Representation');
evt.select = 1;
session.fire_event(evt, s.source, false, true);
pid.deactivate_timeout = 0;
}
});
if (has_pck) s.last_mabr_active = sys.clock_ms();
};
//don't use dash client, we just need to get the files
this.sink.set_source(this.source);
do_log(GF_LOG_INFO, `Service ${this.id} loaded MABR for ${this.mabr}${this.repair ? ' with repair' : ''}`);
if (serv_cfg && serv_cfg.js_mod && typeof serv_cfg.js_mod.service_activation === 'function') {
serv_cfg.js_mod.service_activation(true);
}
};
s.unload_mabr = function() {
if (this.source) {
do_log(GF_LOG_INFO, `Service ${this.id} stopping MABR`);
if (this.sink) session.remove_filter(this.sink);
if (this.source) session.remove_filter(this.source);
this.sink = null;
this.source = null;
this.mabr_loaded = false;
this.mabr_failure = false;
if (serv_cfg && serv_cfg.js_mod && typeof serv_cfg.js_mod.service_activation === 'function') {
serv_cfg.js_mod.service_activation(false);
}
//cancel all pending requests on MABR files
this.check_pending_requests(sys.clock_ms(), true);
//clear all cache files from the multicast
this.mem_cache.forEach(f => {
if (f.cache_type == CACHE_TYPE_MABR) {
f.done = true;
f.aborted = true;
}
});
//abort all active reps
for (let i=0; i<this.active_reps_timeouts.length; i++) {
let o = this.active_reps_timeouts[i];
let active_rep = o.rep;
this.active_reps_timeouts.splice(i, 1);
i--;
active_rep.mabr_active=0;
}
this.mabr_service_id = 0;
}
};
s.check_dyn_mabr = function() {
let mabr_old = this.mabr;
this.mabr = serv_cfg.js_mod.get_mcast_address(serv_cfg.http);
if (this.mabr == mabr_old) return;
if (this.mabr_loaded) this.unload_mabr();
if (!this.mabr) return;
this.load_mabr();
};
s.check_pending_requests = function(now, force_deactivate) {
//check timeout on pending reqs waiting for MABR
for (let i=0; i<this.pending_reqs.length; i++) {
let req = this.pending_reqs[i];
if (!force_deactivate) {
if (!req.waiting_mabr || (req.waiting_mabr>now)) continue;
}
if (! this.url && !this.mabr_repair_url) {
do_log(GF_LOG_DEBUG, `MABR timeout for ${req.url} after ${now - req.waiting_mabr_start} ms`);
this.pending_reqs.splice(i, 1);
i--;
req.reply = 404;
req.send();
continue;
}
do_log(GF_LOG_DEBUG, `MABR timeout for ${req.url} after ${now - req.waiting_mabr_start} ms - fetching from HTTP at ` + req.target_url);
this.pending_reqs.splice(this.pending_reqs.indexOf(req), 1);
i--;
if (req.activate_rep_timeout) {
req.activate_rep_timeout.update();
req.activate_rep_timeout.mabr_canceled = true;
if (req.cache_file) req.cache_file.cache_type = CACHE_TYPE_HTTP
}
req.waiting_mabr = 0;
//the first one will create a cache file if possible, associated it to the other pending requests and potentially removing them
let prev_len = this.pending_reqs.length;
req.fetch_unicast();
//if some pending were removed, restart the loop
if (prev_len != this.pending_reqs.length) {
i = -1;
}
}
};
all_services.push(s);
do_log(GF_LOG_INFO, `Created Service ID ${s.id} for ${s.url ? s.url : serv_cfg.local_base}${(s.mabr || s.dyn_mabr) ? ' with MABR' : ''}`);
//and load if requested - we force mcast activation when an access to the mpd is first detected
if (force_mcast_activate && s.mabr)
s.load_mabr();
s.stats = {
"X-From-MABR": {
"yes": 0,
"no": 0,
"off-edge": 0
}
}
s.update_mabr_stats = function(x_from_mabr){
if(this.stats["X-From-MABR"][x_from_mabr] != undefined){
this.stats["X-From-MABR"][x_from_mabr]++
} else {
do_log(GF_LOG_INFO, `update_mabr_stats ${this.stats} `);
}
}
return s;
}
globalThis.all_services = all_services;
globalThis.create_service = create_service;
let script_args=null;
function do_init()
{
if (http_out_f == null) {
let rdir = (sys.file_exists('.gpac_auth')) ? '.gpac_auth' : "gmem";
sys.args.forEach(a => {
if (a.indexOf('rdirs=')>=0) rdir = null;
});
if (script_args && script_args.indexOf('rdirs=')>=0) rdir = null;
let http_args = "httpout";
if (rdir) http_args += ":rdirs="+rdir;
if (script_args) http_args += script_args;
try {
let http = session.add_filter(http_args);
server_ports = http.get_arg("port");
do_log(GF_LOG_INFO, `Starting HTTP server on port ${server_ports}`);
} catch (e) {
do_log(GF_LOG_ERROR, `Failed to start HTTP server`);
sys.exit(1);
}
} else {
server_ports = http_out_f.get_arg("port");
do_log(GF_LOG_INFO, `Attached server running on port ${server_ports}`);
}
//preload services
services_defs.forEach(sd => {
if (sd.unload) return;
if (sd.dyn_mabr) return;
let s = create_service(sd.http, false, sd);
if (!s) {
do_log(GF_LOG_ERROR, `Failed to create service ${sd.http}`);
} else {
s.load_mabr();
}
});
session.post_task( () => {
let do_gc = false;
let now = sys.clock_ms();
for (let i=0; i<all_services.length; i++) {
let s = all_services[i];
//cleanup mem cache
for (let i=0; i<s.mem_cache.length; i++) {
let f = s.mem_cache[i];
if (f.nb_users || f.sticky) continue;
if (!f.aborted && (f.received + s.purge_delay >= now)) continue;
s.mem_cache.splice(i, 1);
i--;
do_log(GF_LOG_DEBUG, `Removing ${f.url} from cache - remain ${s.mem_cache.length}`);
}
//check timeout on pending reqs waiting for MABR
s.check_pending_requests(now, false);
//check reps that could be deactivated after a MABR timeout
for (let i=0; i<s.pending_deactivate.length; i++) {
let rep = s.pending_deactivate[i];
if (rep.mabr_deactivate_timeout < now) continue;
s.pending_deactivate.splice(i, 1);
i--;
do_log(GF_LOG_INFO, `Service ${s.id} Rep ${rep.ID} no longer on multicast`);
s.activate_mabr(rep, false);
}
//check timeout on active reps
for (let i=0; i<s.active_reps_timeouts.length; i++) {
let o = s.active_reps_timeouts[i];
if (o.timeout > now) break;
let active_rep = o.rep;
let mabr_canceled = o.mabr_canceled;
s.active_reps_timeouts.splice(i, 1);
i--;
active_rep.nb_active--;
if (!active_rep.nb_active) {
do_log(GF_LOG_INFO, `Service ${s.id} Rep ${active_rep.ID} is now inactive`);
if (s.keepalive)
s.unload_timeout = sys.clock_ms() + 1000*s.keepalive;
} else {
do_log(GF_LOG_DEBUG, `Service ${s.id} Rep ${active_rep.ID} is still active (active clients ${active_rep.nb_active})`);
}
if (active_rep.mabr_active && s.mabr_min_active && (active_rep.nb_active<s.mabr_min_active) ) {
if (!mabr_canceled) {
s.activate_mabr(active_rep, false);
} else {
active_rep.mabr_deactivate_timeout = sys.clock_ms() + 2000;
s.pending_deactivate.push(active_rep);
}
}
}
//cleanup XHRs
for (let i=0; i<s.xhr_cache.length; i++) {
let xhr = s.xhr_cache[i];
if (xhr.last_used + 10000 >= now) continue;
s.xhr_cache.splice(i, 1);
i--;
do_gc = true;
}
//check timeout on MABR service deactivation
if (s.mabr_unload_timeout && (s.mabr_unload_timeout < now)) {
s.mabr_unload_timeout = 0;
if (!s.nb_mabr_active) {
s.unload_mabr();
do_gc = true;
}
}
//check service unload
if (s.unload_timeout && (s.unload_timeout < now)) {
s.unload_mabr();
do_log(GF_LOG_INFO, `Service ${s.id} unloading`);
all_services.splice(i, 1);
if (typeof s.on_close == 'function') s.on_close();
if (!all_services.length && do_quit) {
print("No more services and quit requested - exiting");
session.remove_filter(http_out_f);
return false;
}
i--;
do_gc = true;
}
}
if (force_quit) {
print("No more requests and quit requested - exiting");
session.remove_filter(http_out_f);
return false;
}
if (do_gc) sys.gc();
if (session.last_task) return false;
//perform cleanup every 100ms
return 100;
}, "mediaserver_cleanup");
}
let http_out_f = null;
//catch filter creation
session.set_new_filter_fun( (f) => {
//bind our custom http logic
if (!http_out_f && (f.name == "httpout")) {
http_out_f = f;
f.bind(httpout);
}
});
let mods_pending=0;
function get_base_url(url)
{
let str = url.split('/');
str.pop();
return str.join('/') + '/';
}
filter.initialize = function() {
let gpac_help = sys.get_opt("temp", "gpac-help");
let gpac_doc = (sys.get_opt("temp", "gendoc") == "yes") ? true : false;
if (gpac_help || gpac_doc) {
filter._help = filter_help;
filter.set_help(filter._help);
let scripts = sys.enum_directory(filter.jspath, "*.js").sort((a, b) => a.name.localeCompare(b.name));
scripts.forEach(s => {
if (s.name == "init.js") return;
import(s.name).then(obj => {
filter._help += '## ' + obj.description + '\nModule is loaded when using `js='+s.name.replace('.js', '')+'`\n\n' + obj.help + '\n';
filter.set_help(filter._help);
}).catch(err => {});
});
return GF_OK;
}
do_quit = filter.quit;
script_args = filter.src_args;
if (script_args) {
let local_args = ["quit", "services"];
script_args = script_args.split(':');
local_args.forEach(a => {
let e = script_args.find(e => e.startsWith(a));
if (e) script_args.splice(script_args.indexOf(e), 1);
});
script_args = script_args.join(':');
} else {
script_args="";
}
if (!filter.scfg || !filter.scfg.length) {
//todo: we should allow to specify a default service description file in ~/.gpac
do_log(GF_LOG_WARNING, "No service description provided, configuring as proxy for any URL");
services_defs = [ {'http': '*', 'unload': true} ];
} else {
try {
let ab = sys.load_file(filter.scfg, true);
services_defs = JSON.parse(ab);
if (!Array.isArray(services_defs)) throw "Invalid JSON, expecting array";
services_defs = services_defs.filter(e => ((typeof e.active === 'undefined') || e.active) && typeof e.comment === 'undefined' );
services_defs.forEach(sd => {
//check options are valid
if (typeof sd.active == 'undefined') sd.active = true;
else if (typeof sd.active != 'boolean') sd.active = false;
if (typeof sd.http == 'undefined') sd.http = null;
else if (typeof sd.http != 'string') throw "Missing or invalid http property, expecting string got "+typeof sd.http;
if (typeof sd.mabr == 'undefined') sd.mabr = null;
else if (typeof sd.mabr != 'string') throw "Missing or invalid mabr property, expecting string got "+typeof sd.mabr;
if (typeof sd.local == 'undefined') sd.local = null;
else if (typeof sd.local != 'string') throw "Missing or invalid local property, expecting string got "+typeof sd.local;
if (typeof sd.unload != 'number') sd.unload = DEFAULT_MABR_UNLOAD_SEC;
else if (sd.unload < 0) throw "Invalid unload property, expecting positive number";
if (typeof sd.activate != 'number') sd.activate = DEFAULT_ACTIVATE_CLIENTS;
else if (sd.activate < 0) throw "Invalid activate property, expecting positive number";
if (typeof sd.timeshift != 'number') sd.timeshift = DEFAULT_TIMESHIFT;
else if (sd.timeshift < 0) throw "Invalid timeshift property, expecting positive number";
if (typeof sd.mcache != 'boolean') sd.mcache = DEFAULT_MCACHE;
if (typeof sd.repair == 'boolean') {
sd.repair = sd.repair ? 1 : 0;
} else {
if (typeof sd.repair != 'string') sd.repair = DEFAULT_REPAIR;
if (sd.repair === 'true') sd.repair = 1;
else if (sd.repair === 'auto') sd.repair = 2;
else sd.repair = 0;
}
if (typeof sd.gcache != 'boolean') sd.gcache = DEFAULT_GCACHE;
if (typeof sd.keepalive != 'number') sd.keepalive = DEFAULT_KEEPALIVE_SEC;
else if (sd.keepalive < 0) throw "Invalid keepalive property, expecting positive number";
if (typeof sd.check_ip != 'boolean') sd.check_ip = DEFAULT_CHECKIP;
if (typeof sd.id == 'undefined') sd.id = null;
if (typeof sd.sources == 'undefined') sd.sources = [];
else if (!Array.isArray(sd.sources)) {
throw "Invalid sources property, expecting array";
} else {
sd.sources.forEach(a => {
if (typeof a.name != 'string') throw "Invalid name in source element, expecting string";
if (typeof a.url != 'string') throw "Invalid url in source element, expecting string";
});
}
if (typeof sd.js == 'undefined') sd.js = null;
else if (typeof sd.js != 'string') throw "Missing or invalid js property, expecting string got "+typeof sd.local;
if (typeof sd.noproxy == 'undefined') sd.noproxy = (sd.http && sd.local) ? true : false;
else if (typeof sd.noproxy != 'boolean') throw "Missing or invalid noproxy property, expecting boolean got "+typeof sd.noproxy;
if (!sd.http && !sd.local) throw "At least one of `http` or `local` shall be specified";
//safety checks
if (!sd.unload) sd.keepalive = 0;
if (sd.unload>sd.keepalive) sd.keepalive = sd.unload;
//extract proto, host and path
if (sd.http) {
//remove default ports
let url_lwr = sd.http.toLowerCase();
if (url_lwr.startsWith('https://')) {
sd.http = sd.http.replace(":443/", "/");
}
if (url_lwr.startsWith('http://')) {
sd.http = sd.http.replace(":80/", "/");
}
let idx = sd.http.indexOf('://');
sd.http_proto = sd.http.substring(0, idx);
let str = sd.http.substring(idx+3).split('/');
sd.http_host = str[0];
str.shift();
str.pop();
if(str.length) {
sd.http_path = '/'+str.join('/') + '/';
} else {
sd.http_path = '/';
}
} else {
sd.http_proto = null;
sd.http_host = null;
sd.http_path = null;
}
//if local is a file, get base path
sd.local_base = null;
if (sd.local) {
sd.local = sd.local.split('?')[0];
if (!sd.local.length) sd.local = null;
else if (sd.local.charAt(sd.local.length-1) != '/') {
let str = sd.local.split('/')
str.pop();
sd.local_base = str.join('/') + '/';
} else {
sd.local_base = sd.local;
sd.local = null;
}
}
//load JS module if present
sd.dyn_mabr = false;
});
//remove all inactive services
services_defs = services_defs.filter(e => e.active );
//check for duplicates
services_defs.forEach(sd => {
if (!sd.active) return;
let same_orig = services_defs.filter( e => (e.active && e.http === sd.http));
if (same_orig.length > 1) {
throw `Multiple active services with same origin ${sd.http} not allowed`;
}
});
//check for JS modules
services_defs.forEach(sd => {
if (sd.js) {
mods_pending++;
let script_src = sys.url_cat(filter.scfg, sd.js);
//local path
if (sys.file_exists(script_src)) {}
else if (sys.file_exists(sd.js+'.js')) {
script_src = sd.js+'.js';
}
//path in our filter directory
else {
script_src = filter.jspath + sd.js + '.js';
if (!sys.file_exists(script_src)) throw "File " + sd.js + " does not exist";
}
import(script_src).then(obj_mod => {
if (typeof obj_mod.init !== 'function') throw "Invalid module, missing `init` function";
if (typeof obj_mod.get_mcast_address === 'function') {
sd.dyn_mabr = true;
sd.mabr = null;
}
if (sd.mabr || sd.dyn_mabr) {
if (typeof obj_mod.quality_activation !== 'function') obj_mod.quality_activation = null;
} else {
if (typeof obj_mod.resolve !== 'function') throw "Invalid module, missing `resolve` function";
}
mods_pending--;
sd.logname = sd.js;
sd.log = function(level, msg) {
do_log(level, msg, this.logname);
};
obj_mod.init(sd);
sd.js_mod = obj_mod;
}).catch(e => {
do_log(GF_LOG_ERROR, `Failed to load service script ${sd.js}: ${e}`);
sys.exit(1);
});
} else {
sd.js_mod = null;
}
});
} catch (e) {
do_log(GF_LOG_ERROR, `Failed to load services configuration ${filter.scfg}: ${e}`);
sys.exit(1);
}
}
//after this, filter object is no longer available in JS since we don't set caps or setup filter.process
session.post_task( () => {
if (mods_pending) return 100;
do_init();
return false;
}, "init", 200);
};