/*
 * ***** BEGIN GPL LICENSE BLOCK *****
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * 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.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software  Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 * Copyright (c) 2007 The Zdeno Ash Miklas
 *
 * This source file is part of VideoTexture library
 *
 * Contributor(s):
 *
 * ***** END GPL LICENSE BLOCK *****
 */

/** \file gameengine/VideoTexture/VideoFFmpeg.cpp
 *  \ingroup bgevideotex
 */


#ifdef WITH_FFMPEG

// INT64_C fix for some linux machines (C99ism)
#ifndef __STDC_CONSTANT_MACROS
#define __STDC_CONSTANT_MACROS
#endif
#include <stdint.h>


#include "MEM_guardedalloc.h"
#include "PIL_time.h"

#include <string>

#include "VideoFFmpeg.h"
#include "Exception.h"


// default framerate
const double defFrameRate = 25.0;
// time scale constant
const long timeScale = 1000;

// macro for exception handling and logging
#define CATCH_EXCP catch (Exception & exp) \
{ exp.report(); m_status = SourceError; }

// class RenderVideo

// constructor
VideoFFmpeg::VideoFFmpeg (HRESULT * hRslt) : VideoBase(), 
m_codec(NULL), m_formatCtx(NULL), m_codecCtx(NULL), 
m_frame(NULL), m_frameDeinterlaced(NULL), m_frameRGB(NULL), m_imgConvertCtx(NULL),
m_deinterlace(false), m_preseek(0),	m_videoStream(-1), m_baseFrameRate(25.0),
m_lastFrame(-1),  m_eof(false), m_externTime(false), m_curPosition(-1), m_startTime(0), 
m_captWidth(0), m_captHeight(0), m_captRate(0.f), m_isImage(false),
m_isThreaded(false), m_isStreaming(false), m_stopThread(false), m_cacheStarted(false)
{
	// set video format
	m_format = RGB24;
	// force flip because ffmpeg always return the image in the wrong orientation for texture
	setFlip(true);
	// construction is OK
	*hRslt = S_OK;
	BLI_listbase_clear(&m_thread);
	pthread_mutex_init(&m_cacheMutex, NULL);
	BLI_listbase_clear(&m_frameCacheFree);
	BLI_listbase_clear(&m_frameCacheBase);
	BLI_listbase_clear(&m_packetCacheFree);
	BLI_listbase_clear(&m_packetCacheBase);
}

// destructor
VideoFFmpeg::~VideoFFmpeg () 
{
}


// release components
bool VideoFFmpeg::release()
{
	// release
	stopCache();
	if (m_codecCtx)
	{
		avcodec_close(m_codecCtx);
		m_codecCtx = NULL;
	}
	if (m_formatCtx)
	{
		avformat_close_input(&m_formatCtx);
		m_formatCtx = NULL;
	}
	if (m_frame)
	{
		av_free(m_frame);
		m_frame = NULL;
	}
	if (m_frameDeinterlaced)
	{
		MEM_freeN(m_frameDeinterlaced->data[0]);
		av_free(m_frameDeinterlaced);
		m_frameDeinterlaced = NULL;
	}
	if (m_frameRGB)
	{
		MEM_freeN(m_frameRGB->data[0]);
		av_free(m_frameRGB);
		m_frameRGB = NULL;
	}
	if (m_imgConvertCtx)
	{
		sws_freeContext(m_imgConvertCtx);
		m_imgConvertCtx = NULL;
	}
	m_codec = NULL;
	m_status = SourceStopped;
	m_lastFrame = -1;
	return true;
}

AVFrame	*VideoFFmpeg::allocFrameRGB()
{
	AVFrame *frame;
	frame = avcodec_alloc_frame();
	if (m_format == RGBA32)
	{
		avpicture_fill((AVPicture*)frame, 
			(uint8_t*)MEM_callocN(avpicture_get_size(
				PIX_FMT_RGBA,
				m_codecCtx->width, m_codecCtx->height),
				"ffmpeg rgba"),
			PIX_FMT_RGBA, m_codecCtx->width, m_codecCtx->height);
	} else 
	{
		avpicture_fill((AVPicture*)frame, 
			(uint8_t*)MEM_callocN(avpicture_get_size(
				PIX_FMT_RGB24,
				m_codecCtx->width, m_codecCtx->height),
				"ffmpeg rgb"),
			PIX_FMT_RGB24, m_codecCtx->width, m_codecCtx->height);
	}
	return frame;
}

// set initial parameters
void VideoFFmpeg::initParams (short width, short height, float rate, bool image)
{
	m_captWidth = width;
	m_captHeight = height;
	m_captRate = rate;
	m_isImage = image;
}


int VideoFFmpeg::openStream(const char *filename, AVInputFormat *inputFormat, AVDictionary **formatParams)
{
	AVFormatContext *formatCtx = NULL;
	int				i, videoStream;
	AVCodec			*codec;
	AVCodecContext	*codecCtx;

	if (avformat_open_input(&formatCtx, filename, inputFormat, formatParams)!=0)
		return -1;

	if (avformat_find_stream_info(formatCtx, NULL) < 0)
	{
		avformat_close_input(&formatCtx);
		return -1;
	}

	/* Find the first video stream */
	videoStream=-1;
	for (i=0; i<formatCtx->nb_streams; i++)
	{
		if (formatCtx->streams[i] &&
			get_codec_from_stream(formatCtx->streams[i]) && 
			(get_codec_from_stream(formatCtx->streams[i])->codec_type==AVMEDIA_TYPE_VIDEO))
		{
			videoStream=i;
			break;
		}
	}

	if (videoStream==-1) 
	{
		avformat_close_input(&formatCtx);
		return -1;
	}

	codecCtx = get_codec_from_stream(formatCtx->streams[videoStream]);

	/* Find the decoder for the video stream */
	codec=avcodec_find_decoder(codecCtx->codec_id);
	if (codec==NULL) 
	{
		avformat_close_input(&formatCtx);
		return -1;
	}
	codecCtx->workaround_bugs = 1;
	if (avcodec_open2(codecCtx, codec, NULL) < 0)
	{
		avformat_close_input(&formatCtx);
		return -1;
	}

#ifdef FFMPEG_OLD_FRAME_RATE
	if (codecCtx->frame_rate>1000 && codecCtx->frame_rate_base==1)
		codecCtx->frame_rate_base=1000;
	m_baseFrameRate = (double)codecCtx->frame_rate / (double)codecCtx->frame_rate_base;
#else
	m_baseFrameRate = av_q2d(av_get_r_frame_rate_compat(formatCtx->streams[videoStream]));
#endif
	if (m_baseFrameRate <= 0.0) 
		m_baseFrameRate = defFrameRate;

	m_codec = codec;
	m_codecCtx = codecCtx;
	m_formatCtx = formatCtx;
	m_videoStream = videoStream;
	m_frame = avcodec_alloc_frame();
	m_frameDeinterlaced = avcodec_alloc_frame();

	// allocate buffer if deinterlacing is required
	avpicture_fill((AVPicture*)m_frameDeinterlaced, 
		(uint8_t*)MEM_callocN(avpicture_get_size(
		m_codecCtx->pix_fmt,
		m_codecCtx->width, m_codecCtx->height), 
		"ffmpeg deinterlace"), 
		m_codecCtx->pix_fmt, m_codecCtx->width, m_codecCtx->height);

	// check if the pixel format supports Alpha
	if (m_codecCtx->pix_fmt == PIX_FMT_RGB32 ||
		m_codecCtx->pix_fmt == PIX_FMT_BGR32 ||
		m_codecCtx->pix_fmt == PIX_FMT_RGB32_1 ||
		m_codecCtx->pix_fmt == PIX_FMT_BGR32_1) 
	{
		// allocate buffer to store final decoded frame
		m_format = RGBA32;
		// allocate sws context
		m_imgConvertCtx = sws_getContext(
			m_codecCtx->width,
			m_codecCtx->height,
			m_codecCtx->pix_fmt,
			m_codecCtx->width,
			m_codecCtx->height,
			PIX_FMT_RGBA,
			SWS_FAST_BILINEAR,
			NULL, NULL, NULL);
	} else
	{
		// allocate buffer to store final decoded frame
		m_format = RGB24;
		// allocate sws context
		m_imgConvertCtx = sws_getContext(
			m_codecCtx->width,
			m_codecCtx->height,
			m_codecCtx->pix_fmt,
			m_codecCtx->width,
			m_codecCtx->height,
			PIX_FMT_RGB24,
			SWS_FAST_BILINEAR,
			NULL, NULL, NULL);
	}
	m_frameRGB = allocFrameRGB();

	if (!m_imgConvertCtx) {
		avcodec_close(m_codecCtx);
		m_codecCtx = NULL;
		avformat_close_input(&m_formatCtx);
		m_formatCtx = NULL;
		av_free(m_frame);
		m_frame = NULL;
		MEM_freeN(m_frameDeinterlaced->data[0]);
		av_free(m_frameDeinterlaced);
		m_frameDeinterlaced = NULL;
		MEM_freeN(m_frameRGB->data[0]);
		av_free(m_frameRGB);
		m_frameRGB = NULL;
		return -1;
	}
	return 0;
}

/*
 * This thread is used to load video frame asynchronously.
 * It provides a frame caching service. 
 * The main thread is responsible for positioning the frame pointer in the
 * file correctly before calling startCache() which starts this thread.
 * The cache is organized in two layers: 1) a cache of 20-30 undecoded packets to keep
 * memory and CPU low 2) a cache of 5 decoded frames. 
 * If the main thread does not find the frame in the cache (because the video has restarted
 * or because the GE is lagging), it stops the cache with StopCache() (this is a synchronous
 * function: it sends a signal to stop the cache thread and wait for confirmation), then
 * change the position in the stream and restarts the cache thread.
 */
void *VideoFFmpeg::cacheThread(void *data)
{
	VideoFFmpeg* video = (VideoFFmpeg*)data;
	// holds the frame that is being decoded
	CacheFrame *currentFrame = NULL;
	CachePacket *cachePacket;
	bool endOfFile = false;
	int frameFinished = 0;
	double timeBase = av_q2d(video->m_formatCtx->streams[video->m_videoStream]->time_base);
	int64_t startTs = video->m_formatCtx->streams[video->m_videoStream]->start_time;

	if (startTs == AV_NOPTS_VALUE)
		startTs = 0;

	while (!video->m_stopThread)
	{
		// packet cache is used solely by this thread, no need to lock
		// In case the stream/file contains other stream than the one we are looking for,
		// allow a bit of cycling to get rid quickly of those frames
		frameFinished = 0;
		while (	   !endOfFile 
				&& (cachePacket = (CachePacket *)video->m_packetCacheFree.first) != NULL 
				&& frameFinished < 25)
		{
			// free packet => packet cache is not full yet, just read more
			if (av_read_frame(video->m_formatCtx, &cachePacket->packet)>=0) 
			{
				if (cachePacket->packet.stream_index == video->m_videoStream)
				{
					// make sure fresh memory is allocated for the packet and move it to queue
					av_dup_packet(&cachePacket->packet);
					BLI_remlink(&video->m_packetCacheFree, cachePacket);
					BLI_addtail(&video->m_packetCacheBase, cachePacket);
					break;
				} else {
					// this is not a good packet for us, just leave it on free queue
					// Note: here we could handle sound packet
					av_free_packet(&cachePacket->packet);
					frameFinished++;
				}
				
			} else {
				if (video->m_isFile)
					// this mark the end of the file
					endOfFile = true;
				// if we cannot read a packet, no need to continue
				break;
			}
		}
		// frame cache is also used by main thread, lock
		if (currentFrame == NULL) 
		{
			// no current frame being decoded, take free one
			pthread_mutex_lock(&video->m_cacheMutex);
			if ((currentFrame = (CacheFrame *)video->m_frameCacheFree.first) != NULL)
				BLI_remlink(&video->m_frameCacheFree, currentFrame);
			pthread_mutex_unlock(&video->m_cacheMutex);
		}
		if (currentFrame != NULL)
		{
			// this frame is out of free and busy queue, we can manipulate it without locking
			frameFinished = 0;
			while (!frameFinished && (cachePacket = (CachePacket *)video->m_packetCacheBase.first) != NULL)
			{
				BLI_remlink(&video->m_packetCacheBase, cachePacket);
				// use m_frame because when caching, it is not used in main thread
				// we can't use currentFrame directly because we need to convert to RGB first
				avcodec_decode_video2(video->m_codecCtx, 
					video->m_frame, &frameFinished, 
					&cachePacket->packet);
				if (frameFinished) 
				{
					AVFrame * input = video->m_frame;

					/* This means the data wasnt read properly, this check stops crashing */
					if (   input->data[0]!=0 || input->data[1]!=0 
						|| input->data[2]!=0 || input->data[3]!=0)
					{
						if (video->m_deinterlace) 
						{
							if (avpicture_deinterlace(
								(AVPicture*) video->m_frameDeinterlaced,
								(const AVPicture*) video->m_frame,
								video->m_codecCtx->pix_fmt,
								video->m_codecCtx->width,
								video->m_codecCtx->height) >= 0)
							{
								input = video->m_frameDeinterlaced;
							}
						}
						// convert to RGB24
						sws_scale(video->m_imgConvertCtx,
							input->data,
							input->linesize,
							0,
							video->m_codecCtx->height,
							currentFrame->frame->data,
							currentFrame->frame->linesize);
						// move frame to queue, this frame is necessarily the next one
						video->m_curPosition = (long)((cachePacket->packet.dts-startTs) * (video->m_baseFrameRate*timeBase) + 0.5);
						currentFrame->framePosition = video->m_curPosition;
						pthread_mutex_lock(&video->m_cacheMutex);
						BLI_addtail(&video->m_frameCacheBase, currentFrame);
						pthread_mutex_unlock(&video->m_cacheMutex);
						currentFrame = NULL;
					}
				}
				av_free_packet(&cachePacket->packet);
				BLI_addtail(&video->m_packetCacheFree, cachePacket);
			} 
			if (currentFrame && endOfFile) 
			{
				// no more packet and end of file => put a special frame that indicates that
				currentFrame->framePosition = -1;
				pthread_mutex_lock(&video->m_cacheMutex);
				BLI_addtail(&video->m_frameCacheBase, currentFrame);
				pthread_mutex_unlock(&video->m_cacheMutex);
				currentFrame = NULL;
				// no need to stay any longer in this thread
				break;
			}
		}
		// small sleep to avoid unnecessary looping
		PIL_sleep_ms(10);
	}
	// before quitting, put back the current frame to queue to allow freeing
	if (currentFrame)
	{
		pthread_mutex_lock(&video->m_cacheMutex);
		BLI_addtail(&video->m_frameCacheFree, currentFrame);
		pthread_mutex_unlock(&video->m_cacheMutex);
	}
	return 0;
}

// start thread to cache video frame from file/capture/stream
// this function should be called only when the position in the stream is set for the
// first frame to cache
bool VideoFFmpeg::startCache()
{
	if (!m_cacheStarted && m_isThreaded)
	{
		m_stopThread = false;
		for (int i=0; i<CACHE_FRAME_SIZE; i++)
		{
			CacheFrame *frame = new CacheFrame();
			frame->frame = allocFrameRGB();
			BLI_addtail(&m_frameCacheFree, frame);
		}
		for (int i=0; i<CACHE_PACKET_SIZE; i++) 
		{
			CachePacket *packet = new CachePacket();
			BLI_addtail(&m_packetCacheFree, packet);
		}
		BLI_init_threads(&m_thread, cacheThread, 1);
		BLI_insert_thread(&m_thread, this);
		m_cacheStarted = true;
	}
	return m_cacheStarted;
}

void VideoFFmpeg::stopCache()
{
	if (m_cacheStarted)
	{
		m_stopThread = true;
		BLI_end_threads(&m_thread);
		// now delete the cache
		CacheFrame *frame;
		CachePacket *packet;
		while ((frame = (CacheFrame *)m_frameCacheBase.first) != NULL)
		{
			BLI_remlink(&m_frameCacheBase, frame);
			MEM_freeN(frame->frame->data[0]);
			av_free(frame->frame);
			delete frame;
		}
		while ((frame = (CacheFrame *)m_frameCacheFree.first) != NULL)
		{
			BLI_remlink(&m_frameCacheFree, frame);
			MEM_freeN(frame->frame->data[0]);
			av_free(frame->frame);
			delete frame;
		}
		while ((packet = (CachePacket *)m_packetCacheBase.first) != NULL)
		{
			BLI_remlink(&m_packetCacheBase, packet);
			av_free_packet(&packet->packet);
			delete packet;
		}
		while ((packet = (CachePacket *)m_packetCacheFree.first) != NULL)
		{
			BLI_remlink(&m_packetCacheFree, packet);
			delete packet;
		}
		m_cacheStarted = false;
	}
}

void VideoFFmpeg::releaseFrame(AVFrame *frame)
{
	if (frame == m_frameRGB)
	{
		// this is not a frame from the cache, ignore
		return;
	}
	// this frame MUST be the first one of the queue
	pthread_mutex_lock(&m_cacheMutex);
	CacheFrame *cacheFrame = (CacheFrame *)m_frameCacheBase.first;
	assert (cacheFrame != NULL && cacheFrame->frame == frame);
	BLI_remlink(&m_frameCacheBase, cacheFrame);
	BLI_addtail(&m_frameCacheFree, cacheFrame);
	pthread_mutex_unlock(&m_cacheMutex);
}

// open video file
void VideoFFmpeg::openFile (char *filename)
{
	if (openStream(filename, NULL, NULL) != 0)
		return;

	if (m_codecCtx->gop_size)
		m_preseek = (m_codecCtx->gop_size < 25) ? m_codecCtx->gop_size+1 : 25;
	else if (m_codecCtx->has_b_frames)
		m_preseek = 25;	// should determine gopsize
	else
		m_preseek = 0;

	// get video time range
	m_range[0] = 0.0;
	m_range[1] = (double)m_formatCtx->duration / AV_TIME_BASE;

	// open base class
	VideoBase::openFile(filename);

	if (
		// ffmpeg reports that http source are actually non stream
		// but it is really not desirable to seek on http file, so force streaming.
		// It would be good to find this information from the context but there are no simple indication
		!strncmp(filename, "http://", 7) ||
		!strncmp(filename, "rtsp://", 7) ||
		(m_formatCtx->pb && !m_formatCtx->pb->seekable)
		)
	{
		// the file is in fact a streaming source, treat as cam to prevent seeking
		m_isFile = false;
		// but it's not handled exactly like a camera.
		m_isStreaming = true;
		// for streaming it is important to do non blocking read
		m_formatCtx->flags |= AVFMT_FLAG_NONBLOCK;
	}

	if (m_isImage) 
	{
		// the file is to be treated as an image, i.e. load the first frame only
		m_isFile = false;
		// in case of reload, the filename is taken from m_imageName, no need to change it
		if (m_imageName.Ptr() != filename)
			m_imageName = filename;
		m_preseek = 0;
		m_avail = false;
		play();
	}
	// check if we should do multi-threading?
	if (!m_isImage && BLI_system_thread_count() > 1)
	{
		// never thread image: there are no frame to read ahead
		// no need to thread if the system has a single core
		m_isThreaded =  true;
	}
}


// open video capture device
void VideoFFmpeg::openCam (char *file, short camIdx)
{
	// open camera source
	AVInputFormat		*inputFormat;
	AVDictionary		*formatParams = NULL;
	char				filename[28], rateStr[20];

#ifdef WIN32
	// video capture on windows only through Video For Windows driver
	inputFormat = av_find_input_format("vfwcap");
	if (!inputFormat)
		// Video For Windows not supported??
		return;
	sprintf(filename, "%d", camIdx);
#else
	// In Linux we support two types of devices: VideoForLinux and DV1394. 
	// the user specify it with the filename:
	// [<device_type>][:<standard>]
	// <device_type> : 'v4l' for VideoForLinux, 'dv1394' for DV1394. By default 'v4l'
	// <standard>    : 'pal', 'secam' or 'ntsc'. By default 'ntsc'
	// The driver name is constructed automatically from the device type:
	// v4l   : /dev/video<camIdx>
	// dv1394: /dev/dv1394/<camIdx>
	// If you have different driver name, you can specify the driver name explicitly
	// instead of device type. Examples of valid filename:
	//    /dev/v4l/video0:pal
	//    /dev/ieee1394/1:ntsc
	//    dv1394:secam
	//    v4l:pal
	char *p;

	if (file && strstr(file, "1394") != NULL) 
	{
		// the user specifies a driver, check if it is v4l or d41394
		inputFormat = av_find_input_format("dv1394");
		sprintf(filename, "/dev/dv1394/%d", camIdx);
	} else 
	{
		const char *formats[] = {"video4linux2,v4l2", "video4linux2", "video4linux"};
		int i, formatsCount = sizeof(formats) / sizeof(char*);
		for (i = 0; i < formatsCount; i++) {
			inputFormat = av_find_input_format(formats[i]);
			if (inputFormat)
				break;
		}
		sprintf(filename, "/dev/video%d", camIdx);
	}
	if (!inputFormat)
		// these format should be supported, check ffmpeg compilation
		return;
	if (file && strncmp(file, "/dev", 4) == 0) 
	{
		// user does not specify a driver
		strncpy(filename, file, sizeof(filename));
		filename[sizeof(filename)-1] = 0;
		if ((p = strchr(filename, ':')) != 0)
			*p = 0;
	}
	if (file && (p = strchr(file, ':')) != NULL) {
		av_dict_set(&formatParams, "standard", p+1, 0);
	}
#endif
	//frame rate
	if (m_captRate <= 0.f)
		m_captRate = defFrameRate;
	sprintf(rateStr, "%f", m_captRate);

	av_dict_set(&formatParams, "framerate", rateStr, 0);

	if (m_captWidth > 0 && m_captHeight > 0) {
		char video_size[64];
		BLI_snprintf(video_size, sizeof(video_size), "%dx%d", m_captWidth, m_captHeight);
		av_dict_set(&formatParams, "video_size", video_size, 0);
	}

	if (openStream(filename, inputFormat, &formatParams) != 0)
		return;

	// for video capture it is important to do non blocking read
	m_formatCtx->flags |= AVFMT_FLAG_NONBLOCK;
	// open base class
	VideoBase::openCam(file, camIdx);
	// check if we should do multi-threading?
	if (BLI_system_thread_count() > 1)
	{
		// no need to thread if the system has a single core
		m_isThreaded =  true;
	}

	av_dict_free(&formatParams);
}

// play video
bool VideoFFmpeg::play (void)
{
	try
	{
		// if object is able to play
		if (VideoBase::play())
		{
			// set video position
			setPositions();

			if (m_isStreaming)
			{
				av_read_play(m_formatCtx);
			}

			// return success
			return true;
		}
	}
	CATCH_EXCP;
	return false;
}


// pause video
bool VideoFFmpeg::pause (void)
{
	try
	{
		if (VideoBase::pause())
		{
			if (m_isStreaming)
			{
				av_read_pause(m_formatCtx);
			}
			return true;
		}
	}
	CATCH_EXCP;
	return false;
}

// stop video
bool VideoFFmpeg::stop (void)
{
	try
	{
		VideoBase::stop();
		// force restart when play
		m_lastFrame = -1;
		return true;
	}
	CATCH_EXCP;
	return false;
}


// set video range
void VideoFFmpeg::setRange (double start, double stop)
{
	try
	{
		// set range
		if (m_isFile)
		{
			VideoBase::setRange(start, stop);
			// set range for video
			setPositions();
		}
	}
	CATCH_EXCP;
}

// set framerate
void VideoFFmpeg::setFrameRate (float rate)
{
	VideoBase::setFrameRate(rate);
}


// image calculation
// load frame from video
void VideoFFmpeg::calcImage (unsigned int texId, double ts)
{
	if (m_status == SourcePlaying)
	{
		// get actual time
		double startTime = PIL_check_seconds_timer();
		double actTime;
		// timestamp passed from audio actuators can sometimes be slightly negative
		if (m_isFile && ts >= -0.5)
		{
			// allow setting timestamp only when not streaming
			actTime = ts;
			if (actTime * actFrameRate() < m_lastFrame) 
			{
				// user is asking to rewind, force a cache clear to make sure we will do a seek
				// note that this does not decrement m_repeat if ts didn't reach m_range[1]
				stopCache();
			}
		}
		else
		{
			if (m_lastFrame == -1 && !m_isFile)
				m_startTime = startTime;
			actTime = startTime - m_startTime;
		}
		// if video has ended
		if (m_isFile && actTime * m_frameRate >= m_range[1])
		{
			// in any case, this resets the cache
			stopCache();
			// if repeats are set, decrease them
			if (m_repeat > 0) 
				--m_repeat;
			// if video has to be replayed
			if (m_repeat != 0)
			{
				// reset its position
				actTime -= (m_range[1] - m_range[0]) / m_frameRate;
				m_startTime += (m_range[1] - m_range[0]) / m_frameRate;
			}
			// if video has to be stopped, stop it
			else 
			{
				m_status = SourceStopped;
				return;
			}
		}
		// actual frame
		long actFrame = (m_isImage) ? m_lastFrame+1 : long(actTime * actFrameRate());
		// if actual frame differs from last frame
		if (actFrame != m_lastFrame)
		{
			AVFrame* frame;
			// get image
			if ((frame = grabFrame(actFrame)) != NULL)
			{
				if (!m_isFile && !m_cacheStarted) 
				{
					// streaming without cache: detect synchronization problem
					double execTime = PIL_check_seconds_timer() - startTime;
					if (execTime > 0.005) 
					{
						// exec time is too long, it means that the function was blocking
						// resynchronize the stream from this time
						m_startTime += execTime;
					}
				}
				// save actual frame
				m_lastFrame = actFrame;
				// init image, if needed
				init(short(m_codecCtx->width), short(m_codecCtx->height));
				// process image
				process((BYTE*)(frame->data[0]));
				// finished with the frame, release it so that cache can reuse it
				releaseFrame(frame);
				// in case it is an image, automatically stop reading it
				if (m_isImage)
				{
					m_status = SourceStopped;
					// close the file as we don't need it anymore
					release();
				}
			} else if (m_isStreaming)
			{
				// we didn't get a frame and we are streaming, this may be due to
				// a delay in the network or because we are getting the frame too fast.
				// In the later case, shift time by a small amount to compensate for a drift
				m_startTime += 0.001;
			}
		}
	}
}


// set actual position
void VideoFFmpeg::setPositions (void)
{
	// set video start time
	m_startTime = PIL_check_seconds_timer();
	// if file is played and actual position is before end position
	if (!m_eof && m_lastFrame >= 0 && (!m_isFile || m_lastFrame < m_range[1] * actFrameRate()))
		// continue from actual position
		m_startTime -= double(m_lastFrame) / actFrameRate();
	else {
		m_startTime -= m_range[0];
		// start from beginning, stop cache just in case
		stopCache();
	}
}

// position pointer in file, position in second
AVFrame *VideoFFmpeg::grabFrame(long position)
{
	AVPacket packet;
	int frameFinished;
	int posFound = 1;
	bool frameLoaded = false;
	int64_t targetTs = 0;
	CacheFrame *frame;
	int64_t dts = 0;

	if (m_cacheStarted)
	{
		// when cache is active, we must not read the file directly
		do {
			pthread_mutex_lock(&m_cacheMutex);
			frame = (CacheFrame *)m_frameCacheBase.first;
			pthread_mutex_unlock(&m_cacheMutex);
			// no need to remove the frame from the queue: the cache thread does not touch the head, only the tail
			if (frame == NULL)
			{
				// no frame in cache, in case of file it is an abnormal situation
				if (m_isFile)
				{
					// go back to no threaded reading
					stopCache();
					break;
				}
				return NULL;
			}
			if (frame->framePosition == -1) 
			{
				// this frame mark the end of the file (only used for file)
				// leave in cache to make sure we don't miss it
				m_eof = true;
				return NULL;
			}
			// for streaming, always return the next frame, 
			// that's what grabFrame does in non cache mode anyway.
			if (m_isStreaming || frame->framePosition == position)
			{
				return frame->frame;
			}
			// for cam, skip old frames to keep image realtime.
			// There should be no risk of clock drift since it all happens on the same CPU
			if (frame->framePosition > position) 
			{
				// this can happen after rewind if the seek didn't find the first frame
				// the frame in the buffer is ahead of time, just leave it there
				return NULL;
			}
			// this frame is not useful, release it
			pthread_mutex_lock(&m_cacheMutex);
			BLI_remlink(&m_frameCacheBase, frame);
			BLI_addtail(&m_frameCacheFree, frame);
			pthread_mutex_unlock(&m_cacheMutex);
		} while (true);
	}
	double timeBase = av_q2d(m_formatCtx->streams[m_videoStream]->time_base);
	int64_t startTs = m_formatCtx->streams[m_videoStream]->start_time;
	if (startTs == AV_NOPTS_VALUE)
		startTs = 0;

	// come here when there is no cache or cache has been stopped
	// locate the frame, by seeking if necessary (seeking is only possible for files)
	if (m_isFile)
	{
		// first check if the position that we are looking for is in the preseek range
		// if so, just read the frame until we get there
		if (position > m_curPosition + 1 
			&& m_preseek 
			&& position - (m_curPosition + 1) < m_preseek) 
		{
			while (av_read_frame(m_formatCtx, &packet)>=0)
			{
				if (packet.stream_index == m_videoStream) 
				{
					avcodec_decode_video2(
						m_codecCtx, 
						m_frame, &frameFinished, 
						&packet);
					if (frameFinished)
					{
						m_curPosition = (long)((packet.dts-startTs) * (m_baseFrameRate*timeBase) + 0.5);
					}
				}
				av_free_packet(&packet);
				if (position == m_curPosition+1)
					break;
			}
		}
		// if the position is not in preseek, do a direct jump
		if (position != m_curPosition + 1) 
		{ 
			int64_t pos = (int64_t)((position - m_preseek) / (m_baseFrameRate*timeBase));

			if (pos < 0)
				pos = 0;

			pos += startTs;

			if (position <= m_curPosition || !m_eof)
			{
#if 0
				// Tried to make this work but couldn't: seeking on byte is ignored by the
				// format plugin and it will generally continue to read from last timestamp.
				// Too bad because frame seek is not always able to get the first frame
				// of the file.
				if (position <= m_preseek)
				{
					// we can safely go the beginning of the file
					if (av_seek_frame(m_formatCtx, m_videoStream, 0, AVSEEK_FLAG_BYTE) >= 0)
					{
						// binary seek does not reset the timestamp, must do it now
						av_update_cur_dts(m_formatCtx, m_formatCtx->streams[m_videoStream], startTs);
						m_curPosition = 0;
					}
				}
				else
#endif
				{
					// current position is now lost, guess a value. 
					if (av_seek_frame(m_formatCtx, m_videoStream, pos, AVSEEK_FLAG_BACKWARD) >= 0)
					{
						// current position is now lost, guess a value. 
						// It's not important because it will be set at this end of this function
						m_curPosition = position - m_preseek - 1;
					}
				}
			}
			// this is the timestamp of the frame we're looking for
			targetTs = (int64_t)(position / (m_baseFrameRate * timeBase)) + startTs;

			posFound = 0;
			avcodec_flush_buffers(m_codecCtx);
		}
	} else if (m_isThreaded)
	{
		// cache is not started but threading is possible
		// better not read the stream => make take some time, better start caching
		if (startCache())
			return NULL;
		// Abnormal!!! could not start cache, fall back on direct read
		m_isThreaded = false;
	}

	// find the correct frame, in case of streaming and no cache, it means just
	// return the next frame. This is not quite correct, may need more work
	while (av_read_frame(m_formatCtx, &packet) >= 0)
	{
		if (packet.stream_index == m_videoStream) 
		{
			if (m_isImage)
			{
				// If we're an image, we're probably not going to be here often,
				// so we don't want to deal with delayed frames from threading.
				// There might be a better way to handle this, but I'll leave that
				// for people more knowledgeable with ffmpeg than myself. We don't
				// need threading for a single image anyways.
				m_codecCtx->thread_count = 1;
			}

			avcodec_decode_video2(m_codecCtx,
				m_frame, &frameFinished,
				&packet);

			// remember dts to compute exact frame number
			dts = packet.dts;
			if (frameFinished && !posFound) 
			{
				if (dts >= targetTs)
				{
					posFound = 1;
				}
			} 

			if (frameFinished && posFound == 1) 
			{
				AVFrame * input = m_frame;

				/* This means the data wasnt read properly, 
				 * this check stops crashing */
				if (   input->data[0]==0 && input->data[1]==0 
					&& input->data[2]==0 && input->data[3]==0)
				{
					av_free_packet(&packet);
					break;
				}

				if (m_deinterlace) 
				{
					if (avpicture_deinterlace(
						(AVPicture*) m_frameDeinterlaced,
						(const AVPicture*) m_frame,
						m_codecCtx->pix_fmt,
						m_codecCtx->width,
						m_codecCtx->height) >= 0)
					{
						input = m_frameDeinterlaced;
					}
				}
				// convert to RGB24
				sws_scale(m_imgConvertCtx,
					input->data,
					input->linesize,
					0,
					m_codecCtx->height,
					m_frameRGB->data,
					m_frameRGB->linesize);
				av_free_packet(&packet);
				frameLoaded = true;
				break;
			}
		}
		av_free_packet(&packet);
	}
	m_eof = m_isFile && !frameLoaded;
	if (frameLoaded)
	{
		m_curPosition = (long)((dts-startTs) * (m_baseFrameRate*timeBase) + 0.5);
		if (m_isThreaded)
		{
			// normal case for file: first locate, then start cache
			if (!startCache())
			{
				// Abnormal!! could not start cache, return to non-cache mode
				m_isThreaded = false;
			}
		}
		return m_frameRGB;
	}
	return NULL;
}


// python methods


// cast Image pointer to VideoFFmpeg
inline VideoFFmpeg * getVideoFFmpeg (PyImage *self)
{ return static_cast<VideoFFmpeg*>(self->m_image); }


// object initialization
static int VideoFFmpeg_init(PyObject *pySelf, PyObject *args, PyObject *kwds)
{
	PyImage *self = reinterpret_cast<PyImage*>(pySelf);
	// parameters - video source
	// file name or format type for capture (only for Linux: video4linux or dv1394)
	char * file = NULL;
	// capture device number
	short capt = -1;
	// capture width, only if capt is >= 0
	short width = 0;
	// capture height, only if capt is >= 0
	short height = 0;
	// capture rate, only if capt is >= 0
	float rate = 25.f;

	static const char *kwlist[] = {"file", "capture", "rate", "width", "height", NULL};

	// get parameters
	if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|hfhh",
		const_cast<char**>(kwlist), &file, &capt, &rate, &width, &height))
		return -1; 

	try
	{
		// create video object
		Video_init<VideoFFmpeg>(self);

		// set thread usage
		getVideoFFmpeg(self)->initParams(width, height, rate);

		// open video source
		Video_open(getVideo(self), file, capt);
	}
	catch (Exception & exp)
	{
		exp.report();
		return -1;
	}
	// initialization succeded
	return 0;
}

static PyObject *VideoFFmpeg_getPreseek(PyImage *self, void *closure)
{
	return Py_BuildValue("h", getFFmpeg(self)->getPreseek());
}

// set range
static int VideoFFmpeg_setPreseek(PyImage *self, PyObject *value, void *closure)
{
	// check validity of parameter
	if (value == NULL || !PyLong_Check(value))
	{
		PyErr_SetString(PyExc_TypeError, "The value must be an integer");
		return -1;
	}
	// set preseek
	getFFmpeg(self)->setPreseek(PyLong_AsLong(value));
	// success
	return 0;
}

// get deinterlace
static PyObject *VideoFFmpeg_getDeinterlace(PyImage *self, void *closure)
{
	if (getFFmpeg(self)->getDeinterlace())
		Py_RETURN_TRUE;
	else
		Py_RETURN_FALSE;
}

// set flip
static int VideoFFmpeg_setDeinterlace(PyImage *self, PyObject *value, void *closure)
{
	// check parameter, report failure
	if (value == NULL || !PyBool_Check(value))
	{
		PyErr_SetString(PyExc_TypeError, "The value must be a bool");
		return -1;
	}
	// set deinterlace
	getFFmpeg(self)->setDeinterlace(value == Py_True);
	// success
	return 0;
}

// methods structure
static PyMethodDef videoMethods[] =
{ // methods from VideoBase class
	{"play", (PyCFunction)Video_play, METH_NOARGS, "Play (restart) video"},
	{"pause", (PyCFunction)Video_pause, METH_NOARGS, "pause video"},
	{"stop", (PyCFunction)Video_stop, METH_NOARGS, "stop video (play will replay it from start)"},
	{"refresh", (PyCFunction)Video_refresh, METH_NOARGS, "Refresh video - get its status"},
	{NULL}
};
// attributes structure
static PyGetSetDef videoGetSets[] =
{ // methods from VideoBase class
	{(char*)"status", (getter)Video_getStatus, NULL, (char*)"video status", NULL},
	{(char*)"range", (getter)Video_getRange, (setter)Video_setRange, (char*)"replay range", NULL},
	{(char*)"repeat", (getter)Video_getRepeat, (setter)Video_setRepeat, (char*)"repeat count, -1 for infinite repeat", NULL},
	{(char*)"framerate", (getter)Video_getFrameRate, (setter)Video_setFrameRate, (char*)"frame rate", NULL},
	// attributes from ImageBase class
	{(char*)"valid", (getter)Image_valid, NULL, (char*)"bool to tell if an image is available", NULL},
	{(char*)"image", (getter)Image_getImage, NULL, (char*)"image data", NULL},
	{(char*)"size", (getter)Image_getSize, NULL, (char*)"image size", NULL},
	{(char*)"scale", (getter)Image_getScale, (setter)Image_setScale, (char*)"fast scale of image (near neighbor)", NULL},
	{(char*)"flip", (getter)Image_getFlip, (setter)Image_setFlip, (char*)"flip image vertically", NULL},
	{(char*)"filter", (getter)Image_getFilter, (setter)Image_setFilter, (char*)"pixel filter", NULL},
	{(char*)"preseek", (getter)VideoFFmpeg_getPreseek, (setter)VideoFFmpeg_setPreseek, (char*)"nb of frames of preseek", NULL},
	{(char*)"deinterlace", (getter)VideoFFmpeg_getDeinterlace, (setter)VideoFFmpeg_setDeinterlace, (char*)"deinterlace image", NULL},
	{NULL}
};

// python type declaration
PyTypeObject VideoFFmpegType =
{ 
	PyVarObject_HEAD_INIT(NULL, 0)
	"VideoTexture.VideoFFmpeg",   /*tp_name*/
	sizeof(PyImage),          /*tp_basicsize*/
	0,                         /*tp_itemsize*/
	(destructor)Image_dealloc, /*tp_dealloc*/
	0,                         /*tp_print*/
	0,                         /*tp_getattr*/
	0,                         /*tp_setattr*/
	0,                         /*tp_compare*/
	0,                         /*tp_repr*/
	0,                         /*tp_as_number*/
	0,                         /*tp_as_sequence*/
	0,                         /*tp_as_mapping*/
	0,                         /*tp_hash */
	0,                         /*tp_call*/
	0,                         /*tp_str*/
	0,                         /*tp_getattro*/
	0,                         /*tp_setattro*/
	&imageBufferProcs,         /*tp_as_buffer*/
	Py_TPFLAGS_DEFAULT,        /*tp_flags*/
	"FFmpeg video source",       /* tp_doc */
	0,		               /* tp_traverse */
	0,		               /* tp_clear */
	0,		               /* tp_richcompare */
	0,		               /* tp_weaklistoffset */
	0,		               /* tp_iter */
	0,		               /* tp_iternext */
	videoMethods,    /* tp_methods */
	0,                   /* tp_members */
	videoGetSets,          /* tp_getset */
	0,                         /* tp_base */
	0,                         /* tp_dict */
	0,                         /* tp_descr_get */
	0,                         /* tp_descr_set */
	0,                         /* tp_dictoffset */
	(initproc)VideoFFmpeg_init,     /* tp_init */
	0,                         /* tp_alloc */
	Image_allocNew,           /* tp_new */
};

// object initialization
static int ImageFFmpeg_init(PyObject *pySelf, PyObject *args, PyObject *kwds)
{
	PyImage *self = reinterpret_cast<PyImage*>(pySelf);
	// parameters - video source
	// file name or format type for capture (only for Linux: video4linux or dv1394)
	char * file = NULL;

	// get parameters
	if (!PyArg_ParseTuple(args, "s:ImageFFmpeg", &file))
		return -1; 

	try
	{
		// create video object
		Video_init<VideoFFmpeg>(self);

		getVideoFFmpeg(self)->initParams(0, 0, 1.0, true);

		// open video source
		Video_open(getVideo(self), file, -1);
	}
	catch (Exception & exp)
	{
		exp.report();
		return -1;
	}
	// initialization succeded
	return 0;
}

static PyObject *Image_reload(PyImage *self, PyObject *args)
{
	char * newname = NULL;
	if (!PyArg_ParseTuple(args, "|s:reload", &newname))
		return NULL;
	if (self->m_image != NULL)
	{
		VideoFFmpeg* video = getFFmpeg(self);
		// check type of object
		if (!newname)
			newname = video->getImageName();
		if (!newname) {
			// if not set, retport error
			PyErr_SetString(PyExc_RuntimeError, "No image file name given");
			return NULL;
		}
		// make sure the previous file is cleared
		video->release();
		// open the new file
		video->openFile(newname);
	}
	Py_RETURN_NONE;
}

// methods structure
static PyMethodDef imageMethods[] =
{ // methods from VideoBase class
	{"refresh", (PyCFunction)Video_refresh, METH_NOARGS, "Refresh image, i.e. load it"},
	{"reload", (PyCFunction)Image_reload, METH_VARARGS, "Reload image, i.e. reopen it"},
	{NULL}
};
// attributes structure
static PyGetSetDef imageGetSets[] =
{ // methods from VideoBase class
	{(char*)"status", (getter)Video_getStatus, NULL, (char*)"video status", NULL},
	// attributes from ImageBase class
	{(char*)"valid", (getter)Image_valid, NULL, (char*)"bool to tell if an image is available", NULL},
	{(char*)"image", (getter)Image_getImage, NULL, (char*)"image data", NULL},
	{(char*)"size", (getter)Image_getSize, NULL, (char*)"image size", NULL},
	{(char*)"scale", (getter)Image_getScale, (setter)Image_setScale, (char*)"fast scale of image (near neighbor)", NULL},
	{(char*)"flip", (getter)Image_getFlip, (setter)Image_setFlip, (char*)"flip image vertically", NULL},
	{(char*)"filter", (getter)Image_getFilter, (setter)Image_setFilter, (char*)"pixel filter", NULL},
	{NULL}
};

// python type declaration
PyTypeObject ImageFFmpegType =
{ 
	PyVarObject_HEAD_INIT(NULL, 0)
	"VideoTexture.ImageFFmpeg",   /*tp_name*/
	sizeof(PyImage),          /*tp_basicsize*/
	0,                         /*tp_itemsize*/
	(destructor)Image_dealloc, /*tp_dealloc*/
	0,                         /*tp_print*/
	0,                         /*tp_getattr*/
	0,                         /*tp_setattr*/
	0,                         /*tp_compare*/
	0,                         /*tp_repr*/
	0,                         /*tp_as_number*/
	0,                         /*tp_as_sequence*/
	0,                         /*tp_as_mapping*/
	0,                         /*tp_hash */
	0,                         /*tp_call*/
	0,                         /*tp_str*/
	0,                         /*tp_getattro*/
	0,                         /*tp_setattro*/
	&imageBufferProcs,         /*tp_as_buffer*/
	Py_TPFLAGS_DEFAULT,        /*tp_flags*/
	"FFmpeg image source",       /* tp_doc */
	0,		               /* tp_traverse */
	0,		               /* tp_clear */
	0,		               /* tp_richcompare */
	0,		               /* tp_weaklistoffset */
	0,		               /* tp_iter */
	0,		               /* tp_iternext */
	imageMethods,    /* tp_methods */
	0,                   /* tp_members */
	imageGetSets,          /* tp_getset */
	0,                         /* tp_base */
	0,                         /* tp_dict */
	0,                         /* tp_descr_get */
	0,                         /* tp_descr_set */
	0,                         /* tp_dictoffset */
	(initproc)ImageFFmpeg_init,     /* tp_init */
	0,                         /* tp_alloc */
	Image_allocNew,           /* tp_new */
};

#endif	//WITH_FFMPEG


