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