/*
 *			GPAC - Multimedia Framework C SDK
 *
 *			Authors: Jean Le Feuvre
 *			Copyright (c) Telecom ParisTech 2020-2024
 *					All rights reserved
 *
 *  This file is part of GPAC / AVGenerator filter
 *
 *  GPAC 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, or (at your option)
 *  any later version.
 *
 *  GPAC 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 library; see the file COPYING.  If not, write to
 *  the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 */

import * as evg from 'evg'
import { Sys as sys } from 'gpaccore'
import { AudioMixer as amix } from 'gpaccore'
import * as os from 'os'

//import {WebGLContext} from 'webgl'

let WebGLContext = null;
import('webgl').then(obj => { WebGLContext = obj.WebGLContext;}).catch(err => {});


const UPDATE_PID = 1;
const UPDATE_SIZE = 1<<1;
const UPDATE_POS = 1<<2;
const UPDATE_CHILD = 1<<3;
const UPDATE_ALLOW_STRING = 1<<4;
const UPDATE_FX = 1<<5;

/*
	global modules imported for our imported modules
*/
globalThis.sys = sys;
globalThis.evg = evg;
globalThis.os = os;

/*
	global variables for our imported modules
*/

//video playing state
globalThis.video_playing = false;
//audio playing state
globalThis.audio_playing = false;
//output video time
globalThis.video_time = 0;
//output video timescale
globalThis.video_timescale = 0;
//output video width
globalThis.video_width = 0;
//output video height
globalThis.video_height = 0;
//output audio time
globalThis.audio_time = 0;
//output audio timescale
globalThis.audio_timescale = 0;
//output audio samplerate
globalThis.samplerate =0;
//output audio channels
globalThis.channels =0;
//current UTC clock in ms
globalThis.current_utc_clock = Date.now();
//is the canvas YUV or not ? This is used at least by scenes/shape.js to cache RGB static textures to YUV
globalThis.canvas_yuv = false;
//scene update flag indicating the pid has been reconfigured
globalThis.UPDATE_PID = UPDATE_PID;
//scene update flag indicating the scene size has changed
globalThis.UPDATE_SIZE = UPDATE_SIZE;
//scene update flag indicating the scene properties other than size have changed
globalThis.UPDATE_POS = UPDATE_POS;
//scene update allows string for number
globalThis.UPDATE_ALLOW_STRING = UPDATE_ALLOW_STRING;
//scene update flag indicating that if a property with this flag is changed, mix and transition effects must be recomputed (used for OpenGL shader setup)
globalThis.UPDATE_FX = UPDATE_FX;
//indicates if GPU (WebGL) is used
globalThis.use_gpu = false;
//indicates if EVG blit is enabled
globalThis.blit_enabled = evg.BlitEnabled;

/*
	global functions for our imported modules
*/
/*gets media time of output (no args) or of source of given id. Return
 -4 if not found
 -3 if not playing
 -2 if in prefetch
 -1 if timing not yet known
 media time in seconds (float) otherwise
*/
globalThis.get_media_time = get_media_time;
/*resolves URL against media playlist URL. Returns resolved url (string)*/
globalThis.resolve_url = resolve_url;


/*
	global functions for canvas access

Each scene module is associated an array of PidLink objects.
Each transition module is passed an array of PidLink objects.

A PidLink object has the following properties:
- pid: associated input visual pid, null if source is an offscreen group
- texture: associated input visual texture (constructed by avmix, but customized by each module)

The pid of a PidLink shall not be modified. The following variables can be checked for a pid:
- pck: current pck if any
- frame_ts: current frame timestamp in output video timescale
- rotate: video Rotation property
- Mirror: video Mirror property

Each scene must implement:
- update(): check value of this.update_flag to see if scene has to be rebuild
- fullscreen(): return index of pidlink from inputs which is fullscreen (occupies the entire scene area), or -1 if none
- identity(): return true if scene is identity: scene shows exactly the video witout any change in aspect ratio
- is_opaque(): return true if scene is opaque (entire scene area covered and not transparent), false otherwise
- draw(canvas): draw scene on canvas. The canvas can be EVG canvas or WebGL. All texture parameters modification (repeat flags, alpha, matrix, etc) must be done in this call, as the same texture may be used by different scenes

Note: scenes setup to skip drawing and forward input (fullscreen, opaque, identity) must check this.force_draw is not set to true, indicating
that the scene shall be drawn (due to canvas pixel format change for example)

Note: scenes using canvas.blit must use the provided variable this.screen_rect corresponding to the blit area on canvas, {x,y}={0,0} at top-left. If null, this means blit cannot be used (rotations & co)


*/

/*clears canvas - function shall only be used inside scene.draw()
\param color: color to use
\param clip clipper to use (type IRect) , or null/not specified
*/
globalThis.canvas_clear = canvas_clear;
/*sets axis-aligned clipper on canvas - function shall only be used inside scene.draw()
\param clip clipper to use (type IRect), or null to reset
\param use_stack if true, push and intersect the stack if clipper set or pop if clipper not set, otherwise change clipper directly without checking the clipper stack
*/
globalThis.canvas_set_clipper = canvas_set_clipper;
/*draws path on canvas - function shall only be used inside scene.draw()
\param path path (type Path) to draw
\param stencil stencil to use (type Texture or Stencil)
*/
globalThis.canvas_draw = canvas_draw;
/*blits image on canvas - function shall only be used inside scene.draw()
\param texture texture to blit (type Texture)
\param dst_rc destination window (type IRect)
*/
globalThis.canvas_blit = canvas_blit;
/*texture path on canvas - function shall only be used inside scene.draw()
The input textures are fteched from the sequences associated with the scene
\param path path (type Path) to draw
\param op_type (=0) multitexture operand type
\param op_param (=0) multitexture operand param
\param op_param (=null) multitexture second texture
*/
globalThis.canvas_draw_sources = canvas_draw_sources;

/*sets mask mode on canvas (2D or 3D) - function shall only be used inside scene.draw()
\param mode EVG mask mode
*/
globalThis.canvas_set_mask_mode = canvas_set_mask_mode;

/*gets texture associated with an offscreen group, typically used in scene.update()
\param group_id ID of offscreen group
\return associated texture or null if not found ot not offscreen
*/
globalThis.get_group_texture = get_group_texture;

/*gets texture associated with a sequence, typically used in scene.update() and scene.draw()
\param group_id ID of offscreen group
\return associated texture or null if not found ot not offscreen
*/
globalThis.get_sequence_texture = get_sequence_texture;

/*gets screen rectangle (axis-aligned bounds) in pixels for a given path. Shall only be used inside scene.draw()
\param path the path to check
\return rectangle object or null if error
*/
globalThis.get_screen_rect = get_screen_rect;


/*gets a scene by ID
\param id ID of the scene
\return scene object or null if error
*/
globalThis.get_scene = get_scene;

/*gets a group by ID
\param id ID of the group
\return group object or null if error
*/
globalThis.get_group = get_group;

/*removes a scene, group or sequence from playlist

The caller must be a scene module, in update callback.

\param id_or_elem ID of the object or object itself
*/
globalThis.remove_element = remove_element_mod;


/*parses a root playlist element and add it to the current playlist

The caller must be a scene module, in update callback.

\param elt JSON object
*/
globalThis.parse_element = parse_playlist_element_mod;

/*parses a scene

The caller must be a scene module, in update callback.

\param elt JSON object for the scene
\param parent parent group, NULL if at root of scene tree
\return scene object or null if error

Warning: the return value may get remove later on if underlying module loading fails
*/
globalThis.parse_scene = parse_scene_mod;

/*parses a group

The caller must be a scene module, in update callback.

\param elt JSON object for the group
\param parent parent group, NULL if at root of scene tree
\return group object or null if error
*/
globalThis.parse_group = parse_group_mod;

/*parses a new playlist

The caller must be a scene module, in update callback.

If the calling scene is no longer in the resulting scene tree, it will be added to the root of the scene tree.

\param pl JSON playlist object, use empty array to reset
*/
globalThis.reload_playlist = reload_playlist;

/*update an element

The caller must be a scene module, in update callback.

\param ID ID of element to update
\param property name of the property to update
\param value new value to set
\return true if success, false otherwise
*/
globalThis.update_element = update_element_mod;

/*queries a property of an element

The caller must be a scene module, in update callback.

\param ID element ID
\param property name  of property to query
\return property if success, undefined otherwise
*/
globalThis.query_element = query_element_mod;

/*checks if a given point is over a scene
\param evt GPAC mouse event

or

\param x x-coordinate of the point in output reference sapce in pixels (0 means center, x increase towards right)
\param y y-coordinate of the point in output reference sapce in pixels (0 means center, y increase towards top)

\return scene object under the mouse, or null if none
*/
globalThis.mouse_over = mouse_over_mod;


//metadata
filter.set_name("AVMix");
filter.set_class_hint(GF_FS_CLASS_AV);
filter.set_desc("Audio Video mixer");
filter.set_author("GPAC team");

//global options
filter.set_arg({ name: "pl", desc: "local playlist file to load", type: GF_PROP_STRING, def: "avmix.json" } );
filter.set_arg({ name: "live", desc: "live mode", type: GF_PROP_BOOL, def: "true"} );
filter.set_arg({ name: "gpu", desc: `enable GPU usage
- off: no GPU
- mix: only render textured path to GPU, use software rasterizer for the outlines, solid fills and gradients
- all: try to use GPU for everything`, type: GF_PROP_UINT, def: "off", minmax_enum: 'off|mix|all', hint:"advanced"} );
filter.set_arg({ name: "thread", desc: "use threads for software rasterizer (-1 for all available cores)", type: GF_PROP_SINT, def: "-1", hint:"expert"} );
filter.set_arg({ name: "lwait", desc: "timeout in ms before considering no signal is present", type: GF_PROP_UINT, def: "1000", hint:"expert"} );
filter.set_arg({ name: "ltimeout", desc: "timeout in ms before restarting child processes", type: GF_PROP_UINT, def: "4000", hint:"expert"} );
filter.set_arg({ name: "maxdur", desc: "run for given seconds and exit, will not abort if 0 (used for live mode tests)", type: GF_PROP_DOUBLE, def: "0", hint:"expert"} );
filter.set_arg({ name: "updates", desc: "local JSON files for playlist updates", type: GF_PROP_STRING, hint:"advanced"} );
filter.set_arg({ name: "maxdepth", desc: "maximum depth of a branch in the scene graph", type: GF_PROP_UINT, def: "100", hint:"expert"} );

//video output options
filter.set_arg({ name: "vsize", desc: "output video size, 0 disable video output", type: GF_PROP_VEC2, def: "1920x1080"} );
filter.set_arg({ name: "fps", desc: "output video frame rate", type: GF_PROP_FRACTION, def: "25"} );
filter.set_arg({ name: "pfmt", desc: "output pixel format. Use \`rgba\` in GPU mode to force alpha channel", type: GF_PROP_PIXFMT, def: "yuv"} );
filter.set_arg({ name: "dynpfmt", desc: `allow dynamic change of output pixel format in software mode
- off: pixel format is forced to desired value
- init: pixel format is forced to format of fullscreen input in first generated frame
- all: pixel format changes each time a full-screen input PID at same resolution is used`, type: GF_PROP_UINT, def: "init", minmax_enum: 'off|init|all', hint:"expert"} );

//audio output options
filter.set_arg({ name: "sr", desc: "output audio sample rate, 0 disable audio output", type: GF_PROP_UINT, def: "44100"} );
filter.set_arg({ name: "ch", desc: "number of output audio channels, 0 disable audio output", type: GF_PROP_UINT, def: "2"} );
filter.set_arg({ name: "afmt", desc: "output audio format (only s16, s32, flt and dbl are supported)", type: GF_PROP_PCMFMT, def: "s16"} );
filter.set_arg({ name: "alen", desc: "default number of samples per frame", type: GF_PROP_UINT, def: "1024", hint:"expert"} );

filter.max_pids = -1;
filter.make_sticky();
filter.maxdur=0;

let sources = [];
let reload_tests=null;
let reload_timeout=1.0;
let reload_loop=false;
let reload_idx=0;

let last_modification = 0;
let last_updates_modification = 0;
let init_utc = 0;
let init_clock_us = 0;

const TYPE_UNHANDLED = 0;
const TYPE_VIDEO = 1;
const TYPE_AUDIO = 2;

let playlist_url = null;

let active_scene = null;
let scene_in_update = null;

const SCENE_GL_NONE=0;
const SCENE_GL_ANY=1;
const SCENE_GL_ONLY=2;


let terminated = false;

let back_color = 'black';

let playlist_loaded = false;

filter.set_cap({id: "StreamType", value: "Visual", inout: true} );
filter.set_cap({id: "StreamType", value: "Audio", inout: true} );
filter.set_cap({id: "CodecID", value: "raw", inout: true} );

let modules_pending = 0;

let webgl = null;
filter.frame_pending=false;
let single_mod_help=false;
let mod_help_short=false;

const GROUP_OST_NONE = 0;
const GROUP_OST_DUAL = 1;
const GROUP_OST_MASK = 2;
const GROUP_OST_COLOR = 3;

const UNIT_RELATIVE = 0;
const UNIT_PIXEL = 1;


function build_help_mod(obj, name, mod_type, index)
{
	name = name.split('.')[0];
	let rad = '';
	if (mod_type==0) {
		if (!index) {
			filter._help += (mod_help_short ? '\n' : '# ' ) + 'Scene modules\n';
		}
		rad = 'Scene';
	}
	else if (mod_type==1) {
		if (!index) {
			filter._help += (mod_help_short ? '\n' : '# ' ) + 'Transition modules\n';
		}
		rad = 'Transition';
	}

	if (mod_help_short) {
		filter._help += ' - ' + name + ': ' + obj.description + '\n';
		filter.set_help(filter._help);
		return;
	}

	filter._help += (single_mod_help ? '# ' : '## ') + rad + ' `' + name + '`';
	if (mod_type==1) {
		let inst = obj.load();
		if (typeof inst.get_shader_src == 'function') {
  		if (typeof inst.setup != 'function') {
				filter._help += ' - GPU only';
  		} else {
				filter._help += ' - software/GPU';
  		}
		} else {
				filter._help += ' - software only';
		}
	}
	filter._help += '\n' + obj.help + '\n';

	if (obj.options.length)
		filter._help += 'Options:\n';
	for (let i=0; i<obj.options.length; i++) {
		let opt = obj.options[i];
		if (typeof opt.name == 'undefined') continue;
		if (typeof opt.desc == 'undefined') continue;

		filter._help += '- ' + opt.name;
		let opts = "";
		if (typeof opt.value != 'undefined') {
			if (Array.isArray(opt.value)) {
				opts+='[]';
			} else if (typeof opt.value!='string') {
				opts+=opt.value;
			} else /*if (opt.value.length)*/ {
				opts+='\''+opt.value+'\'';
			}
		}
		if (mod_type==0) {
			if ((typeof opt.dirty == 'undefined') || (opt.dirty==-1)) {
				opts+=', not updatable';
			}
		}

		if (opts.length)
			filter._help += ' (' + opts + ')';

		filter._help += ': ' + opt.desc;
		filter._help += '\n';
	}
	filter.set_help(filter._help);
}

function build_help(obj, playlist_only)
{
	let path = filter.jspath;
	if (playlist_only) {
		filter._help = obj.help_playlist + '\n';
		filter.set_help(filter._help);
		return;
	}

	filter._help = obj.help + '\n';
	if (mod_help_short) {
		filter._help += '# More info\n';
		filter._help += 'For playlist syntax, use `gpac -h avmix:playlist`\n';
		filter._help += 'For module `NAME`, use `gpac -h avmix:NAME`\n';
		filter._help += 'For global variables available to modules, use `gpac -h avmix:global`\n';
		filter._help += '# Available modules\n';
	} else {
		filter._help += obj.help_playlist + '\n';
	}

	let	scripts = sys.enum_directory(path+'scenes/', "*.js").sort((a, b) => a.name.localeCompare(b.name));
	for (let i=0; i<scripts.length; i++) {
		let name = scripts[i].name;
		import('./scenes/' + name).then(obj => { build_help_mod(obj, name, 0, i); }).catch(err => {});
	}
	scripts = sys.enum_directory(path+'transitions/', "*.js").sort((a, b) => a.name.localeCompare(b.name));
	for (let i=0; i<scripts.length; i++) {
		let name = scripts[i].name;
		import('./transitions/' + name).then(obj => { build_help_mod(obj, name, 1, i); }).catch(err => {});
	}
	filter.set_help(filter._help);
}

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) {
		let args = sys.args;
		globalThis.help_mod = gpac_doc ? null : sys.get_opt("temp", "gpac-js-help");

		if (help_mod) {
			let name;
			let path = filter.jspath;
			let	script = path+'scenes/'+help_mod+'.js';
			filter._help = '';
			single_mod_help = true;
			if (help_mod == 'global') {
				filter._help = 'Global JS properties available:\n';
				for (var propertyName in globalThis) {
					//skip GPAC constants
					if (propertyName.startsWith('GF_')) continue;
					if (propertyName.startsWith('JSF_')) continue;

					filter._help += '- ' + propertyName+ ': type ' + (typeof globalThis[propertyName]) + '\n';
				}
				filter.set_help(filter._help);
				return GF_OK;
			}
			if (help_mod == 'playlist') {
				import("./help.js").then(obj => { build_help(obj, true); }).catch(err => {});
				return GF_OK;
			}

			if (sys.file_exists(script)) {
				name = help_mod + '.js';
				import('./scenes/' + name).then(obj => { build_help_mod(obj, name, 0, 1); }).catch(err => {});
				return GF_OK;
			}
			script = path+'transitions/'+help_mod+'.js';
			if (sys.file_exists(script)) {
				name = help_mod + '.js';
				import('./transitions/' + name).then(obj => { build_help_mod(obj, name, 1, 1); }).catch(err => {});
				return GF_OK;
			}
			print(GF_LOG_ERROR, 'No such module ' + help_mod + ' path ' + script);
			return GF_OK;
		}
		if ((gpac_help == "h") || (gpac_help == "ha"))
			mod_help_short = !gpac_doc;
		import("./help.js").then(obj => { build_help(obj, false); }).catch(err => {});
		return GF_OK;
	}

	if (!this.pl) {
		print(GF_LOG_ERROR, "No playlist specified, cannot run");
		return GF_BAD_PARAM;
	}
	if (!sys.file_exists(filter.pl)) {
		print(GF_LOG_ERROR, "Playlist " + filter.pl + " not found, cannot run");
		return GF_BAD_PARAM;
	}

	playlist_url = filter.pl.slice();
	let cwd = os.getcwd()[0] + '/';
	playlist_url = sys.url_cat(cwd, playlist_url);

	if (!load_playlist())
		return GF_BAD_PARAM;
	playlist_loaded = true;

	if (filter.updates) {
		last_updates_modification = sys.mod_time(filter.updates);
	}

	if (filter.live) {
		init_utc = sys.get_utc();
		init_clock_us = sys.clock_us();
	}
	if (filter.maxdepth<=0)
		filter.maxdepth = 100;

	if ((WebGLContext == null) && this.gpu) {
		print(GF_LOG_INFO, 'GPAC compiled without WebGL, using software mode');
		this.gpu = 0;
	}
	if (this.gpu) {
		video_width = filter.vsize.x;
		video_height = filter.vsize.y;
			try {
				webgl = new WebGLContext(video_width, video_height);
				//by default do not output alpha channel
				if (filter.pfmt != 'rgba')
					filter.pfmt = 'rgb';
				globalThis.use_gpu = true;
				globalThis.blit_enabled = false;
			} catch (e) {
				print(GF_LOG_ERROR, "Failed to initialize WebGL, disabling GPU support");
				this.gpu = 0;
				globalThis.use_gpu = false;
				globalThis.blit_enabled = evg.BlitEnabled;
			}
	}

	configure_vout();
	configure_aout();
	if (filter.live)
		print(GF_LOG_INFO, 'Initialized at ' + new Date());;

	if (!filter.ltimeout)
			filter.ltimeout = 1000;

	if (filter.lwait > filter.ltimeout)
		filter.lwait = filter.ltimeout;
}


let pids=[];
let vout = null;
let aout = null;

let video_frame_dur_us = 0;
let video_max_resched_dur_us = 0;
let audio_frame_dur_us = 0;
let audio_max_resched_dur_us = 0;

let video_pfmt =0;
let vout_size =0;
let canvas = null;
let canvas_offscreen = null;
let canvas_reconfig = false;
let video_time_inc =0;

let audio_afmt =0;
let sample_size=0;
let max_aframe_size=0;
const sample_per_frame=1024;
let chan_buffer = null;

let sequences = [];

//static top-level group
let root_scene = {};
root_scene.scenes = [];
root_scene.parent = null;
root_scene.update_flag = 0;

let wait_for_pids = 0;
let wait_pid_play = 0;
let live_forced_play = false;

let use_canvas_3d = false;

let defaultOrthoProjectionMatrix = null;

let audio_mix = null;
let generate_default_scene = true;



function configure_vout()
{
	if (!filter.vsize.x || !filter.vsize.y) {
		if (vout) vout.remove();
		vout = null;
		return;
	}
	if (filter.gpu) {
		print(GF_LOG_INFO, (filter.live ? 'Live ' : '' ) + 'Video output ' + filter.vsize.x + 'x' + filter.vsize.y + ' FPS ' + filter.fps.n + '/' + filter.fps.d + ' OpenGL pixfmt ' + filter.pfmt);
	} else {
		let test_canvas=null;
		try {
			test_canvas = new evg.Canvas(48, 48, filter.pfmt);
		} catch (e) {
			let change_fmt;
		  if (sys.pixfmt_yuv(filter.pfmt)) {
				let bpp = sys.pixfmt_depth(filter.pfmt);
				change_fmt = (bpp>8) ? 'yp4l' : 'yuv4';
		  } else {
				change_fmt = 'rgb';
		  }
			print(GF_LOG_WARNING, 'Pixel format ' + filter.pfmt + ' not supported by rasterizer, defaulting to ' + change_fmt);
			filter.pfmt = change_fmt;
		}

		print(GF_LOG_INFO, (filter.live ? 'Live ' : '' ) + 'Video output ' + filter.vsize.x + 'x' + filter.vsize.y + ' FPS ' + filter.fps.n + '/' + filter.fps.d + ' pixfmt ' + filter.pfmt);
	}

	if (!vout) {
		vout = filter.new_pid();
		vout.name = "video";
		wait_pid_play++;
	} else if ((video_width==filter.vsize.x) && (video_height==filter.vsize.y) && (video_pfmt===filter.pfmt)) {
		return;
	}
	video_width = filter.vsize.x;
	video_height = filter.vsize.y;
	video_pfmt = filter.pfmt;

	vout.set_prop('StreamType', 'Visual');
	vout.set_prop('Width', video_width);
	vout.set_prop('Height', video_height);
	vout.set_prop('PixelFormat', video_pfmt);
	vout.set_prop('CodecID', 'raw');
	vout.set_prop('FPS', filter.fps);
	video_timescale = filter.fps.n;
	video_time_inc = filter.fps.d;
	video_frame_dur_us = 1000000 * video_time_inc / video_timescale;
	video_max_resched_dur_us = video_frame_dur_us/3;
	vout.set_prop('Timescale', filter.fps.n);
	//do not buffer output, this would trigger block on vout and break sync
	vout.set_prop('BufferLength', 0);

	if (sys.pixfmt_transparent(video_pfmt)) {
		print(GF_LOG_INFO, 'Enabling alpha on video output');
		back_color = 'none';
	}

	if (!webgl)
		set_canvas_yuv(video_pfmt);

	vout_size = sys.pixfmt_size(filter.pfmt, filter.vsize.x, filter.vsize.y);
	canvas_reconfig = true;

	defaultOrthoProjectionMatrix = new evg.Matrix().ortho(-video_width/2, video_width/2, -video_height/2, video_height/2, 1, -1);
}

function configure_aout()
{
	if (!filter.sr || !filter.ch) {
		if (aout) aout.remove();
		aout = null;
		return;
	}
	print(GF_LOG_INFO, (filter.live ? 'Live ' : '' ) + 'Audio output ' + filter.sr + ' Hz ' + filter.ch + ' Channels');

	if (!aout) {
		aout = filter.new_pid();
		aout.name = "audio";
		wait_pid_play++;
	} else if ((samplerate==filter.sr) && (channels==filter.ch) && (audio_sfmt===filter.afmt)) {
		return;
	}
	samplerate = filter.sr;
	channels = filter.ch;
	audio_afmt = filter.afmt;

	aout.set_prop('StreamType', 'Audio');
	aout.set_prop('SampleRate', samplerate);
	aout.set_prop('NumChannels', channels);
	aout.set_prop('AudioFormat', audio_afmt);
	aout.set_prop('CodecID', 'raw');
	audio_timescale = samplerate;
	aout.set_prop('Timescale', audio_timescale);
	//do not send clock hints for aout, this would trigger block on vout and break sync
	aout.set_prop('BufferLength', 0);

	sample_size = sys.pcmfmt_depth(audio_afmt);
	max_aframe_size = sample_size * channels * sample_per_frame;
	chan_buffer = new Float32Array(channels);

	if ((audio_afmt === 's32') || (audio_afmt === 's16') || (audio_afmt === 'flt') || (audio_afmt === 'dbl')) {
	} else {
		print(GF_LOG_ERROR, 'Unsupported sample format ' + audio_afmt);
		sys.exit(1);
	}
  if (!filter.alen) filter.alen = 100;

  //above 20 samples consider this is a time discontinuity
  //use 50 max samples for cross-fade
  audio_mix = new amix(audio_afmt, channels, 20, 50);

	audio_frame_dur_us = 1000000 * filter.alen / audio_timescale;
	audio_max_resched_dur_us = audio_frame_dur_us/3;
}

filter.configure_pid = function(pid)
{
	if (pids.indexOf(pid)<0) {
		pids.push(pid);
		pid.skipped = false;
		let s = get_source_by_pid(pid);
		if (s) {
			s.pids.push(pid);
			pid.source = s;
			if (s.playing) play_pid(pid, s);
			pid.pck = null;
			pid.old_timescale = pid.timescale;
			pid.is_blocking = false;
			pid.done = false;
			pid.fade = 0;
			pid.active = false;
			pid.texture = null;
			pid.audio_fade = null;
			s.timeline_init = false;
			pid.last_pck_time = /*	current_utc_clock =*/ Date.now();
		} else {
			if (!pid.skipped) {
				print(GF_LOG_ERROR, 'PID ' + pid.name + ' coming from unknown source !');
				return GF_FILTER_NOT_SUPPORTED;
			}
			return GF_EOS;
		}
	}

	let p = pid.get_prop('StreamType');
	let ts = 0;
	if (p == 'Visual') {
		//silently ignore
		if (!vout) return GF_EOS;

		pid.pfmt = pid.get_prop('PixelFormat');
		pid.type = TYPE_VIDEO;
		ts = video_timescale;

		pid.width = pid.get_prop('Width');
		pid.height = pid.get_prop('Height');
		pid.reconfigure = true;
		pid.mirror = pid.get_prop('Mirror');
		pid.rotate = pid.get_prop('Rotate');
		pid.pfmt_check = 0;
	}
	else if (p == 'Audio') {
		//silently ignore
		if (!aout) return GF_EOS;

		pid.type = TYPE_AUDIO;
		pid.data = null;
		ts = audio_timescale;

		pid.channels = pid.get_prop('NumChannels');
		pid.afmt = pid.get_prop('AudioFormat');
		let sr = pid.get_prop('SampleRate');
		if ((sr != filter.sr) || (pid.afmt != filter.afmt)) {
			pid.negotiate_prop('SampleRate', filter.sr);
			pid.negotiate_prop('AudioFormat', filter.afmt);
		}

		pid.ch_buf = new Float32Array(pid.channels);
		pid.samples_used = 0;
		pid.last_sample_time = 0;
	} else {
		pid.type = TYPE_UNHANDLED;
		//silently ignore
		return GF_EOS;
	}

	if (pid.timescale != ts) {
		pid.translate_timestamp = function(value) {
			value -= pid.init_ts;
			value *= ts;
			value /= pid.timescale;
			value += pid.init_clock;
			return value;
		};
	} else {
		pid.translate_timestamp = function(value) {
			value -= pid.init_ts;
			value += pid.init_clock;
			return value;
		};
	}

  let dur = pid.get_prop('Duration');
  if (dur && dur.d && (dur.n > pid.source.duration * dur.d))
  		pid.source.duration = dur.n / dur.d;

	if (pid.old_timescale != pid.timescale) {
		//todo
		pid.old_timescale = pid.timescale;
	}
	do_traverse(root_scene, scene => {
		scene.sequences.forEach(seq => {
			//this is an offscreen group
			if (typeof seq.offscreen != 'undefined') return;

			seq.sources.forEach(src => {
				if (pid.source === src) scene.resetup_pids = true;
			});
		});
	});
}

filter.remove_pid = function(pid)
{
	let index = pids.indexOf(pid);
	if (pid.pck) {
		pid.pck.unref();
		pid.pck = null;
	}
  if (index < 0) return;
  pids.splice(index, 1);

  if (typeof pid.source != 'undefined') {
	  index = pid.source.pids.indexOf(pid);
	  if (index>=0)
			pid.source.pids.splice(index, 1);
  }

	do_traverse(root_scene, scene => {
		for (let i=0; i<scene.mod.pids.length; i++) {
			let pid_link = scene.mod.pids[i];
			if (pid_link.pid === pid) {
				scene.mod.pids.splice(i, 1);
				scene.resetup_pids = true;
				break;
			}
		}
		for (let i=0; i<scene.apids.length; i++) {
			let pid_link = scene.apids[i];
			if (pid_link.pid === pid) {
				scene.resetup_pids = true;
				break;
			}
		}
	});
}

filter.update_arg = function(name, val)
{
}

filter.process_event = function(pid, evt)
{
	if (evt.type == GF_FEVT_USER) {

		event_watchers.forEach(watcher => {
			if ((watcher.evt>0) && (watcher.evt != evt.ui_type)) return;

			if (watcher.evt_param && (watcher.evt_param != sys.keyname(evt.keycode) ) ) return;

			if (watcher.mode==2) {
				watcher.fun.apply(watcher, [evt]);
			} else if (watcher.mode==3) {
				if (watcher.scene.mod && (typeof watcher.scene.mod[watcher.target] == 'function')) {
					watcher.scene.mod[watcher.target].apply(watcher.scene.mod, [evt]);
				}
			}
		});
		return;
	}

	if (evt.type == GF_FEVT_STOP) {
		if (pid==aout) {
			audio_playing = false;
			if (!wait_pid_play) aout.eos = true;
		}
		else if (pid==vout) {
			video_playing = false;
			if (!wait_pid_play) vout.eos = true;
		}

		if (!audio_playing && !video_playing && !wait_pid_play) {
			do_terminate();

			//last stop, don't cancel
			if (!wait_pid_play) return false;
		}
		return true;
	}
	if (evt.type == GF_FEVT_PLAY) {
		if (pid==aout) {
			audio_playing = true;
		} else if (pid==vout) {
			video_playing = true;
		}
		terminated = false;
		if (wait_pid_play)
			wait_pid_play--;
		return true;
	}
	//cancel all other events
	return true;
}

let last_forward_pixfmt = 0;
let video_inputs_ready, audio_inputs_ready;

let blocking_ref_pids = [];
let purge_sources = [];


function do_traverse(elm, fun)
{
	if (Array.isArray(elm.scenes)) {
		for (let i=0; i<elm.scenes.length; i++) {
			let scene = elm.scenes[i];
			let res = do_traverse(scene, fun);
			if (res == true) {
				return;
			}
		}
	} else {
		fun(elm);
	}
}


function do_traverse_all(elm, fun)
{
	let ret = fun(elm) || false;
	if (ret == true) return;

	if (Array.isArray(elm.scenes)) {
		elm.scenes.forEach(scene => {
			do_traverse_all(scene, fun);
		});
	}
}

let indent=0;
function do_traverse_all_indent(elm, fun)
{
	let ret = fun(elm) || false;
	if (ret == true) return;

	if (Array.isArray(elm.scenes)) {
		indent++;
		elm.scenes.forEach(scene => {
			do_traverse_all_indent(scene, fun);
		});
		indent--;
	}
}

function do_terminate()
{
	terminated = true;
	pids.forEach(pid => {
		if (pid.pck) {
			pid.pck.unref();
			pid.pck = null;
		}
		pid.send_event( new FilterEvent(GF_FEVT_STOP) );
		pid.discard = true;
	});
	root_scene.scenes.length = 0;
	sources.forEach(s => { stop_source(s, true); });
	filter.abort();
}

let do_audio=false;
let next_audio_gen_time = 0;
let next_video_gen_time = 0;

let last_pl_test_switch = 0;

filter.process = function()
{
	let do_video = false;
	if (terminated)
		return GF_EOS;

	if (filter.maxdur && (video_time || audio_time) ) {
		let abort = true;
		if (video_playing && (video_time < filter.maxdur*video_timescale)) abort = false;
		if (audio_playing && (audio_time < filter.maxdur*audio_timescale)) abort = false;
		if (wait_for_pids) abort = false;
		if (abort) {
			print(GF_LOG_INFO, 'maxdur ' +  filter.maxdur + ' reached, quit - video time ' + video_time + '/' + video_timescale + ' - audio time ' + audio_time + '/' + audio_timescale);
			do_terminate();
			return;
		}
	}

	if (vout && video_playing) {
		//if no audio or video behind audio, generate frame if not blocking
		if (!audio_playing || (audio_time * video_timescale >= video_time * audio_timescale)) {
			if (!vout.would_block && !filter.frame_pending) {
				do_video = true;
			}
		}
		//audio ahead of next video frame, don't do audio - if not generating video, do nothing
		if (!do_video && (!audio_playing || (audio_time * video_timescale > (video_time+video_time_inc) * audio_timescale))) {
			//notify we are still alive
			filter.reschedule(0);
			if (filter.live) {
				let now = sys.clock_us();
				let next_time = (next_video_gen_time<next_audio_gen_time) ? next_video_gen_time : next_audio_gen_time;

				let diff = next_time - now;
				if (diff>1000)
					filter.reschedule(diff);
			}
			return;
		}
	}

	current_utc_clock = Date.now();

	if (reload_tests && (reload_idx<reload_tests.length) ) {
		if (last_pl_test_switch + reload_timeout*video_timescale < video_time) {
			last_pl_test_switch = video_time;
			filter.pl = sys.url_cat(filter.pl, reload_tests[reload_idx]);
			last_modification = 0;
			reload_idx++;
			if (reload_idx>=reload_tests.length) {
				if (reload_loop>=0) {
					reload_loop --;
					if (reload_loop<0) reload_loop=0;
				}
				if (reload_loop) reload_idx=0;
			}
		}
	}
	load_playlist();
	load_updates();
	//modules are pending, wait for fail/success - async cannot wait here because QuickJS will only try to load the modules once out of JS functions
	if (modules_pending)
		return GF_OK;

	//we had pending events, waiting for import module resolutions, flush them
	if (watchers_deferred.length) {
		flush_watchers();
	}


	video_inputs_ready = video_playing;
	audio_inputs_ready = audio_playing;
	inactive_sources = 0;
	blocking_ref_pids.length = 0;
	sources.forEach(fetch_source);

	if (wait_for_pids) {
		let lwait = filter.lwait;
		if (!filter.live) lwait *= 10;
		if (current_utc_clock - wait_for_pids > lwait) {
			if (filter.live) {
				print(GF_LOG_WARNING, 'No input connection after ' + (current_utc_clock - wait_for_pids) + ' ms, starting');
				wait_for_pids = 0;
				live_forced_play = true;
			} else {
				print(GF_LOG_ERROR, 'No input connection after ' + (current_utc_clock - wait_for_pids) + ' ms, aborting');
				do_terminate();
				return;
			}
		}
		if (filter.connections_pending) {
			filter.reschedule(1000);
			return;
		}
		wait_for_pids = 0;
	}

	if (inactive_sources == sources.length) {
		if (filter.live) {
			//inactive, reschedule in 2 ms. A better way would be to compute next activation time
			filter.reschedule(2000);
			//but do generate frame
		} else {
			if (vout) vout.eos = true;
			if (aout) aout.eos = true;
			do_terminate();
			print(GF_LOG_INFO, 'sources are over, quit');
			return GF_EOS;
		}
	}

	timers.forEach(update_timer);
	update_scripts();

	if (filter.maxdur && (video_time >= filter.maxdur*video_timescale)) {
		if (do_video) {
			//no longer generating video, increase time for audio generation
			video_time += video_time_inc;
			do_video = false;
		}
	}

	do_audio = (aout && audio_playing) ? true : false;
	if (vout && video_playing) {
		if (do_video) {
			process_video();
		}
		//this typically happens upon init
		if (audio_playing && (audio_time * video_timescale > (video_time+video_time_inc) * audio_timescale)) {
			return;
		}
	}

	if (filter.maxdur && (audio_time >= filter.maxdur*audio_timescale)) {
		do_audio = false;
	}

	if (do_audio) {
		//video is not playing, we must update the scenes for audio
		if (!vout) update_display_list();

		process_audio();
		if (!video_playing) {
			video_time = audio_time * video_timescale;
			video_time /= audio_timescale;
		}
	}

	blocking_ref_pids.forEach(pid => {
		pid.pck.unref();
		pid.pck = null;
	});
	purge_sources.forEach(s => {
		sources.splice(sources.indexOf(s), 1);
	});
	purge_sources.length = 0;
}

function scene_resetup_pids(scene)
{
	//purge pid links
	scene.mod.pids.length = 0;
	scene.apids.length = 0;

	let prefetching = [];
	//check all pids from sources
	scene.sequences.forEach(seq => {
		//this is an offscreen group
		if (typeof seq.offscreen != 'undefined') {
			let pid_link = {};
			pid_link.pid = null;
			pid_link.sequence = null;
			pid_link.texture = seq.texture;
			scene.mod.pids.push(pid_link);
			return;
		}

		seq.sources.forEach(src => {
			if (src.in_prefetch) return;
			src.pids.forEach(pid => {

				//check audio pids
		    if (pid.type == TYPE_AUDIO) {
					scene.apids.push(pid);
					return;
		    }

				//only check for video pids
		    if (pid.type != TYPE_VIDEO)
					return;

				//add pid link
				let pid_link = {};
				pid_link.pid = pid;
				pid_link.texture = null;
				pid_link.sequence = seq;
				if (pid.source.next_in_transition)
					prefetching.push(pid_link);
				else
					scene.mod.pids.push(pid_link);
			});
		});
	});

	prefetching.forEach(pid_link => {
		print(GF_LOG_DEBUG, 'transitioning to ' + pid_link.pid.source.logname + '.' + pid_link.pid.name);
		scene.mod.pids.push(pid_link);
	});

	scene.mod.pids.forEach(pid_link => {
		if (pid_link.pid)
			print(GF_LOG_DEBUG, 'created PID link for ' + pid_link.pid.source.logname + '.' + pid_link.pid.name);
	});

}

function create_pid_texture(pid)
{
	pid.texture = new evg.Texture(pid.pck);
	pid.texture._gl_modified = true;
	pid.texture.last_frame_ts = pid.frame_ts;
}

function scene_update_visual_pids(scene)
{
	let ready = true;
	scene.mod.pids.forEach(pidlink => {
		//offscreen group
		if (!pidlink.pid) return;

		if (!pidlink.pid.pck) {
			if (pidlink.pid.source.in_prefetch) {
				return;
			}
			if (pidlink.pid.source.no_signal) {
				if (pidlink.texture) return;
			}
			ready = false;
			return;
		}

		//create texture object at PID level, so we don't end up uploading twice the same data to gpu
		if (!pidlink.pid.texture) {
			if (!pidlink.pid.pck) {
				ready = false;
				return;
			}
			create_pid_texture(pidlink.pid);
		}

		let update_tx = false;
		if (!pidlink.texture) {
			pidlink.texture = pidlink.pid.texture;
			scene.mod.update_flag |= UPDATE_SIZE;
			update_tx = true;
		}
		if (update_tx || (pidlink.pid.frame_ts != pidlink.texture.last_frame_ts)) {
			pidlink.texture.update(pidlink.pid.pck);
			pidlink.texture._gl_modified = true;
			pidlink.texture.last_frame_ts = pidlink.pid.frame_ts;
		}
	});
	return ready;
}


let round_scene_size = 0;

let global_transform = new evg.Matrix2D();
let global_branch_depth = 0;
let active_camera = null;

function update_scene_matrix(scene)
{
	let x=scene.x, y=scene.y, z=scene.z, cx=scene.cx, cy=scene.cy, cz=scene.cz;
	let rotation = scene.rotation;
	let hskew = scene.hskew;
	let vskew = scene.vskew;
	let hscale = scene.hscale;
	let vscale = scene.vscale;
	let zscale = scene.zscale;
	let axis = scene.axis;
	let orientation = scene.orientation;

	if (scene.untransform)
		scene.mx_untransform = true;
	else if (scene.mx_untransform)
		scene.mx_untransform = false;

	//compute absolute values
	if (scene.units == UNIT_RELATIVE) {
		if (typeof x == 'number')
			x = x * reference_width/100;
		if (typeof y == 'number')
			y = y * reference_height/100;
		cx = cx * reference_width/100;
		cy = cy * reference_height/100;

		z = z * reference_height/100;
		cz = cz * reference_height/100;
	}
	//apply special values
	if (x == "y") x = y;
	else if (x == "-y") x = -y;

	if (y == "x") y = x;
	else if (y == "-x") y = -x;

	scene.mx.identity = true;

	try {
		if (scene.mxjs) {
			let is_ok = true;
			let mx_state = {
					x: x, y: y, z: z,
					cx: cx, cy: cy, cz,
					rotation: rotation,
					hscale: hscale, vscale: vscale, zscale: zscale,
					hskew: hskew, vskew: vskew,
					axis: axis,
					orientation: orientation,

					untransform: scene.mx_untransform, update:false, depth: scene.current_depth
			};

			let res = scene.mxjs.apply(scene, [mx_state]);
			if (mx_state.update) scene.update_flag = UPDATE_CHILD;
			if ((typeof res == 'boolean') && !res) {
				return false;
			}
			scene.mx_untransform = mx_state.untransform ? true : false;

			if (typeof mx_state.x == 'number') x = mx_state.x;
			else is_ok = false;
			if (typeof mx_state.y == 'number') y = mx_state.y;
			else is_ok = false;
			if (typeof mx_state.z == 'number') z = mx_state.z;
			else is_ok = false;

			if (typeof mx_state.cx == 'number') cx = mx_state.cx;
			else is_ok = false;
			if (typeof mx_state.cy == 'number') cy = mx_state.cy;
			else is_ok = false;
			if (typeof mx_state.cz == 'number') cz = mx_state.cz;
			else is_ok = false;

			if (typeof mx_state.rotation == 'number') rotation = mx_state.rotation;
			else is_ok = false;

			if (typeof mx_state.hscale == 'number') hscale = mx_state.hscale;
			else is_ok = false;
			if (typeof mx_state.vscale == 'number') vscale = mx_state.vscale;
			else is_ok = false;
			if (typeof mx_state.zscale == 'number') zscale = mx_state.zscale;
			else is_ok = false;

			if (typeof mx_state.hskew == 'number') hskew = mx_state.hskew;
			else is_ok = false;
			if (typeof mx_state.vskew == 'number') vskew = mx_state.vskew;
			else is_ok = false;

			if (!mx_state.axis || Array.isArray(mx_state.axis)) axis = mx_state.axis;
			else is_ok = false;
			if (!mx_state.orientation || Array.isArray(mx_state.orientation)) orientation = mx_state.orientation;
			else is_ok = false;

			if (!is_ok) {
				print(GF_LOG_ERROR, 'Error processing mxjs, result not a valid object');
				scene.mxjs = null;
			}
		}
	} catch (e) {
		print(GF_LOG_ERROR, 'Error processing mxjs : ' + e);
		scene.mxjs = null;
	}

	//validate axis, if 0,0,1 move to null (no 3D rotation)
	if (axis) {
		if (axis.length!=3) axis = null;
		else if (!axis[0] && !axis[1] && (axis[2]>0)) axis = null;
	}

	//validate orientation, if no angle, move to null (no 3D oriented scaling)
	if (orientation) {
		if (orientation.length!=4) orientation = null;
		else if (!orientation[3]) orientation = null;
	}

	let is_3d = false;
	if (z || cz
		|| (zscale != 1)
		|| orientation
		|| axis
	) {
		is_3d = true;
	}

	if (!is_3d) {
		if (scene.mx.is3D) scene.mx = new evg.Matrix2D();
		scene.mx.rotate(cx, cy, rotation * Math.PI / 180);
		if (hskew) scene.mx.skew_x(hskew);
		if (vskew) scene.mx.skew_y(vskew);
		scene.mx.scale(hscale, vscale);
		scene.mx.translate(x, y);
		return true;
	}
	//3D matrix
	if (!scene.mx.is3D) scene.mx = new evg.Matrix();
	if (x || y || scene.z)
		scene.mx.translate(x, y, z);

	let recenter = (cx || cy || cz) ? 1 : 0;
	if (recenter)
		scene.mx.translate(cx, cy, cz);

	if (rotation && axis) scene.mx.rotate(axis[0], axis[1], axis[2], rotation * Math.PI / 180);
	let	scale_rot = (orientation && orientation[3]) ? 1 : 0;
	if (scale_rot)
		scene.mx.rotate(orientation[0], orientation[1], orientation[2], orientation[3] * Math.PI / 180);

	if ((hscale != 1) || (vscale != 1) || (zscale != 1))
		scene.mx.scale(hscale, vscale, zscale);
	if (scale_rot)
		scene.mx.rotate(orientation[0], orientation[1], orientation[2], -orientation[3] * Math.PI / 180);

	if (recenter)
		scene.mx.translate(-cx, -cy, -cz);
	return true;
}

function get_dim(scene, is_width)
{
	let val;
	if (is_width) {
		val = scene.width;
		if (val == -1) return reference_width;
		if (val == "height") {
			if (scene.height == "width") return 0;
			return get_dim(scene, false);
		}
		if (scene.units == UNIT_RELATIVE)
			return val * reference_width / 100;
		return val;
	}

	val = scene.height;
	if (val == -1) return reference_height;
	if (val == "width") {
		if (scene.width == "height") return 0;
		return get_dim(scene, true);
	}
	if (scene.units == UNIT_RELATIVE)
		return val * reference_height / 100;
	return val;
}

let group_bounds = null;

let reference_width = 0;
let reference_height = 0;

function group_draw_offscreen(group)
{
	//store display list and canvas state
	let has_opaque_bck = has_opaque;
	let	display_list_bck = display_list;
	let global_transform_copy = global_transform.copy();
	let use_gpu_bck = globalThis.use_gpu;
	let canvas_bck = canvas;
	let webgl_bck = webgl;
	let round_scene_bck = round_scene_size;
	let group_bounds_bck = group_bounds;
	//reset state
	globalThis.use_gpu = false;
	globalThis.blit_enabled = false;
	global_transform.identity = true;
	display_list = [];
	webgl = null;
	round_scene_size = 0;

	//traverse while collecting bounds
	group_bounds = {x: 0,x: 0,w: 0,h: 0,}


	let ref_width_bck = reference_width;
	let ref_height_bck = reference_height;
	if (group.reference && (group.width>=0) && (group.height>=0)) {
		let rw = get_dim(group, true);
		let rh = get_dim(group, false);
		reference_width = rw;
		reference_height = rh;
	}

	group.scenes.forEach(scene => do_traverse_all(scene, update_scene));

	reference_width = ref_width_bck;
	reference_height = ref_height_bck;

	group.skip_draw = false;
	if ((group_bounds.w <= 0) || (group_bounds.h <= 0) || (group.width==0) || (group.height==0)) {
		group.skip_draw = true;
	}

	let old_w, old_h;
	if (group.canvas_offscreen) {
		old_w = group.canvas_offscreen.width;
		old_h = group.canvas_offscreen.height;
	} else {
		old_w = 0;
		old_h = 0;
	}

	let scaler = group.scaler;
	if (scaler<1) scaler = 1;
	let inv_scaler = 1.0 / scaler;

	let new_w = group_bounds.w;
	let new_h = group_bounds.h;

	if ((group.width>=0) && (group.height>=0)) {
		new_w = get_dim(group, true);
		new_h = get_dim(group, false);
	}
	if (new_w>video_width) new_w = video_width;
	if (new_h>video_height) new_h = video_height;

	new_w = Math.ceil(new_w / scaler);
	new_h = Math.ceil(new_h / scaler);
	while (new_w % 2) new_w++;
	while (new_h % 2) new_h++;

	if ((new_w != old_w) || (new_h != old_h)) {
		let pf;
		let old_pf = group.prev_pixfmt || '';
		let alpha=true;
		if (group.offscreen==GROUP_OST_MASK) {
			pf = 'algr';
		} else {
	    let a = sys.color_component(group.back_color, 0);
			if (a == 1.0) {
				pf = canvas_yuv ? 'yuv4' : 'rgb';
				alpha = false;
			}
			else
				pf = canvas_yuv ? 'y4ap' : 'rgba';
		}
		print(GF_LOG_DEBUG, 'Offscreen group ' + group.id + ' creating offscreen canvas ' + new_w + 'x' + new_h + ' pfmt ' + pf + ' scaler ' + scaler);

		group.canvas_offscreen = new evg.Canvas(new_w, new_h, pf);
		if (!group.canvas_offscreen) return;

		//don't delete texure once created, it might be used by some scenes
		if (!group.texture)
			group.texture = new evg.Texture(group.canvas_offscreen);
		else
			group.texture.update(group.canvas_offscreen);

		if (!group.texture) return;
		group.texture.set_pad_color(group.back_color);

		group.texture.filtering = (scaler > 1) ? GF_TEXTURE_FILTER_HIGH_QUALITY : GF_TEXTURE_FILTER_HIGH_SPEED;

		let mx = new evg.Matrix2D();
		group.texture_mx = mx;

		group.texture.repeat_s = false;
		group.texture.repeat_t = false;

		group.path = new evg.Path().rectangle(0, 0, new_w * scaler, new_h * scaler, true);

		group.prev_pixfmt = pf;
		//texture size changed but not pix format, keep opengl shader otherwise reset it
		if (pf != old_pf)
			group.texture._gl_texture = null;
	}

	if (!group.canvas_offscreen) return;

	print(GF_LOG_DEBUG, 'Redrawing offscreen group ' + group.id);
	group.tr_x = group_bounds.x + group_bounds.w/2;
	group.tr_y = group_bounds.y - group_bounds.h/2;

	global_transform.translate(-group.tr_x, -group.tr_y);
	display_list.forEach(scene => {
		scene.mx.add(global_transform, true);
		scene.mx.scale(inv_scaler, inv_scaler);
	});

	canvas = group.canvas_offscreen;
	canvas.clipper = null;
	canvas.clear(group.back_color);

	draw_display_list_2d();

	//restore state
	global_transform.copy(global_transform_copy);
	has_opaque = has_opaque_bck;
	display_list = display_list_bck;

	globalThis.use_gpu = use_gpu_bck;
	globalThis.blit_enabled = use_gpu_bck ? false : evg.BlitEnabled;
	canvas = canvas_bck;
	webgl = webgl_bck;
	round_scene_size = round_scene_bck;
	group_bounds = group_bounds_bck;

	group.texture._gl_modified = true;
}


function check_camera(group)
{
	if (group.position) return group;
	if (group.target) return group;
	if (group.up) return group;
	if (group.viewport) return group;
	if (group.fov != 45) return group;
	if (group.ar && (group.ar * video_height != video_width)) return group;
	if ((group.znear != 0) || (group.zfar != 0)) return group;
	return null;
}

let dummy_mx = new evg.Matrix2D();

function update_group(group)
{
	let invalidate_offscreen = false;
	let draw_regular = true;

	if (group.update_flag) {
		if (group.update_flag & UPDATE_CHILD)
				invalidate_offscreen = true;

		group.update_flag = 0;

		if (!update_scene_matrix(group))
			return;
	} else if (group.use && group.mxjs) {
		if (!update_scene_matrix(group))
			return;
	}

	if ( (group.opacity<1) || (group.scaler>1) || (group.offscreen>GROUP_OST_NONE)) {
		if (invalidate_offscreen) {
			group_draw_offscreen(group);
		}
		if (group.offscreen > GROUP_OST_DUAL) return;

		//we fail at setting up group opacity, use regular draw
		if (group.canvas_offscreen) draw_regular = false;
	}

	//regular traversal
	if (draw_regular) {
		if (global_branch_depth > filter.maxdepth) {
			print(GF_LOG_ERROR, 'Maximum depth ' + filter.maxdepth + ' reached, aborting scene traversal - try increasing using --maxdepth=N');
			return;
		}

		//we need a copy at each level
		let global_transform_copy = global_transform.copy();
		let last_camera = active_camera;
		let current_camera = check_camera(group);
		if (current_camera) active_camera = current_camera;

		if ((group.mx.is3D || current_camera) && !global_transform.is3D) {
			global_transform = new evg.Matrix(global_transform_copy);
		}

		if (!group.mx_untransform)
			global_transform.add(group.mx, true);
		else
			global_transform.copy(group.mx);

		global_branch_depth ++;

		let first_obj = display_list.length;

		let ref_width_bck = reference_width;
		let ref_height_bck = reference_height;
		if (group.reference && (group.width>=0) && (group.height>=0)) {
			let rw = get_dim(group, true);
			let rh = get_dim(group, false);
			reference_width = rw;
			reference_height = rh;
		}

		if (group.use) {
			let target = get_scene(group.use);
			if (!target) target = get_group(group.use);
			if (target) {
				if ((group.use_depth<0) || (group.use_depth>group.current_depth)) {
					group.current_depth++;
					do_traverse_all(target, update_scene);
					group.current_depth--;
				}
			}
		} else {
			group.scenes.forEach(scene => do_traverse_all(scene, update_scene));
		}

		global_transform = global_transform_copy;
		global_branch_depth --;

		if (current_camera) active_camera = last_camera;
		reference_width = ref_width_bck;
		reference_height = ref_height_bck;

		let last_obj = display_list.length;
		if (group.reverse && (last_obj>first_obj)) {
			let trunc_display_list = display_list.splice(first_obj, last_obj-first_obj);
			trunc_display_list.reverse.apply(trunc_display_list);
			display_list.push.apply(display_list, trunc_display_list);
		}

		return;
	}

	if (group.skip_draw) return;

  //update matrix, opacity and push group to display list
	if (!update_scene_matrix(group))
		return;

	dummy_mx.identity = true;
	dummy_mx.translate(group.tr_x, group.tr_y);
	group.mx.add(dummy_mx, true);

	if (!group.mx_untransform)
		group.mx.add(global_transform);

	let draw_ctx = {};
	draw_ctx.mx = group.mx.copy();
	draw_ctx.screen_rect = null; //TODO
	draw_ctx.scene = null;
	draw_ctx.group = group;
	draw_ctx.opaque_pid = null;
	draw_ctx.is_opaque = false;
	draw_ctx.zorder = group.zorder;
	draw_ctx.camera = check_camera(group);

	display_list.push(draw_ctx);

}

function update_scene(scene)
{
	//this is a group
	if (Array.isArray(scene.scenes)) {
		if (scene.active)
			update_group(scene);
		//abort traversing, it is done in update_group
		return true;
	}

	if (!scene.active) {
		return;
	}

	//apply styles if any
	scene.styles.forEach(sid => {
		let style = get_element_by_id(sid, styles);
		if (!style) return;
		if (!style.updated && !style.forced) return;
		for (var x in style ) {
			if (typeof scene.mod[x] != undefined) {
				scene.set(x, style[x]);
			}
		}
	});

	//recreate our set of inputs for the scene if any
	if (scene.resetup_pids) {
		scene_resetup_pids(scene);
		scene.resetup_pids = false;
		scene.mod.update_flag |= UPDATE_PID;
	}
	//not ready
	if (!scene_update_visual_pids(scene)) return;

	scene.no_signal_time = 0;
	//sequences defined, check for no sequence active if autoshow / nosig are set
	if (scene.sequences.length && scene.check_active) {
		let disabled=true;
		let min_nosig = 0;
		scene.sequences.forEach (seq => {
			if (seq.active_state != 1) {
				if ( (seq.start_time > current_utc_clock) && (!min_nosig || (min_nosig>seq.start_time)) )
					min_nosig = seq.start_time;
				return;
			}
			seq.sources.forEach(s => {
				if (!s.in_prefetch) disabled = false;
			});
		});
		if (disabled) {
			//sequence not yet activated but we monitor no signal message
			if ((scene.nosig == 'before') && min_nosig) {}
			//sequence not activate, don't draw if autoshow
			else if (scene.nosig != 'all') {
				if (scene.autoshow) return;
			}
			scene.no_signal_time = min_nosig ? min_nosig : -1;
		}
	}
	//compute scene size in pixels
	let sw = get_dim(scene, true);
	let sh = get_dim(scene, false);
	if ((scene.mod.width != sw) || (scene.mod.height != sh)) {
		scene.mod.width = sw;
		scene.mod.height = sh;
		scene.mod.update_flag |= UPDATE_SIZE;
	}

	if (scene.mod.update_flag)
		scene.gl_uniforms_reload = true;
	else if (scene.gl_uniforms_reload)
		scene.gl_uniforms_reload = false;


	if (scene.mod.update_flag & UPDATE_FX) {
		scene.transition = null;
		scene.gl_program = null;
	}

	//update the matrix
	if (!update_scene_matrix(scene))
		return;

	if (!scene.mx_untransform) {
		if (!scene.mx.is3D && global_transform.is3D)
				scene.mx = new evg.Matrix(scene.mx);

		scene.mx.add(global_transform);
	}


	//update the scene
	scene.mod.force_draw = false;
	scene_in_update = scene;
	let res = scene.mod.update();
	scene_in_update = null;
	if (!res) return;
	if (res==2) has_clip_mask = true;


	let cover_type = 0;
	let use_rotation = false;
	if (scene.mx.is3D) {
		use_rotation = true;
	} else {
		if (!scene.mx.xx || !scene.mx.yy) return;
		if ((scene.mx.xx<0) || (scene.mx.yy<0) || (scene.mx.xy) || (scene.mx.yx)) use_rotation = true;
	}

	scene.mod.screen_rect = null;

	let rc = {x: - scene.mod.width/2, y : scene.mod.height/2, w: scene.mod.width, h: scene.mod.height};
	rc = scene.mx.apply(rc);

	if (group_bounds) {
			group_bounds = sys.rect_union(group_bounds, rc);
	}

	//if final matrix has rotation
	if (use_rotation) {
			cover_type = 0;
	} else {
		let can_blit = false;

		if ((rc.w == video_width)
				&& (rc.h == video_height)
				&& (rc.x == -video_width/2)
				&& (rc.y == video_height/2)
		) {
			can_blit = true;
			cover_type = 1;
		}
		else if ((rc.w > video_width)
				&& (rc.h > video_height)
				&& (rc.x <= -video_width/2)
				&& (rc.y >= video_height/2)
				&& (rc.x+rc.w >= video_width/2)
				&& (rc.y - rc.h <= -video_height/2)
		) {
			cover_type = 2;
		}

		if ((rc.x >= -video_width/2)
			&& (rc.y <= video_height/2)
			&& (rc.x+rc.w <= video_width/2)
			&& (rc.y - rc.h >= -video_height/2)
		) {
			can_blit = true;
		}

		if (can_blit) {
			rc.x += video_width/2;
			rc.y = video_height/2 - rc.y;

			rc.x = Math.floor(rc.x);
			rc.y = Math.floor(rc.y);
			rc.w = Math.floor(rc.w);
			rc.h = Math.floor(rc.h);
			scene.mod.screen_rect = rc;
		}
	}

	let opaque_pid = null;
	let is_opaque = false;

	if (!has_clip_mask && cover_type) {
		let opaque_pid_idx = scene.mod.fullscreen();
		let skip_opaque_test = false;
		//no pids associated (inactive sequence), do not reuse input pck
		if (scene.sequences.length && !scene.mod.pids.length) {
			skip_opaque_test = true;
		}
		//scene is using mix effect between sequence, do not reuse input pck
		else if ((scene.sequences.length>1) && scene.transition) {
			skip_opaque_test = true;
		} else if (scene.mod.pids.length) {
			let s = scene.mod.pids[0].pid.source;
			//no signal on input, do not reuse input pck (which may be non-null if `hold` was set)
			if (s.no_signal)
				skip_opaque_test = true;
			//sequence us in active transision state, do not reuse input pck
			else if ((s.sequence.transition_state>1) && (s.sequence.transition_state<4))
				skip_opaque_test = true;
		}

		//no reuse, force scene draw
		if (skip_opaque_test) {
			scene.mod.force_draw = true;
		} else {
			//get opaque pid
			if ((opaque_pid_idx>=0) && (opaque_pid_idx<scene.mod.pids.length)) {
				opaque_pid = scene.mod.pids[opaque_pid_idx].pid;
			}
			if (!opaque_pid) {
				is_opaque = scene.mod.is_opaque() || false;
			} else {
				//disable reuse if transition is active on pid
				if ((opaque_pid.source.sequence.transition_state>1) && (opaque_pid.source.sequence.transition_state<4)) {
					opaque_pid = null;
					is_opaque = false;
					scene.mod.force_draw = true;
				}
			}
		}
	}

	if (opaque_pid) {
		has_opaque = true;
		if (pids.indexOf(opaque_pid) < 0) {
			print(GF_LOG_WARNING, 'Broken scene ' + scene.id + ' returned fullscreen pid not in PID list');
			scene.active = false;
			return;
		}
	} else if (!use_rotation && is_opaque) {
		has_opaque = true;
	} else {
		is_opaque = false;
	}

	let draw_ctx = {};
	draw_ctx.mx = scene.mx.copy();
	draw_ctx.screen_rect = scene.mod.screen_rect;
	draw_ctx.scene = scene;
	draw_ctx.group = null;
	draw_ctx.opaque_pid = opaque_pid;
	draw_ctx.is_opaque = is_opaque;
	draw_ctx.zorder = scene.zorder;
	draw_ctx.camera = check_camera(scene);
	if (!draw_ctx.camera) draw_ctx.camera = active_camera;

	display_list.push(draw_ctx);
}

function set_canvas_yuv(pfmt)
{
	globalThis.canvas_yuv = sys.pixfmt_yuv(pfmt);
	round_scene_size = 0;
	if (canvas_yuv) {
		if (pfmt == 'yuv4') {}
		else if (pfmt =='yp4l') {}
		else if (pfmt =='yp4a') {}
		else if (pfmt =='yv4p') {}
		else if (pfmt =='y4ap') {}
		else if (pfmt =='y4lp') {}
		else if ((pfmt == 'yuv2')
			|| (pfmt == 'yp2l')
			|| (pfmt == 'uyvy')
			|| (pfmt == 'vyuy')
			|| (pfmt == 'yuyv')
			|| (pfmt == 'yvyu')
			|| (pfmt == 'uyvl')
			|| (pfmt == 'vyul')
			|| (pfmt == 'yuyl')
			|| (pfmt == 'yvyl')
		) {
			round_scene_size = 1;
		}
		else {
			round_scene_size = 2;
		}
	}

}

let prev_display_list = [];
let display_list = [];
let has_clip_mask = false;
let has_opaque = false;

function set_active_scene(ctx)
{
	active_camera = null;
	if (ctx.group) {
		ctx.group.mx.copy(ctx.mx);
		active_camera = ctx.camera;
		active_scene = null;
		return true;
	}
	if (ctx.scene) {
		ctx.scene.mx.copy(ctx.mx);
		ctx.scene.mod.screen_rect = ctx.screen_rect;
		active_camera = ctx.camera;
		active_scene = ctx.scene;
		return true;
	}
	return false;
}

function round_rect(rc_in)
{
	let rc = rc_in;
	if (rc.x % 2) { rc.x--; rc.w++; }
	if (rc.w % 2) rc.w--;
	//only round y for 420 & co
	if (round_scene_size==2) {
		if (rc.y % 2) { rc.y--; rc.h++; }
		if (rc.h % 2) rc.h--;
	}
	return rc;
}

let mask_mode = 0;

function draw_display_list_2d()
{
	display_list.forEach(ctx => {
		if (!set_active_scene(ctx)) return;
		//offscreen group
		if (ctx.group) {
			//only used to get the matrix
			active_scene = ctx;
			ctx.group.texture.mx = ctx.group.texture_mx;
			ctx.group.texture.set_alphaf(ctx.group.opacity);
			canvas_draw(ctx.group.path, ctx.group.texture, true);
			return;
		}
		let scene = ctx.scene;

		//if mask is active or if transition is active, disable blit
		if (mask_mode
			|| (scene.mod.pids.length && scene.mod.pids[0].pid && ((scene.mod.pids[0].pid.source.sequence.transition_state==3) || scene.mod.pids[0].pid.source.no_signal))
		) {
			scene.mod.screen_rect = null;
		} else if (round_scene_size && scene.sequences.length && scene.mod.screen_rect) {
			scene.mod.screen_rect = round_rect(scene.mod.screen_rect);
		}

		if (scene.no_signal_time) {
			draw_scene_no_signal();
		}
		else if (1) {
				scene.mod.draw(canvas);
		} else {
			try {
				scene.mod.draw(canvas);
			} catch (e) {
				print(GF_LOG_ERROR, 'Error during scene ' + scene.id + ' draw, disabling scene - error was ');
				scene.active = false;
			}
		}

		canvas.matrix = null;
		canvas.path = null;
		//do not reset clipper
	});
}


function update_display_list()
{
  display_list.length = 0;
  has_clip_mask = false;
	has_opaque = false;
	reference_width = video_width;
	reference_height = video_height;
	active_camera = null;
	global_transform.identity = true;
	root_scene.scenes.forEach(elm => do_traverse_all(elm, update_scene) );
}

let nolive_timeout_start = 0;
function process_video()
{
	if (!video_inputs_ready) {
		if (!video_time && !live_forced_play) {
			let waitfor = filter.lwait;
			if (!filter.live) waitfor *= 10;

			if (!nolive_timeout_start) nolive_timeout_start = current_utc_clock;
			else if (current_utc_clock - nolive_timeout_start > waitfor) {
				print(GF_LOG_ERROR, 'No video input after ' + waitfor + ' ms in non-live mode, aborting');
				do_terminate();
				do_audio = false;
				return;
			}
			print(GF_LOG_DEBUG, 'video not init');
			do_audio = false;
			return;
		}
		if (!filter.live) {
			print(GF_LOG_DEBUG, 'waiting for video frame');
			return;
		}
	}

	if (filter.live) {
		let now = sys.clock_us();
		let vtime = init_clock_us + 1000000 * (video_time+1) * video_time_inc / video_timescale;

		if (vtime < now) {
			if (!video_inputs_ready)
				print(pids.length ? GF_LOG_WARNING : GF_LOG_DEBUG, 'video input frame(s) not ready but output frame due since ' + (now - vtime) + ' us, forcing generation');
		} else if (vtime - now > video_frame_dur_us) {
			let next = vtime - now;
			if (next > video_max_resched_dur_us) next = video_max_resched_dur_us;
			//first reset any timeout set previously (due do source not being ready)
			filter.reschedule(0);
			//then set our real timeout
			filter.reschedule(next);
			print(GF_LOG_DEBUG, 'video output frame due in ' + (vtime - now) + ' us, waiting ' + next);
			return;
		}
	}

	update_display_list();

  //sort display list - BROKEN if multiple scene instances
  display_list.sort( (e1, e2) => { return e1.zorder - e2.zorder ; } );

	let pid_background = null;
	let pid_background_forward = false;

	let frame = null;
	let pfmt = video_pfmt;
	let restore_pfmt = false;
  //reverse browse
	if (has_opaque) {
		for (let i=display_list.length; i>0; i--) {
			let ctx = display_list[i-1];

			if (ctx.opaque_pid || ctx.is_opaque) {
				display_list = display_list.splice(i-1);
			}

			if (ctx.opaque_pid) {
				pid_background = ctx.opaque_pid;
				pid_background_forward = (ctx.scene && ctx.scene.mod.identity()) || false;

				//first time we try using this pid as base for canvas, check if pixel format is supported as canvas target
				if (ctx.opaque_pid.pfmt_check==0) {
					ctx.opaque_pid.pfmt_check = 1;
					try {
						let cnv = new evg.Canvas(48, 48, pid.pfmt);
					} catch (e) {
						ctx.opaque_pid.pfmt_check = 2;
					}
				}

				if (!pid_background.pck || (ctx.opaque_pid.pfmt_check == 2)) {
					pid_background_forward = false;
					pid_background = null;
					has_opaque = false;
					if (ctx.scene) ctx.scene.mod.force_draw = true;
				}
				break;
			}
			if (ctx.is_opaque) {
				break;
			}
		}
	}


	if (!filter.gpu && has_opaque) {
		if (display_list.length>1)
			pid_background_forward = false;

		//init canvas 2D from this packet
		if (pid_background && (pid_background.width==video_width) && (pid_background.height==video_height)) {
			//mismatch in pixel format cannot reuse packet
			if (!filter.dynpfmt && (pid_background.pfmt != video_pfmt)) {
				pid_background_forward = false;
				display_list[0].scene.mod.force_draw = true;
			} else {
		  	if (last_forward_pixfmt != pid_background.pfmt) {
					last_forward_pixfmt = pid_background.pfmt;
					canvas_reconfig = true;
					if (last_forward_pixfmt != video_pfmt) {
						print(GF_LOG_DEBUG, 'reconfiguring output video pixel format to ' + last_forward_pixfmt);
						vout.set_prop('PixelFormat', last_forward_pixfmt);
						vout.set_prop('Stride', pid_background.get_prop('Stride') );
						vout.set_prop('StrideUV', pid_background.get_prop('StrideUV') );
					}
				}
				pfmt = last_forward_pixfmt;
				if (pid_background_forward) {
					//direct ref
					frame = vout.new_packet(pid_background.pck);
					print(GF_LOG_DEBUG, 'forwarding packet from source ' + pid_background.source.logname);
				} else {
					//copy packet (we may need to reuse source packet at next frame)
					frame = vout.new_packet(pid_background.pck, false, true);
					print(GF_LOG_DEBUG, 'copying packet from source ' + pid_background.source.logname);
				}
			}
			if (filter.dynpfmt==1) {
				if (last_forward_pixfmt) {
					video_pfmt = last_forward_pixfmt;
				}
				print(GF_LOG_INFO, 'Video output format will be ' + video_pfmt);
				filter.dynpfmt = 0;
			}
		} else {
			if (last_forward_pixfmt && (last_forward_pixfmt != video_pfmt))
					restore_pfmt = true;
		}

	} else if (last_forward_pixfmt && (last_forward_pixfmt != video_pfmt)) {
		restore_pfmt = true;
	}

	if (restore_pfmt) {
		vout.set_prop('PixelFormat', video_pfmt);
		vout.set_prop('Stride', null);
		vout.set_prop('StrideUV', null);
		last_forward_pixfmt = 0;
		canvas_reconfig = true;
	}

	clip_stack.length = 0;
	global_branch_depth = 0;
  if (filter.gpu) {

		webgl.activate(true);


	  webgl.viewport(0, 0, video_width, video_height);
	  webgl.clearDepth(1.0);
  	webgl.enable(webgl.DEPTH_TEST);
  	webgl.enable(webgl.BLEND);
  	webgl.blendFunc(webgl.SRC_ALPHA, webgl.ONE_MINUS_SRC_ALPHA);
  	webgl.depthFunc(webgl.LEQUAL);
  	webgl.clear(webgl.DEPTH_BUFFER_BIT);

		if (!has_opaque) {
			let a = sys.color_component(back_color, 0);
			let r = sys.color_component(back_color, 1);
			let g = sys.color_component(back_color, 2);
			let b = sys.color_component(back_color, 3);
	  	webgl.clearColor(r, g, b, a);
		  webgl.clear(webgl.COLOR_BUFFER_BIT);
		}

	  wgl_clipper = null;

	  if (!display_list.length) {
			active_scene = null;
			draw_scene_no_signal(null);
		} else {
			display_list.forEach(ctx => {
				if (!set_active_scene(ctx)) return;

				//offscreen group
				if (ctx.group) {
					//only used to get the matrix
					active_scene = ctx;
					ctx.group.texture.mx = ctx.group.texture_mx;
					ctx.group.texture.set_alphaf(ctx.group.opacity);
					canvas_draw(ctx.group.path, ctx.group.texture, true);
					return;
				}

				if (ctx.scene.gl_type == SCENE_GL_NONE) {
					ctx.scene.mod.draw();
				} else {
					ctx.scene.mod.draw_gl(webgl);
				}
			});
		}


		canvas_offscreen_deactivate();

	  webgl.disable(webgl.SCISSOR_TEST);

		webgl.flush();
		webgl.activate(false);

		frame = vout.new_packet(webgl, () => { filter.frame_pending=false; } );
		filter.frame_pending = true;
  } else {
		if (!frame) {
			frame = vout.new_packet(vout_size);
			print(GF_LOG_DEBUG, 'allocating packet for framebuffer');
		}

		if (!pid_background_forward) {
			if (!canvas) {
				try {
					canvas = new evg.Canvas(video_width, video_height, pfmt, frame.data);
				} catch (e) {
					print(GF_LOG_ERROR, 'Failed to create canvas: ' + e);
					do_terminate();
					return;
				}

				if (filter.thread) {
					canvas.enable_threading(filter.thread);
				}
				if (use_canvas_3d)
					canvas.enable_3d();

				if (typeof canvas.blit != 'function')
					globalThis.blit_enabled = false;
				else if (!globalThis.use_gpu)
					globalThis.blit_enabled = evg.BlitEnabled;

				canvas.pix_fmt = pfmt;
				set_canvas_yuv(pfmt);
			} else if (canvas_reconfig) {
				canvas_reconfig = false;
				try {
					canvas.reassign(video_width, video_height, pfmt, frame.data);
				} catch (e) {
					print(GF_LOG_ERROR, 'Failed to reassign canvas: ' + e);
					do_terminate();
					return;
				}

				canvas.pix_fmt = pfmt;
				set_canvas_yuv(pfmt);
			} else {
				canvas.reassign(frame.data);
			}

			//reset clipper
			canvas.clipper = null;

			if (!has_opaque) {
				print(GF_LOG_DEBUG, 'canvas clear to ' + back_color);
				canvas.clear(back_color);
			}

		  if (!display_list.length) {
				active_scene = null;
				draw_scene_no_signal(null);
			} else {
				draw_display_list_2d();
			}
		}
  }

	frame.cts = frame.dts = video_time;
	frame.dur = video_time_inc;
	frame.sap = GF_FILTER_SAP_1;
	frame.send();

	if (filter.live) {
		let now = sys.clock_us();
		if (!video_time) {
			init_utc = sys.get_utc();
			init_clock_us = now;
		}
		let vtime  = init_clock_us + 1000000*video_time * filter.fps.d / filter.fps.n;
		if (vtime + video_frame_dur_us < now) {
			print(GF_LOG_WARNING, 'sent video frame TS ' + (video_time) + '/' + video_timescale + ' too late by ' + Math.ceil(now - vtime - video_frame_dur_us) + ' us');
		} else if (vtime < now) {
			print(GF_LOG_DEBUG, 'sent video frame TS ' + (video_time) + '/' + video_timescale + ' (' + Math.ceil(now-vtime) + ' us early)');
		} else {
			print(GF_LOG_DEBUG, 'sent video frame TS ' + (video_time) + '/' + video_timescale);
		}
		next_video_gen_time = vtime + video_frame_dur_us;
	} else {
			print(GF_LOG_DEBUG, 'sent video frame TS ' + (video_time) + '/' + video_timescale);
	}

	video_time += video_time_inc;

	let old_prev = prev_display_list;
	prev_display_list = display_list;
	display_list = old_prev;
	display_list.length = 0;

	styles.forEach(style => {
		style.updated = false;
	});
}

function process_audio()
{
	let nb_samples = 0;
	let i,j,k, nb_active_audio=0;
	let mix_pids = [];

	//if video is playing and not blocking, process audio event if blocking - this avoids cases where output frames are consumed by burst
	//and the video burst happens befor the audio burst: we need to send video to fill the burst then unblock audio, but video won't be processed
	//if audio is blocking...
	if (aout.would_block && (!video_playing || vout.would_block)) {
		return;
	}

	if (!audio_inputs_ready) {
		if (!audio_time && !live_forced_play)
			return;
	}
	if (!video_playing && filter.live) {
		let now = sys.clock_us();
		let atime = init_clock_us + 1000000 * (audio_time) / audio_timescale;

		if (atime - now > audio_frame_dur_us) {
			print(GF_LOG_DEBUG, 'audio output frame due in ' + (atime - now) + ' us, waiting');
			let next = atime - now;
			if (next > audio_max_resched_dur_us) next = audio_max_resched_dur_us;
			filter.reschedule(next);
			return;
		}

	}

	let empty_ok = false;
	pids.forEach(pid => {
		if (pid.type != TYPE_AUDIO) return;
		if (!pid.source.playing) return;
		if (pid.source.no_signal_state) {
			empty_ok = true;
			return;
		}
		if (pid.source.disabled) return;
		if (pid.eos) return;
		if (pid.done) return;
		nb_active_audio++;
		if (!pid.pck) return;
		let samp_avail = pid.nb_samples - pid.samples_used;
		if (!nb_samples || (samp_avail<nb_samples)) nb_samples = samp_avail;
		mix_pids.push(pid);
		if (!pid.data) {
			pid.data = pid.pck.data;
		}
		//default is mute
		pid.volume = 0;
		pid.detached = true;
	});

	if (nb_samples > filter.alen)
		nb_samples = filter.alen;

	let aframe = null;
	let empty=false;

	if (!nb_samples) {
		//todo - check if we need to move forward after some time (and drop input), to allow holes in timeline
		if (nb_active_audio) {
			if (!filter.live) {
				print(GF_LOG_DEBUG, 'waiting for audio frame');
				return;
			}
			if (!empty_ok) {
				let atime = init_clock_us + (audio_time + 2*filter.alen)*1000000/filter.sr;
				let now = sys.clock_us();
				if (atime >= now) {
					print(GF_LOG_DEBUG, 'waiting for audio frame');
					return;
				}
				if (!empty_ok)
					print(GF_LOG_WARNING, 'empty audio input but audio frame due since ' + Math.ceil(now-atime) + ' us');
			}
		}

		//send an empty packet of 100 samples
		aframe = aout.new_packet(channels * filter.alen);
		let output = new Uint16Array(aframe.data);
		output.fill(0);
		empty=true;
		nb_samples = filter.alen;
	} else {

		//apply volume per scene
		do_traverse(root_scene, scene => {
			scene.apids.forEach(pid => {
				pid.detached = false;
				if (pid.source.mix)
					pid.volume = scene.mod.volume * pid.source.mix_volume;
				else
					pid.volume = scene.mod.volume;

				//scene inactive
				if (!scene.active) {
					//pid was active, either mute or fade to 0
					if (pid.active) {
						if ((scene.mod.fade=='out') || (scene.mod.fade=='inout')) {
							pid.fade = 2; //fade to 0
						} else {
							pid.volume = 0;
						}
						pid.active = false;
					} else {
						pid.volume = 0;
					}
				}
				//scene active, pid was inactive: use full volume and fade from to 0
				else if (!pid.active) {
					if ((scene.mod.fade=='in') || (scene.mod.fade=='inout')) {
						pid.fade = 1; //fade from 0
					}
					pid.active = true;
					//remember audio fade type
					pid.audio_fade = scene.mod.fade;
				}
			});
		});

		//also do a fadeout for pids no longer active
		mix_pids.forEach(pid => {
			if (!pid.detached || !pid.active) return;
			if ((pid.audio_fade=='out') || (pid.audio_fade=='inout')) {
				pid.fade = 2; //fade to 0
			}
			pid.active = false;
		});

		aframe = aout.new_packet(nb_samples * channels * sample_size);

		audio_mix.mix(audio_time, aframe.data, mix_pids);

		mix_pids.forEach(pid => {
			if (pid.nb_samples <= pid.samples_used) {
				pid.nb_samples = 0;
				pid.samples_used = 0;
				pid.pck.unref();
				pid.pck = null;
				pid.data = null;
				pid.fade = 0;
			}
		});
	}

	aframe.cts = audio_time;
	aframe.dur = nb_samples;
	aframe.sap = GF_FILTER_SAP_1;
	aframe.send();

	let msg = 'sent ' + (empty ? 'empty ' : '') + 'audio frame TS ' + (audio_time) + '/' + audio_timescale;

	if (filter.live) {
		let now = sys.clock_us();
		if (!audio_time && !video_playing) {
			init_utc = sys.get_utc();
			init_clock_us = now;
		}

		let atime  = init_clock_us + 1000000*audio_time/audio_timescale;
		if (atime + nb_samples < now) {
			print(GF_LOG_WARNING, msg + ' too late by ' + Math.ceil(now - atime - nb_samples) + ' us');
		} else if (atime < now) {
			print(GF_LOG_DEBUG, msg + ' (' + (now-atime) + ' us early)');
		} else {
			print(GF_LOG_DEBUG, msg);
		}
		next_audio_gen_time = atime + audio_frame_dur_us;
	} else {
			print((empty && nb_active_audio) ? GF_LOG_ERROR : GF_LOG_DEBUG, msg);
	}

	audio_time += nb_samples;
}

let inactive_sources = 0;

function next_source(s, prefetch_check)
{
	let next_src_idx = -1;
	let src_idx = s.sequence.sources.indexOf(s);
	if (src_idx<0) {
		print(GF_LOG_WARNING, 'source ' + s.logname + ' was removed from parent sequence while active');
		if (s.sequence.sources.length)
			next_src_idx = 0;
	} else if (src_idx +1 < s.sequence.sources.length) {
		next_src_idx = src_idx+1;
	} else if (s.sequence.loop != 0) {
		if (!prefetch_check && (s.sequence.loop > 0))
			s.sequence.loop -= 1;
		next_src_idx = 0;
	}

	let next_src = null;
	if (next_src_idx>=0) {
		for (let i=next_src_idx; i<s.sequence.sources.length; i++) {
			next_src = s.sequence.sources[i];
			if (!next_src.disabled) break;
			next_src = null;
		}
		if (!next_src && next_src_idx) {
			for (let i=0; i<s.sequence.sources.length; i++) {
				next_src = s.sequence.sources[i];
				if (!next_src.disabled) break;
				next_src = null;
			}
		}
		if (!next_src) {
			print(GF_LOG_INFO, 'All sources in sequence disabled !');
			s.sequence.active_state = 2;
			if (s.sequence.id)
				trigger_watcher(s.sequence, 'active', false);
		}
	}
	return next_src;
}

function sequence_over(s, force_seq_reload)
{
	let is_transition_start = false;
	if (s.sequence.transition_state == 2) {
		is_transition_start = true;
		s.sequence.transition_state = 3;
	}
	//error loading transition, reset state
	else if (s.sequence.transition_state == 4) {
		s.sequence.transition_state = 0;
		s.video_time_at_init = 0;
		s.sequence.sources.forEach(src => { src.next_in_transition = false;});
	}
	//end of transition, reset state and return (next is already loaded)
	else if (s.sequence.transition_state == 3) {
		s.sequence.transition_state = 0;
		print(GF_LOG_INFO, 'End of transition for sequence ' + s.sequence.id);
		do_traverse(root_scene, scene => {
			if (scene.sequences.indexOf(s.sequence) >= 0) {
				scene.resetup_pids = true;
			}
		});
		s.sequence.sources.forEach(src => { src.next_in_transition = false;});
		print(GF_LOG_INFO, 'stopping source ' + s.logname);
  	s.video_time_at_init = 0;
		stop_source(s, true);
		return;
	}

	s.video_time_at_init = 0;
	let force_load_source = false;

	while (1) {

	//no packets for the desired time range
	if (!s.timeline_init && s.sequence.start_offset) {
		let dur = s.duration;
		if (s.media_stop > s.media_start)
			 dur = s.media_stop - s.media_start;
		if (dur < s.sequence.start_offset) {
			s.sequence.start_offset -= dur;
		} else {
			print(GF_LOG_DEBUG, 'reset sequence start offset ' + s.sequence.start_offset + ' to 0 - ' + s.duration);
			s.sequence.start_offset = 0;
		}
	}

	let next_src = next_source(s, false);
	let is_same_source = (!force_load_source && (next_src === s)) ? true : false;
	if (force_seq_reload)
		is_same_source = false;

	if (!is_transition_start) {
		print(GF_LOG_DEBUG, 'source ' + s.logname + ' is over, sequence start offset ' + s.sequence.start_offset);
		if (!is_same_source)
			stop_source(s, true);

		//transition has been started so next source is loaded, only reset scene pids
		if (s.sequence.transition_state==3) {
			do_traverse(root_scene, scene => {
				if (scene.sequences.indexOf(s.sequence) >= 0) {
						scene.resetup_pids = true;
						print(GF_LOG_DEBUG, 'end of transition, scene PID resetup');
				}
			});
			s.sequence.transition_state = 0;
			return;
		}
	}

	if (next_src) {
		if (next_src.in_prefetch) {
			print(GF_LOG_INFO, 'End of prefetch for ' + next_src.logname);
			next_src.in_prefetch = 0;
			next_src.pids.forEach(pid => {
				if (pid.type==TYPE_VIDEO) {
					let pck = pid.get_packet();
					print(GF_LOG_DEBUG,'got packet during prefetch: ' + ((pck==null) ? 'no' : 'yes'));
				}
			});

			do_traverse(root_scene, scene => {
				if (scene.sequences.indexOf(s.sequence) >= 0) {
						scene.resetup_pids = true;
						print(GF_LOG_DEBUG, 'end of prefetch, scene PID resetup');
				}
			});

		} else {
			print(GF_LOG_DEBUG, 'load next in sequence ' + next_src.logname);
			let dur = next_src.duration;
			if (next_src.media_stop > next_src.media_start)
					dur = next_src.media_stop - next_src.media_start;
			if (dur && (dur < s.sequence.start_offset)) {
				print(GF_LOG_DEBUG, 'source ' + next_src.logname + ' will end before sequence start offset ' + s.sequence.start_offset + ' - skipping');
				s = next_src;
				force_load_source = true;
				continue;
			}

			if (!is_same_source) {
				open_source(next_src);
			} else {
				stop_source(next_src, false);
			}
			play_source(next_src);
		}
	} else if (! is_transition_start) {
		inactive_sources++;
		s.sequence.active_state = 2;
		print(GF_LOG_DEBUG, 'sequence over');
	}
	return;

	}
}

function fetch_source(s)
{
	let wait_audio=0, wait_video=0;
	if (s.disabled) {
		inactive_sources++;
		return;
	}
	if (s.removed) {
		inactive_sources++;
		return;
	}

	//check if source must be started
	if (!s.playing) {
		if (!s.sequence.active_state && (s.sequence.start_time>0) && (s.sequence.start_time <= current_utc_clock + s.prefetch_ms)) {
			if (s.sequence.skip_start_offset) {
				s.sequence.start_offset = 0;
				print(GF_LOG_INFO, 'Starting sequence ' + s.sequence.id + ' at UTC ' + current_utc_clock);
			} else {
				s.sequence.start_offset = (current_utc_clock + s.prefetch_ms - s.sequence.start_time ) / 1000.0;
				if (s.sequence.start_offset<0) s.sequence.start_offset = 0;
				print(GF_LOG_INFO, 'Starting sequence ' + s.sequence.id + ' at UTC ' + current_utc_clock + ' with start offset ' + s.sequence.start_offset + ' sec');
			}

			s.sequence.active_state = 1;
			if (s.sequence.id)
				trigger_watcher(s.sequence, 'active', true);

			if (s.media_start + s.media_stop < s.sequence.start_offset) {
					print(GF_LOG_INFO, 'source ' + s.logname + ' will end before sequence start_offset, skipping source');
					s.sequence.start_offset -= s.media_start + s.media_stop;
					inactive_sources++;
					sequence_over(s, false);
					return;
			} else {
					print(GF_LOG_INFO, 'source ' + s.logname + ' open');
				open_source(s);
				play_source(s);
				if (current_utc_clock < s.sequence.start_time) {
						s.in_prefetch = 2;
						inactive_sources++;
						return;
				}
			}
		} else {
			inactive_sources++;
			return;
		}
	}
	if (s.in_prefetch) {
		if ((s.in_prefetch==2) && (s.sequence.start_time>0) && (s.sequence.start_time <= current_utc_clock)) {
			s.in_prefetch = 0;
			print(GF_LOG_DEBUG, 'end of prefetch for ' + s.logname + ' , scene PID resetup');
			do_traverse(root_scene, scene => {
				if (scene.sequences.indexOf(s.sequence) >= 0) {
						scene.resetup_pids = true;
				}
			});
		} else {
			inactive_sources++;
			return;
		}
	}

	//done with sequence
	if ((s.sequence.active_state==1) && (s.sequence.stop_time>0) && (s.sequence.stop_time <= current_utc_clock)) {
	  	s.video_time_at_init = 0;
			stop_source(s, true);
			s.sequence.transition_state = 0;
			do_traverse(root_scene, scene => {
				if (scene.sequences.indexOf(s.sequence) >= 0) {
					scene.resetup_pids = true;
				}
			});
			s.sequence.sources.forEach(src => { src.next_in_transition = false;});
			inactive_sources++;
			print(GF_LOG_INFO, 'End of sequence ' + s.sequence.id + ' at ' + (current_utc_clock-s.sequence.stop_time));
			s.sequence.active_state = 2;
			if (s.sequence.id)
				trigger_watcher(s.sequence, 'active', false);
			return;
	}

	let min_cts = 0;
	let min_timescale = 0;
	let nb_over = 0;
	let nb_active = 0;
	let force_seq_over = false;
	let source_restart = false;
	s.no_signal = 0;

	s.pids.forEach(pid => {
		if (pid.done) {
			nb_over++;
			return;
		}
		while (1) {

		let pck = pid.get_packet();
		if (!pck) {
			let force_wait_pid = !filter.live;
			if (pid.eos) {
				pid.done = true;
				nb_over++;
				return;
			}

			let tdiff = current_utc_clock - pid.last_pck_time;
			if (tdiff > filter.lwait) {
				force_wait_pid = false;
				if (s.no_signal < tdiff) s.no_signal = tdiff;
				//for video pid, drop ref !
				if (pid.pck) {
						pid.pck.unref();
						pid.pck = null;
				}
				if (current_utc_clock - pid.last_pck_time > filter.ltimeout) {
					s.timeline_init = false;
					if (s.has_ka_process) {
						pid.source.src.forEach(src => {
							if (!src.process_id) return;
							let res = os.waitpid(src.process_id, os.WNOHANG);
							let msg = (res[0] == src.process_id) ? ('exited with code ' + res[1]) : 'not responding, restarting';
							print(GF_LOG_ERROR, 'Process for source ' + pid.source.logname + ' (' + src.in + ') ' + msg);
							source_restart = true;
						});
					}
				}
				return;
			}

			if (force_wait_pid || !pid.pck) {
				if (pid.type==TYPE_VIDEO)
					wait_video = true;
				else
					wait_audio = true;
			}
			return;
		}
		if (pid.type==TYPE_UNHANDLED) {
			pid.drop_packet();
			return;
		}
		if ( (!audio_playing && (pid.type==TYPE_AUDIO))
			|| (!video_playing && (pid.type==TYPE_VIDEO))
		) {
			if (!pid.done) {
				pid.send_event( new FilterEvent(GF_FEVT_STOP) );
				pid.done = true;
			}
			nb_over++;
			pid.drop_packet();
			return;
		}

		//we got a packet, if first one init clock
		if (!s.timeline_init) {
			if (!min_timescale || (min_cts * pid.timescale > pck.cts * min_timescale)) {
				min_cts = pck.cts;
				min_timescale = pid.timescale;
			}
			return;
		}

		//source is active (has packets)
		nb_active++;

		let check_transition = false;
		if (video_playing) {
			if (pid.type == TYPE_VIDEO) check_transition = true;
		} else {
			if (pid.type != TYPE_VIDEO) check_transition = true;
		}

		if (!s.sequence.prefetch_check && check_transition) {
			let check_end = s.duration;
			if ((s.media_stop>s.media_start) && (s.media_stop<s.duration)) check_end = s.media_stop;

			if (s.duration && s.hold && (s.media_stop > s.duration)) {
				s.sequence.transition_state = 4;
				s.transition_offset = 0;
				s.sequence.transition_dur = 0;
			}

			if ((s.sequence.transition_state<=1) && s.sequence.transition_effect && next_source(s, true) ) {
				let dur = s.sequence.transition_effect.dur || 0;
				if (dur) {
					s.sequence.transition_state = 1;
					s.sequence.transition_dur = dur;

					if (check_end > dur) check_end -= dur;
					else {
						check_end = 0;
					}
				}
			}

			if (check_end>0) {
				if ((pck.cts - pid.init_ts) > (s.transition_offset + check_end - s.media_start - s.prefetch_sec) * pid.timescale) {
					let next_src = next_source(s, true);
					s.sequence.start_offset = 0;
					if (next_src && (next_src != s)) {
						print(GF_LOG_INFO, 'time to prefetch next source ' + next_src.logname + ' in sequence ' + s.sequence.id +  ' - end time: ' + (check_end - s.media_start), pck.cts, pid.init_ts, pid.timescale, s.transition_offset);
						open_source(next_src);
						play_source(next_src);
						next_src.in_prefetch = 1;
						if ((!s.sequence.transition_state<=1) && s.sequence.transition_effect) {
							next_src.transition_offset = s.sequence.transition_dur;
							next_src.next_in_transition = true;
						}
					}
					s.sequence.prefetch_check = true;
				}
			}
		}
		//except for video, check this packet is not out of play range
		if ((s.media_stop>s.media_start) && (pid.type != TYPE_VIDEO)) {
			if ((pck.cts - pid.init_ts) > (s.transition_offset + s.media_stop - s.media_start) * pid.timescale) {
				pid.drop_packet();
				pid.done = true;
				pid.send_event( new FilterEvent(GF_FEVT_STOP) );
				nb_over++;
				print(GF_LOG_DEBUG, 'End of playback reached for ' + s.logname);
				return;
			}
		}

		if (check_transition && (s.sequence.transition_state==1)) {
			let check_end = s.duration;
			if ((s.media_stop>s.media_start) && (s.media_stop<s.duration)) check_end = s.media_stop;

			if (check_end > s.sequence.transition_dur)
				check_end -= s.sequence.transition_dur;
			else {
				check_end = 0;
			}

			if ((pck.cts - pid.init_ts) >= (s.transition_offset + check_end - s.media_start) * pid.timescale) {
				if (load_transition(s.sequence)) {
					s.next_in_transition = false;
					print(GF_LOG_INFO, 'time to activate transition in sequence ' + s.sequence.id + ' end_time: ' + (check_end - s.media_start) + ' cts ' + pck.cts + ' init_cts ' + pid.init_ts);
					s.sequence.transition_state = 2;
					s.sequence.transition_start = video_time;
				} else {
					//error
					s.sequence.transition_state = 4;
				}
			}
		}

		if (!pid.pck) {
			pid.pck = pck.ref();
			pid.drop_packet();
			pid.frame_ts = pid.translate_timestamp(pid.pck.cts);
			if (pid.pck.blocking_ref) {
				blocking_ref_pids.push(pid);
				pid.is_blocking = true;
			}
			pck = null;
		}

		pid.last_pck_time = current_utc_clock;

		if (pid.type==TYPE_AUDIO) {
			if (!audio_playing) {
				//audio not playing, discard packet if time before current video time
				if (pid.frame_ts * video_timescale < video_time * pid.timescale) {
					pid.pck.unref();
					pid.pck=null;
					continue;
				}
			}

			if ( (s.media_stop>s.media_start) && ((pid.pck.cts + pid.pck.dur) >= s.media_stop * pid.timescale)) {
				let diff_samples = Math.floor(s.media_stop * audio_timescale);
				if (audio_timescale == pid.timescale) {
					diff_samples -= Math.floor(pid.pck.cts);
				} else {
					diff_samples -= Math.floor(pid.pck.cts * audio_timescale / pid.timescale);
				}
				if (diff_samples>0) {
					pid.nb_samples = diff_samples;
					if ((pid.source.audio_fade=='out') || (pid.source.audio_fade=='inout')) {
						pid.fade = 2; //fade to 0
					}
					print(GF_LOG_DEBUG, 'End of playback pending for ' + pid.source.logname + ' - samples left ' + diff_samples);
				} else {
					//we are done
					pid.pck.unref();
					pid.pck = null;
					pid.done = true;
					pid.send_event( new FilterEvent(GF_FEVT_STOP) );
					nb_over++;
					print(GF_LOG_DEBUG, 'End of playback reached for ' + pid.source.logname);
				}
			} else {
				pid.nb_samples = pid.pck.size / pid.channels / sample_size;
			}
			return;
		}
		else if (pid.type!=TYPE_VIDEO) {
			return;
		}
		if (!video_playing) {
			if (pid.frame_ts * audio_timescale < audio_time * pid.timescale) {
				pid.pck.unref();
				pid.pck=null;
				continue;
			}
		}

		//check closest frame
		while (1) {
			pck = pid.get_packet();
			if (!pck) break;
			let next_ts = pid.translate_timestamp(pck.cts);
			if (next_ts > video_time) {
				let diff_next = next_ts - video_time;
				let diff_cur = video_time - pid.frame_ts;
				if (diff_next > diff_cur)
					break;
			}

			pid.pck.unref();
			pid.pck = pck.ref();
			pid.drop_packet();
			pid.frame_ts = next_ts;
		}
		//if current frame is out of play range (>=), consider source done
		if (s.media_stop>s.media_start) {
			if ((pid.pck.cts - pid.init_ts) >= (s.transition_offset + s.media_stop - s.media_start) * pid.timescale) {
				pid.drop_packet();
				pid.done = true;
				pid.send_event( new FilterEvent(GF_FEVT_STOP) );
				nb_over++;
				print(GF_LOG_DEBUG, 'End of playback reached for ' + s.logname);
				return;
			}
		}

		print(GF_LOG_DEBUG, 'Video from ' + s.logname + ' will draw pck CTS ' + pid.pck.cts + '/' + pid.timescale + ' translated ' + pid.frame_ts + ' video time ' + video_time);
		return;
	}
	});
	//done fetching pids
	if (s.pids.length && (nb_over == s.pids.length)) {
		nb_over = 1;
		let relaunch=false;
		s.src.forEach(src => {
			if (!src.process_id) return;
			let res = os.waitpid(src.process_id, os.WNOHANG);
			//child process is dead, if error >2, relaunch if keep alive
			if (res[0] == src.process_id) {
				if (res[1]>2) {
					print(GF_LOG_WARNING, 'Child process for src ' + s.logname + ' exit with error ' + res[1] + (s.keep_alive ? ", restarting source" : "") );
					relaunch = s.keep_alive ? 1 : 0;
				}
			}
			//process is still alive but we are done
			else if (!source_restart) {
				print(GF_LOG_INFO, 'Child process for src ' + s.logname + ' still alive but eos notified');
			}
		});

		if (relaunch) {
			source_restart = true;
		} else if (s.duration && s.hold && (s.media_stop > s.duration)) {
			let pid = s.pids[0];
			let target_end, ref_time, ref_timescale;
			if (pid.type == TYPE_VIDEO) {
				ref_timescale = video_timescale;
				ref_time = video_time;
			} else {
				ref_timescale = audio_timescale;
				ref_time = audio_time;
			}
			target_end = pid.init_clock + ref_timescale * (s.media_stop - s.media_start);

			if (target_end > ref_time) {
				nb_over = 0;
				s.no_signal = true;
				if (!s.no_signal_state) {
					print(GF_LOG_INFO, 'source ' + s.logname + ' is over but hold enabled, entering no signal state');
				}
				s.no_signal_state = (target_end - ref_time) / ref_timescale;
				return;
			} else {
				s.no_signal = false;
				s.no_signal_state = false;
				//first source in sequence is done, reset start offset
				s.sequence.start_offset = 0;
				s.sequence.transition_state = 0;
				print(GF_LOG_INFO, 'source ' + s.logname + ' is over, exiting no signal state');
			}
		} else {
			//first source in sequence is done, reset start offset
			s.sequence.start_offset = 0;
			print(GF_LOG_INFO, 'source ' + s.logname + ' is over - transition state: ' + s.sequence.transition_state);
		}
  } else if (!s.pids.length && s.has_ka_process) {
  	nb_over = 0;
	  s.src.forEach(src => {
			if (src.process_id) {
				let res = os.waitpid(src.process_id, os.WNOHANG);
				if (res[0] == src.process_id) {
					print(GF_LOG_ERROR, 'Child process creation failed for source ' + src.in + ' - removing');
					nb_over ++;
				}
			}
	  });
	  if (nb_over == s.src.length) {
	  	nb_over = 1;
	  	force_seq_over = true;
			return;
	  } else {
	  	nb_over = 0;
	  }
	} else {
		nb_over = 0;
	}

	if (!nb_active && source_restart) {
		nb_over=0;
		s.timeline_init=0;
		stop_source(s, true);
		open_source(s);
		play_source(s);
		video_inputs_ready = false;
	}

	//source is playing but no packets, we need to force reschedule to probe for no signal, we do that every second
	if (!nb_active && !nb_over) {
		filter.reschedule(1000);
	}
	if (!s.timeline_init && !nb_over) {
		if (s.no_signal) {
			return;
		}
		//no pids yet
		if (!min_timescale) {
			video_inputs_ready = audio_inputs_ready = false;
			return;
		}
		if (wait_video || wait_audio) {
			if (wait_video) video_inputs_ready = false;
			if (wait_audio) audio_inputs_ready = false;
			return;
		}

		print(GF_LOG_DEBUG, 'clock is init for source ' + s.logname + ' using TS ' + min_cts + '/' + min_timescale);
		s.pids.forEach(pid => {
			pid.init_ts = min_cts;
			if (min_timescale != pid.timescale) {
				pid.init_ts *= pid.timescale;
				pid.init_ts /= min_timescale;
			}
			if (pid.type==TYPE_VIDEO) {
				pid.init_clock = video_time;
			} else {
				pid.init_clock = audio_time;
				if ((pid.source.audio_fade=='in') || (pid.source.audio_fade=='inout')) {
					pid.fade = 1; //fade from 0
				}
			}
			print(GF_LOG_DEBUG, 'PID ' + s.logname + '.' + pid.name + ' clock init TS ' + pid.init_ts + ' clock value ' + pid.init_clock);
		});
		s.timeline_init = true;

		if (s.seek && !s.video_time_at_init)
			s.video_time_at_init = video_time+1;

		fetch_source(s);
	}

	if (!force_seq_over && (!s.pids.length || !nb_over)) {
		if (s.sequence.transition_state == 2) {
			print(GF_LOG_DEBUG, 'activating next source for transition in sequence ' + s.sequence.id);
			sequence_over(s, false);
		}
		//active sources and waiting for inputs
		else if (s.pids.length) {
			if (wait_video) video_inputs_ready = false;
			if (wait_audio) audio_inputs_ready = false;
		}
		return;
	}

	sequence_over(s, force_seq_over);
	//try at least one cycle before generating the frame
	video_inputs_ready = false;

	//we are done
	if (s.removed) {
		print(GF_LOG_INFO, 'schedule for remove source ' + s.logname);
		purge_sources.push(s);
	}
}


function get_source(id, src, par_seq)
{
	for (let i=0; i<sources.length; i++) {
		let elem = sources[i];
		//if parent sequence is given and differs from source parent, not our source
		if (par_seq && (elem.sequence != par_seq)) continue;
		//same id, this is our source
		if (id && (elem.id == id)) return elem;

		//if source ID is given and differs from source ID, not our source. This allows having 2 sources with the same URLs in the same sequence
		if (id && (elem.id != id)) continue;

		if (src && (elem.src.length == src.length)) {
			let diff = false;
			elem.src.forEach( (v, index) => {
				if (!(v.in === src[index].in)) diff = true; // FIXME: if sources have the same input name they are discarded!!
				let id1 = v.id || 0;
				let id2 = src[index].id || 0;
				if (id1 && id2 && !(id1 === id2)) diff = true;
			} );
			if (!diff) return elem;
		}
	}
	return null;
}

function get_source_by_pid(pid)
{
	let type = pid.get_prop("StreamType");
	let res = null;
	sources.forEach( elem => {
		elem.fsrc.forEach( (f, index) => {
			if (typeof f.__dummy_tmp != 'undefined') {
				if (pid.match_source(f._tmp_arg)) {
					pid.skipped = false;
					res = elem;
				}
				return;
			}
			if (!pid.is_filter_in_parents(f)) return;

			if ((f.media_type=="all")
				|| ((type=='Visual') && (f.media_type.indexOf('v')>=0))
				|| ((type=='Audio') && (f.media_type.indexOf('a')>=0))
				|| ((type=='Text') && (f.media_type.indexOf('t')>=0))
			) {
				pid.skipped = false;
				res = elem;
			} else {
				pid.skipped = true;
			}

		} );
	} );
	return res;
}


function play_pid(pid, source) {
	let evt = new FilterEvent(GF_FEVT_PLAY);
	evt.start_range = source.media_start + source.sequence.start_offset;
	if (source.video_time_at_init) {
		evt.start_range += (video_time - (source.video_time_at_init-1)) / video_timescale;
	}

	pid.done = false;
	print(GF_LOG_DEBUG, 'Playing PID ' + source.logname + '.' + pid.name + ' @start=' + evt.start_range);
	pid.send_event(evt);
}

function play_source(s)
{
	if (s.playing) {
		print(GF_LOG_ERROR, 'Source is already playing ' + s.logname);
		return;
	}
	print(GF_LOG_DEBUG, 'Playing source ' + s.logname);
	if (!s.sequence.active_state)
		trigger_watcher(s.sequence, 'active', true);
	s.sequence.active_state = 1;
	s.playing = true;
	trigger_watcher(s, "active", true);
	s.timeline_init = false;
	s.pids.forEach(pid => { play_pid(pid, s); });
}

function stop_source(s, is_remove)
{
	if (s.playing) {
		s.playing = false;
		trigger_watcher(s, "active", false);
		s.sequence.prefetch_check = false;
		s.timeline_init=0;

		print(GF_LOG_DEBUG, 'Stoping source ' + s.logname);
	  s.src.forEach(src => {
			if (src.process_id) {
				let res = os.waitpid(src.process_id, os.WNOHANG);
				if (res[0] != src.process_id) {
					print(GF_LOG_DEBUG, 'killing process for source ' + src.in);
					os.kill(src.process_id, os.SIGABRT);
				}
				if (src.local_pipe) {
					try {
						sys.del(src.local_pipe);
					} catch (e) {

					}
					src.local_pipe = null;
				}
			}
			src.process_id = null;
	  });

		s.pids.forEach(pid => {
			if (pid.pck) {
					pid.pck.unref();
					pid.pck = null;
			}
			pid.send_event( new FilterEvent(GF_FEVT_STOP) );
		});
	}

	if (!is_remove) return;

	print(GF_LOG_DEBUG, 'Removing source ' + s.logname);
	s.fsrc.forEach( f => {
		f.remove();
	});
	s.fsrc.length = 0;
	s.pids.length = 0;
	s.opened = false;
}

function disable_source(s)
{
	s.disabled = true;
	if (!s.sequence.active_state)
		trigger_watcher(s.sequence, 'active', false);

	s.sequence.active_state = 0;
	s.fsrc.forEach( f => {
		f.remove();
	});
	s.fsrc = [];
	s.pids = [];
	print(GF_LOG_DEBUG, 'Disabling source ' + s.logname);
}

let current_pipe = 1;
let current_port = 2000;

function apply_links(links, target, fchain)
{
	let broken_links=false;
	links.forEach(link => {

	let link_arg = "";
	let link_idx = 0;
	let link_start = false;
	if (link.startsWith("@@")) {
		link = link.slice(2);
	} else {
		link = link.slice(1);
	}

	let sep = link.indexOf('#');
	if (sep<0) {
		if (link.length)
			link_idx = parseInt(link);
	} else {
		let vals = arg.slice(sep);
		link_idx = parseInt(vals[0]);
		link_arg = vals[1];
	}

	if (!link_start) link_idx = fchain.length - link_idx - 1;
	if ((link_idx<0) || (link_idx>=fchain.length)) {
		print(GF_LOG_ERROR, 'Wrong filter index ' + link_idx + ' in link directive ' + link);
		broken_links = true;
		return;
	}
	let f_src = fchain[link_idx];
	if (target) {
		target.set_source(f_src, link_arg);
	} else {
		filter.set_source_restricted(f_src, link_arg);
	}
	f_src.require_source_id = true;
	});
	return broken_links;
}

function open_source(s)
{
	if (s.opened) return;
	s.opened = true;
	s.sequence.prefetch_check = false;
//	filter.reset_source();
	wait_for_pids = current_utc_clock;

	print(GF_LOG_DEBUG, 'Open source ' + s.logname);

  s.has_ka_process = false;
	s.src.forEach(src => {

	let f, args, i;
	let port="";
	let append_filter = null;
	let local_filter = null;

	if (src.in.indexOf('ipid://')<0) {
		if (typeof src.port == 'string') port = src.port;
	}

	src.local_pipe = null;
	src.process_id = null;
	if (port.length) {
		let use_raw = (typeof src.raw == 'boolean') ? src.raw : true;
		let opts="";
		if (typeof src.opts == 'string') opts = src.opts;
		let do_cat_url = true;
		let src_url = "";
		let rfopts = "reframer";
		if (use_raw) {
			rfopts += ":raw=av";
		}
		//use real-time regulation
		if (filter.live) {
			rfopts += ":rt=on";
		}

		if (port==='pipe') {
			src.local_pipe = "gpipe_avmix_" + current_pipe;
			local_filter = "pipe://" + src.local_pipe + ":mkp:sigflush:block_size=10M";
			if (s.keep_alive) {
				local_filter += ':ka';
			  s.has_ka_process = true;
			}
			append_filter = rfopts + " @ dst=pipe://" + src.local_pipe + ':block_size=10M';
			current_pipe++;
			src_url += "gpac src=";
		}
		else if ((port==='tcp') || (port==='tcpu'))  {
			local_filter = port+"://127.0.0.1:" + current_port + "/:listen";
			if (s.keep_alive) {
				local_filter += ':ka';
			  s.has_ka_process = true;
			}
			append_filter = rfopts + " @ dst=" + port + "://127.0.0.1:" + current_port + '/';
			current_port++;
			src_url += "gpac src=";
		}
		else {
			local_filter = port;
			do_cat_url = false;
		}
		//URL relative to playlist
		if (do_cat_url) {

			let idx = -1;
			let search=" :#?";
			for (let ci=0; ci<src.in.length; ci++) {
				let c = src.in.charAt(ci);
				if (search.indexOf(c) >= 0) {
					idx=ci;
					break;
				}
			}
			let cat_url;
			if (idx>=0) {
				cat_url = src.in.slice(0, idx);
			} else {
				cat_url = src.in;
			}
			cat_url = sys.url_cat(filter.pl, cat_url);
			if (idx>=0) {
				cat_url += src.in.slice(idx);
			}
			src_url += cat_url;
		} else {
			src_url += src.in;
		}

		if (append_filter) {
			src_url += ' ' + append_filter+':ext=gsf';
			if ((s.media_start>0) || s.video_time_at_init) {
				let start = s.media_start;
				if (s.video_time_at_init)
						start += (video_time - (s.video_time_at_init-1)) / video_timescale;
				src_url += ':start=' + start;
			}

			if (opts.length)
				src_url += ' ' + opts;
		}

		filter.lock_all(true);
		try {
			f = filter.add_source(local_filter);
		} catch (e) {
			print(GF_LOG_ERROR, 'Add source ' + local_filter + ' failed: ' + e);
			disable_source(s);
			filter.lock_all(false);
			return;
		}
		filter.set_source(f);
		filter.lock_all(false);

		if (typeof src.media != 'string') f.media_type = "all";
		else f.media_type = src.media;

		s.fsrc.push(f);

		args = src_url.split(' ');
		try {
			src.process_id = os.exec(args, {block: false} );
		} catch (e) {
			print(GF_LOG_ERROR, 'Failed to launch process for ' + args + ': ' + e);
		}
		print(GF_LOG_INFO, 'Launch process for ' + args + ' OK');
		return;
	}

	//local load
	filter.lock_all(true);
	//parse command line
	args = src.in.split(' ');
	args = args.filter(function(item){return item;});
	let links = [];
	let fchain = [];
	let prev_f = null;
	for (i=0; i<args.length; i++) {
		let arg = args[i];
		if (arg.charAt(0) === "-")
			continue;

		if (arg.charAt(0) === "@") {
			links.push(arg);
			continue;
		}

		try {
			if (prev_f) {
				f = filter.add_filter(arg);
			}
			//ipid load
			else if (arg.startsWith('ipid://')) {
				f = {};
				f.__dummy_tmp = true;
				f._tmp_arg = arg;
				f.remove = function(){};
				links.length = 0;
					if (i+1<args.length) {
					print(GF_LOG_WARNING, 'Cannot use filter chain with ipid:// source scheme, chain must be specified at prompt or when creating graph - ignoring');
					args.length = 0;
				}
			}
			//relative to playlist
			else {
				f = filter.add_source(arg, filter.pl);
				links.length = 0;
			}
		} catch (e) {
			print(GF_LOG_ERROR, 'Add ' + (prev_f ? 'filter' : 'source') + ' ' + arg + ' failed: ' + e);
			disable_source(s);
			filter.lock_all(false);
			return;
		}

		//setup links
		if (apply_links(links, f, fchain)) {
			disable_source(s);
			filter.lock_all(false);
			return;
		}

		//add to chain
		fchain.push(f);
		//remember source filters
		if (!prev_f) {
			if (typeof src.media != 'string') f.media_type = "all";
			else f.media_type = src.media;
			s.fsrc.push(f);
		}

		prev_f = f;
		links.length = 0;
	}
	if (links.length) {
		//setup links
		if (apply_links(links, null, fchain)) {
			disable_source(s);
			filter.lock_all(false);
			return;
		}
		prev_f = null;
	}
	if (prev_f && (typeof f.__dummy_tmp == 'undefined')) {
		filter.set_source_restricted(prev_f);
	}

	filter.lock_all(false);
	});
}

function get_sequence(seq_id)
{
	for (let i=0; i<sequences.length; i++) {
		if (sequences[i].id === seq_id) return sequences[i];
	}
	return null;
}

function get_sequence_by_json(seq_pl)
{
	let id = (seq_pl==null) ? "_seq_default" : seq_pl.id;
	for (let i=0; i<sequences.length; i++) {
		if (sequences[i].id === id) return sequences[i];
	}
	let seq = {};
	seq.id = id;
	seq.watchers = null;
	seq.pl_update = true;
	seq.active_state = 0;
	seq.start_time = 0;
	seq.stop_time = 0;
	seq.duration = 0;
	seq.start_offset = 0;
	seq.prefetch_check = false;
	seq.loop = false;

	seq.sources = [];
	seq.transition_state = 0;
	seq.transition_effect = null;
	seq.transition = null;
	seq.crc = 0;
	sequences.push(seq);
	print(GF_LOG_DEBUG, 'creating new sequence ID ' + id);
	return seq;
}

function parse_source_opts(src, pl_el)
{
	src.media_start = pl_el.start || 0;
	src.media_stop = pl_el.stop || 0;
	src.audio_fade = pl_el.fade || "inout";
	if (typeof pl_el.mix == 'boolean')
		src.mix = pl_el.mix;
	else
		src.mix = true;

	src.hold = pl_el.hold || false;
	src.seek = pl_el.seek || false;
	if (typeof pl_el.prefetch_ms == 'number') src.prefetch_ms = pl_el.prefetch;
	else src.prefetch_ms = 500;
	src.prefetch_sec = src.prefetch_ms / 1000;
}

function push_source(el, id, seq)
{
	let s = {};
	s.id = id || "";
	s.src = el.src;
	s.watchers = null;
	s.fsrc = [];
	s.pids = [];
	s.timeline_init=false;
	s.playing = false;
	s.duration = 0;
	s.disabled = false;
	s.opened = false;
	s.in_prefetch = 0;
	s.next_in_transition = false;
	s.transition_offset = 0;
	s.removed = 0;
	s.mix_volume = 1.0;
	s.keep_alive = el.keep_alive || false;
	s.video_time_at_init = 0;
	s.no_signal = false;
	s.no_signal_state = false;

	sources.push(s);
	if (s.id==="") {
		s.logname = el.src[0].in.split('\\').pop().split('/').pop();
	} else {
		s.logname = s.id;
	}
	let parent_seq = get_sequence_by_json(seq);

	s.pl_update = true;
	parent_seq.sources.push(s);
	s.sequence = parent_seq;

	parse_source_opts(s, el);

	let do_start = (parent_seq.sources.length==1) ? true : false;
	if (parent_seq.start_time <0) {
			do_start = false;
	} else if (parent_seq.start_time > 0) {
			do_start = false;
//	} else if (parent_seq.start_time>0) {
//			parent_seq.start_offset = (current_utc_clock - parent_seq.start_time) / 1000.0;
	}

	if (do_start) {
		print(GF_LOG_DEBUG, 'open and play source');
		open_source(s);
		play_source(s);
	} else {
		print(GF_LOG_DEBUG, 'queue source in seq');
	}
}

function parse_url(src_pl, seq_pl)
{
	if (!validate_source(src_pl))
		return;

	let id = src_pl.id || 0;

	let par_seq = seq_pl ? get_sequence_by_json(seq_pl) : null;
	let src = get_source(id, src_pl.src, par_seq);

	if (!src) {
		push_source(src_pl, id, seq_pl);
	} else {
		if (src.sequence != par_seq) {
			print(GF_LOG_ERROR, 'source update cannot change parent sequence, ignoring ' + JSON.stringify(seq_pl));
			return;
		}
		src.pl_update = true;

		src.src = src_pl.src;
		parse_source_opts(src, src_pl);
	}
}

function validate_source(pl)
{
	let valid=true;
	if (typeof pl.src == 'undefined') {
		print(GF_LOG_ERROR, 'Source element without `src`, ignoring URL element');
		return false;
	}
	pl.src.forEach(s => {
		if (typeof s.in == 'undefined') {
			print(GF_LOG_ERROR, 'Source element without input URL, ignoring URL element');
			valid=false;
			return;
		}
	});
	if (!valid)
		return false;

	for (var propertyName in pl) {
		if (propertyName == 'id') continue;
		if (propertyName == 'src') continue;
		if (propertyName == 'start') continue;
		if (propertyName == 'stop') continue;
		if (propertyName == 'volume') continue;
		if (propertyName == 'mix') continue;
		if (propertyName == 'fade') continue;
		if (propertyName == 'keep_alive') continue;
		if (propertyName == 'seek') continue;
		if (propertyName == 'hold') continue;

		if (propertyName.charAt(0) == '_') continue;

		print(GF_LOG_WARNING, 'Unrecognized option ' + propertyName + ' in source ' + JSON.stringify(pl) );
	}

	pl.src.forEach(s => {
		for (var propertyName in s) {
			if (propertyName == 'id') continue;
			if (propertyName == 'port') continue;
			if (propertyName == 'in') continue;
			if (propertyName == 'opts') continue;
			if (propertyName == 'media') continue;
			if (propertyName == 'raw') continue;
			if (propertyName.charAt(0) == '_') continue;

			print(GF_LOG_WARNING, 'Unrecognized option ' + propertyName + ' in sourceURL ' + JSON.stringify(pl) );
		}
	});

	return true;
}

function validate_seq(pl)
{
	let valid=true;
	pl.seq.forEach(s => {
		if (!validate_source(s)) {
			valid=false;
			return;
		}
	});
	if (!valid) return false;

	for (var propertyName in pl) {
		if (propertyName == 'id') continue;
		if (propertyName == 'loop') continue;
		if (propertyName == 'start') continue;
		if (propertyName == 'stop') continue;
		if (propertyName == 'transition') continue;
		if (propertyName == 'seq') continue;
		if (propertyName == 'skip') continue;
		if (propertyName.charAt(0) == '_') continue;

		print(GF_LOG_WARNING, 'Unrecognized option ' + propertyName + ' in sequence ' + JSON.stringify(pl) );
	}

	return true;
}

function parse_seq(pl)
{
	if (!validate_seq(pl))
		return;

	let seq = get_sequence_by_json(pl);
	//got new seg
	seq.pl_update = true;

	let first = seq.crc ? false : true;
	let crc = sys.crc32(JSON.stringify(pl));
	if (crc != seq.crc) {
		seq.crc = crc;
	} else {
		seq.sources.forEach(s => {
			s.pl_update = true;
		});
		return;
	}

  //parse UTC start time. If reload and new start and active, we cannot modify the start time
	if (!first && (seq.start_time_json != pl.start) && (seq.active_state == 1)) {
		print(GF_LOG_ERROR, 'Cannot change sequence start time while active, ignoring start ' + pl.start);
	}
	//we're not started or over, parse the new start
	else if (seq.active_state != 1) {
		let old_start = seq.start_time;
		let old_start_json = seq.start_time_json;
		seq.start_time = parse_date_time(pl.start, true);
		if (seq.start_time<0) {
			seq.start_time = -1;
		}
		//start is a date, don't skip range adjustment
		else if ((typeof pl.start == 'string') && (pl.start.indexOf(':')>0)) {
			seq.skip_start_offset = false;
		}
		//start is not a date, source will start from its start range
		else {
			seq.skip_start_offset = true;
		}
		//seq is over, check if we need to restart
		if (seq.active_state>1) {
			if ((pl.start != "now") && (pl.start != old_start_json) && (seq.start_time > old_start))
				seq.active_state = 0;
		}
		seq.start_time_json = pl.start;
	}

	seq.stop_time = parse_date_time(pl.stop, true);
	if ((seq.stop_time<0) || (seq.stop_time<=seq.start_time))
		seq.stop_time = -1;

	seq.transition_effect = pl.transition || null;

	seq.loop = pl.loop || 0;
	if (seq.loop && (typeof pl.loop == 'boolean'))
		seq.loop = -1;
	pl.seq.forEach( p => { parse_url(p, pl); });


	if (seq.id && scene_in_update) {
		//we are called from a module, force seting up scenes using this sequence
		do_traverse(root_scene, scene => {
			let is_match = false;
			//either removed or not yet loaded
			if (!scene.mod) return;

			scene.sources.forEach( seq_id => {
				if (seq_id == seq.id) is_match = true;
			});
			if (is_match) {
				setup_scene(scene, scene.sources, null);
			}
		});
	}
}

function validate_scene(pl)
{
	if (typeof pl.js == 'undefined') {
		pl.js = 'shape';
	}
	else if (typeof pl.js != 'string') {
		print(GF_LOG_WARNING, 'Invalid scene.js, expecting string - ignoring element ' + JSON.stringify(pl) );
		return false;
	}
	if (Array.isArray(pl.sources)) {
		for (let i=0; i<pl.sources.length; i++) {
			let s_id = pl.sources[i];
			if (typeof s_id != 'string') {
				print(GF_LOG_WARNING, 'Invalid scene.sources element ' + s_id + ', expecting string - ignoring element ' + JSON.stringify(pl) );
				return false;
			}
			let src = get_sequence(s_id);
			if (!src) {
				src = get_group(s_id);
				if (src && (src.offscreen==GROUP_OST_NONE)) src = null;
			}
			if (!src) {
				print(GF_LOG_WARNING, 'Invalid scene.sources element ' + s_id + ', source sequence not found - ignoring element ' + JSON.stringify(pl) );
				return false;
			}
		}
	} else if (typeof pl.sources !== 'undefined') {
			print(GF_LOG_WARNING, 'Invalid scene.sources element invalid, expecting undefined or string array - ignoring element ' + JSON.stringify(pl) );
	}

	let scene_id = pl.id || null;
	if (scene_id) {
		let nb_scenes=0;
		do_traverse(root_scene, s => {
			if (s.id === scene_id) nb_scenes++;
		});
		if (nb_scenes>1) {
			print(GF_LOG_WARNING, 'More than one scene defined with the same id ' + scene_id + ', ignoring element ' + JSON.stringify(pl) );
			return false;
		}
	}
	return true;
}


function get_element_by_id(elem_id, list)
{
	if (!elem_id) return null;

	for (let i=0; i<list.length; i++) {
		let e = list[i];
		if (elem_id==e.id)
				return e;
	}
	return null;
}


let ID_scenes = [];

function get_scene(scene_id)
{
	return get_element_by_id(scene_id, ID_scenes);
}

function check_transition(fx_id)
{
	let fx = null;

	do_traverse(root_scene, scene => {
		if (fx || !scene.transition_effect) return;

		let a_fx_id = scene.transition_effect.id || null;
		if (a_fx_id && (fx_id==a_fx_id)) {
			fx = scene.transition_effect;
		}
	});
	if (fx != null) return true;
	return false;
}

function get_a_trans(obj, fx_id, get_json)
{
	if (!obj.transition_effect) return null;

	let a_fx_id = obj.transition_effect.id || null;
	if (a_fx_id && (fx_id==a_fx_id)) {
		return get_json ? obj.transition_effect : obj.transition;
	}
	return null;
}

function get_transition(fx_id, get_json)
{
	let tr = null;
	do_traverse(root_scene, scene => {
		if (tr) return;
		tr = get_a_trans(scene, fx_id, get_json);
	});
	if (tr) return tr;

	for (let i=0; i<sequences.length; i++) {
		tr = get_a_trans(sequences[i], fx_id, get_json);
		if (tr) return tr;
	}
	return null;
}


function parse_scene(pl, parent)
{
	if (!validate_scene(pl))
		return null;

	if (pl.skip || false)
		return null;

	let scene_id = pl.id || null;
	let scene = get_scene(scene_id);
	if (!scene) {
		if (typeof pl.sources == 'undefined') pl.sources = [];

		create_scene(pl.sources, pl, parent);
	} else if (scene.pl_update) {
		print(GF_LOG_WARNING, "Multiple scenes with id `" + scene_id + "` defined, ignoring subsequent declarations");
		return null;
	} else {
		scene.pl_update = true;
		if (parent) parent.scenes.push(scene);
		else root_scene.scenes.push(scene);
		set_scene_options(scene, pl);

		return scene;
	}
}

function parse_config(pl)
{
	for (var propertyName in pl) {
		if (propertyName == 'gpu') {
			if (pl.gpu=='off') filter.gpu = 0;
			else if (pl.gpu=='mix') filter.gpu = 1;
			else if (pl.gpu=='all') filter.gpu = 2;
			else print(GF_LOG_WARNING, "Wrong syntax for option \`gpu\` in playlist config, ignoring");
		}
		else if (propertyName == 'vsize') {
			var s = pl.vsize.split('x');
			if (s.length==2) {
				filter.vsize.x = parseInt(s[0]);
				filter.vsize.y = parseInt(s[1]);
			}
			else print(GF_LOG_WARNING, "Wrong syntax for option \`vsize\` in playlist config, ignoring");
		}
		else if (propertyName == 'fps') {
			if (typeof pl.fps == 'string') {
				var fps = pl.fps.split('/');
				filter.fps.n = parseInt(fps[0]);
				filter.fps.d = (fps.length>2) ? parseInt(fps[1]) : 1;
			} else if (typeof pl.fps == 'number') {
				filter.fps.n = pl.fps;
				filter.fps.d = 1;
			}
			else print(GF_LOG_WARNING, "Wrong syntax for option \`fps\` in playlist config, ignoring");
		}
		else if (propertyName == 'dynpfmt') {
			if (pl.dynpfmt=='off') filter.dynpfmt=0;
			else if (pl.dynpfmt=='init') filter.dynpfmt=1;
			else if (pl.dynpfmt=='all') filter.dynpfmt=2;
			else print(GF_LOG_WARNING, "Wrong syntax for option \`dynpfmt\` in playlist, ignoring");
		}
		else if (propertyName == 'pfmt') filter.pfmt = pl.pfmt;
		else if (propertyName == 'afmt') filter.afmt = pl.afmt;
		else if ( (propertyName == 'thread') || (propertyName == 'lwait') || (propertyName == 'ltimeout')
			 || (propertyName == 'sr') || (propertyName == 'ch') || (propertyName == 'alen') || (propertyName == 'maxdur')
		) {
			if (typeof pl[propertyName]  != 'number') print(GF_LOG_WARNING, "Expecting number for option \`" + propertyName + "\` in playlist config, ignoring");
			else filter[propertyName] = pl[propertyName];
		}
		else if (propertyName == 'live') {
			if ((typeof pl[propertyName]  != 'number') && (typeof pl[propertyName]  != 'boolean')) print(GF_LOG_WARNING, "Expecting boolean for option \`" + propertyName + "\` in playlist config, ignoring");
			else filter[propertyName] = pl[propertyName];
		}
		else if ((propertyName == 'pfmt') || (propertyName == 'afmt')) {
			if (typeof pl[propertyName]  != 'string') print(GF_LOG_WARNING, "Expecting string for option \`" + propertyName + "\` in playlist config, ignoring");
			else filter[propertyName] = pl[propertyName];
		}
		else if (propertyName == 'reload_tests') {
			if (Array.isArray(pl.reload_tests) && (typeof pl.reload_tests[0] == 'string') ) {
				reload_tests = pl.reload_tests;
			}
			else if (typeof pl.reload_tests == 'string') {
				reload_tests = [];
				reload_tests.push(pl.reload_tests);
			} else {
				print(GF_LOG_WARNING, 'Expecting string or string array for option ' + propertyName + ' in playlist config, ignoring');
			}
		}
		else if ((propertyName == 'reload_timeout') || (propertyName == 'reload_loop')) {
			if (typeof pl[propertyName]  != 'number') print(GF_LOG_WARNING, "Expecting number for option \`" + propertyName + "\` in playlist config, ignoring");
			else if (propertyName == 'reload_timeout') reload_timeout = pl.reload_timeout;
			else if (propertyName == 'reload_loop') reload_loop = pl.reload_loop;
		}
		else if (propertyName == 'type' ) {}
		else if (propertyName.charAt(0) != '_') {
			print(GF_LOG_WARNING, 'Unrecognized option ' + propertyName + ' in playlist config ' + JSON.stringify(pl) );
		}
	}
}

function is_scene_pl(pl)
{
		if (Array.isArray(pl.sources)
		|| (typeof pl.js == 'string')
		|| (typeof pl.x == 'number')
		|| (typeof pl.y == 'number')
		|| (typeof pl.width == 'number')
		|| (typeof pl.height == 'number')
	)
		return true;

	return false;
}

function validate_group(pl)
{
	let valid=true;

	if (Array.isArray(pl.scenes)) {
		pl.scenes.forEach(s => {
			if (Array.isArray(s.scenes)) {
				if (!validate_group(s))
					valid = false;
			}
			else if (is_scene_pl(s)) {
				if (!validate_scene(s))
					valid = false;
			}
		});
	}

	if (!valid) return false;

	for (var propertyName in pl) {
		if (propertyName == 'id') continue;
		if (propertyName == 'skip') continue;
		if (group_get_update_type(propertyName, false) >= -1) continue;
		if (propertyName.charAt(0) == '_') continue;

		print(GF_LOG_WARNING, 'Unrecognized option ' + propertyName + ' in group ' + JSON.stringify(pl) );
	}

	return true;
}

let ID_groups = [];

function get_group(group_id)
{
	return get_element_by_id(group_id, ID_groups);
}


const group_transform_props = [
	{ name: "active", def_val: true, update_flag: 0},
	{ name: "zorder", def_val: 0, update_flag: 0},
	{ name: "x", def_val: 0, update_flag: UPDATE_POS|UPDATE_ALLOW_STRING},
	{ name: "y", def_val: 0, update_flag: UPDATE_POS|UPDATE_ALLOW_STRING},
	{ name: "rotation", def_val: 0, update_flag: UPDATE_POS},
	{ name: "cx", def_val: 0, update_flag: UPDATE_POS},
	{ name: "cy", def_val: 0, update_flag: UPDATE_POS},
	{ name: "hskew", def_val: 0, update_flag: UPDATE_POS},
	{ name: "vskew", def_val: 0, update_flag: UPDATE_POS},
	{ name: "vscale", def_val: 1, update_flag: UPDATE_POS},
	{ name: "hscale", def_val: 1, update_flag: UPDATE_POS},
	{ name: "untransform", def_val: false, update_flag: UPDATE_POS},
	{ name: "units", def_val: null, update_flag: -1},
	{ name: "mxjs", def_val: null, update_flag: -1},
	{ name: "z", def_val: 0, update_flag: UPDATE_CHILD},
	{ name: "cz", def_val: 0, update_flag: UPDATE_CHILD},
	{ name: "zscale", def_val: 1, update_flag: UPDATE_CHILD},
	{ name: "orientation", def_val: null, update_flag: UPDATE_CHILD},
	{ name: "axis", def_val: null, update_flag: UPDATE_CHILD},
	{ name: "position", def_val: null, update_flag: UPDATE_CHILD},
	{ name: "target", def_val: null, update_flag: UPDATE_CHILD},
	{ name: "up", def_val: null, update_flag: UPDATE_CHILD},
	{ name: "viewport", def_val: null, update_flag: UPDATE_CHILD|UPDATE_ALLOW_STRING},
	{ name: "fov", def_val: 45, update_flag: UPDATE_CHILD},
	{ name: "ar", def_val: 0, update_flag: UPDATE_CHILD},
	{ name: "znear", def_val: 0, update_flag: UPDATE_CHILD},
	{ name: "zfar", def_val: 0, update_flag: UPDATE_CHILD},
];


const group_props = [
  //first three are not updatable and no default value
	{ name: "scenes", def_val: null, update_flag: -1},
	{ name: "offscreen", def_val: null, update_flag: -1},
	{ name: "use", def_val: null, update_flag: -1},

	{ name: "opacity", def_val: 1, update_flag: UPDATE_CHILD},
	{ name: "scaler", def_val: 1, update_flag: UPDATE_CHILD},
	{ name: "back_color", def_val: 'none', update_flag: UPDATE_CHILD},
	{ name: "width", def_val: -1, update_flag: UPDATE_CHILD|UPDATE_ALLOW_STRING},
	{ name: "height", def_val: -1, update_flag: UPDATE_CHILD|UPDATE_ALLOW_STRING},
	{ name: "use_depth", def_val: -1, update_flag: UPDATE_CHILD},
	{ name: "reverse", def_val: false, update_flag: UPDATE_CHILD},
	{ name: "reference", def_val: false, update_flag: UPDATE_CHILD},
];


function parse_group_transform(scene, params)
{
	scene.position = null;
	scene.target = null;
	scene.up = null;
	scene.viewport = null;

	group_transform_props.forEach(o => {
		parse_val(params, o.name, scene, o.def_val, o.update_flag);
	});

	scene.units = UNIT_RELATIVE;
	if (typeof params.units == 'string') {
		if (params.units == 'pix') scene.units = UNIT_PIXEL;
		else if (params.units == 'rel') scene.units = UNIT_RELATIVE;
		else {
			print(GF_LOG_WARNING, 'Invalid unit type ' + params.units + ', using relative');
		}
	}
	scene.mx_untransform = false;
	scene.mxjs = (typeof params.mxjs == 'string') ? fn_from_script(['tr'], params.mxjs) : null;
}

function group_get_update_type(prop_name, transform_only)
{
	let i;

	for (i=0; i<group_transform_props.length; i++) {
		if (group_transform_props[i].name == prop_name) return group_transform_props[i].update_flag;
	}
	if (transform_only) return -2;

	for (i=0; i<group_props.length; i++) {
		if (group_props[i].name == prop_name) return group_props[i].update_flag;
	}
	return -2;
}

function parse_group(pl, parent)
{
	if (!validate_group(pl))
		return null;

	if (pl.skip || false)
		return null;

	let group_id = pl.id || null;
	let group = get_group(group_id);
	if (!group) {
		group = {};
		group.watchers = null;
		group.id = group_id;
		group.scenes = [];
		group.parent = parent;
		group.current_depth = 0;

		if (group_id) ID_groups.push(group);
		group.mx = new evg.Matrix2D();
		group.offscreen_canvas = null;
		group.update_flag = UPDATE_CHILD;

		group.get = function(prop) {
			if (typeof this[prop] != 'undefined') return this[prop];
			return undefined;
		}
		group.set = function(prop, val) {
			let update_type = group_get_update_type(prop, false);
			//no modifications allowed or not defined
			if (update_type<0) {
				return;
			}

			let allow_string = (update_type & UPDATE_ALLOW_STRING) ? true : false;
			update_type &= ~UPDATE_ALLOW_STRING;

			if ((this[prop] != val) && check_prop_type(typeof this[prop], typeof val, allow_string)) {
				this.update_flag |= update_type;
				this[prop] = val;
				if (update_type)
					invalidate_parent(this);
				trigger_watcher(group, prop, val);
			}
		}
	} else if (group.pl_update) {
		print(GF_LOG_WARNING, "Multiple groups with id `" + group_id + "` defined, ignoring subsequent declarations");
		return null;
	}
	group.pl_update = true;

	group.update_flag |= UPDATE_POS;

	group_props.forEach(o => {
		if (o.def_val == null)  return;
			parse_val(pl, o.name, group, o.def_val, o.update_flag);
	});
	parse_group_transform(group, pl);

	//ad-hoc parsing for offscreen
	group.offscreen = GROUP_OST_NONE;
	if (typeof pl.offscreen == 'string') {
		if (pl.offscreen == 'mask') group.offscreen = GROUP_OST_MASK;
		else if (pl.offscreen == 'color') group.offscreen = GROUP_OST_COLOR;
		else if (pl.offscreen == 'dual') group.offscreen = GROUP_OST_DUAL;
		else if (pl.offscreen == 'none') group.offscreen = GROUP_OST_NONE;
		else {
			print(GF_LOG_WARNING, "Unknown group offscreen mode `" + pl.offscreen + "`, ignoring");
		}
	}
	//ad-hoc parsing for use
	group.use = (typeof pl.use == 'string') ? pl.use : null;

	//tag all scenes with ID as not present
	group.scenes.forEach(scene => {
		scene.pl_update = false;
	});

	group.scenes.length = 0;

	//parse
	if (! group.use) {
		pl.scenes.forEach(s => {
			if (Array.isArray(s.scenes) || (typeof s.use == 'string')) {
				parse_group(s, group);
			}
			else if (is_scene_pl(s)) {
				parse_scene(s, group);
			}
		});
	}

	if (parent) {
		parent.scenes.push(group);
	} else {
		root_scene.scenes.push(group);
	}
	return group;
}

let user_scripts=[];

function get_script(id)
{
	return get_element_by_id(id, user_scripts);
}

function fn_from_script(args, jscode)
{
	let url = sys.url_cat(playlist_url, jscode);
	if (sys.file_exists(url)) {
		return new Function(args, sys.load_file(url, true) );
	} else {
		return new Function(args, jscode);
	}
}

function parse_script(pl)
{
	if (typeof pl.script != 'string') {
		print(GF_LOG_WARNING, 'Unrecognized script ' + JSON.stringify(pl) );
		return;
	}
	if (pl.skip || false)
		return;

	let id = pl.id || null;
	let script_obj = id ? get_script(id) : null;
	if (!script_obj) {
		script_obj = {};
		script_obj.watchers = null;
		script_obj.id = id;
		user_scripts.push(script_obj);
	}	else if (script_obj.pl_update) {
		print(GF_LOG_WARNING, "Multiple scripts with id `" + id + "` defined, ignoring subsequent declarations");
		return null;
	}

	script_obj.pl_update = true;
	script_obj.next_time = 0;

	if (typeof pl.active == 'boolean')
		script_obj.active = pl.active;
	else
		script_obj.active = true;

	try {
		script_obj.run_script = fn_from_script([], pl.script);
	} catch (err) {
		print(GF_LOG_ERROR, 'Invalid script ' + pl.script + ' : ' + err);
		script_obj.run_script = null;
	}

	for (var propertyName in pl) {
		if (propertyName == 'skip') continue;
		if (propertyName == 'id') continue;
		if (propertyName == 'active') continue;
		if (propertyName == 'script') continue;
		if (propertyName.charAt(0) == '_') continue;

		print(GF_LOG_WARNING, 'Unrecognized option ' + propertyName + ' in script ' + JSON.stringify(pl) );
	}

	return;
}

function update_scripts()
{
	for (let i=0; i<user_scripts.length; i++) {
		let script = user_scripts[i];
		if (!script.active) continue;

		if (!script.run_script) {
			user_scripts.splice(i, 1);
			i--;
			continue;
		}

		if (script.next_time) {
			if (video_playing && (script.next_time>video_time)) continue;
			if (audio_playing && (script.next_time>audio_time)) continue;
		}

		try {
			let res = script.run_script.apply(script, []) || 0;
			if (typeof res == 'number') {
				if (res<0) res=0;

				if (video_playing) res = video_time + res*video_timescale;
				else res = audio_time + res*audio_timescale;
			}
			script.next_time = res;
		} catch (err) {
			user_scripts.splice(i, 1);
			i--;
			print(GF_LOG_ERROR, 'Error executing script  ' + JSON.stringify(script.run_script) + ' : ' + err + ' - removing');
		}
	}
}

function validate_watcher(pl)
{
	let valid=true;
	if ( typeof pl.watch != 'string') {
		print(GF_LOG_ERROR, 'Watcher watch must be a string, ignoring ' + JSON.stringify(pl) );
		return false;
	}
	if (pl.watch != 'events') {
		let evt = pl.watch.split('(');
		if (sys.get_event_type(evt[0]) ) {
		}
		else if (pl.watch.indexOf('@') < 0) {
			print(GF_LOG_ERROR, 'Watcher watch must be in the form `ID@field`, ignoring ' + JSON.stringify(pl) );
			return false;
		}
	}
	if ( typeof pl.target != 'string') {
		print(GF_LOG_ERROR, 'Watcher target must be an array, ignoring ' + JSON.stringify(pl) );
		return false;
	}
	if ((typeof pl.mode != 'string') && (typeof pl.mode != 'undefined')) {
		print(GF_LOG_ERROR, 'Watcher target must be an array, ignoring ' + JSON.stringify(pl) );
		return false;
	}
	if ((pl.mode == 'set') && (pl.target.indexOf('@') < 0)) {
		print(GF_LOG_ERROR, 'Watcher target must be in the form `ID@field`, ignoring ' + JSON.stringify(pl) );
		return false;
	}
	return valid;
}

let watchers=[];
let defer_parse_watchers = null;
let watchers_deferred = [];
let event_watchers=[];

function remove_watcher(watcher, parent_only)
{
	let idx;
	if (watcher.parent) {
		idx = watcher.parent.watchers.indexOf(watcher);
		if (idx>=0) watcher.parent.watchers.splice(idx, 1);

		if (watcher.parent.watchers.length == 0) {
			watcher.parent.watchers = null;
		}
		watcher.parent = null;
	} else if (watcher.evt != 0) {
		idx = event_watchers.indexOf(watcher);
		if (idx>=0) event_watchers.splice(idx, 1);
	}

	if (!parent_only) {
		idx = watchers.indexOf(watcher);
		if (idx>=0) watchers.splice(idx, 1);
	}
}


function parse_watcher(pl)
{
	if (!validate_watcher(pl))
		return;

	if (pl.skip || false)
		return;

	let do_register = false;
	let id = pl.id || null;
	let watcher = id ? get_element_by_id(id, watchers) : null;
	if (!watcher) {
		watcher = {}
		watcher.id = id;
		watcher.evt = 0;
		do_register = true;
		watcher.parent = null;
	}	else if (watcher.pl_update) {
		print(GF_LOG_WARNING, "Multiple watchers with id `" + id + "` defined, ignoring subsequent declarations");
		return null;
	}
	watcher.pl_update = true;

	if (typeof pl.active == 'boolean')
		watcher.active = pl.active;
	else
		watcher.active = true;


	for (var propertyName in pl) {
		if (propertyName == 'skip') continue;
		if (propertyName == 'id') continue;
		if (propertyName == 'active') continue;
		if (propertyName == 'watch') continue;
		if (propertyName == 'mode') continue;
		if (propertyName == 'target') continue;
		if (propertyName == 'with') continue;
		if (propertyName.charAt(0) == '_') continue;

		print(GF_LOG_WARNING, 'Unrecognized option ' + propertyName + ' in script ' + JSON.stringify(pl) );
	}

	let elem = null;
	if (pl.watch.indexOf('@')>=0) {
		let vals = pl.watch.split("@");
		let s_id = vals[0];

		elem = get_scene(s_id);
		if (!elem) elem = get_group(s_id);
		if (!elem) elem = get_script(s_id);
		if (!elem) elem = get_timer(s_id);
		if (!elem) elem = get_sequence(s_id);
		if (!elem) elem = get_source(s_id, null, null);

		if (!elem) {
			print(GF_LOG_WARNING, 'No watchable element with ID ' + s_id + ' discarding watcher');
			remove_watcher(watcher, false);
			return;
		}

		//parent changed
		if (watcher.src_id && s_id && (watcher.src_id != s_id)) {
			remove_watcher(watcher, true);
		}
		watcher.src_id = s_id;
		watcher.src_field = vals[1];
		watcher.evt = 0;
	} else {
		let evt = pl.watch.split('(');
		remove_watcher(watcher, true);
		watcher.src_id = null;

		if (evt != 'events') {
			watcher.evt = sys.get_event_type(evt[0]);
			if (!watcher.evt) {
				print(GF_LOG_WARNING, 'Unknown event type ' + pl.watch + ' discarding watcher');
				remove_watcher(watcher, false);
				return;
			}
			if (evt.length>1) {
				evt = evt[1].split(')');
				watcher.evt_param = evt[0];
			} else {
				watcher.evt_param = 0;
			}
		} else {
			watcher.evt = -1;
			watcher.evt_param = 0;
		}
	}

	let vals = [];
	if (pl.target.indexOf(' ')>=0) {}
	else if (pl.target.indexOf(')')>=0) {}
	else if (pl.target.indexOf('(')>=0) {}
	else vals = pl.target.split('.');

	if (vals.length==2) {
		let target_scene = get_scene(vals[0]);
		if (!target_scene) {
			print(GF_LOG_WARNING, 'Cannot find scene for target ' + pl.target + ', discarding watcher');
			remove_watcher(watcher);
			return;
		}
		watcher.mode = 3;
		watcher.scene = target_scene;
		watcher.target = vals[1];
	} else {
		let is_script = false;
		let vals = pl.target.split('@');
		if (vals.length>1) {
			if (vals.length>2) is_script = true;
			else if (vals[1].indexOf(' ')>=0) is_script = true;
			else if (vals[1].indexOf('{')>=0) is_script = true;
			else if (vals[1].indexOf('}')>=0) is_script = true;
			else if (vals[1].indexOf('[')>=0) is_script = true;
			else if (vals[1].indexOf(']')>=0) is_script = true;
			else if (vals[1].indexOf('()')>=0) is_script = true;
			else if (vals[1].indexOf(')')>=0) is_script = true;
			else if (vals[1].indexOf('.')>=0) is_script = true;
		} else if (vals.length==1) {
			is_script = true;
		}

		if (is_script) {
			watcher.mode = 2;
			if (watcher.evt)
				watcher.fun = fn_from_script(['evt'], pl.target);
			else
				watcher.fun = fn_from_script(['value'], pl.target);

			if (!watcher.fun) {
				print(GF_LOG_WARNING, 'Cannot parse action function ' + pl.target + ', discardinf watcher');
				remove_watcher(watcher);
				return;
			}
		} else {
			if (watcher.evt) {
				print(GF_LOG_WARNING, 'Cannot use target `ID@property` for event watchers, discarding watcher');
				remove_watcher(watcher, false);
				return;
			}
			watcher.mode = 0;
			watcher.target = pl.target;
			if (typeof pl.with != 'undefined') {
				watcher.with = pl.with;
				watcher.mode = 1;
			}
		}
	}

	if (do_register)
		watchers.push(watcher);

	if (elem) {
		if (!elem.watchers)
			elem.watchers = [];
		elem.watchers.push(watcher);
		watcher.parent = elem;
	} else {
		if (watcher.evt)
			event_watchers.push(watcher);
	}
	return;
}

function flush_watchers()
{
	watchers_deferred.forEach(evt => {
		trigger_watcher(evt.elem, evt.prop, evt.val);
	});
	watchers_deferred.length = 0;
}

function trigger_watcher(src, prop_name, value)
{
	if (defer_parse_watchers) {
		let evt = {elem: src, prop: prop_name, val: value};
		watchers_deferred.push(evt);
		return;
	}

	if (!src.watchers) return;

	src.watchers.forEach(watcher => {
		if (!watcher.active) return;

		if (watcher.src_field != prop_name) return;

		if (watcher.in_callback) {
			print(GF_LOG_WARNING, 'recursive call for watcher ' + watcher.id + ' value was ' + src.id + '@' + prop_name+ '=' + value + ', ignoring');
			return;
		}
		watcher.in_callback = true;
		if (watcher.mode==0) {
			let cmd = {replace: watcher.target, with: value};
			parse_update_elem(cmd);
		} else if (watcher.mode==1) {
			let cmd = {replace: watcher.target, with: watcher.with};
			parse_update_elem(cmd);
		} else if (watcher.mode==2) {
			try {
				watcher.fun.apply(watcher, [value]);
			} catch (err) {
				print(GF_LOG_ERROR, 'Error executing action function: ' + err);
			}
		} else if (watcher.mode==3) {
			if (watcher.scene.mod) {
				if (typeof watcher.scene.mod[watcher.target] == 'function') {
					watcher.scene.mod[watcher.target].apply(watcher.scene.mod, [value, src.id, prop_name]);
				}
			}
		}
		watcher.in_callback = false;

	});
}

let styles=[];

function parse_style(pl)
{
	if (pl.skip || false)
		return;

	let id = pl.id || null;
	if (!id) return;
	let style = get_element_by_id(id, styles);
	if (style) {
		print(GF_LOG_WARNING, "Multiple styles with id `" + id + "` defined, ignoring subsequent declarations");
		return;
	}
	pl.forced = pl.forced || false;
	pl.updated = true;
	styles.push(pl);
}


function parse_playlist_element(pl)
{
	let type = pl.type || null;

	if (pl.skip || false)
		return;

	//guess type of root elements
	if (Array.isArray(pl.src))
		type = 'url';

	if (Array.isArray(pl.seq))
		type = 'seq';

	if (is_scene_pl(pl))
		type = 'scene';

	if (Array.isArray(pl.scenes) || (typeof pl.use == 'string'))
		type = 'group';

	if (Array.isArray(pl.keys) || Array.isArray(pl.anims))
		type = 'timer';

	if (typeof pl.script == 'string')
		type = 'script';

	if (typeof pl.watch == 'string')
		type = 'watch';

	//try config last
	if ((typeof pl.reload_timeout != 'undefined') || (typeof pl.reload_loop != 'undefined'))
		type = 'config';

	if (!type) {
		let nb_props = 0;
		for (var propertyName in pl) {
			if (typeof filter[propertyName] != 'undefined') {
				nb_props++;
			}
		}
		if (nb_props)
			type = 'config';
	}


	if (type==='url') {
		parse_url(pl, null);
	} else if (type==='seq') {
		parse_seq(pl);
	} else if (type==='scene') {
		parse_scene(pl, null);
	} else if (type==='group') {
		parse_group(pl, null);
	} else if (type==='timer') {
		parse_timer(pl);
	} else if (type==='script') {
			parse_script(pl);
	} else if (type==='watch') {
		if (defer_parse_watchers)
			defer_parse_watchers.push(pl);
		else
			parse_watcher(pl);
	} else if (type==='config') {
		if (!playlist_loaded) {
			parse_config(pl);
		}
	} else if (type==='style') {
		parse_style(pl);
	} else {
		print(GF_LOG_WARNING, 'Root element type ' + type + ' not defined, ignoring element ' + JSON.stringify(pl) );
	}
}

function cleanup_list(type)
{
	let ID_list = null;
	let log_name = null;
	if (!type) { ID_list = ID_scenes; log_name = 'Scene'; }
	else if (type==1) { ID_list = ID_groups; log_name = 'Group'; }
	else if (type==2) { ID_list = user_scripts; log_name = 'Script'; }
	else if (type==3) { ID_list = watchers; log_name = 'Watcher'; }
	else if (type==4) { ID_list = timers; log_name = 'Timer'; }
	else
			return;

	for (let i=0; i<ID_list.length; i++) {
		let s = ID_list[i];
		if (s.pl_update) continue;

		if (s.id)
			print(GF_LOG_INFO, log_name + ' ' + s.id + ' removed');
		else
			print(GF_LOG_INFO, log_name + ' removed');

		ID_list.splice(i, 1);
		i--;

		//group
		if (type==1) {
			let len_before = ID_list.length;
			remove_scene_or_group(s);
			//cleanup of subtree removed a group with ID, restart
			if (len_before != ID_list.length) {
				i=-1;
			}
		}
		//watcher
		else if (type==3) {
			remove_watcher(s, true);
		}
	}
}

function load_playlist()
{
	let pl = null;
	if (arguments.length==1) {
		pl = arguments[0];
		if (!Array.isArray(pl))
			return;
	}

	if (!pl) {
		let last_mtime = sys.mod_time(filter.pl);
		if (!last_mtime || (last_mtime == last_modification))
			return true;

		print(GF_LOG_DEBUG, last_modification ? 'refreshing JSON config' : 'loading JSON config')

		let f = sys.load_file(filter.pl, true);
		if (!f) return  true;

		current_utc_clock = Date.now();

		try {
			pl = JSON.parse(f);
		} catch (e) {
			print(GF_LOG_ERROR, "Invalid JSON playlist specified: " + e);
			last_modification = last_mtime;
			return false;
		}
		last_modification = last_mtime;
	}

	print(GF_LOG_DEBUG, 'Playlist is ' + JSON.stringify(pl) );

	//mark all our objects present
	sequences.forEach(seq => {
		seq.pl_update = false;
		seq.sources.forEach(src => {
			src.pl_update = false;
		});
	});

	//mark all scenes with ID as not updated
	ID_scenes.forEach(scene => { scene.pl_update = false; });
	if (scene_in_update) scene_in_update.pl_update = true;

	//mark all groups with ID as not updated
	ID_groups.forEach(group => { group.pl_update = false; });

	//mark all scripts as not updated
	user_scripts.forEach(o => { o.pl_update = false; });
	//mark all watchers as not updated
	watchers.forEach(o => { o.pl_update = false; });
	//mark all timers as not updated
	timers.forEach(o => { o.pl_update = false; });

	//reset styles
	styles.length = 0;

	//reset root
	root_scene.scenes.length = 0;
	watchers_deferred.length = 0;
	defer_parse_watchers = [];

	if (Array.isArray(pl) ) {
		pl.forEach(parse_playlist_element);
	} else {
		parse_playlist_element(pl);
	}

	//cleanup sources no longer in use
	//we don't use cleanup_list because we don't remove the source right away, this is done at next fetch_source
	for (let i=0; i<sources.length; i++) {
		let s = sources[i];
		if (s.pl_update) continue;
		let idx = s.sequence.sources.indexOf(s);
		if (idx>=0) {
	 		s.sequence.sources.splice(idx, 1);
		}
		print(GF_LOG_INFO, 'Source ' + s.logname + ' removed');
 		s.removed = true;
	}

	//cleanup scenes no longer in use
	cleanup_list(0);
	//cleanup groups no longer in use
	cleanup_list(1);
	//cleanup scripts no longer in use
	cleanup_list(2);
	//cleanup watchers no longer in use
	cleanup_list(3);
	//cleanup timers no longer in use
	cleanup_list(4);

	defer_parse_watchers.forEach( parse_watcher );
	defer_parse_watchers = null;

	if (scene_in_update) {
		if (!scene_in_update.parent && (root_scene.scenes.indexOf(scene_in_update) < 0)) {
			root_scene.scenes.push(scene_in_update);
		}
	}

	if (root_scene.scenes.length) {
		generate_default_scene = false;
	}

	//create default scene if needed
	if (!root_scene.scenes.length && sources.length && generate_default_scene) {
		print(GF_LOG_INFO, 'No scenes defined, generating default one');
		create_scene(null, {"id": "_gpac_scene_default", "js": "shape"}, null);
	}
	return true;
}

function parse_val(params, name, scene_obj, def_val, update_type)
{
	let new_val;
	let prop_set = (typeof scene_obj[name] == 'undefined') ? false : true;

	if (typeof params[name] == 'undefined') {
		//first setup, use default value
		if (! prop_set)
			new_val = def_val;
		else
			return;
	} else if (!Array.isArray(params[name]) && (typeof params[name] == 'object')) {
		return;
	} else {
		new_val = params[name];
	}

	//any change to these properties will require a PID reconfig of the scene
	if (prop_set && (new_val != scene_obj[name])) {
		//scene obj
		if (typeof scene_obj.mod == 'object')
			scene_obj.mod.update_flag |= update_type;
		else
			scene_obj.update_flag |= update_type;
	}
	scene_obj[name] = new_val;
}




const scene_props = [
	{ name: "js", def_val: null, update_flag: -1, is_mod: false},
	{ name: "sources", def_val: null, update_flag: -1, is_mod: false},
	{ name: "mix", def_val: null, update_flag: -1, is_mod: false},
	{ name: "styles", def_val: [], update_flag: 0, is_mod: false},

	{ name: "width", def_val: -1, update_flag: UPDATE_SIZE, is_mod: false},
	{ name: "height", def_val: -1, update_flag: UPDATE_SIZE, is_mod: false},
	{ name: "volume", def_val: 1, update_flag: 0, is_mod: true},
	{ name: "fade", def_val: "inout", update_flag: 0, is_mod: true},
	{ name: "mix_ratio", def_val: 0, update_flag: 0, is_mod: true},
	{ name: "autoshow", def_val: true, update_flag: 0, is_mod: false},
	{ name: "nosig", def_val: "lost", update_flag: 0, is_mod: false},
];

function scene_get_update_type(prop_name)
{
	let i;
	for (i=0; i<scene_props.length; i++) {
		if (scene_props[i].name == prop_name) return scene_props[i].update_flag;
	}
	return -2;
}

function scene_is_mod_option(prop_name)
{
	let i;
	for (i=0; i<scene_props.length; i++) {
		if (scene_props[i].name == prop_name) return scene_props[i].is_mod;
	}
	return -2;
}
function scene_mod_option_update_time(scene, prop_name)
{
	if (!scene.options) return -2;
	for (let i=0; i<scene.options.length; i++) {
		let o = scene.options[i];
		if (o.name != prop_name) continue;
		if (typeof o.dirty == 'number')
			return o.dirty;
		return -1;
	}
	return -2;
}


function set_scene_options(scene, params)
{
	//scene has been removed (duplicated ids)
	if (!scene.mod) return;

	scene_props.forEach(o => {
		if (o.def_val != null) {
			if (o.is_mod)
				parse_val(params, o.name, scene.mod, o.def_val, o.update_flag);
			else {
				parse_val(params, o.name, scene, o.def_val, o.update_flag);
			}
		}
	});

	scene.check_active = false;
	if (scene.autoshow || (scene.nosig != 'lost'))
		scene.check_active = true;

	//common with group
	parse_group_transform(scene, params);

	//parse module options
	if (scene.options) {
		scene.options.forEach( o => {
				if (typeof o.name == 'undefined') return;
				if (typeof o.value == 'undefined') return;
				let prop_set = (typeof scene.mod[o.name] == 'undefined') ? false : true;
				let mix_strings = o.dirty || 0;
				mix_strings = mix_strings & UPDATE_ALLOW_STRING;

				if (params && check_prop_type(typeof params[o.name], typeof o.value, mix_strings) ) {
					if (prop_set && (scene.mod[o.name] != params[o.name])) {
						let modif = true;
						if (Array.isArray(o.value) && (scene.mod[o.name].length == params[o.name].length)) {
							modif = false;
							for (let i=0; i<scene.mod[o.name].length; i++) {
								if (scene.mod[o.name][i] != params[o.name][i]) {
									modif=true;
									break;
								}
							}
						}
						if (modif) {
							let update_type = o.dirty || UPDATE_SIZE;
							scene.mod.update_flag |= update_type;
						}
					}
					scene.mod[o.name] = params[o.name];
					return;
				}
				if (params && (typeof params[o.name] == 'string') && (typeof o.value == 'number') ) {
					 let val = globalThis[params[o.name] ];
					 if (typeof val == typeof o.value) {
							if (prop_set && (scene.mod[o.name] != val)) {
								let update_type = o.dirty || UPDATE_SIZE;
								scene.mod.update_flag |= update_type;
							}
	 						scene.mod[o.name] = val;
	 						return;
					 }
				}
				//allow string to string array
				if (Array.isArray(o.value) && o.value.length && (typeof params[o.name] == 'string') && (typeof o.value[0] == 'string') ) {
						let update_type = o.dirty || UPDATE_SIZE;
						scene.mod.update_flag |= update_type;
 						scene.mod[o.name] = [ params[o.name] ];
 						return;
				}
				if (params && (typeof params[o.name] != 'undefined') ) {
					print(GF_LOG_WARNING, 'Type mismatch for scene parameter ' + o.name + ': expecting ' + typeof o.value + ' got ' + params[o.name] + (prop_set ? ' - ignoring update' : ' - using default value'));
				}
				//set default value
				if (prop_set) return;
				scene.mod[o.name] = o.value;
		});
	}

	for (var propertyName in params) {
		//defined ones at module level
		if (typeof scene.mod[propertyName] != 'undefined') continue;
		//defined ones at scene level
		if (scene_get_update_type(propertyName) > -2) continue;
		if (group_get_update_type(propertyName, true) > -2) continue;
		if (propertyName == 'id') continue;
		if (propertyName == 'skip') continue;
		if (propertyName.charAt(0) == '_') continue;

		if (scene.options && (typeof scene.mod[propertyName] != 'undefined')) continue;

		print(GF_LOG_WARNING, 'Unrecognized scene option ' + propertyName + ' for ' + scene.id + ' ');
	}

	//check if we have a mix instruction
	if (!scene.sequences || (scene.sequences.length<=1)) {
		scene.transition_effect = null;
		scene.transition = null;
	} else {
		let old_fx = scene.transition_effect;
		scene.transition_effect = params.mix || null;

		if (old_fx && scene.transition_effect && (scene.transition_effect.type != old_fx.type)) {
			scene.transition = null;
		}

		if (!scene.transition && scene.transition_effect) {
			scene.transition_state = 0;
			load_transition(scene);
		}
	}
}

function setup_scene(scene, seq_ids, params)
{

	scene.sequences = [];
	scene.resetup_pids = true;
	scene.mod.pids = [];
	scene.apids = [];
	scene.transition_effect = null;
	scene.mod.mix_ratio = 0;
	scene.transition = null;

	scene.sources = seq_ids;

	//default scene
	if (!seq_ids) {
		scene.sequences.push( sequences[0] );
	} else if (typeof seq_ids != 'undefined') {
		seq_ids.forEach(sid =>{
			let s = get_sequence(sid);
			if (!s) {
				s = get_group(sid);
				if (s && (s.offscreen == GROUP_OST_NONE)) s = null;
			}
			if (s)
				scene.sequences.push(s);
		});
	}

	scene.mod.update_flag = UPDATE_PID;
	if (params)
		set_scene_options(scene, params);
}

function remove_scene_or_group(elmt)
{
	let par = elmt.parent ? elmt.parent : root_scene;
  let index = par.scenes.indexOf(elmt);
  if (index > -1) par.scenes.splice(index, 1);

  if (elmt.id) {
		//traverse down tree, remove all elements with ID
		do_traverse_all(elmt, e => {
			e.parent = null;
			if (!e.id) return;
			let id_list = Array.isArray(e.scenes) ? ID_groups : ID_scenes;
			let index = id_list.indexOf(e);
			if (index > -1) id_list.splice(index, 1);
		});
  }
}


function create_scene(seq_ids, params, parent)
{
	let script_src = sys.url_cat(playlist_url, params.js);


	if (! sys.file_exists(script_src)) {
		script_src = filter.jspath + 'scenes/' + params.js + '.js';

		if (! sys.file_exists(script_src)) {
			print(GF_LOG_ERROR, 'No such scene file ' + script_src);
			return null;
		}
	}

	modules_pending ++;
	let scene = {};
	scene.id = params.id || null;
	scene.watchers = null;
	scene.pl_update = true;
	scene.mod = null;
	scene.sequences = [];
	scene.transition_effect = params.mix || null;
	scene.gl_type = SCENE_GL_NONE;
	scene.parent = parent;
	scene.mx = new evg.Matrix2D();

	scene.get = function(prop) {
		if (typeof this[prop] != 'undefined') return this[prop];
		if (typeof this.mod[prop] != 'undefined') return this.mod[prop];
		return undefined;
	}

	scene.set = function(prop, val) {
		let obj = this;

		//special handling of scenes
		if (prop=='sources') {
			if (Array.isArray(val)) {
				setup_scene(obj, val, null);
			}
			return;
		}
		let update_type = group_get_update_type(prop, true);

		if (update_type==-2) {
			update_type = scene_mod_option_update_time(this, prop);
			if (update_type>=0) {
				obj = this.mod;
			} else if (update_type==-2) {
				update_type = scene_get_update_type(prop);
				if ((update_type>=0) && scene_is_mod_option(prop)) {
					obj = this.mod;
				}
			}
		}

		//no modifications allowed or not defined
		if (update_type<0) {
			return;
		}

		let allow_string = (update_type & UPDATE_ALLOW_STRING) ? true : false;
		update_type &= ~UPDATE_ALLOW_STRING;

		if ((obj[prop] != val) && check_prop_type(typeof obj[prop], typeof val, allow_string)) {
			obj.update_flag |= update_type;
			obj[prop] = val;
			if (update_type)
				invalidate_parent(this);
			trigger_watcher(scene, prop, val);
		}
	}

	if (parent)
		parent.scenes.push(scene);
	else
		root_scene.scenes.push(scene);

	if (scene.id)
		ID_scenes.push(scene);

	import(script_src)
		  .then(obj => {
		  		modules_pending--;
		  		print(GF_LOG_DEBUG, 'Module ' + script_src + ' loaded');
		  		scene.mod = obj.load();
					scene.mod.id = scene.id || "unknown";

		  		if (typeof scene.mod.fullscreen != 'function')
		  				scene.mod.fullscreen = function () { return -1; };

		  		if (typeof scene.mod.is_opaque != 'function')
		  				scene.mod.is_opaque = function () { return false; };

		  		if (typeof scene.mod.identity != 'function')
		  				scene.mod.identity = function () { return false; };

		  		let has_draw = (typeof scene.mod.draw == 'function') ? true : false;
		  		let has_draw_gl = (typeof scene.mod.draw_gl == 'function') ? true : false;
		  		if (!has_draw && !has_draw_gl) {
		  				scene.mod.draw = function (canvas) {};
		  		} else if (has_draw && !has_draw_gl) {
		  			scene.gl_type = SCENE_GL_NONE;
		  		} else if (has_draw_gl) {
		  			if (!has_draw) {
			  			scene.gl_type = SCENE_GL_ONLY;
		  				scene.mod.draw = function (canvas) {};
		  				if (!filter.gpu) {
								print(GF_LOG_ERROR, 'GPU not enabled but required for scene ' + params.js + ' - disabling scene');
								remove_scene_or_group(scene);
								return;
		  				}
		  			} else {
			  			scene.gl_type = SCENE_GL_ANY;
		  			}
		  		}

		  		scene.options = obj.options || null;
		  		setup_scene(scene, seq_ids, params);
		  })
		  .catch(err => {
		  		modules_pending--;
					print(GF_LOG_ERROR, "Failed to load scene " + params.js + ' (' + script_src + '): ' + err);
					remove_scene_or_group(scene);
		  });

	//fixme, we need promises here
	return scene;
}

let timers=[];

function validate_timer(pl)
{
	let valid=true;
	if (! Array.isArray(pl.keys)) {
		print(GF_LOG_ERROR, 'Timer keys must be an array, ignoring ' + JSON.stringify(pl) );
		return false;
	}
	if (pl.keys.length<=1) {
		print(GF_LOG_ERROR, 'Timer keys must be at least two, ignoring ' + JSON.stringify(pl) );
		return false;
	}
	if (! Array.isArray(pl.anims)) {
		print(GF_LOG_ERROR, 'Timer anims must be an array, ignoring ' + JSON.stringify(pl) );
		return false;
	}

	let last_val = -1;
	pl.keys.forEach (v => {
			if (typeof v != 'number') valid=false;
			if (last_val<0) {
				if (v) valid = false;
			}
			else if (last_val >= v) valid = false;
			last_val = v;
	})
	if (!valid) {
		print(GF_LOG_ERROR, 'Timer keys must be numbers, from 0 to 1 in ascending order, ignoring ' + JSON.stringify(pl) );
		return false;
	}
	pl.anims.forEach(anim => {
		if (typeof anim != 'object') {
			print(GF_LOG_ERROR, 'Timer anims must be objects not ' + typeof anim + ', ignoring ' + JSON.stringify(pl) );
			valid = false;
			return;
		}
		if (! Array.isArray(anim.values) || ! Array.isArray(anim.targets)) {
			print(GF_LOG_ERROR, 'Timer anims values / targets must be an array, ignoring ' + JSON.stringify(pl) );
			valid = false;
			return;
		}
		if (anim.values.length != pl.keys.length) {
			print(GF_LOG_ERROR, 'Timer anims values must be in same quantity as keys, ignoring ' + JSON.stringify(pl) );
			valid = false;
			return;
		}
		anim.values.forEach(val => {
			if (typeof val != 'string' && typeof val != 'number' && typeof val != 'boolean' && !Array.isArray(val)) {
				print(GF_LOG_ERROR, 'Timer anims values must be string or numbers, ignoring ' + JSON.stringify(pl) );
				valid = false;
				return;
			}
		});
		anim.targets.forEach(val => {
			if (typeof val != 'string') {
				print(GF_LOG_ERROR, 'Timer anims targets must be strings, ignoring ' + JSON.stringify(pl) );
				valid = false;
				return;
			}
			if (val.indexOf('@') < 0) {
				print(GF_LOG_ERROR, 'Timer anims targets must be in the form ID@field, ignoring ' + JSON.stringify(pl) );
				valid = false;
				return;
			}
		});
	});
	if (!valid) return false;

	for (var propertyName in pl) {
		if (propertyName == 'id') continue;
		if (propertyName == 'dur') continue;
		if (propertyName == 'loop') continue;
		if (propertyName == 'start') continue;
		if (propertyName == 'stop') continue;
		if (propertyName == 'keys') continue;
		if (propertyName == 'anims') continue;
		if (propertyName == 'pause') continue;
		if (propertyName.charAt(0) == '_') continue;

		print(GF_LOG_WARNING, 'Unrecognized option ' + propertyName + ' in timer ' + JSON.stringify(pl) );
	}
	pl.anims.forEach(anim => {
		for (var propertyName in anim) {
			if (propertyName == 'values') continue;
			if (propertyName == 'color') continue;
			if (propertyName == 'angle') continue;
			if (propertyName == 'mode') continue;
			if (propertyName == 'postfun') continue;
			if (propertyName == 'end') continue;
			if (propertyName == 'targets') continue;
			if (propertyName.charAt(0) == '_') continue;

			print(GF_LOG_WARNING, 'Unrecognized option ' + propertyName + ' in timer anim ' + JSON.stringify(anim) );
		}
	});

	return true;
}

function parse_date_time(d, for_seq)
{
	let res = -1;
	if (typeof d == 'string') {
		//float as string means offset to current time, whether live or offline
		if (d.indexOf(':')<0) {
			res = parseFloat(d);
			//NaN
			if (res != res)
				res = -1;
			else {
				let now = for_seq ? current_utc_clock : (init_utc + video_time * 1000 / video_timescale);
				res = now + 1000 * res;
			}
		} else if (filter.live) {
			if (d === 'now') {
				res = current_utc_clock;
			} else {
				res = Date.parse(d);
				//NaN
				if (res != res)
					res = -1;
			}
		} else {
			print(GF_LOG_INFO, 'Date ' + d + ' found start/stop but non-live mode used, will use 0');
			res = 0;
		}
	} else if (typeof d == 'number') {
		if (d >= 0) {
			if (for_seq && !d) {
				res = filter.live ? 0 : 0;
			} else {
				res = (for_seq ? current_utc_clock : init_utc) + 1000 * d;
			}
		}
	} else if (typeof d == 'undefined') {
		res = for_seq ? 0 : init_utc;
	}
	return res;
}

function parse_timer(pl)
{
	if (!validate_timer(pl))
		return;

	let eval_start_time = false;
	let timer_id = pl.id || null;
	let timer = timer_id ? get_timer(timer_id) : null;

	if (!timer) {
		timer = {};
		timer.watchers = null;
		timers.push(timer);
		eval_start_time = true;
		timer.active_state = 0;
		timer.is_paused = false;
		timer.id = timer_id;
		timer.crc = 0;
	}	else if (timer.pl_update) {
		print(GF_LOG_WARNING, "Multiple timers with id `" + timer_id + "` defined, ignoring subsequent declarations");
		return null;
	}
	timer.pl_update = true;

	let crc = sys.crc32(JSON.stringify(pl));
	if (crc != timer.crc) {
		//we don't track changes of the timer, we blindly replace it
		if (timer.crc)
				timer_restore(timer);

		timer.crc = crc;
	} else {
		return;
	}

	timer.keys = pl.keys;
	timer.anims = [];
	pl.anims.forEach(anim =>{
		let a = {};
		a.values = anim.values;
		a.color = anim.color || false;
		a.mode = 0;
		a.fun = null;
		if (typeof anim.mode == 'string') {
			if (anim.mode == "discrete") a.mode = 1;
			else if (anim.mode == "linear") a.mode = 0;
			else {
				a.mode = 2;
				a.fun = fn_from_script(['interp'], anim.mode);
			}
		}

		a.postfun = (typeof anim.postfun == 'string') ? fn_from_script(['res', 'interp'], anim.postfun) : null;

		let mod = anim.end || "freeze";
		a.restore = 0;
		if (typeof mod == 'string') {
			if (pl.mod == 'freeze') {}
			else if (mod == 'restore') { a.restore = 1; }
		}

		a.angle = anim.angle || false;

		a.targets = [];
		anim.targets.forEach( t => {
			let vals = t.split("@");
			let tar = {};
			tar.id = vals[0];
			tar.scene = get_scene(vals[0]);
			tar.group = get_group(vals[0]);
			tar.script = get_script(vals[0]);
			tar.watcher = get_element_by_id(vals[0], watchers);
			tar.style = get_element_by_id(vals[0], styles);
			tar.fx = null;
			tar.fx_obj = null;
			if (!tar.scene && !tar.group && check_transition(vals[0]))
				tar.fx = vals[0];

			if (!tar.scene && !tar.group && !tar.fx && !tar.script && !tar.style) {
				print(GF_LOG_ERROR, 'No object with ID ' + vals[0] + ' found, ignoring target');
				return;
			}

			tar.field = vals[1];

			if (tar.field=="id") {
				print(GF_LOG_ERROR, 'ID cannot be animated, ignoring target');
				return;
			}

			tar.scene_obj = false;

			//indexed anim
			if (tar.field.indexOf('[')>0) {
				vals = tar.field.split('[');
				tar.field = vals[0];
				let idx = vals[1].split(']');
				tar.idx = parseInt(idx[0]);
			} else {
				tar.idx = -1;
			}

			if (tar.scene) {
				if (tar.field == 'sources') {
					tar.update_type = UPDATE_PID;
				} else {
					//check transform props
					tar.update_type = group_get_update_type(tar.field, true);
					if (tar.update_type>=0)
							tar.scene_obj = true;
				}

				//check scene (not module) props
				if (tar.update_type == -2) {
					tar.update_type = scene_get_update_type(tar.field);
					if ((tar.update_type>=0) && !scene_is_mod_option(tar.field))
						tar.scene_obj = true;
				}
				if ((tar.update_type == -2) && tar.scene && !tar.scene.mod) {
					//scene module not loaded, defer
					tar.update_type = -3;
				}
				else if ((tar.update_type == -2) && tar.scene && tar.scene.options) {
					tar.update_type = scene_mod_option_update_time(tar.scene, tar.field);
				}
			} else if (tar.group) {
				tar.update_type = group_get_update_type(tar.field, false);
			} else if (tar.script) {
				if (tar.field!="active") {
					print(GF_LOG_ERROR, 'Property ' + tar.field + ' of script cannot be animated, ignoring target');
					return;
				}
				tar.update_type = 0;
			} else if (tar.watcher) {
				if (tar.field!="active") {
					print(GF_LOG_ERROR, 'Property ' + tar.field + ' of watcher cannot be animated, ignoring target');
					return;
				}
				tar.update_type = 0;
			} else if (tar.style) {
				tar.update_type = 0;
			} else {
				tar.update_type = UPDATE_SIZE;
			}

			if (tar.update_type == -2) {
				print(GF_LOG_ERROR, 'No property ' + tar.field + ' in ' + (tar.scene ? 'scene' : 'group') + ', ignoring target');
				return;
			}
			if (tar.update_type == -1) {
				print(GF_LOG_ERROR, 'Property ' + tar.field + ' in ' + (tar.scene ? 'scene' : 'group') + ' cannot be updated, ignoring target');
				return;
			}

			a.targets.push(tar);
		});
		timer.anims.push(a);
	});

	timer.loop = pl.loop || false;
	timer.pause = pl.pause || false;

	if (timer.active_state!=1) {
		eval_start_time = true;
	}

	if (eval_start_time) {
		timer.duration = pl.dur || 0;
		timer.start_time = parse_date_time(pl.start, false);
	}

	timer.stop_time = parse_date_time(pl.stop, false);
	if (timer.stop_time<=timer.start_time)
		timer.stop_time = -1;

	if (!timer.duration) timer.active_state = 2;
}

function timer_restore(timer)
{
	//restore values
	timer.anims.forEach(anim => {
		if (anim.restore != 1) return;

		anim.targets.forEach(target => {
			if (!target.scene) return;
			let scene = target.scene.mod;

			if (target.idx>=0) {
				if (Array.isArray(scene[target.field])) {
					scene.update_flag |= target.update_type;
					scene[target.field][target.idx] = target.orig_value;
				}
			} else {
				scene.update_flag |= target.update_type;
				scene[target.field] = target.orig_value;
			}
		});
	});
}

function invalidate_parent(elm)
{
	while (elm.parent) {
		elm.parent.update_flag |= UPDATE_CHILD;
		elm = elm.parent;
	}
}

function get_timer(timer_id)
{
	return get_element_by_id(timer_id, timers);
}

function update_timer(timer)
{
	let do_store=false;

	if (timer.active_state == 2) return;

	let now = init_utc + video_time * 1000 / video_timescale;
	if ((timer.start_time<0) || (timer.start_time > now)) {
		return;
	}

	if (!timer.active_state) {
		timer.active_state = 1;
		trigger_watcher(timer, "active", true);
		timer.activation_time = video_time;
		do_store = true;
	}

	if (timer.pause) {
		if (!timer.is_paused) {
			timer.pause_time = video_time;
			timer.is_paused = true;
		}
		return;
	} else if (timer.is_paused) {
		timer.activation_time += video_time - timer.pause_time;
		timer.is_paused = false;
	}

	let frac = (video_time - timer.activation_time) * video_time_inc / video_timescale;

	if (frac > timer.duration) {
		if (!timer.loop && (timer.stop_time<0) ) {
			timer.active_state = 2;
			trigger_watcher(timer, "active", false);
		} else {
			while (frac > timer.duration) frac -= timer.duration;
		}
	}
	if ((timer.stop_time > timer.start_time) && (timer.stop_time <= now)) {
		timer.active_state = 2;
		trigger_watcher(timer, "active", false);
	}

	if (timer.active_state == 2) {
		timer_restore(timer);
		return;
	}

	frac /= timer.duration;
	let s_key = timer.keys[0];
	let e_key = 1;
	let k_idx = 0;
	for (let i=1; i<timer.keys.length; i++) {
			if (frac <= timer.keys[i]) {
				e_key = timer.keys[i];
				break;
			}
			s_key = timer.keys[i];
			k_idx = i;
	}

	let interpolate = (frac - s_key) / (e_key - s_key);

	timer.anims.forEach(anim => {
		let res = 0;
		let value1 = anim.values[k_idx];
		let value2 = anim.values[k_idx+1];
		let interp = interpolate;

		if (anim.angle) { interp *= Math.PI; interp /= 180; }

		else if (anim.fun != null) {
				try {
					interp = anim.fun.apply(anim, [interp]);
					if (typeof interp != 'number')
						throw 'Interpolation function result is not a number !';
				} catch (e) {
					print(GF_LOG_ERROR, "Error processing anim fun " + anim.fun + ': ' + e);
					anim.fun = null;
					return;
				}
				if (interp<0) interp=0;
				else if (interp>1) interp=1;
		}

		if (anim.mode==1) {
			res = value1;
			if (anim.postfun) {
				try {
					res = anim.postfun.apply(anim, [res, interp]);
				} catch (e) {
					print(GF_LOG_ERROR, "Error processing anim postfun " + anim.postfun + ': ' + e);
					anim.postfun = null;
				}
			}
		}
		else if (anim.color) {
			res = sys.color_lerp(interp, value1, value2);
		}
		else if (typeof value1 != 'number') res = value1;
		else if (typeof value2 != 'number') res = value1;
		else {
			res = interp * value2 + (1-interp) * value1;

			if (anim.postfun) {
				try {
					res = anim.postfun.apply(anim, [res, interp]);
				} catch (e) {
					print(GF_LOG_ERROR, "Error processing anim postfun " + anim.postfun + ': ' + e);
					anim.postfun = null;
				}
			}
		}

		for (let i=0; i<anim.targets.length; i++) {
			let target = anim.targets[i];
			let watch_src=null;

			let anim_obj = null;

			if (target.scene) {
				anim_obj = target.scene.mod;
				watch_src = target.scene;
			}
			else if (target.group) {
				anim_obj = target.group;
			}
			else if (target.fx) {
				if (!target.fx_obj) {
					target.fx_obj = get_transition(target.fx, false);
					if (!target.fx_obj) {
						print(GF_LOG_WARNING, 'No transition with target ' + target.field + ' found, removing target');
						anim.targets.splice(i, 1);
						i--;
						continue;
					}
				}
				anim_obj = target.fx_obj.mod;
			}
			else if (target.script) {
				anim_obj = target.script;
			}
			else if (target.style) {
				anim_obj = target.style;
			}
			else if (target.watcher) {
				anim_obj = target.watcher;
			}

			if (!anim_obj) {
				print(GF_LOG_WARNING, 'Animation target ' + target.field + ' not found, removing target');
				anim.targets.splice(i, 1);
				i--;
				continue;
			}

			if (target.scene_obj) {
				anim_obj = target.scene;
			}

			if (typeof anim_obj[target.field] == 'undefined') {
				//special case for scene, some options are not in the scene module
				if (target.scene && ((target.update_type==UPDATE_PID) || (typeof target.scene[target.field] != 'undefined'))) {
					anim_obj = target.scene;
				}
				//otherwise error except for style
				else if (!target.style) {
					print(GF_LOG_WARNING, 'No property named ' + target.field + ' in target obj ID ' + target.id + ', removing target');
					anim.targets.splice(i, 1);
					i--;
					continue;
				}
			}
			let update_type = target.update_type;

			//update type was deferred (scene not loaded)
			if ((update_type == -3) && target.scene) {
				if (typeof target.scene.mod[target.field] != 'undefined') {
					update_type = scene_mod_option_update_time(target.scene, target.field);
				}
				if (update_type<0) {
					print(GF_LOG_WARNING, 'No property named ' + target.field + ' in target obj ID ' + target.id + ', removing target');
					anim.targets.splice(i, 1);
					i--;
					continue;
				}
				target.update_type = update_type;
			}


			let allow_string = (update_type & UPDATE_ALLOW_STRING) ? true : false;
			update_type &= ~UPDATE_ALLOW_STRING;

			let final = res;
			if (!watch_src) watch_src = anim_obj;

			if (target.idx>=0) {
				if (Array.isArray(anim_obj[target.field])
					&& (target.idx<anim_obj[target.field].length)
					&& (typeof anim_obj[target.field][target.idx] == typeof final)
				) {
					if (do_store) {
						target.orig_value = anim_obj[target.field][target.idx];
					}
					if ((anim_obj[target.field][target.idx] != final)) {
						anim_obj.update_flag |= update_type;
						anim_obj[target.field][target.idx] = final;
						if (update_type)
							invalidate_parent(target.scene || target.group);

						trigger_watcher(watch_src, target.field, anim_obj[target.field]);
					}
				}
			}
			//we don't check type for style objects, and check ot for others
			else if (target.style || check_prop_type(typeof anim_obj[target.field], typeof final, allow_string)) {
				if (do_store) {
					target.orig_value = anim_obj[target.field];
				}
				if ((anim_obj[target.field] != final)) {
					print(GF_LOG_DEBUG, 'update ' + target.id + '.' + target.field + ' to ' + final);
					if (update_type == UPDATE_PID) {
						setup_scene(target.scene, final, null);
					} else {
						anim_obj.update_flag |= update_type;
						anim_obj[target.field] = final;

						if (target.style)
							target.style.updated = true;
						else if (update_type)
							invalidate_parent(target.group || target.scene);
					}
					trigger_watcher(watch_src, target.field, anim_obj[target.field]);
				}
			} else {
				print(GF_LOG_WARNING, 'Anim type mismatch for ' + target.field + ' got ' + typeof final + ' expecting ' + typeof anim_obj[target.field] + ', ignoring');
			}
		};

	});
}


function clip_to_output(clip)
{
	let rc = {};
	rc.x = clip.x - clip.w/2;
	rc.y = clip.y + clip.h/2;
	rc.w = clip.w;
	rc.h = clip.h;
	rc = active_scene.mx.apply(rc);

	rc.x = Math.floor(rc.x);
	rc.y = Math.floor(rc.y);
	rc.w = Math.floor(rc.w);
	rc.h = Math.floor(rc.h);
	if (!webgl) return round_rect(rc);
	return rc;
}

function canvas_clear(color, clip)
{
	if (!active_scene) return;
	if (active_scene.mx.is3D) {
		print(GF_LOG_ERROR, 'Cannot clear canvas in 3D context');
		return;
	}

	clip = clip_to_output(clip);

	if (webgl) {
		let a = sys.color_component(color, 0);
		let r = sys.color_component(color, 1);
		let g = sys.color_component(color, 2);
		let b = sys.color_component(color, 3);

		webgl.clearColor(r, g, b, a);

		webgl.enable(webgl.SCISSOR_TEST);
		webgl.scissor(video_width/2 + clip.x, clip.y - clip.h + video_height/2, clip.w, clip.h);
		webgl.clear(webgl.COLOR_BUFFER_BIT);
		webgl.disable(webgl.SCISSOR_TEST);

	} else {
		canvas.clear(clip, color);
	}
}

let wgl_clipper = null;

let clip_stack = [];
function canvas_set_clipper(clip, use_stack)
{
	if (!active_scene) return;
	if (active_scene.mx.is3D) {
		print(GF_LOG_ERROR, 'Cannot set clipper in 3D context');
		return;
	}

	if (use_stack) {
		if (!clip) {
			clip_stack.pop();
			clip = clip_stack.length ? clip_stack[clip_stack.length - 1] : null;
		} else {
			clip = clip_to_output(clip);

			if (clip_stack.length) {
				let prev_clip = clip_stack[clip_stack.length - 1];
				clip = sys.rect_intersect(prev_clip, clip);
			}
			clip_stack.push(clip);
		}
	} else {
		if (clip)
			clip = clip_to_output(clip);
	}
	if (webgl) {
		wgl_clipper = clip;
	} else {
		canvas.clipper = clip;
	}
}

let mask_canvas = null;
let mask_canvas_data = null;
let mask_texture = null;
let mask_gl_stencil = null;

function canvas_set_mask_mode(mode)
{
		if (use_gpu) {
			if (mode) {
				if (!mask_canvas_data) {
					mask_canvas_data = new ArrayBuffer(video_width * video_height);
					mask_canvas = new evg.Canvas(video_width, video_height, "grey", mask_canvas_data);
					mask_texture = webgl.createTexture();

					webgl.bindTexture(webgl.TEXTURE_2D, mask_texture);
					webgl.texParameteri(webgl.TEXTURE_2D, webgl.TEXTURE_WRAP_S, webgl.CLAMP_TO_EDGE);
					webgl.texParameteri(webgl.TEXTURE_2D, webgl.TEXTURE_WRAP_T, webgl.CLAMP_TO_EDGE);
					webgl.texParameteri(webgl.TEXTURE_2D, webgl.TEXTURE_MIN_FILTER, webgl.LINEAR);
					webgl.texParameteri(webgl.TEXTURE_2D, webgl.TEXTURE_MAG_FILTER, webgl.LINEAR);
					webgl.texImage2D(webgl.TEXTURE_2D, 0, webgl.LUMINANCE, video_width, video_height, 0, webgl.LUMINANCE, webgl.UNSIGNED_BYTE, mask_canvas_data);
					webgl.bindTexture(webgl.TEXTURE_2D, 0);

					mask_gl_stencil = new evg.SolidBrush();
					mask_gl_stencil.set_color("black");
				}
				let clear = null;
				if (mode == GF_EVGMASK_DRAW) {
					clear = 'black';
				}
				else if (mode == GF_EVGMASK_RECORD) {
					clear = 'white';
				}
				else if (mode == GF_EVGMASK_DRAW_NO_CLEAR) {
					mode = GF_EVGMASK_DRAW;
				}

				if (clear) {
					mask_canvas.clear(clear);
				}
			}
		} else {
			canvas.mask_mode = mode;
		}

		mask_mode = mode;
}


let canvas_offscreen_active = false;

function flush_offscreen_canvas()
{
 	canvas_offscreen._gl_texture.upload(canvas_offscreen._evg_texture);

  let glprog = canvas_offscreen._gl_program;
  webgl.useProgram(glprog.program);

  webgl.viewport(0, 0, video_width, video_height);

  //set video texture
  webgl.activeTexture(webgl.TEXTURE0);
  webgl.bindTexture(webgl.TEXTURE_2D, canvas_offscreen._gl_texture);
	webgl.uniform1i(glprog.textures[0].sampler, 0);

	canvas_offscreen._mesh.draw(webgl, glprog.attribLocations.vertexPosition, glprog.attribLocations.textureCoord);

  webgl.useProgram(null);
}

function canvas_offscreen_activate()
{
	if (canvas_offscreen_active) return true;

	if (!canvas_offscreen) {
		canvas_offscreen = new evg.Canvas(video_width, video_height, 'rgba');
		if (!canvas_offscreen) return false;
		canvas_offscreen._evg_texture = new evg.Texture(canvas_offscreen);
		if (!canvas_offscreen._evg_texture) return false;

		canvas_offscreen._gl_texture = webgl.createTexture(null);
		if (!canvas_offscreen._gl_texture) return false;
		canvas_offscreen._gl_tx_id = webgl.textureName(canvas_offscreen._gl_texture);


	 	canvas_offscreen._gl_texture.upload(canvas_offscreen._evg_texture);

		let path = new evg.Path().rectangle(0, 0, video_width, video_height, true);
		canvas_offscreen._mesh = new evg.Mesh(path);
		if (!canvas_offscreen._mesh) return;
		canvas_offscreen._mesh.update_gl();

  	canvas_offscreen._gl_program = setup_webgl_program(vs_source, fs_source, canvas_offscreen._gl_tx_id);
	}
	canvas_offscreen.clearf(0, 0, 0, 0);
	canvas_offscreen_active = true;
	return true;
}

function canvas_offscreen_deactivate()
{
	if (!canvas_offscreen_active) return;

	canvas_offscreen_active = false;
	flush_offscreen_canvas();
}

let no_signal_text = null;
let no_signal_brush = new evg.SolidBrush();
let no_signal_path = null;
let no_signal_outline = null;

let no_signal_transform = new evg.Matrix2D();

function draw_scene_no_signal()
{
	if (active_scene && (active_scene.nosig == 'no')) return;

	if (!no_signal_path) no_signal_path = new evg.Text();

	let text = no_signal_path;
	let h = active_scene ? active_scene.mod.height : video_height;
	let w = active_scene ? active_scene.mod.width : video_width;
	text.fontsize = h / 20;
  text.font = ['SANS'];
  text.align = GF_TEXT_ALIGN_LEFT;
	text.baseline = GF_TEXT_BASELINE_TOP;

	text.maxWidth = w;
	let d = new Date();
	if (no_signal_text==null) {
		//let version = 'GPAC '+sys.version + ' API '+sys.version_major+'.'+sys.version_minor+'.'+sys.version_micro;
		let version = 'GPAC ' + (sys.test_mode ? 'Test Mode' : sys.version_full);
		no_signal_text = ['No input', '', version, '(c) 2000-2022 Telecom Paris'];
	}
	let s1 = null;
	if (active_scene && active_scene.mod.pids.length) {
		if (sys.test_mode)
			s1 = 'Signal lost';
		else if (active_scene.mod.pids[0].pid.source.no_signal_state)
			s1 = 'No signal (next scheduled in ' + Math.floor(active_scene.mod.pids[0].pid.source.no_signal_state) + ' s)';
		else
			s1 = 'Signal lost (' + Math.floor(active_scene.mod.pids[0].pid.source.no_signal/1000) + ' s)';
	} else if (active_scene && (active_scene.no_signal_time>0)) {
		let next = new Date(active_scene.no_signal_time);
		s1 = 'No input (next schedule ' + next.toUTCString() + ' s)';
	} else {
		s1 = 'No input';
	}
	let s2 = sys.test_mode ? 'Date' : d.toUTCString();
	if ((s1 !== no_signal_text[0]) || (s2 !== no_signal_text[1])) {
		no_signal_text[0] = s1;
		no_signal_text[1] = s2;
	  text.set_text(no_signal_text);
	  no_signal_outline = null;
	}

	if (active_scene)
		no_signal_transform.copy(active_scene.mx);
	else
		no_signal_transform.identity = true;

  no_signal_transform.translate(-w/2, 0);
  //comment if align to left
  let m = text.measure();
  no_signal_transform.translate(m.width/2, 0);

  if (!no_signal_outline) {
  	no_signal_outline = text.get_path().outline( { width: 6, align: GF_PATH_LINE_CENTER, cap: GF_LINE_CAP_ROUND, join: GF_LINE_JOIN_ROUND } );
  }
  no_signal_brush.set_color('black');

  let scene = active_scene;
  active_scene = null;
	canvas_draw(no_signal_outline, no_signal_brush, true);

/*  no_signal_brush.set_color('black');
	canvas_draw(text, mx, no_signal_brush, true);
  mx.translate(-2, 2);
*/
  no_signal_brush.set_color('white');
	canvas_draw(text, no_signal_brush, true);

  active_scene = scene;
}


function push_texture_uniforms(tx, prog_tx_uni)
{
	let mx2d = tx.mx || null;
	const txmx = mx2d ? new evg.Matrix(mx2d) : new evg.Matrix();
	webgl.uniformMatrix4fv(prog_tx_uni.matrix, false, txmx.m);

	webgl.uniform1f(prog_tx_uni.alpha, tx.get_alphaf() );

	let cmx_val = tx.cmx;
	if (cmx_val.identity) {
		  webgl.uniform1i(prog_tx_uni.cmx_use, 0);
	} else {
		let cmx_mul = [];
		cmx_mul.length = 16;

		cmx_mul[0] = cmx_val.rr; cmx_mul[1] = cmx_val.rg; cmx_mul[2] = cmx_val.rb; cmx_mul[3] = cmx_val.ra;
		cmx_mul[4] = cmx_val.gr; cmx_mul[5] = cmx_val.gg; cmx_mul[6] = cmx_val.gb; cmx_mul[7] = cmx_val.ga;
		cmx_mul[8] = cmx_val.br; cmx_mul[9] = cmx_val.bg; cmx_mul[10] = cmx_val.bb; cmx_mul[11] = cmx_val.ba;
		cmx_mul[12] = cmx_val.ar; cmx_mul[13] = cmx_val.ag; cmx_mul[14] = cmx_val.ab; cmx_mul[15] = cmx_val.aa;

		webgl.uniformMatrix4fv(prog_tx_uni.cmx_mul, false, cmx_mul);
		webgl.uniform4f(prog_tx_uni.cmx_add, cmx_val.tr, cmx_val.tg, cmx_val.tb, cmx_val.ta);
		webgl.uniform1i(prog_tx_uni.cmx_use, 1);
	}

	if (prog_tx_uni.r_s) {
	  webgl.uniform1i(prog_tx_uni.r_s, tx.repeat_s);
	  webgl.uniform1i(prog_tx_uni.r_t, tx.repeat_t);


		let color = tx.get_pad_color();
		let a = sys.color_component(color, 0);
		let r = sys.color_component(color, 1);
		let g = sys.color_component(color, 2);
		let b = sys.color_component(color, 3);
		webgl.uniform4f(prog_tx_uni.pad_color, r, g, b, a);
	}
}

function canvas_set_matrix(cnv, mx, prog_info)
{
	if (!mx.is3D) {
		if (cnv) {
			cnv.matrix = mx;
		} else {
			const modelViewMatrix = new evg.Matrix(mx);
			webgl.uniformMatrix4fv(prog_info.modelViewMatrix, false, modelViewMatrix.m);
		}
		return;
	}
  let fieldOfView = active_camera ? (active_camera.fov*Math.PI / 180) : (Math.PI / 4);
  let aspect =  (active_camera && active_camera.ar) ? active_camera.ar : (video_width / video_height);
  let zNear = (active_camera && active_camera.znear) ? active_camera.znear : 0.1;
  let zFar = (active_camera && active_camera.zfar) ? active_camera.zfar : (10 * (video_width>video_height ? video_width : video_height));

  //pick view distance so that with no transformation, a rectangular scene with size 100%x100% is exactly fullscreen
	let final_z = video_height/2 / Math.tan(fieldOfView/2);
	let pos = {x:0, y:0, z: final_z};
	let center = {x:0, y:0, z: 0};
	let up = {x:0, y:1, z: 0};

  if (active_camera) {
		if (active_camera.position && (active_camera.position.length==3)) {
			pos.x = active_camera.position[0];
			pos.y = active_camera.position[1];
			pos.z = active_camera.position[2];
		}
		if (active_camera.target && (active_camera.target.length==3)) {
			center.x = active_camera.target[0];
			center.y = active_camera.target[1];
			center.z = active_camera.target[2];
		}
		if (active_camera.up && (active_camera.up.length==3)) {
			up.x = active_camera.up[0];
			up.y = active_camera.up[1];
			up.z = active_camera.up[2];
		}
	}

  let projectionMatrix = new evg.Matrix().perspective(fieldOfView, aspect, zNear, zFar);
  let modelViewMatrix = new evg.Matrix();

  modelViewMatrix.lookat(pos, center, up);

  modelViewMatrix.add(mx);

  if (cnv) {
	  projectionMatrix.add(modelViewMatrix, true);
	  cnv.matrix3d = projectionMatrix;
  }
  //webgl
  else {
		webgl.uniformMatrix4fv(prog_info.projectionMatrix, false, projectionMatrix.m);
	  webgl.uniformMatrix4fv(prog_info.modelViewMatrix, false, modelViewMatrix.m);
  }

  if (active_camera && active_camera.viewport && (active_camera.viewport.length==4)) {
		let vp_x = active_camera.viewport[0];
		let vp_y = active_camera.viewport[1];
		let vp_w = active_camera.viewport[2];
		let vp_h = active_camera.viewport[3];
		if (active_camera.units == UNIT_RELATIVE) {
			if (typeof vp_x == 'number') vp_x = vp_x * video_width / 100;
			if (typeof vp_y == 'number') vp_y = vp_y * video_height / 100;
			if (typeof vp_w == 'number') vp_w = vp_w * video_width / 100;
			if (typeof vp_h == 'number') vp_h = vp_h * video_height / 100;
		}

		if (vp_x == 'y') vp_x = vp_y;
		else if (vp_x == '-y') vp_x = -vp_y;
		if (vp_y == 'x') vp_y = vp_x;
		else if (vp_y == '-x') vp_y = -vp_x;
		if (vp_w == 'height') vp_w = vp_h;
		if (vp_h == 'width') vp_h = vp_w;

		vp_x = video_width/2 + vp_x - vp_w/2;
		vp_y = video_height/2 + vp_y - vp_h/2;

	  if (cnv) {
			vp_y = video_height - vp_y - vp_h;

			if (round_scene_size) {
				if (vp_x % 2) { vp_x--; vp_w++; };
				if (vp_w % 2) vp_w--;
				if (round_scene_size==2) {
					if (vp_y % 2) { vp_y--; vp_h++; };
					if (vp_h % 2) vp_h--;
				}
			}
			cnv.viewport(vp_x, vp_y, vp_w, vp_h);
	  } else {
		  webgl.viewport(vp_x, vp_y, vp_w, vp_h);
	  }
  }

}

function canvas_draw(path, stencil)
{
	let uniform_reload = false;
	//regular call
	if (arguments.length==2) {
		if (!path)
			return;
		if (!stencil) {
			//this only happens if no more input pids for scene
			draw_scene_no_signal();
			return;
		}
		if (active_scene.mod.pids.length && active_scene.mod.pids[0].pid && active_scene.mod.pids[0].pid.source.no_signal) {
			draw_scene_no_signal();
			return;
		}
		uniform_reload = active_scene.gl_uniforms_reload;
	} else {
		//group or no signal, always reload uniforms
		uniform_reload = true;
	}

	let matrix = active_scene ? active_scene.mx : no_signal_transform;

	if (!webgl) {
	  canvas.path = path;
	  canvas_set_matrix(canvas, matrix);
	  canvas.fill(stencil);
	  return;
	}

	let is_texture = stencil.width || 0;
	let is_text = path.fontsize || 0;
	let use_soft_raster = false;
	if (!is_texture) {
		if (stencil.solid_brush) {
			use_soft_raster = (filter.gpu==2) ? false : true;
		}
		//we don't support gradients on GPU for now
		else use_soft_raster = true;
	}
	//we only draw text through software rasterizer
	else if (is_text) use_soft_raster = true;

	if (mask_mode) {
		use_soft_raster = false;
	}

	if (use_soft_raster) {
		canvas_offscreen_activate();
		if (canvas_offscreen.mask_mode != mask_mode) {
			canvas_offscreen.mask_mode = mask_mode;
		}
	  canvas_offscreen.path = path;
	  canvas_set_matrix(canvas_offscreen, matrix);
	  canvas_offscreen.fill(stencil);
		return;
	}

	canvas_offscreen_deactivate();

	let mesh = path.mesh || null;
	if (!mesh) {
		mesh = path.mesh = new evg.Mesh(path);
		if (!path.mesh) return;
		path.mesh.update_gl();
	}

	if (is_texture) {
		let texture = stencil._gl_texture || null;
		if (!texture) {
			stencil._gl_texture = webgl.createTexture(null);
			if (!stencil._gl_texture) return;
			stencil._gl_tx_id = webgl.textureName(stencil._gl_texture);

			if (stencil._gl_program) {
				stencil._gl_program = null;
				print(GF_LOG_INFO, 'texture format change, reloading GLSL shader');
			}
			stencil._gl_modified = true;
		}

		if (stencil._gl_modified == true) {
			webgl.bindTexture(webgl.TEXTURE_2D, stencil._gl_texture);
	    stencil._gl_texture.upload(stencil);
			stencil._gl_modified = false;
		}
	}

	let prog_info = stencil._gl_program || null;
  if (!stencil._gl_program) {
		if (is_texture) {
			let color_mx = stencil.cmx;
			if (color_mx.identity) color_mx = null;

			let fs_src = fs_source;

			prog_info = stencil._gl_program = setup_webgl_program(vs_source, fs_src, stencil._gl_tx_id);
			if (!stencil._gl_program) {
				filter.abort();
				return;
			}
		} else {
			prog_info = stencil._gl_program = setup_webgl_program(vs_const_col, fs_const_col, null);
			if (!stencil._gl_program) {
				filter.abort();
				return;
			}
		}
		webgl.useProgram(prog_info.program);
		webgl.uniform1f(prog_info.mask_w, video_width);
		webgl.uniform1f(prog_info.mask_h, video_height);
	}

	let use_mask = 0;
	if (mask_mode==GF_EVGMASK_DRAW) {
	  mask_canvas.path = path;
	  canvas_set_matrix(mask_canvas, matrix);
	  mask_canvas.fill(stencil);
		return;
	}
	else if ((mask_mode==GF_EVGMASK_USE) || (mask_mode==GF_EVGMASK_RECORD)) {
	  use_mask = 1;
	} else if (mask_mode==GF_EVGMASK_USE_INV) {
	  use_mask = 2;
	}

  webgl.useProgram(prog_info.program);

  webgl.viewport(0, 0, video_width, video_height);

  if (wgl_clipper) {
		webgl.enable(webgl.SCISSOR_TEST);

		webgl.scissor(video_width/2 + wgl_clipper.x, wgl_clipper.y - wgl_clipper.h + video_height/2, wgl_clipper.w, wgl_clipper.h);
  } else {
		webgl.disable(webgl.SCISSOR_TEST);
  }

  //set transform
	canvas_set_matrix(null, matrix, prog_info);

  //set mask
  webgl.uniform1i(prog_info.mask_mode, use_mask);

	if (use_mask) {
		let tx_slot = is_texture ? stencil._gl_texture.nb_textures : 0;

	  webgl.activeTexture(webgl.TEXTURE0 + tx_slot);
		webgl.bindTexture(webgl.TEXTURE_2D, mask_texture);
		//not a named texture
		webgl.texSubImage2D(webgl.TEXTURE_2D, 0, 0, 0, video_width, video_height,webgl.LUMINANCE, webgl.UNSIGNED_BYTE, mask_canvas_data);
		webgl.uniform1i(prog_info.mask_tx, tx_slot);
	}

  if (is_texture) {
		if (uniform_reload) {
			push_texture_uniforms(stencil, prog_info.textures[0]);
		}

	  //set video texture
	  webgl.activeTexture(webgl.TEXTURE0);
	  webgl.bindTexture(webgl.TEXTURE_2D, stencil._gl_texture);
		webgl.uniform1i(prog_info.textures[0].sampler, 0);

		mesh.draw(webgl, prog_info.attribLocations.vertexPosition, prog_info.attribLocations.textureCoord);
  } else {
		if (uniform_reload) {
			let color = stencil.get_color();
			let alpha = stencil.get_alphaf();
			let a = sys.color_component(color, 0) * alpha;
			let r = sys.color_component(color, 1);
			let g = sys.color_component(color, 2);
			let b = sys.color_component(color, 3);
			webgl.uniform4f(prog_info.color, r, g, b, a);
		}

		mesh.draw(webgl, prog_info.attribLocations.vertexPosition);
  }
  webgl.useProgram(null);
	webgl.disable(webgl.SCISSOR_TEST);

	//record mode, also draw using black stencil on mask canvas
	if (mask_mode==GF_EVGMASK_RECORD) {
	  mask_canvas.path = path;
	  canvas_set_matrix(mask_canvas, matrix);
	  mask_canvas.fill(mask_gl_stencil);
	}
}

const transition_vs_source = `
attribute vec4 aVertexPosition;
attribute vec2 aTextureCoord;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
varying vec2 txcoord_from;
varying vec2 txcoord_to;
uniform mat4 textureMatrixFrom;
uniform mat4 textureMatrixTo;
void main() {
  gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
	txcoord_from = vec2(textureMatrixFrom * vec4(aTextureCoord.x, aTextureCoord.y, 0, 1) );
	txcoord_to = vec2(textureMatrixTo * vec4(aTextureCoord.x, aTextureCoord.y, 0, 1) );
}
`;

const single_in_op_vs_source = `
attribute vec4 aVertexPosition;
attribute vec2 aTextureCoord;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
varying vec2 txcoord_from;
varying vec2 txcoord_op;
uniform mat4 textureMatrixFrom;
uniform mat4 textureMatrixOp;
void main() {
  gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
	txcoord_from = vec2(textureMatrixFrom * vec4(aTextureCoord.x, aTextureCoord.y, 0, 1) );
	txcoord_op = vec2(textureMatrixOp * vec4(aTextureCoord.x, aTextureCoord.y, 0, 1) );
}
`;


const dual_in_op_vs_source = `
attribute vec4 aVertexPosition;
attribute vec2 aTextureCoord;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
varying vec2 txcoord_from;
varying vec2 txcoord_to;
varying vec2 txcoord_op;
uniform mat4 textureMatrixFrom;
uniform mat4 textureMatrixTo;
uniform mat4 textureMatrixOp;
void main() {
  gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
	txcoord_from = vec2(textureMatrixFrom * vec4(aTextureCoord.x, aTextureCoord.y, 0, 1) );
	txcoord_to = vec2(textureMatrixTo * vec4(aTextureCoord.x, aTextureCoord.y, 0, 1) );
	txcoord_op = vec2(textureMatrixOp * vec4(aTextureCoord.x, aTextureCoord.y, 0, 1) );
}
`;

const fs_trans_source_prefix = `
uniform sampler2D mask_tx;
uniform int mask_mode;
uniform float mask_w, mask_h;

uniform vec4 pad_color;
uniform bool r_s, r_t;
uniform float ratio;
uniform float video_ar;

varying vec2 txcoord_from;
uniform sampler2D maintx1;
uniform float _alpha_from;
uniform bool cmx1_use;
uniform mat4 cmx1_mul;
uniform vec4 cmx1_add;

varying vec2 txcoord_to;
uniform sampler2D maintx2;
uniform float _alpha_to;
uniform bool cmx2_use;
uniform mat4 cmx2_mul;
uniform vec4 cmx2_add;

vec4 get_pixel_from(vec2 tx_coord)
{
	vec4 col;
	if (!r_s && ((tx_coord.s<0.0) || (tx_coord.s>1.0))) {
		col = pad_color;
	} else if (!r_t && ((tx_coord.t<0.0) || (tx_coord.t>1.0))) {
		col = pad_color;
	} else {
		vec2 tx = tx_coord;
		while (tx.s<0.0) tx.s+= 1.0;
		tx.s = mod(tx.s, 1.0);
		while (tx.t<0.0) tx.t+= 1.0;
		tx.t = mod(tx.t, 1.0);

		col = texture2D(maintx1, tx);
	}
	if (cmx1_use) {
		col = cmx1_mul * col;
		col += cmx1_add;
		col = clamp(col, 0.0, 1.0);
	}
	col.a *= _alpha_from;
	return col;
}

vec4 get_pixel_to(vec2 tx_coord)
{
	vec4 col;
	if (!r_s && ((tx_coord.s<0.0) || (tx_coord.s>1.0))) {
		col = pad_color;
	} else if (!r_t && ((tx_coord.t<0.0) || (tx_coord.t>1.0))) {
		col = pad_color;
	} else {
		vec2 tx = tx_coord;
		while (tx.s<0.0) tx.s+= 1.0;
		tx.s = mod(tx.s, 1.0);
		while (tx.t<0.0) tx.t+= 1.0;
		tx.t = mod(tx.t, 1.0);

		col = texture2D(maintx2, tx);
	}
	if (cmx2_use) {
		col = cmx2_mul * col;
		col += cmx2_add;
		col = clamp(col, 0.0, 1.0);
	}
	col.a *= _alpha_to;
	return col;
}

`;

const fs_single_in_op_source_prefix = `
uniform sampler2D mask_tx;
uniform int mask_mode;
uniform float mask_w, mask_h;

varying vec2 txcoord_from;
uniform sampler2D maintx1;
uniform float _alpha_from;
uniform bool cmx1_use;
uniform mat4 cmx1_mul;
uniform vec4 cmx1_add;

uniform vec4 pad_color;
uniform bool r_s, r_t;
uniform float ratio;

vec4 get_pixel_from(vec2 tx_coord)
{
	vec4 col;
	if (!r_s && ((tx_coord.s<0.0) || (tx_coord.s>1.0))) {
		col = pad_color;
	} else if (!r_t && ((tx_coord.t<0.0) || (tx_coord.t>1.0))) {
		col = pad_color;
	} else {
		vec2 tx = tx_coord;
		while (tx.s<0.0) tx.s+= 1.0;
		tx.s = mod(tx.s, 1.0);
		while (tx.t<0.0) tx.t+= 1.0;
		tx.t = mod(tx.t, 1.0);

		col = texture2D(maintx1, tx);
	}
	if (cmx1_use) {
		col = cmx1_mul * col;
		col += cmx1_add;
		col = clamp(col, 0.0, 1.0);
	}
	col.a *= _alpha_from;
	return col;
}

`;


function canvas_draw_sources(path)
{
	let scene = active_scene;
	let pids = scene.mod.pids;
	let op_type = 0;
	let op_param = 0;
	let op_tx = null;
	if (arguments.length == 4) {
		op_type = arguments[1];
		op_param = arguments[2];
		op_tx = arguments[3];
	}


	if (pids.length==0) {
		print(GF_LOG_ERROR, 'Broken scene ' + scene.id + ': call to draw_sources without any source pid !');
		scene.active=false;
		return;
	}

	if (pids[0].pid.source.no_signal) {
		draw_scene_no_signal();
		return;
	}
	if ((pids.length==1) && !op_type) {
		let tx = pids[0].texture;
		//should not happen
		if (!tx) return;

		canvas_draw(path, tx);
	  return;
	}

	if (op_type) {
	  if ((op_type == GF_EVG_OPERAND_MIX_DYN) || (op_type == GF_EVG_OPERAND_MIX_DYN_ALPHA)) {
			if (pids.length<=1) {
				print(GF_LOG_WARNING, 'Canvas multitexture fill with 3 textures but only 2 textures provided');
				canvas_draw(path, pids[0].texture);
				return;
			}
		}

		if (!use_gpu) {
			let mx = op_tx.mx;

			canvas_set_matrix(canvas, active_scene.mx);
		  canvas.path = path;
		  if ((op_type == GF_EVG_OPERAND_MIX_DYN) || (op_type == GF_EVG_OPERAND_MIX_DYN_ALPHA)) {
			  canvas.fill(op_type, [op_param], pids[0].texture, pids[1].texture, op_tx);
		  } else {
			  canvas.fill(op_type, [op_param], pids[0].texture, op_tx);
		  }
			return;
		}
	}

	if (pids[0].pid && pids[0].pid.source.sequence.transition_state==4) {
		print(GF_LOG_DEBUG, 'transition in error, using simple draw');
		canvas_draw(path, pids[0].texture);
		return;
	}

	if (!op_type && pids[1].pid && pids[1].pid.source.no_signal) {
		draw_scene_no_signal();
		return;
	}

	let transition = null;
	let seq = null;
	let ratio = 0;

	if (pids[0].pid.source.sequence.transition_state==3) {
		seq = pids[0].pid.source.sequence;
		transition = seq.transition;

		let time = video_time - seq.transition_start;
		//in sec
		time /= video_timescale;

		//as fraction
		ratio = time / seq.transition_dur;
		if (ratio>1) ratio=1;
	} else if (scene.transition) {
		let skip_tansition = !use_gpu;
		transition = scene.transition;
		ratio = scene.mod.mix_ratio;
		if (ratio<0) ratio = 0;
		else if (ratio>1) ratio = 1;

		if (pids[0].pid) pids[0].pid.source.mix_volume = 1-ratio;
		if (pids[1].pid) pids[1].pid.source.mix_volume = ratio;

		if (scene.transition_state==4) {
			if (ratio>0.5) ratio = 1;
			else ratio = 0;
			skip_tansition = true;
		}

		//for gpu we must setup the program and use it
		if (skip_tansition) {
			if (ratio==0) {
				canvas_draw(path, pids[0].texture);
				return;
			}
			if (ratio==1) {
				canvas_draw(path, pids[1].texture);
				return;
			}
		}
	}

	if (!op_type && !transition) {
		canvas_draw(path, pids[0].texture);
		return;
	}

	if (transition && transition.fun) {
		let old_ratio = ratio;
		try {
			ratio = transition.fun.apply(transition, [ratio]);
			if (typeof ratio != 'number')
				throw 'Ratio function result is not a number !';
		} catch (e) {
			print(GF_LOG_WARNING, 'Error processing transition fun ' + transition.fun + ': ' + e);
			transition.fun = null;
		}
		if (!old_ratio) ratio = 0;
		else if (!old_ratio==1) ratio = 1;
	}

	if (!use_gpu) {
		if (!pids[0].texture || !pids[1].texture) {
			canvas_draw(path, pids[0].texture);
			return;
		}
		canvas_set_matrix(canvas, active_scene.mx);

		transition.mod.apply(canvas, ratio, path, pids);
		return;
	}

	//gpu: create mesh, bind textures, call apply and draw mesh

	let use_mask = 0;
	if ((mask_mode==GF_EVGMASK_DRAW) || (mask_mode==GF_EVGMASK_RECORD)) {
	  mask_canvas.path = path;
	  canvas_set_matrix(mask_canvas, matrix);
	  mask_canvas.fill(pids[0].texture);
	  if (mask_mode==GF_EVGMASK_DRAW) return;
	  use_mask = 1;
	}
	else if (mask_mode==GF_EVGMASK_USE) {
	  use_mask = 1;
	} else if (mask_mode==GF_EVGMASK_USE_INV) {
	  use_mask = 2;
	}

	canvas_offscreen_deactivate();

	let mesh = path.mesh || null;
	if (!mesh) {
		mesh = path.mesh = new evg.Mesh(path);
		if (!path.mesh) return;
		path.mesh.update_gl();
	}

	//setup all textures
	let tx_slot = 0;
	let all_textures = [];
	pids.forEach (pid => {
			all_textures.push(pid.texture);
	});
	if (op_tx)
		all_textures.push(op_tx);

	all_textures.forEach (tx => {
		let texture = tx._gl_texture || null;

		webgl.activeTexture(webgl.TEXTURE0 + tx_slot);

		if (!texture) {
			tx._gl_texture = webgl.createTexture(null);
			if (!tx._gl_texture) return;
			tx._gl_tx_id = webgl.textureName(tx._gl_texture);

			if (transition && transition.gl_program) {
				print(GF_LOG_INFO, 'texture format changed, reloading GLSL program');
				transition.gl_program = null;
			}
			if (active_scene && active_scene.gl_program) {
				print(GF_LOG_INFO, 'texture format changed, reloading GLSL program');
				active_scene.gl_program = null;
			}
			tx._gl_modified = true;
		}
		//upload data if modified
		if (tx._gl_modified == true) {
			webgl.bindTexture(webgl.TEXTURE_2D, tx._gl_texture);
			tx._gl_texture.upload(tx);
			tx._gl_modified = false;
		}

		tx_slot += tx._gl_texture.nb_textures;
	});


	//check program is still OK
	let prog_info;
	if (transition) {
		prog_info = transition.gl_program || null;
	} else {
		prog_info = active_scene.gl_program || null;
	}

	//create program
	if (!prog_info) {
		let frag_source;
		let has_dual_in = false;
		let op_tx_reuse = 0;
		let tx_source = '';
		let vertex_src;

		if (transition) {
			tx_source = transition.mod.get_shader_src( pids[0].texture );
			if (typeof tx_source != 'string') {
				print(GF_LOG_ERROR, 'Aborting transition, invalid shader source ' + tx_source);
				if (seq)
					seq.transition_state = 4;
				else
					scene.transition_state = 4;
				return;
			}

			let first_uni = tx_source.indexOf('uniform');
			if (first_uni>0) {
				let head = tx_source.slice(0, first_uni);
				let tail = tx_source.slice(first_uni);
				frag_source = head + fs_trans_source_prefix + tail;
			} else {
				frag_source = fs_trans_source_prefix + tx_source;
			}
			has_dual_in = true;

			vertex_src = transition_vs_source;
		} else {
			  if ((op_type == GF_EVG_OPERAND_MIX_DYN) || (op_type == GF_EVG_OPERAND_MIX_DYN_ALPHA)) {
					has_dual_in = true;
					frag_source = fs_trans_source_prefix;
					vertex_src = dual_in_op_vs_source;
				} else {
					frag_source = fs_single_in_op_source_prefix;
					vertex_src = single_in_op_vs_source;
				}
		}

		//replace maintx1 by first texture name
		frag_source = frag_source.replaceAll('maintx1', pids[0].texture._gl_tx_id);

		//replace maintx2 by second texture name
		if (has_dual_in)
			frag_source = frag_source.replaceAll('maintx2', pids[1].texture._gl_tx_id);


		//replace op
		if (!transition) {
			if (op_tx._gl_tx_id == pids[0].texture._gl_tx_id) {
				op_tx_reuse = 1;
				frag_source += `
vec4 gf_apply_effect()
{
	vec4 col_1 = get_pixel_from(txcoord_from);
	vec4 col_op = col_1;
`;

			} else if (has_dual_in && (op_tx._gl_tx_id == pids[1].texture._gl_tx_id)) {
				op_tx_reuse = 2;
				frag_source += `
vec4 gf_apply_effect()
{
	vec4 col_1 = get_pixel_from(txcoord_from);
`;

			} else {
				frag_source += `
varying vec2 txcoord_op;
uniform sampler2D optx;
uniform float _alpha_op;
uniform bool r_s_op, r_t_op;
uniform vec4 pad_color_op;

uniform bool cmx_op_use;
uniform mat4 cmx_op_mul;
uniform vec4 cmx_op_add;

vec4 get_pixel_op(vec2 tx_coord)
{
	vec4 col;
	if (!r_s_op && ((tx_coord.s<0.0) || (tx_coord.s>1.0))) {
		col = pad_color_op;
	} else if (!r_t_op && ((tx_coord.t<0.0) || (tx_coord.t>1.0))) {
		col = pad_color_op;
	} else {
		vec2 tx = tx_coord;
		while (tx.s<0.0) tx.s+= 1.0;
		tx.s = mod(tx.s, 1.0);
		while (tx.t<0.0) tx.t+= 1.0;
		tx.t = mod(tx.t, 1.0);

		col = texture2D(optx, tx);
	}
	if (cmx_op_use) {
		col = cmx_op_mul * col;
		col += cmx_op_add;
		col = clamp(col, 0.0, 1.0);
	}
	col.a *= _alpha_op;
	return col;
}

vec4 gf_apply_effect()
{
	vec4 col_1 = get_pixel_from(txcoord_from);
	vec4 col_op = get_pixel_op(txcoord_op);
`;
				frag_source = frag_source.replaceAll('optx', op_tx._gl_tx_id);
			}

			if (has_dual_in) {
				frag_source += `	vec4 col_2 = get_pixel_to(txcoord_to);
`;
				if (op_tx_reuse == 2) {
					frag_source += `	vec4 col_op = col_2;
`;
				}
			}

			if ((op_type == GF_EVG_OPERAND_REPLACE_ALPHA) || (op_type == GF_EVG_OPERAND_REPLACE_ONE_MINUS_ALPHA)
				|| (op_type == GF_EVG_OPERAND_MIX_DYN) || (op_type == GF_EVG_OPERAND_MIX_DYN_ALPHA)
			) {
				let rep = 'a';
				if (op_param >= 3) rep = 'b';
				else if (op_param >= 2) rep = 'g';
				else if (op_param >= 1) rep = 'r';

				if (op_type == GF_EVG_OPERAND_REPLACE_ALPHA)
					frag_source += `	col_1.a = col_op.`+rep+`;`;
				else if (op_type == GF_EVG_OPERAND_REPLACE_ONE_MINUS_ALPHA)
					frag_source += `	col_1.a = 1.0 - col_op.`+rep+`;`;
				else if (op_type == GF_EVG_OPERAND_MIX_DYN)
					frag_source += `	col_1 = mix(col_1, col_2, col_op.`+rep+`); col_1.a = 1.0;`;
				else //(op_type == GF_EVG_OPERAND_MIX_DYN)
					frag_source += `	col_1 = mix(col_1, col_2, col_op.`+rep+`);`;
			} else if (op_type == GF_EVG_OPERAND_MIX) {
				frag_source += `	col_1 = mix(col_1, col_op, ratio); col_1.a = 1.0;`;
			} else if (op_type == GF_EVG_OPERAND_MIX_ALPHA) {
				frag_source += `	col_1 = mix(col_1, col_op, ratio);`;
			}

			frag_source += `
	return col_1;
}
`;
		}

		frag_source += `
void main() {
	if (mask_mode>0) {
		vec2 mask_uv = vec2(gl_FragCoord.x/mask_w, 1.0 - gl_FragCoord.t/mask_h);
		vec4 mask = texture2D(mask_tx, mask_uv);
		vec4 col = gf_apply_effect();

		if (mask_mode>1)
			col.a *= (1.0-mask.r);
		else
			col.a *= mask.r;

	  gl_FragColor = col;
	} else {
		gl_FragColor = gf_apply_effect();
	}
}
`

		const shaderProgram = webgl_init_shaders(vertex_src, frag_source, null);
		if (!shaderProgram) {
			print(GF_LOG_ERROR, 'shader creation failed, aborting transition');
			if (seq)
				seq.transition_state = 4;
			else
				scene.transition_state = 4;
			return;
		}

		prog_info = {
			program: shaderProgram,
			vertexPosition: webgl.getAttribLocation(shaderProgram, 'aVertexPosition'),
			textureCoord: webgl.getAttribLocation(shaderProgram, 'aTextureCoord'),
			projectionMatrix: webgl.getUniformLocation(shaderProgram, 'uProjectionMatrix'),
			modelViewMatrix: webgl.getUniformLocation(shaderProgram, 'uModelViewMatrix'),
			mask_mode: webgl.getUniformLocation(shaderProgram, 'mask_mode'),
			mask_tx: webgl.getUniformLocation(shaderProgram, 'mask_tx'),
			mask_w: webgl.getUniformLocation(shaderProgram, 'mask_w'),
			mask_h: webgl.getUniformLocation(shaderProgram, 'mask_h'),
			ratio: webgl.getUniformLocation(shaderProgram, 'ratio'),
			//used by gl-transitions
			video_ar: webgl.getUniformLocation(shaderProgram, 'video_ar'),
			textures: [
				{
					sampler: webgl.getUniformLocation(shaderProgram, pids[0].texture._gl_tx_id),
			    matrix: webgl.getUniformLocation(shaderProgram, 'textureMatrixFrom'),
			    alpha: webgl.getUniformLocation(shaderProgram, '_alpha_from'),
					cmx_use: webgl.getUniformLocation(shaderProgram, 'cmx1_use'),
					cmx_mul: webgl.getUniformLocation(shaderProgram, 'cmx1_mul'),
					cmx_add: webgl.getUniformLocation(shaderProgram, 'cmx1_add'),
					r_s: webgl.getUniformLocation(shaderProgram, 'r_s'),
					r_t: webgl.getUniformLocation(shaderProgram, 'r_t'),
					pad_color: webgl.getUniformLocation(shaderProgram, 'pad_color'),
				}
				]
		};

		if (has_dual_in) {
			prog_info.textures.push(
				{
					sampler: webgl.getUniformLocation(shaderProgram, pids[1].texture._gl_tx_id),
			    matrix: webgl.getUniformLocation(shaderProgram, 'textureMatrixTo'),
			    alpha: webgl.getUniformLocation(shaderProgram, '_alpha_to'),
					cmx_use: webgl.getUniformLocation(shaderProgram, 'cmx2_use'),
					cmx_mul: webgl.getUniformLocation(shaderProgram, 'cmx2_mul'),
					cmx_add: webgl.getUniformLocation(shaderProgram, 'cmx2_add'),
					r_s: null,
					r_t: null,
					pad_color: null,
				} );
		}

		prog_info.op_texture = null;
		if (!transition && op_tx) {
				if (op_tx_reuse) {
					prog_info.op_texture = null;
				} else {
					prog_info.op_texture = {
						sampler: webgl.getUniformLocation(shaderProgram, op_tx._gl_tx_id),
				    matrix: webgl.getUniformLocation(shaderProgram, 'textureMatrixOp'),
				    alpha: webgl.getUniformLocation(shaderProgram, '_alpha_op'),
						cmx_use: webgl.getUniformLocation(shaderProgram, 'cmx_op_use'),
						cmx_mul: webgl.getUniformLocation(shaderProgram, 'cmx_op_mul'),
						cmx_add: webgl.getUniformLocation(shaderProgram, 'cmx_op_add'),
				    r_s: webgl.getUniformLocation(shaderProgram, 'r_s_op'),
				    r_t: webgl.getUniformLocation(shaderProgram, 'r_t_op'),
				    pad_color: webgl.getUniformLocation(shaderProgram, 'pad_color_op'),
					};
				}
		}


		if (transition)
			transition.gl_program = prog_info;
		else
			active_scene.gl_program = prog_info;

		webgl.useProgram(prog_info.program);
		webgl.uniformMatrix4fv(prog_info.projectionMatrix, false, defaultOrthoProjectionMatrix.m);
	  webgl.uniform1f(prog_info.mask_w, video_width);
	  webgl.uniform1f(prog_info.mask_h, video_height);

		if (transition) {
			let nb_in_tx = has_dual_in ? 2 : 1;
			tx_slot = 0;
			for (let i=0; i<nb_in_tx; i++) {
				let pid = pids[i];
				tx_slot += pid.texture._gl_texture.nb_textures;
			}
			transition.mod.setup_gl(webgl, shaderProgram, prog_info.ratio, tx_slot);
		}

		if (prog_info.video_ar)
			webgl.uniform1f(prog_info.video_ar, pids[0].texture.width / pids[0].texture.height);

		active_scene.gl_uniforms_reload = true;
	}

	webgl.useProgram(prog_info.program);
	webgl.viewport(0, 0, video_width, video_height);

	let nb_in_tx = (transition || (prog_info.textures.length==2)) ? 2 : 1;

  if (wgl_clipper) {
		webgl.enable(webgl.SCISSOR_TEST);
		webgl.scissor(video_width/2 + wgl_clipper.x, wgl_clipper.y - wgl_clipper.h + video_height/2, wgl_clipper.w, wgl_clipper.h);
  } else {
		webgl.disable(webgl.SCISSOR_TEST);
  }

	//set transform
	canvas_set_matrix(null, active_scene.mx, prog_info);

	if (!transition) ratio = active_scene.mod.mix_ratio;

	webgl.uniform1f(prog_info.ratio, ratio);

	tx_slot = 0;
	//set alpha and texture transforms per pid - 2 max
	for (let i=0; i<nb_in_tx; i++) {
		let pid = pids[i];

		if (active_scene.gl_uniforms_reload) {
			push_texture_uniforms(pid.texture, prog_info.textures[i]);
	  }

		webgl.activeTexture(webgl.TEXTURE0 + tx_slot);
		//and bind our named texture (this will setup active texture slots)
		webgl.bindTexture(webgl.TEXTURE_2D, pid.texture._gl_texture);
		webgl.uniform1i(prog_info.textures[i].sampler, tx_slot);

		tx_slot += pid.texture._gl_texture.nb_textures;
	}

	//set op texture
	if (op_tx && prog_info.op_texture) {
		if (active_scene.gl_uniforms_reload) {
			push_texture_uniforms(op_tx, prog_info.op_texture);
		}

		webgl.activeTexture(webgl.TEXTURE0 + tx_slot);
		//and bind our named texture (this will setup active texture slots)
		webgl.bindTexture(webgl.TEXTURE_2D, op_tx._gl_texture);
		webgl.uniform1i(prog_info.op_texture.sampler, tx_slot);

		tx_slot += op_tx._gl_texture.nb_textures;
	}

  //set mask
  webgl.uniform1i(prog_info.mask_mode, use_mask);
	if (use_mask) {
	  webgl.activeTexture(webgl.TEXTURE0 + tx_slot);
		webgl.bindTexture(webgl.TEXTURE_2D, mask_texture);
		//not a named texture
		webgl.texSubImage2D(webgl.TEXTURE_2D, 0, 0, 0, video_width, video_height,webgl.LUMINANCE, webgl.UNSIGNED_BYTE, mask_canvas_data);
		webgl.uniform1i(prog_info.mask_tx, tx_slot);
	}

	//apply transition, ie set uniforms
	if (transition) {
		transition.mod.apply(webgl, ratio, path, pids);
	}

	mesh.draw(webgl, prog_info.vertexPosition, prog_info.textureCoord);
	webgl.useProgram(null);
}


function canvas_blit(stencil, dst_rc)
{
	if (webgl) {
			print(GF_LOG_ERROR, 'Broken scene ' + active_scene.id + ': call to blit in GPU context');
			active_scene.active=false;
			return;
	}
  canvas.blit(stencil, dst_rc, null);
}



const vs_source = `
attribute vec4 aVertexPosition;
attribute vec2 aTextureCoord;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
uniform mat4 textureMatrix;
varying vec2 vTextureCoord;

void main() {
  gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
	vTextureCoord = vec2(textureMatrix * vec4(aTextureCoord.x, aTextureCoord.y, 0, 1) );
}
`;

const fs_source = `
uniform sampler2D mask_tx;
uniform int mask_mode;
uniform float mask_w, mask_h;

uniform bool r_s, r_t;
uniform vec4 pad_color;
varying vec2 vTextureCoord;
uniform sampler2D maintx;
uniform float alpha;
uniform bool cmx_use;
uniform mat4 cmx_mul;
uniform vec4 cmx_add;
void main(void) {

	vec4 col;
	if (!r_s && ((vTextureCoord.s<0.0) || (vTextureCoord.s>1.0))) {
		col = pad_color;
	} else if (!r_t && ((vTextureCoord.t<0.0) || (vTextureCoord.t>1.0))) {
		col = pad_color;
	} else {
		vec2 tx = vTextureCoord;
		while (tx.s<0.0) tx.s+= 1.0;
		tx.s = mod(tx.s, 1.0);
		while (tx.t<0.0) tx.t+= 1.0;
		tx.t = mod(tx.t, 1.0);

		col = texture2D(maintx, tx);
	}
	if (cmx_use) {
		col = cmx_mul * col;
		col += cmx_add;
		col = clamp(col, 0.0, 1.0);
	}
	col.a *= alpha;

	if (mask_mode>0) {
		vec2 mask_uv = vec2(gl_FragCoord.x/mask_w, 1.0 - gl_FragCoord.t/mask_h);
		vec4 mask = texture2D(mask_tx, mask_uv);
		float m;
		if (mask_mode>1)
			col.a *= (1.0-mask.r);
		else
			col.a *= mask.r;
	}
  gl_FragColor = col;
}
`;


const vs_const_col = `
attribute vec4 aVertexPosition;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;

void main() {
  gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
}
`;


const fs_const_col = `
uniform sampler2D mask_tx;
uniform int mask_mode;
uniform float mask_w, mask_h;

uniform vec4 color;
void main(void) {

	if (mask_mode>0) {
		vec4 col = color;
		vec2 mask_uv = vec2(gl_FragCoord.x/mask_w, 1.0 - gl_FragCoord.t/mask_h);
		vec4 mask = texture2D(mask_tx, mask_uv);
		if (mask_mode>1)
			col.a *= (1.0-mask.r);
		else
			col.a *= mask.r;

	  gl_FragColor = col;
	} else {
	  gl_FragColor = color;
	}
}
`;


function webgl_load_shader(type, source, tx_name) {
  const shader = webgl.createShader(type);

  if (tx_name) {
  	source = source.replaceAll('maintx', tx_name);
  }
  webgl.shaderSource(shader, source);
  webgl.compileShader(shader);
  if (!webgl.getShaderParameter(shader, webgl.COMPILE_STATUS)) {
    print(GF_LOG_ERROR, 'An error occurred compiling the shaders ' + type + ' : ' + webgl.getShaderInfoLog(shader));
    print(GF_LOG_WARNING, 'Shader was \n' + source);
    webgl.deleteShader(shader);
    return null;
  }
  return shader;
}

function webgl_init_shaders(vsSource, fsSource, tx_name) {
  const vertexShader = webgl_load_shader(webgl.VERTEX_SHADER, vsSource, null);
  const fragmentShader = webgl_load_shader(webgl.FRAGMENT_SHADER, fsSource, tx_name);

  if (!vertexShader || !fragmentShader) return null;

  const shaderProgram = webgl.createProgram();
  webgl.attachShader(shaderProgram, vertexShader);
  webgl.attachShader(shaderProgram, fragmentShader);
  webgl.linkProgram(shaderProgram);

  if (!webgl.getProgramParameter(shaderProgram, webgl.LINK_STATUS)) {
    print(GF_LOG_ERROR, 'Unable to initialize the shader program: ' + webgl.getProgramInfoLog(shaderProgram));
    return null;
  }
  return shaderProgram;
}


function setup_webgl_program(vsSource, fsSource, tx_name)
{
  const shaderProgram = webgl_init_shaders(vsSource, fsSource, tx_name);
  let prog = {
    program: shaderProgram,
    attribLocations: {
      vertexPosition: webgl.getAttribLocation(shaderProgram, 'aVertexPosition'),
    },
    projectionMatrix: webgl.getUniformLocation(shaderProgram, 'uProjectionMatrix'),
    modelViewMatrix: webgl.getUniformLocation(shaderProgram, 'uModelViewMatrix'),
    mask_mode: webgl.getUniformLocation(shaderProgram, 'mask_mode'),
    mask_w: webgl.getUniformLocation(shaderProgram, 'mask_w'),
    mask_h: webgl.getUniformLocation(shaderProgram, 'mask_h'),
		mask_tx: webgl.getUniformLocation(shaderProgram, "mask_tx")
  };

  if (tx_name) {
  	prog.attribLocations.textureCoord = webgl.getAttribLocation(shaderProgram, 'aTextureCoord');
  	prog.textures = [
			{
				sampler: webgl.getUniformLocation(shaderProgram, tx_name),
				matrix: webgl.getUniformLocation(shaderProgram, 'textureMatrix'),
				alpha: webgl.getUniformLocation(shaderProgram, 'alpha'),
				r_s: webgl.getUniformLocation(shaderProgram, 'r_s'),
				r_t: webgl.getUniformLocation(shaderProgram, 'r_t'),
				pad_color: webgl.getUniformLocation(shaderProgram, 'pad_color'),
				cmx_use: webgl.getUniformLocation(shaderProgram, 'cmx_use'),
				cmx_mul: webgl.getUniformLocation(shaderProgram, 'cmx_mul'),
				cmx_add: webgl.getUniformLocation(shaderProgram, 'cmx_add'),
			}
		];
  } else {
    prog.color = webgl.getUniformLocation(shaderProgram, 'color');
  }

  //load default texture matrix (identity)
  webgl.useProgram(shaderProgram);
	let ident = new evg.Matrix();
	webgl.uniformMatrix4fv(prog.modelViewMatrix, false, ident.m);
  webgl.uniformMatrix4fv(prog.projectionMatrix, false, defaultOrthoProjectionMatrix.m);
  webgl.uniform1i(prog.mask_mode, 0);
  if (tx_name) {
	  webgl.uniformMatrix4fv(prog.textures[0].matrix, false, ident.m);
	  webgl.uniform1f(prog.textures[0].alpha, 1.0);
	  webgl.uniform1i(prog.textures[0].cmx_use, 0);
	}
  webgl.useProgram(null);

  return prog;
}


function apply_transition_options(seq, opts, strict)
{
	//parse all params
	opts.forEach(o => {
		seq.transition.mod[o.name] = o.value;
		let typ = typeof seq.transition_effect[o.name];
		if (typ != 'undefined') {
			if (typ == typeof o.value) {
				if (strict && Array.isArray(o.value) ) {
					if (o.value.length == seq.transition_effect[o.name].length) {
						seq.transition.mod[o.name] = seq.transition_effect[o.name];
					} else {
						print(GF_LOG_WARNING, 'Array length mismatch for transition ' + seq.transition.type + '.' + o.name + ': got ' + seq.transition_effect[o.name].length + ', expecting ' + o.value.length + ', ignoring');
					}
				} else {
					seq.transition.mod[o.name] = seq.transition_effect[o.name];
				}
			} else {
				print(GF_LOG_WARNING, 'Type mismatch for transition ' + seq.transition.type + '.' + o.name + ': got ' + typ + ', expecting ' + typeof o.value + ', ignoring');
			}
		}
	});
}

function setup_transition(seq)
{
	/*builtin parameters*/
	seq.transition.fun = (typeof seq.transition_effect.fun == 'string') ? fn_from_script(['ratio'], seq.transition_effect.fun) : null;
	seq.transition.id = (typeof seq.transition_effect.id == 'string') ? seq.transition_effect.id : null;

	seq.transition.mod.update_flag = 0;
	//parse all params
	apply_transition_options(seq, seq.transition.options, false);

	//parse all per-instance params
	if (Array.isArray(seq.transition.mod.options)) {
		apply_transition_options(seq, seq.transition.mod.options, true);
	}

	for (var propertyName in seq.transition_effect) {
		if (propertyName == 'type') continue;
		if (propertyName == 'dur') continue;
		if (propertyName == 'fun') continue;
		if (propertyName == 'id') continue;
		if (propertyName.charAt(0) == '_') continue;
		if (typeof seq.transition.mod[propertyName] != 'undefined') continue;

		print(GF_LOG_WARNING, 'Unrecognized transition option ' + propertyName + ' for ' + seq.transition.type);
	}
	seq.transition.gl_program = null;
	if (!use_gpu)
		seq.transition.mod.setup();
}

function default_transition(canvas, ratio, path, pids)
{
		canvas_draw(path, pids[0].texture);
}

function load_transition(seq)
{
	let type = seq.transition_effect.type || null;
	if (!type) return false;

	let script_src = sys.url_cat(playlist_url, type);
	if (! sys.file_exists(script_src)) {
		script_src = filter.jspath + 'transitions/' + type + '.js';

		if (! sys.file_exists(script_src)) {
			print(GF_LOG_ERROR, 'No such transition effect ' + script_src);
			return false;
		}
	}

	if (seq.transition && (seq.transition.type == type)) {
		setup_transition(seq);
		return true;
	}


	modules_pending ++;
	seq.transition = {};
	seq.transition.type = type;

	import(script_src)
		  .then(obj => {
		  		modules_pending--;
		  		print(GF_LOG_DEBUG, 'Module ' + script_src + ' loaded');
					seq.transition.mod = obj.load(seq.transition_effect);

		  		let has_gl = 0;

		  		//can do gl
		  		if (typeof seq.transition.mod.get_shader_src == 'function') {
		  			has_gl = 1;
			  		if (typeof seq.transition.mod.setup != 'function')
			  			has_gl = 2;
		  		}

		  		if (use_gpu) {
		  			if (!has_gl) {
		  				seq.transition_state = 4;
		  				print(GF_LOG_WARNING, 'Transition ' + type + ' cannot run in GPU mode, disabling');
		  				return;
		  			}
			  		if (typeof seq.transition.mod.setup_gl != 'function')
			  				seq.transition.mod.setup_gl = function () {};
		  		} else {
		  			if (has_gl==2) {
		  				seq.transition_state = 4;
		  				print(GF_LOG_WARNING, 'Transition ' + type + ' cannot run in non-GPU mode, disabling');
		  				return;
			  		} else if (typeof seq.transition.mod.setup != 'function') {
				  		print(GF_LOG_ERROR, "Missing setup function for transition '" + type + "', disabling transition");
				  		seq.transition_state = 4;
				  		return;
			  		}
		  		}

		  		if (typeof seq.transition.mod.apply != 'function')
		  				seq.transition.mod.apply = default_transition;

		  		seq.transition.options = obj.options || null;
		  		setup_transition(seq);
		  })
		  .catch(err => {
		  		modules_pending--;
		  		print(GF_LOG_ERROR, "Failed to load transition '" + type + "': " + err);
		  		seq.transition_state = 4;
		  });

	return true;
}


function get_media_time()
{
	let media_time = -4;
	if ((arguments.length == 1) && arguments[0]) {
		let s = get_source(arguments[0], null, null);
		if (!s) return -4;
		if (!s.playing) return -3;
		if (s.in_prefetch) return -1;
		if (!s.timeline_init) return -2;

		media_time = s.pids[0].pck.cts / s.pids[0].timescale;
	} else if (video_playing) {
		media_time = video_time / video_timescale;
	} else {
		media_time = audio_time / audio_timescale;
	}
	return media_time;
}

function resolve_url(url)
{
	return sys.url_cat(playlist_url, url);
}

function apply_transition_update(transition, prop_name, value)
{
	if ((prop_name=='dur')) {
		if (typeof value == 'number') {
			transition.dur = value;
		} else {
			print(GF_LOG_ERROR, 'Wrong type ' + typeof value + ' for transition.dur');
			return false;
		}
	}
	else if ((prop_name=='type')) {
		if (typeof value == 'string') {
			transition.type = value;
		} else {
			print(GF_LOG_ERROR, 'Wrong type ' + typeof value + ' for transition.type');
			return false;
		}
	} else {
		//blindly updat the transition
		transition[prop_name] = value;
	}
	return true;
}

function check_prop_type(orig, final, allow_string)
{
	if (orig == final) return true;
	if (! allow_string) return false;
	if ((final != 'number') && (final != 'string')) return false;
	if ((orig != 'number') && (orig != 'string')) return false;
	return true;
}

function parse_update_elem(pl, array)
{
	if (pl.skip || false)
		return true;

	if (typeof pl.replace != 'string') {
		print(GF_LOG_WARNING, "Invalid replace command " + JSON.stringify(pl));
		return false;
	}
	if (typeof pl.with == 'undefined') {
		print(GF_LOG_WARNING, "Invalid replace command " + JSON.stringify(pl));
		return false;
	}
	let src = pl.replace.split('@');
	if (src.length != 2) {
		print(GF_LOG_WARNING, "Invalid replace syntax " + src + ', expecting \`ID@name\`');
		return false;
	}
	let prop_name = src[1];
	src = src[0];
	let field_idx=-1;

	if (prop_name.indexOf('[')>0) {
		let vals = prop_name.split('[');
		prop_name = vals[0];
		let idx = vals[1].split(']');
		field_idx = parseInt(idx[0]);
	}

	if (prop_name=='id') {
		print(GF_LOG_WARNING, "ID property cannot be updated");
		return false;
	}

	//locate scene, transition or group
	let scene = get_scene(src);
	let transition = scene ? null : get_transition(src, false);
	let group = (scene || transition) ? null : get_group(src);
	if (scene || transition || group) {
		let mod = scene ? scene.mod : (transition ? transition.mod : group);

		//if transition we must modify the source (transition effect) since this is the object being reloaded
		//do this even when the transition is active
		if (transition) {
			apply_transition_update(get_transition(src, true), prop_name, pl.with);
		}

		let update_type = -2;

		let logname='';
		if (scene) {

			if (prop_name == "sources") {
				update_type = UPDATE_PID;
				mod = scene;
			} else {
				//module options
				update_type = scene_mod_option_update_time(scene, prop_name);
			}

			//transformation options
			if (update_type==-2) {
				update_type = group_get_update_type(prop_name, true);
				mod = scene;
			}

			if (update_type==-2) {
				//scene options, not module ones
				update_type = scene_get_update_type(prop_name);
				if (update_type>=0) {
					if (!scene_is_mod_option(prop_name))
						mod = scene;
				}
			}
			logname = 'scene ' + src;
		} else if (group) {
			update_type = group_get_update_type(prop_name, false);
			logname = 'group ' + src;
		}
		//transition updates use a single invalidate flag
		else {
			//for now we allow all modifications (they are evaluated when reloading the transition)
			update_type = UPDATE_SIZE;
			logname = 'transition ' + src;
		}

		if (update_type==-2) {
			print(GF_LOG_ERROR, 'No property ' + prop_name + ' in ' + logname + ', ignoring');
			return false;
		}
		if (update_type==-2) {
			print(GF_LOG_ERROR, 'Property ' + prop_name + ' in ' + logname + ' cannot be updated');
			return false;
		}

		let prop_type = typeof mod[prop_name];
		if (prop_type == 'undefined') {
			if (!transition) {
				print(GF_LOG_WARNING, 'No property ' + prop_name + ' in scene ' + src);
			}
			return false;
		}

		let res = pl.with;
		let allow_string = (update_type & UPDATE_ALLOW_STRING) ? true : false;
		update_type &= ~UPDATE_ALLOW_STRING;

		if (Array.isArray(mod[prop_name]) && (field_idx>=0)) {
			if (field_idx >= mod[prop_name].length) {
				print(GF_LOG_WARNING, 'Field index ' + field_idx + ' greater than number of elements ' + mod[prop_name].length + ' in property ' + prop_name);
				return;
			} else if (typeof res == typeof mod[prop_name][0]) {
				if (mod[prop_name][field_idx] != res) {
					if (update_type==UPDATE_PID) {
						mod[prop_name][field_idx] = res;
						setup_scene(scene, mod[prop_name], null);
					} else {
						mod.update_flag |= update_type;
						mod[prop_name][field_idx] = res;
						if (update_type && (scene||group))
							invalidate_parent(scene || group);
					}
				}
				trigger_watcher(scene || transition || group , prop_name, mod[prop_name][field_idx]);
			} else {
				print(GF_LOG_WARNING, 'Property ' + prop_name + ' type is ' + (typeof mod[prop_name][0]) + ' but replacement value type is ' + rep_type);
				return false;
			}
		} else {
			let res_type = typeof res;
			if (!check_prop_type(prop_type, res_type, allow_string)) {
				print(GF_LOG_ERROR, 'Property ' + prop_name + ' type is ' + prop_type + ' but replacement value type is ' + res_type + ', ignoring');
				return false;
			}

			if (mod[prop_name] != res) {
				if (update_type==UPDATE_PID) {
					setup_scene(scene, res, null);
				} else {
					mod.update_flag |= update_type;
					mod[prop_name] = res;
					if (update_type && (scene||group))
						invalidate_parent(scene || group);
				}
				trigger_watcher(scene || transition || group, prop_name, res);
			}
		}
		return true;
	}


	//locate timer
	let timer = get_timer(src);
	if (timer) {
		if (prop_name == 'start') {
			if (timer.active_state == 1) {
				print(GF_LOG_WARNING, 'Cannot modify start time of active timer');
				return false;
			}
			timer.start_time = parse_date_time(pl.with, false);
			timer.active_state = 0;
			timer.stop_time = 0;
		}
		else if (prop_name == 'stop') {
			timer.stop_time = parse_date_time(pl.with, false);
		} else if (prop_name == 'loop') {
			if (typeof pl.with == 'boolean') {
				timer.loop = pl.with ? -1 : 0;
			} else if (typeof pl.with == 'number') {
				timer.loop = pl.with;
			} else {
				print(GF_LOG_WARNING, 'Wrong type ' + (typeof pl.with) + ' for timer.loop');
				return false;
			}
		} else if (prop_name == 'dur') {
			if (typeof pl.with == 'number') {
				timer.duration = pl.with;
			} else {
				print(GF_LOG_WARNING, 'Wrong type ' + (typeof pl.with) + ' for timer.dur');
				return false;
			}
		} else if (prop_name == 'pause') {
			if (typeof pl.with == 'boolean') {
				timer.pause = pl.with;
			} else {
				print(GF_LOG_WARNING, 'Wrong type ' + (typeof pl.with) + ' for timer.pause');
				return false;
			}
		} else if (typeof timer[prop_name] != 'undefined') {
			print(GF_LOG_WARNING, 'Property ' + prop_name + ' of timer not updatable');
				return false;
		} else {
			print(GF_LOG_WARNING, 'Unknown timer property ' + prop_name);
				return false;
		}
		trigger_watcher(timer, prop_name, timer[prop_name]);
		return true;
	}

	//locate sequence
	let seq = get_sequence(src);
	if (seq) {
		if (prop_name == 'start') {
			if (seq.active_state==1) {
				print(GF_LOG_WARNING, 'Cannot modify start of active sequence');
				return false;
			} else {
				seq.start_time = parse_date_time(pl.with, true);
				//start is a date, don't skip range adjustment
				if ((typeof pl.with == 'string') && (pl.with.indexOf(':')>0)) {
					seq.skip_start_offset = false;
				}
				//start is not a date, source will start from its start range
				else {
					seq.skip_start_offset = true;
				}
				seq.active_state = 0;
				seq.stop_time = 0;
			}
		}
		else if (prop_name == 'stop') {
			seq.stop_time = parse_date_time(pl.with, false);
			if ((seq.stop_time<0) || (seq.stop_time<=seq.start_time))
				seq.stop_time = -1;
		} else if (prop_name == 'loop') {
			if (typeof pl.with == 'boolean') {
				seq.loop = pl.with ? -1 : 0;
			} else if (typeof pl.with == 'number') {
				seq.loop = pl.with;
			} else {
				print(GF_LOG_WARNING, 'Wrong type ' + (typeof pl.with) + ' for sequence.loop');
				return false;
			}
		} else if (prop_name == 'transition') {
			if (typeof pl.with == 'object') {
				seq.transition_effect = pl.with;
			} else {
				print(GF_LOG_WARNING, 'Wrong type ' + (typeof pl.with) + ' for sequence.transition');
				return false;
			}
		} else if (typeof seq[prop_name] != 'undefined') {
			print(GF_LOG_WARNING, 'Property ' + prop_name + ' of sequence not updatable');
			return false;
		} else {
			print(GF_LOG_WARNING, 'Unknown sequence property ' + prop_name);
			return false;
		}
		trigger_watcher(seq, prop_name, timer[prop_name]);
		return true;
	}
	//try inactive transition
	transition = get_transition(src, true);
	if (transition) {
		return apply_transition_update(transition, prop_name, pl.with);
	}

	//try scripts and watchers, only active property
	let script=get_script(src);
	let log_name='script';
	if (!script) {
		script=get_element_by_id(src, watchers);
		log_name='watcher';
	}
	if (script) {
		if (prop_name == 'active') {
			if (typeof pl.with == 'boolean') {
				script.active = pl.with;
			} else {
				print(GF_LOG_WARNING, 'Wrong type ' + (typeof pl.with) + ' for ' + log_name + '.active');
				return false;
			}
		} else {
			print(GF_LOG_WARNING, 'Property ' + prop_name + ' of ' + log_name + ' not updatable');
			return false;
		}
		trigger_watcher(script, prop_name, timer[prop_name]);
		return true;
	}

	//try styles and watchers, only active property
	let style = get_element_by_id(src, styles);
	if (style) {
		style[prop_name] = pl.with;
		style.updated = true;
		return true;
	}

	print(GF_LOG_WARNING, "No updatable element with id " + src + ' found');
	return false;
}

function load_updates()
{
	if (!filter.updates) return;

	let last_mtime = sys.mod_time(filter.updates);
	if (!last_mtime || (last_mtime == last_updates_modification))
		return;

	last_updates_modification = last_mtime;

	let f = sys.load_file(filter.updates, true);
	if (!f) return;

	let pl;
	try {
		pl = JSON.parse(f);
	} catch (e) {
		print(GF_LOG_WARNING, "Invalid JSON update playlist specified: " + e);
		return false;
	}

	print(GF_LOG_DEBUG, 'Playlist update is ' + JSON.stringify(pl) );

	if (Array.isArray(pl) ) {
		pl.forEach(parse_update_elem);
	} else {
		parse_update_elem(pl);
	}
}


function get_sequence_texture(seq_id)
{
	let seq = get_sequence(seq_id);
	if (!seq) return null;
	let active_src = null;
	seq.sources.forEach(s => {
			if (s.playing) active_src = s;
	});
	if (!active_src) return null;
	let active_pid = null;
	active_src.pids.forEach(p => {
		if ((p.type == TYPE_VIDEO) && p.pck) active_pid = p;
	});
	if (!active_pid) return null;

	if (!active_pid.texture) {
		create_pid_texture(active_pid);
	} else if (active_pid.frame_ts != active_pid.texture.last_frame_ts) {
		active_pid.texture.update(active_pid.pck);
	  active_pid.texture._gl_modified = true;
	  active_pid.texture.last_frame_ts = active_pid.frame_ts;
	 }
	return active_pid.texture;
}

function get_group_texture(group_id)
{
	let group = get_group(group_id);
	if (!group || (group.offscreen==GROUP_OST_NONE)) return null;
	if (!group.canvas_offscreen) {
		group_draw_offscreen(group);
	}
	return group.texture;
}

function get_screen_rect(path)
{
	if (!active_scene || !path) return null;
	if (active_scene.mx.is3D) return null;
	let rc = active_scene.mx.apply(path.bounds);

	rc.x += video_width/2;
	rc.y = video_height/2 - rc.y;

	rc.x = Math.floor(rc.x);
	rc.y = Math.floor(rc.y);
	rc.w = Math.floor(rc.w);
	rc.h = Math.floor(rc.h);

	return round_rect(rc);
}


function reload_playlist(content)
{
	if (!scene_in_update) {
		print(GF_LOG_ERROR, "reload_playlist called outside of module.update() callback, ignoring");
		return;
	}
	load_playlist(content);

	display_list.length = 0;
}

function parse_group_mod(content, parent)
{
	if (!scene_in_update) {
		print(GF_LOG_ERROR, "parse_group called outside of module.update() callback, ignoring");
		return;
	}
	parse_group(content, parent);
}
function parse_scene_mod(content, parent)
{
	if (!scene_in_update) {
		print(GF_LOG_ERROR, "parse_scene called outside of module.update() callback, ignoring");
		return;
	}
	parse_scene(content, parent);
}

function parse_playlist_element_mod(content)
{
	if (!scene_in_update) {
		print(GF_LOG_ERROR, "parse_playlist_element called outside of module.update() callback, ignoring");
		return;
	}
	parse_playlist_element(content);
}

function remove_element_mod(elmt)
{
	if (!scene_in_update) {
		print(GF_LOG_ERROR, "remove_element called outside of module.update() callback, ignoring");
		return;
	}

	if (typeof elmt == 'string') {
		let the_elem = get_group(elmt);
		if (!the_elem)
				the_elem = get_scene(elmt);
		if (!the_elem)
				the_elem = get_sequence(elmt);
		if (!the_elem)
				the_elem = get_timer(elmt);
		if (!the_elem)
				the_elem = get_script(elmt);
		if (!the_elem)
				the_elem = get_element_by_id(elmt, watchers);

		elmt = the_elem;
	}
	if (! elmt) return;

	//group
	if (elmt.scenes) {
		remove_scene_or_group(elmt);
		return;
	}
	//scene
	if (Array.isArray(elmt.sources) && Array.isArray(elmt.sequences)) {
		remove_scene_or_group(elmt);
		return;
	}
	//sequence
	if (Array.isArray(elmt.sources)) {
		let idx = sequences.indexOf(elmt);
		if (idx>=0) sequences.splice(idx, 1);

		sources.forEach(s => {
			if (s.sequence != elmt) return;
			stop_source(s, true);
			s.removed = true;
		});
	}
	//timer
	if (Array.isArray(elmt.keys)) {
		let idx = timers.indexOf(elmt);
		if (idx>=0) timers.splice(idx, 1);
		timer_restore(elmt);
	}
	//scripts
	if (typeof elmt.script == 'function') {
		let idx = user_scripts.indexOf(elmt);
		if (idx>=0) user_scripts.splice(idx, 1);
	}
	//watcher
	if (typeof elmt.target == 'string') {
		remove_watcher(elmt, false);
	}
}


function update_element_mod(id, prop, value)
{
	let update = { replace: id+'@'+prop, with: value};
	return parse_update_elem(update);
}


function query_element_mod(id, prop)
{
	let elt = get_scene(id);
	if (elt) {
		if (elt.mod && typeof elt.mod[prop] != 'undefined') return elt.mod[prop];
		if (typeof elt[prop] != 'undefined') return elt[prop];
		return undefined;
	}
	elt = get_group(id);
	if (elt) {
		if (typeof elt[prop] != 'undefined') return elt[prop];
		return undefined;
	}
	elt = get_timer(id);
	if (elt) {
		if (typeof elt[prop] != 'undefined') return elt[prop];
		return undefined;
	}
	elt = get_script(id);
	if (elt) {
		if (typeof elt[prop] != 'undefined') return elt[prop];
		return undefined;
	}
	elt = get_sequence(id);
	if (elt) {
		if (typeof elt[prop] != 'undefined') return elt[prop];
		return undefined;
	}
	return undefined;
}

function mouse_over_mod()
{
	if (!arguments.length) return null;
	let x, y;

	if (typeof (arguments[0]) == 'object') {
		let evt = arguments[0];
		x = evt.mouse_x;
		y = evt.mouse_y;
		x -= video_width/2;
		y = video_height/2 - y;
	} else if (arguments.length==2) {
		x = arguments[0];
		y = arguments[1];
	}

	let len = prev_display_list.length;
	let pt = {x: x, y: y};

	for (let i=0; i<len; i++) {
		let ctx = prev_display_list[len-i-1];
		if (ctx.mx.is3D) {
			continue;
		}

		let inv_mx = ctx.mx.copy().inverse();
		let a_pt = inv_mx.apply(pt);
		let w = ctx.scene.mod.width;
		let h = ctx.scene.mod.height;

		if (a_pt.x < -w/2) continue;
		if (a_pt.x > w/2) continue;
		if (a_pt.y < -h/2) continue;
		if (a_pt.y > h/2) continue;

		//offscreen group
		if (ctx.group) {
			return ctx.group;
		}
		let over = false;
		if (typeof ctx.scene.mod.point_over == 'function') {
			over = ctx.scene.mod.point_over(a_pt.x, a_pt.y);
		}
		if (over) return ctx.scene;
	}
	return null;
}
