diff options
author | Klaus Schmidinger <vdr@tvdr.de> | 2000-04-15 17:38:11 +0200 |
---|---|---|
committer | Klaus Schmidinger <vdr@tvdr.de> | 2000-04-15 17:38:11 +0200 |
commit | 735093b8faef4f667e6a6c65f7290858286816e3 (patch) | |
tree | f5b3cb133d5ec1ede63de04cf9c394bcf4a514a2 /dvbapi.c | |
parent | 571686d90996968c01a8bc560659e364efab2942 (diff) | |
download | vdr-735093b8faef4f667e6a6c65f7290858286816e3.tar.gz vdr-735093b8faef4f667e6a6c65f7290858286816e3.tar.bz2 |
Implemented actual record/replay; support for CICAM0.0.3
Diffstat (limited to 'dvbapi.c')
-rw-r--r-- | dvbapi.c | 1394 |
1 files changed, 1291 insertions, 103 deletions
@@ -4,171 +4,1024 @@ * See the main source file 'osm.c' for copyright information and * how to reach the author. * - * $Id: dvbapi.c 1.2 2000/03/11 10:39:09 kls Exp $ + * $Id: dvbapi.c 1.3 2000/04/15 14:45:04 kls Exp $ */ #include "dvbapi.h" +#include <errno.h> #include <fcntl.h> -#include <stdio.h> +#include <signal.h> +#include <stdlib.h> #include <sys/ioctl.h> +#include <sys/stat.h> +#include <sys/time.h> +#include <sys/wait.h> #include <unistd.h> #include "interface.h" #include "tools.h" #define VIDEODEVICE "/dev/video" -const char *DvbQuality = "LMH"; // Low, Medium, High +// The size of the array used to buffer video data: +#define VIDEOBUFSIZE (1024*1024) -bool DvbSetChannel(int FrequencyMHz, char Polarization, int Diseqc, int Srate, int Vpid, int Apid) +// The minimum amount of video data necessary to identify frames +// (must be smaller than VIDEOBUFSIZE!): +#define MINVIDEODATA (20*1024) // just a safe guess (max. size of any AV_PES block, plus some safety) + +// The maximum time the buffer is allowed to write data to disk when recording: +#define MAXRECORDWRITETIME 50 // ms + +#define AV_PES_HEADER_LEN 8 + +// AV_PES block types: +#define AV_PES_VIDEO 1 +#define AV_PES_AUDIO 2 + +// Picture types: +#define NO_PICTURE 0 +#define I_FRAME 1 +#define P_FRAME 2 +#define B_FRAME 3 + +#define FRAMESPERSEC 25 + +// The maximum file size is limited by the range that can be covered +// with 'int'. 4GB might be possible (if the range is considered +// 'unsigned'), 2GB should be possible (even if the range is considered +// 'signed'), so let's use 1GB for absolute safety (the actual file size +// may be slightly higher because we stop recording only before the next +// 'I' frame, to have a complete Group Of Pictures): +#define MAXVIDEOFILESIZE (1024*1024*1024) +#define MAXFILESPERRECORDING 255 + +#define INDEXFILESUFFIX "/index.vdr" +#define RESUMEFILESUFFIX "/resume.vdr" +#define RECORDFILESUFFIX "/%03d.vdr" +#define RECORDFILESUFFIXLEN 20 // some additional bytes for safety... +#define MAXPROCESSTIMEOUT 3 // seconds + +// The number of frames to back up when resuming an interrupted replay session: +#define RESUMEBACKUP (10 * FRAMESPERSEC) + +typedef unsigned char uchar; + +// --- cIndexFile ------------------------------------------------------------ + +class cIndexFile { +private: + struct tIndex { int offset; uchar type; uchar number; short reserved; }; + int f; + char *fileName, *pFileExt; + int last, resume; + tIndex *index; +public: + cIndexFile(const char *FileName, bool Record = false); + ~cIndexFile(); + void Write(uchar PictureType, uchar FileNumber, int FileOffset); + bool Get(int Index, uchar *FileNumber, int *FileOffset, uchar *PictureType = NULL); + int GetNextIFrame(int Index, bool Forward, uchar *FileNumber, int *FileOffset, int *Length = NULL); + int Get(uchar FileNumber, int FileOffset); + int Last(void) { return last; } + int GetResume(void) { return resume; } + bool StoreResume(int Index); + static char *Str(int Index); + }; + +cIndexFile::cIndexFile(const char *FileName, bool Record) { - int v = open(VIDEODEVICE, O_RDWR); - - if (v >= 0) { - struct frontend front; - ioctl(v, VIDIOCGFRONTEND, &front); - unsigned int freq = FrequencyMHz; - front.ttk = (freq < 11800UL) ? 0 : 1; - if (freq < 11800UL) - freq -= 9750UL; + f = -1; + fileName = pFileExt = NULL; + last = resume = -1; + index = NULL; + if (FileName) { + fileName = new char[strlen(FileName) + strlen(INDEXFILESUFFIX) + strlen(RESUMEFILESUFFIX) + 1]; + if (fileName) { // no max() function at hand... + strcpy(fileName, FileName); + pFileExt = fileName + strlen(fileName); + strcpy(pFileExt, INDEXFILESUFFIX); + int delta = 0; + if (access(fileName, R_OK) == 0) { + struct stat buf; + if (stat(fileName, &buf) == 0) { + delta = buf.st_size % sizeof(tIndex); + if (delta) { + delta = sizeof(tIndex) - delta; + esyslog(LOG_ERR, "ERROR: invalid file size (%d) in '%s'", buf.st_size, fileName); + } + last = (buf.st_size + delta) / sizeof(tIndex) - 1; + if (!Record && last >= 0) { + index = new tIndex[last + 1]; + int fi = open(fileName, O_RDONLY); + if (fi >= 0) { + if ((int)read(fi, index, buf.st_size) != buf.st_size) { + esyslog(LOG_ERR, "ERROR: can't read from file '%s'", fileName); + delete index; + index = NULL; + } + close(fi); + } + else + LOG_ERROR_STR(fileName); + } + } + else + LOG_ERROR; + } + if (Record) { + if ((f = open(fileName, O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR)) >= 0) { + if (delta) { + esyslog(LOG_ERR, "ERROR: padding index file with %d '0' bytes", delta); + while (delta--) + writechar(f, 0); + } + } + else + LOG_ERROR_STR(fileName); + delete fileName; + fileName = pFileExt = NULL; + } + else { + strcpy(pFileExt, RESUMEFILESUFFIX); + int resumeFile = open(fileName, O_RDONLY); + if (resumeFile >= 0) { + if (read(resumeFile, &resume, sizeof(resume)) != sizeof(resume)) { + resume = -1; + LOG_ERROR_STR(fileName); + } + close(resumeFile); + } + else if (errno != ENOENT) + LOG_ERROR_STR(fileName); + } + } else - freq -= 10600UL; - front.freq = freq * 1000000UL; - front.diseqc = Diseqc; - front.srate = Srate * 1000; - front.volt = (Polarization == 'v') ? 0 : 1; - front.video_pid = Vpid; - front.audio_pid = Apid; - front.AFC = 1; - ioctl(v, VIDIOCSFRONTEND, &front); - close(v); - if (front.sync & 0x1F == 0x1F) + esyslog(LOG_ERR, "ERROR: can't copy file name '%s'", FileName); + } +} + +cIndexFile::~cIndexFile() +{ + if (f >= 0) + close(f); + delete fileName; +} + +void cIndexFile::Write(uchar PictureType, uchar FileNumber, int FileOffset) +{ + if (f >= 0) { + tIndex i = { FileOffset, PictureType, FileNumber, 0 }; + if (write(f, &i, sizeof(i)) != sizeof(i)) { + esyslog(LOG_ERR, "ERROR: can't write to index file"); + close(f); + f = -1; + return; + } + last++; + } +} + +bool cIndexFile::Get(int Index, uchar *FileNumber, int *FileOffset, uchar *PictureType) +{ + if (index) { + if (Index >= 0 && Index <= last) { + *FileNumber = index[Index].number; + *FileOffset = index[Index].offset; + if (PictureType) + *PictureType = index[Index].type; return true; - esyslog(LOG_ERR, "channel not sync'ed (front.sync=%X)!", front.sync); + } } - else - Interface.Error("can't open VIDEODEVICE");//XXX return false; } -// -- cDvbRecorder ----------------------------------------------------------- +int cIndexFile::GetNextIFrame(int Index, bool Forward, uchar *FileNumber, int *FileOffset, int *Length) +{ + if (index) { + int d = Forward ? 1 : -1; + for (;;) { + Index += d; + if (Index >= 0 && Index <= last) { + if (index[Index].type == I_FRAME) { + *FileNumber = index[Index].number; + *FileOffset = index[Index].offset; + if (Length) { + // all recordings end with a non-I_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(LOG_ERR, "ERROR: 'I' frame at end of file #%d", *FileNumber); + *Length = -1; + } + } + return Index; + } + } + else + break; + } + } + return -1; +} + +int cIndexFile::Get(uchar FileNumber, int FileOffset) +{ + if (index) { + //TODO implement binary search! + int i; + for (i = 0; i < last; i++) { + if (index[i].number > FileNumber || (index[i].number == FileNumber) && index[i].offset >= FileOffset) + break; + } + return i; + } + return -1; +} -cDvbRecorder::cDvbRecorder(void) +bool cIndexFile::StoreResume(int Index) { - recording = false; + if (fileName) { + int resumeFile = open(fileName, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR); + if (resumeFile >= 0) { + if (write(resumeFile, &Index, sizeof(Index)) != sizeof(Index)) + LOG_ERROR_STR(fileName); + close(resumeFile); + return true; + } + else if (errno != ENOENT) + LOG_ERROR_STR(fileName); + } + return false; } -cDvbRecorder::~cDvbRecorder() +char *cIndexFile::Str(int Index) { - Stop(); + static char buffer[16]; + int f = (Index % FRAMESPERSEC) + 1; + int s = (Index / FRAMESPERSEC); + int m = s / 60 % 60; + int h = s / 3600; + s %= 60; + snprintf(buffer, sizeof(buffer), "%d:%02d:%02d.%02d", h, m, s, f); + return buffer; } -bool cDvbRecorder::Recording(void) +// --- cRingBuffer ----------------------------------------------------------- + +/* cRingBuffer reads data from an input file, stores it in a buffer and writes + it to an output file upon request. The Read() and Write() functions should + be called only when the associated file is ready to provide or receive data + (use the 'select()' function to determine that), and the files should be + opened in non-blocking mode. + The '...Limit' parameters define safety limits. If they are exceeded a log entry + will be made. +*/ + +class cRingBuffer { +private: + uchar *buffer; + int size, head, tail, freeLimit, availLimit; + int countLimit, countOverflow; + int minFree; + bool eof; + int *inFile, *outFile; +protected: + int Free(void) { return ((tail >= head) ? size + head - tail : head - tail) - 1; } + int Available(void) { return (tail >= head) ? tail - head : size - head + tail; } + int Readable(void) { return (tail >= head) ? size - tail - (head ? 0 : 1) : head - tail - 1; } // keep a 1 byte gap! + int Writeable(void) { return (tail > head) ? tail - head : size - head; } + int Byte(int Offset); + bool WaitForOutFile(int Timeout); +public: + cRingBuffer(int *InFile, int *OutFile, int Size, int FreeLimit = 0, int AvailLimit = 0); + virtual ~cRingBuffer(); + virtual int Read(int Max = -1); + virtual int Write(int Max = -1); + bool EndOfFile(void) { return eof; } + bool Empty(void) { return Available() == 0; } + void Clear(void) { head = tail = 0; } + void Skip(int n); + }; + +cRingBuffer::cRingBuffer(int *InFile, int *OutFile, int Size, int FreeLimit, int AvailLimit) { - return recording; + inFile = InFile; + outFile = OutFile; + size = Size; + Clear(); + freeLimit = FreeLimit; + availLimit = AvailLimit; + eof = false; + countLimit = countOverflow = 0; + minFree = size - 1; + buffer = new uchar[size]; + if (!buffer) + esyslog(LOG_ERR, "ERROR: can't allocate ring buffer (size=%d)", size); } -bool cDvbRecorder::Record(const char *FileName, char Quality) +cRingBuffer::~cRingBuffer() { - isyslog(LOG_INFO, "record %s (%c)", FileName, Quality); - if (MakeDirs(FileName)) { - FILE *f = fopen(FileName, "a"); - if (f) { - fprintf(f, "%s, %c\n", FileName, Quality); - fclose(f); - recording = true; - // TODO - Interface.Error("Recording not yet implemented!"); + dsyslog(LOG_INFO, "buffer stats: %d free, %d overflows, limit exceeded %d times", minFree, countOverflow, countLimit); + delete buffer; +} + +int cRingBuffer::Byte(int Offset) +{ + if (buffer && Offset < Available()) { + Offset += head; + if (Offset >= size) + Offset -= size; + return buffer[Offset]; + } + return -1; +} + +void cRingBuffer::Skip(int n) +{ + if (head < tail) { + head += n; + if (head > tail) + head = tail; + } + else if (head > tail) { + head += n; + if (head >= size) + head -= size; + if (head > tail) + head = tail; + } +} + +bool cRingBuffer::WaitForOutFile(int Timeout) +{ + fd_set set; + FD_ZERO(&set); + FD_SET(*outFile, &set); + struct timeval timeout; + timeout.tv_sec = 0; + timeout.tv_usec = Timeout; + if (select(FD_SETSIZE, NULL, &set, NULL, &timeout) > 0) { + if (FD_ISSET(*outFile, &set)) return true; + } + esyslog(LOG_ERR, "ERROR: timeout in WaitForOutFile(%d)", Timeout); + return false; +} + +int cRingBuffer::Read(int Max) +{ + if (buffer) { + eof = false; + int free = Free(); + if (free < minFree) + minFree = free; + if (freeLimit) { + if (free == 0) { + esyslog(LOG_ERR, "ERROR: buffer overflow (size=%d)", size); + countOverflow++; + } + else if (free < freeLimit) { + dsyslog(LOG_INFO, "free buffer space dipped into limit (%d < %d)", free, freeLimit); + countLimit++; + } + } + if (free == 0) + return 0; // the buffer is full + int readin = 0; + for (int i = 0; i < 2; i++) { + // If we read in exactly as many bytes as are immediately + // "readable" we have to do it again, because that means we + // were at the very end of the physical buffer and possibly only + // read in very few bytes. + int immediate = Readable(); + int n = immediate; + if (Max > 0 && n > Max) + n = Max; + if (n > 0) { + int r = read(*inFile, buffer + tail, n); + if (r > 0) { + readin += r; + tail += r; + if (tail > size) + esyslog(LOG_ERR, "ERROR: ooops: buffer tail (%d) exceeds size (%d)", tail, size); + if (tail >= size) + tail = 0; + } + else if (r < 0 && errno != EAGAIN) { + LOG_ERROR; + return -1; + } + else + eof = true; + if (r == immediate && Max != immediate && tail == 0) + Max -= immediate; + else + break; + } + } + return readin; + } + return -1; +} + +int cRingBuffer::Write(int Max) +{ + if (buffer) { + int avail = Available(); + if (availLimit) { + //XXX stats??? + if (avail == 0) + //XXX esyslog(LOG_ERR, "ERROR: buffer empty!"); + {//XXX + esyslog(LOG_ERR, "ERROR: buffer empty! %d", Max); + return Max > 0 ? Max : 0; + }//XXX + else if (avail < availLimit) +;//XXX dsyslog(LOG_INFO, "available buffer data dipped into limit (%d < %d)", avail, availLimit); + } + if (avail == 0) + return 0; // the buffer is empty + int n = Writeable(); + if (Max > 0 && n > Max) + n = Max; + int w = write(*outFile, buffer + head, n); + if (w > 0) { + head += w; + if (head > size) + esyslog(LOG_ERR, "ERROR: ooops: buffer head (%d) exceeds size (%d)", head, size); + if (head >= size) + head = 0; + } + else if (w < 0) { + if (errno != EAGAIN) + LOG_ERROR; + else + w = 0; + } + return w; + } + return -1; +} + +// --- cFileBuffer ----------------------------------------------------------- + +class cFileBuffer : public cRingBuffer { +protected: + cIndexFile *index; + uchar fileNumber; + char *fileName, *pFileNumber; + bool stop; + int GetAvPesLength(void) + { + if (Byte(0) == 'A' && Byte(1) == 'V' && Byte(4) == 'U') + return (Byte(6) << 8) + Byte(7) + AV_PES_HEADER_LEN; + return 0; + } + int GetAvPesType(void) { return Byte(2); } + int GetAvPesTag(void) { return Byte(3); } + int FindAvPesBlock(void); + int GetPictureType(int Offset) { return (Byte(Offset + 5) >> 3) & 0x07; } + int FindPictureStartCode(int Length); +public: + cFileBuffer(int *InFile, int *OutFile, const char *FileName, bool Recording, int Size, int FreeLimit = 0, int AvailLimit = 0); + virtual ~cFileBuffer(); + void Stop(void) { stop = true; } + }; + +cFileBuffer::cFileBuffer(int *InFile, int *OutFile, const char *FileName, bool Recording, int Size, int FreeLimit = 0, int AvailLimit = 0) +:cRingBuffer(InFile, OutFile, Size, FreeLimit, AvailLimit) +{ + index = NULL; + fileNumber = 0; + stop = false; + // Prepare the file name: + fileName = new char[strlen(FileName) + RECORDFILESUFFIXLEN]; + if (!fileName) { + esyslog(LOG_ERR, "ERROR: can't copy file name '%s'", fileName); + return; + } + strcpy(fileName, FileName); + pFileNumber = fileName + strlen(fileName); + // Create the index file: + index = new cIndexFile(FileName, Recording); + if (!index) + esyslog(LOG_ERR, "ERROR: can't allocate index"); + // let's continue without index, so we'll at least have the recording +} + +cFileBuffer::~cFileBuffer() +{ + delete index; + delete fileName; +} + +int cFileBuffer::FindAvPesBlock(void) +{ + int Skipped = 0; + + while (Available() > MINVIDEODATA) { + if (GetAvPesLength()) + return Skipped; + Skip(1); + Skipped++; } - else { - Interface.Error("Can't write to file!"); + return -1; +} + +int cFileBuffer::FindPictureStartCode(int Length) +{ + for (int i = AV_PES_HEADER_LEN; i < Length; i++) { + if (Byte(i) == 0 && Byte(i + 1) == 0 && Byte(i + 2) == 1 && Byte(i + 3) == 0) + return i; + } + return -1; +} + +// --- cRecordBuffer --------------------------------------------------------- + +class cRecordBuffer : public cFileBuffer { +private: + uchar pictureType; + int fileSize; + int recordFile; + uchar tagAudio, tagVideo; + bool ok, synced; + int Synchronize(void); + bool NextFile(void); + virtual int Write(int Max = -1); +public: + cRecordBuffer(int *InFile, const char *FileName); + virtual ~cRecordBuffer(); + int WriteWithTimeout(void); + }; + +cRecordBuffer::cRecordBuffer(int *InFile, const char *FileName) +:cFileBuffer(InFile, &recordFile, FileName, true, VIDEOBUFSIZE, VIDEOBUFSIZE / 10, 0) +{ + pictureType = NO_PICTURE; + fileSize = 0; + recordFile = -1; + tagAudio = tagVideo = 0; + ok = synced = false; + if (!fileName) + return;//XXX find a better way??? + // Find the highest existing file suffix: + for (;;) { + sprintf(pFileNumber, RECORDFILESUFFIX, ++fileNumber); + if (access(fileName, F_OK) < 0) { + if (errno == ENOENT) + break; // found a non existing file suffix + LOG_ERROR; + return; + } + } + ok = true; + //XXX hack to make the video device go into 'recording' mode: + char dummy; + read(*InFile, &dummy, sizeof(dummy)); +} + +cRecordBuffer::~cRecordBuffer() +{ + if (recordFile >= 0) + close(recordFile); +} + +int cRecordBuffer::Synchronize(void) +{ + int Length = 0; + int Skipped = 0; + int s; + + while ((s = FindAvPesBlock()) >= 0) { + pictureType = NO_PICTURE; + Skipped += s; + Length = GetAvPesLength(); + if (Length <= MINVIDEODATA) { + switch (GetAvPesType()) { + case AV_PES_VIDEO: + { + int PictureOffset = FindPictureStartCode(Length); + if (PictureOffset >= 0) { + pictureType = GetPictureType(PictureOffset); + if (pictureType < I_FRAME || pictureType > B_FRAME) + esyslog(LOG_ERR, "ERROR: unknown picture type '%d'", pictureType); + } + } + if (!synced) { + tagVideo = GetAvPesTag(); + if (pictureType == I_FRAME) { + synced = true; + Skipped = 0; + } + else { + Skip(Length - 1); + Length = 0; + } + } + else { + if (++tagVideo != GetAvPesTag()) { + esyslog(LOG_ERR, "ERROR: missed video data block #%d (next block is #%d)", tagVideo, GetAvPesTag()); + tagVideo = GetAvPesTag(); + } + } + break; + case AV_PES_AUDIO: + if (!synced) { + tagAudio = GetAvPesTag(); + Skip(Length - 1); + Length = 0; + } + else { + //XXX might get fooled the first time!!! + if (++tagAudio != GetAvPesTag()) { + esyslog(LOG_ERR, "ERROR: missed audio data block #%d (next block is #%d)", tagAudio, GetAvPesTag()); + tagAudio = GetAvPesTag(); + } + } + break; + default: // unknown data + Length = 0; // don't skip entire block - maybe we're just not in sync with AV_PES yet + if (synced) + esyslog(LOG_ERR, "ERROR: unknown data type '%d'", GetAvPesType()); + } + if (Length > 0) + break; + } + else if (synced) { + esyslog(LOG_ERR, "ERROR: block length too big (%d)", Length); + Length = 0; + } + Skip(1); + Skipped++; + } + if (synced && Skipped) + esyslog(LOG_ERR, "ERROR: skipped %d bytes", Skipped); + return Length; +} + +bool cRecordBuffer::NextFile(void) +{ + if (recordFile >= 0 && fileSize > MAXVIDEOFILESIZE && pictureType == I_FRAME) { + if (close(recordFile) < 0) + LOG_ERROR; + // don't return 'false', maybe we can still record into the next file + recordFile = -1; + fileNumber++; + if (fileNumber == 0) + esyslog(LOG_ERR, "ERROR: max number of files (%d) exceeded", MAXFILESPERRECORDING); + fileSize = 0; + } + if (recordFile < 0) { + sprintf(pFileNumber, RECORDFILESUFFIX, fileNumber); + dsyslog(LOG_INFO, "recording to '%s'", fileName); + recordFile = open(fileName, O_RDWR | O_CREAT | O_NONBLOCK, S_IRUSR | S_IWUSR); + if (recordFile < 0) { + LOG_ERROR; return false; } } - // TODO - return false; + return true; } -bool cDvbRecorder::Play(const char *FileName, int Frame) +int cRecordBuffer::Write(int Max) { - if (!recording) { - isyslog(LOG_INFO, "play %s (%d)", FileName, Frame); - // TODO - Interface.Error("Playback not yet implemented!"); - return true; + // This function ignores the incoming 'Max'! + // It tries to write out exactly *one* block of AV_PES data. + if (!ok) + return -1; + int n = Synchronize(); + if (n) { + if (stop && pictureType == I_FRAME) { + ok = false; + return -1; // finish the recording before the next 'I' frame + } + if (NextFile()) { + if (index && pictureType != NO_PICTURE) + index->Write(pictureType, fileNumber, fileSize); + int written = 0; + for (;;) { + int w = cFileBuffer::Write(n); + if (w >= 0) { + fileSize += w; + written += w; + n -= w; + if (n == 0) + return written; + } + else + return w; + } + } + return -1; } - return false; + return 0; } -bool cDvbRecorder::FastForward(void) +int cRecordBuffer::WriteWithTimeout(void) { - isyslog(LOG_INFO, "fast forward"); - // TODO - return false; + int w, written = 0; + int t0 = time_ms(); + while ((w = Write()) > 0 && time_ms() - t0 < MAXRECORDWRITETIME) + written += w; + return w < 0 ? w : written; +} + +// --- cReplayBuffer --------------------------------------------------------- + +enum eReplayMode { rmPlay, rmFastForward, rmFastRewind }; + +class cReplayBuffer : public cFileBuffer { +private: + int fileOffset; + int replayFile; + eReplayMode mode; + bool skipAudio; + int lastIndex; + void SkipAudioBlocks(void); + bool NextFile(uchar FileNumber = 0, int FileOffset = -1); + void Close(void); +public: + cReplayBuffer(int *OutFile, const char *FileName); + virtual ~cReplayBuffer(); + virtual int Read(int Max = -1); + virtual int Write(int Max = -1); + void SetMode(eReplayMode Mode); + int Resume(void); + bool Save(void); + void SkipSeconds(int Seconds); + }; + +cReplayBuffer::cReplayBuffer(int *OutFile, const char *FileName) +:cFileBuffer(&replayFile, OutFile, FileName, false, VIDEOBUFSIZE, 0, VIDEOBUFSIZE / 10) +{ + fileOffset = 0; + replayFile = -1; + mode = rmPlay; + skipAudio = false; + lastIndex = -1; + if (!fileName) + return;//XXX find a better way??? + // All recordings start with '1': + fileNumber = 1; //TODO what if it doesn't start with '1'??? + //XXX hack to make the video device go into 'replaying' mode: + char dummy; + write(*OutFile, &dummy, sizeof(dummy)); +} + +cReplayBuffer::~cReplayBuffer() +{ + Close(); } -bool cDvbRecorder::FastRewind(void) +void cReplayBuffer::Close(void) { - isyslog(LOG_INFO, "fast rewind"); - // TODO + if (replayFile >= 0) { + if (close(replayFile) < 0) + LOG_ERROR; + replayFile = -1; + fileOffset = 0; + } +} + +void cReplayBuffer::SetMode(eReplayMode Mode) +{ + mode = Mode; + skipAudio = Mode != rmPlay; + if (mode != rmPlay) + Clear(); +} + +int cReplayBuffer::Resume(void) +{ + if (index) { + int Index = index->GetResume(); + if (Index >= 0) { + uchar FileNumber; + int FileOffset; + if (index->Get(Index, &FileNumber, &FileOffset) && NextFile(FileNumber, FileOffset)) + return Index; + } + } + return -1; +} + +bool cReplayBuffer::Save(void) +{ + if (index) { + int Index = index->Get(fileNumber, fileOffset); + if (Index >= 0) { + Index -= RESUMEBACKUP; + if (Index > 0) { + uchar FileNumber; + int FileOffset; + Index = index->GetNextIFrame(Index, false, &FileNumber, &FileOffset); + } + else + Index = 0; + if (Index >= 0) + return index->StoreResume(Index); + } + } return false; } -bool cDvbRecorder::Pause(void) +void cReplayBuffer::SkipSeconds(int Seconds) +{ + if (index && Seconds) { + int Index = index->Get(fileNumber, fileOffset); + if (Index >= 0) { + if (Seconds < 0) { + int sec = index->Last() / FRAMESPERSEC; + if (Seconds < -sec) + Seconds = - sec; + } + Index += Seconds * FRAMESPERSEC; + if (Index < 0) + Index = 1; + uchar FileNumber; + int FileOffset; + if (index->GetNextIFrame(Index, false, &FileNumber, &FileOffset) >= 0) + NextFile(FileNumber, FileOffset); + } + } +} + +void cReplayBuffer::SkipAudioBlocks(void) { - isyslog(LOG_INFO, "pause"); - // TODO + int Length; + + while ((Length = GetAvPesLength()) > 0) { + if (GetAvPesType() == AV_PES_AUDIO) + Skip(Length); + else + break; + } +} + +bool cReplayBuffer::NextFile(uchar FileNumber, int FileOffset) +{ + if (FileNumber > 0) { + Clear(); + if (FileNumber != fileNumber) { + Close(); + fileNumber = FileNumber; + } + } + if (replayFile >= 0 && EndOfFile()) { + Close(); + fileNumber++; + if (fileNumber == 0) + esyslog(LOG_ERR, "ERROR: max number of files (%d) exceeded", MAXFILESPERRECORDING); + } + if (replayFile < 0) { + sprintf(pFileNumber, RECORDFILESUFFIX, fileNumber); + if (access(fileName, R_OK) == 0) { + dsyslog(LOG_INFO, "playing '%s'", fileName); + replayFile = open(fileName, O_RDONLY | O_NONBLOCK); + if (replayFile < 0) { + LOG_ERROR; + return false; + } + } + else if (errno != ENOENT) + LOG_ERROR; + } + if (replayFile >= 0) { + if (FileOffset >= 0) { + if ((fileOffset = lseek(replayFile, FileOffset, SEEK_SET)) != FileOffset) + LOG_ERROR; + // don't return 'false', maybe we can still replay the file + } + return true; + } return false; } -void cDvbRecorder::Stop(void) +int cReplayBuffer::Read(int Max = -1) { - isyslog(LOG_INFO, "stop"); - recording = false; - // TODO + if (stop) + return -1; + if (mode != rmPlay) { + if (index) { + if (Available()) + return 0; // write out the entire block + int Index = (lastIndex >= 0) ? lastIndex : index->Get(fileNumber, fileOffset); + if (Index >= 0) { + uchar FileNumber; + int FileOffset, Length; + Index = index->GetNextIFrame(Index, mode == rmFastForward, &FileNumber, &FileOffset, &Length); + if (Index >= 0) { + if (!NextFile(FileNumber, FileOffset)) + return -1; + Max = Length; + } + lastIndex = Index; + } + if (Index < 0) { + // This results in normal replay after a fast rewind. + // After a fast forward it will stop. + // TODO Could we cause it to pause at the last frame? + SetMode(rmPlay); + return 0; + } + } + } + else + lastIndex = -1; + //XXX timeout as in recording??? + if (NextFile()) { + int readin = 0; + do { + // If Max is > 0 here we need to make sure we read in the entire block! + int r = cFileBuffer::Read(Max); + if (r >= 0) + readin += r; + else + return -1; + } while (readin < Max && Free() > 0); + return readin; + } + if (Available() > 0) + return 0; + return -1; } -int cDvbRecorder::Frame(void) +int cReplayBuffer::Write(int Max) { - isyslog(LOG_INFO, "frame"); - // TODO - return 0; + int Written = 0; + + do { + if (skipAudio) { + SkipAudioBlocks(); + Max = GetAvPesLength(); + } + while (Max) { + int w = cFileBuffer::Write(Max); + if (w >= 0) { + fileOffset += w; + Written += w; + if (Max < 0) + break; + Max -= w; + } + else + return w; + //XXX??? Why does the buffer get empty here??? + if (Empty() || !WaitForOutFile(1000000)) + return Written; + } + } while (skipAudio && Available()); + return Written; } -// --- cDvbOsd --------------------------------------------------------------- +// --- cDvbApi --------------------------------------------------------------- -cDvbOsd::cDvbOsd(void) +cDvbApi::cDvbApi(void) { + isMainProcess = true; + pidRecord = pidReplay = 0; + fromRecord = toRecord = -1; + fromReplay = toReplay = -1; + videoDev = open(VIDEODEVICE, O_RDWR | O_NONBLOCK); + if (videoDev < 0) + LOG_ERROR; cols = rows = 0; -#ifdef DEBUG_OSD - memset(&colorPairs, 0, sizeof(colorPairs)); +#if defined(DEBUG_OSD) || defined(DEBUG_REMOTE) initscr(); - start_color(); keypad(stdscr, TRUE); nonl(); cbreak(); noecho(); timeout(1000); +#endif +#if defined(DEBUG_OSD) + memset(&colorPairs, 0, sizeof(colorPairs)); + start_color(); leaveok(stdscr, TRUE); window = stdscr; #endif -#ifdef DEBUG_REMOTE - initscr(); - keypad(stdscr, TRUE); - nonl(); - cbreak(); - noecho(); - timeout(1000); -#endif } -cDvbOsd::~cDvbOsd() +cDvbApi::~cDvbApi() { - Close(); + if (isMainProcess) { + if (videoDev >= 0) { + Close(); + StopReplay(); + StopRecord(); + close(videoDev); + } +#if defined(DEBUG_REMOTE) || defined(DEBUG_OSD) + endwin(); +#endif + } } #ifdef DEBUG_OSD -void cDvbOsd::SetColor(eDvbColor colorFg, eDvbColor colorBg) +void cDvbApi::SetColor(eDvbColor colorFg, eDvbColor colorBg) { int color = (colorBg << 16) | colorFg | 0x80000000; for (int i = 0; i < MaxColorPairs; i++) { @@ -185,11 +1038,9 @@ void cDvbOsd::SetColor(eDvbColor colorFg, eDvbColor colorBg) } } #else -void cDvbOsd::Cmd(OSD_Command cmd, int color, int x0, int y0, int x1, int y1, const void *data) +void cDvbApi::Cmd(OSD_Command cmd, int color, int x0, int y0, int x1, int y1, const void *data) { - int v = open(VIDEODEVICE, O_RDWR); - - if (v >= 0) { + if (videoDev >= 0) { struct drawcmd dc; dc.cmd = cmd; dc.color = color; @@ -198,15 +1049,12 @@ void cDvbOsd::Cmd(OSD_Command cmd, int color, int x0, int y0, int x1, int y1, co dc.x1 = x1; dc.y1 = y1; dc.data = (void *)data; - ioctl(v, VIDIOCSOSDCOMMAND, &dc); - close(v); + ioctl(videoDev, VIDIOCSOSDCOMMAND, &dc); } - else - Interface.Error("can't open VIDEODEVICE");//XXX } #endif -void cDvbOsd::Open(int w, int h) +void cDvbApi::Open(int w, int h) { cols = w; rows = h; @@ -233,14 +1081,14 @@ void cDvbOsd::Open(int w, int h) SETCOLOR(clrWhite, 0xFC, 0xFC, 0xFC, 255); } -void cDvbOsd::Close(void) +void cDvbApi::Close(void) { #ifndef DEBUG_OSD Cmd(OSD_Close); #endif } -void cDvbOsd::Clear(void) +void cDvbApi::Clear(void) { #ifdef DEBUG_OSD SetColor(clrBackground, clrBackground); @@ -250,7 +1098,7 @@ void cDvbOsd::Clear(void) #endif } -void cDvbOsd::Fill(int x, int y, int w, int h, eDvbColor color) +void cDvbApi::Fill(int x, int y, int w, int h, eDvbColor color) { if (x < 0) x = cols + x; if (y < 0) y = rows + y; @@ -265,12 +1113,12 @@ void cDvbOsd::Fill(int x, int y, int w, int h, eDvbColor color) #endif } -void cDvbOsd::ClrEol(int x, int y, eDvbColor color) +void cDvbApi::ClrEol(int x, int y, eDvbColor color) { Fill(x, y, cols - x, 1, color); } -void cDvbOsd::Text(int x, int y, const char *s, eDvbColor colorFg, eDvbColor colorBg) +void cDvbApi::Text(int x, int y, const char *s, eDvbColor colorFg, eDvbColor colorBg) { if (x < 0) x = cols + x; if (y < 0) y = rows + y; @@ -282,3 +1130,343 @@ void cDvbOsd::Text(int x, int y, const char *s, eDvbColor colorFg, eDvbColor col Cmd(OSD_Text, (int(colorBg) << 16) | colorFg, x * charWidth, y * lineHeight, 1, 0, s); #endif } + +bool cDvbApi::SetChannel(int FrequencyMHz, char Polarization, int Diseqc, int Srate, int Vpid, int Apid, int Ca, int Pnr) +{ + if (videoDev >= 0) { + struct frontend front; + ioctl(videoDev, VIDIOCGFRONTEND, &front); + unsigned int freq = FrequencyMHz; + front.ttk = (freq < 11800UL) ? 0 : 1; + if (freq < 11800UL) + freq -= 9750UL; + else + freq -= 10600UL; + front.channel_flags = Ca ? DVB_CHANNEL_CA : DVB_CHANNEL_FTA; + front.pnr = Pnr; + front.freq = freq * 1000000UL; + front.diseqc = Diseqc; + front.srate = Srate * 1000; + front.volt = (Polarization == 'v') ? 0 : 1; + front.video_pid = Vpid; + front.audio_pid = Apid; + front.fec = 8; + front.AFC = 1; + ioctl(videoDev, VIDIOCSFRONTEND, &front); + if (front.sync & 0x1F == 0x1F) + return true; + esyslog(LOG_ERR, "ERROR: channel not sync'ed (front.sync=%X)!", front.sync); + } + return false; +} + +void cDvbApi::KillProcess(pid_t pid) +{ + pid_t Pid2Wait4 = pid; + for (time_t t0 = time(NULL); time(NULL) - t0 < MAXPROCESSTIMEOUT; ) { + int status; + pid_t pid = waitpid(Pid2Wait4, &status, WNOHANG); + if (pid < 0) { + if (errno != ECHILD) + LOG_ERROR; + return; + } + if (pid == Pid2Wait4) + return; + } + esyslog(LOG_ERR, "ERROR: process %d won't end (waited %d seconds) - terminating it...", Pid2Wait4, MAXPROCESSTIMEOUT); + if (kill(Pid2Wait4, SIGTERM) < 0) { + esyslog(LOG_ERR, "ERROR: process %d won't terminate (%s) - killing it...", Pid2Wait4, strerror(errno)); + if (kill(Pid2Wait4, SIGKILL) < 0) + esyslog(LOG_ERR, "ERROR: process %d won't die (%s) - giving up", Pid2Wait4, strerror(errno)); + } +} + +bool cDvbApi::Recording(void) +{ + return pidRecord; +} + +bool cDvbApi::Replaying(void) +{ + return pidReplay; +} + +bool cDvbApi::StartRecord(const char *FileName) +{ + if (Recording()) { + esyslog(LOG_ERR, "ERROR: StartRecord() called while recording - ignored!"); + return false; + } + if (videoDev >= 0) { + + // Check FileName: + + if (!FileName) { + esyslog(LOG_ERR, "ERROR: StartRecord: file name is (null)"); + return false; + } + isyslog(LOG_INFO, "record %s", FileName); + + // Create directories if necessary: + + if (!MakeDirs(FileName, true)) + return false; + + // Open pipes for recording process: + + int fromRecordPipe[2], toRecordPipe[2]; + + if (pipe(fromRecordPipe) != 0) { + LOG_ERROR; + return false; + } + if (pipe(toRecordPipe) != 0) { + LOG_ERROR; + return false; + } + + // Create recording process: + + pidRecord = fork(); + if (pidRecord < 0) { + LOG_ERROR; + return false; + } + if (pidRecord == 0) { + + // This is the actual recording process + + dsyslog(LOG_INFO, "start recording process (pid=%d)", getpid()); + isMainProcess = false; + int fromMain = toRecordPipe[0]; + int toMain = fromRecordPipe[1]; + cRecordBuffer *Buffer = new cRecordBuffer(&videoDev, FileName); + if (Buffer) { + for (;;) { + fd_set set; + FD_ZERO(&set); + FD_SET(videoDev, &set); + FD_SET(fromMain, &set); + struct timeval timeout; + timeout.tv_sec = 1; + timeout.tv_usec = 0; + if (select(FD_SETSIZE, &set, NULL, NULL, &timeout) > 0) { + if (FD_ISSET(videoDev, &set)) { + if (Buffer->Read() < 0) + break; + } + if (FD_ISSET(fromMain, &set)) { + switch (readchar(fromMain)) { + case dvbStop: Buffer->Stop(); break; + break; + } + } + } + else + esyslog(LOG_ERR, "ERROR: video data stream broken"); + if (Buffer->WriteWithTimeout() < 0) + break; + } + delete Buffer; + } + else + esyslog(LOG_ERR, "ERROR: can't allocate recording buffer"); + close(fromMain); + close(toMain); + dsyslog(LOG_INFO, "end recording process"); + exit(0); + } + + // Establish communication with the recording process: + + fromRecord = fromRecordPipe[0]; + toRecord = toRecordPipe[1]; + return true; + } + return false; +} + +void cDvbApi::StopRecord(void) +{ + if (pidRecord) { + writechar(toRecord, dvbStop); + close(toRecord); + close(fromRecord); + toRecord = fromRecord = -1; + KillProcess(pidRecord); + pidRecord = 0; + SetReplayMode(VID_PLAY_RESET); //XXX + } +} + +void cDvbApi::SetReplayMode(int Mode) +{ + if (videoDev >= 0) { + struct video_play_mode pmode; + pmode.mode = Mode; + ioctl(videoDev, VIDIOCSPLAYMODE, &pmode); + } +} + +bool cDvbApi::StartReplay(const char *FileName) +{ + if (Recording()) { + esyslog(LOG_ERR, "ERROR: StartReplay() called while recording - ignored!"); + return false; + } + StopReplay(); + if (videoDev >= 0) { + + // Check FileName: + + if (!FileName) { + esyslog(LOG_ERR, "ERROR: StartReplay: file name is (null)"); + return false; + } + isyslog(LOG_INFO, "replay %s", FileName); + + // Open pipes for replay process: + + int fromReplayPipe[2], toReplayPipe[2]; + + if (pipe(fromReplayPipe) != 0) { + LOG_ERROR; + return false; + } + if (pipe(toReplayPipe) != 0) { + LOG_ERROR; + return false; + } + + // Create replay process: + + pidReplay = fork(); + if (pidReplay < 0) { + LOG_ERROR; + return false; + } + if (pidReplay == 0) { + + // This is the actual replaying process + + dsyslog(LOG_INFO, "start replaying process (pid=%d)", getpid()); + isMainProcess = false; + int fromMain = toReplayPipe[0]; + int toMain = fromReplayPipe[1]; + cReplayBuffer *Buffer = new cReplayBuffer(&videoDev, FileName); + if (Buffer) { + bool Paused = false; + bool FastForward = false; + bool FastRewind = false; + int ResumeIndex = Buffer->Resume(); + if (ResumeIndex >= 0) + isyslog(LOG_INFO, "resuming replay at index %d (%s)", ResumeIndex, cIndexFile::Str(ResumeIndex)); + for (;;) { + if (Buffer->Read() < 0) + break; + fd_set setIn, setOut; + FD_ZERO(&setIn); + FD_ZERO(&setOut); + FD_SET(fromMain, &setIn); + FD_SET(videoDev, &setOut); + struct timeval timeout; + timeout.tv_sec = 1; + timeout.tv_usec = 0; + if (select(FD_SETSIZE, &setIn, &setOut, NULL, &timeout) > 0) { + if (FD_ISSET(videoDev, &setOut)) { + if (Buffer->Write() < 0) + break; + } + if (FD_ISSET(fromMain, &setIn)) { + switch (readchar(fromMain)) { + case dvbStop: Buffer->Stop(); break; + case dvbPauseReplay: SetReplayMode(Paused ? VID_PLAY_NORMAL : VID_PLAY_PAUSE); + Paused = !Paused; + FastForward = FastRewind = false; + Buffer->SetMode(rmPlay); + break; + case dvbFastForward: SetReplayMode(VID_PLAY_NORMAL); + FastForward = !FastForward; + FastRewind = Paused = false; + Buffer->SetMode(FastForward ? rmFastForward : rmPlay); + break; + case dvbFastRewind: SetReplayMode(VID_PLAY_NORMAL); + FastRewind = !FastRewind; + FastForward = Paused = false; + Buffer->SetMode(FastRewind ? rmFastRewind : rmPlay); + break; + case dvbSkip: { + int Seconds; + if (readint(fromMain, Seconds)) { + SetReplayMode(VID_PLAY_NORMAL); + FastForward = FastRewind = Paused = false; + Buffer->SetMode(rmPlay); + Buffer->SkipSeconds(Seconds); + } + } + break; + } + } + } + } + Buffer->Save(); + delete Buffer; + } + else + esyslog(LOG_ERR, "ERROR: can't allocate replaying buffer"); + close(fromMain); + close(toMain); + SetReplayMode(VID_PLAY_RESET); //XXX + dsyslog(LOG_INFO, "end replaying process"); + exit(0); + } + + // Establish communication with the replay process: + + fromReplay = fromReplayPipe[0]; + toReplay = toReplayPipe[1]; + return true; + } + return false; +} + +void cDvbApi::StopReplay(void) +{ + if (pidReplay) { + writechar(toReplay, dvbStop); + close(toReplay); + close(fromReplay); + toReplay = fromReplay = -1; + KillProcess(pidReplay); + pidReplay = 0; + SetReplayMode(VID_PLAY_RESET); //XXX + } +} + +void cDvbApi::PauseReplay(void) +{ + if (pidReplay) + writechar(toReplay, dvbPauseReplay); +} + +void cDvbApi::FastForward(void) +{ + if (pidReplay) + writechar(toReplay, dvbFastForward); +} + +void cDvbApi::FastRewind(void) +{ + if (pidReplay) + writechar(toReplay, dvbFastRewind); +} + +void cDvbApi::Skip(int Seconds) +{ + if (pidReplay) { + writechar(toReplay, dvbSkip); + writeint(toReplay, Seconds); + } +} + |