vdr/recorder.c

259 lines
9.7 KiB
C

/*
* 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.10 2024/09/19 09:49:02 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
// --- cRecorder -------------------------------------------------------------
cRecorder::cRecorder(const char *FileName, const cChannel *Channel, int Priority)
:cReceiver(Channel, Priority)
,cThread("recording")
{
recordingName = strdup(FileName);
recordingInfo = new cRecordingInfo(recordingName);
recordingInfo->Read();
oldErrors = max(0, recordingInfo->Errors()); // in case this is a re-started recording
errors = 0;
lastErrors = 0;
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;
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) {
if (errors > lastErrors) {
int d = errors - lastErrors;
esyslog("%s: %d new error%s (total %d)", recordingName, d, d > 1 ? "s" : "", oldErrors + errors);
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);
}
}
void cRecorder::Action(void)
{
cTimeMs t(MAXBROKENTIMEOUT);
bool InfoWritten = false;
bool pendIndependentFrame = false;
uint16_t pendNumber = 0;
off_t pendFileSize = 0;
bool pendErrors = false;
bool pendMissing = false;
// Check if this is a resumed recording, in which case we definitely missed frames:
NextFile();
if (fileName->Number() > 1 || oldErrors)
frameDetector->SetMissing();
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())) ||
frameDetector->FrameWidth() != recordingInfo->FrameWidth() ||
frameDetector->FrameHeight() != recordingInfo->FrameHeight() ||
frameDetector->AspectRatio() != recordingInfo->AspectRatio()) {
recordingInfo->SetFramesPerSecond(frameDetector->FramesPerSecond());
recordingInfo->SetFrameParams(frameDetector->FrameWidth(), frameDetector->FrameHeight(), frameDetector->ScanType(), frameDetector->AspectRatio());
recordingInfo->Write();
LOCK_RECORDINGS_WRITE;
Recordings->UpdateByName(recordingName);
}
InfoWritten = true;
cRecordingUserCommand::InvokeCommand(RUC_STARTRECORDING, recordingName);
}
if (firstIframeSeen || frameDetector->IndependentFrame()) {
firstIframeSeen = true; // start recording with the first I-frame
if (!NextFile())
break;
int PreviousErrors = 0;
int MissingFrames = 0;
if (frameDetector->NewFrame(&PreviousErrors, &MissingFrames)) {
if (index) {
if (pendNumber > 0)
index->Write(pendIndependentFrame, pendNumber, pendFileSize, pendErrors, pendMissing);
pendIndependentFrame = frameDetector->IndependentFrame();
pendNumber = fileName->Number();
pendFileSize = fileSize;
pendErrors = PreviousErrors;
pendMissing = MissingFrames;
}
if (PreviousErrors)
errors++;
if (MissingFrames)
errors++;
}
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()) {
if (pendNumber > 0)
index->Write(pendIndependentFrame, pendNumber, pendFileSize, pendErrors, pendMissing);
frameDetector->SetMissing();
errors += MAXBROKENTIMEOUT / 1000 * frameDetector->FramesPerSecond();
HandleErrors(true);
esyslog("ERROR: video data stream broken");
ShutdownHandler.RequestEmergencyExit();
t.Set(MAXBROKENTIMEOUT);
}
}
if (pendNumber > 0)
index->Write(pendIndependentFrame, pendNumber, pendFileSize, pendErrors, pendMissing);
HandleErrors(true);
}