vdr 2.8.1
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.14 2026/02/16 10:33:00 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
19#define MINFREEDISKSPACE (512) // MB
20#define DISKCHECKINTERVAL 100 // seconds
21
22// --- cRecorder -------------------------------------------------------------
23
24cRecorder::cRecorder(const char *FileName, const cChannel *Channel, int Priority)
25:cReceiver(Channel, Priority)
26,cThread("recording")
27{
28 recordingName = strdup(FileName);
30 recordingInfo->Read();
31 tmpErrors = recordingInfo->TmpErrors();
32 oldErrors = max(0, recordingInfo->Errors()) - tmpErrors; // in case this is a re-started recording
33 errors = 0;
35 working = false;
36 firstIframeSeen = false;
37
38 // Make sure the disk is up and running:
39
40 SpinUpDisk(FileName);
41
43 ringBuffer->SetTimeouts(0, 100);
44 ringBuffer->SetIoThrottle();
45
46 int Pid = Channel->Vpid();
47 int Type = Channel->Vtype();
48 if (!Pid && Channel->Apid(0)) {
49 Pid = Channel->Apid(0);
50 Type = 0x04;
51 }
52 if (!Pid && Channel->Dpid(0)) {
53 Pid = Channel->Dpid(0);
54 Type = 0x06;
55 }
56 frameDetector = new cFrameDetector(Pid, Type);
57 index = NULL;
58 fileSize = 0;
59 lastDiskSpaceCheck = time(NULL);
60 lastErrorLog = 0;
61 fileName = new cFileName(FileName, true);
62 // Check if this is a resumed recording, in which case we definitely missed frames:
63 NextFile();
64 if (fileName->Number() > 1 || oldErrors)
66 patPmtGenerator.SetChannel(Channel);
67 recordFile = fileName->Open();
68 if (!recordFile)
69 return;
70 // Create the index file:
71 index = new cIndexFile(FileName, true);
72 if (!index)
73 esyslog("ERROR: can't allocate index");
74 // let's continue without index, so we'll at least have the recording
75}
76
78{
79 Cancel(3); // in case the caller didn't call Stop()
80 Detach();
81 delete index;
82 delete fileName;
83 delete frameDetector;
84 delete ringBuffer;
85 free(recordingName);
86}
87
89{
90 Cancel(3);
91}
92
93#define ERROR_LOG_DELTA 1 // seconds between logging errors
94
96{
97 // We don't log every single error separately, to avoid spamming the log file:
98 if (Force || time(NULL) - lastErrorLog >= ERROR_LOG_DELTA) {
99 int AllErrors = oldErrors + errors + tmpErrors;
100 if (AllErrors != lastErrors) {
101 int d = AllErrors - lastErrors;
102 esyslog("%s: %d new error%s (total %d)", recordingName, d, d > 1 ? "s" : "", AllErrors);
103 recordingInfo->SetErrors(AllErrors, tmpErrors);
104 recordingInfo->Write();
106 Recordings->UpdateByName(recordingName);
107 lastErrors = AllErrors;
108 }
109 lastErrorLog = time(NULL);
110 }
111}
112
114{
115 if (time(NULL) > lastDiskSpaceCheck + DISKCHECKINTERVAL) {
116 int Free = FreeDiskSpaceMB(fileName->Name());
117 lastDiskSpaceCheck = time(NULL);
118 if (Free < MINFREEDISKSPACE) {
119 dsyslog("low disk space (%d MB, limit is %d MB)", Free, MINFREEDISKSPACE);
120 return true;
121 }
122 }
123 return false;
124}
125
127{
128 if (recordFile && frameDetector->IndependentFrame()) { // every file shall start with an independent frame
129 if (fileSize > MEGABYTE(off_t(Setup.MaxVideoFileSize)) || RunningLowOnDiskSpace()) {
130 recordFile = fileName->NextFile();
131 fileSize = 0;
132 }
133 }
134 return recordFile != NULL;
135}
136
138{
139 if (On)
140 Start();
141 else
142 Cancel(3);
143}
144
145void cRecorder::Receive(const uchar *Data, int Length)
146{
147 if (working) {
148 static const uchar aff[TS_SIZE - 4] = { 0xB7, 0x00,
149 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
150 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
151 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
152 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
153 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
154 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
155 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
156 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
157 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
158 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
159 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
160 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
161 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
162 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
163 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
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}; // Length is always TS_SIZE!
168 if ((Data[3] & 0b00110000) == 0b00100000 && !memcmp(Data + 4, aff, sizeof(aff)))
169 return; // Adaptation Field Filler found, skipping
170 int p = ringBuffer->Put(Data, Length);
171 if (p != Length && working)
172 ringBuffer->ReportOverflow(Length - p);
173 }
174}
175
176#define MIN_IFRAMES_FOR_LAST_PTS 2
177
178void cRecorder::GetLastPts(const char *RecordingName)
179{
180 dsyslog("getting last PTS of '%s'", RecordingName);
181 if (cIndexFile *Index = new cIndexFile(RecordingName, false)) {
182 cFileName *FileName = new cFileName(RecordingName, false);
183 uint16_t FileNumber;
184 off_t FileOffset;
185 bool Independent;
186 uchar Buffer[MAXFRAMESIZE];
187 int Length;
188 int64_t LastPts = -1;
189 int IframesSeen = 0;
190 cPatPmtParser PatPmtParser;
191 bool GotPatPmtVersions = false;
192 for (int i = Index->Last(); i >= 0; i--) {
193 if (Index->Get(i, &FileNumber, &FileOffset, &Independent, &Length)) {
194 if (cUnbufferedFile *f = FileName->SetOffset(FileNumber, FileOffset)) {
195 int l = ReadFrame(f, Buffer, Length, sizeof(Buffer));
196 if (l > 0) {
197 int64_t Pts = TsGetPts(Buffer, l);
198 if (LastPts < 0 || PtsDiff(LastPts, Pts) > 0) {
199 LastPts = Pts;
200 IframesSeen = 0;
201 }
202 if (Independent) {
203 if (!GotPatPmtVersions && PatPmtParser.ParsePatPmt(Buffer, l)) {
204 int PatVersion;
205 int PmtVersion;
206 if (PatPmtParser.GetVersions(PatVersion, PmtVersion)) {
207 patPmtGenerator.SetVersions(PatVersion + 1, PmtVersion + 1);
208 GotPatPmtVersions = true;
209 }
210 }
211 if (++IframesSeen >= MIN_IFRAMES_FOR_LAST_PTS)
212 break;
213 }
214 }
215 else
216 break;
217 }
218 else
219 break;
220 }
221 else
222 break;
223 }
224 frameDetector->SetLastPts(LastPts);
225 delete FileName;
226 delete Index;
227 }
228}
229
231{
232//#define TEST_VDSB 40000 // 40000 to test VDSB without restart, 70000 for VDSB with restart
233#ifdef TEST_VDSB
234 cTimeMs VdsbTimer;
235#endif
237 bool InfoWritten = false;
238 bool pendIndependentFrame = false;
239 uint16_t pendNumber = 0;
240 off_t pendFileSize = 0;
241 bool pendMissing = false;
242 int NumIframesSeen = 0;
243 working = true;
244 while (true) {
245#ifdef TEST_VDSB
246 int Vdsb = VdsbTimer.Elapsed();
247 if (Vdsb > 30000 && Vdsb < TEST_VDSB) {
248 working = false;
250 }
251 working = true;
252#endif
253 int r;
254 uchar *b = ringBuffer->Get(r);
255 if (b) {
256 int Count = frameDetector->Analyze(b, r);
257 if (Count) {
258 if (!Running() && frameDetector->IndependentFrame()) { // finish the recording before the next independent frame
259 working = false;
260 break;
261 }
262 if (frameDetector->Synced()) {
263 if (!InfoWritten) {
264 if ((frameDetector->FramesPerSecond() > 0 && DoubleEqual(recordingInfo->FramesPerSecond(), DEFAULTFRAMESPERSECOND) && !DoubleEqual(recordingInfo->FramesPerSecond(), frameDetector->FramesPerSecond())) ||
265 frameDetector->FrameWidth() != recordingInfo->FrameWidth() ||
266 frameDetector->FrameHeight() != recordingInfo->FrameHeight() ||
267 frameDetector->AspectRatio() != recordingInfo->AspectRatio()) {
268 recordingInfo->SetFramesPerSecond(frameDetector->FramesPerSecond());
269 recordingInfo->SetFrameParams(frameDetector->FrameWidth(), frameDetector->FrameHeight(), frameDetector->ScanType(), frameDetector->AspectRatio());
270 recordingInfo->Write();
272 Recordings->UpdateByName(recordingName);
273 }
274 InfoWritten = true;
276 }
277 if (firstIframeSeen || frameDetector->IndependentFrame()) {
278 firstIframeSeen = true; // start recording with the first I-frame
279 if (!NextFile())
280 break;
281 bool PreviousErrors = false;
282 bool MissingFrames = false;
283 if (frameDetector->NewFrame(PreviousErrors, MissingFrames)) {
284 if (index) {
285 if (pendNumber > 0)
286 index->Write(pendIndependentFrame, pendNumber, pendFileSize, PreviousErrors, pendMissing);
287 pendIndependentFrame = frameDetector->IndependentFrame();
288 pendNumber = fileName->Number();
289 pendFileSize = fileSize;
290 pendMissing = MissingFrames;
291 }
292 errors = frameDetector->Errors();
293 }
294 if (frameDetector->IndependentFrame()) {
295 NumIframesSeen++;
296 tmpErrors = 0;
297 recordFile->Write(patPmtGenerator.GetPat(), TS_SIZE);
298 fileSize += TS_SIZE;
299 int Index = 0;
300 while (uchar *pmt = patPmtGenerator.GetPmt(Index)) {
301 recordFile->Write(pmt, TS_SIZE);
302 fileSize += TS_SIZE;
303 }
304 t.Reset();
305 }
306 if (recordFile->Write(b, Count) < 0) {
307 LOG_ERROR_STR(fileName->Name());
308 break;
309 }
310 if (NumIframesSeen >= 2) // avoids extra log entry when resuming a recording
311 HandleErrors();
312 fileSize += Count;
313 }
314 }
315 ringBuffer->Del(Count);
316 }
317 }
318 if (t.TimedOut()) {
319 esyslog("ERROR: video data stream broken");
320 tmpErrors += int(round(frameDetector->FramesPerSecond() * t.Elapsed() / 1000));
321 if (pendNumber > 0) {
322 bool PreviousErrors = false;
323 errors = frameDetector->Errors(&PreviousErrors);
324 index->Write(pendIndependentFrame, pendNumber, pendFileSize, PreviousErrors, pendMissing);
325 pendNumber = 0;
326 }
327 HandleErrors(true);
328 ShutdownHandler.RequestEmergencyExit();
329 t.Reset();
330 }
331 if (!Running() && ShutdownHandler.EmergencyExitRequested())
332 break;
333 }
334 // Estimate the number of missing frames in case the data stream was broken, but the timer
335 // didn't reach the timeout, yet:
336 if (working) {
337 int dt = t.Elapsed();
338 if (dt > 0)
339 tmpErrors += int(round(frameDetector->FramesPerSecond() * dt / 1000));
340 }
341 if (pendNumber > 0) {
342 bool PreviousErrors = false;
343 errors = frameDetector->Errors(&PreviousErrors);
344 index->Write(pendIndependentFrame, pendNumber, pendFileSize, PreviousErrors, pendMissing);
345 }
346 HandleErrors(true);
347}
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:3382
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:940
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:921
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:24
bool NextFile(void)
Definition recorder.c:126
cFileName * fileName
Definition recorder.h:24
cIndexFile * index
Definition recorder.h:26
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:145
time_t lastErrorLog
Definition recorder.h:33
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:137
void HandleErrors(bool Force=false)
Definition recorder.c:95
void GetLastPts(const char *RecordingName)
Definition recorder.c:178
bool working
Definition recorder.h:29
void Stop(void)
Stops the recorder.
Definition recorder.c:88
off_t fileSize
Definition recorder.h:31
cRecordingInfo * recordingInfo
Definition recorder.h:25
cFrameDetector * frameDetector
Definition recorder.h:22
time_t lastDiskSpaceCheck
Definition recorder.h:32
cUnbufferedFile * recordFile
Definition recorder.h:27
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:230
bool firstIframeSeen
Definition recorder.h:30
cRingBufferLinear * ringBuffer
Definition recorder.h:21
int tmpErrors
Definition recorder.h:35
char * recordingName
Definition recorder.h:28
int oldErrors
Definition recorder.h:34
bool RunningLowOnDiskSpace(void)
Definition recorder.c:113
int errors
Definition recorder.h:36
virtual ~cRecorder() override
Definition recorder.c:77
cPatPmtGenerator patPmtGenerator
Definition recorder.h:23
int lastErrors
Definition recorder.h:37
static void InvokeCommand(const char *State, const char *RecordingFileName, const char *SourceFileName=NULL)
Definition recording.c:2648
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 DISKCHECKINTERVAL
Definition recorder.c:20
#define MIN_IFRAMES_FOR_LAST_PTS
Definition recorder.c:176
#define ERROR_LOG_DELTA
Definition recorder.c:93
#define MINFREEDISKSPACE
Definition recorder.c:19
#define RECORDERBUFSIZE
Definition recorder.c:13
int ReadFrame(cUnbufferedFile *f, uchar *b, int Length, int Max)
Definition recording.c:3558
#define DEFAULTFRAMESPERSECOND
Definition recording.h:401
#define LOCK_RECORDINGS_WRITE
Definition recording.h:353
#define RUC_STARTRECORDING
Definition recording.h:478
#define MAXFRAMESIZE
Definition recording.h:497
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:503
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