vdr 2.7.9
recorder.c
Go to the documentation of this file.
1/*
2 * recorder.c: The actual DVB recorder
3 *
4 * See the main source file 'vdr.c' for copyright information and
5 * how to reach the author.
6 *
7 * $Id: recorder.c 5.13 2025/12/29 14:14:05 kls Exp $
8 */
9
10#include "recorder.h"
11#include "shutdown.h"
12
13#define RECORDERBUFSIZE (MEGABYTE(20) / TS_SIZE * TS_SIZE) // multiple of TS_SIZE
14
15// The maximum time we wait before assuming that a recorded video data stream
16// is broken:
17#define MAXBROKENTIMEOUT 30000 // milliseconds
18#define LEFTOVERTIMEOUT 2000 // milliseconds
19
20#define MINFREEDISKSPACE (512) // MB
21#define DISKCHECKINTERVAL 100 // seconds
22
23// --- cRecorder -------------------------------------------------------------
24
25cRecorder::cRecorder(const char *FileName, const cChannel *Channel, int Priority)
26:cReceiver(Channel, Priority)
27,cThread("recording")
28{
29 recordingName = strdup(FileName);
31 recordingInfo->Read();
32 tmpErrors = recordingInfo->TmpErrors();
33 oldErrors = max(0, recordingInfo->Errors()) - tmpErrors; // in case this is a re-started recording
34 errors = 0;
36 working = false;
37 firstIframeSeen = false;
38
39 // Make sure the disk is up and running:
40
41 SpinUpDisk(FileName);
42
44 ringBuffer->SetTimeouts(0, 100);
45 ringBuffer->SetIoThrottle();
46
47 int Pid = Channel->Vpid();
48 int Type = Channel->Vtype();
49 if (!Pid && Channel->Apid(0)) {
50 Pid = Channel->Apid(0);
51 Type = 0x04;
52 }
53 if (!Pid && Channel->Dpid(0)) {
54 Pid = Channel->Dpid(0);
55 Type = 0x06;
56 }
57 frameDetector = new cFrameDetector(Pid, Type);
58 if ( Type == 0x1B // MPEG4 video
59 && (Setup.DumpNaluFill ? (strstr(FileName, "NALUKEEP") == NULL) : (strstr(FileName, "NALUDUMP") != NULL))) { // MPEG4
60 isyslog("Starting NALU fill dumper");
62 naluStreamProcessor->SetPid(Pid);
63 }
64 else
66 index = NULL;
67 fileSize = 0;
68 lastDiskSpaceCheck = time(NULL);
69 lastErrorLog = 0;
70 fileName = new cFileName(FileName, true);
71 // Check if this is a resumed recording, in which case we definitely missed frames:
72 NextFile();
73 if (fileName->Number() > 1 || oldErrors)
75 patPmtGenerator.SetChannel(Channel);
76 recordFile = fileName->Open();
77 if (!recordFile)
78 return;
79 // Create the index file:
80 index = new cIndexFile(FileName, true);
81 if (!index)
82 esyslog("ERROR: can't allocate index");
83 // let's continue without index, so we'll at least have the recording
84}
85
87{
88 Cancel(3); // in case the caller didn't call Stop()
89 Detach();
91 long long int TotalPackets = naluStreamProcessor->GetTotalPackets();
92 long long int DroppedPackets = naluStreamProcessor->GetDroppedPackets();
93 isyslog("NALU fill dumper: %lld of %lld packets dropped, %lli%%", DroppedPackets, TotalPackets, TotalPackets ? DroppedPackets*100/TotalPackets : 0);
95 }
96 delete index;
97 delete fileName;
98 delete frameDetector;
99 delete ringBuffer;
100 free(recordingName);
101}
102
104{
105 Cancel(3);
106}
107
108#define ERROR_LOG_DELTA 1 // seconds between logging errors
109
111{
112 // We don't log every single error separately, to avoid spamming the log file:
113 if (Force || time(NULL) - lastErrorLog >= ERROR_LOG_DELTA) {
114 int AllErrors = oldErrors + errors + tmpErrors;
115 if (AllErrors != lastErrors) {
116 int d = AllErrors - lastErrors;
117 esyslog("%s: %d new error%s (total %d)", recordingName, d, d > 1 ? "s" : "", AllErrors);
118 recordingInfo->SetErrors(AllErrors, tmpErrors);
119 recordingInfo->Write();
121 Recordings->UpdateByName(recordingName);
122 lastErrors = AllErrors;
123 }
124 lastErrorLog = time(NULL);
125 }
126}
127
129{
130 if (time(NULL) > lastDiskSpaceCheck + DISKCHECKINTERVAL) {
131 int Free = FreeDiskSpaceMB(fileName->Name());
132 lastDiskSpaceCheck = time(NULL);
133 if (Free < MINFREEDISKSPACE) {
134 dsyslog("low disk space (%d MB, limit is %d MB)", Free, MINFREEDISKSPACE);
135 return true;
136 }
137 }
138 return false;
139}
140
142{
143 if (recordFile && frameDetector->IndependentFrame()) { // every file shall start with an independent frame
144 if (fileSize > MEGABYTE(off_t(Setup.MaxVideoFileSize)) || RunningLowOnDiskSpace()) {
145 recordFile = fileName->NextFile();
146 fileSize = 0;
147 }
148 }
149 return recordFile != NULL;
150}
151
153{
154 if (On)
155 Start();
156 else
157 Cancel(3);
158}
159
160void cRecorder::Receive(const uchar *Data, int Length)
161{
162 if (working) {
163 static const uchar aff[TS_SIZE - 4] = { 0xB7, 0x00,
164 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
165 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
166 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
167 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
168 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
169 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
170 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
171 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
172 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
173 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
174 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
175 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
176 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
177 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
178 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
179 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
180 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
181 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
182 0xFF, 0xFF}; // Length is always TS_SIZE!
183 if ((Data[3] & 0b00110000) == 0b00100000 && !memcmp(Data + 4, aff, sizeof(aff)))
184 return; // Adaptation Field Filler found, skipping
185 int p = ringBuffer->Put(Data, Length);
186 if (p != Length && working)
187 ringBuffer->ReportOverflow(Length - p);
188 }
189}
190
191#define MIN_IFRAMES_FOR_LAST_PTS 2
192
193void cRecorder::GetLastPts(const char *RecordingName)
194{
195 dsyslog("getting last PTS of '%s'", RecordingName);
196 if (cIndexFile *Index = new cIndexFile(RecordingName, false)) {
197 cFileName *FileName = new cFileName(RecordingName, false);
198 uint16_t FileNumber;
199 off_t FileOffset;
200 bool Independent;
201 uchar Buffer[MAXFRAMESIZE];
202 int Length;
203 int64_t LastPts = -1;
204 int IframesSeen = 0;
205 cPatPmtParser PatPmtParser;
206 bool GotPatPmtVersions = false;
207 for (int i = Index->Last(); i >= 0; i--) {
208 if (Index->Get(i, &FileNumber, &FileOffset, &Independent, &Length)) {
209 if (cUnbufferedFile *f = FileName->SetOffset(FileNumber, FileOffset)) {
210 int l = ReadFrame(f, Buffer, Length, sizeof(Buffer));
211 if (l > 0) {
212 int64_t Pts = TsGetPts(Buffer, l);
213 if (LastPts < 0 || PtsDiff(LastPts, Pts) > 0) {
214 LastPts = Pts;
215 IframesSeen = 0;
216 }
217 if (Independent) {
218 if (!GotPatPmtVersions && PatPmtParser.ParsePatPmt(Buffer, l)) {
219 int PatVersion;
220 int PmtVersion;
221 if (PatPmtParser.GetVersions(PatVersion, PmtVersion)) {
222 patPmtGenerator.SetVersions(PatVersion + 1, PmtVersion + 1);
223 GotPatPmtVersions = true;
224 }
225 }
226 if (++IframesSeen >= MIN_IFRAMES_FOR_LAST_PTS)
227 break;
228 }
229 }
230 else
231 break;
232 }
233 else
234 break;
235 }
236 else
237 break;
238 }
239 frameDetector->SetLastPts(LastPts);
240 delete FileName;
241 delete Index;
242 }
243}
244
246{
247//#define TEST_VDSB 40000 // 40000 to test VDSB without restart, 70000 for VDSB with restart
248#ifdef TEST_VDSB
249 cTimeMs VdsbTimer;
250#endif
252 bool InfoWritten = false;
253 bool pendIndependentFrame = false;
254 uint16_t pendNumber = 0;
255 off_t pendFileSize = 0;
256 bool pendMissing = false;
257 int NumIframesSeen = 0;
258 working = true;
259 while (true) {
260#ifdef TEST_VDSB
261 int Vdsb = VdsbTimer.Elapsed();
262 if (Vdsb > 30000 && Vdsb < TEST_VDSB) {
263 working = false;
265 }
266 working = true;
267#endif
268 int r;
269 uchar *b = ringBuffer->Get(r);
270 if (b) {
271 int Count = frameDetector->Analyze(b, r);
272 if (Count) {
273 if (!Running() && frameDetector->IndependentFrame()) { // finish the recording before the next independent frame
274 working = false;
275 break;
276 }
277 if (frameDetector->Synced()) {
278 if (!InfoWritten) {
279 if ((frameDetector->FramesPerSecond() > 0 && DoubleEqual(recordingInfo->FramesPerSecond(), DEFAULTFRAMESPERSECOND) && !DoubleEqual(recordingInfo->FramesPerSecond(), frameDetector->FramesPerSecond())) ||
280 frameDetector->FrameWidth() != recordingInfo->FrameWidth() ||
281 frameDetector->FrameHeight() != recordingInfo->FrameHeight() ||
282 frameDetector->AspectRatio() != recordingInfo->AspectRatio()) {
283 recordingInfo->SetFramesPerSecond(frameDetector->FramesPerSecond());
284 recordingInfo->SetFrameParams(frameDetector->FrameWidth(), frameDetector->FrameHeight(), frameDetector->ScanType(), frameDetector->AspectRatio());
285 recordingInfo->Write();
287 Recordings->UpdateByName(recordingName);
288 }
289 InfoWritten = true;
291 }
292 if (firstIframeSeen || frameDetector->IndependentFrame()) {
293 firstIframeSeen = true; // start recording with the first I-frame
294 if (!NextFile())
295 break;
296 bool PreviousErrors = false;
297 bool MissingFrames = false;
298 if (frameDetector->NewFrame(PreviousErrors, MissingFrames)) {
299 if (index) {
300 if (pendNumber > 0)
301 index->Write(pendIndependentFrame, pendNumber, pendFileSize, PreviousErrors, pendMissing);
302 pendIndependentFrame = frameDetector->IndependentFrame();
303 pendNumber = fileName->Number();
304 pendFileSize = fileSize;
305 pendMissing = MissingFrames;
306 }
307 errors = frameDetector->Errors();
308 }
309 if (frameDetector->IndependentFrame()) {
310 NumIframesSeen++;
311 tmpErrors = 0;
312 recordFile->Write(patPmtGenerator.GetPat(), TS_SIZE);
313 fileSize += TS_SIZE;
314 int Index = 0;
315 while (uchar *pmt = patPmtGenerator.GetPmt(Index)) {
316 recordFile->Write(pmt, TS_SIZE);
317 fileSize += TS_SIZE;
318 }
319 t.Reset();
320 }
322 naluStreamProcessor->PutBuffer(b, Count);
323 bool Fail = false;
324 while (true) {
325 int OutLength = 0;
326 uchar *OutData = naluStreamProcessor->GetBuffer(OutLength);
327 if (!OutData || OutLength <= 0)
328 break;
329 if (recordFile->Write(OutData, OutLength) < 0) {
330 LOG_ERROR_STR(fileName->Name());
331 Fail = true;
332 break;
333 }
334 fileSize += OutLength;
335 }
336 if (Fail)
337 break;
338 }
339 else {
340 if (recordFile->Write(b, Count) < 0) {
341 LOG_ERROR_STR(fileName->Name());
342 break;
343 }
344 fileSize += Count;
345 }
346 if (NumIframesSeen >= 2) // avoids extra log entry when resuming a recording
347 HandleErrors();
348 }
349 }
350 ringBuffer->Del(Count);
351 }
352 }
353 if (t.TimedOut()) {
354 esyslog("ERROR: video data stream broken");
355 tmpErrors += int(round(frameDetector->FramesPerSecond() * t.Elapsed() / 1000));
356 if (pendNumber > 0) {
357 bool PreviousErrors = false;
358 errors = frameDetector->Errors(&PreviousErrors);
359 index->Write(pendIndependentFrame, pendNumber, pendFileSize, PreviousErrors, pendMissing);
360 pendNumber = 0;
361 }
362 HandleErrors(true);
363 ShutdownHandler.RequestEmergencyExit();
364 t.Reset();
365 }
366 if (!Running() && ShutdownHandler.EmergencyExitRequested())
367 break;
368 }
369 // Estimate the number of missing frames in case the data stream was broken, but the timer
370 // didn't reach the timeout, yet:
371 int dt = t.Elapsed();
372 if (dt > LEFTOVERTIMEOUT)
373 tmpErrors += int(round(frameDetector->FramesPerSecond() * dt / 1000));
374 if (pendNumber > 0) {
375 bool PreviousErrors = false;
376 errors = frameDetector->Errors(&PreviousErrors);
377 index->Write(pendIndependentFrame, pendNumber, pendFileSize, PreviousErrors, pendMissing);
378 }
379 HandleErrors(true);
380}
int Vpid(void) const
Definition channels.h:154
int Dpid(int i) const
Definition channels.h:161
int Vtype(void) const
Definition channels.h:156
int Apid(int i) const
Definition channels.h:160
static void SleepMs(int TimeoutMs)
Creates a cCondWait object and uses it to sleep for TimeoutMs milliseconds, immediately giving up the...
Definition thread.c:73
cUnbufferedFile * SetOffset(int Number, off_t Offset=0)
Definition recording.c:3315
bool GetVersions(int &PatVersion, int &PmtVersion) const
Returns true if a valid PAT/PMT has been parsed and stores the current version numbers in the given v...
Definition remux.c:976
bool ParsePatPmt(const uchar *Data, int Length)
Parses the given Data (which may consist of several TS packets, typically an entire frame) and extrac...
Definition remux.c:957
int Priority(void)
Definition receiver.h:57
void Detach(void)
Definition receiver.c:125
cReceiver(const cChannel *Channel=NULL, int Priority=MINPRIORITY)
Creates a new receiver for the given Channel with the given Priority.
Definition receiver.c:14
cRecorder(const char *FileName, const cChannel *Channel, int Priority)
Creates a new recorder for the given Channel and the given Priority that will record into the file Fi...
Definition recorder.c:25
bool NextFile(void)
Definition recorder.c:141
cFileName * fileName
Definition recorder.h:25
cIndexFile * index
Definition recorder.h:27
virtual void Receive(const uchar *Data, int Length) override
This function is called from the cDevice we are attached to, and delivers one TS packet from the set ...
Definition recorder.c:160
time_t lastErrorLog
Definition recorder.h:34
virtual void Activate(bool On) override
If you override Activate() you need to call Detach() (which is a member of the cReceiver class) from ...
Definition recorder.c:152
void HandleErrors(bool Force=false)
Definition recorder.c:110
void GetLastPts(const char *RecordingName)
Definition recorder.c:193
bool working
Definition recorder.h:30
void Stop(void)
Stops the recorder.
Definition recorder.c:103
off_t fileSize
Definition recorder.h:32
cRecordingInfo * recordingInfo
Definition recorder.h:26
cFrameDetector * frameDetector
Definition recorder.h:22
time_t lastDiskSpaceCheck
Definition recorder.h:33
cUnbufferedFile * recordFile
Definition recorder.h:28
virtual void Action(void) override
A derived cThread class must implement the code it wants to execute as a separate thread in this func...
Definition recorder.c:245
bool firstIframeSeen
Definition recorder.h:31
cRingBufferLinear * ringBuffer
Definition recorder.h:21
int tmpErrors
Definition recorder.h:36
char * recordingName
Definition recorder.h:29
int oldErrors
Definition recorder.h:35
bool RunningLowOnDiskSpace(void)
Definition recorder.c:128
int errors
Definition recorder.h:37
virtual ~cRecorder() override
Definition recorder.c:86
cNaluStreamProcessor * naluStreamProcessor
Definition recorder.h:24
cPatPmtGenerator patPmtGenerator
Definition recorder.h:23
int lastErrors
Definition recorder.h:38
static void InvokeCommand(const char *State, const char *RecordingFileName, const char *SourceFileName=NULL)
Definition recording.c:2581
void bool Start(void)
Sets the description of this thread, which will be used when logging starting or stopping of the thre...
Definition thread.c:305
bool Running(void)
Returns false if a derived cThread object shall leave its Action() function.
Definition thread.h:101
cThread(const char *Description=NULL, bool LowPriority=false)
Creates a new thread.
Definition thread.c:239
void Cancel(int WaitSeconds=0)
Cancels the thread by first setting 'running' to false, so that the Action() loop can finish in an or...
Definition thread.c:355
uint64_t Elapsed(void) const
Returns the number of milliseconds that have elapsed since the last call to Set().
Definition tools.c:825
bool TimedOut(void) const
Returns true if the number of milliseconds given in the last call to Set() have passed.
Definition tools.c:820
void Reset(void)
Resets the timer to the same timeout as given in the last call to Set().
Definition tools.c:835
cUnbufferedFile is used for large files that are mainly written or read in a streaming manner,...
Definition tools.h:507
cSetup Setup
Definition config.c:372
#define MAXBROKENTIMEOUT
Definition recorder.c:17
#define LEFTOVERTIMEOUT
Definition recorder.c:18
#define DISKCHECKINTERVAL
Definition recorder.c:21
#define MIN_IFRAMES_FOR_LAST_PTS
Definition recorder.c:191
#define ERROR_LOG_DELTA
Definition recorder.c:108
#define MINFREEDISKSPACE
Definition recorder.c:20
#define RECORDERBUFSIZE
Definition recorder.c:13
int ReadFrame(cUnbufferedFile *f, uchar *b, int Length, int Max)
Definition recording.c:3491
#define DEFAULTFRAMESPERSECOND
Definition recording.h:385
#define LOCK_RECORDINGS_WRITE
Definition recording.h:337
#define RUC_STARTRECORDING
Definition recording.h:462
#define MAXFRAMESIZE
Definition recording.h:481
int64_t PtsDiff(int64_t Pts1, int64_t Pts2)
Returns the difference between two PTS values.
Definition remux.c:234
int64_t TsGetPts(const uchar *p, int l)
Definition remux.c:160
#define TS_SIZE
Definition remux.h:34
#define MIN_TS_PACKETS_FOR_FRAME_DETECTOR
Definition remux.h:509
cShutdownHandler ShutdownHandler
Definition shutdown.c:27
int FreeDiskSpaceMB(const char *Directory, int *UsedMB)
Definition tools.c:477
bool SpinUpDisk(const char *FileName)
Definition tools.c:698
#define MEGABYTE(n)
Definition tools.h:45
#define LOG_ERROR_STR(s)
Definition tools.h:40
unsigned char uchar
Definition tools.h:31
#define dsyslog(a...)
Definition tools.h:37
bool DoubleEqual(double a, double b)
Definition tools.h:97
T max(T a, T b)
Definition tools.h:64
#define esyslog(a...)
Definition tools.h:35
#define isyslog(a...)
Definition tools.h:36