diff -Nru vdr-1.6.0/config.c vdr-1.6.0-HardLinkCutter/config.c --- vdr-1.6.0/config.c 2008-04-12 22:31:05.000000000 +0200 +++ vdr-1.6.0-HardLinkCutter/config.c 2008-04-12 22:31:59.000000000 +0200 @@ -288,7 +288,9 @@ FontSmlSize = 18; FontFixSize = 20; MaxVideoFileSize = MAXVIDEOFILESIZE; + MaxRecordingSize = DEFAULTRECORDINGSIZE; SplitEditedFiles = 0; + HardLinkCutter = 1; MinEventTimeout = 30; MinUserInactivity = 300; NextWakeupTime = 0; @@ -474,7 +476,9 @@ else if (!strcasecmp(Name, "FontSmlSize")) FontSmlSize = atoi(Value); else if (!strcasecmp(Name, "FontFixSize")) FontFixSize = atoi(Value); else if (!strcasecmp(Name, "MaxVideoFileSize")) MaxVideoFileSize = atoi(Value); + else if (!strcasecmp(Name, "MaxRecordingSize")) MaxRecordingSize = atoi(Value); else if (!strcasecmp(Name, "SplitEditedFiles")) SplitEditedFiles = atoi(Value); + else if (!strcasecmp(Name, "HardLinkCutter")) HardLinkCutter = atoi(Value); else if (!strcasecmp(Name, "MinEventTimeout")) MinEventTimeout = atoi(Value); else if (!strcasecmp(Name, "MinUserInactivity")) MinUserInactivity = atoi(Value); else if (!strcasecmp(Name, "NextWakeupTime")) NextWakeupTime = atoi(Value); @@ -594,7 +598,9 @@ Store("FontSmlSize", FontSmlSize); Store("FontFixSize", FontFixSize); Store("MaxVideoFileSize", MaxVideoFileSize); + Store("MaxRecordingSize", MaxRecordingSize); Store("SplitEditedFiles", SplitEditedFiles); + Store("HardLinkCutter", HardLinkCutter); Store("MinEventTimeout", MinEventTimeout); Store("MinUserInactivity", MinUserInactivity); Store("NextWakeupTime", NextWakeupTime); diff -Nru vdr-1.6.0/config.h vdr-1.6.0-HardLinkCutter/config.h --- vdr-1.6.0/config.h 2008-04-12 22:31:05.000000000 +0200 +++ vdr-1.6.0-HardLinkCutter/config.h 2008-04-12 22:31:59.000000000 +0200 @@ -273,7 +273,9 @@ int FontSmlSize; int FontFixSize; int MaxVideoFileSize; + int MaxRecordingSize; int SplitEditedFiles; + int HardLinkCutter; int MinEventTimeout, MinUserInactivity; time_t NextWakeupTime; int MultiSpeedMode; diff -Nru vdr-1.6.0/cutter.c vdr-1.6.0-HardLinkCutter/cutter.c --- vdr-1.6.0/cutter.c 2008-01-13 13:22:21.000000000 +0100 +++ vdr-1.6.0-HardLinkCutter/cutter.c 2008-04-12 22:31:59.000000000 +0200 @@ -71,6 +71,7 @@ Mark = fromMarks.Next(Mark); int FileSize = 0; int CurrentFileNumber = 0; + bool SkipThisSourceFile = false; int LastIFrame = 0; toMarks.Add(0); toMarks.Save(); @@ -88,12 +89,92 @@ // Read one frame: - if (fromIndex->Get(Index++, &FileNumber, &FileOffset, &PictureType, &Length)) { - if (FileNumber != CurrentFileNumber) { - fromFile = fromFileName->SetOffset(FileNumber, FileOffset); - fromFile->SetReadAhead(MEGABYTE(20)); - CurrentFileNumber = FileNumber; - } + if (!fromIndex->Get(Index++, &FileNumber, &FileOffset, &PictureType, &Length)) { + // Error, unless we're past last cut-in and there's no cut-out + if (Mark || LastMark) + error = "index"; + break; + } + + if (FileNumber != CurrentFileNumber) { + fromFile = fromFileName->SetOffset(FileNumber, FileOffset); + fromFile->SetReadAhead(MEGABYTE(20)); + CurrentFileNumber = FileNumber; + if (SkipThisSourceFile) { + // At end of fast forward: Always skip to next file + toFile = toFileName->NextFile(); + if (!toFile) { + error = "toFile 4"; + break; + } + FileSize = 0; + SkipThisSourceFile = false; + } + + + if (Setup.HardLinkCutter && FileOffset == 0) { + // We are at the beginning of a new source file. + // Do we need to copy the whole file? + + // if !Mark && LastMark, then we're past the last cut-out and continue to next I-frame + // if !Mark && !LastMark, then there's just a cut-in, but no cut-out + // if Mark, then we're between a cut-in and a cut-out + + uchar MarkFileNumber; + int MarkFileOffset; + // Get file number of next cut mark + if (!Mark && !LastMark + || Mark + && fromIndex->Get(Mark->position, &MarkFileNumber, &MarkFileOffset) + && (MarkFileNumber != CurrentFileNumber)) { + // The current source file will be copied completely. + // Start new output file unless we did that already + if (FileSize != 0) { + toFile = toFileName->NextFile(); + if (!toFile) { + error = "toFile 3"; + break; + } + FileSize = 0; + } + + // Safety check that file has zero size + struct stat buf; + if (stat(toFileName->Name(), &buf) == 0) { + if (buf.st_size != 0) { + esyslog("cCuttingThread: File %s exists and has nonzero size", toFileName->Name()); + error = "nonzero file exist"; + break; + } + } + else if (errno != ENOENT) { + esyslog("cCuttingThread: stat failed on %s", toFileName->Name()); + error = "stat"; + break; + } + + // Clean the existing 0-byte file + toFileName->Close(); + cString ActualToFileName(ReadLink(toFileName->Name()), true); + unlink(ActualToFileName); + unlink(toFileName->Name()); + + // Try to create a hard link + if (HardLinkVideoFile(fromFileName->Name(), toFileName->Name())) { + // Success. Skip all data transfer for this file + SkipThisSourceFile = true; + cutIn = false; + toFile = NULL; // was deleted by toFileName->Close() + } + else { + // Fallback: Re-open the file if necessary + toFile = toFileName->Open(); + } + } + } + } + + if (!SkipThisSourceFile) { if (fromFile) { int len = ReadFrame(fromFile, buffer, Length, sizeof(buffer)); if (len < 0) { @@ -110,19 +191,13 @@ 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 (PictureType == I_FRAME) { // every file shall start with an I_FRAME if (LastMark) // edited version shall end before next I-frame break; - if (FileSize > MEGABYTE(Setup.MaxVideoFileSize)) { + if (!SkipThisSourceFile && FileSize > toFileName->MaxFileSize()) { toFile = toFileName->NextFile(); if (!toFile) { error = "toFile 1"; @@ -132,12 +207,12 @@ } LastIFrame = 0; - if (cutIn) { + if (!SkipThisSourceFile && cutIn) { cRemux::SetBrokenLink(buffer, Length); cutIn = false; } } - if (toFile->Write(buffer, Length) < 0) { + if (!SkipThisSourceFile && toFile->Write(buffer, Length) < 0) { error = "safe_write"; break; } @@ -172,7 +247,7 @@ } } else - LastMark = true; + LastMark = true; // After last cut-out: Write on until next I-frame, then exit } } Recordings.TouchUpdate(); diff -Nru vdr-1.6.0/menu.c vdr-1.6.0-HardLinkCutter/menu.c --- vdr-1.6.0/menu.c 2008-04-12 22:31:05.000000000 +0200 +++ vdr-1.6.0-HardLinkCutter/menu.c 2008-04-12 22:31:59.000000000 +0200 @@ -3128,7 +3128,9 @@ Add(new cMenuEditStrItem( tr("Setup.Recording$Name instant recording"), data.NameInstantRecord, sizeof(data.NameInstantRecord))); Add(new cMenuEditIntItem( tr("Setup.Recording$Instant rec. time (min)"), &data.InstantRecordTime, 1, MAXINSTANTRECTIME)); Add(new cMenuEditIntItem( tr("Setup.Recording$Max. video file size (MB)"), &data.MaxVideoFileSize, MINVIDEOFILESIZE, MAXVIDEOFILESIZE)); + Add(new cMenuEditIntItem( tr("Setup.Recording$Max. recording size (GB)"), &data.MaxRecordingSize, MINRECORDINGSIZE, MAXRECORDINGSIZE)); Add(new cMenuEditBoolItem(tr("Setup.Recording$Split edited files"), &data.SplitEditedFiles)); + Add(new cMenuEditBoolItem(tr("Setup.Recording$Hard Link Cutter"), &data.HardLinkCutter)); Add(new cMenuEditBoolItem(tr("Setup.Recording$Show date"), &data.ShowRecDate)); Add(new cMenuEditBoolItem(tr("Setup.Recording$Show time"), &data.ShowRecTime)); Add(new cMenuEditBoolItem(tr("Setup.Recording$Show length"), &data.ShowRecLength)); diff -Nru vdr-1.6.0/po/fr_FR.po vdr-1.6.0-HardLinkCutter/po/fr_FR.po --- vdr-1.6.0/po/fr_FR.po 2008-04-12 22:31:05.000000000 +0200 +++ vdr-1.6.0-HardLinkCutter/po/fr_FR.po 2008-04-12 22:31:59.000000000 +0200 @@ -770,9 +770,15 @@ msgid "Setup.Recording$Max. video file size (MB)" msgstr "Taille maxi des fichiers (Mo)" +msgid "Setup.Recording$Max. recording size (GB)" +msgstr "Taille maxi des enregistrements (Go)" + msgid "Setup.Recording$Split edited files" msgstr "Séparer les séquences éditées" +msgid "Setup.Recording$Hard Link Cutter" +msgstr "Hard Link Cutter" + msgid "Replay" msgstr "Lecture" diff -Nru vdr-1.6.0/README-HLCUTTER vdr-1.6.0-HardLinkCutter/README-HLCUTTER --- vdr-1.6.0/README-HLCUTTER 1970-01-01 01:00:00.000000000 +0100 +++ vdr-1.6.0-HardLinkCutter/README-HLCUTTER 2008-04-12 22:31:59.000000000 +0200 @@ -0,0 +1,116 @@ + VDR-HLCUTTER README + + +Written by: Udo Richter +Available at: http://www.udo-richter.de/vdr/patches.html#hlcutter + http://www.udo-richter.de/vdr/patches.en.html#hlcutter +Contact: udo_richter@gmx.de + + + +About +----- + +The hard link cutter patch changes the recording editing algorithms of VDR to +use filesystem hard links to 'copy' recording files whenever possible to speed +up editing recordings noticeably. + +The patch has matured to be quite stable, at least I'm using it without issues. +Nevertheless the patch is still in development and should be used with caution. +The patch is EXPERIMENTAL for multiple /videoxx folders. The safety checks +should prevent data loss, but you should always carefully check the results. + +While editing a recording, the patch searches for any 00x.vdr files that dont +contain editing marks and would normally be copied 1:1 unmodified to the edited +recording. In this case the current target 00x.vdr file will be aborted, and +the cutter process attempts to duplicate the source file as a hard link, so +that both files share the same disk space. If this succeeds, the editing +process fast-forwards through the duplicated file and continues normally +beginning with the next source file. If hard linking fails, the cutter process +continues with plain old copying. (but does not take up the aborted last file.) + +After editing, the un-edited recording can be deleted as usual, the hard linked +copies will continue to exist as the only remaining copy. + +To be effective, the default 'Max. video file size (MB)' should be lowered. +The patch lowers the smallest possible file size to 1mb. Since VDR only +supports up to 255 files, this would limit the recording size to 255Mb or +10 minutes, in other words: This setting is insane! + +To make sure that the 255 file limit will not be reached, the patch also +introduces "Max. recording size (GB)" with a default of 100Gb (66 hours), and +increases the file size to 2000Mb early enough, so that 100Gb-recordings will +fit into the 255 files. + +Picking the right parameters can be tricky. The smaller the file size, the +faster the editing process works. However, with a small file size, long +recordings will fall back to 2000Mb files soon, that are slow on editing again. + +Here are some examples: + +Max file size: 100Gb 100Gb 100Gb 100Gb 100Gb 100Gb 100Gb +Max recording size: 1Mb 10Mb 20Mb 30Mb 40Mb 50Mb 100Mb + +Small files: 1-203 1-204 1-205 1-206 1-207 1-209 1-214 + GBytes: 0.2 2.0 4.0 6.0 8.1 10.2 20.9 + Hours: 0.13 1.3 2.65 4 5.4 6.8 13.9 + +Big (2000mb) files: 204-255 204-255 206-255 207-255 208-255 210-255 215-255 + GBytes: 101.5 99.6 97.7 95.7 93.8 89.8 80.1 + Hours: 67 66 65 63 62 60 53 + +A recording limit of 100Gb keeps plenty of reserve without blocking too much +file numbers. And with a file size of 30-40Mb, recordings of 4-5 hours fit into +small files completely. (depends on bit rate of course) + + + +The patch must be enabled in Setup-> Recordings-> Hard Link Cutter. When +disabled, the cutter process behaves identical to VDR's default cutter. + +There's a //#define HARDLINK_TEST_ONLY in the videodir.c file that enables a +test-mode that hard-links 00x.vdr_ files only, and continues the classic +editing. The resulting 00x.vdr and 00x.vdr_ files should be identical. If you +delete the un-edited recording, dont forget to delete the *.vdr_ files too, +they will now eat real disk space. + +Note: 'du' displays the disk space of hard links only on first appearance, and +usually you will see a noticeably smaller size on the edited recording. + + +History +------- + +Version 0.2.0 + New: Support for multiple /videoXX recording folders, using advanced searching + for matching file systems where a hard link can be created. + Also supports deep mounted file systems. + Fix: Do not fail if last mark is a cut-in. (Again.) + +Version 0.1.4 + New: Dynamic increase of file size before running out of xxx.vdr files + Fix: Last edit mark is not a cut-out + Fix: Write error if link-copied file is smaller than allowed file size + Fix: Broken index/marks if cut-in is at the start of a new file + Fix: Clear dangeling pointer to free'd cUnbufferedFile, + thx to Matthias Schwarzott + +Version 0.1.0 + Initial release + + + + +Future plans +------------ + +Since original and edited copy share disk space, free space is wrong if one of +them is moved to *.del. Free space should only count files with hard link +count = 1. This still goes wrong if all copies get deleted. + + +For more safety, the hard-linked files may be made read-only, as modifications +to one copy will affect the other copy too. (except deleting, of course) + + +SetBrokenLink may get lost on rare cases, this needs some more thoughts. diff -Nru vdr-1.6.0/recorder.c vdr-1.6.0-HardLinkCutter/recorder.c --- vdr-1.6.0/recorder.c 2008-04-12 22:29:58.000000000 +0200 +++ vdr-1.6.0-HardLinkCutter/recorder.c 2008-04-12 22:31:59.000000000 +0200 @@ -90,7 +90,7 @@ bool cFileWriter::NextFile(void) { if (recordFile && pictureType == I_FRAME) { // every file shall start with an I_FRAME - if (fileSize > MEGABYTE(Setup.MaxVideoFileSize) || RunningLowOnDiskSpace()) { + if (fileSize > fileName->MaxFileSize() || RunningLowOnDiskSpace()) { recordFile = fileName->NextFile(); fileSize = 0; } diff -Nru vdr-1.6.0/recording.c vdr-1.6.0-HardLinkCutter/recording.c --- vdr-1.6.0/recording.c 2008-04-12 22:30:00.000000000 +0200 +++ vdr-1.6.0-HardLinkCutter/recording.c 2008-04-12 22:31:59.000000000 +0200 @@ -1598,6 +1598,16 @@ return NULL; } +int cFileName::MaxFileSize() { + const int smallFiles = (255 * MAXVIDEOFILESIZE - 1024 * Setup.MaxRecordingSize) + / max(MAXVIDEOFILESIZE - Setup.MaxVideoFileSize, 1); + + if (fileNumber <= smallFiles) + return MEGABYTE(Setup.MaxVideoFileSize); + + return MEGABYTE(MAXVIDEOFILESIZE); +} + cUnbufferedFile *cFileName::NextFile(void) { return SetOffset(fileNumber + 1); diff -Nru vdr-1.6.0/recording.h vdr-1.6.0-HardLinkCutter/recording.h --- vdr-1.6.0/recording.h 2008-04-12 22:30:00.000000000 +0200 +++ vdr-1.6.0-HardLinkCutter/recording.h 2008-04-12 22:31:59.000000000 +0200 @@ -206,7 +206,15 @@ // may be slightly higher because we stop recording only before the next // 'I' frame, to have a complete Group Of Pictures): #define MAXVIDEOFILESIZE 2000 // MB -#define MINVIDEOFILESIZE 100 // MB +#define MINVIDEOFILESIZE 1 // MB + +#define MINRECORDINGSIZE 25 // GB +#define MAXRECORDINGSIZE 500 // GB +#define DEFAULTRECORDINGSIZE 100 // GB +// Dynamic recording size: +// Keep recording file size at Setup.MaxVideoFileSize for as long as possible, +// but switch to MAXVIDEOFILESIZE early enough, so that Setup.MaxRecordingSize +// will be reached, before recording to file 255.vdr class cIndexFile { private: @@ -247,6 +255,8 @@ cUnbufferedFile *Open(void); void Close(void); cUnbufferedFile *SetOffset(int Number, int Offset = 0); + int MaxFileSize(); + // Dynamic file size for this file cUnbufferedFile *NextFile(void); }; diff -Nru vdr-1.6.0/videodir.c vdr-1.6.0-HardLinkCutter/videodir.c --- vdr-1.6.0/videodir.c 2008-02-16 14:00:03.000000000 +0100 +++ vdr-1.6.0-HardLinkCutter/videodir.c 2008-04-12 22:31:59.000000000 +0200 @@ -19,6 +19,8 @@ #include "recording.h" #include "tools.h" +//#define HARDLINK_TEST_ONLY + const char *VideoDirectory = VIDEODIR; class cVideoDirectory { @@ -168,6 +170,120 @@ return RemoveFileOrDir(FileName, true); } +static bool StatNearestDir(const char *FileName, struct stat *Stat) +{ + cString Name(FileName); + char *p; + while ((p = strrchr((const char*)Name + 1, '/')) != NULL) { + *p = 0; // truncate at last '/' + if (stat(Name, Stat) == 0) { + isyslog("StatNearestDir: Stating %s", (const char*)Name); + return true; + } + } + return false; +} + +bool HardLinkVideoFile(const char *OldName, const char *NewName) +{ + // Incoming name must be in base video directory: + if (strstr(OldName, VideoDirectory) != OldName) { + esyslog("ERROR: %s not in %s", OldName, VideoDirectory); + return false; + } + if (strstr(NewName, VideoDirectory) != NewName) { + esyslog("ERROR: %s not in %s", NewName, VideoDirectory); + return false; + } + + const char *ActualNewName = NewName; + cString ActualOldName(ReadLink(OldName), true); + + // Some safety checks: + struct stat StatOldName; + if (lstat(ActualOldName, &StatOldName) == 0) { + if (S_ISLNK(StatOldName.st_mode)) { + esyslog("HardLinkVideoFile: Failed to resolve symbolic link %s", (const char*)ActualOldName); + return false; + } + } + else { + esyslog("HardLinkVideoFile: lstat failed on %s", (const char*)ActualOldName); + return false; + } + isyslog("HardLinkVideoFile: %s is on %i", (const char*)ActualOldName, (int)StatOldName.st_dev); + + // Find the video directory where ActualOldName is located + + cVideoDirectory Dir; + struct stat StatDir; + if (!StatNearestDir(NewName, &StatDir)) { + esyslog("HardLinkVideoFile: stat failed on %s", NewName); + return false; + } + + isyslog("HardLinkVideoFile: %s is on %i", NewName, (int)StatDir.st_dev); + if (StatDir.st_dev != StatOldName.st_dev) { + // Not yet found. + + if (!Dir.IsDistributed()) { + esyslog("HardLinkVideoFile: No matching video folder to hard link %s", (const char*)ActualOldName); + return false; + } + + // Search in video01 and upwards + bool found = false; + while (Dir.Next()) { + Dir.Store(); + const char *TmpNewName = Dir.Adjust(NewName); + if (StatNearestDir(TmpNewName, &StatDir) && StatDir.st_dev == StatOldName.st_dev) { + isyslog("HardLinkVideoFile: %s is on %i (match)", TmpNewName, (int)StatDir.st_dev); + ActualNewName = TmpNewName; + found = true; + break; + } + isyslog("HardLinkVideoFile: %s is on %i", TmpNewName, (int)StatDir.st_dev); + } + if (ActualNewName == NewName) { + esyslog("HardLinkVideoFile: No matching video folder to hard link %s", (const char*)ActualOldName); + return false; + } + + // Looking good, we have a match. Create necessary folders. + if (!MakeDirs(ActualNewName, false)) + return false; + // There's no guarantee that the directory of ActualNewName + // is on the same device as the dir that StatNearestDir found. + // But worst case is that the link fails. + } + +#ifdef HARDLINK_TEST_ONLY + // Do the hard link to *.vdr_ for testing only + char *name = NULL; + asprintf(&name, "%s_",ActualNewName); + link(ActualOldName, name); + free(name); + return false; +#endif // HARDLINK_TEST_ONLY + + // Try creating the hard link + if (link(ActualOldName, ActualNewName) != 0) { + // Failed to hard link. Maybe not allowed on file system. + LOG_ERROR_STR(ActualNewName); + isyslog("HardLinkVideoFile: failed to hard link from %s to %s", (const char*)ActualOldName, ActualNewName); + return false; + } + + if (ActualNewName != NewName) { + // video01 and up. Do the remaining symlink + if (symlink(ActualNewName, NewName) < 0) { + LOG_ERROR_STR(NewName); + return false; + } + } + return true; +} + bool VideoFileSpaceAvailable(int SizeMB) { cVideoDirectory Dir; diff -Nru vdr-1.6.0/videodir.h vdr-1.6.0-HardLinkCutter/videodir.h --- vdr-1.6.0/videodir.h 2008-02-16 13:53:11.000000000 +0100 +++ vdr-1.6.0-HardLinkCutter/videodir.h 2008-04-12 22:31:59.000000000 +0200 @@ -19,6 +19,7 @@ int CloseVideoFile(cUnbufferedFile *File); bool RenameVideoFile(const char *OldName, const char *NewName); bool RemoveVideoFile(const char *FileName); +bool HardLinkVideoFile(const char *OldName, const char *NewName); bool VideoFileSpaceAvailable(int SizeMB); int VideoDiskSpace(int *FreeMB = NULL, int *UsedMB = NULL); // returns the used disk space in percent cString PrefixVideoFileName(const char *FileName, char Prefix);