blob: b7e52083bbc683b9de999e64667572cd28612f78 [file] [edit]
/*
* 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;
}