vdr  2.0.2
recording.c
Go to the documentation of this file.
1 /*
2  * recording.c: Recording file handling
3  *
4  * See the main source file 'vdr.c' for copyright information and
5  * how to reach the author.
6  *
7  * $Id: recording.c 2.91.1.1 2013/04/11 08:20:03 kls Exp $
8  */
9 
10 #include "recording.h"
11 #include <ctype.h>
12 #include <dirent.h>
13 #include <errno.h>
14 #include <fcntl.h>
15 #define __STDC_FORMAT_MACROS // Required for format specifiers
16 #include <inttypes.h>
17 #include <math.h>
18 #include <stdio.h>
19 #include <string.h>
20 #include <sys/stat.h>
21 #include <unistd.h>
22 #include "channels.h"
23 #include "i18n.h"
24 #include "interface.h"
25 #include "remux.h"
26 #include "ringbuffer.h"
27 #include "skins.h"
28 #include "tools.h"
29 #include "videodir.h"
30 
31 #define SUMMARYFALLBACK
32 
33 #define RECEXT ".rec"
34 #define DELEXT ".del"
35 /* This was the original code, which works fine in a Linux only environment.
36  Unfortunately, because of Windows and its brain dead file system, we have
37  to use a more complicated approach, in order to allow users who have enabled
38  the --vfat command line option to see their recordings even if they forget to
39  enable --vfat when restarting VDR... Gee, do I hate Windows.
40  (kls 2002-07-27)
41 #define DATAFORMAT "%4d-%02d-%02d.%02d:%02d.%02d.%02d" RECEXT
42 #define NAMEFORMAT "%s/%s/" DATAFORMAT
43 */
44 #define DATAFORMATPES "%4d-%02d-%02d.%02d%*c%02d.%02d.%02d" RECEXT
45 #define NAMEFORMATPES "%s/%s/" "%4d-%02d-%02d.%02d.%02d.%02d.%02d" RECEXT
46 #define DATAFORMATTS "%4d-%02d-%02d.%02d.%02d.%d-%d" RECEXT
47 #define NAMEFORMATTS "%s/%s/" DATAFORMATTS
48 
49 #define RESUMEFILESUFFIX "/resume%s%s"
50 #ifdef SUMMARYFALLBACK
51 #define SUMMARYFILESUFFIX "/summary.vdr"
52 #endif
53 #define INFOFILESUFFIX "/info"
54 #define MARKSFILESUFFIX "/marks"
55 
56 #define SORTMODEFILE ".sort"
57 
58 #define MINDISKSPACE 1024 // MB
59 
60 #define REMOVECHECKDELTA 60 // seconds between checks for removing deleted files
61 #define DELETEDLIFETIME 300 // seconds after which a deleted recording will be actually removed
62 #define DISKCHECKDELTA 100 // seconds between checks for free disk space
63 #define REMOVELATENCY 10 // seconds to wait until next check after removing a file
64 #define MARKSUPDATEDELTA 10 // seconds between checks for updating editing marks
65 #define MININDEXAGE 3600 // seconds before an index file is considered no longer to be written
66 
67 #define MAX_LINK_LEVEL 6
68 
69 int DirectoryPathMax = PATH_MAX - 1;
70 int DirectoryNameMax = NAME_MAX;
71 bool DirectoryEncoding = false;
72 int InstanceId = 0;
73 
75 
76 // --- cRemoveDeletedRecordingsThread ----------------------------------------
77 
79 protected:
80  virtual void Action(void);
81 public:
83  };
84 
86 :cThread("remove deleted recordings", true)
87 {
88 }
89 
91 {
92  // Make sure only one instance of VDR does this:
93  cLockFile LockFile(VideoDirectory);
94  if (LockFile.Lock()) {
95  bool deleted = false;
96  cThreadLock DeletedRecordingsLock(&DeletedRecordings);
97  for (cRecording *r = DeletedRecordings.First(); r; ) {
99  return;
100  if (r->Deleted() && time(NULL) - r->Deleted() > DELETEDLIFETIME) {
101  cRecording *next = DeletedRecordings.Next(r);
102  r->Remove();
104  r = next;
105  deleted = true;
106  continue;
107  }
108  r = DeletedRecordings.Next(r);
109  }
110  if (deleted) {
111  const char *IgnoreFiles[] = { SORTMODEFILE, NULL };
112  RemoveEmptyVideoDirectories(IgnoreFiles);
113  }
114  }
115 }
116 
118 
119 // ---
120 
122 {
123  static time_t LastRemoveCheck = 0;
124  if (time(NULL) - LastRemoveCheck > REMOVECHECKDELTA) {
125  if (!RemoveDeletedRecordingsThread.Active()) {
126  cThreadLock DeletedRecordingsLock(&DeletedRecordings);
127  for (cRecording *r = DeletedRecordings.First(); r; r = DeletedRecordings.Next(r)) {
128  if (r->Deleted() && time(NULL) - r->Deleted() > DELETEDLIFETIME) {
129  RemoveDeletedRecordingsThread.Start();
130  break;
131  }
132  }
133  }
134  LastRemoveCheck = time(NULL);
135  }
136 }
137 
138 void AssertFreeDiskSpace(int Priority, bool Force)
139 {
140  static cMutex Mutex;
141  cMutexLock MutexLock(&Mutex);
142  // With every call to this function we try to actually remove
143  // a file, or mark a file for removal ("delete" it), so that
144  // it will get removed during the next call.
145  static time_t LastFreeDiskCheck = 0;
146  int Factor = (Priority == -1) ? 10 : 1;
147  if (Force || time(NULL) - LastFreeDiskCheck > DISKCHECKDELTA / Factor) {
149  // Make sure only one instance of VDR does this:
150  cLockFile LockFile(VideoDirectory);
151  if (!LockFile.Lock())
152  return;
153  // Remove the oldest file that has been "deleted":
154  isyslog("low disk space while recording, trying to remove a deleted recording...");
155  cThreadLock DeletedRecordingsLock(&DeletedRecordings);
156  if (DeletedRecordings.Count()) {
158  cRecording *r0 = NULL;
159  while (r) {
160  if (r->IsOnVideoDirectoryFileSystem()) { // only remove recordings that will actually increase the free video disk space
161  if (!r0 || r->Start() < r0->Start())
162  r0 = r;
163  }
164  r = DeletedRecordings.Next(r);
165  }
166  if (r0) {
167  if (r0->Remove())
168  LastFreeDiskCheck += REMOVELATENCY / Factor;
170  return;
171  }
172  }
173  else {
174  // DeletedRecordings was empty, so to be absolutely sure there are no
175  // deleted recordings we need to double check:
177  if (DeletedRecordings.Count())
178  return; // the next call will actually remove it
179  }
180  // No "deleted" files to remove, so let's see if we can delete a recording:
181  isyslog("...no deleted recording found, trying to delete an old recording...");
182  cThreadLock RecordingsLock(&Recordings);
183  if (Recordings.Count()) {
184  cRecording *r = Recordings.First();
185  cRecording *r0 = NULL;
186  while (r) {
187  if (r->IsOnVideoDirectoryFileSystem()) { // only delete recordings that will actually increase the free video disk space
188  if (!r->IsEdited() && r->Lifetime() < MAXLIFETIME) { // edited recordings and recordings with MAXLIFETIME live forever
189  if ((r->Lifetime() == 0 && Priority > r->Priority()) || // the recording has no guaranteed lifetime and the new recording has higher priority
190  (r->Lifetime() > 0 && (time(NULL) - r->Start()) / SECSINDAY >= r->Lifetime())) { // the recording's guaranteed lifetime has expired
191  if (r0) {
192  if (r->Priority() < r0->Priority() || (r->Priority() == r0->Priority() && r->Start() < r0->Start()))
193  r0 = r; // in any case we delete the one with the lowest priority (or the older one in case of equal priorities)
194  }
195  else
196  r0 = r;
197  }
198  }
199  }
200  r = Recordings.Next(r);
201  }
202  if (r0 && r0->Delete()) {
203  Recordings.Del(r0);
204  return;
205  }
206  }
207  // Unable to free disk space, but there's nothing we can do about that...
208  isyslog("...no old recording found, giving up");
209  Skins.QueueMessage(mtWarning, tr("Low disk space!"), 5, -1);
210  }
211  LastFreeDiskCheck = time(NULL);
212  }
213 }
214 
215 // --- cResumeFile -----------------------------------------------------------
216 
217 cResumeFile::cResumeFile(const char *FileName, bool IsPesRecording)
218 {
219  isPesRecording = IsPesRecording;
220  const char *Suffix = isPesRecording ? RESUMEFILESUFFIX ".vdr" : RESUMEFILESUFFIX;
221  fileName = MALLOC(char, strlen(FileName) + strlen(Suffix) + 1);
222  if (fileName) {
223  strcpy(fileName, FileName);
224  sprintf(fileName + strlen(fileName), Suffix, Setup.ResumeID ? "." : "", Setup.ResumeID ? *itoa(Setup.ResumeID) : "");
225  }
226  else
227  esyslog("ERROR: can't allocate memory for resume file name");
228 }
229 
231 {
232  free(fileName);
233 }
234 
236 {
237  int resume = -1;
238  if (fileName) {
239  struct stat st;
240  if (stat(fileName, &st) == 0) {
241  if ((st.st_mode & S_IWUSR) == 0) // no write access, assume no resume
242  return -1;
243  }
244  if (isPesRecording) {
245  int f = open(fileName, O_RDONLY);
246  if (f >= 0) {
247  if (safe_read(f, &resume, sizeof(resume)) != sizeof(resume)) {
248  resume = -1;
250  }
251  close(f);
252  }
253  else if (errno != ENOENT)
255  }
256  else {
257  FILE *f = fopen(fileName, "r");
258  if (f) {
259  cReadLine ReadLine;
260  char *s;
261  int line = 0;
262  while ((s = ReadLine.Read(f)) != NULL) {
263  ++line;
264  char *t = skipspace(s + 1);
265  switch (*s) {
266  case 'I': resume = atoi(t);
267  break;
268  default: ;
269  }
270  }
271  fclose(f);
272  }
273  else if (errno != ENOENT)
275  }
276  }
277  return resume;
278 }
279 
280 bool cResumeFile::Save(int Index)
281 {
282  if (fileName) {
283  if (isPesRecording) {
284  int f = open(fileName, O_WRONLY | O_CREAT | O_TRUNC, DEFFILEMODE);
285  if (f >= 0) {
286  if (safe_write(f, &Index, sizeof(Index)) < 0)
288  close(f);
290  return true;
291  }
292  }
293  else {
294  FILE *f = fopen(fileName, "w");
295  if (f) {
296  fprintf(f, "I %d\n", Index);
297  fclose(f);
299  }
300  else
302  return true;
303  }
304  }
305  return false;
306 }
307 
309 {
310  if (fileName) {
311  if (remove(fileName) == 0)
313  else if (errno != ENOENT)
315  }
316 }
317 
318 // --- cRecordingInfo --------------------------------------------------------
319 
320 cRecordingInfo::cRecordingInfo(const cChannel *Channel, const cEvent *Event)
321 {
322  channelID = Channel ? Channel->GetChannelID() : tChannelID::InvalidID;
323  channelName = Channel ? strdup(Channel->Name()) : NULL;
324  ownEvent = Event ? NULL : new cEvent(0);
325  event = ownEvent ? ownEvent : Event;
326  aux = NULL;
330  fileName = NULL;
331  if (Channel) {
332  // Since the EPG data's component records can carry only a single
333  // language code, let's see whether the channel's PID data has
334  // more information:
336  if (!Components)
337  Components = new cComponents;
338  for (int i = 0; i < MAXAPIDS; i++) {
339  const char *s = Channel->Alang(i);
340  if (*s) {
341  tComponent *Component = Components->GetComponent(i, 2, 3);
342  if (!Component)
343  Components->SetComponent(Components->NumComponents(), 2, 3, s, NULL);
344  else if (strlen(s) > strlen(Component->language))
345  strn0cpy(Component->language, s, sizeof(Component->language));
346  }
347  }
348  // There's no "multiple languages" for Dolby Digital tracks, but
349  // we do the same procedure here, too, in case there is no component
350  // information at all:
351  for (int i = 0; i < MAXDPIDS; i++) {
352  const char *s = Channel->Dlang(i);
353  if (*s) {
354  tComponent *Component = Components->GetComponent(i, 4, 0); // AC3 component according to the DVB standard
355  if (!Component)
356  Component = Components->GetComponent(i, 2, 5); // fallback "Dolby" component according to the "Premiere pseudo standard"
357  if (!Component)
358  Components->SetComponent(Components->NumComponents(), 2, 5, s, NULL);
359  else if (strlen(s) > strlen(Component->language))
360  strn0cpy(Component->language, s, sizeof(Component->language));
361  }
362  }
363  // The same applies to subtitles:
364  for (int i = 0; i < MAXSPIDS; i++) {
365  const char *s = Channel->Slang(i);
366  if (*s) {
367  tComponent *Component = Components->GetComponent(i, 3, 3);
368  if (!Component)
369  Components->SetComponent(Components->NumComponents(), 3, 3, s, NULL);
370  else if (strlen(s) > strlen(Component->language))
371  strn0cpy(Component->language, s, sizeof(Component->language));
372  }
373  }
374  if (Components != event->Components())
375  ((cEvent *)event)->SetComponents(Components);
376  }
377 }
378 
379 cRecordingInfo::cRecordingInfo(const char *FileName)
380 {
382  channelName = NULL;
383  ownEvent = new cEvent(0);
384  event = ownEvent;
385  aux = NULL;
389  fileName = strdup(cString::sprintf("%s%s", FileName, INFOFILESUFFIX));
390 }
391 
393 {
394  delete ownEvent;
395  free(aux);
396  free(channelName);
397  free(fileName);
398 }
399 
400 void cRecordingInfo::SetData(const char *Title, const char *ShortText, const char *Description)
401 {
402  if (!isempty(Title))
403  ((cEvent *)event)->SetTitle(Title);
404  if (!isempty(ShortText))
405  ((cEvent *)event)->SetShortText(ShortText);
406  if (!isempty(Description))
407  ((cEvent *)event)->SetDescription(Description);
408 }
409 
410 void cRecordingInfo::SetAux(const char *Aux)
411 {
412  free(aux);
413  aux = Aux ? strdup(Aux) : NULL;
414 }
415 
416 void cRecordingInfo::SetFramesPerSecond(double FramesPerSecond)
417 {
419 }
420 
421 bool cRecordingInfo::Read(FILE *f)
422 {
423  if (ownEvent) {
424  cReadLine ReadLine;
425  char *s;
426  int line = 0;
427  while ((s = ReadLine.Read(f)) != NULL) {
428  ++line;
429  char *t = skipspace(s + 1);
430  switch (*s) {
431  case 'C': {
432  char *p = strchr(t, ' ');
433  if (p) {
434  free(channelName);
435  channelName = strdup(compactspace(p));
436  *p = 0; // strips optional channel name
437  }
438  if (*t)
440  }
441  break;
442  case 'E': {
443  unsigned int EventID;
444  time_t StartTime;
445  int Duration;
446  unsigned int TableID = 0;
447  unsigned int Version = 0xFF;
448  int n = sscanf(t, "%u %ld %d %X %X", &EventID, &StartTime, &Duration, &TableID, &Version);
449  if (n >= 3 && n <= 5) {
450  ownEvent->SetEventID(EventID);
451  ownEvent->SetStartTime(StartTime);
452  ownEvent->SetDuration(Duration);
453  ownEvent->SetTableID(uchar(TableID));
454  ownEvent->SetVersion(uchar(Version));
455  }
456  }
457  break;
458  case 'F': framesPerSecond = atod(t);
459  break;
460  case 'L': lifetime = atoi(t);
461  break;
462  case 'P': priority = atoi(t);
463  break;
464  case '@': free(aux);
465  aux = strdup(t);
466  break;
467  case '#': break; // comments are ignored
468  default: if (!ownEvent->Parse(s)) {
469  esyslog("ERROR: EPG data problem in line %d", line);
470  return false;
471  }
472  break;
473  }
474  }
475  return true;
476  }
477  return false;
478 }
479 
480 bool cRecordingInfo::Write(FILE *f, const char *Prefix) const
481 {
482  if (channelID.Valid())
483  fprintf(f, "%sC %s%s%s\n", Prefix, *channelID.ToString(), channelName ? " " : "", channelName ? channelName : "");
484  event->Dump(f, Prefix, true);
485  fprintf(f, "%sF %s\n", Prefix, *dtoa(framesPerSecond, "%.10g"));
486  fprintf(f, "%sP %d\n", Prefix, priority);
487  fprintf(f, "%sL %d\n", Prefix, lifetime);
488  if (aux)
489  fprintf(f, "%s@ %s\n", Prefix, aux);
490  return true;
491 }
492 
494 {
495  bool Result = false;
496  if (fileName) {
497  FILE *f = fopen(fileName, "r");
498  if (f) {
499  if (Read(f))
500  Result = true;
501  else
502  esyslog("ERROR: EPG data problem in file %s", fileName);
503  fclose(f);
504  }
505  else if (errno != ENOENT)
507  }
508  return Result;
509 }
510 
511 bool cRecordingInfo::Write(void) const
512 {
513  bool Result = false;
514  if (fileName) {
515  cSafeFile f(fileName);
516  if (f.Open()) {
517  if (Write(f))
518  Result = true;
519  f.Close();
520  }
521  else
523  }
524  return Result;
525 }
526 
527 // --- cRecording ------------------------------------------------------------
528 
529 #define RESUME_NOT_INITIALIZED (-2)
530 
531 struct tCharExchange { char a; char b; };
533  { FOLDERDELIMCHAR, '/' },
534  { '/', FOLDERDELIMCHAR },
535  { ' ', '_' },
536  // backwards compatibility:
537  { '\'', '\'' },
538  { '\'', '\x01' },
539  { '/', '\x02' },
540  { 0, 0 }
541  };
542 
543 const char *InvalidChars = "\"\\/:*?|<>#";
544 
545 bool NeedsConversion(const char *p)
546 {
547  return DirectoryEncoding &&
548  (strchr(InvalidChars, *p) // characters that can't be part of a Windows file/directory name
549  || *p == '.' && (!*(p + 1) || *(p + 1) == FOLDERDELIMCHAR)); // Windows can't handle '.' at the end of file/directory names
550 }
551 
552 char *ExchangeChars(char *s, bool ToFileSystem)
553 {
554  char *p = s;
555  while (*p) {
556  if (DirectoryEncoding) {
557  // Some file systems can't handle all characters, so we
558  // have to take extra efforts to encode/decode them:
559  if (ToFileSystem) {
560  switch (*p) {
561  // characters that can be mapped to other characters:
562  case ' ': *p = '_'; break;
563  case FOLDERDELIMCHAR: *p = '/'; break;
564  case '/': *p = FOLDERDELIMCHAR; break;
565  // characters that have to be encoded:
566  default:
567  if (NeedsConversion(p)) {
568  int l = p - s;
569  if (char *NewBuffer = (char *)realloc(s, strlen(s) + 10)) {
570  s = NewBuffer;
571  p = s + l;
572  char buf[4];
573  sprintf(buf, "#%02X", (unsigned char)*p);
574  memmove(p + 2, p, strlen(p) + 1);
575  strncpy(p, buf, 3);
576  p += 2;
577  }
578  else
579  esyslog("ERROR: out of memory");
580  }
581  }
582  }
583  else {
584  switch (*p) {
585  // mapped characters:
586  case '_': *p = ' '; break;
587  case FOLDERDELIMCHAR: *p = '/'; break;
588  case '/': *p = FOLDERDELIMCHAR; break;
589  // encoded characters:
590  case '#': {
591  if (strlen(p) > 2 && isxdigit(*(p + 1)) && isxdigit(*(p + 2))) {
592  char buf[3];
593  sprintf(buf, "%c%c", *(p + 1), *(p + 2));
594  uchar c = uchar(strtol(buf, NULL, 16));
595  if (c) {
596  *p = c;
597  memmove(p + 1, p + 3, strlen(p) - 2);
598  }
599  }
600  }
601  break;
602  // backwards compatibility:
603  case '\x01': *p = '\''; break;
604  case '\x02': *p = '/'; break;
605  case '\x03': *p = ':'; break;
606  default: ;
607  }
608  }
609  }
610  else {
611  for (struct tCharExchange *ce = CharExchange; ce->a && ce->b; ce++) {
612  if (*p == (ToFileSystem ? ce->a : ce->b)) {
613  *p = ToFileSystem ? ce->b : ce->a;
614  break;
615  }
616  }
617  }
618  p++;
619  }
620  return s;
621 }
622 
623 char *LimitNameLengths(char *s, int PathMax, int NameMax)
624 {
625  // Limits the total length of the directory path in 's' to PathMax, and each
626  // individual directory name to NameMax. The lengths of characters that need
627  // conversion when using 's' as a file name are taken into account accordingly.
628  // If a directory name exceeds NameMax, it will be truncated. If the whole
629  // directory path exceeds PathMax, individual directory names will be shortened
630  // (from right to left) until the limit is met, or until the currently handled
631  // directory name consists of only a single character. All operations are performed
632  // directly on the given 's', which may become shorter (but never longer) than
633  // the original value.
634  // Returns a pointer to 's'.
635  int Length = strlen(s);
636  int PathLength = 0;
637  // Collect the resulting lengths of each character:
638  bool NameTooLong = false;
639  int8_t a[Length];
640  int n = 0;
641  int NameLength = 0;
642  for (char *p = s; *p; p++) {
643  if (*p == FOLDERDELIMCHAR) {
644  a[n] = -1; // FOLDERDELIMCHAR is a single character, neg. sign marks it
645  NameTooLong |= NameLength > NameMax;
646  NameLength = 0;
647  PathLength += 1;
648  }
649  else if (NeedsConversion(p)) {
650  a[n] = 3; // "#xx"
651  NameLength += 3;
652  PathLength += 3;
653  }
654  else {
655  int8_t l = Utf8CharLen(p);
656  a[n] = l;
657  NameLength += l;
658  PathLength += l;
659  while (l-- > 1) {
660  a[++n] = 0;
661  p++;
662  }
663  }
664  n++;
665  }
666  NameTooLong |= NameLength > NameMax;
667  // Limit names to NameMax:
668  if (NameTooLong) {
669  while (n > 0) {
670  // Calculate the length of the current name:
671  int NameLength = 0;
672  int i = n;
673  int b = i;
674  while (i-- > 0 && a[i] >= 0) {
675  NameLength += a[i];
676  b = i;
677  }
678  // Shorten the name if necessary:
679  if (NameLength > NameMax) {
680  int l = 0;
681  i = n;
682  while (i-- > 0 && a[i] >= 0) {
683  l += a[i];
684  if (NameLength - l <= NameMax) {
685  memmove(s + i, s + n, Length - n + 1);
686  memmove(a + i, a + n, Length - n + 1);
687  Length -= n - i;
688  PathLength -= l;
689  break;
690  }
691  }
692  }
693  // Switch to the next name:
694  n = b - 1;
695  }
696  }
697  // Limit path to PathMax:
698  n = Length;
699  while (PathLength > PathMax && n > 0) {
700  // Calculate how much to cut off the current name:
701  int i = n;
702  int b = i;
703  int l = 0;
704  while (--i > 0 && a[i - 1] >= 0) {
705  if (a[i] > 0) {
706  l += a[i];
707  b = i;
708  if (PathLength - l <= PathMax)
709  break;
710  }
711  }
712  // Shorten the name if necessary:
713  if (l > 0) {
714  memmove(s + b, s + n, Length - n + 1);
715  Length -= n - b;
716  PathLength -= l;
717  }
718  // Switch to the next name:
719  n = i - 1;
720  }
721  return s;
722 }
723 
724 cRecording::cRecording(cTimer *Timer, const cEvent *Event)
725 {
727  titleBuffer = NULL;
729  fileName = NULL;
730  name = NULL;
731  fileSizeMB = -1; // unknown
732  channel = Timer->Channel()->Number();
734  isPesRecording = false;
735  isOnVideoDirectoryFileSystem = -1; // unknown
737  numFrames = -1;
738  deleted = 0;
739  // set up the actual name:
740  const char *Title = Event ? Event->Title() : NULL;
741  const char *Subtitle = Event ? Event->ShortText() : NULL;
742  if (isempty(Title))
743  Title = Timer->Channel()->Name();
744  if (isempty(Subtitle))
745  Subtitle = " ";
746  const char *macroTITLE = strstr(Timer->File(), TIMERMACRO_TITLE);
747  const char *macroEPISODE = strstr(Timer->File(), TIMERMACRO_EPISODE);
748  if (macroTITLE || macroEPISODE) {
749  name = strdup(Timer->File());
750  name = strreplace(name, TIMERMACRO_TITLE, Title);
751  name = strreplace(name, TIMERMACRO_EPISODE, Subtitle);
752  // avoid blanks at the end:
753  int l = strlen(name);
754  while (l-- > 2) {
755  if (name[l] == ' ' && name[l - 1] != FOLDERDELIMCHAR)
756  name[l] = 0;
757  else
758  break;
759  }
760  if (Timer->IsSingleEvent()) {
761  Timer->SetFile(name); // this was an instant recording, so let's set the actual data
763  }
764  }
765  else if (Timer->IsSingleEvent() || !Setup.UseSubtitle)
766  name = strdup(Timer->File());
767  else
768  name = strdup(cString::sprintf("%s~%s", Timer->File(), Subtitle));
769  // substitute characters that would cause problems in file names:
770  strreplace(name, '\n', ' ');
771  start = Timer->StartTime();
772  priority = Timer->Priority();
773  lifetime = Timer->Lifetime();
774  // handle info:
775  info = new cRecordingInfo(Timer->Channel(), Event);
776  info->SetAux(Timer->Aux());
779 }
780 
781 cRecording::cRecording(const char *FileName)
782 {
784  fileSizeMB = -1; // unknown
785  channel = -1;
786  instanceId = -1;
787  priority = MAXPRIORITY; // assume maximum in case there is no info file
789  isPesRecording = false;
790  isOnVideoDirectoryFileSystem = -1; // unknown
792  numFrames = -1;
793  deleted = 0;
794  titleBuffer = NULL;
796  FileName = fileName = strdup(FileName);
797  if (*(fileName + strlen(fileName) - 1) == '/')
798  *(fileName + strlen(fileName) - 1) = 0;
799  if (strstr(FileName, VideoDirectory) == FileName)
800  FileName += strlen(VideoDirectory) + 1;
801  const char *p = strrchr(FileName, '/');
802 
803  name = NULL;
805  if (p) {
806  time_t now = time(NULL);
807  struct tm tm_r;
808  struct tm t = *localtime_r(&now, &tm_r); // this initializes the time zone in 't'
809  t.tm_isdst = -1; // makes sure mktime() will determine the correct DST setting
810  if (7 == sscanf(p + 1, DATAFORMATTS, &t.tm_year, &t.tm_mon, &t.tm_mday, &t.tm_hour, &t.tm_min, &channel, &instanceId)
811  || 7 == sscanf(p + 1, DATAFORMATPES, &t.tm_year, &t.tm_mon, &t.tm_mday, &t.tm_hour, &t.tm_min, &priority, &lifetime)) {
812  t.tm_year -= 1900;
813  t.tm_mon--;
814  t.tm_sec = 0;
815  start = mktime(&t);
816  name = MALLOC(char, p - FileName + 1);
817  strncpy(name, FileName, p - FileName);
818  name[p - FileName] = 0;
819  name = ExchangeChars(name, false);
821  }
822  else
823  return;
824  GetResume();
825  // read an optional info file:
826  cString InfoFileName = cString::sprintf("%s%s", fileName, isPesRecording ? INFOFILESUFFIX ".vdr" : INFOFILESUFFIX);
827  FILE *f = fopen(InfoFileName, "r");
828  if (f) {
829  if (!info->Read(f))
830  esyslog("ERROR: EPG data problem in file %s", *InfoFileName);
831  else if (!isPesRecording) {
835  }
836  fclose(f);
837  }
838  else if (errno == ENOENT)
840  else
841  LOG_ERROR_STR(*InfoFileName);
842 #ifdef SUMMARYFALLBACK
843  // fall back to the old 'summary.vdr' if there was no 'info.vdr':
844  if (isempty(info->Title())) {
845  cString SummaryFileName = cString::sprintf("%s%s", fileName, SUMMARYFILESUFFIX);
846  FILE *f = fopen(SummaryFileName, "r");
847  if (f) {
848  int line = 0;
849  char *data[3] = { NULL };
850  cReadLine ReadLine;
851  char *s;
852  while ((s = ReadLine.Read(f)) != NULL) {
853  if (*s || line > 1) {
854  if (data[line]) {
855  int len = strlen(s);
856  len += strlen(data[line]) + 1;
857  if (char *NewBuffer = (char *)realloc(data[line], len + 1)) {
858  data[line] = NewBuffer;
859  strcat(data[line], "\n");
860  strcat(data[line], s);
861  }
862  else
863  esyslog("ERROR: out of memory");
864  }
865  else
866  data[line] = strdup(s);
867  }
868  else
869  line++;
870  }
871  fclose(f);
872  if (!data[2]) {
873  data[2] = data[1];
874  data[1] = NULL;
875  }
876  else if (data[1] && data[2]) {
877  // if line 1 is too long, it can't be the short text,
878  // so assume the short text is missing and concatenate
879  // line 1 and line 2 to be the long text:
880  int len = strlen(data[1]);
881  if (len > 80) {
882  if (char *NewBuffer = (char *)realloc(data[1], len + 1 + strlen(data[2]) + 1)) {
883  data[1] = NewBuffer;
884  strcat(data[1], "\n");
885  strcat(data[1], data[2]);
886  free(data[2]);
887  data[2] = data[1];
888  data[1] = NULL;
889  }
890  else
891  esyslog("ERROR: out of memory");
892  }
893  }
894  info->SetData(data[0], data[1], data[2]);
895  for (int i = 0; i < 3; i ++)
896  free(data[i]);
897  }
898  else if (errno != ENOENT)
899  LOG_ERROR_STR(*SummaryFileName);
900  }
901 #endif
902  }
903 }
904 
906 {
907  free(titleBuffer);
908  free(sortBufferName);
909  free(sortBufferTime);
910  free(fileName);
911  free(name);
912  delete info;
913 }
914 
915 char *cRecording::StripEpisodeName(char *s, bool Strip)
916 {
917  char *t = s, *s1 = NULL, *s2 = NULL;
918  while (*t) {
919  if (*t == '/') {
920  if (s1) {
921  if (s2)
922  s1 = s2;
923  s2 = t;
924  }
925  else
926  s1 = t;
927  }
928  t++;
929  }
930  if (s1 && s2) {
931  // To have folders sorted before plain recordings, the '/' s1 points to
932  // is replaced by the character '1'. All other slashes will be replaced
933  // by '0' in SortName() (see below), which will result in the desired
934  // sequence:
935  *s1 = '1';
936  if (Strip) {
937  s1++;
938  memmove(s1, s2, t - s2 + 1);
939  }
940  }
941  return s;
942 }
943 
944 char *cRecording::SortName(void) const
945 {
947  if (!*sb) {
948  char *s = strdup(FileName() + strlen(VideoDirectory));
951  strreplace(s, '/', '0'); // some locales ignore '/' when sorting
952  int l = strxfrm(NULL, s, 0) + 1;
953  *sb = MALLOC(char, l);
954  strxfrm(*sb, s, l);
955  free(s);
956  }
957  return *sb;
958 }
959 
961 {
964 }
965 
966 int cRecording::GetResume(void) const
967 {
969  cResumeFile ResumeFile(FileName(), isPesRecording);
970  resume = ResumeFile.Read();
971  }
972  return resume;
973 }
974 
975 int cRecording::Compare(const cListObject &ListObject) const
976 {
977  cRecording *r = (cRecording *)&ListObject;
978  return strcasecmp(SortName(), r->SortName());
979 }
980 
981 const char *cRecording::FileName(void) const
982 {
983  if (!fileName) {
984  struct tm tm_r;
985  struct tm *t = localtime_r(&start, &tm_r);
986  const char *fmt = isPesRecording ? NAMEFORMATPES : NAMEFORMATTS;
987  int ch = isPesRecording ? priority : channel;
988  int ri = isPesRecording ? lifetime : instanceId;
989  char *Name = LimitNameLengths(strdup(name), DirectoryPathMax - strlen(VideoDirectory) - 1 - 42, DirectoryNameMax); // 42 = length of an actual recording directory name (generated with DATAFORMATTS) plus some reserve
990  if (strcmp(Name, name) != 0)
991  dsyslog("recording file name '%s' truncated to '%s'", name, Name);
992  Name = ExchangeChars(Name, true);
993  fileName = strdup(cString::sprintf(fmt, VideoDirectory, Name, t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, ch, ri));
994  free(Name);
995  }
996  return fileName;
997 }
998 
999 const char *cRecording::Title(char Delimiter, bool NewIndicator, int Level) const
1000 {
1001  char New = NewIndicator && IsNew() ? '*' : ' ';
1002  free(titleBuffer);
1003  titleBuffer = NULL;
1004  if (Level < 0 || Level == HierarchyLevels()) {
1005  struct tm tm_r;
1006  struct tm *t = localtime_r(&start, &tm_r);
1007  char *s;
1008  if (Level > 0 && (s = strrchr(name, FOLDERDELIMCHAR)) != NULL)
1009  s++;
1010  else
1011  s = name;
1012  cString Length("");
1013  if (NewIndicator) {
1014  int Minutes = max(0, (LengthInSeconds() + 30) / 60);
1015  Length = cString::sprintf("%c%d:%02d",
1016  Delimiter,
1017  Minutes / 60,
1018  Minutes % 60
1019  );
1020  }
1021  titleBuffer = strdup(cString::sprintf("%02d.%02d.%02d%c%02d:%02d%s%c%c%s",
1022  t->tm_mday,
1023  t->tm_mon + 1,
1024  t->tm_year % 100,
1025  Delimiter,
1026  t->tm_hour,
1027  t->tm_min,
1028  *Length,
1029  New,
1030  Delimiter,
1031  s));
1032  // let's not display a trailing FOLDERDELIMCHAR:
1033  if (!NewIndicator)
1035  s = &titleBuffer[strlen(titleBuffer) - 1];
1036  if (*s == FOLDERDELIMCHAR)
1037  *s = 0;
1038  }
1039  else if (Level < HierarchyLevels()) {
1040  const char *s = name;
1041  const char *p = s;
1042  while (*++s) {
1043  if (*s == FOLDERDELIMCHAR) {
1044  if (Level--)
1045  p = s + 1;
1046  else
1047  break;
1048  }
1049  }
1050  titleBuffer = MALLOC(char, s - p + 3);
1051  *titleBuffer = Delimiter;
1052  *(titleBuffer + 1) = Delimiter;
1053  strn0cpy(titleBuffer + 2, p, s - p + 1);
1054  }
1055  else
1056  return "";
1057  return titleBuffer;
1058 }
1059 
1060 const char *cRecording::PrefixFileName(char Prefix)
1061 {
1062  cString p = PrefixVideoFileName(FileName(), Prefix);
1063  if (*p) {
1064  free(fileName);
1065  fileName = strdup(p);
1066  return fileName;
1067  }
1068  return NULL;
1069 }
1070 
1071 const char *cRecording::UpdateFileName(const char *FileName)
1072 {
1073  if (FileName && *FileName) {
1074  free(fileName);
1075  fileName = strdup(FileName);
1076  return fileName;
1077  }
1078  return NULL;
1079 }
1080 
1082 {
1083  const char *s = name;
1084  int level = 0;
1085  while (*++s) {
1086  if (*s == FOLDERDELIMCHAR)
1087  level++;
1088  }
1089  return level;
1090 }
1091 
1092 bool cRecording::IsEdited(void) const
1093 {
1094  const char *s = strrchr(name, FOLDERDELIMCHAR);
1095  s = !s ? name : s + 1;
1096  return *s == '%';
1097 }
1098 
1100 {
1104 }
1105 
1107 {
1108  info->Read();
1109  priority = info->priority;
1110  lifetime = info->lifetime;
1112 }
1113 
1115 {
1116  cString InfoFileName = cString::sprintf("%s%s", fileName, isPesRecording ? INFOFILESUFFIX ".vdr" : INFOFILESUFFIX);
1117  FILE *f = fopen(InfoFileName, "w");
1118  if (f) {
1119  info->Write(f);
1120  fclose(f);
1121  }
1122  else
1123  LOG_ERROR_STR(*InfoFileName);
1124  return true;
1125 }
1126 
1127 void cRecording::SetStartTime(time_t Start)
1128 {
1129  start = Start;
1130  free(fileName);
1131  fileName = NULL;
1132 }
1133 
1135 {
1136  bool result = true;
1137  char *NewName = strdup(FileName());
1138  char *ext = strrchr(NewName, '.');
1139  if (ext && strcmp(ext, RECEXT) == 0) {
1140  strncpy(ext, DELEXT, strlen(ext));
1141  if (access(NewName, F_OK) == 0) {
1142  // the new name already exists, so let's remove that one first:
1143  isyslog("removing recording '%s'", NewName);
1144  RemoveVideoFile(NewName);
1145  }
1146  isyslog("deleting recording '%s'", FileName());
1147  if (access(FileName(), F_OK) == 0) {
1148  result = RenameVideoFile(FileName(), NewName);
1150  }
1151  else {
1152  isyslog("recording '%s' vanished", FileName());
1153  result = true; // well, we were going to delete it, anyway
1154  }
1155  }
1156  free(NewName);
1157  return result;
1158 }
1159 
1161 {
1162  // let's do a final safety check here:
1163  if (!endswith(FileName(), DELEXT)) {
1164  esyslog("attempt to remove recording %s", FileName());
1165  return false;
1166  }
1167  isyslog("removing recording %s", FileName());
1168  return RemoveVideoFile(FileName());
1169 }
1170 
1172 {
1173  bool result = true;
1174  char *NewName = strdup(FileName());
1175  char *ext = strrchr(NewName, '.');
1176  if (ext && strcmp(ext, DELEXT) == 0) {
1177  strncpy(ext, RECEXT, strlen(ext));
1178  if (access(NewName, F_OK) == 0) {
1179  // the new name already exists, so let's not remove that one:
1180  esyslog("ERROR: attempt to undelete '%s', while recording '%s' exists", FileName(), NewName);
1181  result = false;
1182  }
1183  else {
1184  isyslog("undeleting recording '%s'", FileName());
1185  if (access(FileName(), F_OK) == 0)
1186  result = RenameVideoFile(FileName(), NewName);
1187  else {
1188  isyslog("deleted recording '%s' vanished", FileName());
1189  result = false;
1190  }
1191  }
1192  }
1193  free(NewName);
1194  return result;
1195 }
1196 
1197 void cRecording::ResetResume(void) const
1198 {
1200 }
1201 
1202 int cRecording::NumFrames(void) const
1203 {
1204  if (numFrames < 0) {
1207  return nf; // check again later for ongoing recordings
1208  numFrames = nf;
1209  }
1210  return numFrames;
1211 }
1212 
1214 {
1215  int nf = NumFrames();
1216  if (nf >= 0)
1217  return int(nf / FramesPerSecond());
1218  return -1;
1219 }
1220 
1221 int cRecording::FileSizeMB(void) const
1222 {
1223  if (fileSizeMB < 0) {
1224  int fs = DirSizeMB(FileName());
1226  return fs; // check again later for ongoing recordings
1227  fileSizeMB = fs;
1228  }
1229  return fileSizeMB;
1230 }
1231 
1232 // --- cRecordings -----------------------------------------------------------
1233 
1235 
1236 char *cRecordings::updateFileName = NULL;
1237 
1239 :cThread("video directory scanner")
1240 {
1241  deleted = Deleted;
1242  lastUpdate = 0;
1243  state = 0;
1244 }
1245 
1247 {
1248  Cancel(3);
1249 }
1250 
1252 {
1253  Refresh();
1254 }
1255 
1257 {
1258  if (!updateFileName)
1259  updateFileName = strdup(AddDirectory(VideoDirectory, ".update"));
1260  return updateFileName;
1261 }
1262 
1263 void cRecordings::Refresh(bool Foreground)
1264 {
1265  lastUpdate = time(NULL); // doing this first to make sure we don't miss anything
1266  Lock();
1267  Clear();
1268  ChangeState();
1269  Unlock();
1270  ScanVideoDir(VideoDirectory, Foreground);
1271 }
1272 
1273 void cRecordings::ScanVideoDir(const char *DirName, bool Foreground, int LinkLevel)
1274 {
1275  cReadDir d(DirName);
1276  struct dirent *e;
1277  while ((Foreground || Running()) && (e = d.Next()) != NULL) {
1278  cString buffer = AddDirectory(DirName, e->d_name);
1279  struct stat st;
1280  if (lstat(buffer, &st) == 0) {
1281  int Link = 0;
1282  if (S_ISLNK(st.st_mode)) {
1283  if (LinkLevel > MAX_LINK_LEVEL) {
1284  isyslog("max link level exceeded - not scanning %s", *buffer);
1285  continue;
1286  }
1287  Link = 1;
1288  if (stat(buffer, &st) != 0)
1289  continue;
1290  }
1291  if (S_ISDIR(st.st_mode)) {
1292  if (endswith(buffer, deleted ? DELEXT : RECEXT)) {
1293  cRecording *r = new cRecording(buffer);
1294  if (r->Name()) {
1295  r->NumFrames(); // initializes the numFrames member
1296  r->FileSizeMB(); // initializes the fileSizeMB member
1297  if (deleted)
1298  r->deleted = time(NULL);
1299  Lock();
1300  Add(r);
1301  ChangeState();
1302  Unlock();
1303  }
1304  else
1305  delete r;
1306  }
1307  else
1308  ScanVideoDir(buffer, Foreground, LinkLevel + Link);
1309  }
1310  }
1311  }
1312 }
1313 
1315 {
1316  int NewState = state;
1317  bool Result = State != NewState;
1318  State = state;
1319  return Result;
1320 }
1321 
1323 {
1324  bool needsUpdate = NeedsUpdate();
1326  if (!needsUpdate)
1327  lastUpdate = time(NULL); // make sure we don't trigger ourselves
1328 }
1329 
1331 {
1332  time_t lastModified = LastModifiedTime(UpdateFileName());
1333  if (lastModified > time(NULL))
1334  return false; // somebody's clock isn't running correctly
1335  return lastUpdate < lastModified;
1336 }
1337 
1338 bool cRecordings::Update(bool Wait)
1339 {
1340  if (Wait) {
1341  Refresh(true);
1342  return Count() > 0;
1343  }
1344  else
1345  Start();
1346  return false;
1347 }
1348 
1349 cRecording *cRecordings::GetByName(const char *FileName)
1350 {
1351  if (FileName) {
1352  for (cRecording *recording = First(); recording; recording = Next(recording)) {
1353  if (strcmp(recording->FileName(), FileName) == 0)
1354  return recording;
1355  }
1356  }
1357  return NULL;
1358 }
1359 
1360 void cRecordings::AddByName(const char *FileName, bool TriggerUpdate)
1361 {
1362  LOCK_THREAD;
1363  cRecording *recording = GetByName(FileName);
1364  if (!recording) {
1365  recording = new cRecording(FileName);
1366  Add(recording);
1367  ChangeState();
1368  if (TriggerUpdate)
1369  TouchUpdate();
1370  }
1371 }
1372 
1373 void cRecordings::DelByName(const char *FileName, bool RemoveRecording)
1374 {
1375  LOCK_THREAD;
1376  cRecording *recording = GetByName(FileName);
1377  if (recording) {
1378  cThreadLock DeletedRecordingsLock(&DeletedRecordings);
1379  Del(recording, false);
1380  char *ext = strrchr(recording->fileName, '.');
1381  if (ext && RemoveRecording) {
1382  strncpy(ext, DELEXT, strlen(ext));
1383  if (access(recording->FileName(), F_OK) == 0) {
1384  recording->deleted = time(NULL);
1385  DeletedRecordings.Add(recording);
1386  recording = NULL; // to prevent it from being deleted below
1387  }
1388  }
1389  delete recording;
1390  ChangeState();
1391  TouchUpdate();
1392  }
1393 }
1394 
1395 void cRecordings::UpdateByName(const char *FileName)
1396 {
1397  LOCK_THREAD;
1398  cRecording *recording = GetByName(FileName);
1399  if (recording)
1400  recording->ReadInfo();
1401 }
1402 
1404 {
1405  int size = 0;
1406  LOCK_THREAD;
1407  for (cRecording *recording = First(); recording; recording = Next(recording)) {
1408  int FileSizeMB = recording->FileSizeMB();
1409  if (FileSizeMB > 0 && recording->IsOnVideoDirectoryFileSystem())
1410  size += FileSizeMB;
1411  }
1412  return size;
1413 }
1414 
1416 {
1417  int size = 0;
1418  int length = 0;
1419  LOCK_THREAD;
1420  for (cRecording *recording = First(); recording; recording = Next(recording)) {
1421  if (recording->IsOnVideoDirectoryFileSystem()) {
1422  int FileSizeMB = recording->FileSizeMB();
1423  if (FileSizeMB > 0) {
1424  int LengthInSeconds = recording->LengthInSeconds();
1425  if (LengthInSeconds > 0) {
1426  size += FileSizeMB;
1427  length += LengthInSeconds;
1428  }
1429  }
1430  }
1431  }
1432  return (size && length) ? double(size) * 60 / length : -1;
1433 }
1434 
1435 void cRecordings::ResetResume(const char *ResumeFileName)
1436 {
1437  LOCK_THREAD;
1438  for (cRecording *recording = First(); recording; recording = Next(recording)) {
1439  if (!ResumeFileName || strncmp(ResumeFileName, recording->FileName(), strlen(recording->FileName())) == 0)
1440  recording->ResetResume();
1441  }
1442  ChangeState();
1443 }
1444 
1446 {
1447  LOCK_THREAD;
1448  for (cRecording *recording = First(); recording; recording = Next(recording))
1449  recording->ClearSortName();
1450 }
1451 
1452 // --- cMark -----------------------------------------------------------------
1453 
1456 
1457 cMark::cMark(int Position, const char *Comment, double FramesPerSecond)
1458 {
1459  position = Position;
1460  comment = Comment;
1461  framesPerSecond = FramesPerSecond;
1462 }
1463 
1465 {
1466 }
1467 
1469 {
1470  return cString::sprintf("%s%s%s\n", *IndexToHMSF(position, true, framesPerSecond), Comment() ? " " : "", Comment() ? Comment() : "");
1471 }
1472 
1473 bool cMark::Parse(const char *s)
1474 {
1475  comment = NULL;
1478  const char *p = strchr(s, ' ');
1479  if (p) {
1480  p = skipspace(p);
1481  if (*p)
1482  comment = strdup(p);
1483  }
1484  return true;
1485 }
1486 
1487 bool cMark::Save(FILE *f)
1488 {
1489  return fprintf(f, "%s", *ToText()) > 0;
1490 }
1491 
1492 // --- cMarks ----------------------------------------------------------------
1493 
1494 bool cMarks::Load(const char *RecordingFileName, double FramesPerSecond, bool IsPesRecording)
1495 {
1496  recordingFileName = RecordingFileName;
1497  fileName = AddDirectory(RecordingFileName, IsPesRecording ? MARKSFILESUFFIX ".vdr" : MARKSFILESUFFIX);
1498  framesPerSecond = FramesPerSecond;
1499  isPesRecording = IsPesRecording;
1500  nextUpdate = 0;
1501  lastFileTime = -1; // the first call to Load() must take place!
1502  lastChange = 0;
1503  return Update();
1504 }
1505 
1506 bool cMarks::Update(void)
1507 {
1508  time_t t = time(NULL);
1509  if (t > nextUpdate) {
1510  time_t LastModified = LastModifiedTime(fileName);
1511  if (LastModified != lastFileTime) // change detected, or first run
1512  lastChange = LastModified > 0 ? LastModified : t;
1513  int d = t - lastChange;
1514  if (d < 60)
1515  d = 1; // check frequently if the file has just been modified
1516  else if (d < 3600)
1517  d = 10; // older files are checked less frequently
1518  else
1519  d /= 360; // phase out checking for very old files
1520  nextUpdate = t + d;
1521  if (LastModified != lastFileTime) { // change detected, or first run
1522  lastFileTime = LastModified;
1523  if (lastFileTime == t)
1524  lastFileTime--; // make sure we don't miss updates in the remaining second
1525  cMutexLock MutexLock(&MutexMarkFramesPerSecond);
1528  Align();
1529  Sort();
1530  return true;
1531  }
1532  }
1533  }
1534  return false;
1535 }
1536 
1537 bool cMarks::Save(void)
1538 {
1539  if (cConfig<cMark>::Save()) {
1541  return true;
1542  }
1543  return false;
1544 }
1545 
1546 void cMarks::Align(void)
1547 {
1548  cIndexFile IndexFile(recordingFileName, false, isPesRecording);
1549  for (cMark *m = First(); m; m = Next(m)) {
1550  int p = IndexFile.GetClosestIFrame(m->Position());
1551  if (int d = m->Position() - p) {
1552  isyslog("aligned editing mark %s to %s (off by %d frame%s)", *IndexToHMSF(m->Position(), true, framesPerSecond), *IndexToHMSF(p, true, framesPerSecond), d, abs(d) > 1 ? "s" : "");
1553  m->SetPosition(p);
1554  }
1555  }
1556 }
1557 
1558 void cMarks::Sort(void)
1559 {
1560  for (cMark *m1 = First(); m1; m1 = Next(m1)) {
1561  for (cMark *m2 = Next(m1); m2; m2 = Next(m2)) {
1562  if (m2->Position() < m1->Position()) {
1563  swap(m1->position, m2->position);
1564  swap(m1->comment, m2->comment);
1565  }
1566  }
1567  }
1568 }
1569 
1570 void cMarks::Add(int Position)
1571 {
1572  cConfig<cMark>::Add(new cMark(Position, NULL, framesPerSecond));
1573  Sort();
1574 }
1575 
1576 cMark *cMarks::Get(int Position)
1577 {
1578  for (cMark *mi = First(); mi; mi = Next(mi)) {
1579  if (mi->Position() == Position)
1580  return mi;
1581  }
1582  return NULL;
1583 }
1584 
1585 cMark *cMarks::GetPrev(int Position)
1586 {
1587  for (cMark *mi = Last(); mi; mi = Prev(mi)) {
1588  if (mi->Position() < Position)
1589  return mi;
1590  }
1591  return NULL;
1592 }
1593 
1594 cMark *cMarks::GetNext(int Position)
1595 {
1596  for (cMark *mi = First(); mi; mi = Next(mi)) {
1597  if (mi->Position() > Position)
1598  return mi;
1599  }
1600  return NULL;
1601 }
1602 
1604 {
1605  cMark *BeginMark = EndMark ? Next(EndMark) : First();
1606  if (BeginMark) {
1607  while (cMark *NextMark = Next(BeginMark)) {
1608  if (BeginMark->Position() == NextMark->Position()) { // skip Begin/End at the same position
1609  if (!(BeginMark = Next(NextMark)))
1610  break;
1611  }
1612  else
1613  break;
1614  }
1615  }
1616  return BeginMark;
1617 }
1618 
1620 {
1621  if (!BeginMark)
1622  return NULL;
1623  cMark *EndMark = Next(BeginMark);
1624  if (EndMark) {
1625  while (cMark *NextMark = Next(EndMark)) {
1626  if (EndMark->Position() == NextMark->Position()) { // skip End/Begin at the same position
1627  if (!(EndMark = Next(NextMark)))
1628  break;
1629  }
1630  else
1631  break;
1632  }
1633  }
1634  return EndMark;
1635 }
1636 
1638 {
1639  int NumSequences = 0;
1640  if (cMark *BeginMark = GetNextBegin()) {
1641  while (cMark *EndMark = GetNextEnd(BeginMark)) {
1642  NumSequences++;
1643  BeginMark = GetNextBegin(EndMark);
1644  }
1645  if (BeginMark) {
1646  NumSequences++; // the last sequence had no actual "end" mark
1647  if (NumSequences == 1 && BeginMark->Position() == 0)
1648  NumSequences = 0; // there is only one actual "begin" mark at offset zero, and no actual "end" mark
1649  }
1650  }
1651  return NumSequences;
1652 }
1653 
1654 // --- cRecordingUserCommand -------------------------------------------------
1655 
1656 const char *cRecordingUserCommand::command = NULL;
1657 
1658 void cRecordingUserCommand::InvokeCommand(const char *State, const char *RecordingFileName, const char *SourceFileName)
1659 {
1660  if (command) {
1661  cString cmd;
1662  if (SourceFileName)
1663  cmd = cString::sprintf("%s %s \"%s\" \"%s\"", command, State, *strescape(RecordingFileName, "\\\"$"), *strescape(SourceFileName, "\\\"$"));
1664  else
1665  cmd = cString::sprintf("%s %s \"%s\"", command, State, *strescape(RecordingFileName, "\\\"$"));
1666  isyslog("executing '%s'", *cmd);
1667  SystemExec(cmd);
1668  }
1669 }
1670 
1671 // --- cIndexFileGenerator ---------------------------------------------------
1672 
1673 #define IFG_BUFFER_SIZE KILOBYTE(100)
1674 
1676 private:
1678 protected:
1679  virtual void Action(void);
1680 public:
1681  cIndexFileGenerator(const char *RecordingName);
1683  };
1684 
1686 :cThread("index file generator")
1687 ,recordingName(RecordingName)
1688 {
1689  Start();
1690 }
1691 
1693 {
1694  Cancel(3);
1695 }
1696 
1698 {
1699  bool IndexFileComplete = false;
1700  bool IndexFileWritten = false;
1701  bool Rewind = false;
1702  cFileName FileName(recordingName, false);
1703  cUnbufferedFile *ReplayFile = FileName.Open();
1705  cPatPmtParser PatPmtParser;
1706  cFrameDetector FrameDetector;
1707  cIndexFile IndexFile(recordingName, true);
1708  int BufferChunks = KILOBYTE(1); // no need to read a lot at the beginning when parsing PAT/PMT
1709  off_t FileSize = 0;
1710  off_t FrameOffset = -1;
1711  Skins.QueueMessage(mtInfo, tr("Regenerating index file"));
1712  while (Running()) {
1713  // Rewind input file:
1714  if (Rewind) {
1715  ReplayFile = FileName.SetOffset(1);
1716  Buffer.Clear();
1717  Rewind = false;
1718  }
1719  // Process data:
1720  int Length;
1721  uchar *Data = Buffer.Get(Length);
1722  if (Data) {
1723  if (FrameDetector.Synced()) {
1724  // Step 3 - generate the index:
1725  if (TsPid(Data) == PATPID)
1726  FrameOffset = FileSize; // the PAT/PMT is at the beginning of an I-frame
1727  int Processed = FrameDetector.Analyze(Data, Length);
1728  if (Processed > 0) {
1729  if (FrameDetector.NewFrame()) {
1730  IndexFile.Write(FrameDetector.IndependentFrame(), FileName.Number(), FrameOffset >= 0 ? FrameOffset : FileSize);
1731  FrameOffset = -1;
1732  IndexFileWritten = true;
1733  }
1734  FileSize += Processed;
1735  Buffer.Del(Processed);
1736  }
1737  }
1738  else if (PatPmtParser.Vpid()) {
1739  // Step 2 - sync FrameDetector:
1740  int Processed = FrameDetector.Analyze(Data, Length);
1741  if (Processed > 0) {
1742  if (FrameDetector.Synced()) {
1743  // Synced FrameDetector, so rewind for actual processing:
1744  Rewind = true;
1745  }
1746  Buffer.Del(Processed);
1747  }
1748  }
1749  else {
1750  // Step 1 - parse PAT/PMT:
1751  uchar *p = Data;
1752  while (Length >= TS_SIZE) {
1753  int Pid = TsPid(p);
1754  if (Pid == PATPID)
1755  PatPmtParser.ParsePat(p, TS_SIZE);
1756  else if (PatPmtParser.IsPmtPid(Pid))
1757  PatPmtParser.ParsePmt(p, TS_SIZE);
1758  Length -= TS_SIZE;
1759  p += TS_SIZE;
1760  if (PatPmtParser.Vpid()) {
1761  // Found Vpid, so rewind to sync FrameDetector:
1762  FrameDetector.SetPid(PatPmtParser.Vpid(), PatPmtParser.Vtype());
1763  BufferChunks = IFG_BUFFER_SIZE;
1764  Rewind = true;
1765  break;
1766  }
1767  }
1768  Buffer.Del(p - Data);
1769  }
1770  }
1771  // Read data:
1772  else if (ReplayFile) {
1773  int Result = Buffer.Read(ReplayFile, BufferChunks);
1774  if (Result == 0) { // EOF
1775  ReplayFile = FileName.NextFile();
1776  FileSize = 0;
1777  FrameOffset = -1;
1778  Buffer.Clear();
1779  }
1780  }
1781  // Recording has been processed:
1782  else {
1783  IndexFileComplete = true;
1784  break;
1785  }
1786  }
1787  if (IndexFileComplete) {
1788  if (IndexFileWritten) {
1789  cRecordingInfo RecordingInfo(recordingName);
1790  if (RecordingInfo.Read()) {
1791  if (FrameDetector.FramesPerSecond() > 0 && !DoubleEqual(RecordingInfo.FramesPerSecond(), FrameDetector.FramesPerSecond())) {
1792  RecordingInfo.SetFramesPerSecond(FrameDetector.FramesPerSecond());
1793  RecordingInfo.Write();
1794  Recordings.UpdateByName(recordingName);
1795  }
1796  }
1797  Skins.QueueMessage(mtInfo, tr("Index file regeneration complete"));
1798  return;
1799  }
1800  else
1801  Skins.QueueMessage(mtError, tr("Index file regeneration failed!"));
1802  }
1803  // Delete the index file if the recording has not been processed entirely:
1804  IndexFile.Delete();
1805 }
1806 
1807 // --- cIndexFile ------------------------------------------------------------
1808 
1809 #define INDEXFILESUFFIX "/index"
1810 
1811 // The maximum time to wait before giving up while catching up on an index file:
1812 #define MAXINDEXCATCHUP 8 // number of retries
1813 #define INDEXCATCHUPWAIT 100 // milliseconds
1814 
1815 struct tIndexPes {
1816  uint32_t offset;
1819  uint16_t reserved;
1820  };
1821 
1822 struct tIndexTs {
1823  uint64_t offset:40; // up to 1TB per file (not using off_t here - must definitely be exactly 64 bit!)
1824  int reserved:7; // reserved for future use
1825  int independent:1; // marks frames that can be displayed by themselves (for trick modes)
1826  uint16_t number:16; // up to 64K files per recording
1827  tIndexTs(off_t Offset, bool Independent, uint16_t Number)
1828  {
1829  offset = Offset;
1830  reserved = 0;
1831  independent = Independent;
1832  number = Number;
1833  }
1834  };
1835 
1836 #define MAXWAITFORINDEXFILE 10 // max. time to wait for the regenerated index file (seconds)
1837 #define INDEXFILECHECKINTERVAL 500 // ms between checks for existence of the regenerated index file
1838 #define INDEXFILETESTINTERVAL 10 // ms between tests for the size of the index file in case of pausing live video
1839 
1840 cIndexFile::cIndexFile(const char *FileName, bool Record, bool IsPesRecording, bool PauseLive)
1841 :resumeFile(FileName, IsPesRecording)
1842 {
1843  f = -1;
1844  size = 0;
1845  last = -1;
1846  index = NULL;
1847  isPesRecording = IsPesRecording;
1848  indexFileGenerator = NULL;
1849  if (FileName) {
1850  fileName = IndexFileName(FileName, isPesRecording);
1851  if (!Record && PauseLive) {
1852  // Wait until the index file contains at least two frames:
1853  time_t tmax = time(NULL) + MAXWAITFORINDEXFILE;
1854  while (time(NULL) < tmax && FileSize(fileName) < off_t(2 * sizeof(tIndexTs)))
1856  }
1857  int delta = 0;
1858  if (!Record && access(fileName, R_OK) != 0) {
1859  // Index file doesn't exist, so try to regenerate it:
1860  if (!isPesRecording) { // sorry, can only do this for TS recordings
1861  resumeFile.Delete(); // just in case
1862  indexFileGenerator = new cIndexFileGenerator(FileName);
1863  // Wait until the index file exists:
1864  time_t tmax = time(NULL) + MAXWAITFORINDEXFILE;
1865  do {
1866  cCondWait::SleepMs(INDEXFILECHECKINTERVAL); // start with a sleep, to give it a head start
1867  } while (access(fileName, R_OK) != 0 && time(NULL) < tmax);
1868  }
1869  }
1870  if (access(fileName, R_OK) == 0) {
1871  struct stat buf;
1872  if (stat(fileName, &buf) == 0) {
1873  delta = int(buf.st_size % sizeof(tIndexTs));
1874  if (delta) {
1875  delta = sizeof(tIndexTs) - delta;
1876  esyslog("ERROR: invalid file size (%"PRId64") in '%s'", buf.st_size, *fileName);
1877  }
1878  last = int((buf.st_size + delta) / sizeof(tIndexTs) - 1);
1879  if (!Record && last >= 0) {
1880  size = last + 1;
1881  index = MALLOC(tIndexTs, size);
1882  if (index) {
1883  f = open(fileName, O_RDONLY);
1884  if (f >= 0) {
1885  if (safe_read(f, index, size_t(buf.st_size)) != buf.st_size) {
1886  esyslog("ERROR: can't read from file '%s'", *fileName);
1887  free(index);
1888  index = NULL;
1889  }
1890  else if (isPesRecording)
1892  if (!index || time(NULL) - buf.st_mtime >= MININDEXAGE) {
1893  close(f);
1894  f = -1;
1895  }
1896  // otherwise we don't close f here, see CatchUp()!
1897  }
1898  else
1900  }
1901  else
1902  esyslog("ERROR: can't allocate %zd bytes for index '%s'", size * sizeof(tIndexTs), *fileName);
1903  }
1904  }
1905  else
1906  LOG_ERROR;
1907  }
1908  else if (!Record)
1909  isyslog("missing index file %s", *fileName);
1910  if (Record) {
1911  if ((f = open(fileName, O_WRONLY | O_CREAT | O_APPEND, DEFFILEMODE)) >= 0) {
1912  if (delta) {
1913  esyslog("ERROR: padding index file with %d '0' bytes", delta);
1914  while (delta--)
1915  writechar(f, 0);
1916  }
1917  }
1918  else
1920  }
1921  }
1922 }
1923 
1925 {
1926  if (f >= 0)
1927  close(f);
1928  free(index);
1929  delete indexFileGenerator;
1930 }
1931 
1932 cString cIndexFile::IndexFileName(const char *FileName, bool IsPesRecording)
1933 {
1934  return cString::sprintf("%s%s", FileName, IsPesRecording ? INDEXFILESUFFIX ".vdr" : INDEXFILESUFFIX);
1935 }
1936 
1937 void cIndexFile::ConvertFromPes(tIndexTs *IndexTs, int Count)
1938 {
1939  tIndexPes IndexPes;
1940  while (Count-- > 0) {
1941  memcpy(&IndexPes, IndexTs, sizeof(IndexPes));
1942  IndexTs->offset = IndexPes.offset;
1943  IndexTs->independent = IndexPes.type == 1; // I_FRAME
1944  IndexTs->number = IndexPes.number;
1945  IndexTs++;
1946  }
1947 }
1948 
1949 void cIndexFile::ConvertToPes(tIndexTs *IndexTs, int Count)
1950 {
1951  tIndexPes IndexPes;
1952  while (Count-- > 0) {
1953  IndexPes.offset = uint32_t(IndexTs->offset);
1954  IndexPes.type = uchar(IndexTs->independent ? 1 : 2); // I_FRAME : "not I_FRAME" (exact frame type doesn't matter)
1955  IndexPes.number = uchar(IndexTs->number);
1956  IndexPes.reserved = 0;
1957  memcpy(IndexTs, &IndexPes, sizeof(*IndexTs));
1958  IndexTs++;
1959  }
1960 }
1961 
1962 bool cIndexFile::CatchUp(int Index)
1963 {
1964  // returns true unless something really goes wrong, so that 'index' becomes NULL
1965  if (index && f >= 0) {
1966  cMutexLock MutexLock(&mutex);
1967  // Note that CatchUp() is triggered even if Index is 'last' (and thus valid).
1968  // This is done to make absolutely sure we don't miss any data at the very end.
1969  for (int i = 0; i <= MAXINDEXCATCHUP && (Index < 0 || Index >= last); i++) {
1970  struct stat buf;
1971  if (fstat(f, &buf) == 0) {
1972  int newLast = int(buf.st_size / sizeof(tIndexTs) - 1);
1973  if (newLast > last) {
1974  int NewSize = size;
1975  if (NewSize <= newLast) {
1976  NewSize *= 2;
1977  if (NewSize <= newLast)
1978  NewSize = newLast + 1;
1979  }
1980  if (tIndexTs *NewBuffer = (tIndexTs *)realloc(index, NewSize * sizeof(tIndexTs))) {
1981  size = NewSize;
1982  index = NewBuffer;
1983  int offset = (last + 1) * sizeof(tIndexTs);
1984  int delta = (newLast - last) * sizeof(tIndexTs);
1985  if (lseek(f, offset, SEEK_SET) == offset) {
1986  if (safe_read(f, &index[last + 1], delta) != delta) {
1987  esyslog("ERROR: can't read from index");
1988  free(index);
1989  index = NULL;
1990  close(f);
1991  f = -1;
1992  break;
1993  }
1994  if (isPesRecording)
1995  ConvertFromPes(&index[last + 1], newLast - last);
1996  last = newLast;
1997  }
1998  else
2000  }
2001  else {
2002  esyslog("ERROR: can't realloc() index");
2003  break;
2004  }
2005  }
2006  }
2007  else
2009  if (Index < last)
2010  break;
2011  cCondVar CondVar;
2012  CondVar.TimedWait(mutex, INDEXCATCHUPWAIT);
2013  }
2014  }
2015  return index != NULL;
2016 }
2017 
2018 bool cIndexFile::Write(bool Independent, uint16_t FileNumber, off_t FileOffset)
2019 {
2020  if (f >= 0) {
2021  tIndexTs i(FileOffset, Independent, FileNumber);
2022  if (isPesRecording)
2023  ConvertToPes(&i, 1);
2024  if (safe_write(f, &i, sizeof(i)) < 0) {
2026  close(f);
2027  f = -1;
2028  return false;
2029  }
2030  last++;
2031  }
2032  return f >= 0;
2033 }
2034 
2035 bool cIndexFile::Get(int Index, uint16_t *FileNumber, off_t *FileOffset, bool *Independent, int *Length)
2036 {
2037  if (CatchUp(Index)) {
2038  if (Index >= 0 && Index <= last) {
2039  *FileNumber = index[Index].number;
2040  *FileOffset = index[Index].offset;
2041  if (Independent)
2042  *Independent = index[Index].independent;
2043  if (Length) {
2044  if (Index < last) {
2045  uint16_t fn = index[Index + 1].number;
2046  off_t fo = index[Index + 1].offset;
2047  if (fn == *FileNumber)
2048  *Length = int(fo - *FileOffset);
2049  else
2050  *Length = -1; // this means "everything up to EOF" (the buffer's Read function will act accordingly)
2051  }
2052  else
2053  *Length = -1;
2054  }
2055  return true;
2056  }
2057  }
2058  return false;
2059 }
2060 
2061 int cIndexFile::GetNextIFrame(int Index, bool Forward, uint16_t *FileNumber, off_t *FileOffset, int *Length)
2062 {
2063  if (CatchUp()) {
2064  int d = Forward ? 1 : -1;
2065  for (;;) {
2066  Index += d;
2067  if (Index >= 0 && Index <= last) {
2068  if (index[Index].independent) {
2069  uint16_t fn;
2070  if (!FileNumber)
2071  FileNumber = &fn;
2072  off_t fo;
2073  if (!FileOffset)
2074  FileOffset = &fo;
2075  *FileNumber = index[Index].number;
2076  *FileOffset = index[Index].offset;
2077  if (Length) {
2078  if (Index < last) {
2079  uint16_t fn = index[Index + 1].number;
2080  off_t fo = index[Index + 1].offset;
2081  if (fn == *FileNumber)
2082  *Length = int(fo - *FileOffset);
2083  else
2084  *Length = -1; // this means "everything up to EOF" (the buffer's Read function will act accordingly)
2085  }
2086  else
2087  *Length = -1;
2088  }
2089  return Index;
2090  }
2091  }
2092  else
2093  break;
2094  }
2095  }
2096  return -1;
2097 }
2098 
2100 {
2101  if (last > 0) {
2102  Index = constrain(Index, 0, last);
2103  if (index[Index].independent)
2104  return Index;
2105  int il = Index - 1;
2106  int ih = Index + 1;
2107  for (;;) {
2108  if (il >= 0) {
2109  if (index[il].independent)
2110  return il;
2111  il--;
2112  }
2113  else if (ih > last)
2114  break;
2115  if (ih <= last) {
2116  if (index[ih].independent)
2117  return ih;
2118  ih++;
2119  }
2120  else if (il < 0)
2121  break;
2122  }
2123  }
2124  return 0;
2125 }
2126 
2127 int cIndexFile::Get(uint16_t FileNumber, off_t FileOffset)
2128 {
2129  if (CatchUp()) {
2130  //TODO implement binary search!
2131  int i;
2132  for (i = 0; i <= last; i++) {
2133  if (index[i].number > FileNumber || (index[i].number == FileNumber) && off_t(index[i].offset) >= FileOffset)
2134  break;
2135  }
2136  return i;
2137  }
2138  return -1;
2139 }
2140 
2142 {
2143  return f >= 0;
2144 }
2145 
2147 {
2148  if (*fileName) {
2149  dsyslog("deleting index file '%s'", *fileName);
2150  if (f >= 0) {
2151  close(f);
2152  f = -1;
2153  }
2154  unlink(fileName);
2155  }
2156 }
2157 
2158 int cIndexFile::GetLength(const char *FileName, bool IsPesRecording)
2159 {
2160  struct stat buf;
2161  cString s = IndexFileName(FileName, IsPesRecording);
2162  if (*s && stat(s, &buf) == 0)
2163  return buf.st_size / (IsPesRecording ? sizeof(tIndexTs) : sizeof(tIndexPes));
2164  return -1;
2165 }
2166 
2167 bool GenerateIndex(const char *FileName)
2168 {
2169  if (DirectoryOk(FileName)) {
2170  cRecording Recording(FileName);
2171  if (Recording.Name()) {
2172  if (!Recording.IsPesRecording()) {
2173  cString IndexFileName = AddDirectory(FileName, INDEXFILESUFFIX);
2174  unlink(IndexFileName);
2175  cIndexFileGenerator *IndexFileGenerator = new cIndexFileGenerator(FileName);
2176  while (IndexFileGenerator->Active())
2178  if (access(IndexFileName, R_OK) == 0)
2179  return true;
2180  else
2181  fprintf(stderr, "cannot create '%s'\n", *IndexFileName);
2182  }
2183  else
2184  fprintf(stderr, "'%s' is not a TS recording\n", FileName);
2185  }
2186  else
2187  fprintf(stderr, "'%s' is not a recording\n", FileName);
2188  }
2189  else
2190  fprintf(stderr, "'%s' is not a directory\n", FileName);
2191  return false;
2192 }
2193 
2194 // --- cFileName -------------------------------------------------------------
2195 
2196 #define MAXFILESPERRECORDINGPES 255
2197 #define RECORDFILESUFFIXPES "/%03d.vdr"
2198 #define MAXFILESPERRECORDINGTS 65535
2199 #define RECORDFILESUFFIXTS "/%05d.ts"
2200 #define RECORDFILESUFFIXLEN 20 // some additional bytes for safety...
2201 
2202 cFileName::cFileName(const char *FileName, bool Record, bool Blocking, bool IsPesRecording)
2203 {
2204  file = NULL;
2205  fileNumber = 0;
2206  record = Record;
2207  blocking = Blocking;
2208  isPesRecording = IsPesRecording;
2209  // Prepare the file name:
2210  fileName = MALLOC(char, strlen(FileName) + RECORDFILESUFFIXLEN);
2211  if (!fileName) {
2212  esyslog("ERROR: can't copy file name '%s'", fileName);
2213  return;
2214  }
2215  strcpy(fileName, FileName);
2216  pFileNumber = fileName + strlen(fileName);
2217  SetOffset(1);
2218 }
2219 
2221 {
2222  Close();
2223  free(fileName);
2224 }
2225 
2226 bool cFileName::GetLastPatPmtVersions(int &PatVersion, int &PmtVersion)
2227 {
2228  if (fileName && !isPesRecording) {
2229  // Find the last recording file:
2230  int Number = 1;
2231  for (; Number <= MAXFILESPERRECORDINGTS + 1; Number++) { // +1 to correctly set Number in case there actually are that many files
2232  sprintf(pFileNumber, RECORDFILESUFFIXTS, Number);
2233  if (access(fileName, F_OK) != 0) { // file doesn't exist
2234  Number--;
2235  break;
2236  }
2237  }
2238  for (; Number > 0; Number--) {
2239  // Search for a PAT packet from the end of the file:
2240  cPatPmtParser PatPmtParser;
2241  sprintf(pFileNumber, RECORDFILESUFFIXTS, Number);
2242  int fd = open(fileName, O_RDONLY | O_LARGEFILE, DEFFILEMODE);
2243  if (fd >= 0) {
2244  off_t pos = lseek(fd, -TS_SIZE, SEEK_END);
2245  while (pos >= 0) {
2246  // Read and parse the PAT/PMT:
2247  uchar buf[TS_SIZE];
2248  while (read(fd, buf, sizeof(buf)) == sizeof(buf)) {
2249  if (buf[0] == TS_SYNC_BYTE) {
2250  int Pid = TsPid(buf);
2251  if (Pid == PATPID)
2252  PatPmtParser.ParsePat(buf, sizeof(buf));
2253  else if (PatPmtParser.IsPmtPid(Pid)) {
2254  PatPmtParser.ParsePmt(buf, sizeof(buf));
2255  if (PatPmtParser.GetVersions(PatVersion, PmtVersion)) {
2256  close(fd);
2257  return true;
2258  }
2259  }
2260  else
2261  break; // PAT/PMT is always in one sequence
2262  }
2263  else
2264  return false;
2265  }
2266  pos = lseek(fd, pos - TS_SIZE, SEEK_SET);
2267  }
2268  close(fd);
2269  }
2270  else
2271  break;
2272  }
2273  }
2274  return false;
2275 }
2276 
2278 {
2279  if (!file) {
2280  int BlockingFlag = blocking ? 0 : O_NONBLOCK;
2281  if (record) {
2282  dsyslog("recording to '%s'", fileName);
2283  file = OpenVideoFile(fileName, O_RDWR | O_CREAT | O_LARGEFILE | BlockingFlag);
2284  if (!file)
2286  }
2287  else {
2288  if (access(fileName, R_OK) == 0) {
2289  dsyslog("playing '%s'", fileName);
2290  file = cUnbufferedFile::Create(fileName, O_RDONLY | O_LARGEFILE | BlockingFlag);
2291  if (!file)
2293  }
2294  else if (errno != ENOENT)
2296  }
2297  }
2298  return file;
2299 }
2300 
2302 {
2303  if (file) {
2304  if (CloseVideoFile(file) < 0)
2306  file = NULL;
2307  }
2308 }
2309 
2310 cUnbufferedFile *cFileName::SetOffset(int Number, off_t Offset)
2311 {
2312  if (fileNumber != Number)
2313  Close();
2314  int MaxFilesPerRecording = isPesRecording ? MAXFILESPERRECORDINGPES : MAXFILESPERRECORDINGTS;
2315  if (0 < Number && Number <= MaxFilesPerRecording) {
2316  fileNumber = uint16_t(Number);
2318  if (record) {
2319  if (access(fileName, F_OK) == 0) {
2320  // file exists, check if it has non-zero size
2321  struct stat buf;
2322  if (stat(fileName, &buf) == 0) {
2323  if (buf.st_size != 0)
2324  return SetOffset(Number + 1); // file exists and has non zero size, let's try next suffix
2325  else {
2326  // zero size file, remove it
2327  dsyslog("cFileName::SetOffset: removing zero-sized file %s", fileName);
2328  unlink(fileName);
2329  }
2330  }
2331  else
2332  return SetOffset(Number + 1); // error with fstat - should not happen, just to be on the safe side
2333  }
2334  else if (errno != ENOENT) { // something serious has happened
2336  return NULL;
2337  }
2338  // found a non existing file suffix
2339  }
2340  if (Open() >= 0) {
2341  if (!record && Offset >= 0 && file && file->Seek(Offset, SEEK_SET) != Offset) {
2343  return NULL;
2344  }
2345  }
2346  return file;
2347  }
2348  esyslog("ERROR: max number of files (%d) exceeded", MaxFilesPerRecording);
2349  return NULL;
2350 }
2351 
2353 {
2354  return SetOffset(fileNumber + 1);
2355 }
2356 
2357 // --- Index stuff -----------------------------------------------------------
2358 
2359 cString IndexToHMSF(int Index, bool WithFrame, double FramesPerSecond)
2360 {
2361  const char *Sign = "";
2362  if (Index < 0) {
2363  Index = -Index;
2364  Sign = "-";
2365  }
2366  double Seconds;
2367  int f = int(modf((Index + 0.5) / FramesPerSecond, &Seconds) * FramesPerSecond + 1);
2368  int s = int(Seconds);
2369  int m = s / 60 % 60;
2370  int h = s / 3600;
2371  s %= 60;
2372  return cString::sprintf(WithFrame ? "%s%d:%02d:%02d.%02d" : "%s%d:%02d:%02d", Sign, h, m, s, f);
2373 }
2374 
2375 int HMSFToIndex(const char *HMSF, double FramesPerSecond)
2376 {
2377  int h, m, s, f = 1;
2378  int n = sscanf(HMSF, "%d:%d:%d.%d", &h, &m, &s, &f);
2379  if (n == 1)
2380  return h - 1; // plain frame number
2381  if (n >= 3)
2382  return int(round((h * 3600 + m * 60 + s) * FramesPerSecond)) + f - 1;
2383  return 0;
2384 }
2385 
2386 int SecondsToFrames(int Seconds, double FramesPerSecond)
2387 {
2388  return int(round(Seconds * FramesPerSecond));
2389 }
2390 
2391 // --- ReadFrame -------------------------------------------------------------
2392 
2393 int ReadFrame(cUnbufferedFile *f, uchar *b, int Length, int Max)
2394 {
2395  if (Length == -1)
2396  Length = Max; // this means we read up to EOF (see cIndex)
2397  else if (Length > Max) {
2398  esyslog("ERROR: frame larger than buffer (%d > %d)", Length, Max);
2399  Length = Max;
2400  }
2401  int r = f->Read(b, Length);
2402  if (r < 0)
2403  LOG_ERROR;
2404  return r;
2405 }
2406 
2407 // --- Recordings Sort Mode --------------------------------------------------
2408 
2410 
2411 bool HasRecordingsSortMode(const char *Directory)
2412 {
2413  return access(AddDirectory(Directory, SORTMODEFILE), R_OK) == 0;
2414 }
2415 
2416 void GetRecordingsSortMode(const char *Directory)
2417 {
2418  if (FILE *f = fopen(AddDirectory(Directory, SORTMODEFILE), "r")) {
2419  char buf[8];
2420  if (fgets(buf, sizeof(buf), f))
2422  fclose(f);
2423  }
2424 }
2425 
2426 void SetRecordingsSortMode(const char *Directory, eRecordingsSortMode SortMode)
2427 {
2428  if (FILE *f = fopen(AddDirectory(Directory, SORTMODEFILE), "w")) {
2429  fputs(cString::sprintf("%d\n", SortMode), f);
2430  fclose(f);
2431  }
2432 }
2433 
2434 void IncRecordingsSortMode(const char *Directory)
2435 {
2436  GetRecordingsSortMode(Directory);
2441 }
2442