vdr  1.7.31
cutter.c
Go to the documentation of this file.
1 /*
2  * cutter.c: The video cutting facilities
3  *
4  * See the main source file 'vdr.c' for copyright information and
5  * how to reach the author.
6  *
7  * $Id: cutter.c 2.14 2012/09/20 09:12:47 kls Exp $
8  */
9 
10 #include "cutter.h"
11 #include "interface.h"
12 #include "menu.h"
13 #include "recording.h"
14 #include "remux.h"
15 #include "videodir.h"
16 
17 // --- cCuttingThread --------------------------------------------------------
18 
19 class cCuttingThread : public cThread {
20 private:
21  const char *error;
28 protected:
29  virtual void Action(void);
30 public:
31  cCuttingThread(const char *FromFileName, const char *ToFileName);
32  virtual ~cCuttingThread();
33  const char *Error(void) { return error; }
34  };
35 
36 cCuttingThread::cCuttingThread(const char *FromFileName, const char *ToFileName)
37 :cThread("video cutting")
38 {
39  error = NULL;
40  fromFile = toFile = NULL;
41  fromFileName = toFileName = NULL;
42  fromIndex = toIndex = NULL;
43  cRecording Recording(FromFileName);
44  isPesRecording = Recording.IsPesRecording();
45  if (fromMarks.Load(FromFileName, Recording.FramesPerSecond(), isPesRecording) && fromMarks.Count()) {
46  fromFileName = new cFileName(FromFileName, false, true, isPesRecording);
47  toFileName = new cFileName(ToFileName, true, true, isPesRecording);
48  fromIndex = new cIndexFile(FromFileName, false, isPesRecording);
49  toIndex = new cIndexFile(ToFileName, true, isPesRecording);
50  toMarks.Load(ToFileName, Recording.FramesPerSecond(), isPesRecording); // doesn't actually load marks, just sets the file name
54  Start();
55  }
56  else
57  esyslog("no editing marks found for %s", FromFileName);
58 }
59 
61 {
62  Cancel(3);
63  delete fromFileName;
64  delete toFileName;
65  delete fromIndex;
66  delete toIndex;
67 }
68 
70 {
71  cMark *Mark = fromMarks.First();
72  if (Mark) {
73  SetPriority(19);
74  SetIOPriority(7);
76  toFile = toFileName->Open();
77  if (!fromFile || !toFile)
78  return;
80  int Index = Mark->Position();
81  Mark = fromMarks.Next(Mark);
82  off_t FileSize = 0;
83  int CurrentFileNumber = 0;
84  bool SkipThisSourceFile = false;
85  int LastIFrame = 0;
86  toMarks.Add(0);
87  toMarks.Save();
88  uchar buffer[MAXFRAMESIZE], buffer2[MAXFRAMESIZE];
89  int Length2;
90  bool CheckForSeamlessStream = false;
91  bool LastMark = false;
92  bool cutIn = true;
93  bool suspensionLogged = false;
94  while (Running()) {
95  uint16_t FileNumber;
96  off_t FileOffset;
97  int Length;
98  bool Independent;
99 
100  // Suspend cutting if we have severe throughput problems:
101 
102  if (cIoThrottle::Engaged()) {
103  if (!suspensionLogged) {
104  dsyslog("suspending cutter thread");
105  suspensionLogged = true;
106  }
107  cCondWait::SleepMs(100);
108  continue;
109  }
110  else if (suspensionLogged) {
111  dsyslog("resuming cutter thread");
112  suspensionLogged = false;
113  }
114 
115  // Make sure there is enough disk space:
116 
118 
119  // Read one frame:
120 
121  if (!fromIndex->Get(Index++, &FileNumber, &FileOffset, &Independent, &Length)) {
122  // Error, unless we're past last cut-in and there's no cut-out
123  if (Mark || LastMark)
124  error = "index";
125  break;
126  }
127 
128  if (FileNumber != CurrentFileNumber) {
129  fromFile = fromFileName->SetOffset(FileNumber, FileOffset);
130  if (fromFile)
132  CurrentFileNumber = FileNumber;
133  if (SkipThisSourceFile) {
134  // At end of fast forward: Always skip to next file
136  if (!toFile) {
137  error = "toFile 4";
138  break;
139  }
140  FileSize = 0;
141  SkipThisSourceFile = false;
142  }
143 
144 
145  if (Setup.HardLinkCutter && FileOffset == 0) {
146  // We are at the beginning of a new source file.
147  // Do we need to copy the whole file?
148 
149  // if !Mark && LastMark, then we're past the last cut-out and continue to next I-frame
150  // if !Mark && !LastMark, then there's just a cut-in, but no cut-out
151  // if Mark, then we're between a cut-in and a cut-out
152 
153  uint16_t MarkFileNumber;
154  off_t MarkFileOffset;
155  // Get file number of next cut mark
156  if (!Mark && !LastMark
157  || Mark
158  && fromIndex->Get(Mark->Position(), &MarkFileNumber, &MarkFileOffset)
159  && (MarkFileNumber != CurrentFileNumber)) {
160  // The current source file will be copied completely.
161  // Start new output file unless we did that already
162  if (FileSize != 0) {
164  if (!toFile) {
165  error = "toFile 3";
166  break;
167  }
168  FileSize = 0;
169  }
170 
171  // Safety check that file has zero size
172  struct stat buf;
173  if (stat(toFileName->Name(), &buf) == 0) {
174  if (buf.st_size != 0) {
175  esyslog("cCuttingThread: File %s exists and has nonzero size", toFileName->Name());
176  error = "nonzero file exist";
177  break;
178  }
179  }
180  else if (errno != ENOENT) {
181  esyslog("cCuttingThread: stat failed on %s", toFileName->Name());
182  error = "stat";
183  break;
184  }
185 
186  // Clean the existing 0-byte file
187  toFileName->Close();
188  cString ActualToFileName(ReadLink(toFileName->Name()), true);
189  unlink(ActualToFileName);
190  unlink(toFileName->Name());
191 
192  // Try to create a hard link
194  // Success. Skip all data transfer for this file
195  SkipThisSourceFile = true;
196  cutIn = false;
197  toFile = NULL; // was deleted by toFileName->Close()
198  }
199  else {
200  // Fallback: Re-open the file if necessary
201  toFile = toFileName->Open();
202  }
203  }
204  }
205  }
206 
207  if (!SkipThisSourceFile) {
208  if (fromFile) {
209  int len = ReadFrame(fromFile, buffer, Length, sizeof(buffer));
210  if (len < 0) {
211  error = "ReadFrame";
212  break;
213  }
214  if (len != Length) {
215  CurrentFileNumber = 0; // this re-syncs in case the frame was larger than the buffer
216  Length = len;
217  }
218  }
219  else {
220  error = "fromFile";
221  break;
222  }
223  }
224  // Write one frame:
225 
226  if (Independent) { // every file shall start with an independent frame
227  if (LastMark) // edited version shall end before next I-frame
228  break;
229  if (!SkipThisSourceFile && FileSize > toFileName->MaxFileSize()) {
231  if (!toFile) {
232  error = "toFile 1";
233  break;
234  }
235  FileSize = 0;
236  }
237  LastIFrame = 0;
238  // Compare the current frame with the previously stored one, to see if this is a seamlessly merged recording of the same stream:
239  if (CheckForSeamlessStream) {
240  if (Length == Length2) {
241  int diffs = 0;
242  for (int i = 0; i < Length; i++) {
243  if (buffer[i] != buffer2[i]) {
244  if (diffs++ > 10)
245  break;
246  }
247  }
248  if (diffs < 10) // the continuity counters of the PAT/PMT packets may differ
249  cutIn = false; // it's apparently a seamless stream, so no need for "broken" handling
250  }
251  CheckForSeamlessStream = false;
252  }
253  if (!SkipThisSourceFile && cutIn) {
254  if (isPesRecording)
255  cRemux::SetBrokenLink(buffer, Length);
256  else
257  TsSetTeiOnBrokenPackets(buffer, Length);
258  cutIn = false;
259  }
260  }
261  if (!SkipThisSourceFile && toFile->Write(buffer, Length) < 0) {
262  error = "safe_write";
263  break;
264  }
265  if (!toIndex->Write(Independent, toFileName->Number(), FileSize)) {
266  error = "toIndex";
267  break;
268  }
269  FileSize += Length;
270  if (!LastIFrame)
271  LastIFrame = toIndex->Last();
272 
273  // Check editing marks:
274 
275  if (Mark && Index >= Mark->Position()) {
276  Mark = fromMarks.Next(Mark);
277  toMarks.Add(LastIFrame);
278  if (Mark)
279  toMarks.Add(toIndex->Last() + 1);
280  toMarks.Save();
281  if (Mark) {
282  // Read the next frame, for later comparison with the first frame at this mark:
283  if (fromIndex->Get(Index, &FileNumber, &FileOffset, &Independent, &Length2)) {
284  if (FileNumber != CurrentFileNumber)
285  fromFile = fromFileName->SetOffset(FileNumber, FileOffset);
286  if (fromFile) {
287  int len = ReadFrame(fromFile, buffer2, Length2, sizeof(buffer2));
288  if (len >= 0 && len == Length2)
289  CheckForSeamlessStream = true;
290  }
291  }
292  Index = Mark->Position();
293  Mark = fromMarks.Next(Mark);
294  CurrentFileNumber = 0; // triggers SetOffset before reading next frame
295  cutIn = true;
296  if (Setup.SplitEditedFiles) {
298  if (!toFile) {
299  error = "toFile 2";
300  break;
301  }
302  FileSize = 0;
303  }
304  }
305  else
306  LastMark = true; // After last cut-out: Write on until next I-frame, then exit
307  }
308  }
310  }
311  else
312  esyslog("no editing marks found!");
313 }
314 
315 // --- cCutter ---------------------------------------------------------------
316 
321 bool cCutter::error = false;
322 bool cCutter::ended = false;
323 
324 bool cCutter::Start(const char *FileName, const char *TargetFileName, bool Overwrite)
325 {
326  cMutexLock MutexLock(&mutex);
327  if (!cuttingThread) {
328  error = false;
329  ended = false;
330  originalVersionName = FileName;
331  cRecording Recording(FileName);
332 
333  cMarks FromMarks;
334  FromMarks.Load(FileName, Recording.FramesPerSecond(), Recording.IsPesRecording());
335  if (cMark *First = FromMarks.First())
336  Recording.SetStartTime(Recording.Start() + (int(First->Position() / Recording.FramesPerSecond() + 30) / 60) * 60);
337 
338  cString evn = (TargetFileName && *TargetFileName) ? Recording.UpdateFileName(TargetFileName) : Recording.PrefixFileName('%');
339  if (!Overwrite && *evn && (access(*evn, F_OK) == 0) && !Interface->Confirm(tr("File already exists - overwrite?"))) {
340  do {
341  evn = PrefixVideoFileName(*evn, '%');
342  } while (*evn && (access(*evn, F_OK) == 0));
343  }
344  if (*evn && RemoveVideoFile(*evn) && MakeDirs(*evn, true)) {
345  // XXX this can be removed once RenameVideoFile() follows symlinks (see videodir.c)
346  // remove a possible deleted recording with the same name to avoid symlink mixups:
347  char *s = strdup(*evn);
348  char *e = strrchr(s, '.');
349  if (e) {
350  if (strcmp(e, ".rec") == 0) {
351  strcpy(e, ".del");
352  RemoveVideoFile(s);
353  }
354  }
355  free(s);
356  // XXX
357  editedVersionName = evn;
358  Recording.WriteInfo();
361  return true;
362  }
363  }
364  return false;
365 }
366 
367 void cCutter::Stop(void)
368 {
369  cMutexLock MutexLock(&mutex);
370  bool Interrupted = cuttingThread && cuttingThread->Active();
371  const char *Error = cuttingThread ? cuttingThread->Error() : NULL;
372  delete cuttingThread;
373  cuttingThread = NULL;
374  if ((Interrupted || Error) && *editedVersionName) {
375  if (Interrupted)
376  isyslog("editing process has been interrupted");
377  if (Error)
378  esyslog("ERROR: '%s' during editing process", Error);
383  }
384 }
385 
386 bool cCutter::Active(const char *FileName)
387 {
388  cMutexLock MutexLock(&mutex);
389  if (cuttingThread) {
390  if (cuttingThread->Active())
391  return !FileName || strcmp(FileName, originalVersionName) == 0 || strcmp(FileName, editedVersionName) == 0;
393  Stop();
394  if (!error)
396  originalVersionName = NULL;
397  editedVersionName = NULL;
398  ended = true;
399  }
400  return false;
401 }
402 
403 bool cCutter::Error(void)
404 {
405  cMutexLock MutexLock(&mutex);
406  bool result = error;
407  error = false;
408  return result;
409 }
410 
411 bool cCutter::Ended(void)
412 {
413  cMutexLock MutexLock(&mutex);
414  bool result = ended;
415  ended = false;
416  return result;
417 }
418 
419 #define CUTTINGCHECKINTERVAL 500 // ms between checks for the active cutting process
420 
421 bool CutRecording(const char *FileName)
422 {
423  if (DirectoryOk(FileName)) {
424  cRecording Recording(FileName);
425  if (Recording.Name()) {
426  cMarks Marks;
427  if (Marks.Load(FileName, Recording.FramesPerSecond(), Recording.IsPesRecording()) && Marks.Count()) {
428  if (cCutter::Start(FileName)) {
429  while (cCutter::Active())
431  return true;
432  }
433  else
434  fprintf(stderr, "can't start editing process\n");
435  }
436  else
437  fprintf(stderr, "'%s' has no editing marks\n", FileName);
438  }
439  else
440  fprintf(stderr, "'%s' is not a recording\n", FileName);
441  }
442  else
443  fprintf(stderr, "'%s' is not a directory\n", FileName);
444  return false;
445 }