vdr/cutter.c
Klaus Schmidinger fa6845328c Version 1.7.31
VDR developer version 1.7.31 is now available at

       ftp://ftp.tvdr.de/vdr/Developer/vdr-1.7.31.tar.bz2

A 'diff' against the previous version is available at

       ftp://ftp.tvdr.de/vdr/Developer/vdr-1.7.30-1.7.31.diff

MD5 checksums:

a3edd18a352465dd26c97c1990f7bcfd  vdr-1.7.31.tar.bz2
32ff98697d1b383478a6e1932e4afc9c  vdr-1.7.30-1.7.31.diff

WARNING:
========

This is a developer version. Even though I use it in my productive
environment. I strongly recommend that you only use it under controlled
conditions and for testing and debugging.

The default skin "LCARS" displays the signal strengths and qualities of
all devices in its main menu. For devices that have an stb0899 frontend chip
(like the TT-budget S2-3200) retrieving this information from the driver is
rather slow, which results in a sluggish response to user input in the main
menu. To speed this up you may want to apply the patches from

   ftp://ftp.tvdr.de/vdr/Developer/Driver-Patches

to the LinuxDVB driver source.

The changes since version 1.7.30:

- If regenerating an index file fails and no data is written to the file, VDR now
   reports this error and removes the empty index file.
- The setup parameter "Recording/Instant rec. time (min)" can now be set to '0',
   which means to record only the currently running event (based on a patch from Matti
   Lehtimäki).
- Decreased the ring buffer put/get trigger sizes from 1/3 to 1/10.
- The script given to VDR with the '-r' option is now also called whenever a
   recording is deleted (thanks to Alexander Wenzel).
- Improved detecting frames in MPEG 4 video (reported by Andrey Pridvorov).
- cPatPmtParser::ParsePmt() now also recognizes stream type 0x81 as "AC3", so that
   recordings that have been converted from the old PES format to TS can be played
   (suggested by Jens Vogel).
- Fixed a leftover frame counter in the LCARS skin's replay display after jumping to
   an editing mark and resuming replay.
- The new class cIoThrottle is used to allow I/O intense threads to temporarily
   suspend their activities in case buffers run full (suggested by Torsten Lang).
   Currently the cutter thread is suspended if the TS or Recorder buffer use more
   than 50% of their capacity. Plugin authors may want to participate in this
   mechanism if they use intense background I/O.
- Increased the size of the TS buffer to 5MB and that of the Recorder buffer to
   20MB to better handle HD recordings (suggested by Torsten Lang).
- Moved cleaning up the EPG data and writing the epg.data file into a separate
   thread to avoid sluggish response to user input on slow systems (based on a patch from
   Sören Moch).
- Fixed sorting folders before recordings in case of UTF-8 (thanks to Sören Moch).
- Reactivated stripping control characters from EPG texts and adapted it to UTF-8.
- Added missing decrementing of 'len' in libsi/si.c's String::decodeText() functions.
- When checking whether a video directory is empty, file names that start with a
   dot ('.') are no longer automatically ignored and implicitly removed if the directory
   contains no other files. Instead, RemoveEmptyDirectories() now has an additional
   parameter that can be given a list of files that shall be ignored when considering
   whether a directory is empty. This allows users to continue to use files such as
   ".keep" to prevent a directory from being deleted when it is empty. Currently the
   only file name that is ignored is ".sort".
2012-09-30 16:05:19 +02:00

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;
}