/*
 *			GPAC - Multimedia Framework C SDK
 *
 *			Authors: Jean Le Feuvre
 *			Copyright (c) Telecom ParisTech 2020-2026
 *					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 { Bitstream as BS } from 'gpaccore'
import { Sys as sys } from 'gpaccore'
import { File as File } from 'gpaccore'

filter.pids = [];

filter.set_name("avgen");
filter.set_class_hint(GF_FS_CLASS_MM_IO);
filter.set_desc("AV Counter Generator");
filter.set_version("1.0");
filter.set_author("GPAC Team");
filter.set_help(
"This filter generates AV streams representing a counter. Streams can be enabled or disabled using [-type]().\n"
+"The filter is software-based and does not use GPU.\n"
+"\n"
+"When [-adjust]() is set, the first video frame is adjusted such that a full circle happens at each exact second according to the system UTC clock.\n"
+"By default, video UTC and date are computed at each frame generation from current clock and not from frame number.\n"
+"This will result in broken UTC timing text when playing at speeds other than 1.0.\n"
+"This can be changed using [-lock]().\n"
+"\n"
+"Audio beep is generated every second, with octave (2xfreq) of even beep used every 10 seconds.\n"
+"When video is generated, beep is synchronized to video at each exact second.\n"
+"\n"
+"If NTP injection is used, each video packet (but not audio ones) has a `SenderNTP` property set; if video is not used, each audio packet has a `SenderNTP` property set.\n"
+"\n"
+"# Multiple output stream generation\n"
+"More than one output size can be specified. This will result in multiple sources being generated, one per size.\n"
+"A size can be specified more than once, resulting in packet references when [-copy]() is not set, or full copies otherwise.\n"
+"Target encoding bitrates can be assigned to each output using [-rates](). This can be useful when generating dash:\n"
+"EX gpac avgen:sizes=1280x720,1920x1080:rates=2M,5M c=aac:FID=1 c=264:FID=2:clone -o live.mpd:SID=1,2\n"
+"\n"
+"# Multiview generation\n"
+"In multiview mode, only the animated counter will move in depth backward and forward, as indicated by the [-disparity]() value.\n"
+"When [-pack]() is set, a packed stereo couple is generated for each video packet.\n"
+"Otherwise, when [-views]() is greater than 2, each view is generated on a dedicated output PID with the property `ViewIdx` set in [1, views].\n"
+"Multi-view output forces usage of [-copy]() mode.\n"
+"\n"
+"# PID Naming\n"
+"The audio PID is assigned the name `audio` and ID `1`.\n"
+"If a single video PID is produced, it is assigned the name `video` and ID `2`.\n"
+"If multiple video PIDs are produced, they are assigned the names `videoN` and ID `N+1`, N in [1, sizes].\n"
+"If multiple [-views]() are generated, they are assigned the names `videoN_vK` and ID `N*views+K-1`, N in [1, sizes], K in [1, views].\n"
+"# Discontinuity simulation\n"
+"Using [-disc](), discontinuities can be simulated at given interval. The timestamp will be reset to 0 at each discontinuity.\n"
);

filter.set_arg({ name: "type", desc: "output selection\n- a: audio only\n- v: video only\n- av: audio and video", type: GF_PROP_UINT, def: "av", minmax_enum: "a|v|av"} );
filter.set_arg({ name: "evte", desc: "output event stream\n- 0: disable\n- 1+: period (sec) of dummy events", type: GF_PROP_UINT, def: "0"} );
filter.set_arg({ name: "freq", desc: "frequency of beep", type: GF_PROP_UINT, def: "440"} );
filter.set_arg({ name: "freq2", desc: "frequency of odd beep", type: GF_PROP_UINT, def: "659"} );
filter.set_arg({ name: "sr", desc: "output samplerate", type: GF_PROP_UINT, def: "44100"} );
filter.set_arg({ name: "flen", desc: "output frame length in samples", type: GF_PROP_UINT, def: "1024"} );
filter.set_arg({ name: "ch", desc: "number of channels", type: GF_PROP_UINT, def: "1"} );
filter.set_arg({ name: "alter", desc: "beep alternatively on each channel", type: GF_PROP_BOOL, def: "false"} );
filter.set_arg({ name: "blen", desc: "length of beep in milliseconds", type: GF_PROP_UINT, def: "50"} );
filter.set_arg({ name: "fps", desc: "video frame rate", type: GF_PROP_FRACTION, def: "25"} );
filter.set_arg({ name: "sizes", desc: "video size in pixels", type: GF_PROP_VEC2I_LIST, def: "1280x720"} );
filter.set_arg({ name: "disc", desc: "discontinuity interval - see filter help", type: GF_PROP_FRACTION, def: "-1/1"} );
filter.set_arg({ name: "pfmt", desc: "output pixel format", type: GF_PROP_PIXFMT, def: "yuv"} );
filter.set_arg({ name: "lock", desc: "lock timing to video generation", type: GF_PROP_BOOL, def: "false"} );
filter.set_arg({ name: "dyn", desc: "move bottom banner", type: GF_PROP_BOOL, def: "true"} );
filter.set_arg({ name: "ntp", desc: "send NTP along with packets", type: GF_PROP_BOOL, def: "true"} );
filter.set_arg({ name: "copy", desc: "copy the framebuffer into each video packet instead of using packet references", type: GF_PROP_BOOL, def: "false"} );
filter.set_arg({ name: "dur", desc: "run for the given time in second", type: GF_PROP_FRACTION, def: "0/0"} );
filter.set_arg({ name: "adjust", desc: "adjust start time to synchronize counter and UTC", type: GF_PROP_BOOL, def: "true"} );
filter.set_arg({ name: "pack", desc: "packing mode for stereo views\n- no: no packing\n- ss: side by side packing, forces [-views]() to 2\n- tb: top-bottom packing, forces [-views]() to 2", type: GF_PROP_UINT, def: "no", minmax_enum: "no|ss|tb"} );
filter.set_arg({ name: "disparity", desc: "disparity in pixels between left-most and right-most views", type: GF_PROP_UINT, def: "20"} );
filter.set_arg({ name: "views", desc: "number of views", type: GF_PROP_UINT, def: "1"} );
filter.set_arg({ name: "rates", desc: "number of target bitrates to assign, one per size", type: GF_PROP_STRING_LIST} );
filter.set_arg({ name: "logt", desc: "log frame time to console", type: GF_PROP_BOOL} );
filter.set_arg({ name: "banner", desc: "banner text to display", type: GF_PROP_STRING, def: "many thanks to QuickJS, FreeType, OpenSSL, SDL, FFmpeg, OpenHEVC, libjpeg, libpng, faad2, libmad, a52dec, xvid, OGG ..."} );

let evte_cts = 0;
let evte_pid = null;
let evte_playing = false;

let audio_osize=0;
let audio_cts=0;
let audio_pid = null;
let audio_pos=0;
let nb_secs=0;
let audio_odd=0;
let audio_10s=1;
let audio_beep_len=0;
let audio_playing=false;
let audio_in_beep=false;
let audio_beep_ch=0;
let audio_cts_offset=0;

let videos = [];
let video_cts=0;
let video_frame=0;
let video_cts_offset=0;

let brush = new evg.SolidBrush();
let video_playing=false;
let start_date = 0;
let frame_offset = 0;
let nb_frame_init = 0;
let utc_init = 0;
let ntp_init = 0;
let start_time = 0;

/*create a text*/
let text = null;

filter.frame_pending = 0;

filter.initialize = function() {

	if (filter.type != 1) {
		this.set_cap({id: "StreamType", value: "Audio", output: true} );
	}
	if (filter.type != 0) {
		this.set_cap({id: "StreamType", value: "Video", output: true} );
	}
	if (filter.evte) {
		this.set_cap({id: "StreamType", value: "Metadata", output: true} );
	}
	this.set_cap({id: "CodecID", value: "raw", output: true} );

	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) return;

	text = new evg.Text();
	text.font = 'SANS';
	text.fontsize = 20;
	text.baseline = GF_TEXT_BASELINE_HANGING;
	text.align=GF_TEXT_ALIGN_CENTER;
	text.lineSpacing=0;

	let pid_id_offset = 1;

	//setup event
	if (filter.evte) {
		evte_pid = this.new_pid();
		evte_pid.set_prop('StreamType', 'Metadata');
		evte_pid.set_prop('CodecID', 'evte');
		evte_pid.set_prop('Cached', true);
		evte_pid.set_prop('Timescale', filter.fps.n);
		evte_pid.name = "event";
		evte_pid.set_prop('ID', pid_id_offset++);

		//we send 8 bytes empty events
		let bps = Math.max(Math.floor(8*8 / filter.evte), 1);
		evte_pid.set_prop('Bitrate', bps);

		//send first at cts=0
		evte_cts -= filter.evte * filter.fps.n;
	}

	//setup audio
	if (filter.type != 1) {
		audio_pid = this.new_pid();
		audio_pid.set_prop('StreamType', 'Audio');
		audio_pid.set_prop('CodecID', 'raw');
		audio_pid.set_prop('SampleRate', filter.sr);
		audio_pid.set_prop('NumChannels', filter.ch);
		audio_pid.set_prop('Timescale', filter.sr);
		audio_pid.set_prop('AudioFormat', 'flt');
		audio_pid.set_prop('Cached', true);
		audio_pid.name = "audio";
		audio_pid.set_prop('ID', pid_id_offset);
		if (!filter.freq)
			filter.freq = 440;

		if (!filter.freq2)
			filter.freq2 = filter.freq;

		audio_osize = 4 * filter.flen * filter.ch;
		audio_beep_len = filter.sr * filter.blen / 1000;
	}

	if (!filter.sizes || !filter.sizes.length)
		filter.type = 0;

	//setup video
	if (filter.type != 0) {
		let assign_rates = false;
		//single view output if packed modes
		if (filter.pack) filter.views = 1;
		else if (!filter.views) filter.views = 1;

		if (filter.rates && filter.rates.length) {
			if (filter.rates.length == filter.sizes.length) {
				assign_rates = true;
			} else {
				print(3, "Number of rates differ from number of video sizes, not assigning rates");
			}
		}

		for (let vid=0; vid<filter.sizes.length; vid++) {
			//setup one source
			let vsrc = {};
			vsrc.canvas=null;
			vsrc.video_buffer=null;
			vsrc.video_tcard_height=0;
			vsrc.video_pids = [];
			vsrc.forward_srcs = [];
			vsrc.init_banner_done = false;
			vsrc.w = filter.sizes[vid].x;
			vsrc.h = filter.sizes[vid].y;
			vsrc.is_forward = false;

			for (let prev_vid=0; prev_vid<videos.length; prev_vid++) {
				let a_vid = videos[prev_vid];
				if ((a_vid.w == vsrc.w) && (a_vid.h == vsrc.h)) {
					a_vid.forward_srcs.push(vsrc);
					vsrc.is_forward = true;
				}
			}
			videos.push(vsrc);
			for (let view=0; view<filter.views; view++) {
				let vpid = this.new_pid();
				vpid.set_prop('StreamType', 'Video');
				vpid.set_prop('CodecID', 'raw');
				vpid.set_prop('FPS', filter.fps);
				vpid.set_prop('Timescale', filter.fps.n);
				vpid.set_prop('PixelFormat', filter.pfmt);
				vpid.set_prop('Width', vsrc.w);
				vpid.set_prop('Stride', vsrc.w);
				vpid.set_prop('Height', vsrc.h);
				vpid.set_prop('Cached', true);

				if (assign_rates) {
					vpid.set_prop('TargetRate', filter.rates[vid]);
				}

				let name = "video";
				if (filter.sizes.length>1)
					name += "" + (vid+1)*filter.views;

				if (filter.views>1) {
					vpid.name = name+"_v" + (view+1);
					vpid.set_prop('ViewIdx', (view+1));
					filter.copy = true;
				} else {
					vpid.name = name;
				}
				vpid.set_prop('ID', pid_id_offset + (vid+1)*filter.views + view);
				vsrc.video_pids.push(vpid);
			}

			if (vsrc.is_forward) continue;

			let osize = sys.pixfmt_size(filter.pfmt, vsrc.w, vsrc.h);
			vsrc.video_buffer = new ArrayBuffer(osize);
			vsrc.canvas = new evg.Canvas(vsrc.w, vsrc.h, filter.pfmt, vsrc.video_buffer);
			vsrc.canvas.centered = true;
			//clear to black and write our static content
			vsrc.canvas.clearf('black');
			vsrc.banner_fontsize = vsrc.h / 36;
			vsrc.txt_fontsize = 20;

			let tx_card = new evg.Texture("testcard.png", true);
			let tx_gpac = new evg.Texture("$GSHARE/res/gpac_highres.png", false);
			tx_card.repeat_s = true;
			tx_card.repeat_t = true;
			tx_gpac.repeat_s = true;
			tx_gpac.repeat_t = true;

			put_image(vsrc, tx_card, true, true);
			put_image(vsrc, tx_gpac, false, true);
			if (filter.pack) {
				put_image(vsrc, tx_card, true, false);
				put_image(vsrc, tx_gpac, false, false);
			}
			text.align=GF_TEXT_ALIGN_LEFT;
		}
	}
	start_time = sys.clock_ms();
}

function put_image(vsrc, tx, is_testcard, is_first)
{

	let disp_w = vsrc.w;
	let disp_h = vsrc.h;
	let t_x = 0;
	let t_y = 0;
	if (filter.pack==1) {
		if (is_first) {
			t_x = -disp_w/2;
		} else {
			t_x = disp_w/2;
		}
	}
	else if (filter.pack==2) {
		if (is_first) {
			t_y = disp_h/2;
		} else {
			t_y = -disp_h/2;
		}
	}

	let path = new evg.Path();
	let scale;
	if (is_testcard) {
		scale = disp_w/2 / tx.width;
	} else {
		scale = disp_w/6 / tx.width;
	}
	let rw = scale * tx.width;
	let rh = scale * tx.height;

	let mmx = new evg.Matrix2D();
	if (filter.pack==1)
		mmx.scale(0.5, 1);
	else if (filter.pack==2)
		mmx.scale(1, 0.5);
	vsrc.canvas.matrix = mmx;
	mmx.scale(scale, scale);
	let ox, oy;
	//testcard in top-left quarter of image
	if (is_testcard) {
		ox = t_x - disp_w/2;
		oy = t_y + disp_h/2;
		vsrc.video_tcard_height = rh;
		if (filter.pack==2)
			vsrc.video_tcard_height /= 2;
	}
	//logo & text in top-right quarter
	else {
		ox = t_x + disp_w/2 - rw;
		oy = t_y + disp_h/2;
	}
	path.rectangle(ox, oy, rw, rh);
	mmx.identity = true;
	if (filter.pack==1)
		mmx.scale(0.5, 1);
	else if (filter.pack==2)
		mmx.scale(1, 0.5);
	mmx.scale(scale, scale);

	if (filter.pack==2)
		mmx.translate(ox, oy/2);
	else
		mmx.translate(ox, oy);

	vsrc.canvas.path = path;
	//put a gradient behind gpac transparent logo
	if (!is_testcard) {
		if (filter.pack) {
			brush.set_color('white');
			vsrc.canvas.fill(brush);
		} else {
			let ch = 0.8;
			let grad = new evg.RadialGradient();
			grad.set_points(ox+rw/2, oy-rh/2, ox+0.46*rw, oy-0.44*rh, rw/2, rh/2);
			grad.set_stopf(0.0, ch, ch, ch, 0.0);
			grad.set_stopf(0.5, ch, ch, ch, 1.0);
			grad.set_stopf(0.65, 1.0, ch, ch, 1.0);
			grad.set_stopf(0.8, ch, ch, ch, 1.0);
			grad.set_stopf(1.0, 0.0, 0.0, 0.0, 0.0);
			grad.mode = GF_GRADIENT_MODE_SPREAD;
			vsrc.canvas.fill(grad);
		}
	}
	tx.mx = mmx;
	tx.auto_mx=false;
	vsrc.canvas.fill(tx);

	if (is_testcard) return;

	/*create a text*/
	text.fontsize = rh/9;
	text.align=GF_TEXT_ALIGN_CENTER;
	let vprop = '' + vsrc.w + 'x' + vsrc.h + '@';
	let fps = filter.fps.n / filter.fps.d;
	let ifps = Math.floor(fps);
	if (ifps==fps) fps = ifps;
	else fps = Math.floor(100*fps) / 100;

	vprop += '' + fps + ' FPS';
	try {
		text.set_text(['GPAC AV Generator', 'v'+sys.version_full, ' ',  'UTC Locked: ' + (filter.lock ? 'yes' : 'no'), ' ', vprop]);
	} catch (e) {
		print(GF_LOG_INFO, "----")
		print(GF_LOG_INFO, e)
		print(GF_LOG_INFO, "----")
		print(GF_LOG_WARNING, "Fonts disabled");
	}

	mmx.identity = true;
	mmx.translate(t_x+10, oy-rh/5);
	if (filter.pack==1)
		mmx.scale(0.5, 1);
	else if (filter.pack==2)
		mmx.scale(1, 0.5);
	vsrc.canvas.matrix = mmx;
	vsrc.canvas.path = text;
	brush.set_color('white');
	vsrc.canvas.fill(brush);

	vsrc.txt_fontsize = rh/7;
}

filter.process_event = function(pid, evt)
{
	if (evt.type == GF_FEVT_STOP) {
		if (pid === audio_pid) audio_playing = false;
		else if (pid === evte_pid) evte_playing = false;
		else video_playing = false;
	}
	else if (evt.type == GF_FEVT_PLAY) {
		if (pid === audio_pid) audio_playing = true;
		else if (pid === evte_pid) evte_playing = true;
		else video_playing = true;
		filter.reschedule();
	}
}

filter.process = function()
{
	if (!audio_playing && !video_playing && !evte_playing) return GF_EOS;

	//start by processing event, then video (adjusting start time)
	if (evte_playing)
		process_eventmsg();

	if (video_playing)
		process_video();

	if (audio_playing)
		process_audio();
	return GF_OK;
}

function get_emeb_box()
{
	let pck = evte_pid.new_packet(8);
	pck.cts = evte_cts;
	pck.dur = filter.evte * filter.fps.n;
	pck.sap = GF_FILTER_SAP_1;

	let bs = new BS(pck.data, true);
	bs.put_u32(8);      //size
	bs.put_4cc("emeb"); //type

	return pck;
}

function process_eventmsg()
{
	if (!evte_pid || evte_pid.would_block)
		return;
	//perform regulation iof audio or video are being generated
	if (audio_playing || video_playing) {
		let nb_sec;
		if (filter.type == 0) {
			nb_sec = audio_cts * filter.dur.d / filter.sr;
		} else {
			nb_sec = video_cts * filter.fps.d / filter.fps.n;
		}

		//send event for the period
		if (nb_sec * filter.fps.n < evte_cts + filter.evte * filter.fps.n) return;
	}
	evte_cts += filter.evte * filter.fps.n;

	let pck = get_emeb_box();
	pck.send();

	let done = false;
	//evte only, check duration
	if (!audio_playing && !video_playing) {
		if (filter.dur.d && (evte_cts * filter.dur.d >= filter.dur.n * filter.fps.n)) {
			print("done playing, cts " + evte_cts);
			done = true;
		}
	} else {
		if ((!audio_playing || !video_playing) && evte_cts > 0) {
			done=true;
		}
	}
	if (done) {
		evte_playing = false
		evte_pid.eos = true;
	}
}

function process_audio()
{
	if (!audio_pid || audio_pid.would_block)
		return;

	let pck = audio_pid.new_packet(audio_osize);

	let farray = new Float32Array(pck.data);
	for (let i=0; i<filter.flen; i++) {
		let idx;
		let samp = 0;
		let cur_pos = audio_pos - nb_secs*filter.sr;
		if (cur_pos < 0) {}
		else if (cur_pos > audio_beep_len) {
			if (audio_in_beep) {
				audio_beep_ch ++;
				if (audio_beep_ch==filter.ch) audio_beep_ch = 0;
			}
			audio_in_beep = false;
		}
		else {
			audio_in_beep = true;
			if (audio_odd) {
				samp = Math.sin( audio_pos * 2 * Math.PI * filter.freq2 / filter.sr);
			} else if (audio_10s) {
				samp = Math.sin( audio_pos * 2 * Math.PI * 2 * filter.freq / filter.sr);
			} else {
				samp = Math.sin( audio_pos * 2 * Math.PI * filter.freq / filter.sr);
			}
			//triangle amplitude
			let scaler = 0.5 - cur_pos / audio_beep_len;
			if (scaler>=0) scaler = (0.5 - scaler) * 2;
			else scaler = (scaler+0.5) * 2;
			samp *= scaler;
		}
		idx = filter.ch * i;
		for (let k=0; k<filter.ch; k++) {
			if (filter.alter && (audio_beep_ch!=k)) {
				farray[idx + k] = 0;
			} else {
				farray[idx + k] = samp;
			}
		}
		audio_pos++;
		if (cur_pos >= filter.sr) {
			nb_secs++;
			audio_odd = (nb_secs % 2);
			audio_10s = ! (nb_secs % 10);
		}
	}

	if (!video_playing && filter.disc.n > 0) {
		let disc_cts = filter.disc.n * filter.flen / filter.disc.d;
		if (audio_cts % disc_cts == 0 && audio_cts > 0) audio_cts_offset = audio_cts;
	}

	/*set packet properties and send it*/
	pck.cts = audio_cts - audio_cts_offset;
	pck.dur = filter.flen;
	pck.sap = GF_FILTER_SAP_1;

	//when prop value is set to true for 'SenderNTP', automatically set
	if (!videos.length && filter.ntp)
		pck.set_prop('SenderNTP', true);
	pck.send();
	audio_cts += filter.flen;

	if (filter.dur.d) {
		if (audio_cts * filter.dur.d >= filter.sr * filter.dur.n) {
			audio_playing = false;
			audio_pid.eos = true;
		}
	}
}

let log_text = '';

function process_video()
{
	if (!filter.copy) {
		if (filter.frame_pending)
			return;
	}

	let date = new Date();
	let utc, ntp;
	//remember start date for lock, and compute initial offset in cycles so that we reach tull cycle at each second
	if (!start_date) {
		start_date = date.getTime();
		let ms_init = date.getMilliseconds();
		if (filter.adjust) {
			frame_offset = filter.fps.n * ms_init / 1000;

			//in audio samples
			audio_pos = Math.floor(ms_init * filter.sr / 1000);

			//move to nb frames
			nb_frame_init = Math.floor(frame_offset / filter.fps.d);
			frame_offset = filter.fps.d * nb_frame_init;
		}
		utc_init = sys.get_utc();
		ntp_init = sys.get_ntp();
		utc = utc_init;
		ntp = ntp_init;
	}
	else if (filter.lock) {
		let elapsed_us = (1000000 * video_cts) / filter.fps.n;
		let elapsed_ms = Math.floor(elapsed_us / 1000);
		date.setTime(start_date + elapsed_ms);
		utc = utc_init + elapsed_ms;
		ntp = sys.ntp_shift(ntp_init, elapsed_us);
	} else {
		utc = sys.get_utc();
		ntp = sys.get_ntp();
	}

	let time = (video_cts + frame_offset) / filter.fps.n;
	let sec = Math.floor(time);
	let col_idx = sec % 2;
	let cycle_time = time - sec;

	let ms = time - sec;
	let h = Math.floor(sec / 3600);
	let m = Math.floor(sec / 60 - h*60);
	let s = Math.floor(sec - h*3600 - m * 60);
	//adjusted nb frames, reset cycle time
	let nbf = Math.floor( ((video_cts + frame_offset) - sec * filter.fps.n) / filter.fps.d );
	if (!nbf) cycle_time = 0;

	let col = (!cycle_time && !(sec % 10)) ? 'slateblue' : 'black';

	for (let vid=0; vid<videos.length; vid++) {
		let vsrc = videos[vid];
		if (vsrc.is_forward) continue;

		for (let i=0; i<filter.views; i++) {
			let vpid = vsrc.video_pids[i];
			if (filter.copy && vpid.would_block)
				continue;

			let nb_pack_views = filter.pack ? 2 : 1;
			for (let j=0; j<nb_pack_views; j++) {
				draw_view(vsrc, i+j, col, col_idx, cycle_time, h, m, s, nbf, video_frame, date, utc, ntp);
			}

			if (!vsrc.init_banner_done) {
				vsrc.init_banner_done = true;
			}

			let forward_idx = 0;
			let a_src = vsrc;
			while (a_src) {
				let pck = null;
				if (filter.copy) {
					pck = vpid.new_packet(vsrc.video_buffer);
					filter.frame_pending = 0;
				} else {
					pck = vpid.new_packet(vsrc.video_buffer, true,  () => { filter.frame_pending--; } );
					filter.frame_pending ++;
				}
				if (filter.disc.n > 0) {
					let disc_cts = filter.disc.n * filter.fps.n / filter.disc.d;
					if (video_cts % disc_cts == 0 && video_cts > 0) video_cts_offset = video_cts;
				}
				/*set packet properties*/
				pck.cts = video_cts - video_cts_offset;
				pck.dur = filter.fps.d;
				pck.sap = GF_FILTER_SAP_1;
				if (filter.ntp)
					pck.set_prop('SenderNTP', ntp);
				pck.send();
				a_src = null;
				if (forward_idx < vsrc.forward_srcs.length) {
					a_src = vsrc.forward_srcs[forward_idx];
					forward_idx++;
					vpid = a_src.video_pids[i];
				} else {
					break;
				}
			}
		}
	}

	if (filter.logt) {
		print(log_text + '\r');
	}

	video_cts += filter.fps.d;
	video_frame++;
	let fps = 1000 * video_frame / (sys.clock_ms() - start_time);

	filter.update_status(`Frame ${video_frame} time=${video_cts}/${filter.fps.n} fps=${fps}`);

	if (filter.dur.d && (video_cts * filter.dur.d >= filter.fps.n * filter.dur.n)) {
		print("done playing, cts " + video_cts);
		video_playing = false;
		for (let vid=0; vid<videos.length; vid++) {
			let vsrc = videos[vid];
			for (let i=0; i<filter.views; i++) {
				vsrc.video_pids[i].eos = true;
			}
		}
	}
}

function ntpFractionToInt(fraction) {
    if (typeof fraction !== 'bigint') {
        // Coerce to BigInt so that very large values are handled safely
        fraction = BigInt(Math.floor(Number(fraction)));
    }

    const DENUM   = 2**32-1;

    // Perform the division in double precision.
    return Number(fraction) / Number(DENUM);
}

function draw_view(vsrc, view_idx, col, col_idx, cycle_time, h, m, s, nbf, video_frame, date, utc, ntp)
{
	let disp_w = vsrc.w;
	let disp_h = vsrc.h;
	let t_x = 0;
	let t_y = 0;
	let r = disp_h/4;
	if (filter.pack==1) {
		disp_w /= 2;
		if (!view_idx) {
			t_x = -disp_w/2;
		} else {
			t_x = disp_w/2;
		}
	}
	else if (filter.pack==2) {
		disp_h /= 2;
		if (!view_idx) {
			t_y = disp_h/2;
		} else {
			t_y = -disp_h/2;
		}
	}


	let clear_crop = filter.dyn ? 0 : 3*vsrc.banner_fontsize;
	if (view_idx && !filter.pack)
		clear_crop = 3*vsrc.banner_fontsize;

	let remain_y = disp_h - vsrc.video_tcard_height;
	//clear bottom-left
	if (!view_idx || filter.pack)
		vsrc.canvas.clearf({x: t_x -disp_w/2-2,y: t_y - remain_y/2, w: disp_w/2+4, h: remain_y - clear_crop}, 'black');
	//clear circle
	vsrc.canvas.clearf({x: t_x + 1, y: t_y + disp_h/5, w:disp_w/2, h: 7*disp_h/10 - clear_crop}, col);

	let cx = t_x + disp_w/4;
	let cy = t_y - disp_h/4 + disp_h/5;


	let disparity = 0;
	if (filter.pack || (filter.views>1)) {
		let num_views = filter.pack ? 2 : filter.views;
		//disparity of first view
		let d = (cycle_time-0.5) * filter.disparity;
		if (col_idx) {
			d *= -1;
		}
		let dstep = -2*d / (num_views-1);
		disparity = d + view_idx * dstep;
	}

	let mx = new evg.Matrix2D();
	if (filter.pack==1)
		mx.scale(0.5, 1);
	else if (filter.pack==2)
		mx.scale(1, 0.5);
	mx.translate(cx + disparity, cy);
	vsrc.canvas.matrix = mx;

	let path = new evg.Path();

	if (col_idx) {
		brush.set_color('white');
	} else {
		brush.set_color('grey');
	}
	path.ellipse(0, 0, 2*r, 2*r);
	vsrc.canvas.path = path;
	vsrc.canvas.fill(brush);

	if (cycle_time) {
		path.reset();
		let start = Math.PI/2 - cycle_time * 2 * Math.PI;
		path.arc(r/2, start, Math.PI/2, 2);
	} else {
		col_idx = !col_idx;
	}

	if (col_idx) {
		brush.set_color('grey');
	} else {
		brush.set_color('white');
	}
	vsrc.canvas.path = path;
	vsrc.canvas.fill(brush);

	text.align=GF_TEXT_ALIGN_LEFT;
	text.fontsize = vsrc.txt_fontsize;

	let t = 'Time: ';
	if (h<10) t = t+'0'+h;
	else t = t+''+h;

	if (m<10) t = t+':0'+m;
	else t = t + ':' + m;

	if (s<10) t = t+':0'+s;
	else t = t+':'+s;

	if (nbf<10) t = t+'/0'+nbf;
	else t = t+'/'+nbf;

	text.set_text(['Frame: '+(video_frame+nb_frame_init), t]);
	log_text = t;

	if (filter.pack || (filter.views>1)) {
		mx.identity = true;
		mx.translate(-r/2, -1.2*r);
		if (filter.pack==1)
			mx.scale(0.5, 1);
		else if (filter.pack==2)
			mx.scale(1, 0.5);
		mx.translate(cx, cy);
	} else {
		mx.translate(-r, -1.2*r);
	}


	vsrc.canvas.matrix = mx;
	vsrc.canvas.path = text;
	brush.set_color('white');
	vsrc.canvas.fill(brush);

	//non packed, Nth view: no changes
	if (view_idx && !filter.pack)
		return;

	text.set_text([' Date: ' + date.toUTCString(), ' Local: ' + date], ' UTC (ms): ' + utc, ' NTP (s.f):  ' + ntp.n + '.' + ntpFractionToInt(ntp.d) );

	mx.identity = true;
	if (filter.pack==1)
		mx.scale(0.5, 1);
	else if (filter.pack==2)
		mx.scale(1, 0.5);
	mx.translate(t_x -disp_w/2, t_y - disp_h/4);
	vsrc.canvas.matrix = mx;
	vsrc.canvas.path = text;
	brush.set_color('white');
	vsrc.canvas.fill(brush);

	if (!filter.dyn && vsrc.init_banner_done)
		return;

	if (filter.pack==1)
		vsrc.canvas.clipper = {x:t_x - disp_w/2,y:0, w: disp_w, h: filter.h };

	let pos_x = (video_frame+nb_frame_init) % (16 * filter.fps.n);
	pos_x /= 10*filter.fps.n;
	pos_x = - (pos_x + 0.25) * disp_w;
	if (!filter.dyn) {
		pos_x = pos_x - 2*disp_w/5;
		text.align=GF_TEXT_ALIGN_CENTER;
	}
	text.fontsize = vsrc.banner_fontsize;
	text.bold = true;
	text.italic = true;
	mx.identity = true;
	if (filter.pack==1)
		mx.scale(0.5, 1);
	else if (filter.pack==2)
		mx.scale(1, 0.5);

	if (!filter.dyn) {
		text.set_text([sys.copyright, filter.banner]);
		mx.translate(sys.copyright.length*text.fontsize/2.5, text.fontsize/2);
	} else {
		text.set_text([sys.copyright + ' - ' + filter.banner]);
	}
	mx.translate(t_x + pos_x, t_y -disp_h/2 + text.fontsize);

	vsrc.canvas.matrix = mx;
	vsrc.canvas.path = text;
	brush.set_color('red');
	vsrc.canvas.fill(brush);
	brush.set_color('white');
	mx.translate(-1, 1);
	vsrc.canvas.matrix = mx;
	vsrc.canvas.fill(brush);
	text.bold = false;
	text.italic = false;
	text.fontsize = vsrc.txt_fontsize;

	vsrc.canvas.clipper = null;
}
