/*************************************************************************** * Copyright (c) 2003,2004 by Marcel Wiesweg * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include #include "txtrecv.h" #include "tables.h" #include "setup.h" #include #include #include #include #include #include #include #include #include #include const char *RootDir::root = "/var/cache/vdr/vtx"; void RootDir::setRootDir(const char *newRoot) { root=newRoot; } const char *RootDir::getRootDir() { return root; } int Storage::doCleanUp() { DIR *top=opendir(root); int pagesDeleted=0; if (top) { struct dirent *chandir, path; struct stat chandirstat; char fullPath[PATH_MAX]; while ( (!readdir_r(top, &path, &chandir) && chandir != NULL) ) { if (strcmp(chandir->d_name, "..")==0 || strcmp(chandir->d_name, ".")==0) continue; snprintf(fullPath, PATH_MAX, "%s/%s", root, chandir->d_name); if (stat(fullPath, &chandirstat)==0) { if (S_ISDIR(chandirstat.st_mode)) { pagesDeleted+=cleanSubDir(fullPath); } } } closedir(top); } else { esyslog("Error opening teletext storage directory \"%s\": %s", root, strerror(errno)); } return pagesDeleted; } int Storage::cleanSubDir(const char *dir) { static bool reportedError=false; //avoid filling up syslog DIR *d=opendir(dir); bool hadError=false; int bytesDeleted=0, filesize; if (d) { struct dirent *txtfile, path; struct stat txtfilestat; char fullPath[PATH_MAX]; while ( (!readdir_r(d, &path, &txtfile) && txtfile != NULL) ) { int len=strlen(txtfile->d_name); //check that the file end with .vtx to avoid accidents and disasters if (strcmp(txtfile->d_name+len-4, ".vtx")==0) { snprintf(fullPath, PATH_MAX, "%s/%s", dir, txtfile->d_name); stat(fullPath, &txtfilestat); filesize=actualFileSize(txtfilestat.st_size); int ret=unlink(fullPath); if (ret==0) bytesDeleted+=filesize; else hadError=ret; } } closedir(d); rmdir(dir); } else { if (!reportedError) { esyslog("OSD-Teletext: Error opening teletext storage subdirectory \"%s\": %s", dir, strerror(errno)); reportedError=true; } } if (hadError && !reportedError) { esyslog("OSD-Teletext: Error removing teletext storage subdirectory \"%s\": %s", dir, strerror(hadError)); reportedError=true; } return bytesDeleted; } Storage *Storage::s_self = 0; Storage::StorageSystem Storage::system = Storage::StorageSystemPacked; Storage::Storage() { s_self=this; byteCount=0; currentDir=0; storageOption=-1; failedFreeSpace=false; } Storage::~Storage() { } void Storage::setSystem(StorageSystem s) { system=s; } Storage *Storage::instance() { if (!s_self) { switch (system) { case StorageSystemLegacy: s_self=new LegacyStorage(); break; case StorageSystemPacked: default: s_self=new PackedStorage(); break; } } return s_self; } void Storage::setMaxStorage(int maxMB) { storageOption=maxMB; } void Storage::init() { cleanUp(); initMaxStorage(storageOption); } void Storage::freeSpace() { //there might be a situation where only the current directory is left and //occupies the whole space. We cannot delete anything. Don't waste time scanning. if (failedFreeSpace) return; //printf("freeSpace()\n"); time_t min=time(0); char minDir[PATH_MAX]; char fullPath[PATH_MAX]; int haveDir=0; DIR *top=opendir(getRootDir()); if (top) { struct dirent *chandir, path; struct stat chandirstat; while ( (!readdir_r(top, &path, &chandir) && chandir != NULL) ) { if (strcmp(chandir->d_name, "..")==0 || strcmp(chandir->d_name, ".")==0) continue; snprintf(fullPath, PATH_MAX, "%s/%s", getRootDir(), chandir->d_name); if (stat(fullPath, &chandirstat)==0) { if (S_ISDIR(chandirstat.st_mode)) { if (chandirstat.st_ctime < min && strcmp(fullPath, currentDir)) { min=chandirstat.st_ctime; strcpy(minDir, fullPath); haveDir++; } } } } closedir(top); //if haveDir, only current directory present, which must not be deleted if (haveDir>=2) byteCount-=cleanSubDir(minDir); else failedFreeSpace=true; } } bool Storage::exists(const char* file) { struct stat s; return (stat(file, &s)==0); } void Storage::getFilename(char *buffer, int bufLength, PageID page) { snprintf(buffer, bufLength, "%s/%s/%03x_%02x.vtx", getRootDir(), #if VDRVERSNUM >= 10318 *page.channel.ToString(), #else page.channel.ToString(), #endif page.page, page.subPage); } void Storage::prepareDirectory(tChannelID chan) { free(currentDir); asprintf(¤tDir, "%s/%s", root, #if VDRVERSNUM >= 10318 *chan.ToString() #else chan.ToString() #endif ); MakeDirs(currentDir, 1); failedFreeSpace=false; } #define TELETEXT_PAGESIZE 972 LegacyStorage::LegacyStorage() { maxBytes=0; fsBlockSize=1; pageBytes=TELETEXT_PAGESIZE; } LegacyStorage::~LegacyStorage() { } /* static inline int FilesForMegabytes(double MB, int blocksize) { double pageBytes; if (TELETEXT_PAGESIZE<=blocksize) pageBytes=blocksize; else pageBytes=((TELETEXT_PAGESIZE/blocksize)+1)*blocksize; //reserve 10% for directories return (int)( (1024.0 * 1024.0 * (MB-MB*0.1)) / pageBytes ); }*/ int LegacyStorage::actualFileSize(int netFileSize) { if (netFileSize<=0) return 0; if (netFileSize<=fsBlockSize) return fsBlockSize; else return ((netFileSize/fsBlockSize)+1)*fsBlockSize; } //max==0 means unlimited, max==-1 means a reasonable default value shall be calculated void LegacyStorage::initMaxStorage(int maxMB) { struct statfs fs; if (statfs(getRootDir(), &fs)!=0) { esyslog("OSD-Teletext: Error statfs'ing root directory \"%s\": %s, cache size uncontrolled", getRootDir(), strerror(errno)); return; } fsBlockSize=fs.f_bsize; pageBytes=actualFileSize(TELETEXT_PAGESIZE); if (maxMB>=0) { if (maxMB<3) { esyslog("OSD-Teletext: Request to use at most %d MB for caching. This is not enough, using 3 MB", maxMB); maxMB=3; } maxBytes=MEGABYTE(maxMB); } else if (maxMB==-1) { //calculate a default value double blocksPerMeg = 1024.0 * 1024.0 / fs.f_bsize; double capacityMB=fs.f_blocks / blocksPerMeg; double freeMB=(fs.f_bavail / blocksPerMeg); if (capacityMB<=50 || freeMB<50) { //small (<=50MB) filesystems as root dir are assumed to be dedicated for use as a teletext cache //for others, the maximum default size is set to 50 MB maxBytes=MEGABYTE((int)freeMB); //maxPages= FilesForMegabytes(freeMB, fs.f_bsize); if (freeMB<3.0) { esyslog("OSD-Teletext: Less than %.1f MB free on filesystem of root directory \"%s\"!", freeMB, getRootDir()); maxBytes=MEGABYTE(3); } } else { //the maximum default size is set to 50 MB maxBytes=MEGABYTE(50); } //printf("Set maxBytes to %ld, %.2f %.2f\n", maxBytes, capacityMB, freeMB); } } void LegacyStorage::cleanUp() { byteCount -= Storage::doCleanUp(); } void LegacyStorage::registerFile(PageID page) { //pageBytes is already effective size if ( maxBytes && (byteCount+=pageBytes)>maxBytes ) freeSpace(); } StorageHandle LegacyStorage::openForReading(PageID page, bool countAsAccess) { //the countAsAccess argument was intended for use in a LRU cache, currently unused char filename[PATH_MAX]; getFilename(filename, sizeof(filename), page); StorageHandle ret=(StorageHandle)open(filename, O_RDONLY); return ret; } StorageHandle LegacyStorage::openForWriting(PageID page) { static bool wroteError=false; char filename[PATH_MAX]; getFilename(filename, sizeof(filename), page); bool existed=exists(filename); //first try StorageHandle fd=(StorageHandle)open(filename, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); if (fd) { if (!existed) registerFile(page); return fd; } //no space on disk? make some space available if (errno == ENOSPC) freeSpace(); //second try fd=(StorageHandle)open(filename, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); if (!fd && !wroteError) { //report error to syslog - once! wroteError=true; esyslog("OSD-Teletext: Error opening teletext file %s: %s", filename, strerror(errno)); } //make sure newly created files are counted if (fd && !existed) registerFile(page); return fd; } ssize_t LegacyStorage::write(const void *ptr, size_t size, StorageHandle stream) { ssize_t written; if (!(written=::write((int)stream, ptr, size)) ) { switch (errno) { case ENOSPC: freeSpace(); return ::write((int)stream, ptr, size); case EINTR: esyslog("OSD-Teletext: EINTR while writing. Please contact the author and tell him this happened."); break; default: break; } } return written; } PackedStorage::PackedStorage() { } #define TOC_SIZE 8 //The file structure is simple: // TOC_SIZE*PageAddress contains the numbers of the following pages // TOC_SIZE*TELETEXT_PAGESIZE contains the page data //and the same again. bool PackedStorage::seekTo(PageID page, int desc, bool create) { lseek(desc, 0, SEEK_SET); PageAddress addr[TOC_SIZE]; while (::read(desc, addr, sizeof(addr)) == sizeof(addr)) { lseek(desc, 0, SEEK_CUR); for (int index=0; index= 10318 *page.channel.ToString(), #else page.channel.ToString(), #endif (page.page & 0xFF0)); } StorageHandle PackedStorage::openForWriting(PageID page) { static bool wroteError=false; char filename[PATH_MAX]; getFilename(filename, sizeof(filename), page); //first try int desc=open(filename, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); if (desc != -1) { if (!seekTo(page, desc, true)) { ::close(desc); return StorageHandle(); } if ( maxBytes && byteCount>maxBytes ) freeSpace(); return (StorageHandle)desc; } //no space on disk? make some space available if (errno == ENOSPC) freeSpace(); //second try desc=open(filename, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); if (desc==-1 && !wroteError) { //report error to syslog - once! wroteError=true; esyslog("OSD-Teletext: Error opening teletext file %s: %s", filename, strerror(errno)); } if (desc==-1) return StorageHandle(); else if (!seekTo(page, desc, true)) { ::close(desc); return StorageHandle(); } if ( maxBytes && byteCount>maxBytes ) freeSpace(); return (StorageHandle)desc; } StorageHandle PackedStorage::openForReading(PageID page, bool countAsAccess) { int desc; if ( (desc=(int)LegacyStorage::openForReading(page, false))!= -1 ) { if (!seekTo(page, desc, false)) { //this is not an error condition here, may and shall happen! ::close(desc); } else { return (StorageHandle)desc; } } return StorageHandle(); } cTelePage::cTelePage(PageID t_page, uchar t_flags, uchar t_lang,int t_mag) : mag(t_mag), flags(t_flags), lang(t_lang), page(t_page) { memset(pagebuf,' ',26*40); } cTelePage::~cTelePage() { } void cTelePage::SetLine(int line, uchar *myptr) { memcpy(pagebuf+40*line,myptr,40); } void cTelePage::save() { Storage *s=Storage::instance(); unsigned char buf; StorageHandle fd; if ( (fd=s->openForWriting(page)) ) { s->write("VTXV4",5,fd); buf=0x01; s->write(&buf,1,fd); buf=mag; s->write(&buf,1,fd); buf=page.page; s->write(&buf,1,fd); buf=flags; s->write(&buf,1,fd); buf=lang; s->write(&buf,1,fd); buf=0x00; s->write(&buf,1,fd); buf=0x00; s->write(&buf,1,fd); s->write(pagebuf,24*40,fd); s->close(fd); } } cTxtStatus::cTxtStatus(void) { receiver = NULL; //running=false; TPid=0; /*doNotSuspend=false; doNotReceive=false;*/ //suspended=false; } cTxtStatus::~cTxtStatus() { /*if (running) Cancel(3);*/ if (receiver) { receiver->Stop(); delete receiver; } } void cTxtStatus::ChannelSwitch(const cDevice *Device, int ChannelNumber) { if (Device->IsPrimaryDevice() || Device == cDevice::ActualDevice()) { /*#ifdef OSDTELETEXT_REINSERTION_PATCH if (ttSetup.suspendReceiving) { if (!running) Start(); } else if (running) { //setup option changed, apply running=false; Cancel(3); } #endif*/ CheckDeleteReceiver(); if (ChannelNumber) { cChannel *channel = Channels.GetByNumber(ChannelNumber); if (channel && channel->Tpid()) { /*#ifdef OSDTELETEXT_REINSERTION_PATCH cMutexLock MutexLock(&mutex); count=0; //reset 20 second intervall condVar.Broadcast(); //other thread is locked on the mutex until the end of this function! #endif */ TPid=channel->Tpid(); chan=channel->GetChannelID(); CheckCreateReceiver(); } } } } void cTxtStatus::CheckCreateReceiver() { if (!receiver && TPid ) { cChannel *channel = Channels.GetByChannelID(chan); if (!channel) return; //primary device a full-featured card if (cDevice::ActualDevice()->ProvidesChannel(channel, Setup.PrimaryLimit)) { receiver = new cTxtReceiver(TPid, chan); cDevice::ActualDevice()->AttachReceiver(receiver); //dsyslog("OSDTeletext: Created teletext receiver for channel %d, PID %d on primary device", ChNum, TPid); //primary device a DXR3 or similar } else { int devNum = cDevice::NumDevices(); bool bFound = false; cDevice* pDevice = 0; for (int i = 0; i < devNum && !bFound; ++i) { pDevice = cDevice::GetDevice(i); if (pDevice && pDevice->ProvidesChannel(channel, Setup.PrimaryLimit) && pDevice->Receiving(true)) { bFound = true; receiver = new cTxtReceiver(TPid, chan); pDevice->AttachReceiver(receiver); //dsyslog("OSDTeletext: Created teletext receiver for channel %d, PID %d on device %d", ChNum, TPid, i); } } if (!bFound) //can this happen? esyslog("OSDTeletext: Did not find appropriate device for teletext receiver for channel %s, PID %d", channel->Name(), TPid); } } } void cTxtStatus::CheckDeleteReceiver() { if (receiver) { //dsyslog("OSDTeletext: Deleted teletext receiver"); delete receiver; /*#ifdef OSDTELETEXT_REINSERTION_PATCH //the patch only makes sense if primary device is a DVB card, so no handling for DXR3 cDevice::PrimaryDevice()->ReinsertTeletextPid(TPid); #endif*/ receiver = NULL; } } /* //only used for suspending the receiver, if selected by user in setup void cTxtStatus::Action() { #ifdef OSDTELETEXT_REINSERTION_PATCH running=true; dsyslog("OSDTeletext waiting thread started with pid %d", getpid()); count=0; while (running) { cMutexLock MutexLock(&mutex); if (doNotSuspend) { CheckCreateReceiver(); count=0; } else if (doNotReceive) { CheckDeleteReceiver(); count=0; } else { count++; if (count <= 20) CheckCreateReceiver(); else if (count < 20+5*60) CheckDeleteReceiver(); else count=0; //if count=20+5*60 } condVar.TimedWait(mutex, 1000); //one second } running=false; dsyslog("OSDTeletext waiting thread ended"); #endif } //only has an effect when suspending is enabled: //prevents receiving from suspension when argument is true //reenables suspension when argument is false, // but does not necessarily suspend immediately, that is the task of ForceSuspending, // in contrast to which it does not make any sense if suspending is // not enabled. //In clear words: When the plugin is in use, it calls the function //with onOrOff=true so that data is received continously during the //TeletextBrowser object's lifetime. When it is destroyed, it releases //this constraint by calling onOrOff=false. void cTxtStatus::ForceReceiving(bool onOrOff) { #ifdef OSDTELETEXT_REINSERTION_PATCH if (!running) return; if (onOrOff && !doNotSuspend) { cMutexLock MutexLock(&mutex); doNotSuspend=true; condVar.Broadcast(); } else if (!onOrOff && doNotSuspend) { cMutexLock MutexLock(&mutex); doNotSuspend=false; condVar.Broadcast(); } #endif } //opposite as above: //allows to switch off receiving void cTxtStatus::ForceSuspending(bool onOrOff) { #ifdef OSDTELETEXT_REINSERTION_PATCH if (!running) { //thread is not running, suspend anyway if (onOrOff) { CheckDeleteReceiver(); } else { CheckCreateReceiver(); } } else { doNotSuspend=false; //ForceReceive may have been called before if (onOrOff && !doNotReceive) { cMutexLock MutexLock(&mutex); doNotReceive=true; condVar.Broadcast(); } else if (!onOrOff && doNotReceive) { cMutexLock MutexLock(&mutex); doNotReceive=false; condVar.Broadcast(); } } #endif } */ cTxtReceiver::cTxtReceiver(int TPid, tChannelID chan) #if VDRVERSNUM >= 10500 : cReceiver(chan, -1, TPid), #elif VDRVERSNUM >= 10319 : cReceiver(0, -1, TPid), #else : cReceiver(0, -1, 1, TPid), #endif cThread("osdteletext-receiver"), chan(chan), TxtPage(0), buffer((188+60)*75), running(false) { Storage::instance()->prepareDirectory(chan); // 10 ms timeout on getting TS frames buffer.SetTimeouts(0, 10); } cTxtReceiver::~cTxtReceiver() { cReceiver::Detach(); if (running) { running=false; buffer.Signal(); Cancel(2); } buffer.Clear(); delete TxtPage; } void cTxtReceiver::Stop() { Activate(false); } void cTxtReceiver::Activate(bool On) { if (On) { if (!running) { running=true; Start(); } } else if (running) { running = false; buffer.Signal(); Cancel(2); } } void cTxtReceiver::Receive(uchar *Data, int Length) { int len = Length+60; if (!buffer.Check(len)) { // Buffer overrun buffer.Signal(); return; } cFrame *frame=new cFrame(Data, len); if (frame && !buffer.Put(frame)) { // Buffer overrun delete frame; buffer.Signal(); } } void cTxtReceiver::Action() { while (running) { cFrame *frame=buffer.Get(); if (frame) { uchar *Datai=frame->Data(); for (int i=0; i < 4; i++) { if (Datai[4+i*46]==2 || Datai[4+i*46]==3) { for (int j=(8+i*46);j<(50+i*46);j++) Datai[j]=invtab[Datai[j]]; DecodeTXT(&Datai[i*46]); } } buffer.Drop(frame); } else buffer.Wait(); } buffer.Clear(); running=false; } uchar cTxtReceiver::unham16 (uchar *p) { unsigned short c1,c2; c1=unhamtab[p[0]]; c2=unhamtab[p[1]]; return (c1 & 0x0F) | (c2 & 0x0F) *16; } void cTxtReceiver::DecodeTXT(uchar* TXT_buf) { // Format of buffer: // 0x00-0x04 ? // 0x05-0x06 Clock Run-In? // 0x07 Framing Code? // 0x08 Magazine number (100-digit of page number) // 0x09 Line number // 0x0A..0x31 Line data // Line 0 only: // 0x0A 10-digit of page number // 0x0B 1-digit of page number // 0x0C Sub-Code bits 0..3 // 0x0D Sub-Code bits 4..6 + C4 flag // 0x0E Sub-Code bits 8..11 // 0x0F Sub-Code bits 12..13 + C5,C6 flag // 0x10 C7-C10 flags // 0x11 C11-C14 flags // // Flags: // C4 - Erase last page, new page transmitted // C5 - News flash, boxed display // C6 - Subtitle, boxed display // C7 - Suppress Header, dont show line 0 // C8 - Update, page has changed // C9 - Interrupt Sequence, page number is out of order // C10 - Inhibit Display // C11 - Magazine Serial mode // C12-C14 - Language selection, lower 3 bits int hdr,mag,mag8,line; uchar *ptr; uchar flags,lang; hdr = unham16 (&TXT_buf[0x8]); mag = hdr & 0x07; mag8 = mag ?: 8; line = (hdr>>3) & 0x1f; ptr = &TXT_buf[10]; switch (line) { case 0: { unsigned char b1, b2, b3, b4; int pgno, subno; b1 = unham16 (ptr); // Page no, 10- and 1-digit if (b1 == 0xff) break; if (TxtPage) { TxtPage->save(); delete TxtPage; TxtPage=NULL; } b2 = unham16 (ptr+2); // Sub-code 0..6 + C4 b3 = unham16 (ptr+4); // Sub-code 8..13 + C5,C6 b4 = unham16 (ptr+6); // C7..C14 // flags: // 0x80 C4 - Erase page // 0x40 C5 - News flash // 0x20 C6 - Subtitle // 0x10 C7 - Suppress Header // 0x08 C8 - Update // 0x04 C9 - Interrupt Sequence // 0x02 C9 (Bug?) // 0x01 C11 - Magazine Serial mode flags=b2 & 0x80; flags|=(b3&0x40)|((b3>>2)&0x20); //?????? flags|=((b4<<4)&0x10)|((b4<<2)&0x08)|(b4&0x04)|((b4>>1)&0x02)|((b4>>4)&0x01); lang=((b4>>5) & 0x07); pgno = mag8 * 256 + b1; subno = (b2 + b3 * 256) & 0x3f7f; // Sub Page Number TxtPage = new cTelePage(PageID(chan, pgno, subno), flags, lang, mag); TxtPage->SetLine((int)line,(uchar *)ptr); break; } case 1 ... 25: { if (TxtPage) TxtPage->SetLine((int)line,(uchar *)ptr); break; } /*case 23: { if (TxtPage) { TxtPage->save(); delete TxtPage; TxtPage=NULL; } break; }*/ default: break; } }