mirror of
https://github.com/VDR4Arch/vdr.git
synced 2023-10-10 13:36:52 +02:00
366 lines
12 KiB
C
366 lines
12 KiB
C
/*
|
|
* 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.14 2012/09/20 09:12:47 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")
|
|
{
|
|
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) {
|
|
SetPriority(19);
|
|
SetIOPriority(7);
|
|
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;
|
|
}
|