/*
 * recorder.c: The actual DVB recorder
 *
 * See the main source file 'vdr.c' for copyright information and
 * how to reach the author.
 *
 * $Id: recorder.c 5.4 2021/06/19 14:21:16 kls Exp $
 */

#include "recorder.h"
#include "shutdown.h"

#define RECORDERBUFSIZE  (MEGABYTE(20) / TS_SIZE * TS_SIZE) // multiple of TS_SIZE

// The maximum time we wait before assuming that a recorded video data stream
// is broken:
#define MAXBROKENTIMEOUT 30000 // milliseconds

#define MINFREEDISKSPACE    (512) // MB
#define DISKCHECKINTERVAL   100 // seconds

static bool DebugChecks = false;

// cTsChecker and cFrameChecker are used to detect errors in the recorded data stream.
// While cTsChecker checks the continuity counter of the incoming TS packets, cFrameChecker
// works on entire frames, checking their PTS (Presentation Time Stamps) to see whether
// all expected frames arrive. The resulting number of errors is not a precise value.
// If it is zero, the recording can be safely considered error free. The higher the value,
// the more damaged the recording is.

// --- cTsChecker ------------------------------------------------------------

#define TS_CC_UNKNOWN    0xFF

class cTsChecker {
private:
  uchar counter[MAXPID];
  int errors;
  void Report(int Pid, const char *Message);
public:
  cTsChecker(void);
  void CheckTs(const uchar *Data, int Length);
  int Errors(void) { return errors; }
  };

cTsChecker::cTsChecker(void)
{
  memset(counter, TS_CC_UNKNOWN, sizeof(counter));
  errors = 0;
}

void cTsChecker::Report(int Pid, const char *Message)
{
  errors++;
  if (DebugChecks)
     fprintf(stderr, "%s: TS error #%d on PID %d (%s)\n", *TimeToString(time(NULL)), errors, Pid, Message);
}

void cTsChecker::CheckTs(const uchar *Data, int Length)
{
  int Pid = TsPid(Data);
  uchar Cc = TsContinuityCounter(Data);
  if (TsHasPayload(Data)) {
     if (TsError(Data))
        Report(Pid, "tei");
     else if (TsIsScrambled(Data))
        Report(Pid, "scrambled");
     else {
        uchar OldCc = counter[Pid];
        if (OldCc != TS_CC_UNKNOWN) {
           uchar NewCc = (OldCc + 1) & TS_CONT_CNT_MASK;
           if (Cc != NewCc)
              Report(Pid, "continuity");
           }
        }
     }
  counter[Pid] = Cc;
}

// --- cFrameChecker ---------------------------------------------------------

#define MAX_BACK_REFS  32

class cFrameChecker {
private:
  int frameDelta;
  int64_t lastPts;
  uint32_t backRefs;
  int lastFwdRef;
  int errors;
  void Report(const char *Message, int NumErrors = 1);
public:
  cFrameChecker(void);
  void SetFrameDelta(int FrameDelta) { frameDelta = FrameDelta; }
  void CheckFrame(const uchar *Data, int Length);
  void ReportBroken(void);
  int Errors(void) { return errors; }
  };

cFrameChecker::cFrameChecker(void)
{
  frameDelta = PTSTICKS / DEFAULTFRAMESPERSECOND;
  lastPts = -1;
  backRefs = 0;
  lastFwdRef = 0;
  errors = 0;
}

void cFrameChecker::Report(const char *Message, int NumErrors)
{
  errors += NumErrors;
  if (DebugChecks)
     fprintf(stderr, "%s: frame error #%d (%s)\n", *TimeToString(time(NULL)), errors, Message);
}

void cFrameChecker::CheckFrame(const uchar *Data, int Length)
{
  int64_t Pts = TsGetPts(Data, Length);
  if (Pts >= 0) {
     if (lastPts >= 0) {
        int Diff = int(round((PtsDiff(lastPts, Pts) / double(frameDelta))));
        if (Diff > 0) {
           if (Diff <= MAX_BACK_REFS) {
              if (lastFwdRef > 1) {
                 if (backRefs != uint32_t((1 << (lastFwdRef - 1)) - 1))
                    Report("missing backref");
                 }
              }
           else
              Report("missed", Diff);
           backRefs = 0;
           lastFwdRef = Diff;
           lastPts = Pts;
           }
        else if (Diff < 0) {
           Diff = -Diff;
           if (Diff <= MAX_BACK_REFS) {
              int b = 1 << (Diff - 1);
              if ((backRefs & b) != 0)
                 Report("duplicate backref");
              backRefs |= b;
              }
           else
              Report("rev diff too big");
           }
        else
           Report("zero diff");
        }
     else
        lastPts = Pts;
     }
  else
     Report("no PTS");
}

void cFrameChecker::ReportBroken(void)
{
  int MissedFrames = MAXBROKENTIMEOUT / 1000 * PTSTICKS / frameDelta;
  Report("missed", MissedFrames);
}

// --- cRecorder -------------------------------------------------------------

cRecorder::cRecorder(const char *FileName, const cChannel *Channel, int Priority)
:cReceiver(Channel, Priority)
,cThread("recording")
{
  tsChecker = new cTsChecker;
  frameChecker = new cFrameChecker;
  recordingName = strdup(FileName);
  recordingInfo = new cRecordingInfo(recordingName);
  recordingInfo->Read();
  oldErrors = max(0, recordingInfo->Errors()); // in case this is a re-started recording
  errors = oldErrors;
  lastErrors = errors;
  firstIframeSeen = false;

  // Make sure the disk is up and running:

  SpinUpDisk(FileName);

  ringBuffer = new cRingBufferLinear(RECORDERBUFSIZE, MIN_TS_PACKETS_FOR_FRAME_DETECTOR * TS_SIZE, true, "Recorder");
  ringBuffer->SetTimeouts(0, 100);
  ringBuffer->SetIoThrottle();

  int Pid = Channel->Vpid();
  int Type = Channel->Vtype();
  if (!Pid && Channel->Apid(0)) {
     Pid = Channel->Apid(0);
     Type = 0x04;
     }
  if (!Pid && Channel->Dpid(0)) {
     Pid = Channel->Dpid(0);
     Type = 0x06;
     }
  frameDetector = new cFrameDetector(Pid, Type);
  index = NULL;
  fileSize = 0;
  lastDiskSpaceCheck = time(NULL);
  lastErrorLog = 0;
  fileName = new cFileName(FileName, true);
  int PatVersion, PmtVersion;
  if (fileName->GetLastPatPmtVersions(PatVersion, PmtVersion))
     patPmtGenerator.SetVersions(PatVersion + 1, PmtVersion + 1);
  patPmtGenerator.SetChannel(Channel);
  recordFile = fileName->Open();
  if (!recordFile)
     return;
  // Create the index file:
  index = new cIndexFile(FileName, true);
  if (!index)
     esyslog("ERROR: can't allocate index");
     // let's continue without index, so we'll at least have the recording
}

cRecorder::~cRecorder()
{
  Detach();
  delete index;
  delete fileName;
  delete frameDetector;
  delete ringBuffer;
  delete frameChecker;
  delete tsChecker;
  free(recordingName);
}

#define ERROR_LOG_DELTA 1 // seconds between logging errors

void cRecorder::HandleErrors(bool Force)
{
  // We don't log every single error separately, to avoid spamming the log file:
  if (Force || time(NULL) - lastErrorLog >= ERROR_LOG_DELTA) {
     errors = tsChecker->Errors() + frameChecker->Errors();
     if (errors > lastErrors) {
        int d = errors - lastErrors;
        if (DebugChecks)
           fprintf(stderr, "%s: %s: %d error%s\n", *TimeToString(time(NULL)), recordingName, d, d > 1 ? "s" : "");
        esyslog("%s: %d error%s", recordingName, d, d > 1 ? "s" : "");
        recordingInfo->SetErrors(oldErrors + errors);
        recordingInfo->Write();
        LOCK_RECORDINGS_WRITE;
        Recordings->UpdateByName(recordingName);
        }
     lastErrors = errors;
     lastErrorLog = time(NULL);
     }
}

bool cRecorder::RunningLowOnDiskSpace(void)
{
  if (time(NULL) > lastDiskSpaceCheck + DISKCHECKINTERVAL) {
     int Free = FreeDiskSpaceMB(fileName->Name());
     lastDiskSpaceCheck = time(NULL);
     if (Free < MINFREEDISKSPACE) {
        dsyslog("low disk space (%d MB, limit is %d MB)", Free, MINFREEDISKSPACE);
        return true;
        }
     }
  return false;
}

bool cRecorder::NextFile(void)
{
  if (recordFile && frameDetector->IndependentFrame()) { // every file shall start with an independent frame
     if (fileSize > MEGABYTE(off_t(Setup.MaxVideoFileSize)) || RunningLowOnDiskSpace()) {
        recordFile = fileName->NextFile();
        fileSize = 0;
        }
     }
  return recordFile != NULL;
}

void cRecorder::Activate(bool On)
{
  if (On)
     Start();
  else
     Cancel(3);
}

void cRecorder::Receive(const uchar *Data, int Length)
{
  if (Running()) {
     static const uchar aff[TS_SIZE - 4] = { 0xB7, 0x00,
       0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
       0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
       0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
       0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
       0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
       0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
       0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
       0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
       0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
       0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
       0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
       0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
       0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
       0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
       0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
       0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
       0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
       0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
       0xFF, 0xFF}; // Length is always TS_SIZE!
     if ((Data[3] & 0b00110000) == 0b00100000 && !memcmp(Data + 4, aff, sizeof(aff)))
        return; // Adaptation Field Filler found, skipping
     int p = ringBuffer->Put(Data, Length);
     if (p != Length && Running())
        ringBuffer->ReportOverflow(Length - p);
     else if (firstIframeSeen) // we ignore any garbage before the first I-frame
        tsChecker->CheckTs(Data, Length);
     }
}

void cRecorder::Action(void)
{
  cTimeMs t(MAXBROKENTIMEOUT);
  bool InfoWritten = false;
  while (Running()) {
        int r;
        uchar *b = ringBuffer->Get(r);
        if (b) {
           int Count = frameDetector->Analyze(b, r);
           if (Count) {
              if (!Running() && frameDetector->IndependentFrame()) // finish the recording before the next independent frame
                 break;
              if (frameDetector->Synced()) {
                 if (!InfoWritten) {
                    if (frameDetector->FramesPerSecond() > 0 && DoubleEqual(recordingInfo->FramesPerSecond(), DEFAULTFRAMESPERSECOND) && !DoubleEqual(recordingInfo->FramesPerSecond(), frameDetector->FramesPerSecond())) {
                       recordingInfo->SetFramesPerSecond(frameDetector->FramesPerSecond());
                       recordingInfo->Write();
                       LOCK_RECORDINGS_WRITE;
                       Recordings->UpdateByName(recordingName);
                       }
                    InfoWritten = true;
                    cRecordingUserCommand::InvokeCommand(RUC_STARTRECORDING, recordingName);
                    frameChecker->SetFrameDelta(PTSTICKS / frameDetector->FramesPerSecond());
                    }
                 if (firstIframeSeen || frameDetector->IndependentFrame()) {
                    firstIframeSeen = true; // start recording with the first I-frame
                    if (!NextFile())
                       break;
                    if (frameDetector->NewFrame()) {
                       if (index)
                          index->Write(frameDetector->IndependentFrame(), fileName->Number(), fileSize);
                       if (frameChecker)
                          frameChecker->CheckFrame(b, Count);
                       }
                    if (frameDetector->IndependentFrame()) {
                       recordFile->Write(patPmtGenerator.GetPat(), TS_SIZE);
                       fileSize += TS_SIZE;
                       int Index = 0;
                       while (uchar *pmt = patPmtGenerator.GetPmt(Index)) {
                             recordFile->Write(pmt, TS_SIZE);
                             fileSize += TS_SIZE;
                             }
                       t.Set(MAXBROKENTIMEOUT);
                       }
                    if (recordFile->Write(b, Count) < 0) {
                       LOG_ERROR_STR(fileName->Name());
                       break;
                       }
                    HandleErrors();
                    fileSize += Count;
                    }
                 }
              ringBuffer->Del(Count);
              }
           }
        if (t.TimedOut()) {
           frameChecker->ReportBroken();
           HandleErrors(true);
           esyslog("ERROR: video data stream broken");
           ShutdownHandler.RequestEmergencyExit();
           t.Set(MAXBROKENTIMEOUT);
           }
        }
  HandleErrors(true);
}
