mirror of
https://github.com/vdr-projects/vdr.git
synced 2025-03-01 10:50:46 +00:00
- Updated the Russian OSD texts (thanks to Oleg Roitburd). - Fixed handling the 'pointer field' in generating and parsing PAT/PMT (thanks to Frank Schmirler). - Fixed handling modulation types for DVB-S transponders when processing the NIT. - Changed cDvbDevice::GrabImage() to use V4L2 (thanks to Marco Schlüßler). - Added a poll to cDvbDevice::PlayVideo() and cDvbDevice::PlayAudio() to avoid excessive CPU load (this is just a makeshift solution until the FF DVB cards can play TS directly). - The recording format is now Transport Stream. Existing recordings in PES format can still be replayed and edited, but new recordings are done in TS. All code for recording in PES has been removed. The following changes were made to switch to TS recording format: + The index file format has been changed to support file sizes of up to 1TB (previously 2GB), and up to 65535 separate files per recording (previously 255). + The recording file names are now of the form 00001.ts (previously 001.vdr). + The frame rate is now detected by looking at two subsequent PTS values. The "frame duration" (in multiples of 1/90000) is stored in the info.vdr file using the new tag F (thanks to Artur Skawina for helping to get the IndexToHMSF() calculation right). + Several functions now have an additional parameter FramesPerSecond. + Several functions now have an additional parameter IsPesRecording. + The functionality of cFileWriter was moved into cRecorder, and cRemux is now obsolete. This also avoids one level of data copying while recording. + cRemux, cRingBufferLinearPes, cTS2PES and all c*Repacker classes have been removed. + A PAT/PMT is inserted before every independent frame, so that no extra measures need to be taken when editing a recording. + The directory name for a recording has been changed from YYYY-MM-DD-hh[.:]mm.pr.lt.rec (pr=priority, lt=lifetime) to YYYY-MM-DD-hh.mm.ch-ri.rec (ch=channel, ri=resumeId). Priority and Lifetime are now stored in the info.vdr file with the new tags P and L (if no such file exists, the maximum values are assumed by default, which avoids inadvertently deleting a recording if disk space is low). No longer storing Priority and Lifetime in the directory name avoids starting a new recording if one of these is changed in the timer and the recording is re-started for some reason. Instead of Priority and Lifetime, the directory name now contains the channel number from which the recording was made, and the "resume id" of this instance of VDR. This avoids problems if several VDR instances record the same show on different channels, or even on the same channel. The '-' between channel number and resumeId prevents older versions of VDR from "seeing" these recordings, which makes sure they won't even try to replay them, or remove them in case the disk runs full. + The semantics of PlayTs*() have been changed. These functions are now required to return the given Length (which is TS_SIZE) if they have processed the TS packet. + The files "index", "info", "marks" and "resume" within a TS recording directory are now created without the ".vdr" extension. + The "resume" file is no longer a binary file, but contains tagged lines to be able to store additional information, like the selected audio or subtitle track. + cDevice::StillPicture() will now be called with either TS or PES data. + cDvbPlayer::Goto() no longer appends a "sequence end code" to the data. If the output device needs this, it has to take care of it by itself. - Fixed cPatPmtParser::ParsePmt() to reset vpid and vtype when switching from a video to an audio channel (thanks to Reinhard Nissl). - cDvbDevice now uses the FE_CAN_2G_MODULATION flag to determine whether a device can handle DVB-S2. The #define is still there to allow people with older drivers who don't need DVB-S2 to use this version without pathcing.
1739 lines
52 KiB
C
1739 lines
52 KiB
C
/*
|
|
* recording.c: Recording file handling
|
|
*
|
|
* See the main source file 'vdr.c' for copyright information and
|
|
* how to reach the author.
|
|
*
|
|
* $Id: recording.c 2.4 2009/01/06 14:41:11 kls Exp $
|
|
*/
|
|
|
|
#include "recording.h"
|
|
#include <ctype.h>
|
|
#include <dirent.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <math.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
#include "channels.h"
|
|
#include "i18n.h"
|
|
#include "interface.h"
|
|
#include "skins.h"
|
|
#include "tools.h"
|
|
#include "videodir.h"
|
|
|
|
#define SUMMARYFALLBACK
|
|
|
|
#define RECEXT ".rec"
|
|
#define DELEXT ".del"
|
|
/* This was the original code, which works fine in a Linux only environment.
|
|
Unfortunately, because of Windows and its brain dead file system, we have
|
|
to use a more complicated approach, in order to allow users who have enabled
|
|
the --vfat command line option to see their recordings even if they forget to
|
|
enable --vfat when restarting VDR... Gee, do I hate Windows.
|
|
(kls 2002-07-27)
|
|
#define DATAFORMAT "%4d-%02d-%02d.%02d:%02d.%02d.%02d" RECEXT
|
|
#define NAMEFORMAT "%s/%s/" DATAFORMAT
|
|
*/
|
|
#define DATAFORMATPES "%4d-%02d-%02d.%02d%*c%02d.%02d.%02d" RECEXT
|
|
#define NAMEFORMATPES "%s/%s/" "%4d-%02d-%02d.%02d.%02d.%02d.%02d" RECEXT
|
|
#define DATAFORMATTS "%4d-%02d-%02d.%02d.%02d.%d-%d" RECEXT
|
|
#define NAMEFORMATTS "%s/%s/" DATAFORMATTS
|
|
|
|
#define RESUMEFILESUFFIX "/resume%s%s"
|
|
#ifdef SUMMARYFALLBACK
|
|
#define SUMMARYFILESUFFIX "/summary.vdr"
|
|
#endif
|
|
#define INFOFILESUFFIX "/info"
|
|
#define MARKSFILESUFFIX "/marks"
|
|
|
|
#define MINDISKSPACE 1024 // MB
|
|
|
|
#define REMOVECHECKDELTA 60 // seconds between checks for removing deleted files
|
|
#define DELETEDLIFETIME 300 // seconds after which a deleted recording will be actually removed
|
|
#define DISKCHECKDELTA 100 // seconds between checks for free disk space
|
|
#define REMOVELATENCY 10 // seconds to wait until next check after removing a file
|
|
|
|
#define TIMERMACRO_TITLE "TITLE"
|
|
#define TIMERMACRO_EPISODE "EPISODE"
|
|
|
|
#define MAX_SUBTITLE_LENGTH 40
|
|
|
|
#define MAX_LINK_LEVEL 6
|
|
|
|
bool VfatFileSystem = false;
|
|
|
|
cRecordings DeletedRecordings(true);
|
|
|
|
// --- cRemoveDeletedRecordingsThread ----------------------------------------
|
|
|
|
class cRemoveDeletedRecordingsThread : public cThread {
|
|
protected:
|
|
virtual void Action(void);
|
|
public:
|
|
cRemoveDeletedRecordingsThread(void);
|
|
};
|
|
|
|
cRemoveDeletedRecordingsThread::cRemoveDeletedRecordingsThread(void)
|
|
:cThread("remove deleted recordings")
|
|
{
|
|
}
|
|
|
|
void cRemoveDeletedRecordingsThread::Action(void)
|
|
{
|
|
SetPriority(19);
|
|
// Make sure only one instance of VDR does this:
|
|
cLockFile LockFile(VideoDirectory);
|
|
if (LockFile.Lock()) {
|
|
bool deleted = false;
|
|
cThreadLock DeletedRecordingsLock(&DeletedRecordings);
|
|
for (cRecording *r = DeletedRecordings.First(); r; ) {
|
|
if (r->deleted && time(NULL) - r->deleted > DELETEDLIFETIME) {
|
|
cRecording *next = DeletedRecordings.Next(r);
|
|
r->Remove();
|
|
DeletedRecordings.Del(r);
|
|
r = next;
|
|
deleted = true;
|
|
continue;
|
|
}
|
|
r = DeletedRecordings.Next(r);
|
|
}
|
|
if (deleted)
|
|
RemoveEmptyVideoDirectories();
|
|
}
|
|
}
|
|
|
|
static cRemoveDeletedRecordingsThread RemoveDeletedRecordingsThread;
|
|
|
|
// ---
|
|
|
|
void RemoveDeletedRecordings(void)
|
|
{
|
|
static time_t LastRemoveCheck = 0;
|
|
if (time(NULL) - LastRemoveCheck > REMOVECHECKDELTA) {
|
|
if (!RemoveDeletedRecordingsThread.Active()) {
|
|
cThreadLock DeletedRecordingsLock(&DeletedRecordings);
|
|
for (cRecording *r = DeletedRecordings.First(); r; r = DeletedRecordings.Next(r)) {
|
|
if (r->deleted && time(NULL) - r->deleted > DELETEDLIFETIME) {
|
|
RemoveDeletedRecordingsThread.Start();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
LastRemoveCheck = time(NULL);
|
|
}
|
|
}
|
|
|
|
void AssertFreeDiskSpace(int Priority, bool Force)
|
|
{
|
|
static cMutex Mutex;
|
|
cMutexLock MutexLock(&Mutex);
|
|
// With every call to this function we try to actually remove
|
|
// a file, or mark a file for removal ("delete" it), so that
|
|
// it will get removed during the next call.
|
|
static time_t LastFreeDiskCheck = 0;
|
|
int Factor = (Priority == -1) ? 10 : 1;
|
|
if (Force || time(NULL) - LastFreeDiskCheck > DISKCHECKDELTA / Factor) {
|
|
if (!VideoFileSpaceAvailable(MINDISKSPACE)) {
|
|
// Make sure only one instance of VDR does this:
|
|
cLockFile LockFile(VideoDirectory);
|
|
if (!LockFile.Lock())
|
|
return;
|
|
// Remove the oldest file that has been "deleted":
|
|
isyslog("low disk space while recording, trying to remove a deleted recording...");
|
|
cThreadLock DeletedRecordingsLock(&DeletedRecordings);
|
|
if (DeletedRecordings.Count()) {
|
|
cRecording *r = DeletedRecordings.First();
|
|
cRecording *r0 = NULL;
|
|
while (r) {
|
|
if (IsOnVideoDirectoryFileSystem(r->FileName())) { // only remove recordings that will actually increase the free video disk space
|
|
if (!r0 || r->start < r0->start)
|
|
r0 = r;
|
|
}
|
|
r = DeletedRecordings.Next(r);
|
|
}
|
|
if (r0 && r0->Remove()) {
|
|
DeletedRecordings.Del(r0);
|
|
LastFreeDiskCheck += REMOVELATENCY / Factor;
|
|
return;
|
|
}
|
|
}
|
|
else {
|
|
// DeletedRecordings was empty, so to be absolutely sure there are no
|
|
// deleted recordings we need to double check:
|
|
DeletedRecordings.Update(true);
|
|
if (DeletedRecordings.Count())
|
|
return; // the next call will actually remove it
|
|
}
|
|
// No "deleted" files to remove, so let's see if we can delete a recording:
|
|
isyslog("...no deleted recording found, trying to delete an old recording...");
|
|
cThreadLock RecordingsLock(&Recordings);
|
|
if (Recordings.Count()) {
|
|
cRecording *r = Recordings.First();
|
|
cRecording *r0 = NULL;
|
|
while (r) {
|
|
if (IsOnVideoDirectoryFileSystem(r->FileName())) { // only delete recordings that will actually increase the free video disk space
|
|
if (!r->IsEdited() && r->lifetime < MAXLIFETIME) { // edited recordings and recordings with MAXLIFETIME live forever
|
|
if ((r->lifetime == 0 && Priority > r->priority) || // the recording has no guaranteed lifetime and the new recording has higher priority
|
|
(r->lifetime > 0 && (time(NULL) - r->start) / SECSINDAY >= r->lifetime)) { // the recording's guaranteed lifetime has expired
|
|
if (r0) {
|
|
if (r->priority < r0->priority || (r->priority == r0->priority && r->start < r0->start))
|
|
r0 = r; // in any case we delete the one with the lowest priority (or the older one in case of equal priorities)
|
|
}
|
|
else
|
|
r0 = r;
|
|
}
|
|
}
|
|
}
|
|
r = Recordings.Next(r);
|
|
}
|
|
if (r0 && r0->Delete()) {
|
|
Recordings.Del(r0);
|
|
return;
|
|
}
|
|
}
|
|
// Unable to free disk space, but there's nothing we can do about that...
|
|
isyslog("...no old recording found, giving up");
|
|
Skins.QueueMessage(mtWarning, tr("Low disk space!"), 5, -1);
|
|
}
|
|
LastFreeDiskCheck = time(NULL);
|
|
}
|
|
}
|
|
|
|
// --- cResumeFile -----------------------------------------------------------
|
|
|
|
cResumeFile::cResumeFile(const char *FileName, bool IsPesRecording)
|
|
{
|
|
isPesRecording = IsPesRecording;
|
|
const char *Suffix = isPesRecording ? RESUMEFILESUFFIX ".vdr" : RESUMEFILESUFFIX;
|
|
fileName = MALLOC(char, strlen(FileName) + strlen(Suffix) + 1);
|
|
if (fileName) {
|
|
strcpy(fileName, FileName);
|
|
sprintf(fileName + strlen(fileName), Suffix, Setup.ResumeID ? "." : "", Setup.ResumeID ? *itoa(Setup.ResumeID) : "");
|
|
}
|
|
else
|
|
esyslog("ERROR: can't allocate memory for resume file name");
|
|
}
|
|
|
|
cResumeFile::~cResumeFile()
|
|
{
|
|
free(fileName);
|
|
}
|
|
|
|
int cResumeFile::Read(void)
|
|
{
|
|
int resume = -1;
|
|
if (fileName) {
|
|
struct stat st;
|
|
if (stat(fileName, &st) == 0) {
|
|
if ((st.st_mode & S_IWUSR) == 0) // no write access, assume no resume
|
|
return -1;
|
|
}
|
|
if (isPesRecording) {
|
|
int f = open(fileName, O_RDONLY);
|
|
if (f >= 0) {
|
|
if (safe_read(f, &resume, sizeof(resume)) != sizeof(resume)) {
|
|
resume = -1;
|
|
LOG_ERROR_STR(fileName);
|
|
}
|
|
close(f);
|
|
}
|
|
else if (errno != ENOENT)
|
|
LOG_ERROR_STR(fileName);
|
|
}
|
|
else {
|
|
FILE *f = fopen(fileName, "r");
|
|
if (f) {
|
|
cReadLine ReadLine;
|
|
char *s;
|
|
int line = 0;
|
|
while ((s = ReadLine.Read(f)) != NULL) {
|
|
++line;
|
|
char *t = skipspace(s + 1);
|
|
switch (*s) {
|
|
case 'I': resume = atoi(t);
|
|
break;
|
|
}
|
|
}
|
|
fclose(f);
|
|
}
|
|
else if (errno != ENOENT)
|
|
LOG_ERROR_STR(fileName);
|
|
}
|
|
}
|
|
return resume;
|
|
}
|
|
|
|
bool cResumeFile::Save(int Index)
|
|
{
|
|
if (fileName) {
|
|
if (isPesRecording) {
|
|
int f = open(fileName, O_WRONLY | O_CREAT | O_TRUNC, DEFFILEMODE);
|
|
if (f >= 0) {
|
|
if (safe_write(f, &Index, sizeof(Index)) < 0)
|
|
LOG_ERROR_STR(fileName);
|
|
close(f);
|
|
Recordings.ResetResume(fileName);
|
|
return true;
|
|
}
|
|
}
|
|
else {
|
|
FILE *f = fopen(fileName, "w");
|
|
if (f) {
|
|
fprintf(f, "I %d\n", Index);
|
|
fclose(f);
|
|
}
|
|
else
|
|
LOG_ERROR_STR(fileName);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void cResumeFile::Delete(void)
|
|
{
|
|
if (fileName) {
|
|
if (remove(fileName) < 0 && errno != ENOENT)
|
|
LOG_ERROR_STR(fileName);
|
|
Recordings.ResetResume(fileName);
|
|
}
|
|
}
|
|
|
|
// --- cRecordingInfo --------------------------------------------------------
|
|
|
|
cRecordingInfo::cRecordingInfo(const cChannel *Channel, const cEvent *Event)
|
|
{
|
|
channelID = Channel ? Channel->GetChannelID() : tChannelID::InvalidID;
|
|
channelName = Channel ? strdup(Channel->Name()) : NULL;
|
|
ownEvent = Event ? NULL : new cEvent(0);
|
|
event = ownEvent ? ownEvent : Event;
|
|
aux = NULL;
|
|
framesPerSecond = DEFAULTFRAMESPERSECOND;
|
|
priority = MAXPRIORITY;
|
|
lifetime = MAXLIFETIME;
|
|
fileName = NULL;
|
|
if (Channel) {
|
|
// Since the EPG data's component records can carry only a single
|
|
// language code, let's see whether the channel's PID data has
|
|
// more information:
|
|
cComponents *Components = (cComponents *)event->Components();
|
|
if (!Components)
|
|
Components = new cComponents;
|
|
for (int i = 0; i < MAXAPIDS; i++) {
|
|
const char *s = Channel->Alang(i);
|
|
if (*s) {
|
|
tComponent *Component = Components->GetComponent(i, 2, 3);
|
|
if (!Component)
|
|
Components->SetComponent(Components->NumComponents(), 2, 3, s, NULL);
|
|
else if (strlen(s) > strlen(Component->language))
|
|
strn0cpy(Component->language, s, sizeof(Component->language));
|
|
}
|
|
}
|
|
// There's no "multiple languages" for Dolby Digital tracks, but
|
|
// we do the same procedure here, too, in case there is no component
|
|
// information at all:
|
|
for (int i = 0; i < MAXDPIDS; i++) {
|
|
const char *s = Channel->Dlang(i);
|
|
if (*s) {
|
|
tComponent *Component = Components->GetComponent(i, 4, 0); // AC3 component according to the DVB standard
|
|
if (!Component)
|
|
Component = Components->GetComponent(i, 2, 5); // fallback "Dolby" component according to the "Premiere pseudo standard"
|
|
if (!Component)
|
|
Components->SetComponent(Components->NumComponents(), 2, 5, s, NULL);
|
|
else if (strlen(s) > strlen(Component->language))
|
|
strn0cpy(Component->language, s, sizeof(Component->language));
|
|
}
|
|
}
|
|
// The same applies to subtitles:
|
|
for (int i = 0; i < MAXSPIDS; i++) {
|
|
const char *s = Channel->Slang(i);
|
|
if (*s) {
|
|
tComponent *Component = Components->GetComponent(i, 3, 3);
|
|
if (!Component)
|
|
Components->SetComponent(Components->NumComponents(), 3, 3, s, NULL);
|
|
else if (strlen(s) > strlen(Component->language))
|
|
strn0cpy(Component->language, s, sizeof(Component->language));
|
|
}
|
|
}
|
|
if (Components != event->Components())
|
|
((cEvent *)event)->SetComponents(Components);
|
|
}
|
|
}
|
|
|
|
cRecordingInfo::cRecordingInfo(const char *FileName)
|
|
{
|
|
channelID = tChannelID::InvalidID;
|
|
channelName = NULL;
|
|
ownEvent = new cEvent(0);
|
|
event = ownEvent;
|
|
aux = NULL;
|
|
framesPerSecond = DEFAULTFRAMESPERSECOND;
|
|
priority = MAXPRIORITY;
|
|
lifetime = MAXLIFETIME;
|
|
fileName = strdup(cString::sprintf("%s%s", FileName, INFOFILESUFFIX));
|
|
}
|
|
|
|
cRecordingInfo::~cRecordingInfo()
|
|
{
|
|
delete ownEvent;
|
|
free(aux);
|
|
free(channelName);
|
|
free(fileName);
|
|
}
|
|
|
|
void cRecordingInfo::SetData(const char *Title, const char *ShortText, const char *Description)
|
|
{
|
|
if (!isempty(Title))
|
|
((cEvent *)event)->SetTitle(Title);
|
|
if (!isempty(ShortText))
|
|
((cEvent *)event)->SetShortText(ShortText);
|
|
if (!isempty(Description))
|
|
((cEvent *)event)->SetDescription(Description);
|
|
}
|
|
|
|
void cRecordingInfo::SetAux(const char *Aux)
|
|
{
|
|
free(aux);
|
|
aux = Aux ? strdup(Aux) : NULL;
|
|
}
|
|
|
|
void cRecordingInfo::SetFramesPerSecond(double FramesPerSecond)
|
|
{
|
|
framesPerSecond = FramesPerSecond;
|
|
}
|
|
|
|
bool cRecordingInfo::Read(FILE *f)
|
|
{
|
|
if (ownEvent) {
|
|
cReadLine ReadLine;
|
|
char *s;
|
|
int line = 0;
|
|
while ((s = ReadLine.Read(f)) != NULL) {
|
|
++line;
|
|
char *t = skipspace(s + 1);
|
|
switch (*s) {
|
|
case 'C': {
|
|
char *p = strchr(t, ' ');
|
|
if (p) {
|
|
free(channelName);
|
|
channelName = strdup(compactspace(p));
|
|
*p = 0; // strips optional channel name
|
|
}
|
|
if (*t)
|
|
channelID = tChannelID::FromString(t);
|
|
}
|
|
break;
|
|
case 'E': {
|
|
unsigned int EventID;
|
|
time_t StartTime;
|
|
int Duration;
|
|
unsigned int TableID = 0;
|
|
unsigned int Version = 0xFF;
|
|
int n = sscanf(t, "%u %ld %d %X %X", &EventID, &StartTime, &Duration, &TableID, &Version);
|
|
if (n >= 3 && n <= 5) {
|
|
ownEvent->SetEventID(EventID);
|
|
ownEvent->SetStartTime(StartTime);
|
|
ownEvent->SetDuration(Duration);
|
|
ownEvent->SetTableID(TableID);
|
|
ownEvent->SetVersion(Version);
|
|
}
|
|
}
|
|
break;
|
|
case 'F': framesPerSecond = atof(t);
|
|
break;
|
|
case 'L': lifetime = atoi(t);
|
|
break;
|
|
case 'P': priority = atoi(t);
|
|
break;
|
|
case '@': free(aux);
|
|
aux = strdup(t);
|
|
break;
|
|
case '#': break; // comments are ignored
|
|
default: if (!ownEvent->Parse(s)) {
|
|
esyslog("ERROR: EPG data problem in line %d", line);
|
|
return false;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool cRecordingInfo::Write(FILE *f, const char *Prefix) const
|
|
{
|
|
if (channelID.Valid())
|
|
fprintf(f, "%sC %s%s%s\n", Prefix, *channelID.ToString(), channelName ? " " : "", channelName ? channelName : "");
|
|
event->Dump(f, Prefix, true);
|
|
fprintf(f, "%sF %.10g\n", Prefix, framesPerSecond);
|
|
fprintf(f, "%sP %d\n", Prefix, priority);
|
|
fprintf(f, "%sL %d\n", Prefix, lifetime);
|
|
if (aux)
|
|
fprintf(f, "%s@ %s\n", Prefix, aux);
|
|
return true;
|
|
}
|
|
|
|
bool cRecordingInfo::Read(void)
|
|
{
|
|
bool Result = false;
|
|
if (fileName) {
|
|
FILE *f = fopen(fileName, "r");
|
|
if (f) {
|
|
if (Read(f))
|
|
Result = true;
|
|
else
|
|
esyslog("ERROR: EPG data problem in file %s", fileName);
|
|
fclose(f);
|
|
}
|
|
else if (errno != ENOENT)
|
|
LOG_ERROR_STR(fileName);
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
bool cRecordingInfo::Write(void) const
|
|
{
|
|
bool Result = false;
|
|
if (fileName) {
|
|
cSafeFile f(fileName);
|
|
if (f.Open()) {
|
|
if (Write(f))
|
|
Result = true;
|
|
f.Close();
|
|
}
|
|
else
|
|
LOG_ERROR_STR(fileName);
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
// --- cRecording ------------------------------------------------------------
|
|
|
|
#define RESUME_NOT_INITIALIZED (-2)
|
|
|
|
struct tCharExchange { char a; char b; };
|
|
tCharExchange CharExchange[] = {
|
|
{ '~', '/' },
|
|
{ '/', '~' },
|
|
{ ' ', '_' },
|
|
// backwards compatibility:
|
|
{ '\'', '\'' },
|
|
{ '\'', '\x01' },
|
|
{ '/', '\x02' },
|
|
{ 0, 0 }
|
|
};
|
|
|
|
char *ExchangeChars(char *s, bool ToFileSystem)
|
|
{
|
|
char *p = s;
|
|
while (*p) {
|
|
if (VfatFileSystem) {
|
|
// The VFAT file system can't handle all characters, so we
|
|
// have to take extra efforts to encode/decode them:
|
|
if (ToFileSystem) {
|
|
const char *InvalidChars = "\"\\/:*?|<>#";
|
|
switch (*p) {
|
|
// characters that can be mapped to other characters:
|
|
case ' ': *p = '_'; break;
|
|
case '~': *p = '/'; break;
|
|
// characters that have to be encoded:
|
|
default:
|
|
if (strchr(InvalidChars, *p) || *p == '.' && (!*(p + 1) || *(p + 1) == '~')) { // Windows can't handle '.' at the end of file/directory names
|
|
int l = p - s;
|
|
s = (char *)realloc(s, strlen(s) + 10);
|
|
p = s + l;
|
|
char buf[4];
|
|
sprintf(buf, "#%02X", (unsigned char)*p);
|
|
memmove(p + 2, p, strlen(p) + 1);
|
|
strncpy(p, buf, 3);
|
|
p += 2;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
switch (*p) {
|
|
// mapped characters:
|
|
case '_': *p = ' '; break;
|
|
case '/': *p = '~'; break;
|
|
// encoded characters:
|
|
case '#': {
|
|
if (strlen(p) > 2 && isxdigit(*(p + 1)) && isxdigit(*(p + 2))) {
|
|
char buf[3];
|
|
sprintf(buf, "%c%c", *(p + 1), *(p + 2));
|
|
unsigned char c = strtol(buf, NULL, 16);
|
|
if (c) {
|
|
*p = c;
|
|
memmove(p + 1, p + 3, strlen(p) - 2);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
// backwards compatibility:
|
|
case '\x01': *p = '\''; break;
|
|
case '\x02': *p = '/'; break;
|
|
case '\x03': *p = ':'; break;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
for (struct tCharExchange *ce = CharExchange; ce->a && ce->b; ce++) {
|
|
if (*p == (ToFileSystem ? ce->a : ce->b)) {
|
|
*p = ToFileSystem ? ce->b : ce->a;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
p++;
|
|
}
|
|
return s;
|
|
}
|
|
|
|
cRecording::cRecording(cTimer *Timer, const cEvent *Event)
|
|
{
|
|
resume = RESUME_NOT_INITIALIZED;
|
|
titleBuffer = NULL;
|
|
sortBuffer = NULL;
|
|
fileName = NULL;
|
|
name = NULL;
|
|
fileSizeMB = -1; // unknown
|
|
channel = Timer->Channel()->Number();
|
|
resumeId = Setup.ResumeID;
|
|
isPesRecording = false;
|
|
framesPerSecond = DEFAULTFRAMESPERSECOND;
|
|
deleted = 0;
|
|
// set up the actual name:
|
|
const char *Title = Event ? Event->Title() : NULL;
|
|
const char *Subtitle = Event ? Event->ShortText() : NULL;
|
|
char SubtitleBuffer[MAX_SUBTITLE_LENGTH];
|
|
if (isempty(Title))
|
|
Title = Timer->Channel()->Name();
|
|
if (isempty(Subtitle))
|
|
Subtitle = " ";
|
|
else if (strlen(Subtitle) > MAX_SUBTITLE_LENGTH) {
|
|
// let's make sure the Subtitle doesn't produce too long a file name:
|
|
Utf8Strn0Cpy(SubtitleBuffer, Subtitle, MAX_SUBTITLE_LENGTH);
|
|
Subtitle = SubtitleBuffer;
|
|
}
|
|
char *macroTITLE = strstr(Timer->File(), TIMERMACRO_TITLE);
|
|
char *macroEPISODE = strstr(Timer->File(), TIMERMACRO_EPISODE);
|
|
if (macroTITLE || macroEPISODE) {
|
|
name = strdup(Timer->File());
|
|
name = strreplace(name, TIMERMACRO_TITLE, Title);
|
|
name = strreplace(name, TIMERMACRO_EPISODE, Subtitle);
|
|
// avoid blanks at the end:
|
|
int l = strlen(name);
|
|
while (l-- > 2) {
|
|
if (name[l] == ' ' && name[l - 1] != '~')
|
|
name[l] = 0;
|
|
else
|
|
break;
|
|
}
|
|
if (Timer->IsSingleEvent()) {
|
|
Timer->SetFile(name); // this was an instant recording, so let's set the actual data
|
|
Timers.SetModified();
|
|
}
|
|
}
|
|
else if (Timer->IsSingleEvent() || !Setup.UseSubtitle)
|
|
name = strdup(Timer->File());
|
|
else
|
|
name = strdup(cString::sprintf("%s~%s", Timer->File(), Subtitle));
|
|
// substitute characters that would cause problems in file names:
|
|
strreplace(name, '\n', ' ');
|
|
start = Timer->StartTime();
|
|
priority = Timer->Priority();
|
|
lifetime = Timer->Lifetime();
|
|
// handle info:
|
|
info = new cRecordingInfo(Timer->Channel(), Event);
|
|
info->SetAux(Timer->Aux());
|
|
info->priority = priority;
|
|
info->lifetime = lifetime;
|
|
}
|
|
|
|
cRecording::cRecording(const char *FileName)
|
|
{
|
|
resume = RESUME_NOT_INITIALIZED;
|
|
fileSizeMB = -1; // unknown
|
|
channel = -1;
|
|
resumeId = -1;
|
|
priority = MAXPRIORITY; // assume maximum in case there is no info file
|
|
lifetime = MAXLIFETIME;
|
|
isPesRecording = false;
|
|
framesPerSecond = DEFAULTFRAMESPERSECOND;
|
|
deleted = 0;
|
|
titleBuffer = NULL;
|
|
sortBuffer = NULL;
|
|
fileName = strdup(FileName);
|
|
FileName += strlen(VideoDirectory) + 1;
|
|
char *p = strrchr(FileName, '/');
|
|
|
|
name = NULL;
|
|
info = new cRecordingInfo;
|
|
if (p) {
|
|
time_t now = time(NULL);
|
|
struct tm tm_r;
|
|
struct tm t = *localtime_r(&now, &tm_r); // this initializes the time zone in 't'
|
|
t.tm_isdst = -1; // makes sure mktime() will determine the correct DST setting
|
|
if (7 == sscanf(p + 1, DATAFORMATTS, &t.tm_year, &t.tm_mon, &t.tm_mday, &t.tm_hour, &t.tm_min, &channel, &resumeId)
|
|
|| 7 == sscanf(p + 1, DATAFORMATPES, &t.tm_year, &t.tm_mon, &t.tm_mday, &t.tm_hour, &t.tm_min, &priority, &lifetime)) {
|
|
t.tm_year -= 1900;
|
|
t.tm_mon--;
|
|
t.tm_sec = 0;
|
|
start = mktime(&t);
|
|
name = MALLOC(char, p - FileName + 1);
|
|
strncpy(name, FileName, p - FileName);
|
|
name[p - FileName] = 0;
|
|
name = ExchangeChars(name, false);
|
|
isPesRecording = resumeId < 0;
|
|
}
|
|
else
|
|
return;
|
|
GetResume();
|
|
// read an optional info file:
|
|
cString InfoFileName = cString::sprintf("%s%s", fileName, isPesRecording ? INFOFILESUFFIX ".vdr" : INFOFILESUFFIX);
|
|
FILE *f = fopen(InfoFileName, "r");
|
|
if (f) {
|
|
if (!info->Read(f))
|
|
esyslog("ERROR: EPG data problem in file %s", *InfoFileName);
|
|
else if (!isPesRecording) {
|
|
priority = info->priority;
|
|
lifetime = info->lifetime;
|
|
framesPerSecond = info->framesPerSecond;
|
|
}
|
|
fclose(f);
|
|
}
|
|
else if (errno != ENOENT)
|
|
LOG_ERROR_STR(*InfoFileName);
|
|
#ifdef SUMMARYFALLBACK
|
|
// fall back to the old 'summary.vdr' if there was no 'info.vdr':
|
|
if (isempty(info->Title())) {
|
|
cString SummaryFileName = cString::sprintf("%s%s", fileName, SUMMARYFILESUFFIX);
|
|
FILE *f = fopen(SummaryFileName, "r");
|
|
if (f) {
|
|
int line = 0;
|
|
char *data[3] = { NULL };
|
|
cReadLine ReadLine;
|
|
char *s;
|
|
while ((s = ReadLine.Read(f)) != NULL) {
|
|
if (*s || line > 1) {
|
|
if (data[line]) {
|
|
int len = strlen(s);
|
|
len += strlen(data[line]) + 1;
|
|
data[line] = (char *)realloc(data[line], len + 1);
|
|
strcat(data[line], "\n");
|
|
strcat(data[line], s);
|
|
}
|
|
else
|
|
data[line] = strdup(s);
|
|
}
|
|
else
|
|
line++;
|
|
}
|
|
fclose(f);
|
|
if (!data[2]) {
|
|
data[2] = data[1];
|
|
data[1] = NULL;
|
|
}
|
|
else if (data[1] && data[2]) {
|
|
// if line 1 is too long, it can't be the short text,
|
|
// so assume the short text is missing and concatenate
|
|
// line 1 and line 2 to be the long text:
|
|
int len = strlen(data[1]);
|
|
if (len > 80) {
|
|
data[1] = (char *)realloc(data[1], len + 1 + strlen(data[2]) + 1);
|
|
strcat(data[1], "\n");
|
|
strcat(data[1], data[2]);
|
|
free(data[2]);
|
|
data[2] = data[1];
|
|
data[1] = NULL;
|
|
}
|
|
}
|
|
info->SetData(data[0], data[1], data[2]);
|
|
for (int i = 0; i < 3; i ++)
|
|
free(data[i]);
|
|
}
|
|
else if (errno != ENOENT)
|
|
LOG_ERROR_STR(*SummaryFileName);
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
cRecording::~cRecording()
|
|
{
|
|
free(titleBuffer);
|
|
free(sortBuffer);
|
|
free(fileName);
|
|
free(name);
|
|
delete info;
|
|
}
|
|
|
|
char *cRecording::StripEpisodeName(char *s)
|
|
{
|
|
char *t = s, *s1 = NULL, *s2 = NULL;
|
|
while (*t) {
|
|
if (*t == '/') {
|
|
if (s1) {
|
|
if (s2)
|
|
s1 = s2;
|
|
s2 = t;
|
|
}
|
|
else
|
|
s1 = t;
|
|
}
|
|
t++;
|
|
}
|
|
if (s1 && s2)
|
|
memmove(s1 + 1, s2, t - s2 + 1);
|
|
return s;
|
|
}
|
|
|
|
char *cRecording::SortName(void) const
|
|
{
|
|
if (!sortBuffer) {
|
|
char *s = StripEpisodeName(strdup(FileName() + strlen(VideoDirectory) + 1));
|
|
strreplace(s, '/', 'a'); // some locales ignore '/' when sorting
|
|
int l = strxfrm(NULL, s, 0) + 1;
|
|
sortBuffer = MALLOC(char, l);
|
|
strxfrm(sortBuffer, s, l);
|
|
free(s);
|
|
}
|
|
return sortBuffer;
|
|
}
|
|
|
|
int cRecording::GetResume(void) const
|
|
{
|
|
if (resume == RESUME_NOT_INITIALIZED) {
|
|
cResumeFile ResumeFile(FileName(), isPesRecording);
|
|
resume = ResumeFile.Read();
|
|
}
|
|
return resume;
|
|
}
|
|
|
|
int cRecording::Compare(const cListObject &ListObject) const
|
|
{
|
|
cRecording *r = (cRecording *)&ListObject;
|
|
return strcasecmp(SortName(), r->SortName());
|
|
}
|
|
|
|
const char *cRecording::FileName(void) const
|
|
{
|
|
if (!fileName) {
|
|
struct tm tm_r;
|
|
struct tm *t = localtime_r(&start, &tm_r);
|
|
const char *fmt = isPesRecording ? NAMEFORMATPES : NAMEFORMATTS;
|
|
int ch = isPesRecording ? priority : channel;
|
|
int ri = isPesRecording ? lifetime : resumeId;
|
|
name = ExchangeChars(name, true);
|
|
fileName = strdup(cString::sprintf(fmt, VideoDirectory, name, t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, ch, ri));
|
|
name = ExchangeChars(name, false);
|
|
}
|
|
return fileName;
|
|
}
|
|
|
|
const char *cRecording::Title(char Delimiter, bool NewIndicator, int Level) const
|
|
{
|
|
char New = NewIndicator && IsNew() ? '*' : ' ';
|
|
free(titleBuffer);
|
|
titleBuffer = NULL;
|
|
if (Level < 0 || Level == HierarchyLevels()) {
|
|
struct tm tm_r;
|
|
struct tm *t = localtime_r(&start, &tm_r);
|
|
char *s;
|
|
if (Level > 0 && (s = strrchr(name, '~')) != NULL)
|
|
s++;
|
|
else
|
|
s = name;
|
|
titleBuffer = strdup(cString::sprintf("%02d.%02d.%02d%c%02d:%02d%c%c%s",
|
|
t->tm_mday,
|
|
t->tm_mon + 1,
|
|
t->tm_year % 100,
|
|
Delimiter,
|
|
t->tm_hour,
|
|
t->tm_min,
|
|
New,
|
|
Delimiter,
|
|
s));
|
|
// let's not display a trailing '~':
|
|
if (!NewIndicator)
|
|
stripspace(titleBuffer);
|
|
s = &titleBuffer[strlen(titleBuffer) - 1];
|
|
if (*s == '~')
|
|
*s = 0;
|
|
}
|
|
else if (Level < HierarchyLevels()) {
|
|
const char *s = name;
|
|
const char *p = s;
|
|
while (*++s) {
|
|
if (*s == '~') {
|
|
if (Level--)
|
|
p = s + 1;
|
|
else
|
|
break;
|
|
}
|
|
}
|
|
titleBuffer = MALLOC(char, s - p + 3);
|
|
*titleBuffer = Delimiter;
|
|
*(titleBuffer + 1) = Delimiter;
|
|
strn0cpy(titleBuffer + 2, p, s - p + 1);
|
|
}
|
|
else
|
|
return "";
|
|
return titleBuffer;
|
|
}
|
|
|
|
const char *cRecording::PrefixFileName(char Prefix)
|
|
{
|
|
cString p = PrefixVideoFileName(FileName(), Prefix);
|
|
if (*p) {
|
|
free(fileName);
|
|
fileName = strdup(p);
|
|
return fileName;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
int cRecording::HierarchyLevels(void) const
|
|
{
|
|
const char *s = name;
|
|
int level = 0;
|
|
while (*++s) {
|
|
if (*s == '~')
|
|
level++;
|
|
}
|
|
return level;
|
|
}
|
|
|
|
bool cRecording::IsEdited(void) const
|
|
{
|
|
const char *s = strrchr(name, '~');
|
|
s = !s ? name : s + 1;
|
|
return *s == '%';
|
|
}
|
|
|
|
bool cRecording::WriteInfo(void)
|
|
{
|
|
cString InfoFileName = cString::sprintf("%s%s", fileName, isPesRecording ? INFOFILESUFFIX ".vdr" : INFOFILESUFFIX);
|
|
FILE *f = fopen(InfoFileName, "w");
|
|
if (f) {
|
|
info->Write(f);
|
|
fclose(f);
|
|
}
|
|
else
|
|
LOG_ERROR_STR(*InfoFileName);
|
|
return true;
|
|
}
|
|
|
|
bool cRecording::Delete(void)
|
|
{
|
|
bool result = true;
|
|
char *NewName = strdup(FileName());
|
|
char *ext = strrchr(NewName, '.');
|
|
if (ext && strcmp(ext, RECEXT) == 0) {
|
|
strncpy(ext, DELEXT, strlen(ext));
|
|
if (access(NewName, F_OK) == 0) {
|
|
// the new name already exists, so let's remove that one first:
|
|
isyslog("removing recording '%s'", NewName);
|
|
RemoveVideoFile(NewName);
|
|
}
|
|
isyslog("deleting recording '%s'", FileName());
|
|
if (access(FileName(), F_OK) == 0)
|
|
result = RenameVideoFile(FileName(), NewName);
|
|
else {
|
|
isyslog("recording '%s' vanished", FileName());
|
|
result = true; // well, we were going to delete it, anyway
|
|
}
|
|
}
|
|
free(NewName);
|
|
return result;
|
|
}
|
|
|
|
bool cRecording::Remove(void)
|
|
{
|
|
// let's do a final safety check here:
|
|
if (!endswith(FileName(), DELEXT)) {
|
|
esyslog("attempt to remove recording %s", FileName());
|
|
return false;
|
|
}
|
|
isyslog("removing recording %s", FileName());
|
|
return RemoveVideoFile(FileName());
|
|
}
|
|
|
|
bool cRecording::Undelete(void)
|
|
{
|
|
bool result = true;
|
|
char *NewName = strdup(FileName());
|
|
char *ext = strrchr(NewName, '.');
|
|
if (ext && strcmp(ext, DELEXT) == 0) {
|
|
strncpy(ext, RECEXT, strlen(ext));
|
|
if (access(NewName, F_OK) == 0) {
|
|
// the new name already exists, so let's not remove that one:
|
|
esyslog("ERROR: attempt to undelete '%s', while recording '%s' exists", FileName(), NewName);
|
|
result = false;
|
|
}
|
|
else {
|
|
isyslog("undeleting recording '%s'", FileName());
|
|
if (access(FileName(), F_OK) == 0)
|
|
result = RenameVideoFile(FileName(), NewName);
|
|
else {
|
|
isyslog("deleted recording '%s' vanished", FileName());
|
|
result = false;
|
|
}
|
|
}
|
|
}
|
|
free(NewName);
|
|
return result;
|
|
}
|
|
|
|
void cRecording::ResetResume(void) const
|
|
{
|
|
resume = RESUME_NOT_INITIALIZED;
|
|
}
|
|
|
|
// --- cRecordings -----------------------------------------------------------
|
|
|
|
cRecordings Recordings;
|
|
|
|
char *cRecordings::updateFileName = NULL;
|
|
|
|
cRecordings::cRecordings(bool Deleted)
|
|
:cThread("video directory scanner")
|
|
{
|
|
deleted = Deleted;
|
|
lastUpdate = 0;
|
|
state = 0;
|
|
}
|
|
|
|
cRecordings::~cRecordings()
|
|
{
|
|
Cancel(3);
|
|
}
|
|
|
|
void cRecordings::Action(void)
|
|
{
|
|
Refresh();
|
|
}
|
|
|
|
const char *cRecordings::UpdateFileName(void)
|
|
{
|
|
if (!updateFileName)
|
|
updateFileName = strdup(AddDirectory(VideoDirectory, ".update"));
|
|
return updateFileName;
|
|
}
|
|
|
|
void cRecordings::Refresh(bool Foreground)
|
|
{
|
|
lastUpdate = time(NULL); // doing this first to make sure we don't miss anything
|
|
Lock();
|
|
Clear();
|
|
ChangeState();
|
|
Unlock();
|
|
ScanVideoDir(VideoDirectory, Foreground);
|
|
}
|
|
|
|
void cRecordings::ScanVideoDir(const char *DirName, bool Foreground, int LinkLevel)
|
|
{
|
|
cReadDir d(DirName);
|
|
struct dirent *e;
|
|
while ((Foreground || Running()) && (e = d.Next()) != NULL) {
|
|
if (strcmp(e->d_name, ".") && strcmp(e->d_name, "..")) {
|
|
char *buffer = strdup(AddDirectory(DirName, e->d_name));
|
|
struct stat st;
|
|
if (stat(buffer, &st) == 0) {
|
|
int Link = 0;
|
|
if (S_ISLNK(st.st_mode)) {
|
|
if (LinkLevel > MAX_LINK_LEVEL) {
|
|
isyslog("max link level exceeded - not scanning %s", buffer);
|
|
continue;
|
|
}
|
|
Link = 1;
|
|
char *old = buffer;
|
|
buffer = ReadLink(old);
|
|
free(old);
|
|
if (!buffer)
|
|
continue;
|
|
if (stat(buffer, &st) != 0) {
|
|
free(buffer);
|
|
continue;
|
|
}
|
|
}
|
|
if (S_ISDIR(st.st_mode)) {
|
|
if (endswith(buffer, deleted ? DELEXT : RECEXT)) {
|
|
cRecording *r = new cRecording(buffer);
|
|
if (r->Name()) {
|
|
Lock();
|
|
Add(r);
|
|
ChangeState();
|
|
Unlock();
|
|
if (deleted) {
|
|
r->fileSizeMB = DirSizeMB(buffer);
|
|
r->deleted = time(NULL);
|
|
}
|
|
}
|
|
else
|
|
delete r;
|
|
}
|
|
else
|
|
ScanVideoDir(buffer, Foreground, LinkLevel + Link);
|
|
}
|
|
}
|
|
free(buffer);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool cRecordings::StateChanged(int &State)
|
|
{
|
|
int NewState = state;
|
|
bool Result = State != NewState;
|
|
State = state;
|
|
return Result;
|
|
}
|
|
|
|
void cRecordings::TouchUpdate(void)
|
|
{
|
|
bool needsUpdate = NeedsUpdate();
|
|
TouchFile(UpdateFileName());
|
|
if (!needsUpdate)
|
|
lastUpdate = time(NULL); // make sure we don't tigger ourselves
|
|
}
|
|
|
|
bool cRecordings::NeedsUpdate(void)
|
|
{
|
|
time_t lastModified = LastModifiedTime(UpdateFileName());
|
|
if (lastModified > time(NULL))
|
|
return false; // somebody's clock isn't running correctly
|
|
return lastUpdate < lastModified;
|
|
}
|
|
|
|
bool cRecordings::Update(bool Wait)
|
|
{
|
|
if (Wait) {
|
|
Refresh(true);
|
|
return Count() > 0;
|
|
}
|
|
else
|
|
Start();
|
|
return false;
|
|
}
|
|
|
|
cRecording *cRecordings::GetByName(const char *FileName)
|
|
{
|
|
if (FileName) {
|
|
for (cRecording *recording = First(); recording; recording = Next(recording)) {
|
|
if (strcmp(recording->FileName(), FileName) == 0)
|
|
return recording;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
void cRecordings::AddByName(const char *FileName, bool TriggerUpdate)
|
|
{
|
|
LOCK_THREAD;
|
|
cRecording *recording = GetByName(FileName);
|
|
if (!recording) {
|
|
recording = new cRecording(FileName);
|
|
Add(recording);
|
|
ChangeState();
|
|
if (TriggerUpdate)
|
|
TouchUpdate();
|
|
}
|
|
}
|
|
|
|
void cRecordings::DelByName(const char *FileName)
|
|
{
|
|
LOCK_THREAD;
|
|
cRecording *recording = GetByName(FileName);
|
|
if (recording) {
|
|
cThreadLock DeletedRecordingsLock(&DeletedRecordings);
|
|
Del(recording, false);
|
|
char *ext = strrchr(recording->FileName(), '.');
|
|
if (ext) {
|
|
strncpy(ext, DELEXT, strlen(ext));
|
|
recording->fileSizeMB = DirSizeMB(recording->FileName());
|
|
recording->deleted = time(NULL);
|
|
DeletedRecordings.Add(recording);
|
|
}
|
|
else
|
|
delete recording;
|
|
ChangeState();
|
|
TouchUpdate();
|
|
}
|
|
}
|
|
|
|
int cRecordings::TotalFileSizeMB(void)
|
|
{
|
|
int size = 0;
|
|
LOCK_THREAD;
|
|
for (cRecording *recording = First(); recording; recording = Next(recording)) {
|
|
if (recording->fileSizeMB > 0 && IsOnVideoDirectoryFileSystem(recording->FileName()))
|
|
size += recording->fileSizeMB;
|
|
}
|
|
return size;
|
|
}
|
|
|
|
void cRecordings::ResetResume(const char *ResumeFileName)
|
|
{
|
|
LOCK_THREAD;
|
|
for (cRecording *recording = First(); recording; recording = Next(recording)) {
|
|
if (!ResumeFileName || strncmp(ResumeFileName, recording->FileName(), strlen(recording->FileName())) == 0)
|
|
recording->ResetResume();
|
|
}
|
|
ChangeState();
|
|
}
|
|
|
|
// --- cMark -----------------------------------------------------------------
|
|
|
|
double MarkFramesPerSecond = DEFAULTFRAMESPERSECOND;
|
|
cMutex MutexMarkFramesPerSecond;
|
|
|
|
cMark::cMark(int Position, const char *Comment, double FramesPerSecond)
|
|
{
|
|
position = Position;
|
|
comment = Comment ? strdup(Comment) : NULL;
|
|
framesPerSecond = FramesPerSecond;
|
|
}
|
|
|
|
cMark::~cMark()
|
|
{
|
|
free(comment);
|
|
}
|
|
|
|
cString cMark::ToText(void)
|
|
{
|
|
return cString::sprintf("%s%s%s\n", *IndexToHMSF(position, true, framesPerSecond), comment ? " " : "", comment ? comment : "");
|
|
}
|
|
|
|
bool cMark::Parse(const char *s)
|
|
{
|
|
free(comment);
|
|
comment = NULL;
|
|
framesPerSecond = MarkFramesPerSecond;
|
|
position = HMSFToIndex(s, framesPerSecond);
|
|
const char *p = strchr(s, ' ');
|
|
if (p) {
|
|
p = skipspace(p);
|
|
if (*p)
|
|
comment = strdup(p);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool cMark::Save(FILE *f)
|
|
{
|
|
return fprintf(f, "%s", *ToText()) > 0;
|
|
}
|
|
|
|
// --- cMarks ----------------------------------------------------------------
|
|
|
|
bool cMarks::Load(const char *RecordingFileName, double FramesPerSecond, bool IsPesRecording)
|
|
{
|
|
cMutexLock MutexLock(&MutexMarkFramesPerSecond);
|
|
framesPerSecond = FramesPerSecond;
|
|
MarkFramesPerSecond = framesPerSecond;
|
|
if (cConfig<cMark>::Load(AddDirectory(RecordingFileName, IsPesRecording ? MARKSFILESUFFIX ".vdr" : MARKSFILESUFFIX))) {
|
|
Sort();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void cMarks::Sort(void)
|
|
{
|
|
for (cMark *m1 = First(); m1; m1 = Next(m1)) {
|
|
for (cMark *m2 = Next(m1); m2; m2 = Next(m2)) {
|
|
if (m2->position < m1->position) {
|
|
swap(m1->position, m2->position);
|
|
swap(m1->comment, m2->comment);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
cMark *cMarks::Add(int Position)
|
|
{
|
|
cMark *m = Get(Position);
|
|
if (!m) {
|
|
cConfig<cMark>::Add(m = new cMark(Position, NULL, framesPerSecond));
|
|
Sort();
|
|
}
|
|
return m;
|
|
}
|
|
|
|
cMark *cMarks::Get(int Position)
|
|
{
|
|
for (cMark *mi = First(); mi; mi = Next(mi)) {
|
|
if (mi->position == Position)
|
|
return mi;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
cMark *cMarks::GetPrev(int Position)
|
|
{
|
|
for (cMark *mi = Last(); mi; mi = Prev(mi)) {
|
|
if (mi->position < Position)
|
|
return mi;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
cMark *cMarks::GetNext(int Position)
|
|
{
|
|
for (cMark *mi = First(); mi; mi = Next(mi)) {
|
|
if (mi->position > Position)
|
|
return mi;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// --- cRecordingUserCommand -------------------------------------------------
|
|
|
|
const char *cRecordingUserCommand::command = NULL;
|
|
|
|
void cRecordingUserCommand::InvokeCommand(const char *State, const char *RecordingFileName)
|
|
{
|
|
if (command) {
|
|
cString cmd = cString::sprintf("%s %s \"%s\"", command, State, *strescape(RecordingFileName, "\\\"$"));
|
|
isyslog("executing '%s'", *cmd);
|
|
SystemExec(cmd);
|
|
}
|
|
}
|
|
|
|
// --- cIndexFile ------------------------------------------------------------
|
|
|
|
#define INDEXFILESUFFIX "/index"
|
|
|
|
// The number of frames to stay off the end in case of time shift:
|
|
#define INDEXSAFETYLIMIT 150 // frames
|
|
|
|
// The maximum time to wait before giving up while catching up on an index file:
|
|
#define MAXINDEXCATCHUP 8 // seconds
|
|
|
|
// The minimum age of an index file for considering it no longer to be written:
|
|
#define MININDEXAGE 3600 // seconds
|
|
|
|
struct tIndexPes {
|
|
uint32_t offset;
|
|
uchar type;
|
|
uchar number;
|
|
uint16_t reserved;
|
|
};
|
|
|
|
struct tIndexTs {
|
|
uint64_t offset:40; // up to 1TB per file (not using off_t here - must definitely be exactly 64 bit!)
|
|
int reserved:7; // reserved for future use
|
|
int independent:1; // marks frames that can be displayed by themselves (for trick modes)
|
|
uint16_t number:16; // up to 64K files per recording
|
|
tIndexTs(off_t Offset, bool Independent, uint16_t Number)
|
|
{
|
|
offset = Offset;
|
|
reserved = 0;
|
|
independent = Independent;
|
|
number = Number;
|
|
}
|
|
};
|
|
|
|
cIndexFile::cIndexFile(const char *FileName, bool Record, bool IsPesRecording)
|
|
:resumeFile(FileName, IsPesRecording)
|
|
{
|
|
f = -1;
|
|
fileName = NULL;
|
|
size = 0;
|
|
last = -1;
|
|
index = NULL;
|
|
isPesRecording = IsPesRecording;
|
|
if (FileName) {
|
|
const char *Suffix = isPesRecording ? INDEXFILESUFFIX ".vdr" : INDEXFILESUFFIX;
|
|
fileName = MALLOC(char, strlen(FileName) + strlen(Suffix) + 1);
|
|
if (fileName) {
|
|
strcpy(fileName, FileName);
|
|
char *pFileExt = fileName + strlen(fileName);
|
|
strcpy(pFileExt, Suffix);
|
|
int delta = 0;
|
|
if (access(fileName, R_OK) == 0) {
|
|
struct stat buf;
|
|
if (stat(fileName, &buf) == 0) {
|
|
delta = buf.st_size % sizeof(tIndexTs);
|
|
if (delta) {
|
|
delta = sizeof(tIndexTs) - delta;
|
|
esyslog("ERROR: invalid file size (%lld) in '%s'", buf.st_size, fileName);
|
|
}
|
|
last = (buf.st_size + delta) / sizeof(tIndexTs) - 1;
|
|
if (!Record && last >= 0) {
|
|
size = last + 1;
|
|
index = MALLOC(tIndexTs, size);
|
|
if (index) {
|
|
f = open(fileName, O_RDONLY);
|
|
if (f >= 0) {
|
|
if ((int)safe_read(f, index, buf.st_size) != buf.st_size) {
|
|
esyslog("ERROR: can't read from file '%s'", fileName);
|
|
free(index);
|
|
index = NULL;
|
|
close(f);
|
|
f = -1;
|
|
}
|
|
// we don't close f here, see CatchUp()!
|
|
else if (isPesRecording)
|
|
ConvertFromPes(index, size);
|
|
}
|
|
else
|
|
LOG_ERROR_STR(fileName);
|
|
}
|
|
else
|
|
esyslog("ERROR: can't allocate %zd bytes for index '%s'", size * sizeof(tIndexTs), fileName);
|
|
}
|
|
}
|
|
else
|
|
LOG_ERROR;
|
|
}
|
|
else if (!Record)
|
|
isyslog("missing index file %s", fileName);
|
|
if (Record) {
|
|
if ((f = open(fileName, O_WRONLY | O_CREAT | O_APPEND, DEFFILEMODE)) >= 0) {
|
|
if (delta) {
|
|
esyslog("ERROR: padding index file with %d '0' bytes", delta);
|
|
while (delta--)
|
|
writechar(f, 0);
|
|
}
|
|
}
|
|
else
|
|
LOG_ERROR_STR(fileName);
|
|
}
|
|
}
|
|
else
|
|
esyslog("ERROR: can't copy file name '%s'", FileName);
|
|
}
|
|
}
|
|
|
|
cIndexFile::~cIndexFile()
|
|
{
|
|
if (f >= 0)
|
|
close(f);
|
|
free(fileName);
|
|
free(index);
|
|
}
|
|
|
|
void cIndexFile::ConvertFromPes(tIndexTs *IndexTs, int Count)
|
|
{
|
|
tIndexPes IndexPes;
|
|
while (Count-- > 0) {
|
|
memcpy(&IndexPes, IndexTs, sizeof(IndexPes));
|
|
IndexTs->offset = IndexPes.offset;
|
|
IndexTs->independent = IndexPes.type == 1; // I_FRAME
|
|
IndexTs->number = IndexPes.number;
|
|
IndexTs++;
|
|
}
|
|
}
|
|
|
|
bool cIndexFile::CatchUp(int Index)
|
|
{
|
|
// returns true unless something really goes wrong, so that 'index' becomes NULL
|
|
if (index && f >= 0) {
|
|
cMutexLock MutexLock(&mutex);
|
|
for (int i = 0; i <= MAXINDEXCATCHUP && (Index < 0 || Index >= last); i++) {
|
|
struct stat buf;
|
|
if (fstat(f, &buf) == 0) {
|
|
if (time(NULL) - buf.st_mtime > MININDEXAGE) {
|
|
// apparently the index file is not being written any more
|
|
close(f);
|
|
f = -1;
|
|
break;
|
|
}
|
|
int newLast = buf.st_size / sizeof(tIndexTs) - 1;
|
|
if (newLast > last) {
|
|
if (size <= newLast) {
|
|
size *= 2;
|
|
if (size <= newLast)
|
|
size = newLast + 1;
|
|
}
|
|
index = (tIndexTs *)realloc(index, size * sizeof(tIndexTs));
|
|
if (index) {
|
|
int offset = (last + 1) * sizeof(tIndexTs);
|
|
int delta = (newLast - last) * sizeof(tIndexTs);
|
|
if (lseek(f, offset, SEEK_SET) == offset) {
|
|
if (safe_read(f, &index[last + 1], delta) != delta) {
|
|
esyslog("ERROR: can't read from index");
|
|
free(index);
|
|
index = NULL;
|
|
close(f);
|
|
f = -1;
|
|
break;
|
|
}
|
|
if (isPesRecording)
|
|
ConvertFromPes(&index[last + 1], newLast - last);
|
|
last = newLast;
|
|
}
|
|
else
|
|
LOG_ERROR_STR(fileName);
|
|
}
|
|
else
|
|
esyslog("ERROR: can't realloc() index");
|
|
}
|
|
}
|
|
else
|
|
LOG_ERROR_STR(fileName);
|
|
if (Index < last - (i ? 2 * INDEXSAFETYLIMIT : 0) || Index > 10 * INDEXSAFETYLIMIT) // keep off the end in case of "Pause live video"
|
|
break;
|
|
cCondWait::SleepMs(1000);
|
|
}
|
|
}
|
|
return index != NULL;
|
|
}
|
|
|
|
bool cIndexFile::Write(bool Independent, uint16_t FileNumber, off_t FileOffset)
|
|
{
|
|
if (f >= 0) {
|
|
tIndexTs i(FileOffset, Independent, FileNumber);
|
|
if (safe_write(f, &i, sizeof(i)) < 0) {
|
|
LOG_ERROR_STR(fileName);
|
|
close(f);
|
|
f = -1;
|
|
return false;
|
|
}
|
|
last++;
|
|
}
|
|
return f >= 0;
|
|
}
|
|
|
|
bool cIndexFile::Get(int Index, uint16_t *FileNumber, off_t *FileOffset, bool *Independent, int *Length)
|
|
{
|
|
if (CatchUp(Index)) {
|
|
if (Index >= 0 && Index < last) {
|
|
*FileNumber = index[Index].number;
|
|
*FileOffset = index[Index].offset;
|
|
if (Independent)
|
|
*Independent = index[Index].independent;
|
|
if (Length) {
|
|
int fn = index[Index + 1].number;
|
|
int fo = index[Index + 1].offset;
|
|
if (fn == *FileNumber)
|
|
*Length = fo - *FileOffset;
|
|
else
|
|
*Length = -1; // this means "everything up to EOF" (the buffer's Read function will act accordingly)
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
int cIndexFile::GetNextIFrame(int Index, bool Forward, uint16_t *FileNumber, off_t *FileOffset, int *Length, bool StayOffEnd)
|
|
{
|
|
if (CatchUp()) {
|
|
int d = Forward ? 1 : -1;
|
|
for (;;) {
|
|
Index += d;
|
|
if (Index >= 0 && Index < last - ((Forward && StayOffEnd) ? INDEXSAFETYLIMIT : 0)) {
|
|
if (index[Index].independent) {
|
|
uint16_t fn;
|
|
if (!FileNumber)
|
|
FileNumber = &fn;
|
|
off_t fo;
|
|
if (!FileOffset)
|
|
FileOffset = &fo;
|
|
*FileNumber = index[Index].number;
|
|
*FileOffset = index[Index].offset;
|
|
if (Length) {
|
|
// all recordings end with a non-independent frame, so the following should be safe:
|
|
int fn = index[Index + 1].number;
|
|
int fo = index[Index + 1].offset;
|
|
if (fn == *FileNumber)
|
|
*Length = fo - *FileOffset;
|
|
else {
|
|
esyslog("ERROR: 'I' frame at end of file #%d", *FileNumber);
|
|
*Length = -1;
|
|
}
|
|
}
|
|
return Index;
|
|
}
|
|
}
|
|
else
|
|
break;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
int cIndexFile::Get(uint16_t FileNumber, off_t FileOffset)
|
|
{
|
|
if (CatchUp()) {
|
|
//TODO implement binary search!
|
|
int i;
|
|
for (i = 0; i < last; i++) {
|
|
if (index[i].number > FileNumber || (index[i].number == FileNumber) && off_t(index[i].offset) >= FileOffset)
|
|
break;
|
|
}
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
bool cIndexFile::IsStillRecording()
|
|
{
|
|
return f >= 0;
|
|
}
|
|
|
|
// --- cFileName -------------------------------------------------------------
|
|
|
|
#define MAXFILESPERRECORDINGPES 255
|
|
#define RECORDFILESUFFIXPES "/%03d.vdr"
|
|
#define MAXFILESPERRECORDINGTS 65535
|
|
#define RECORDFILESUFFIXTS "/%05d.ts"
|
|
#define RECORDFILESUFFIXLEN 20 // some additional bytes for safety...
|
|
|
|
cFileName::cFileName(const char *FileName, bool Record, bool Blocking, bool IsPesRecording)
|
|
{
|
|
file = NULL;
|
|
fileNumber = 0;
|
|
record = Record;
|
|
blocking = Blocking;
|
|
isPesRecording = IsPesRecording;
|
|
// Prepare the file name:
|
|
fileName = MALLOC(char, strlen(FileName) + RECORDFILESUFFIXLEN);
|
|
if (!fileName) {
|
|
esyslog("ERROR: can't copy file name '%s'", fileName);
|
|
return;
|
|
}
|
|
strcpy(fileName, FileName);
|
|
pFileNumber = fileName + strlen(fileName);
|
|
SetOffset(1);
|
|
}
|
|
|
|
cFileName::~cFileName()
|
|
{
|
|
Close();
|
|
free(fileName);
|
|
}
|
|
|
|
cUnbufferedFile *cFileName::Open(void)
|
|
{
|
|
if (!file) {
|
|
int BlockingFlag = blocking ? 0 : O_NONBLOCK;
|
|
if (record) {
|
|
dsyslog("recording to '%s'", fileName);
|
|
file = OpenVideoFile(fileName, O_RDWR | O_CREAT | O_LARGEFILE | BlockingFlag);
|
|
if (!file)
|
|
LOG_ERROR_STR(fileName);
|
|
}
|
|
else {
|
|
if (access(fileName, R_OK) == 0) {
|
|
dsyslog("playing '%s'", fileName);
|
|
file = cUnbufferedFile::Create(fileName, O_RDONLY | O_LARGEFILE | BlockingFlag);
|
|
if (!file)
|
|
LOG_ERROR_STR(fileName);
|
|
}
|
|
else if (errno != ENOENT)
|
|
LOG_ERROR_STR(fileName);
|
|
}
|
|
}
|
|
return file;
|
|
}
|
|
|
|
void cFileName::Close(void)
|
|
{
|
|
if (file) {
|
|
if (CloseVideoFile(file) < 0)
|
|
LOG_ERROR_STR(fileName);
|
|
file = NULL;
|
|
}
|
|
}
|
|
|
|
cUnbufferedFile *cFileName::SetOffset(int Number, off_t Offset)
|
|
{
|
|
if (fileNumber != Number)
|
|
Close();
|
|
int MaxFilesPerRecording = isPesRecording ? MAXFILESPERRECORDINGPES : MAXFILESPERRECORDINGTS;
|
|
if (0 < Number && Number <= MaxFilesPerRecording) {
|
|
fileNumber = Number;
|
|
sprintf(pFileNumber, isPesRecording ? RECORDFILESUFFIXPES : RECORDFILESUFFIXTS, fileNumber);
|
|
if (record) {
|
|
if (access(fileName, F_OK) == 0) {
|
|
// files exists, check if it has non-zero size
|
|
struct stat buf;
|
|
if (stat(fileName, &buf) == 0) {
|
|
if (buf.st_size != 0)
|
|
return SetOffset(Number + 1); // file exists and has non zero size, let's try next suffix
|
|
else {
|
|
// zero size file, remove it
|
|
dsyslog("cFileName::SetOffset: removing zero-sized file %s", fileName);
|
|
unlink(fileName);
|
|
}
|
|
}
|
|
else
|
|
return SetOffset(Number + 1); // error with fstat - should not happen, just to be on the safe side
|
|
}
|
|
else if (errno != ENOENT) { // something serious has happened
|
|
LOG_ERROR_STR(fileName);
|
|
return NULL;
|
|
}
|
|
// found a non existing file suffix
|
|
}
|
|
if (Open() >= 0) {
|
|
if (!record && Offset >= 0 && file && file->Seek(Offset, SEEK_SET) != Offset) {
|
|
LOG_ERROR_STR(fileName);
|
|
return NULL;
|
|
}
|
|
}
|
|
return file;
|
|
}
|
|
esyslog("ERROR: max number of files (%d) exceeded", MaxFilesPerRecording);
|
|
return NULL;
|
|
}
|
|
|
|
cUnbufferedFile *cFileName::NextFile(void)
|
|
{
|
|
return SetOffset(fileNumber + 1);
|
|
}
|
|
|
|
// --- Index stuff -----------------------------------------------------------
|
|
|
|
cString IndexToHMSF(int Index, bool WithFrame, double FramesPerSecond)
|
|
{
|
|
char buffer[16];
|
|
double Seconds;
|
|
int f = modf((Index + 0.5) / FramesPerSecond, &Seconds) * FramesPerSecond + 1;
|
|
int s = int(Seconds);
|
|
int m = s / 60 % 60;
|
|
int h = s / 3600;
|
|
s %= 60;
|
|
snprintf(buffer, sizeof(buffer), WithFrame ? "%d:%02d:%02d.%02d" : "%d:%02d:%02d", h, m, s, f);
|
|
return buffer;
|
|
}
|
|
|
|
int HMSFToIndex(const char *HMSF, double FramesPerSecond)
|
|
{
|
|
int h, m, s, f = 1;
|
|
int n = sscanf(HMSF, "%d:%d:%d.%d", &h, &m, &s, &f);
|
|
if (n == 1)
|
|
return h - 1; // plain frame number
|
|
if (n >= 3)
|
|
return int(round((h * 3600 + m * 60 + s) * FramesPerSecond)) + f - 1;
|
|
return 0;
|
|
}
|
|
|
|
int SecondsToFrames(int Seconds, double FramesPerSecond)
|
|
{
|
|
return round(Seconds * FramesPerSecond);
|
|
}
|
|
|
|
// --- ReadFrame -------------------------------------------------------------
|
|
|
|
int ReadFrame(cUnbufferedFile *f, uchar *b, int Length, int Max)
|
|
{
|
|
if (Length == -1)
|
|
Length = Max; // this means we read up to EOF (see cIndex)
|
|
else if (Length > Max) {
|
|
esyslog("ERROR: frame larger than buffer (%d > %d)", Length, Max);
|
|
Length = Max;
|
|
}
|
|
int r = f->Read(b, Length);
|
|
if (r < 0)
|
|
LOG_ERROR;
|
|
return r;
|
|
}
|