vdr  1.7.27
cutter.c
Go to the documentation of this file.
00001 /*
00002  * cutter.c: The video cutting facilities
00003  *
00004  * See the main source file 'vdr.c' for copyright information and
00005  * how to reach the author.
00006  *
00007  * $Id: cutter.c 2.11 2012/02/16 12:08:39 kls Exp $
00008  */
00009 
00010 #include "cutter.h"
00011 #include "interface.h"
00012 #include "menu.h"
00013 #include "recording.h"
00014 #include "remux.h"
00015 #include "videodir.h"
00016 
00017 // --- cCuttingThread --------------------------------------------------------
00018 
00019 class cCuttingThread : public cThread {
00020 private:
00021   const char *error;
00022   bool isPesRecording;
00023   cUnbufferedFile *fromFile, *toFile;
00024   cFileName *fromFileName, *toFileName;
00025   cIndexFile *fromIndex, *toIndex;
00026   cMarks fromMarks, toMarks;
00027   off_t maxVideoFileSize;
00028 protected:
00029   virtual void Action(void);
00030 public:
00031   cCuttingThread(const char *FromFileName, const char *ToFileName);
00032   virtual ~cCuttingThread();
00033   const char *Error(void) { return error; }
00034   };
00035 
00036 cCuttingThread::cCuttingThread(const char *FromFileName, const char *ToFileName)
00037 :cThread("video cutting")
00038 {
00039   error = NULL;
00040   fromFile = toFile = NULL;
00041   fromFileName = toFileName = NULL;
00042   fromIndex = toIndex = NULL;
00043   cRecording Recording(FromFileName);
00044   isPesRecording = Recording.IsPesRecording();
00045   if (fromMarks.Load(FromFileName, Recording.FramesPerSecond(), isPesRecording) && fromMarks.Count()) {
00046      fromFileName = new cFileName(FromFileName, false, true, isPesRecording);
00047      toFileName = new cFileName(ToFileName, true, true, isPesRecording);
00048      fromIndex = new cIndexFile(FromFileName, false, isPesRecording);
00049      toIndex = new cIndexFile(ToFileName, true, isPesRecording);
00050      toMarks.Load(ToFileName, Recording.FramesPerSecond(), isPesRecording); // doesn't actually load marks, just sets the file name
00051      maxVideoFileSize = MEGABYTE(Setup.MaxVideoFileSize);
00052      if (isPesRecording && maxVideoFileSize > MEGABYTE(MAXVIDEOFILESIZEPES))
00053         maxVideoFileSize = MEGABYTE(MAXVIDEOFILESIZEPES);
00054      Start();
00055      }
00056   else
00057      esyslog("no editing marks found for %s", FromFileName);
00058 }
00059 
00060 cCuttingThread::~cCuttingThread()
00061 {
00062   Cancel(3);
00063   delete fromFileName;
00064   delete toFileName;
00065   delete fromIndex;
00066   delete toIndex;
00067 }
00068 
00069 void cCuttingThread::Action(void)
00070 {
00071   cMark *Mark = fromMarks.First();
00072   if (Mark) {
00073      SetPriority(19);
00074      SetIOPriority(7);
00075      fromFile = fromFileName->Open();
00076      toFile = toFileName->Open();
00077      if (!fromFile || !toFile)
00078         return;
00079      fromFile->SetReadAhead(MEGABYTE(20));
00080      int Index = Mark->Position();
00081      Mark = fromMarks.Next(Mark);
00082      off_t FileSize = 0;
00083      int CurrentFileNumber = 0;
00084      bool SkipThisSourceFile = false;
00085      int LastIFrame = 0;
00086      toMarks.Add(0);
00087      toMarks.Save();
00088      uchar buffer[MAXFRAMESIZE];
00089      bool LastMark = false;
00090      bool cutIn = true;
00091      while (Running()) {
00092            uint16_t FileNumber;
00093            off_t FileOffset;
00094            int Length;
00095            bool Independent;
00096 
00097            // Make sure there is enough disk space:
00098 
00099            AssertFreeDiskSpace(-1);
00100 
00101            // Read one frame:
00102 
00103            if (!fromIndex->Get(Index++, &FileNumber, &FileOffset, &Independent, &Length)) {
00104               // Error, unless we're past last cut-in and there's no cut-out
00105               if (Mark || LastMark)
00106                  error = "index";
00107               break;
00108               }
00109 
00110            if (FileNumber != CurrentFileNumber) {
00111               fromFile = fromFileName->SetOffset(FileNumber, FileOffset);
00112               if (fromFile)
00113                  fromFile->SetReadAhead(MEGABYTE(20));
00114               CurrentFileNumber = FileNumber;
00115               if (SkipThisSourceFile) {
00116                  // At end of fast forward: Always skip to next file
00117                  toFile = toFileName->NextFile();
00118                  if (!toFile) {
00119                     error = "toFile 4";
00120                     break;
00121                     }
00122                  FileSize = 0;
00123                  SkipThisSourceFile = false;
00124                  }                 
00125               
00126 
00127               if (Setup.HardLinkCutter && FileOffset == 0) {
00128                  // We are at the beginning of a new source file.
00129                  // Do we need to copy the whole file?
00130 
00131                  // if !Mark && LastMark, then we're past the last cut-out and continue to next I-frame
00132                  // if !Mark && !LastMark, then there's just a cut-in, but no cut-out
00133                  // if Mark, then we're between a cut-in and a cut-out
00134                  
00135                  uint16_t MarkFileNumber;
00136                  off_t MarkFileOffset;
00137                  // Get file number of next cut mark
00138                  if (!Mark && !LastMark
00139                      || Mark
00140                         && fromIndex->Get(Mark->Position(), &MarkFileNumber, &MarkFileOffset)
00141                         && (MarkFileNumber != CurrentFileNumber)) {
00142                     // The current source file will be copied completely.
00143                     // Start new output file unless we did that already
00144                     if (FileSize != 0) {
00145                        toFile = toFileName->NextFile();
00146                        if (!toFile) {
00147                           error = "toFile 3";
00148                           break;
00149                           }
00150                        FileSize = 0;
00151                        }
00152 
00153                     // Safety check that file has zero size
00154                     struct stat buf;
00155                     if (stat(toFileName->Name(), &buf) == 0) {
00156                        if (buf.st_size != 0) {
00157                           esyslog("cCuttingThread: File %s exists and has nonzero size", toFileName->Name());
00158                           error = "nonzero file exist";
00159                           break;
00160                           }
00161                        }
00162                     else if (errno != ENOENT) {
00163                        esyslog("cCuttingThread: stat failed on %s", toFileName->Name());
00164                        error = "stat";
00165                        break;
00166                        }
00167 
00168                     // Clean the existing 0-byte file
00169                     toFileName->Close();
00170                     cString ActualToFileName(ReadLink(toFileName->Name()), true);
00171                     unlink(ActualToFileName);
00172                     unlink(toFileName->Name());
00173 
00174                     // Try to create a hard link
00175                     if (HardLinkVideoFile(fromFileName->Name(), toFileName->Name())) {
00176                        // Success. Skip all data transfer for this file
00177                        SkipThisSourceFile = true;
00178                        cutIn = false;
00179                        toFile = NULL; // was deleted by toFileName->Close()
00180                        } 
00181                     else {
00182                        // Fallback: Re-open the file if necessary
00183                        toFile = toFileName->Open();
00184                        }
00185                     }
00186                  } 
00187               }
00188 
00189            if (!SkipThisSourceFile) {
00190               if (fromFile) {
00191                  int len = ReadFrame(fromFile, buffer,  Length, sizeof(buffer));
00192                  if (len < 0) {
00193                     error = "ReadFrame";
00194                     break;
00195                     }
00196                  if (len != Length) {
00197                     CurrentFileNumber = 0; // this re-syncs in case the frame was larger than the buffer
00198                     Length = len;
00199                     }
00200                  }
00201               else {
00202                  error = "fromFile";
00203                  break;
00204                  }
00205               }
00206            // Write one frame:
00207 
00208            if (Independent) { // every file shall start with an independent frame
00209               if (LastMark) // edited version shall end before next I-frame
00210                  break;
00211               if (!SkipThisSourceFile && FileSize > toFileName->MaxFileSize()) {
00212                  toFile = toFileName->NextFile();
00213                  if (!toFile) {
00214                     error = "toFile 1";
00215                     break;
00216                     }
00217                  FileSize = 0;
00218                  }
00219               LastIFrame = 0;
00220 
00221               if (!SkipThisSourceFile && cutIn) {
00222                  if (isPesRecording)
00223                     cRemux::SetBrokenLink(buffer, Length);
00224                  else
00225                     TsSetTeiOnBrokenPackets(buffer, Length);
00226                  cutIn = false;
00227                  }
00228               }
00229            if (!SkipThisSourceFile && toFile->Write(buffer, Length) < 0) {
00230               error = "safe_write";
00231               break;
00232               }
00233            if (!toIndex->Write(Independent, toFileName->Number(), FileSize)) {
00234               error = "toIndex";
00235               break;
00236               }
00237            FileSize += Length;
00238            if (!LastIFrame)
00239               LastIFrame = toIndex->Last();
00240 
00241            // Check editing marks:
00242 
00243            if (Mark && Index >= Mark->Position()) {
00244               Mark = fromMarks.Next(Mark);
00245               toMarks.Add(LastIFrame);
00246               if (Mark)
00247                  toMarks.Add(toIndex->Last() + 1);
00248               toMarks.Save();
00249               if (Mark) {
00250                  Index = Mark->Position();
00251                  Mark = fromMarks.Next(Mark);
00252                  CurrentFileNumber = 0; // triggers SetOffset before reading next frame
00253                  cutIn = true;
00254                  if (Setup.SplitEditedFiles) {
00255                     toFile = toFileName->NextFile();
00256                     if (!toFile) {
00257                        error = "toFile 2";
00258                        break;
00259                        }
00260                     FileSize = 0;
00261                     }
00262                  }
00263               else
00264                  LastMark = true; // After last cut-out: Write on until next I-frame, then exit
00265               }
00266            }
00267      Recordings.TouchUpdate();
00268      }
00269   else
00270      esyslog("no editing marks found!");
00271 }
00272 
00273 // --- cCutter ---------------------------------------------------------------
00274 
00275 cMutex cCutter::mutex;
00276 cString cCutter::originalVersionName;
00277 cString cCutter::editedVersionName;
00278 cCuttingThread *cCutter::cuttingThread = NULL;
00279 bool cCutter::error = false;
00280 bool cCutter::ended = false;
00281 
00282 bool cCutter::Start(const char *FileName, const char *TargetFileName, bool Overwrite)
00283 {
00284   cMutexLock MutexLock(&mutex);
00285   if (!cuttingThread) {
00286      error = false;
00287      ended = false;
00288      originalVersionName = FileName;
00289      cRecording Recording(FileName);
00290 
00291      cMarks FromMarks;
00292      FromMarks.Load(FileName, Recording.FramesPerSecond(), Recording.IsPesRecording());
00293      if (cMark *First = FromMarks.First())
00294         Recording.SetStartTime(Recording.Start() + (int(First->Position() / Recording.FramesPerSecond() + 30) / 60) * 60);
00295 
00296      cString evn = (TargetFileName && *TargetFileName) ? Recording.UpdateFileName(TargetFileName) : Recording.PrefixFileName('%');
00297      if (!Overwrite && *evn && (access(*evn, F_OK) == 0) && !Interface->Confirm(tr("File already exists - overwrite?"))) {
00298         do {
00299            evn = PrefixVideoFileName(*evn, '%');
00300         } while (*evn && (access(*evn, F_OK) == 0));
00301         }
00302      if (*evn && RemoveVideoFile(*evn) && MakeDirs(*evn, true)) {
00303         // XXX this can be removed once RenameVideoFile() follows symlinks (see videodir.c)
00304         // remove a possible deleted recording with the same name to avoid symlink mixups:
00305         char *s = strdup(*evn);
00306         char *e = strrchr(s, '.');
00307         if (e) {
00308            if (strcmp(e, ".rec") == 0) {
00309               strcpy(e, ".del");
00310               RemoveVideoFile(s);
00311               }
00312            }
00313         free(s);
00314         // XXX
00315         editedVersionName = evn;
00316         Recording.WriteInfo();
00317         Recordings.AddByName(editedVersionName, false);
00318         cuttingThread = new cCuttingThread(FileName, editedVersionName);
00319         return true;
00320         }
00321      }
00322   return false;
00323 }
00324 
00325 void cCutter::Stop(void)
00326 {
00327   cMutexLock MutexLock(&mutex);
00328   bool Interrupted = cuttingThread && cuttingThread->Active();
00329   const char *Error = cuttingThread ? cuttingThread->Error() : NULL;
00330   delete cuttingThread;
00331   cuttingThread = NULL;
00332   if ((Interrupted || Error) && *editedVersionName) {
00333      if (Interrupted)
00334         isyslog("editing process has been interrupted");
00335      if (Error)
00336         esyslog("ERROR: '%s' during editing process", Error);
00337      if (cReplayControl::NowReplaying() && strcmp(cReplayControl::NowReplaying(), editedVersionName) == 0)
00338         cControl::Shutdown();
00339      RemoveVideoFile(editedVersionName);
00340      Recordings.DelByName(editedVersionName);
00341      }
00342 }
00343 
00344 bool cCutter::Active(const char *FileName)
00345 {
00346   cMutexLock MutexLock(&mutex);
00347   if (cuttingThread) {
00348      if (cuttingThread->Active())
00349         return !FileName || strcmp(FileName, originalVersionName) == 0 || strcmp(FileName, editedVersionName) == 0;
00350      error = cuttingThread->Error();
00351      Stop();
00352      if (!error)
00353         cRecordingUserCommand::InvokeCommand(RUC_EDITEDRECORDING, editedVersionName);
00354      originalVersionName = NULL;
00355      editedVersionName = NULL;
00356      ended = true;
00357      }
00358   return false;
00359 }
00360 
00361 bool cCutter::Error(void)
00362 {
00363   cMutexLock MutexLock(&mutex);
00364   bool result = error;
00365   error = false;
00366   return result;
00367 }
00368 
00369 bool cCutter::Ended(void)
00370 {
00371   cMutexLock MutexLock(&mutex);
00372   bool result = ended;
00373   ended = false;
00374   return result;
00375 }
00376 
00377 #define CUTTINGCHECKINTERVAL 500 // ms between checks for the active cutting process
00378 
00379 bool CutRecording(const char *FileName)
00380 {
00381   if (DirectoryOk(FileName)) {
00382      cRecording Recording(FileName);
00383      if (Recording.Name()) {
00384         cMarks Marks;
00385         if (Marks.Load(FileName, Recording.FramesPerSecond(), Recording.IsPesRecording()) && Marks.Count()) {
00386            if (cCutter::Start(FileName)) {
00387               while (cCutter::Active())
00388                     cCondWait::SleepMs(CUTTINGCHECKINTERVAL);
00389               return true;
00390               }
00391            else
00392               fprintf(stderr, "can't start editing process\n");
00393            }
00394         else
00395            fprintf(stderr, "'%s' has no editing marks\n", FileName);
00396         }
00397      else
00398         fprintf(stderr, "'%s' is not a recording\n", FileName);
00399      }
00400   else
00401      fprintf(stderr, "'%s' is not a directory\n", FileName);
00402   return false;
00403 }