/*
#  $Id$
# -----------------------------------------------------------------------------
#  The part of 'VideoNEXT MediaClient SDK'
# -----------------------------------------------------------------------------
#  Author: Podlesniy Gleb, 02/07/2020
#  Edited by:
#  QA by:
#  Copyright: videoNEXT LLC
# -----------------------------------------------------------------------------
*/

#define __STDC_FORMAT_MACROS 1

#include <inttypes.h>
#include "ace/OS_NS_time.h"
#include <string.h>
#include <dirent.h>
#include <algorithm>
#include <sys/stat.h>
#include "boost/format.hpp"
#include <boost/interprocess/sync/file_lock.hpp>
#include <boost/interprocess/sync/scoped_lock.hpp>

#include "AudioVideoPlaybackController.h"
#include "MediaStreamHandler.h"
#include "FileRecorder.h"

#ifndef WIN32
#include <pthread.h>
#endif

extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
}

inline int mk_dir(const char* path)
{
#if defined(_WIN32)
    std::string s(path);
    std::replace(s.begin(), s.end(), '/', '\\'); // replace all slashes to backslashes
    return _mkdir(s.c_str());
#else
    return mkdir(path, S_IRUSR|S_IWUSR|S_IXUSR|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH);
#endif
}

namespace videonext { namespace media {

static boost::shared_ptr<std::string> last_error_message()
{
    char buffer[128] = {"\0"};
#ifdef __APPLE__
    strerror_r(errno, buffer, sizeof buffer);
    return boost::shared_ptr<std::string>(new std::string(buffer));
#else
    return boost::shared_ptr<std::string>(new std::string(ACE_OS::strerror_r(errno, buffer, sizeof buffer)));
#endif
}
#define LAST_ERROR_MSG last_error_message()->c_str()

#define META_TITLE "Media data:\n" \
				   "Camera: [%s] %s\n" \
				   "Sound: [%s] %s\n" \
				   "Start time (UTC): %s\n" \
				   "Time offset from UTC: %s"

#define META_COMMENT "Camera: [%s] %s\n" \
					 "Sound: [%s] %s\n" \
					 "Time Zone: %s\n" \
					 "PTZ: %s\n" \
					 "Motion Detection: %s\n" \
					 "Analytics: %s\n" \
					 "GEO Latitude: %s\n" \
					 "GEO Longitude: %s\n" \
					 "GEO Altitude: %s"

#define META_COPYRIGHT "Name: %s\n" \
					   "Location: %s\n" \
					   "Contact: %s"

#define META_AUTHOR "Camera: [%s] %s\n" \
					"Location: %s\n" \
					"Additional: %s"

#define META_ALBUM "Camera: [%s] %s"

#define META_GENRE "Security/Surveillance"

#define BUFFER_SIZE 32768 * 4

/* NAL unit types */
enum
{
	NAL_SLICE = 1,
	NAL_DPA,
	NAL_DPB,
	NAL_DPC,
	NAL_IDR_SLICE,
	NAL_SEI,
	NAL_SPS,
	NAL_PPS,
	NAL_AUD,
	NAL_END_SEQUENCE,
	NAL_END_STREAM,
	NAL_FILLER_DATA,
	NAL_SPS_EXT,
	NAL_AUXILIARY_SLICE = 19
};

FileRecorder::StreamInfo::StreamInfo()
	: codec(NULL), width(0), height(0), extraData(NULL), extraDataSize(0)
{}

FileRecorder::StreamInfo::StreamInfo(const AVCodec* _codec, int _width, int _height, const unsigned char* _extraData, unsigned _extraDataSize)
	: codec(_codec), width(_width), height(_height), extraData(_extraData), extraDataSize(_extraDataSize)
{}

FileRecorder::FileRecorder(AudioVideoPlaybackController* playbackController, bool chunked)
    : playbackController_(playbackController), file_(NULL), status_(RECORDING_IDLE), progress_(0), chunk_timestamp_(0),
      extraData_(NULL), extraDataSize_(0),
      lock_(), cond_(lock_),
#ifdef WIN32
      threadHandle_(NULL),
#else
      thread_(0),
#endif
      formatCtx_(NULL), buf_(NULL), videoStreamAdded_(false), headerSent_(false), headerWritten_(false),
      indexFrameFound_(false), chunked_(chunked), firstVideoPts_(0), prevVdts_(0)
{
    rec_time_.tv_sec = rec_time_.tv_usec = 0;
}

FileRecorder::~FileRecorder()
{
	{
		end(true);
#ifdef WIN32
		WaitForSingleObject(threadHandle_, INFINITE);
#else
	if (thread_)
		pthread_join(thread_, NULL);
#endif
	}
#ifdef WIN32
	if (threadHandle_)
	{
		CloseHandle(threadHandle_);
	}
#endif
}

bool FileRecorder::start(const char* filename, const std::map<std::string, std::string>& metadata)
{
    //printf("\nStarting record to '%s'\n", filename);
    using namespace boost::interprocess;

    ACE_Guard<ACE_Thread_Mutex> mg(lock_);
	try
	{
		if (status_ != RECORDING_IDLE)
		{
			throw std::runtime_error("Recorder status is invalid");
		}

		if (!filename)
		{
            //chunked_ = true; // special case to make local storage
            throw std::runtime_error("File name is not specified");
		}

        filename_ = filename;
        filename_tmp_ = filename_ + ".tmp";
        metadata_ = metadata;

        if (FILE *file = fopen(filename, "r")) {
            fclose(file);
            setStatus(RECORDING_SKIPPED, 0, (std::string("Recording to '") + filename_ + " is already done").c_str());
            return true;
        }

        struct stat st;
        memset(&st, 0, sizeof(st));
        if (stat(filename_tmp_.c_str(), &st) == 0)
        {
            struct timeval tv;
            gettimeofday(&tv, NULL);
#ifdef WIN32
            //fprintf(stderr, "stat64(\"%s\"): size=%lld, a_time=%lld, m_time=%lld, c_time=%lld (now: %ld)\n",
            //        filename_tmp_.c_str(), st.st_size, st.st_atime, st.st_mtime, st.st_ctime, tv.tv_sec);
            if (tv.tv_sec - st.st_mtime < 31)
#elif APPLE
            //fprintf(stderr, "stat64(\"%s\"): size=%lld, a_time=%ld, m_time=%ld, c_time=%ld (now: %ld)\n",
            //        filename_tmp_.c_str(), st.st_size, st.st_atimespec.tv_sec, st.st_mtimespec.tv_sec, st.st_ctimespec.tv_sec, tv.tv_sec);
            if (tv.tv_sec - st.st_mtimespec.tv_sec < 31)
#else
            if (tv.tv_sec - st.st_mtime < 31)
#endif
            {
                setStatus(RECORDING_SKIPPED, 0, (std::string("Recording to '") + filename_tmp_ + "' is already started").c_str());
                return true;
            }
        }
        else {
            if (errno != ENOENT) {
                fprintf(stderr, "Fail to stat file '%s': code=%d (%s), %d\n", filename_tmp_.c_str(), errno, strerror(errno), errno == ENOENT);
#if 0
                std::string msg = "Fail to stat file '" + filename_tmp_ + "': ";
                msg += strerror(errno);
                throw std::runtime_error(msg.c_str());
#endif
            }
        }

        file_ = fopen(filename_tmp_.c_str(), "wb");
        if (file_ == NULL)
        {
            std::string msg = "Could not open output file " + filename_tmp_;
            throw std::runtime_error(msg.c_str());
        }

        file_lock f_lock(filename_tmp_.c_str());
        scoped_lock<file_lock> lock(f_lock, try_to_lock);

        if(!lock) {
            setStatus(RECORDING_SKIPPED, 0, (std::string("Recording to '") + filename_tmp_ + "' is already started").c_str());
            fclose(file_);
            file_ = NULL;
            return true;
        }

        initAV();

        setvbuf(file_, NULL, _IOFBF, BUFFER_SIZE);

        setStatus(RECORDING_STARTED);

#ifdef WIN32
		threadHandle_ = CreateThread(NULL, 0, &recordingThreadFunc, this, 0, NULL);
		if (threadHandle_ == NULL)
		{
			throw std::runtime_error("Could not create recorder thread");
		}
#else
		if (pthread_create(&thread_, 0, recordingThreadFunc, this))
		{
			throw std::runtime_error("Could not create recorder thread");
		}
#endif

        std::string msg = "Recording started to file " + filename_tmp_ + "\n";
		playbackController_->mediaStreamHandler()->log(msg.c_str());

		return true;

        // here the scoped lock is automatically released
    } catch (interprocess_exception& e) {
        setStatus(RECORDING_ERROR, 0, e.what());
    } catch (std::exception& e)	{
        freeAV();
		setStatus(RECORDING_ERROR, 0, e.what());
	}

    return false;
}

bool FileRecorder::start(time_t ts, const std::string& base_storage_dir, const std::map<std::string, std::string>& meta)
{
    struct tm ptm;
    char tmp_buff[80];
    chunk_timestamp_ = ts;

    /* Fill ptm struct */
    ACE_OS::gmtime_r(&ts, &ptm);
    DIR *dir;

    // Creating base_dir if needed */
    if ((dir = opendir(base_storage_dir.c_str())) == NULL)
    {
        if (mk_dir(base_storage_dir.c_str()) == -1)
        {
            throw std::runtime_error(std::string("Couldn't create dir ") + (base_storage_dir + ": ") + LAST_ERROR_MSG);
        }
    }
    else closedir(dir);

    // Creating YYMMDD dir if needed
    strftime(tmp_buff, sizeof(tmp_buff), "%y%m%d", &ptm);
    std::string dayDir(base_storage_dir + "/" + tmp_buff);
    if ((dir = opendir(dayDir.c_str())) == NULL)
    {
        if (mk_dir(dayDir.c_str()) == -1)
        {
            throw std::runtime_error(std::string("Couldn't create dir ") + (dayDir + ": ") + LAST_ERROR_MSG);
        }
    }
    else closedir(dir);

    char chunkSuffix_str[16];
    if (snprintf(chunkSuffix_str, sizeof chunkSuffix_str, "-%02d", 1/*hardcoded*/) == -1)
    {
        throw std::runtime_error(LAST_ERROR_MSG);
    }

    // Creating HH dir if needed
    strftime(tmp_buff, sizeof(tmp_buff), "%H", &ptm);
    std::string hourDir(dayDir + "/" + tmp_buff + chunkSuffix_str);

    if ((dir = opendir(hourDir.c_str())) == NULL)
    {
        if (mk_dir(hourDir.c_str()) == -1)
        {
            throw std::runtime_error(std::string("Couldn't create dir ") + (hourDir + ": ") + LAST_ERROR_MSG);
        }
#ifndef _WIN32
        std::string hourDirLink(dayDir + "/" + tmp_buff + ".0000" + chunkSuffix_str);
        if (symlink(hourDir.c_str(), hourDirLink.c_str()) == -1)
        {
            throw std::runtime_error(std::string("Couldn't make link ") + (hourDirLink + ": ") + LAST_ERROR_MSG);
        }
#endif
    }
    else closedir(dir);

    /* Creating full path to chunk */
    strftime(tmp_buff, sizeof(tmp_buff), "%y%m%d%H%M%S.mov", &ptm);

    return start((hourDir + "/" + tmp_buff).c_str(), meta);
}

bool FileRecorder::is_running()
{
    ACE_Guard<ACE_Thread_Mutex> mg(lock_);
    return status_ == RECORDING_STARTED || status_ == RECORDING_ENDING ? true : false;
}

void FileRecorder::end(bool stop)
{
    ACE_Guard<ACE_Thread_Mutex> mg(lock_);

    if (status_ == RECORDING_SKIPPED)
    {
        setStatus(RECORDING_ENDED);
        return;
    }

	if (status_ != RECORDING_STARTED)
	{
		return;
	}

    if (stop)
    {
        setStatus(RECORDING_ERROR, 0, "Playback was stopped");
        frameQueue_.push(CPMediaFrame(0));
        cond_.signal();
        return;
    }

	setStatus(RECORDING_ENDING);

	char msg[128];
	snprintf(msg, sizeof(msg), "Recording ending: %d frames in queue\n", static_cast<int>(frameQueue_.size()));
	playbackController_->mediaStreamHandler()->log(msg);

	frameQueue_.push(CPMediaFrame(0));
	cond_.signal();
}

void FileRecorder::pushFrame(const StreamInfo& streamInfo, const CPMediaFrame& frame)
{
    if (rec_time_.tv_sec == 0)
        gettimeofday(&rec_time_, NULL);

    ACE_Guard<ACE_Thread_Mutex> mg(lock_);
    //printf("pushFrame(), status=%d\n", status_);
    if (status_ != RECORDING_STARTED)
    {
        return;
    }

	if (!videoStreamAdded_)
	{
		if (streamInfo.extraData != streamInfo_.extraData || streamInfo.extraDataSize != streamInfo_.extraDataSize)
		{
			delete[] extraData_;
			extraData_ = NULL;
			extraDataSize_ = streamInfo.extraDataSize;
			if (streamInfo.extraData && streamInfo.extraDataSize)
			{
				extraData_ = new unsigned char[extraDataSize_];
				memcpy(extraData_, streamInfo.extraData, extraDataSize_);
			}
		}
		streamInfo_ = streamInfo;
	}

	frameQueue_.push(frame);
	cond_.signal();
}

bool FileRecorder::popFrame(CPMediaFrame& frame, size_t& queueSize)
{
    ACE_Guard<ACE_Thread_Mutex> mg(lock_);
	while (frameQueue_.empty())
	{
		cond_.wait();
	}
	frame = frameQueue_.front();
	frameQueue_.pop();
	queueSize = frameQueue_.size();

	return frame.get();
}

VN_PLAYER_RECORDING_STATUS FileRecorder::status()
{
    //ACE_Guard<ACE_Thread_Mutex> mg(lock_);
	return status_;
}

void FileRecorder::setStatus(VN_PLAYER_RECORDING_STATUS status, int progress, const char* error)
{
	std::string newError = error ? error : "";
    //ACE_Guard<ACE_Thread_Mutex> mg(lock_);
	if (status_ != status || progress_ != progress || error_ != newError)
	{
		status_ = status;
		progress_ = progress;
		error_ = newError;

		vn_player_recording_status_t st;
		st.status = status_;
		st.progress = progress_;
        st.timestamp = chunk_timestamp_;
        st.filename = status == RECORDING_ENDED && !filename_.empty() ? filename_.c_str()
                                                                      : !filename_tmp_.empty() ? filename_tmp_.c_str() : NULL;
		st.error_str = !error_.empty() ? error_.c_str() : NULL;
		playbackController_->mediaStreamHandler()->recordingStatusChanged(st);

		if (!error_.empty())
		{
			std::string msg(status == RECORDING_ERROR ? "Recording error: " + error_ + "\n" : "Recording message: " + error_ + "\n");
			playbackController_->mediaStreamHandler()->log(msg.c_str());
		}
	}
}

#ifdef WIN32
/*static*/ DWORD WINAPI FileRecorder::recordingThreadFunc(LPVOID arg)
#else
/*static*/ void* FileRecorder::recordingThreadFunc(void* arg)
#endif
{
	FileRecorder* recorder = (FileRecorder*)arg;
	recorder->recordingThread();
	return 0;
}

void FileRecorder::recordingThread()
{
	std::string error;
	while (status() == RECORDING_STARTED && error.empty())
	{
		CPMediaFrame frame(0);
		size_t queueSize = 0;
		int count = 0;
		while (popFrame(frame, queueSize))
		{
			if (!videoStreamAdded_)
			{
				try
				{
                    if (addVideoStream(frame))
					{
						videoStreamAdded_ = true;
                        addMetaData(frame);

						char msg[256];
						snprintf(msg, sizeof(msg), "Recording: video codec %s, %dx%d, extra data size %d\n",
							streamInfo_.codec->name, streamInfo_.width, streamInfo_.height, streamInfo_.extraDataSize);

                        for (unsigned i= 0; i < (streamInfo_.extraDataSize>180?180:streamInfo_.extraDataSize); i++)
                            fprintf(stderr, "%02X ", (uint8_t)((unsigned char*)streamInfo_.extraData)[i]);
                        fprintf(stderr, "\n");

						playbackController_->mediaStreamHandler()->log(msg);
                    }
				}
				catch (const std::exception& e)
				{
					error = e.what();
					break;
				}
			}

			if (!videoStreamAdded_)
			{
				continue;
			}

			try
			{
				writeFrame(frame);
				count++;
			}
			catch (std::exception& e)
			{
				error = e.what();
				break;
			}

			if (status() == RECORDING_ENDING)
			{
				int progress = count / (queueSize + 1);
				setStatus(RECORDING_ENDING, progress);
			}
		}
	}

    //using namespace boost::interprocess;

    try {
        //file_lock f_lock(filename_tmp_.c_str());
        //scoped_lock<file_lock> lock(f_lock);

        if (error.empty() && status() != RECORDING_ERROR)
        {
            endWrite();

            if (file_) {
                fclose(file_);
                file_ = NULL;
            }

            if (!indexFrameFound_)
            {
                playbackController_->mediaStreamHandler()->log("Recording: didn't get key frame, writing was not started\n");
                remove(filename_tmp_.c_str());
            }
            else
                rename(filename_tmp_.c_str(), filename_.c_str());

            char msg[128];
            struct timeval tv;
            gettimeofday(&tv, NULL);
            uint64_t rec_dur = (tv.tv_sec - rec_time_.tv_sec) * 1000000 + tv.tv_usec - rec_time_.tv_usec;
            snprintf(msg, sizeof(msg), "Recording ended in %" PRId64 ".%06" PRId64 " secs\n", rec_dur / 1000000, rec_dur % 1000000);
            //fprintf(stderr, "%s", msg);
            playbackController_->mediaStreamHandler()->log(msg);

            freeAV();

            setStatus(RECORDING_ENDED);
        }
        else
        {
            if (file_) {
                fclose(file_);
                file_ = NULL;
            }

            freeAV();

            remove(filename_.c_str());
            remove(filename_tmp_.c_str());
            if (status() != RECORDING_ERROR)
                setStatus(RECORDING_ERROR, 0, error.c_str());
        }

        return;
        // here the scoped lock is automatically released
    /*} catch (interprocess_exception& e) {
        setStatus(RECORDING_ERROR, 0, e.what());*/
    } catch (std::exception& e)	{
        setStatus(RECORDING_ERROR, 0, e.what());
    } catch (...) {
        setStatus(RECORDING_ERROR, 0, "Unknown exception");
    }

    if (file_) {
        fclose(file_);
        file_ = NULL;
    }

    freeAV();

    remove(filename_.c_str());
    remove(filename_tmp_.c_str());
}

void FileRecorder::initAV()
{
	freeAV();

	buf_ = new uint8_t[BUFFER_SIZE];

	formatCtx_ = avformat_alloc_context();
	if (!formatCtx_)
	{
		throw std::runtime_error("Could not allocate format context");
	}

	formatCtx_->oformat = av_guess_format(NULL, ".mov", NULL);
	if (!formatCtx_->oformat)
	{
		throw std::runtime_error("Unable to find a suitable output format for '.mov' container");
	}

#if 0
    printf("Format name: %s\n", formatCtx_->oformat->name);
    printf("Format long name: %s\n", formatCtx_->oformat->long_name);
    printf("Format Mime type: %s\n", formatCtx_->oformat->mime_type);
    printf("Format extensions: %s\n", formatCtx_->oformat->extensions);
    printf("Format Video Codec: %d\n",formatCtx_->oformat->video_codec);
    printf("AVFMT_NOFILE: %d\n", !( formatCtx_->oformat->flags & AVFMT_NOFILE ));

    //avio_open(&formatCtx_->pb, "-.mov", AVIO_FLAG_WRITE);
#endif
	formatCtx_->pb = avio_alloc_context(buf_, /*buffer */
										BUFFER_SIZE, /*buffer_size*/
										AVIO_FLAG_WRITE, /*write_flag*/
										this, /*opaque*/
										0, /*int (*read_packet)(void *opaque, uint8_t *buf, int buf_size)*/
										write_i, /*int (*write_packet)(void *opaque, uint8_t *buf, int buf_size)*/
										seek_i /*offset_t (*seek)(void *opaque, offset_t offset, int whence)*/
	);
    formatCtx_->pb->max_packet_size = formatCtx_->pb->buffer_size;
}

void FileRecorder::freeAV()
{
	videoStreamAdded_ = false;
	headerSent_ = false;
	headerWritten_ = false;
	indexFrameFound_ = false;
	firstVideoPts_ = 0;
	prevVdts_ = 0;

	if (formatCtx_ && formatCtx_->pb)
	{
		av_free(formatCtx_->pb);
	}

	if (formatCtx_)
	{
		avformat_free_context(formatCtx_);
		formatCtx_ = NULL;
	}

	delete[] extraData_;
	extraData_ = NULL;
	extraDataSize_ = 0;

	delete[] buf_;
	buf_ = NULL;
}

bool FileRecorder::addVideoStream(const CPMediaFrame& frame)
{
	if (!frame.get() || !frame->data || ! frame->size)
	{
		return false;
	}

	if (!streamInfo_.codec || !streamInfo_.width || !streamInfo_.height)
	{
		throw std::runtime_error("Invalid stream info");
	}

	AVCodecID codecId = streamInfo_.codec->id;

	uint8_t* mpeg4ptr = NULL;
	int mpeg4size = 0;
	if (codecId == AV_CODEC_ID_MPEG4)
	{
		mpeg4ptr = findMpeg4ExtraData(frame->data, frame->size, mpeg4size);
		if (!mpeg4ptr)
		{
			return false;
		}
	}

	int mpeg2extraDataSize = 0;
	if (codecId == AV_CODEC_ID_MPEG2VIDEO)
	{
		if (!isIndexFrame(frame->data, frame->size))
		{
			return false;
		}
		// split VIDEO_SEQUENCE and EXTENSION
		int i;
		for (i = 0; i < static_cast<int>(frame->size) - 5; i++)
		{
			if (frame->data[i] == 0 && frame->data[i + 1] == 0 && frame->data[i + 2] == 1 && (frame->data[i + 3] == 0xB8 || frame->data[i + 3] == 0x00))
				break; // found
		}

		if (i > 256)
		{
			throw std::runtime_error("MPEG2 VIDEO_SEQUENCE and EXTENSION not found");
		}
		mpeg2extraDataSize = i;
	}

	AVStream* st = avformat_new_stream(formatCtx_, NULL);
	if (!st)
	{
		throw std::runtime_error("Could not alloc video stream");
	}

	st->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
	st->codecpar->codec_id = codecId;
	st->codecpar->width = streamInfo_.width;
	st->codecpar->height = streamInfo_.height;

	if (codecId == AV_CODEC_ID_MPEG4)
	{
		st->codecpar->extradata_size = mpeg4size;
		st->codecpar->extradata = (uint8_t*)av_mallocz(st->codecpar->extradata_size);
		memcpy(st->codecpar->extradata, mpeg4ptr, mpeg4size);
	}
	else if (codecId == AV_CODEC_ID_MPEG2VIDEO)
	{
		st->codecpar->extradata_size = mpeg2extraDataSize;
		st->codecpar->extradata = (uint8_t*)av_mallocz(st->codecpar->extradata_size);
		memcpy(st->codecpar->extradata, frame->data, st->codecpar->extradata_size);
	}
	else if (extraData_ && extraDataSize_)
	{
		st->codecpar->extradata_size = extraDataSize_;
		st->codecpar->extradata = (uint8_t*)av_mallocz(st->codecpar->extradata_size);
		memcpy(st->codecpar->extradata, extraData_, extraDataSize_);
	}

	st->time_base.num = 1;
	st->time_base.den = 90000;
	st->codecpar->frame_size = 1;
	formatCtx_->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
	return true;
}

void FileRecorder::addMetaData(const CPMediaFrame& frame)
{
	if (!frame.get() || !frame->data || ! frame->size)
	{
		return;
	}

	char startTimeFormated[64];
	time_t startTime = frame->tv.tv_sec;
	struct tm ptm;
	ACE_OS::gmtime_r(&startTime, &ptm);
	strftime(startTimeFormated, sizeof(startTimeFormated), "%Y/%m/%d %H:%M:%S", &ptm);

	std::string title((boost::format(META_TITLE)
		% metadata_["Id"] % metadata_["Name"]
		% metadata_["AudioId"] % metadata_["AudioName"]
		% startTimeFormated
		% metadata_["TimeZone"]).str());

	av_dict_set(&formatCtx_->metadata, "title", title.c_str(), 0);

	std::string comment((boost::format(META_COMMENT)
		% metadata_["Id"] % metadata_["Name"]
		% metadata_["AudioId"] % metadata_["AudioName"]
		% metadata_["TimeZone"]
		% metadata_["PTZ"]
		% metadata_["MD"]
		% metadata_["Analytics"]
		% metadata_["GeoLat"]
		% metadata_["GeoLong"]
		% metadata_["GeoAlt"]).str());

	av_dict_set(&formatCtx_->metadata, "comment", comment.c_str(), 0);

	std::string copyright((boost::format(META_COPYRIGHT)
		% metadata_["SystemName"]
		% metadata_["SystemLocation"]
		% metadata_["ContactInfo"]).str());

	av_dict_set(&formatCtx_->metadata, "copyright", copyright.c_str(), 0);

	std::string author((boost::format(META_AUTHOR)
		% metadata_["Id"] % metadata_["Name"]
		% metadata_["Location"]
		% metadata_["Note"]).str());

	av_dict_set(&formatCtx_->metadata, "author", author.c_str(), 0);

	std::string album((boost::format(META_ALBUM) % metadata_["Id"] % metadata_["Name"]).str());

	av_dict_set(&formatCtx_->metadata, "album", album.c_str(), 0);

	av_dict_set(&formatCtx_->metadata, "genre", META_GENRE, 0);

	av_dict_set(&formatCtx_->metadata, "year", (boost::format("%1%") % (startTime / (24 * 3600 * 366) + 1970)).str().c_str(), 0);
}

void FileRecorder::fillAvPkt(uint8_t* frame, int frameSize, AVPacket& pkt)
{
	AVCodecID codecId = streamInfo_.codec->id;
	if (codecId == AV_CODEC_ID_MPEG4)
	{
		uint8_t* ptr = findMpeg4Start(frame, frameSize);
		if (!ptr)
		{
			playbackController_->mediaStreamHandler()->log("Recording: Start of frame not found. Skiping...\n");
			return;
		}

		pkt.data = ptr;
		pkt.size = frameSize - (ptr - frame);

		if (isIndexFrame(pkt.data, pkt.size))
			pkt.flags |= AV_PKT_FLAG_KEY;
		else
			pkt.flags = 0;
	}
	else if (codecId == AV_CODEC_ID_MJPEG)
	{
		pkt.data = frame;
		pkt.size = frameSize;
		pkt.flags |= AV_PKT_FLAG_KEY;
	}
	else if (codecId == AV_CODEC_ID_H264)
	{
		for (int i = frameSize - 5; i >= 0; i--)
		{
			if (frame[i] == 0x00 && frame[i + 1] == 0x00 && frame[i + 2] == 0x00 && frame[i + 3] == 0x01 && frame[i + 4] == NAL_SEI)
			{
				frameSize = i;
				break;
			}
		}

		pkt.data = frame;
		pkt.size = frameSize;

		if (isIndexFrame(pkt.data, pkt.size))
			pkt.flags |= AV_PKT_FLAG_KEY;
		else
			pkt.flags = 0;
	}
    else if (codecId == AV_CODEC_ID_HEVC)
    {
        for (int i = frameSize - 5; i >= 0; i--)
        {
            if (frame[i] == 0x00 && frame[i + 1] == 0x00 && frame[i + 2] == 0x00 && frame[i + 3] == 0x01 && (frame[i + 4] >> 1 & 0x3F) == 39)
            {
                frameSize = i;
                break;
            }
        }

        pkt.data = frame;
        pkt.size = frameSize;
        //printf("FRAMESIZE=%d\n", frameSize);

        if (isIndexFrame(pkt.data, pkt.size))
            pkt.flags |= AV_PKT_FLAG_KEY;
        else
            pkt.flags = 0;
    }
	else if (codecId == AV_CODEC_ID_MPEG2VIDEO)
	{
		pkt.data = frame;
		pkt.size = frameSize;

		if (isIndexFrame(pkt.data, pkt.size))
			pkt.flags |= AV_PKT_FLAG_KEY;
		else
			pkt.flags = 0;
	}
}

uint8_t* FileRecorder::findMpeg4ExtraData(uint8_t* frame, int frameSize, int& size)
{
	size = 0;
	if (!frame)
	{
		return NULL;
	}
	/* skiping until meet 0x000001B0*/
	uint8_t* ptr = frame;
	uint8_t* pStart = NULL;
	while (ptr < frame + (frameSize - 4))
	{
		if (ptr[0] == 0 && ptr[1] == 0 && ptr[2] == 1 && ptr[3] == 0xB0)
		{
			pStart = ptr;
		}
		/* skiping until meet 0x000001B2 or length exceeded 55 */
		if (pStart && ptr[0] == 0 && ptr[1] == 0 && ptr[2] == 1 && ptr[3] == 0xB2)
		{
			break;
		}
		if (pStart)
		{
			size++;
			if (size >= 55)
			{
				break;
			}
		}
		ptr++;
	}
	return pStart;
}

uint8_t* FileRecorder::findMpeg4Start(uint8_t* frame, int frameSize)
{
	if (!frame)
	{
		return NULL;
	}
	/* skiping until meet 0x000001[B3|B6]*/
	uint8_t* ptr = frame;
	bool isStartOfFrameFound = false;
	while (ptr < frame + (frameSize - 4))
	{
		if (ptr[0] == 0 && ptr[1] == 0 && ptr[2] == 1 && (ptr[3] == 0xB3 || ptr[3] == 0xB6))
		{
			isStartOfFrameFound = true;
			break;
		}
		ptr++;
	}
	return isStartOfFrameFound ? ptr : NULL;
}

bool FileRecorder::isIndexFrame(uint8_t* frame, int size)
{
	AVCodecID codecId = streamInfo_.codec->id;
	if (codecId == AV_CODEC_ID_MPEG4)
	{
		if (size < 30)
		{
			playbackController_->mediaStreamHandler()->log("Recording: Frame size is too small for parsing\n");
			return false;
		}

		unsigned first4Bytes;
		uint8_t* ptr = frame;

		first4Bytes = (ptr[0] << 24) | (ptr[1] << 16) | (ptr[2] << 8) | ptr[3];
		if (first4Bytes == 0x000001b0 || first4Bytes == 0x000001b5 || first4Bytes == 0x000001b3)
		{
			return true; // because next going I_VOP
		}

		ptr += 4; // USER_DATA
		uint8_t* pos = (uint8_t*)memchr(ptr, '\0', size - (ptr - frame));
		if (pos == NULL || size - (pos - frame) < 4)
			return false;
		ptr += (pos - ptr + 1); // Assuming here is SKM header + zero byte
		if (ptr + 4 > frame + size)
			return false;
		first4Bytes = (ptr[0] << 24) | (ptr[1] << 16) | (ptr[2] << 8) | ptr[3];

		if (first4Bytes == 0x000001b0 || first4Bytes == 0x000001b5 || first4Bytes == 0x000001b3)
		{
			return true; // because next going I_VOP
		}

		if (first4Bytes == 0x000001b6 && ptr + 4 < frame + size) // VOP
		{
			uint8_t nextByte = ptr[4];
			uint8_t vop_coding_type = nextByte >> 6;
			return vop_coding_type == 0;
		}
		else
			return false;
	}
	else if (codecId == AV_CODEC_ID_MPEG2VIDEO)
	{
		if (size < 4)
		{
			return false;
		}

		unsigned char* ptr = frame;
		unsigned first4Bytes = (ptr[0] << 24) | (ptr[1] << 16) | (ptr[2] << 8) | ptr[3];

		if (first4Bytes == 0x000001b3 || first4Bytes == 0x000001b8)
		{
			return true; // because next going I_VOP
		}
		else if (first4Bytes == 0x00000100 && ptr + 7 < frame + size) // VOP
		{
			ptr += 4;
			unsigned next4Bytes = (ptr[0] << 24) | (ptr[1] << 16) | (ptr[2] << 8) | ptr[3];
			unsigned char picture_coding_type = (next4Bytes & 0x00380000) >> 19;
			if (picture_coding_type == 1) // Intra-coded
				return true;
		}

		return false;
	}
	else if (codecId == AV_CODEC_ID_H264)
	{
		if (size < 5)
		{
			return false;
		}

		unsigned char* ptr = frame;
		ptr += 4; // NALU_START_CODE

		if ((ptr[0] & 0x1F) == 6 /*SEI*/)
		{
			ptr += 3; //SEI header
		}
		else if ((ptr[0] & 0x1F) == 7 /*SPS*/ || (ptr[0] & 0x1F) == 8 /*PPS*/)
		{
			return true;
		}
		return ptr < frame + size && (ptr[0] & 0x1F) == 5; // idr slice
	}
    else if (codecId == AV_CODEC_ID_HEVC)
    {
        if (size < 5)
            return false;

        unsigned char* ptr = frame;
        ptr += 4; // NALU_START_CODE
        uint8_t type = ptr[0] >> 1 & 0x3F;
        //printf("TYPE: %d, size=%d\n", type, size-4);

        if ( type == 39 /*SEI*/) {
            ptr += 3; //SEI header
            type = ptr[0] >> 1 & 0x3F;
            //printf("TYPE after SEI: %d, size: %d\n", type, size);
        } else if (type == 32) {
#if 0
            fprintf(stderr, "PARAM_SET. Size: %d\n", size-4);
            for(int i=0; i<(size > 184 ? 180 : size-4); i++) {
                if(i%8 == 0)
                    fprintf(stderr, "\n");
                fprintf(stderr, "0x%02x ", ptr[i]);
            }
            fprintf(stderr, "\n");
#endif
            return true;
        }

        return ptr < frame + size && (type == 32 || type == 19 || type == 20); // IDR slice or VPS
    }
	else if (codecId == AV_CODEC_ID_MJPEG)
	{
		return true;
	}

	return false; // no sense. just preventing compiler's warning
}

void FileRecorder::writeFrame(const CPMediaFrame& frame)
{
	if (!frame.get() || !frame->data || !frame->size)
	{
		return;
	}

    if (!headerSent_)
	{
		headerSent_ = true;
		int ret = avformat_write_header(formatCtx_, 0);
		if (ret < 0)
		{
			throw std::runtime_error("Could not write header");
		}
		headerWritten_ = true;
	}

	if (formatCtx_->pb == NULL)
	{
		throw std::runtime_error("Pointer to buffer is NULL");
	}

	int64_t pts = (int64_t)frame->tv.tv_sec * 1000000 + frame->tv.tv_usec;
	if (pts < 0)
	{
		throw std::runtime_error("Invalid frame time");
	}

	if (firstVideoPts_ == 0)
	{
		firstVideoPts_ = pts;
	}
	pts -= firstVideoPts_;

	AVPacket pkt = {};
	av_init_packet(&pkt);

	fillAvPkt(frame->data, frame->size, pkt);

	if (pkt.size == 0)
	{
		av_packet_unref(&pkt);
		return;
	}

	if (!indexFrameFound_)
	{
		if (isIndexFrame(pkt.data, pkt.size))
		{
			indexFrameFound_ = true;
			playbackController_->mediaStreamHandler()->log("Recording: got key frame, writing started\n");
		}
		else
		{
			av_packet_unref(&pkt);
			return;
		}
	}

	pkt.stream_index = formatCtx_->streams[0]->index;

	pkt.pts = av_rescale_q(pts, (AVRational){1, 1000000}, formatCtx_->streams[0]->time_base);
	pkt.dts = pkt.pts;

	if (prevVdts_ != 0 && pkt.dts <= prevVdts_)
	{
		pkt.dts = prevVdts_ + 1;
		pkt.pts = pkt.dts;
	}
	prevVdts_ = pkt.dts;

//	formatCtx_->streams[0]->codec->frame_number++;

    if (av_interleaved_write_frame(formatCtx_, &pkt) != 0)
	{
		av_packet_unref(&pkt);
		throw std::runtime_error("Could not write video frame");
	}

	av_packet_unref(&pkt);
        
    if (av_write_frame(formatCtx_, NULL) < 0)
	{
		throw std::runtime_error("Could not flush video frame");
	}
	avio_flush(formatCtx_->pb);
}

void FileRecorder::endWrite()
{
	if (!headerWritten_)
	{
		return;
	}

	// here is moov atom will be populated
	if (av_write_trailer(formatCtx_) != 0)
	{
		throw std::runtime_error("Could not write trailer");
	}

    av_free(formatCtx_->pb);
    //avio_close(formatCtx_->pb);
	formatCtx_->pb = NULL;
}

/*static*/ int FileRecorder::write_i(void* opaque, uint8_t* buf, int buf_size)
{
	FileRecorder* recorder = (FileRecorder*)opaque;
	if (buf_size > 8 && (buf[4] == 's' && buf[5] == 't' && buf[6] == 'b' && buf[7] == 'l'))
	{
        //printf("write_i_1\n");
		for (int i = 8; i < buf_size - 4; i++)
		{
			if (buf[i] == 'm' && buf[i + 1] == '2' && buf[i + 2] == 'v' && buf[i + 3] == '1')
			{
                //printf("write_i_2\n");
				memcpy(&buf[i], "mpg2", 4);
				break;
			}
		}
	}
    int bytes_written = fwrite(buf, 1, buf_size, recorder->file_);
    if (bytes_written != buf_size || errno != 0)
        fprintf(stderr, "bytes_written=%d, buf_size=%d, error=%s\n", bytes_written, buf_size, LAST_ERROR_MSG);
    return bytes_written;
}

/*static*/ int64_t FileRecorder::seek_i(void* opaque, int64_t offset, int whence)
{
	FileRecorder* recorder = (FileRecorder*)opaque;
	if (fseek(recorder->file_, offset, whence) == -1)
		return -1;
	else
		return offset;
}

} // namespace media
} // namespace videonext
