/* * cutter.c: The video cutting facilities * * See the main source file 'vdr.c' for copyright information and * how to reach the author. * * $Id: cutter.c 2.15 2012/10/04 12:19:37 kls Exp $ */ #include "cutter.h" #include "menu.h" #include "recording.h" #include "remux.h" #include "videodir.h" // --- cCuttingThread -------------------------------------------------------- class cCuttingThread : public cThread { private: const char *error; bool isPesRecording; cUnbufferedFile *fromFile, *toFile; cFileName *fromFileName, *toFileName; cIndexFile *fromIndex, *toIndex; cMarks fromMarks, toMarks; off_t maxVideoFileSize; protected: virtual void Action(void); public: cCuttingThread(const char *FromFileName, const char *ToFileName); virtual ~cCuttingThread(); const char *Error(void) { return error; } }; cCuttingThread::cCuttingThread(const char *FromFileName, const char *ToFileName) :cThread("video cutting", true) { error = NULL; fromFile = toFile = NULL; fromFileName = toFileName = NULL; fromIndex = toIndex = NULL; cRecording Recording(FromFileName); isPesRecording = Recording.IsPesRecording(); if (fromMarks.Load(FromFileName, Recording.FramesPerSecond(), isPesRecording) && fromMarks.Count()) { fromFileName = new cFileName(FromFileName, false, true, isPesRecording); toFileName = new cFileName(ToFileName, true, true, isPesRecording); fromIndex = new cIndexFile(FromFileName, false, isPesRecording); toIndex = new cIndexFile(ToFileName, true, isPesRecording); toMarks.Load(ToFileName, Recording.FramesPerSecond(), isPesRecording); // doesn't actually load marks, just sets the file name maxVideoFileSize = MEGABYTE(Setup.MaxVideoFileSize); if (isPesRecording && maxVideoFileSize > MEGABYTE(MAXVIDEOFILESIZEPES)) maxVideoFileSize = MEGABYTE(MAXVIDEOFILESIZEPES); Start(); } else esyslog("no editing marks found for %s", FromFileName); } cCuttingThread::~cCuttingThread() { Cancel(3); delete fromFileName; delete toFileName; delete fromIndex; delete toIndex; } void cCuttingThread::Action(void) { cMark *Mark = fromMarks.First(); if (Mark) { fromFile = fromFileName->Open(); toFile = toFileName->Open(); if (!fromFile || !toFile) return; fromFile->SetReadAhead(MEGABYTE(20)); int Index = Mark->Position(); Mark = fromMarks.Next(Mark); off_t FileSize = 0; int CurrentFileNumber = 0; int LastIFrame = 0; toMarks.Add(0); toMarks.Save(); uchar buffer[MAXFRAMESIZE], buffer2[MAXFRAMESIZE]; int Length2; bool CheckForSeamlessStream = false; bool LastMark = false; bool cutIn = true; bool suspensionLogged = false; while (Running()) { uint16_t FileNumber; off_t FileOffset; int Length; bool Independent; // Suspend cutting if we have severe throughput problems: if (cIoThrottle::Engaged()) { if (!suspensionLogged) { dsyslog("suspending cutter thread"); suspensionLogged = true; } cCondWait::SleepMs(100); continue; } else if (suspensionLogged) { dsyslog("resuming cutter thread"); suspensionLogged = false; } // Make sure there is enough disk space: AssertFreeDiskSpace(-1); // Read one frame: if (fromIndex->Get(Index++, &FileNumber, &FileOffset, &Independent, &Length)) { if (FileNumber != CurrentFileNumber) { fromFile = fromFileName->SetOffset(FileNumber, FileOffset); if (fromFile) fromFile->SetReadAhead(MEGABYTE(20)); CurrentFileNumber = FileNumber; } if (fromFile) { int len = ReadFrame(fromFile, buffer, Length, sizeof(buffer)); if (len < 0) { error = "ReadFrame"; break; } if (len != Length) { CurrentFileNumber = 0; // this re-syncs in case the frame was larger than the buffer Length = len; } } else { error = "fromFile"; break; } } else { // Error, unless we're past the last cut-in and there's no cut-out if (Mark || LastMark) error = "index"; break; } // Write one frame: if (Independent) { // every file shall start with an independent frame if (LastMark) // edited version shall end before next I-frame break; if (FileSize > maxVideoFileSize) { toFile = toFileName->NextFile(); if (!toFile) { error = "toFile 1"; break; } FileSize = 0; } LastIFrame = 0; // Compare the current frame with the previously stored one, to see if this is a seamlessly merged recording of the same stream: if (CheckForSeamlessStream) { if (Length == Length2) { int diffs = 0; for (int i = 0; i < Length; i++) { if (buffer[i] != buffer2[i]) { if (diffs++ > 10) break; } } if (diffs < 10) // the continuity counters of the PAT/PMT packets may differ cutIn = false; // it's apparently a seamless stream, so no need for "broken" handling } CheckForSeamlessStream = false; } if (cutIn) { if (isPesRecording) cRemux::SetBrokenLink(buffer, Length); else TsSetTeiOnBrokenPackets(buffer, Length); cutIn = false; } } if (toFile->Write(buffer, Length) < 0) { error = "safe_write"; break; } if (!toIndex->Write(Independent, toFileName->Number(), FileSize)) { error = "toIndex"; break; } FileSize += Length; if (!LastIFrame) LastIFrame = toIndex->Last(); // Check editing marks: if (Mark && Index >= Mark->Position()) { Mark = fromMarks.Next(Mark); toMarks.Add(LastIFrame); if (Mark) toMarks.Add(toIndex->Last() + 1); toMarks.Save(); if (Mark) { // Read the next frame, for later comparison with the first frame at this mark: if (fromIndex->Get(Index, &FileNumber, &FileOffset, &Independent, &Length2)) { if (FileNumber != CurrentFileNumber) fromFile = fromFileName->SetOffset(FileNumber, FileOffset); if (fromFile) { int len = ReadFrame(fromFile, buffer2, Length2, sizeof(buffer2)); if (len >= 0 && len == Length2) CheckForSeamlessStream = true; } } Index = Mark->Position(); Mark = fromMarks.Next(Mark); CurrentFileNumber = 0; // triggers SetOffset before reading next frame cutIn = true; if (Setup.SplitEditedFiles) { toFile = toFileName->NextFile(); if (!toFile) { error = "toFile 2"; break; } FileSize = 0; } } else LastMark = true; } } Recordings.TouchUpdate(); } else esyslog("no editing marks found!"); } // --- cCutter --------------------------------------------------------------- cMutex cCutter::mutex; cString cCutter::originalVersionName; cString cCutter::editedVersionName; cCuttingThread *cCutter::cuttingThread = NULL; bool cCutter::error = false; bool cCutter::ended = false; bool cCutter::Start(const char *FileName) { cMutexLock MutexLock(&mutex); if (!cuttingThread) { error = false; ended = false; originalVersionName = FileName; cRecording Recording(FileName); cMarks FromMarks; FromMarks.Load(FileName, Recording.FramesPerSecond(), Recording.IsPesRecording()); if (cMark *First = FromMarks.First()) Recording.SetStartTime(Recording.Start() + (int(First->Position() / Recording.FramesPerSecond() + 30) / 60) * 60); const char *evn = Recording.PrefixFileName('%'); if (evn && RemoveVideoFile(evn) && MakeDirs(evn, true)) { // XXX this can be removed once RenameVideoFile() follows symlinks (see videodir.c) // remove a possible deleted recording with the same name to avoid symlink mixups: char *s = strdup(evn); char *e = strrchr(s, '.'); if (e) { if (strcmp(e, ".rec") == 0) { strcpy(e, ".del"); RemoveVideoFile(s); } } free(s); // XXX editedVersionName = evn; Recording.WriteInfo(); Recordings.AddByName(editedVersionName, false); cuttingThread = new cCuttingThread(FileName, editedVersionName); return true; } } return false; } void cCutter::Stop(void) { cMutexLock MutexLock(&mutex); bool Interrupted = cuttingThread && cuttingThread->Active(); const char *Error = cuttingThread ? cuttingThread->Error() : NULL; delete cuttingThread; cuttingThread = NULL; if ((Interrupted || Error) && *editedVersionName) { if (Interrupted) isyslog("editing process has been interrupted"); if (Error) esyslog("ERROR: '%s' during editing process", Error); if (cReplayControl::NowReplaying() && strcmp(cReplayControl::NowReplaying(), editedVersionName) == 0) cControl::Shutdown(); RemoveVideoFile(editedVersionName); Recordings.DelByName(editedVersionName); } } bool cCutter::Active(const char *FileName) { cMutexLock MutexLock(&mutex); if (cuttingThread) { if (cuttingThread->Active()) return !FileName || strcmp(FileName, originalVersionName) == 0 || strcmp(FileName, editedVersionName) == 0; error = cuttingThread->Error(); Stop(); if (!error) cRecordingUserCommand::InvokeCommand(RUC_EDITEDRECORDING, editedVersionName, originalVersionName); originalVersionName = NULL; editedVersionName = NULL; ended = true; } return false; } bool cCutter::Error(void) { cMutexLock MutexLock(&mutex); bool result = error; error = false; return result; } bool cCutter::Ended(void) { cMutexLock MutexLock(&mutex); bool result = ended; ended = false; return result; } #define CUTTINGCHECKINTERVAL 500 // ms between checks for the active cutting process bool CutRecording(const char *FileName) { if (DirectoryOk(FileName)) { cRecording Recording(FileName); if (Recording.Name()) { cMarks Marks; if (Marks.Load(FileName, Recording.FramesPerSecond(), Recording.IsPesRecording()) && Marks.Count()) { if (cCutter::Start(FileName)) { while (cCutter::Active()) cCondWait::SleepMs(CUTTINGCHECKINTERVAL); return true; } else fprintf(stderr, "can't start editing process\n"); } else fprintf(stderr, "'%s' has no editing marks\n", FileName); } else fprintf(stderr, "'%s' is not a recording\n", FileName); } else fprintf(stderr, "'%s' is not a directory\n", FileName); return false; }