//**************************************************************************
//*                     This file is part of the                           *
//*                MMC - Mpxplay Multimedia Commander                      *
//*                   The source code of MMC is                            *
//*        (C) copyright 1998-2016 by PDSoft (Attila Padar)                *
//*                http://mpxplay.sourceforge.net                          *
//*                  email: mpxplay@freemail.hu                            *
//**************************************************************************
//*  This program 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.                  *
//*  Please contact with the author (with me) if you want to use           *
//*  or modify this source.                                                *
//**************************************************************************
//function: ffmpeg video playing

//#define MPXPLAY_USE_DEBUGF 1
#define DISPQT_DEBUG_OUTPUT NULL //stdout
#define DISPQT_DEBUG_ERROR stderr
#define DISPQT_DEBUGOUT_WARNING stderr
#define DISPQT_DEBUGOUT_QUEUE  NULL // stdout
#define DISPQT_DEBUGOUT_FFMPEG NULL
#define DISPQT_DEBUGOUT_TIMED  stdout
#define DISPQT_DEBUGOUT_VTHREAD  NULL // stdout
#define DISPQT_DEBUGOUT_RENDER stdout
#define DISPQT_DEBUGOUT_SUBTITLE NULL // stdout
#define DISPQT_DEBUGOUT_VFCC stdout

#include <QtGui>
#include <QtWidgets>
#include <QtGui/QPixMap>
#include "moc_video_qt.h"
#include "moc_mainwindow.h"
#include "deparser/ffmpeg.h"

#define INFFMPG_MUTEX_ONTIMER_TIMEOUT     10 // used on 10ms timer
#define INFFMPG_MUTEX_PACKETGET_TIMEOUT  100
#define INFFMPG_MUTEX_COMMAND_TIMEOUT   5000

#define MPXPLAY_VIDEO_WORKER_THREAD_SLEEPTIME_DEFAULT    100
#define MPXPLAY_VIDEO_WORKER_THREAD_SLEEPTIME_SHORT      10
#define MPXPLAY_VIDEO_WORKER_THREAD_CRITICAL_ERROR_COUNT 10

#ifdef MPXPLAY_LINK_ORIGINAL_FFMPEG

extern "C" {
 extern unsigned long mpxplay_config_video_audiovisualization_type;
}

#ifdef DISPQT_FFMPEG_TEST_SPEED
static mpxp_int64_t decode_time = 0;
#endif

extern struct dispqt_videofilter_processdescriptor_s mpxplay_dispqt_videofilter_workerprocess_descriptors_all;
extern unsigned int mpxplay_dispqt_videofilter_workerprocess_nb_descriptors;

static unsigned int video_ffmpeg_subtitle_decode(ffmpegvideo_subtitle_info_s *sub_lines, AVSubtitleRect *rect)
{
	unsigned int lines = 0, textlen, flags, count;
	char *text, *next, *subtext_begin, *sb, strtmp[512];

	if(!sub_lines || !rect)
		return lines;

	pds_memset(sub_lines, 0, sizeof(*sub_lines));

	switch(rect->type){
		case SUBTITLE_TEXT:
			text = rect->text;
			break;
		case SUBTITLE_ASS:
			text = rect->ass;
			count = 9;
			do{
				next = pds_strchr(text, ',');
				if(!next)
					break;
				text = next + 1;
			}while(--count);
			break;
		default: return lines;
	}

	subtext_begin = text;

	mpxplay_debugf(DISPQT_DEBUGOUT_SUBTITLE,"subtext_begin: %s", subtext_begin);

	text = subtext_begin;
	flags = 0;
	do{
		sub_lines->line_flags[lines] = flags;
		textlen = 0;
		next = text;
		switch(rect->type){
			case SUBTITLE_TEXT:
				if((next = pds_strchr(text, '\n'))){
					textlen = next - text;
					next++;
				}
				break;
			case SUBTITLE_ASS:
				while((next = pds_strchr(next, '\\'))){
					if((next[1] == 'N') || (next[1] == 'n')){
						textlen = next - text;
						next += 2;
						break;
					}
					next++;
				}
				break;
		}
		if(!textlen){
			textlen = pds_strlen(text);
			if(!textlen)
				break;
		}
		if(textlen >= sizeof(strtmp))
			textlen = sizeof(strtmp) - 1;
		pds_strncpy(strtmp, text, textlen);
		strtmp[textlen] = 0;
		switch(rect->type){
			case SUBTITLE_TEXT: // TODO: not tested
				while((sb = pds_strchr(strtmp,'<'))){
					int ctrllen = 0;
					if(sb[2] == '>'){
						if(sb[1] == 'i'){  // "<i>"
							funcbit_enable(sub_lines->line_flags[lines], INFFMPEG_ASS_DECODED_FLAG_ITALIC); // TODO: flag changes inside line is not handled (only at begin and end)
							funcbit_enable(flags, INFFMPEG_ASS_DECODED_FLAG_ITALIC);
						}
						ctrllen = 3;
					}else if(sb[1] == '/' && sb[3] == '>'){
						if(sb[2] == 'i')  // "</i>"
							funcbit_disable(flags, INFFMPEG_ASS_DECODED_FLAG_ITALIC);
						ctrllen = 4;
					}

					if(ctrllen){
						pds_strcpy(sb, sb + ctrllen);
						textlen = pds_strlen(strtmp);
					}
				}
				break;
			case SUBTITLE_ASS:
				while((sb = pds_strchr(strtmp,'{'))){
					char *se;
					if(sb[1] == '\\' && sb[2] =='i')
						if(sb[3] == '1'){
							funcbit_enable(sub_lines->line_flags[lines], INFFMPEG_ASS_DECODED_FLAG_ITALIC);  // TODO: flag changes inside line is not handled (only at begin and end)
							funcbit_enable(flags, INFFMPEG_ASS_DECODED_FLAG_ITALIC);
						}else if(sb[3] == '0')
							funcbit_disable(flags, INFFMPEG_ASS_DECODED_FLAG_ITALIC);
					se = pds_strchr(sb, '}');
					if(se){
						if(se[1])
							pds_strcpy(sb, &se[1]);
						else // end of line
							sb[0] = 0;
					}
					textlen = pds_strlen(strtmp);
				}
				break;
		}
		sub_lines->line_texts[lines] = (char *)av_malloc(textlen + 1);
		pds_memcpy(sub_lines->line_texts[lines], strtmp, textlen + 1);
		lines++;
		text = next;
	}while(text && (lines < INFFMPEG_ASS_DECODED_MAX_LINES));
	sub_lines->nb_lines = lines;

	return lines;
}

void video_ffmpeg_subtitleinfo_clear(ffmpegvideo_subtitle_info_s *sub_lines)
{
	int i;
	if(!sub_lines || !sub_lines->nb_lines)
		return;
	for(i = 0; i < sub_lines->nb_lines; i++)
		av_freep(&sub_lines->line_texts[i]);
	pds_memset(sub_lines->line_flags, 0, sizeof(sub_lines->line_flags));
	sub_lines->nb_lines = 0;
}

void video_ffmpeg_subtitleinfo_copy(ffmpegvideo_subtitle_info_s *dest_lines, ffmpegvideo_subtitle_info_s *sub_lines)
{
	int i;
	if(!dest_lines || !sub_lines)
		return;
	dest_lines->nb_lines = sub_lines->nb_lines;
	pds_memcpy(dest_lines->line_flags, sub_lines->line_flags, sizeof(dest_lines->line_flags));
	for(i = 0; i < sub_lines->nb_lines; i++){
		unsigned int len = pds_strlen(sub_lines->line_texts[i]);
		if(len){
			len++;
			dest_lines->line_texts[i] = (char *)av_mallocz(len);
			if(dest_lines->line_texts[i])
				pds_memcpy(dest_lines->line_texts[i], sub_lines->line_texts[i], len);
		}else
			dest_lines->line_texts[i] = NULL;
	}
}

#if 0
static void videowidget_avsubtitle_copy(AVSubtitle *avsub_dest, AVSubtitle *avsub_src)
{
	if(!avsub_dest || !avsub_src)
		return;
	pds_memcpy(avsub_dest, avsub_src, sizeof(AVSubtitle));
	if(avsub_src->num_rects && avsub_src->rects)
	{
		int i;
		avsub_dest->rects = (AVSubtitleRect **)av_mallocz(avsub_src->num_rects * sizeof(AVSubtitleRect *));
		for(i = 0; i < avsub_dest->num_rects; i++)
		{
			unsigned int len;
			avsub_dest->rects[i] = (AVSubtitleRect *)av_mallocz(sizeof(AVSubtitleRect));
			if(!avsub_dest->rects[i])
				break;
			//pds_memcpy(avsub_dest->rects[i], avsub_src->rects[i], sizeof(AVSubtitleRect)); //TODO
			avsub_dest->rects[i]->type = avsub_src->rects[i]->type;
			len = pds_strlen(avsub_src->rects[i]->text);
			if(len){
				avsub_dest->rects[i]->text = (char *)av_malloc(len + 1);
				pds_memcpy(avsub_dest->rects[i]->text, avsub_src->rects[i]->text, len + 1);
			}
			len = pds_strlen(avsub_src->rects[i]->ass);
			if(len){
				avsub_dest->rects[i]->ass = (char *)av_malloc(len + 1);
				pds_memcpy(avsub_dest->rects[i]->ass, avsub_src->rects[i]->ass, len + 1);
			}
		}
	}
}
#endif

// --------------------------------------------------------------------------------------------------------------------

static void video_worker_videodecoder_context_process(struct ffmpegvideo_callback_list_s *cbelem, AVCodecContext **video_ctx, bool *resetvideo, bool *resetfilter, unsigned int *sleep_time, unsigned int *packet_flags)
{
	int streamtype_index = MPXPLAY_STREAMTYPEINDEX_VIDEO, flags = MPXPLAY_PACKETLISTFLAG_CLEARBUF;
	mpxplay_packetlist_t *videoctx_pktelem = NULL;

	if((cbelem->infile_callback(cbelem->infile_passdata, MPXPLAY_INFILE_CBKCTRL_GET_CODECCTXINIT_FFMPEG, &videoctx_pktelem, &streamtype_index, INFFMPG_MUTEX_PACKETGET_TIMEOUT) == MPXPLAY_ERROR_OK) && videoctx_pktelem)
	{
		*video_ctx = (AVCodecContext *)videoctx_pktelem->codec_ctx;
		//mpxplay_debugf(DISPQT_DEBUGOUT_VTHREAD, "video_decoder_worker_thread GET_CODECCTXINIT pd:%8.8X ctx:%8.8X", (unsigned int)cbelem->infile_passdata, (unsigned int)video_ctx);
		if(!cbelem->video_pktlist_elem || (videoctx_pktelem->flags & MPXPLAY_PACKETLISTFLAG_CLEARBUF))
		{
			if(videoctx_pktelem->flags & MPXPLAY_PACKETLISTFLAG_CLEARBUF)
			{
				funcbit_disable(videoctx_pktelem->flags, MPXPLAY_PACKETLISTFLAG_CLEARBUF);
				*resetvideo = *resetfilter = true;
			}
			if(videoctx_pktelem->flags & MPXPLAY_PACKETLISTFLAG_STILLPICT)
				funcbit_enable(*packet_flags, MPXPLAY_PACKETLISTFLAG_STILLPICT);
			if( (cbelem->infile_callback(cbelem->infile_passdata, MPXPLAY_INFILE_CBKCTRL_SET_PACKETQUEUE_CLEAR, &streamtype_index, &flags, INFFMPG_MUTEX_COMMAND_TIMEOUT) == 1)
			 || (*resetvideo && cbelem->video_pktlist_elem && (cbelem->video_pktlist_elem->codec_ctx != *video_ctx)) // TODO: check (wrong video_pktlist_elem->codec_ctx can crash the program)
			){
				if(cbelem->video_pktlist_elem){
					mpxplay_ffmpgdec_queuelistelem_clear(cbelem->video_pktlist_elem);
					cbelem->video_pktlist_elem = NULL;
				}
			}
			if(!cbelem->video_pktlist_elem)
				cbelem->infile_callback(cbelem->infile_passdata, MPXPLAY_INFILE_CBKCTRL_GET_PACKETLIST_ELEM, &cbelem->video_pktlist_elem, &streamtype_index, INFFMPG_MUTEX_PACKETGET_TIMEOUT);
			if(cbelem->video_pktlist_elem)
			{
				if(funcbit_test(cbelem->video_pktlist_elem->flags, MPXPLAY_PACKETLISTFLAG_CLEARBUF))
					*resetvideo = *resetfilter = true;
			}
			if(*resetvideo && funcbit_test(cbelem->flags, DISPQT_FFMPGVIDEOCALLBACKFLAG_PRIMARYVIDEO))
				funcbit_enable(cbelem->flags, (DISPQT_FFMPGVIDEOCALLBACKFLAG_IGNORETIMESTAMP | DISPQT_FFMPGVIDEOCALLBACKFLAG_DISPLAYONEFRAME));
			*sleep_time = MPXPLAY_VIDEO_WORKER_THREAD_SLEEPTIME_SHORT;
		}
		else if(*video_ctx)
		{
			*sleep_time = MPXPLAY_VIDEO_WORKER_THREAD_SLEEPTIME_SHORT;
		}
	}
}

static int video_worker_videodecoder_filter_process(struct ffmpegvideo_callback_list_s *cbelem, AVFrame **vid_frame_filtered)
{
	int filter_retVal = -1;
	if(cbelem->video_frame_filter_workerprocess)
	{
		filter_retVal = cbelem->video_frame_filter_workerprocess->video_filter_apply(cbelem->vid_frame_decoded, vid_frame_filtered, AV_PIX_FMT_NONE);
		if(cbelem->vid_frame_decoded)
		{
			if(funcbit_test(filter_retVal, DISPQT_VIDEOFILTER_APPLYRETBIT_INPUTFRAME_PROCESSED))
			{
				av_frame_free(&cbelem->vid_frame_decoded);
			}
			else if(!funcbit_test(filter_retVal, DISPQT_VIDEOFILTER_APPLYRETBIT_OUTPUTFRAME_PRODUCED))
			{
				*vid_frame_filtered = cbelem->vid_frame_decoded;
				cbelem->vid_frame_decoded = NULL;
			}
		}
	}
	return filter_retVal;
}

static void video_worker_videodecoder_update_metadata_from_vframe(struct ffmpegvideo_callback_list_s *cbelem, AVFrame *vid_frame_decoded, unsigned int packet_flags)
{
	if(funcbit_test(packet_flags, MPXPLAY_PACKETLISTFLAG_STILLPICT) && vid_frame_decoded)
	{
		cbelem->infile_callback(cbelem->infile_passdata, MPXPLAY_INFILE_CBKCTRL_SET_VIDFRAME_METADATA, vid_frame_decoded->metadata, NULL, INFFMPG_MUTEX_COMMAND_TIMEOUT);
	}
}

static void video_worker_videodecoder_frameprocess_speedup_on_delay(struct ffmpegvideo_callback_list_s *cbelem, AVCodecContext *video_ctx)
{
	video_ctx->skip_loop_filter = (video_ctx->codec_id == AV_CODEC_ID_HEVC)? AVDISCARD_ALL : AVDISCARD_DEFAULT; // HEVC has no skip_frame function, we use skip_loop_filter
	video_ctx->skip_frame = AVDISCARD_DEFAULT;
	switch(cbelem->video_delay_type)
	{
		case DISPQT_FFMPGVIDEOCALLBACK_VIDEODELAYTYPE_SIGNIFICANT:
			video_ctx->skip_frame = AVDISCARD_NONREF;//AVDISCARD_NONKEY;
			break;
		case DISPQT_FFMPGVIDEOCALLBACK_VIDEODELAYTYPE_LARGE:
			//video_ctx->skip_frame = AVDISCARD_NONREF;//AVDISCARD_NONINTRA;
			break;
		case DISPQT_FFMPGVIDEOCALLBACK_VIDEODELAYTYPE_MIDDLE:
			//video_ctx->skip_frame = AVDISCARD_BIDIR;//AVDISCARD_NONREF;
			break;
		case DISPQT_FFMPGVIDEOCALLBACK_VIDEODELAYTYPE_SMALL:
			break;
		default:
			video_ctx->skip_loop_filter = AVDISCARD_DEFAULT;
			break;
	}
}

static void video_worker_videodecoder_frame_process(struct ffmpegvideo_callback_list_s *cbelem, AVCodecContext *video_ctx, bool *resetvideo, bool *resetfilter, unsigned int *sleep_time, unsigned int packet_flags)
{
	int retcode_video_receive = -1, retcode_video_send = -1, retry_videodec = 3, packet_used = 0, streamtype_index = MPXPLAY_STREAMTYPEINDEX_VIDEO;
	AVFrame *vid_frame_filtered = NULL;
	if(*resetfilter)
	{
		av_frame_free(&cbelem->vid_frame_decoded);
		if(cbelem->video_frame_filter_workerprocess)
			cbelem->video_frame_filter_workerprocess->video_filter_close();  // TODO: other way to clear?
		*resetfilter = false;
	}
	if(!video_ctx)
	{
		return;
	}

	do{
		if(!cbelem->vid_frame_decoded)
		{
			cbelem->vid_frame_decoded = av_frame_alloc();
			if(!cbelem->vid_frame_decoded)
				break;

			video_worker_videodecoder_frameprocess_speedup_on_delay(cbelem, video_ctx);

			retcode_video_receive = avcodec_receive_frame(video_ctx, cbelem->vid_frame_decoded);

			video_worker_videodecoder_update_metadata_from_vframe(cbelem, cbelem->vid_frame_decoded, packet_flags);
#if 0		// doesn't work
			if((retcode_video_receive < 0) || (cbelem->vid_frame_decoded->flags & AV_FRAME_FLAG_CORRUPT)) // || (vid_frame->decode_error_flags & (FF_DECODE_ERROR_INVALID_BITSTREAM | FF_DECODE_ERROR_MISSING_REFERENCE)))
			{
				if(retcode_video_receive != AVERROR(EAGAIN))
				{
					mpxplay_debugf(DISPQT_DEBUGOUT_WARNING, "avcodec_receive_frame ERROR %8.8X r:%d f:%d df:%d d:%8.8X ls:%d ", (unsigned int)cbelem,
							retcode_video_receive, cbelem->vid_frame_decoded->flags, cbelem->vid_frame_decoded->decode_error_flags, (unsigned int)cbelem->vid_frame_decoded->data[0], cbelem->vid_frame_decoded->linesize[0]);
				}
			}
#endif
#ifdef MPXPLAY_LINK_DXVA
			InputStream *videohwdec_stream = (InputStream *)video_ctx->opaque;
			if(videohwdec_stream)
			{
				if((retcode_video_receive == 0) && videohwdec_stream && videohwdec_stream->dec_ctx && videohwdec_stream->hwaccel_ctx && videohwdec_stream->hwaccel_retrieve_data && (cbelem->vid_frame_decoded->format == videohwdec_stream->hwaccel_pix_fmt))
				{
					retcode_video_receive = videohwdec_stream->hwaccel_retrieve_data(videohwdec_stream->dec_ctx, cbelem->vid_frame_decoded);
				}
				if(retcode_video_receive == 0)
				{
					videohwdec_stream->hwaccel_retrieved_pix_fmt = (enum AVPixelFormat)cbelem->vid_frame_decoded->format;
				}
			}
#endif
			if((retcode_video_receive == 0) && (!cbelem->vid_frame_decoded->data[0] || (cbelem->vid_frame_decoded->linesize[0] <= 0) || (cbelem->vid_frame_decoded->flags & AV_FRAME_FLAG_DISCARD)))
				retcode_video_receive = -1;
			if(retcode_video_receive < 0)
				av_frame_free(&cbelem->vid_frame_decoded);
		}

		if(video_worker_videodecoder_filter_process(cbelem, &vid_frame_filtered) < 0)
		{
			break;
		}
		if(vid_frame_filtered || packet_used || !cbelem->video_pktlist_elem)
		{
			break;
		}

		AVCodecContext *packet_vcodec_ctx = (AVCodecContext *)cbelem->video_pktlist_elem->codec_ctx;
		if(!packet_vcodec_ctx || !cbelem->video_pktlist_elem->frame_pkt || (cbelem->video_pktlist_elem->packet_type != MPXPLAY_PACKETTYPE_AVPACKET))
		{
			packet_used = 1;
			break;
		}
		if((packet_vcodec_ctx->codec_type != AVMEDIA_TYPE_VIDEO) || (packet_vcodec_ctx->codec_id == AV_CODEC_ID_NONE) || !avcodec_is_open(packet_vcodec_ctx) || !packet_vcodec_ctx->codec) // codec is not initialized (avcodec_open2 not called)
		{
			cbelem->videoctx_init_error_count++;
			if(cbelem->videoctx_init_error_count > INFFMPEG_VIDEO_INITERROR_LIMIT) // we drop the packet (containing the incorrect ctx) after a time
				packet_used = 1;
			break;
		}

		retcode_video_send = avcodec_send_packet(packet_vcodec_ctx, (AVPacket *)cbelem->video_pktlist_elem->frame_pkt);
		if(retcode_video_send == 0)
		{
			video_ctx = packet_vcodec_ctx;
			packet_used = 1;
		}
		else
		{
			mpxplay_debugf(DISPQT_DEBUG_ERROR, "avcodec_send_packet VIDEO failed ret:%d pctx:%8.8X vctx:%8.8X", retcode_video_send, packet_vcodec_ctx, video_ctx);
		}
	}while(--retry_videodec);

	if(packet_used || !retry_videodec) // clear packet if it's sent or seems to be unusefull
	{
		mpxplay_ffmpgdec_queuelistelem_clear(cbelem->video_pktlist_elem);
		cbelem->video_pktlist_elem = NULL;
		cbelem->videoctx_init_error_count = 0;
		*sleep_time = 0;
	}
	if(vid_frame_filtered)
	{
		if(vid_frame_filtered->width <= 0)
			vid_frame_filtered->width = video_ctx->width;
		if(vid_frame_filtered->height <= 0)
			vid_frame_filtered->height = video_ctx->height;
		if(vid_frame_filtered->format < 0)
			vid_frame_filtered->format = ((video_ctx->pix_fmt > AV_PIX_FMT_NONE)? video_ctx->pix_fmt : AV_PIX_FMT_YUV420P); // FIXME: default AV_PIX_FMT_YUV420P
		if((vid_frame_filtered->sample_aspect_ratio.num < video_ctx->sample_aspect_ratio.num) || (vid_frame_filtered->sample_aspect_ratio.den < video_ctx->sample_aspect_ratio.den)){
			vid_frame_filtered->sample_aspect_ratio.num = video_ctx->sample_aspect_ratio.num;
			vid_frame_filtered->sample_aspect_ratio.den = video_ctx->sample_aspect_ratio.den;
		}

		if(*resetvideo)
		{
			cbelem->infile_callback(cbelem->infile_passdata, MPXPLAY_INFILE_CBKCTRL_SET_VIDEOTIMESTAMP, vid_frame_filtered, NULL, INFFMPG_MUTEX_COMMAND_TIMEOUT);
			funcbit_enable(packet_flags, MPXPLAY_PACKETLISTFLAG_CLEARBUF);
			*resetvideo = false;
		}
		else if(cbelem->video_delay_type >= DISPQT_FFMPGVIDEOCALLBACK_VIDEODELAYTYPE_SMALL)
		{
			cbelem->infile_callback(cbelem->infile_passdata, MPXPLAY_INFILE_CBKCTRL_SET_VIDEOTIMESTAMP, vid_frame_filtered, NULL, INFFMPG_MUTEX_ONTIMER_TIMEOUT);
		}

		cbelem->infile_callback(cbelem->infile_passdata, MPXPLAY_INFILE_CBKCTRL_GET_CORRECTEDVTIMESTAMP, vid_frame_filtered, (int *)&vid_frame_filtered->pts, INFFMPG_MUTEX_COMMAND_TIMEOUT);

		mpxplay_ffmpgdec_packetqueue_put(&cbelem->videoframe_queue, MPXPLAY_PACKETTYPE_AVFRAME_VIDEO, packet_flags, video_ctx, vid_frame_filtered);

		cbelem->infile_callback(cbelem->infile_passdata, MPXPLAY_INFILE_CBKCTRL_SET_CODECCTX_CONSOLIDATE, video_ctx, &streamtype_index, INFFMPG_MUTEX_COMMAND_TIMEOUT);
		*sleep_time = 0;
		mpxplay_debugf(DISPQT_DEBUGOUT_VTHREAD, "video_decoder_worker_thread decode DONE w:%d h:%d pts:%lld skip:%d", vid_frame_filtered->width, vid_frame_filtered->height, vid_frame_filtered->pts, video_ctx->skip_frame);
	}
}

static void video_worker_audiovisualization_process(struct ffmpegvideo_callback_list_s *cbelem, bool *resetvideo, unsigned int *sleep_time, unsigned int packet_flags)
{
	int streamtype_index = MPXPLAY_STREAMTYPEINDEX_VIDEO;
	bool audio_input_processed = true;
	if(!cbelem->video_pktlist_elem)
		cbelem->infile_callback(cbelem->infile_passdata, MPXPLAY_INFILE_CBKCTRL_GET_PACKETLIST_ELEM, &cbelem->video_pktlist_elem, &streamtype_index, INFFMPG_MUTEX_PACKETGET_TIMEOUT);
	if(cbelem->video_pktlist_elem)
	{
		AVFrame *audio_frame_decoded = (AVFrame *)cbelem->video_pktlist_elem->frame_pkt;
		if((cbelem->video_pktlist_elem->flags & MPXPLAY_PACKETLISTFLAG_CLEARBUF) || (cbelem->videoframe_queue.nb_packets > DISPQT_FFMPGVIDEO_MAX_DELAY_VISVIDEOFRAME))
		{
			funcbit_disable(cbelem->video_pktlist_elem->flags, MPXPLAY_PACKETLISTFLAG_CLEARBUF);
			*resetvideo = true;
		}
		//mpxplay_debugf(DISPQT_DEBUGOUT_VTHREAD, "audio visualization pe:%8.8X afd:%8.8X n:%d", (int)cbelem->video_pktlist_elem, (int)audio_frame_decoded, cbelem->videoframe_queue.nb_packets);
		if(audio_frame_decoded && (cbelem->video_pktlist_elem->packet_type == MPXPLAY_PACKETTYPE_AVFRAME_AUDIO) && (cbelem->videoframe_queue.nb_packets < DISPQT_FFMPGVIDEO_MAX_QUEUED_VISVIDEOFRAME) && cbelem->video_frame_filter_workerprocess)
		{
			AVFrame *vid_frame_visual = NULL;
			int retcode = cbelem->video_frame_filter_workerprocess->video_filter_apply(audio_frame_decoded, &vid_frame_visual, DISPQT_VIDEO_FILTERPROCESS_AVFORMAT);
			if(vid_frame_visual)
			{
				if(*resetvideo)
				{
					cbelem->infile_callback(cbelem->infile_passdata, MPXPLAY_INFILE_CBKCTRL_SET_VIDEOTIMESTAMP, vid_frame_visual, NULL, INFFMPG_MUTEX_COMMAND_TIMEOUT);
					cbelem->video_frame_filter_workerprocess->video_filter_close();  // TODO: other way to reset?
					funcbit_enable(packet_flags, MPXPLAY_PACKETLISTFLAG_CLEARBUF);
					funcbit_enable(cbelem->flags, DISPQT_FFMPGVIDEOCALLBACKFLAG_IGNORETIMESTAMP);
					*resetvideo = false;
					mpxplay_debugf(DISPQT_DEBUGOUT_VTHREAD, "audio visualization RESET n:%d w:%d h:%d pts:%lld", cbelem->videoframe_queue.nb_packets, vid_frame_visual->width, vid_frame_visual->height, vid_frame_visual->pts);
				}
				// FIXME: this is a workaround for large audio blocks + visual plugins
				else
				{
#if 1
					mpxp_int64_t diff = vid_frame_visual->pts - cbelem->video_dec_last_timestamp;
					mpxplay_debugf(DISPQT_DEBUGOUT_VTHREAD, "vid_frame_visual pts:%lld DIFF:%lld", vid_frame_visual->pts, diff);
					if((diff > -(AV_TIME_BASE / 24)) && (diff < (AV_TIME_BASE / 50))) // visual plugin shall send with 25 fps
					{
						vid_frame_visual->pts = cbelem->video_dec_last_timestamp + (AV_TIME_BASE / 26);
					}
#endif
					if(!funcbit_test(retcode, DISPQT_VIDEOFILTER_APPLYRETBIT_INPUTFRAME_PROCESSED))
					{
						audio_input_processed = false;
					}
				}
				cbelem->video_dec_last_timestamp = vid_frame_visual->pts;
				//funcbit_enable(packet_flags, MPXPLAY_PACKETLISTFLAG_IGNORE_TS);
				//funcbit_enable(cbelem->flags, DISPQT_FFMPGVIDEOCALLBACKFLAG_IGNORETIMESTAMP); // for testing only
				funcbit_enable(vid_frame_visual->flags, (DISPQT_AVFRAME_FLAG_NOSKIPFRAME | DISPQT_AVFRAME_FLAG_AR_WINDOW));
				if(mpxplay_ffmpgdec_packetqueue_put(&cbelem->videoframe_queue, MPXPLAY_PACKETTYPE_AVFRAME_VIDEO, packet_flags, NULL, vid_frame_visual) < 0)
				{
					av_frame_free(&vid_frame_visual);
				}
				//mpxplay_debugf(DISPQT_DEBUGOUT_VTHREAD, "mpxplay_ffmpgdec_packetqueue_put DONE n:%d w:%d h:%d pts:%lld", cbelem->videoframe_queue.nb_packets, vid_frame_visual->width, vid_frame_visual->height, vid_frame_visual->pts);
				*sleep_time = MPXPLAY_VIDEO_WORKER_THREAD_SLEEPTIME_SHORT;
			}
			else
			{
				*sleep_time = 0;
			}
		}

		if(audio_input_processed)
		{
			mpxplay_ffmpgdec_queuelistelem_clear(cbelem->video_pktlist_elem);
			cbelem->video_pktlist_elem = NULL;
		}
	}
}

#if defined(PDS_THREADS_POSIX_THREAD)
static void *video_decoder_worker_thread(void *decoder_data)
#else
static unsigned int video_decoder_worker_thread(void *decoder_data)
#endif
{
	struct ffmpegvideo_callback_list_s *cbelem = (struct ffmpegvideo_callback_list_s *)decoder_data;
	int streamtype_index = MPXPLAY_STREAMTYPEINDEX_VIDEO, flags = 0, cberror_count = 0;
	bool resetvideo = false, resetfilter = false;
	unsigned int packet_flags, sleep_time;
	AVCodecContext *video_ctx;

	do{
#ifdef DISPQT_FFMPEG_TEST_SPEED
		mpxp_int64_t curr_time_ms = pds_gettimem();
#endif
		video_ctx = NULL;
		sleep_time = MPXPLAY_VIDEO_WORKER_THREAD_SLEEPTIME_DEFAULT;
		if(!cbelem || !cbelem->infile_callback)
		{
			if(++cberror_count < MPXPLAY_VIDEO_WORKER_THREAD_CRITICAL_ERROR_COUNT)
			{
				pds_threads_sleep(sleep_time);
				continue;
			}
			mpxplay_debugf(DISPQT_DEBUG_ERROR, "video_decoder_worker_thread ERROR cbelem");
			break;
		}
		cberror_count = 0;
		packet_flags = 0;
		video_worker_videodecoder_context_process(cbelem, &video_ctx, &resetvideo, &resetfilter, &sleep_time, &packet_flags);

		if(funcbit_test(cbelem->flags, DISPQT_FFMPGVIDEOCALLBACKFLAG_PRIMARYVIDEO))
		{
			if((mpxplay_config_video_audiovisualization_type == DISPQT_AUDIOVISUALIZATIONTYPE_NONE) || video_ctx) // TODO: extra switch to show audio visualization for video files too
			{
				if((cbelem->videoframe_queue.nb_packets < DISPQT_FFMPGVIDEO_MAX_QUEUED_VIDEOFRAME) || resetvideo)
				{
					video_worker_videodecoder_frame_process(cbelem, video_ctx, &resetvideo, &resetfilter, &sleep_time, packet_flags);
				}
#ifdef DISPQT_FFMPEG_TEST_SPEED
			decode_time += pds_gettimem() - curr_time_ms;
#endif
			}
			else if(funcbit_test(cbelem->flags, DISPQT_FFMPGVIDEOCALLBACKFLAG_PRIMARYVIDEO))
			{
				video_worker_audiovisualization_process(cbelem, &resetvideo, &sleep_time, packet_flags); // audio visualization
			}
			if(resetvideo)
			{
				mpxplay_ffmpgdec_packetqueue_put(&cbelem->videoframe_queue, MPXPLAY_PACKETTYPE_AVFRAME_VIDEO, MPXPLAY_PACKETLISTFLAG_CLEARBUF, NULL, NULL);
			}
		}
		else
		{ // TODO: currently we simply drop non primary video packets (later can be video crossfade or ...)
			if(cbelem->video_pktlist_elem)
			{
				mpxplay_ffmpgdec_queuelistelem_clear(cbelem->video_pktlist_elem);
				cbelem->video_pktlist_elem = NULL;
			}
			resetvideo = false;
			mpxplay_debugf(DISPQT_DEBUGOUT_VTHREAD, "video_decoder_worker_thread DROP END");
		}
		if(sleep_time > 0)
		{
			pds_threads_sleep(sleep_time);
		}
	}while(!funcbit_test(cbelem->flags, DISPQT_FFMPGVIDEOCALLBACKFLAG_EXITEVENT));

	if(video_ctx)
	{
		if(avcodec_is_open(video_ctx) && video_ctx->codec)
			avcodec_flush_buffers(video_ctx);
		cbelem->infile_callback(cbelem->infile_passdata, MPXPLAY_INFILE_CBKCTRL_SET_CODECCTX_CLEAR, &streamtype_index, &flags, INFFMPG_MUTEX_COMMAND_TIMEOUT);
	}
	mpxplay_debugf(DISPQT_DEBUGOUT_VFCC, "video_decoder_worker_thread END");
	funcbit_enable(cbelem->flags, DISPQT_FFMPGVIDEOCALLBACKFLAG_THEXITDONE);
	pds_threads_sleep(DISPQT_VIDEO_MUTEX_TIMEOUT);  // required for pds_threads_thread_close -> TerminateThread
	return 0;
}

FFMpegVideoDecoderWorker::FFMpegVideoDecoderWorker(MainWindow *main_window, QWidget *parent) : QObject(parent)
{
	this->main_window = main_window;
	this->last_is_video_open = false;
	this->is_paused = true;
	this->cblist_first = NULL;
	this->cblist_last = NULL;
	this->nb_callbacks = 0;
#ifdef DISPQT_FFMPEG_TEST_SPEED
	this->timedevent_prev_start_time = 0;
#endif
}

FFMpegVideoDecoderWorker::~FFMpegVideoDecoderWorker()
{
	this->mutex_decoderthread.tryLock(DISPQT_VIDEO_MUTEX_TIMEOUT);
	this->cb_queue_clear();
	this->mutex_decoderthread.unlock();
}

int FFMpegVideoDecoderWorker::cb_queue_put_entry(mpxplay_infile_callback_t *cbk_func, void *passdata, bool prepend)
{
	ffmpegvideo_callback_list_s *cbelem_new;

	cbelem_new = (ffmpegvideo_callback_list_s *)av_mallocz(sizeof(ffmpegvideo_callback_list_s));
	if(!cbelem_new)
		goto err_out_put_entry;

	cbelem_new->infile_passdata = passdata;
	funcbit_enable(cbelem_new->flags, (DISPQT_FFMPGVIDEOCALLBACKFLAG_FIRSTFRAME | DISPQT_FFMPGVIDEOCALLBACKFLAG_DISPLAYONEFRAME | DISPQT_FFMPGVIDEOCALLBACKFLAG_IGNORETIMESTAMP));
	if(prepend)
		funcbit_enable(cbelem_new->flags, DISPQT_FFMPGVIDEOCALLBACKFLAG_PRIMARYVIDEO);

	if(cbk_func)
	{
		mpxplay_ffmpgdec_packetqueue_init(&cbelem_new->videoframe_queue, MPXPLAY_STREAMTYPEINDEX_VIDEO);
		cbelem_new->videodecoder_thread_id = pds_threads_thread_create((mpxp_thread_func)video_decoder_worker_thread, cbelem_new, MPXPLAY_THREAD_PRIORITY_NORMAL, 0);
		if(!cbelem_new->videodecoder_thread_id)
			goto err_out_put_entry;
	}

	cbelem_new->video_frame_filter_workerprocess = new DispQtVideoFrameFilter(this->main_window, this, &mpxplay_dispqt_videofilter_workerprocess_descriptors_all, mpxplay_dispqt_videofilter_workerprocess_nb_descriptors);
	if(!cbelem_new->video_frame_filter_workerprocess)
		goto err_out_put_entry;

	if (!this->cblist_last){
		this->cblist_first = cbelem_new;
		this->cblist_last = cbelem_new;
	}else if(prepend){
		funcbit_disable(this->cblist_first->flags, DISPQT_FFMPGVIDEOCALLBACKFLAG_PRIMARYVIDEO);
		cbelem_new->next = this->cblist_first;
		this->cblist_first = cbelem_new;
	}else{ // TODO: cb_queue_del_entry DISPQT_FFMPGVIDEOCALLBACKFLAG_PRIMARYVIDEO flag at append mode
		this->cblist_last->next = cbelem_new;
		this->cblist_last = cbelem_new;
	}
	cbelem_new->video_init_framecounter = DISPQT_FFMPGVIDEOCALLBACK_INITFRAMECOUNTER_START;
	cbelem_new->infile_callback = cbk_func;
	this->nb_callbacks++;

	mpxplay_debugf(DISPQT_DEBUGOUT_QUEUE, "cb_queue_put n:%d f:%8.8X l:%8.8X cb:%8.8X pd:%8.8X", this->nb_callbacks,
			(int)this->cblist_first, (int)this->cblist_last, (unsigned long)cbk_func, (unsigned long)passdata);

	return 0;

err_out_put_entry:
	this->cb_queue_elem_clear(cbelem_new);
	mpxplay_debugf(DISPQT_DEBUGOUT_QUEUE, "cb_queue_put FAILED");
	return -1;
}

void FFMpegVideoDecoderWorker::cb_queue_elem_clear(struct ffmpegvideo_callback_list_s *cbelem)
{
	if(!cbelem)
		return;
	mpxplay_debugf(DISPQT_DEBUGOUT_QUEUE, "cb_queue_elem_clear START pd:%8.8X", (unsigned int)cbelem->infile_passdata);
	if(cbelem->videodecoder_thread_id)
	{
		int retry = DISPQT_VIDEO_MUTEX_TIMEOUT / MPXPLAY_VIDEO_WORKER_THREAD_SLEEPTIME_SHORT;
		funcbit_enable(cbelem->flags, DISPQT_FFMPGVIDEOCALLBACKFLAG_EXITEVENT);
		while(!funcbit_test(cbelem->flags, DISPQT_FFMPGVIDEOCALLBACKFLAG_THEXITDONE) && (--retry)) { pds_threads_sleep(MPXPLAY_VIDEO_WORKER_THREAD_SLEEPTIME_SHORT); }
		pds_threads_thread_close(cbelem->videodecoder_thread_id);
	}
	cbelem->infile_callback = NULL;
	cbelem->infile_passdata = NULL;
	if(cbelem->videoframe_pktlist_elem)
		mpxplay_ffmpgdec_queuelistelem_clear(cbelem->videoframe_pktlist_elem);
	if(cbelem->video_pktlist_elem)
		mpxplay_ffmpgdec_queuelistelem_clear(cbelem->video_pktlist_elem);
	if(cbelem->subtitle_pktlist_elem)
		mpxplay_ffmpgdec_queuelistelem_clear(cbelem->subtitle_pktlist_elem);
	if(cbelem->subtitle_infos){
		video_ffmpeg_subtitleinfo_clear(cbelem->subtitle_infos);
		av_freep(&cbelem->subtitle_infos);
	}
	if(cbelem->vid_frame_decoded)
		av_frame_free(&cbelem->vid_frame_decoded);
	mpxplay_ffmpgdec_packetqueue_close(&cbelem->videoframe_queue);
	if(cbelem->video_frame_filter_workerprocess)
	{
		delete cbelem->video_frame_filter_workerprocess;
		cbelem->video_frame_filter_workerprocess = NULL;
	}
	av_free(cbelem);
	mpxplay_debugf(DISPQT_DEBUGOUT_QUEUE, "cb_queue_elem_clear END pd:%8.8X", (unsigned int)cbelem->infile_passdata);
}

int FFMpegVideoDecoderWorker::cb_queue_del_entry(mpxplay_infile_callback_t *cbk_func, void *passdata)
{
	ffmpegvideo_callback_list_s *cbelem = this->cblist_first, *cbe_prev = NULL;

	do{
		if(!cbelem)
			return -1;
		if((cbelem->infile_callback == cbk_func) && (cbelem->infile_passdata == passdata)){
			if(cbe_prev)
				cbe_prev->next = cbelem->next;
			else if(cbelem == this->cblist_first)
				this->cblist_first = cbelem->next;
			if(cbelem == this->cblist_last)
				this->cblist_last = cbe_prev;
			cb_queue_elem_clear(cbelem);
			this->nb_callbacks--;
			break;
		}
		cbe_prev = cbelem;
		cbelem = cbelem->next;
	}while(1);

	mpxplay_debugf(DISPQT_DEBUGOUT_QUEUE, "cb_queue_del n:%d f:%8.8X l:%8.8X cb:%8.8X pd:%8.8X", this->nb_callbacks,
			(int)this->cblist_first, (int)this->cblist_last, (unsigned long)cbk_func, (unsigned long)passdata);

	return 0;
}

void FFMpegVideoDecoderWorker::cb_queue_clear(void)
{
	ffmpegvideo_callback_list_s *cbelem = this->cblist_first;
	while(cbelem){
		ffmpegvideo_callback_list_s *nextcbe = cbelem->next;
		cb_queue_elem_clear(cbelem);
		cbelem = nextcbe;
	}
	this->cblist_first = NULL;
	this->cblist_last = NULL;
	this->nb_callbacks = 0;
}

bool FFMpegVideoDecoderWorker::videothread_ffmpeg_config_callback(unsigned int cfgcmd, mpxplay_infile_callback_t *cbk_func, void *passdata)
{
	ffmpegvideo_callback_list_s *cbelem;
	mpxplay_packetlist_t *videoctx_pktelem;
	bool lock_success, video_is_open = false;
	int video_streamtypeindex;

	lock_success = this->mutex_decoderthread.tryLock(DISPQT_VIDEO_MUTEX_TIMEOUT);
	mpxplay_debugf(DISPQT_DEBUGOUT_VFCC, "videothread_ffmpeg_config_callback BEGIN cmd:%8.8X cbk:%8.8X pd:%8.8X lock:%d", cfgcmd, (unsigned int)cbk_func, (unsigned int)passdata, (unsigned int)lock_success);

	switch(cfgcmd)
	{
		case MPXPLAY_INFILE_CBKCFG_SRVFFMV_CLEAR_CALLBACKS:
			this->cb_queue_clear();
			break;
		case MPXPLAY_INFILE_CBKCFG_SRVFFMV_PAUSE:
			this->is_paused = true;
			break;
		case MPXPLAY_INFILE_CBKCFG_SRVFFMV_UNPAUSE:
			this->is_paused = false;
			break;
		case MPXPLAY_INFILE_CBKCFG_SRVFFMV_OPEN_CALLBACK:
			this->cb_queue_put_entry(cbk_func, passdata, true); // newly opened video is always the primary (putting it on the begin of cb queue)
			break;
		case MPXPLAY_INFILE_CBKCFG_SRVFFMV_CLOSE_CALLBACK:
			this->cb_queue_del_entry(cbk_func, passdata);
			if(this->nb_callbacks < 0)
				this->nb_callbacks = 0;
			break;
		case MPXPLAY_INFILE_CBKCFG_SRVFFMV_REFRESH_CALLBACK:
			break;
	}

	if(this->nb_callbacks > 0)
	{
		mpxplay_debugf(DISPQT_DEBUGOUT_VFCC, "videothread_ffmpeg_config_callback SEARCH cmd:%8.8X pd:%8.8X", cfgcmd, (unsigned int)passdata);
		cbelem = this->cblist_first;
		while(cbelem)
		{
			int retval = MPXPLAY_ERROR_INFILE_NODATA;
			video_streamtypeindex = MPXPLAY_STREAMTYPEINDEX_VIDEO;
			videoctx_pktelem = NULL;
			if(cbelem->infile_callback)
				retval = cbelem->infile_callback(cbelem->infile_passdata, MPXPLAY_INFILE_CBKCTRL_GET_CODECCTX_FFMPEG, &videoctx_pktelem, &video_streamtypeindex, DISPQT_VIDEO_MUTEX_TIMEOUT);
			mpxplay_debugf(DISPQT_DEBUGOUT_VFCC, "videothread_ffmpeg_config_callback infile_callback FOUND ret:%d pd:%8.8X ctx:%8.8X next:%8.8X", retval,
							(int)cbelem->infile_passdata, (unsigned int)videoctx_pktelem, (unsigned int)cbelem->next);
			if(((retval == MPXPLAY_ERROR_OK) && videoctx_pktelem) || (mpxplay_config_video_audiovisualization_type != DISPQT_AUDIOVISUALIZATIONTYPE_NONE)){
				video_is_open = true;
				//break;
			}
			//cbelem = cbelem->next;
			break; // currently we check the primary video only
		}
	}

	this->last_is_video_open = video_is_open;

	mpxplay_debugf(DISPQT_DEBUGOUT_VFCC, "videothread_ffmpeg_config_callback END open:%d nb:%d cmd:%8.8X pd:%8.8X", (int)this->last_is_video_open, this->nb_callbacks, cfgcmd, (int)passdata);
	if(lock_success)
		this->mutex_decoderthread.unlock();

	return video_is_open;
}

int FFMpegVideoDecoderWorker::videothread_ffmpeg_callback_call(int globalpos_x, int globalpos_y, unsigned long command, void *arg1, int *arg2)
{
	this->mutex_decoderthread.tryLock(DISPQT_VIDEO_MUTEX_TIMEOUT);
	ffmpegvideo_callback_list_s *cbelem = this->cblist_first; // TODO: select by globalpos
	int retval = MPXPLAY_ERROR_INFILE_NODATA;
	if(cbelem && cbelem->infile_callback)
		retval = cbelem->infile_callback(cbelem->infile_passdata, command, arg1, arg2, DISPQT_VIDEO_MUTEX_TIMEOUT);
	this->mutex_decoderthread.unlock();
	return retval;
}

// quality tune (reduce) based on video delay
static mpxp_bool_t ffmpegvideodecoderworker_video_delay_detect(ffmpegvideo_callback_list_s *cbelem, mpxp_int64_t video_delay, struct mmc_dispqt_config_s *gui_config)
{
	int framedrop_counter = DISPQT_FFMPGVIDEOCALLBACK_VIDEODELAYFRAMEDROP_NONE;
	mpxp_bool_t doFrameDrop = FALSE;

	if(cbelem->video_init_framecounter > 0)
	{
		cbelem->video_init_framecounter--;
		return doFrameDrop;
	}
	if(gui_config->selected_videoplayer_control & MPXPLAY_CONFIG_VIDEOPLAYERCONTROL_DISABLESKIPFRAME)
	{
		cbelem->video_delay_type = DISPQT_FFMPGVIDEOCALLBACK_VIDEODELAYTYPE_NONE;
		cbelem->video_delay_large_counter = 0;
		cbelem->video_delay_framedrop_counter = 0;
		return doFrameDrop;
	}

	if(video_delay >= DISPQT_FFMPGVIDEOCALLBACK_VIDEODELAYLIMIT_MIDDLE)
	{
		if(cbelem->video_delay_large_counter >= DISPQT_FFMPGVIDEOCALLBACK_VIDEODELAYCOUNT_MIDDLE) // to avoid peek sync errors
		{
			if((video_delay >= DISPQT_FFMPGVIDEOCALLBACK_VIDEODELAYLIMIT_SIGNIFICANT) && (cbelem->video_delay_large_counter >= DISPQT_FFMPGVIDEOCALLBACK_VIDEODELAYCOUNT_SIGNIFICANT))
			{
				cbelem->video_delay_type = DISPQT_FFMPGVIDEOCALLBACK_VIDEODELAYTYPE_SIGNIFICANT;
				framedrop_counter = DISPQT_FFMPGVIDEOCALLBACK_VIDEODELAYFRAMEDROP_SIGNIFICANT;
			}
			else if((video_delay >= DISPQT_FFMPGVIDEOCALLBACK_VIDEODELAYLIMIT_LARGE) && (cbelem->video_delay_large_counter >= DISPQT_FFMPGVIDEOCALLBACK_VIDEODELAYCOUNT_LARGE))
			{
				cbelem->video_delay_type = DISPQT_FFMPGVIDEOCALLBACK_VIDEODELAYTYPE_LARGE;
				framedrop_counter = DISPQT_FFMPGVIDEOCALLBACK_VIDEODELAYFRAMEDROP_LARGE;
			}
			else
			{
				cbelem->video_delay_type = DISPQT_FFMPGVIDEOCALLBACK_VIDEODELAYTYPE_MIDDLE;
			}
		}
		else
		{
			cbelem->video_delay_type = DISPQT_FFMPGVIDEOCALLBACK_VIDEODELAYTYPE_SMALL;
		}
		if(cbelem->video_delay_large_counter < DISPQT_FFMPGVIDEOCALLBACK_VIDEODELAYCOUNT_SIGNIFICANT)
		{
			cbelem->video_delay_large_counter++;
		}
	}
	else
	{
		cbelem->video_delay_type = (video_delay >= DISPQT_FFMPGVIDEOCALLBACK_VIDEODELAYLIMIT_SMALL)? DISPQT_FFMPGVIDEOCALLBACK_VIDEODELAYTYPE_SMALL : DISPQT_FFMPGVIDEOCALLBACK_VIDEODELAYTYPE_NONE;
		if(cbelem->video_delay_large_counter > 0)
		{
			cbelem->video_delay_large_counter--;
		}
	}
	if((cbelem->video_delay_framedrop_counter > 0) && (framedrop_counter > 0))
	{
		cbelem->video_delay_framedrop_counter--;
		doFrameDrop = TRUE;
	}
	else
	{
		cbelem->video_delay_framedrop_counter = framedrop_counter;
	}
	return doFrameDrop;
}

#define INFMMPG_DECODERWORKER_DRAWFRAME_RETRY 3

#define INFFMPG_SUBTITLE_DURATION_MUL  (1200)              // msec to AV_TIME_BASE, +20% duration for subtitles
#define INFFMPG_SUBTITLE_DISPLAY_GAP   (AV_TIME_BASE / 10) // min 0.1 sec gap between subtitles
#define INFFMPG_SUBTITLE_DISPLAY_MIN   (AV_TIME_BASE    )  // min 1 sec subtitle displaying (even if the next one is coming sooner)
#define INFFMPG_SUBTITLE_DISPLAY_HOLD  (AV_TIME_BASE * 2)  // hold for (min) 2 secs the subtitle displaying (even if the duration is less)
#define INFFMPG_SUBTITLE_DISPLAY_MAX   (AV_TIME_BASE * 10) // max 10 secs subtitle displaying

void FFMpegVideoDecoderWorker::decoderworker_subtitle_load_next_frame(ffmpegvideo_callback_list_s *cbelem)
{
	int retval_subtitle;
	int streamtype_index = MPXPLAY_STREAMTYPEINDEX_SUBTITLE;
	retval_subtitle = cbelem->infile_callback(cbelem->infile_passdata, MPXPLAY_INFILE_CBKCTRL_GET_PACKETLIST_ELEM, &cbelem->subtitle_pktlist_elem, &streamtype_index, INFFMPG_MUTEX_ONTIMER_TIMEOUT);
	if((retval_subtitle >= 0) && cbelem->subtitle_pktlist_elem && (cbelem->subtitle_pktlist_elem->timestamp_us > 0) && (cbelem->subtitle_end_timestamp >= cbelem->subtitle_pktlist_elem->timestamp_us))
	{
		cbelem->subtitle_end_timestamp = cbelem->subtitle_pktlist_elem->timestamp_us - INFFMPG_SUBTITLE_DISPLAY_GAP;
		if((cbelem->subtitle_end_timestamp - cbelem->subtitle_begin_timestamp) < INFFMPG_SUBTITLE_DISPLAY_MIN)
			cbelem->subtitle_end_timestamp = cbelem->subtitle_begin_timestamp + INFFMPG_SUBTITLE_DISPLAY_MIN;
	}
}

void FFMpegVideoDecoderWorker::decoderworker_subtitle_decode(ffmpegvideo_callback_list_s *cbelem, mpxp_int64_t timestamp_curr_us)
{
	if(cbelem->subtitle_end_timestamp)
	{
		if(timestamp_curr_us >= cbelem->subtitle_end_timestamp)
		{
			video_ffmpeg_subtitleinfo_clear(cbelem->subtitle_infos);
			cbelem->subtitle_end_timestamp = 0;
		}
	}
	else if(cbelem->subtitle_pktlist_elem)
	{
		AVCodecContext *subtitle_codec_ctx = (AVCodecContext *)cbelem->subtitle_pktlist_elem->codec_ctx;
		int got_frame = 0;
		AVSubtitle subtitle_frame;

		pds_memset(&subtitle_frame, 0, sizeof(subtitle_frame));

		if(!cbelem->subtitle_infos)
			cbelem->subtitle_infos = (ffmpegvideo_subtitle_info_s *)av_mallocz(sizeof(ffmpegvideo_subtitle_info_s));
		else
			video_ffmpeg_subtitleinfo_clear(cbelem->subtitle_infos);

		if(subtitle_codec_ctx && (avcodec_decode_subtitle2(subtitle_codec_ctx, &subtitle_frame, &got_frame, (AVPacket *)(cbelem->subtitle_pktlist_elem->frame_pkt)) >= 0) && got_frame && subtitle_frame.num_rects && subtitle_frame.rects)
		{
			if(video_ffmpeg_subtitle_decode(cbelem->subtitle_infos, subtitle_frame.rects[0]))  // TODO: possible multiply rects?
			{
				AVPacket *subtitle_packet = (AVPacket *)(cbelem->subtitle_pktlist_elem->frame_pkt);
				mpxp_int64_t duration;
				mpxplay_debugf(DISPQT_DEBUGOUT_SUBTITLE, "subtitle pts:%llu s:%d e:%d d:%llu", subtitle_packet->pts,
					subtitle_frame.start_display_time, subtitle_frame.end_display_time, subtitle_packet->duration);
				cbelem->subtitle_begin_timestamp = cbelem->subtitle_pktlist_elem->timestamp_us;
				duration = subtitle_packet->duration * INFFMPG_SUBTITLE_DURATION_MUL;
				if(duration < INFFMPG_SUBTITLE_DISPLAY_HOLD)
					duration = INFFMPG_SUBTITLE_DISPLAY_HOLD;
				else if(duration > INFFMPG_SUBTITLE_DISPLAY_MAX)
					duration = INFFMPG_SUBTITLE_DISPLAY_MAX;
				cbelem->subtitle_end_timestamp = cbelem->subtitle_begin_timestamp + duration;
			}
		}
		mpxplay_ffmpgdec_queuelistelem_clear(cbelem->subtitle_pktlist_elem);
		cbelem->subtitle_pktlist_elem = NULL;
		avsubtitle_free(&subtitle_frame);
	}
}

void FFMpegVideoDecoderWorker::decoderworker_subtitle_clear(ffmpegvideo_callback_list_s *cbelem)
{
	if(cbelem->subtitle_pktlist_elem)
	{
		mpxplay_ffmpgdec_queuelistelem_clear(cbelem->subtitle_pktlist_elem);
		cbelem->subtitle_pktlist_elem = NULL;
	}
}

int FFMpegVideoDecoderWorker::decoderworker_video_load_next_frame(ffmpegvideo_callback_list_s *cbelem, bool *resetvideo)
{
	int retval_video;
	if(cbelem->videoframe_pktlist_elem)
	{
		mpxplay_ffmpgdec_queuelistelem_clear(cbelem->videoframe_pktlist_elem);
		cbelem->videoframe_pktlist_elem = NULL;
	}
	retval_video = mpxplay_ffmpgdec_packetqueue_get(cbelem->infile_passdata, &cbelem->videoframe_queue, &cbelem->videoframe_pktlist_elem, 0);
	funcbit_disable(cbelem->flags, DISPQT_FFMPGVIDEOCALLBACKFLAG_STILLPICT);
	if(cbelem->videoframe_pktlist_elem)
	{
		if(cbelem->videoframe_pktlist_elem->flags & MPXPLAY_PACKETLISTFLAG_CLEARBUF)
			*resetvideo = true;
		if((cbelem->videoframe_pktlist_elem->flags & MPXPLAY_PACKETLISTFLAG_STILLPICT) && !(cbelem->flags & DISPQT_FFMPGVIDEOCALLBACKFLAG_FIRSTFRAME))
			funcbit_enable(cbelem->flags, DISPQT_FFMPGVIDEOCALLBACKFLAG_STILLPICT);
	}
	return retval_video;
}

void FFMpegVideoDecoderWorker::decoderworker_video_reset(ffmpegvideo_callback_list_s *cbelem, int video_index)
{
	emit this->signal_ffmpeg_frame_display(video_index, cbelem->flags, NULL, NULL);
	if(funcbit_test(cbelem->flags, DISPQT_FFMPGVIDEOCALLBACKFLAG_FIRSTFRAME))
	{
		cbelem->video_delay_type = DISPQT_FFMPGVIDEOCALLBACK_VIDEODELAYTYPE_NONE;
		cbelem->video_delay_large_counter = 0;
		funcbit_disable(cbelem->flags, DISPQT_FFMPGVIDEOCALLBACKFLAG_FIRSTFRAME);
	}
	if(cbelem->subtitle_pktlist_elem){
		mpxplay_ffmpgdec_queuelistelem_clear(cbelem->subtitle_pktlist_elem);
		cbelem->subtitle_pktlist_elem = NULL;
	}
	video_ffmpeg_subtitleinfo_clear(cbelem->subtitle_infos);
	cbelem->video_last_timestamp = 0;
	cbelem->subtitle_begin_timestamp = cbelem->subtitle_end_timestamp = 0;
	funcbit_enable(cbelem->flags, DISPQT_FFMPGVIDEOCALLBACKFLAG_IGNORETIMESTAMP);
	if(this->is_paused)
		funcbit_enable(cbelem->flags, DISPQT_FFMPGVIDEOCALLBACKFLAG_DISPLAYONEFRAME);
	mpxplay_debugf(DISPQT_DEBUGOUT_WARNING, "TimedEvent RESETVIDEO");
}

void FFMpegVideoDecoderWorker::decoderworker_video_frame_deliver_to_render(ffmpegvideo_callback_list_s *cbelem, int video_index, mpxp_int64_t timestamp_curr_us)
{
	if(cbelem->videoframe_pktlist_elem)
	{
		mpxp_int64_t timestamp_vpacket_us = cbelem->videoframe_pktlist_elem->timestamp_us;
		mpxp_int64_t video_delay = timestamp_curr_us - timestamp_vpacket_us;
		mpxp_int64_t curr_time_ms = pds_gettimem(), render_time_diff_ms = curr_time_ms - cbelem->video_last_render_time;
		mpxp_bool_t doIgnoreTimestamp = (funcbit_test(cbelem->flags, DISPQT_FFMPGVIDEOCALLBACKFLAG_IGNORETIMESTAMP))? TRUE : FALSE;
#ifdef DISPQT_FFMPEG_TEST_SPEED
		mpxp_int64_t timedevent_start_time_ms = pds_gettimem(), gap_timedevent_ms = timedevent_start_time_ms - this->timedevent_prev_start_time;
		this->timedevent_prev_start_time = timedevent_start_time_ms;
//		mpxplay_debugf(DISPQT_DEBUGOUT_TIMED, "TimedEvent TIMESYNC cts:%lld vts:%lld cd:%5lld vd:%lld", timestamp_curr_us, timestamp_vpacket_us, (timestamp_curr_us - cbelem->timedevent_prev_sync_time), video_delay);
#endif
		if(!doIgnoreTimestamp && (timestamp_curr_us > 0) && (cbelem->video_last_timestamp > 0))
		{
			if(timestamp_vpacket_us > (timestamp_curr_us + DISPQT_FFMPGVIDEO_AVSYNC_MAX_VALID_DIFF_US))
			{
				mpxplay_debugf(DISPQT_DEBUGOUT_TIMED, "TimedEvent vd:%lld rtd:%lld", (timestamp_vpacket_us - cbelem->video_last_timestamp), render_time_diff_ms);
				if((timestamp_vpacket_us - cbelem->video_last_timestamp) < (render_time_diff_ms * 1000))
				{
					doIgnoreTimestamp = TRUE; // to start video with invalid PTS too
				}
			}
		}

		if( doIgnoreTimestamp
		 || ((timestamp_curr_us > 0) && (timestamp_vpacket_us <= timestamp_curr_us))
		 || (cbelem->video_last_timestamp && (
				    (timestamp_vpacket_us < cbelem->video_last_timestamp)
				 || (timestamp_vpacket_us > (cbelem->video_last_timestamp + DISPQT_FFMPGVIDEO_VIDEO_MAX_FRAME_DIFF_US)))
			)
		){
			ffmpegvideo_subtitle_info_s *subtitle  = (cbelem->subtitle_end_timestamp && (timestamp_curr_us >= cbelem->subtitle_begin_timestamp))? cbelem->subtitle_infos : NULL;
			AVFrame *videoout_frame = (AVFrame *)(cbelem->videoframe_pktlist_elem->frame_pkt);
			mpxp_bool_t doFrameDrop = FALSE;
#ifdef DISPQT_FFMPEG_TEST_SPEED
			mpxp_int64_t render_time_length = curr_time_ms, sync_diff = (timestamp_curr_us - cbelem->timedevent_prev_sync_time);
			cbelem->timedevent_prev_sync_time = timestamp_curr_us;
//			mpxplay_debugf(DISPQT_DEBUGOUT_TIMED, "TimedEvent VIDEOOUT n:%d vpts:%lld sync:%lld SDIFF:%5lld VDIFF:%lld PDIFF:%lld", cbelem->videoframe_queue.nb_packets, timestamp_vpacket_us, timestamp_curr_us, sync_diff, (timestamp_vpacket_us - cbelem->video_last_timestamp), video_delay);
#endif
			if(!funcbit_test(cbelem->flags, DISPQT_FFMPGVIDEOCALLBACKFLAG_IGNORETIMESTAMP) && !(videoout_frame->flags & DISPQT_AVFRAME_FLAG_NOSKIPFRAME))
			{
				doFrameDrop = ffmpegvideodecoderworker_video_delay_detect(cbelem, video_delay, this->main_window->gui_config);
			}

			if(  funcbit_test(cbelem->flags, DISPQT_FFMPGVIDEOCALLBACKFLAG_IGNORETIMESTAMP)
			 || (render_time_diff_ms >= INFFMPEG_VIDEO_RENDER_MAX_INTERVAL_MS)
			 || ((doFrameDrop == FALSE) && (render_time_diff_ms >= INFFMPEG_VIDEO_RENDER_MIN_INTERVAL_MS))
			){
				if(cbelem->videoframe_pktlist_elem->flags & MPXPLAY_PACKETLISTFLAG_STILLPICT)  // TODO: check: still pictures always use pixel aspect ratio override
					videoout_frame->sample_aspect_ratio.num = videoout_frame->sample_aspect_ratio.den = 1;
				emit this->signal_ffmpeg_frame_display(video_index, cbelem->flags, videoout_frame, subtitle);
				funcbit_disable(cbelem->flags, DISPQT_FFMPGVIDEOCALLBACKFLAG_FIRSTFRAME);
				cbelem->video_last_render_time = curr_time_ms;
				cbelem->videoframe_pktlist_elem->frame_pkt = NULL;  // to keep AVFrame for videowidget_ffmpeg_frame_display
#ifdef DISPQT_FFMPEG_TEST_SPEED
				render_time_length = pds_gettimem() - render_time_length;
				if((render_time_diff_ms < 35) || (render_time_diff_ms > 50) || (sync_diff < 35000) || (sync_diff > 55000) || (render_time_length >= (DISPQT_FFMPEG_WORKERTHREAD_REFRESH_MS * 2)))
				{
					mpxplay_debugf(DISPQT_DEBUGOUT_RENDER, "GAP rtd:%3d rtl:%2llu gte:%2llu sd:%5lld vd:%lld vy:%5lld vdt:%d lc:%d",
						(int)render_time_diff_ms, render_time_length, gap_timedevent_ms, sync_diff, (timestamp_vpacket_us - cbelem->video_last_timestamp),
						video_delay, cbelem->video_delay_type, cbelem->video_delay_large_counter);
				}
#endif
			}
			else
			{
#ifdef DISPQT_FFMPEG_TEST_SPEED
				mpxplay_debugf(DISPQT_DEBUGOUT_RENDER, "DRP grt:%3d sd:%lld vd:%lld vy:%lld vdt:%d dc:%d", (int)render_time_diff_ms, sync_diff, (timestamp_vpacket_us - cbelem->video_last_timestamp), video_delay, cbelem->video_delay_type, cbelem->video_delay_framedrop_counter);
#else
				mpxplay_debugf(DISPQT_DEBUGOUT_RENDER, "DRP grt:%3d vd:%lld vy:%lld vdt:%d", (int)render_time_diff, (timestamp_vpacket_us - cbelem->video_last_timestamp), video_delay, cbelem->video_delay_type);
#endif
			}
			cbelem->video_last_timestamp = timestamp_vpacket_us;
			mpxplay_ffmpgdec_queuelistelem_clear(cbelem->videoframe_pktlist_elem);
			cbelem->videoframe_pktlist_elem = NULL;
			funcbit_disable(cbelem->flags, (DISPQT_FFMPGVIDEOCALLBACKFLAG_DISPLAYONEFRAME | DISPQT_FFMPGVIDEOCALLBACKFLAG_IGNORETIMESTAMP));
		}
		else if((video_delay < 0) && (cbelem->video_delay_type != DISPQT_FFMPGVIDEOCALLBACK_VIDEODELAYTYPE_NONE))
		{
			ffmpegvideodecoderworker_video_delay_detect(cbelem, video_delay, this->main_window->gui_config);
		}
	}
}

void FFMpegVideoDecoderWorker::decoderworker_TimedEvent(void)
{
	ffmpegvideo_callback_list_s *cbelem;
	int video_index = 0, retry;

	if(!this->last_is_video_open)
		return;
	if(!this->mutex_decoderthread.tryLock(0))
		return;

	retry = INFMMPG_DECODERWORKER_DRAWFRAME_RETRY;
	cbelem = this->cblist_first;
	while(cbelem)
	{
		int retval_video = -1;
//		mpxplay_debugf(DISPQT_DEBUGOUT_TIMED, "TimedEvent BEGIN n:%d f:%8.8X l:%8.8X cb:%8.8X pd:%8.8X qn:%d", this->nb_callbacks,
//				(int)this->cblist_first, (int)this->cblist_last, (unsigned long)this->cblist_first->infile_callback, (unsigned long)this->cblist_first->infile_passdata, cbelem->videoframe_queue.nb_packets);
		if(cbelem->infile_callback && cbelem->infile_passdata)
		{
			bool resetvideo = (mpxplay_ffmpgdec_packetqueue_clear(&cbelem->videoframe_queue, MPXPLAY_PACKETLISTFLAG_CLEARBUF))? true : false;
			if(!cbelem->videoframe_pktlist_elem || resetvideo)
			{
				retval_video = this->decoderworker_video_load_next_frame(cbelem, &resetvideo);
				if(resetvideo && (cbelem == this->cblist_first) && funcbit_test(cbelem->flags, DISPQT_FFMPGVIDEOCALLBACKFLAG_PRIMARYVIDEO))
				{
					this->decoderworker_video_reset(cbelem, video_index);
				}
			}
			if(this->is_paused && !funcbit_test(cbelem->flags, DISPQT_FFMPGVIDEOCALLBACKFLAG_DISPLAYONEFRAME))
			{
//				mpxplay_debugf(DISPQT_DEBUGOUT_TIMED, "TimedEvent PAUSED");
				break;
			}
			if(!cbelem->subtitle_pktlist_elem)
			{
				decoderworker_subtitle_load_next_frame(cbelem);
			}
//			mpxplay_debugf(DISPQT_DEBUGOUT_TIMED, "infile_callback END pd:%8.8X r:%d vf:%8.8X", (unsigned int)cbelem->infile_passdata, retval_video, (unsigned int)cbelem->videoframe_pktlist_elem);
		}
		if((cbelem == this->cblist_first) && funcbit_test(cbelem->flags, DISPQT_FFMPGVIDEOCALLBACKFLAG_PRIMARYVIDEO))
		{
			mpxp_int64_t timestamp_curr_us = 0;
			if(cbelem->videoframe_pktlist_elem || cbelem->subtitle_end_timestamp)
				cbelem->infile_callback(cbelem->infile_passdata, MPXPLAY_INFILE_CBKCTRL_GET_TIMESYNC, &timestamp_curr_us, NULL, INFFMPG_MUTEX_ONTIMER_TIMEOUT);
			this->decoderworker_video_frame_deliver_to_render(cbelem, video_index, timestamp_curr_us);
			this->decoderworker_subtitle_decode(cbelem, timestamp_curr_us);
		}
		else // TODO: currently we simply drop non primary video packets (later can be video crossfade or ...)
		{
			decoderworker_subtitle_clear(cbelem);
		}
		if(!cbelem->videoframe_pktlist_elem && (retval_video >= 0) && (--retry))
			continue;
		retry = INFMMPG_DECODERWORKER_DRAWFRAME_RETRY;
//		mpxplay_debugf(DISPQT_DEBUGOUT_TIMED, "TimedEvent END n:%d f:%8.8X l:%8.8X cb:%8.8X pd:%8.8X", this->nb_callbacks,
//							(int)this->cblist_first, (int)this->cblist_last, (unsigned long)this->cblist_first->infile_callback, (unsigned long)this->cblist_first->infile_passdata);
		cbelem = cbelem->next;
		video_index++;
	}
	this->mutex_decoderthread.unlock();
}


#endif // MPXPLAY_LINK_ORIGINAL_FFMPEG
