player-mplayer.c
author nathan
Tue, 03 Feb 2009 20:33:04 +0800
branchtrunk
changeset 22 93aaf15c145a
parent 0 474a1293c3c0
child 23 3b14b8aacaa0
permissions -rw-r--r--
remove compatibility for VDR < 1.4.5
     1 /*
     2  * MP3/MPlayer plugin to VDR (C++)
     3  *
     4  * (C) 2001-2009 Stefan Huelswitt <s.huelswitt@gmx.de>
     5  *
     6  * This code is free software; you can redistribute it and/or
     7  * modify it under the terms of the GNU General Public License
     8  * as published by the Free Software Foundation; either version 2
     9  * of the License, or (at your option) any later version.
    10  *
    11  * This code is distributed in the hope that it will be useful,
    12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
    13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14  * GNU General Public License for more details.
    15  *
    16  * You should have received a copy of the GNU General Public License
    17  * along with this program; if not, write to the Free Software
    18  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
    19  * Or, point your browser to http://www.gnu.org/copyleft/gpl.html
    20  */
    21 
    22 #include <ctype.h>
    23 #include <stdlib.h>
    24 #include <stdio.h>
    25 #include <stdarg.h>
    26 #include <string.h>
    27 #include <fcntl.h>
    28 #include <errno.h>
    29 #include <sys/ioctl.h>
    30 #include <sys/types.h>
    31 #include <sys/poll.h>
    32 #include <unistd.h>
    33 #include <signal.h>
    34 #include <wait.h>
    35 #include <math.h>
    36 #include <locale.h>
    37 
    38 #include <vdr/device.h>
    39 #include <vdr/videodir.h>
    40 
    41 #include "common.h"
    42 #include "data.h"
    43 #include "player-mplayer.h"
    44 #include "setup-mplayer.h"
    45 
    46 //#define DEBUG_SLAVE
    47 
    48 #define MPLAYER_VOL_STEP 3.0
    49 
    50 const char *MPlayerCmd = "mplayer.sh";
    51 int MPlayerAid=-1;
    52 const char *globalResumeDir = 0;
    53 
    54 // -- cMPlayerStatus -----------------------------------------------------------
    55 
    56 cMPlayerStatus *status;
    57 
    58 cMPlayerStatus::cMPlayerStatus(void)
    59 {
    60   mute=changed=false;
    61   volume=0;
    62 }
    63 
    64 bool cMPlayerStatus::GetVolume(int &Volume, bool &Mute)
    65 {
    66   Lock();
    67   bool r=changed;
    68   // convert according cDvbDevice::SetVolumeDevice();
    69   // take into account that VDR does non-linear changes, while
    70   // MPlayer does linear volume changes.
    71   Volume=((2*volume-volume*volume/255)*100+128)/256;
    72   Mute=mute;
    73   changed=false;
    74   Unlock();
    75   return r;
    76 }
    77 
    78 void cMPlayerStatus::SetVolume(int Volume, bool Absolute)
    79 {
    80   Lock();
    81   if(Absolute && Volume==0) mute=true;
    82   else {
    83     if(!Absolute)
    84       volume+=Volume;
    85     else
    86       volume=Volume;
    87     if(volume>0) mute=false;
    88     }
    89   d(printf("status: volume=%d mute=%d\n",volume,mute))
    90   changed=true;
    91   Unlock();
    92 }
    93 
    94 // --- cResumeEntry ------------------------------------------------------------
    95 
    96 class cResumeEntry : public cListObject {
    97 public:
    98   char *name;
    99   float pos;
   100   //
   101   cResumeEntry(void);
   102   ~cResumeEntry();
   103   };
   104 
   105 cResumeEntry::cResumeEntry(void)
   106 {
   107   name=0;
   108 }
   109 
   110 cResumeEntry::~cResumeEntry()
   111 {
   112   free(name);
   113 }
   114 
   115 // --- cMPlayerResume ----------------------------------------------------------
   116 
   117 #define RESUME_FILE ".mplayer.resume"
   118 #define GLOBAL_RESUME_FILE "global.mplayer.resume"
   119 
   120 class cMPlayerResume : public cList<cResumeEntry> {
   121 private:
   122   char *resfile;
   123   bool modified, global;
   124   cFileObj *resobj;
   125   //
   126   bool OpenResume(const cFileObj *file);
   127   bool SaveResume(void);
   128   void Purge(void);
   129   cResumeEntry *FindResume(const cFileObj *file);
   130 public:
   131   cMPlayerResume(void);
   132   ~cMPlayerResume();
   133   void SetResume(const cFileObj *file, float pos);
   134   bool GetResume(const cFileObj *file, float &pos);
   135   };
   136 
   137 cMPlayerResume::cMPlayerResume(void)
   138 {
   139   resfile=0; resobj=0;
   140 }
   141 
   142 cMPlayerResume::~cMPlayerResume()
   143 {
   144   SaveResume();
   145   free(resfile);
   146   delete resobj;
   147 }
   148 
   149 void cMPlayerResume::SetResume(const cFileObj *file, float pos)
   150 {
   151   if(pos<0.001) pos=0.0;
   152   else if(pos>99.0) pos=99.0;
   153   cResumeEntry *re;
   154   if(OpenResume(file) && (re=FindResume(file))) {
   155     d(printf("resume: setting resume %f (update)\n",pos))
   156     }
   157   else {
   158     re=new cResumeEntry;
   159     re->name=strdup(global ? file->FullPath() : file->Name());
   160     Add(re);
   161     d(printf("resume: setting resume %f (new)\n",pos))
   162     }
   163   re->pos=pos;
   164   modified=true;
   165 }
   166 
   167 bool cMPlayerResume::GetResume(const cFileObj *file, float &pos)
   168 {
   169   cResumeEntry *re;
   170   if(OpenResume(file) && (re=FindResume(file))) {
   171     pos=re->pos;
   172     return true;
   173     }
   174   return false;
   175 }
   176 
   177 bool cMPlayerResume::OpenResume(const cFileObj *file)
   178 {
   179   if(!resfile) {
   180     Clear();
   181     modified=global=false;
   182     free(resfile); resfile=0;
   183     delete resobj; resobj=new cFileObj(file);
   184     char *s;
   185     asprintf(&s,file->Subdir() ? "%s/%s":"%s",file->Source()->BaseDir(),file->Subdir());
   186     if(MPlayerSetup.ResumeMode==1 || 
   187        (access(s,W_OK) && (errno==EACCES || errno==EROFS))) {
   188       global=true;
   189       resfile=AddPath(globalResumeDir?globalResumeDir:VideoDirectory,GLOBAL_RESUME_FILE);
   190       d(printf("resume: using global file\n"))
   191       }
   192     else {
   193       resfile=AddPath(s,RESUME_FILE);
   194       }
   195     free(s);
   196     d(printf("resume: resume file is '%s'\n",resfile))
   197     FILE *f=fopen(resfile,"r");
   198     if(f) {
   199       d(printf("resume: successfully opened resume file\n"))
   200       char line[768];
   201       while(fgets(line,sizeof(line),f)) {
   202         char name[512];
   203         float p;
   204         if(sscanf(line,"%f:%511[^\n]",&p,name)==2) {
   205           cResumeEntry *re=new cResumeEntry;
   206           re->name=strdup(name);
   207           re->pos=p;
   208           Add(re);
   209           }
   210         }
   211       fclose(f);
   212       return true;
   213       }
   214     else {
   215       d(printf("resume: assuming empty resume file\n"))
   216       return false;
   217       }
   218     }
   219   return true;
   220 }
   221 
   222 bool cMPlayerResume::SaveResume(void)
   223 {
   224   if(resfile && modified) {
   225     Purge();
   226     d(printf("resume: saving resume file\n"))
   227     cSafeFile f(resfile);
   228     if(f.Open()) {
   229       for(cResumeEntry *re=First(); re; re=Next(re))
   230         fprintf(f,"%06.2f:%s\n",re->pos,re->name);
   231       f.Close();
   232       return true;
   233       }
   234     else
   235       d(printf("resume: failed to save resume file\n"))
   236     }
   237   return false;
   238 }
   239 
   240 void cMPlayerResume::Purge(void)
   241 {
   242   d(printf("resume: purging from resume file\n"))
   243   for(cResumeEntry *re=First(); re;) {
   244     bool del=false;
   245     if(re->pos<1.0 || re->pos>99.0) {
   246       del=true;
   247       d(printf("resume: purging due to position: %s\n",re->name))
   248       }
   249     else if(!global) {
   250       resobj->SetName(re->name);
   251       if(access(resobj->FullPath(),F_OK)<0) {
   252         del=true;
   253         d(printf("resume: purging due to access: %s\n",re->name))
   254         }
   255       }
   256     if(del) {
   257       cResumeEntry *n=Next(re);
   258       Del(re);
   259       modified=true;
   260       re=n;
   261       }
   262     else
   263       re=Next(re);
   264     }
   265 }
   266 
   267 cResumeEntry *cMPlayerResume::FindResume(const cFileObj *file)
   268 {
   269  if(resfile) {
   270    d(printf("resume: searching resume  position for '%s'\n",file->Name()))
   271    const char *s=global ? file->FullPath() : file->Name();
   272    for(cResumeEntry *re=First(); re; re=Next(re))
   273      if(!strcasecmp(re->name,s)) {
   274        d(printf("resume: found resume position %.1f%%\n",re->pos))
   275        return re;
   276        }
   277    }
   278  d(printf("resume: no resume position found\n"))
   279  return 0;
   280 }
   281 
   282 // --- cMPlayerPlayer ----------------------------------------------------------
   283 
   284 cMPlayerPlayer::cMPlayerPlayer(const cFileObj *File, bool Rewind)
   285 :cPlayer(pmExtern_THIS_SHOULD_BE_AVOIDED)
   286 {
   287   started=slave=brokenPipe=false; run=true; pid=-1; pipefl=0;
   288   playMode=pmPlay; index=saveIndex=total=-1; nextTime=nextPos=0;
   289   currentName=0;
   290   file=new cFileObj(File);
   291   rewind=Rewind;
   292   resume=MPlayerSetup.ResumeMode ? new cMPlayerResume : 0;
   293 }
   294 
   295 cMPlayerPlayer::~cMPlayerPlayer()
   296 {
   297   Detach();
   298   ClosePipe();
   299   delete file;
   300   delete resume;
   301   free(currentName);
   302 }
   303 
   304 void cMPlayerPlayer::ClosePipe(void)
   305 {
   306   if(pipefl&1) close(inpipe[0]);
   307   if(pipefl&2) close(inpipe[1]);
   308   if(pipefl&4) close(outpipe[0]);
   309   if(pipefl&8) close(outpipe[1]);
   310   pipefl=0;
   311 }
   312 
   313 void cMPlayerPlayer::Activate(bool On)
   314 {
   315   if(On) {
   316     if(file && !started) {
   317       if(Fork()) started=true;
   318       }
   319     }
   320   else if(started) {
   321     run=false;
   322     if(Active()) {
   323       if(slave) {
   324         Play(); // MPlayer ignores "quit" while paused
   325         MPlayerControl("quit");
   326         int until=time_ms()+3000; // wait some time until MPlayer is gone
   327         d(printf("mplayer: waiting for child exit"))
   328         while(Active()) {
   329           if(time_ms()>until) {
   330             kill(pid,SIGKILL); // kill it anyways
   331             d(printf(" SIGKILL"))
   332             break;
   333             }
   334           SLEEP(250);
   335           d(printf(".")) d(fflush(stdout))
   336           }
   337         d(printf("\n"))
   338         }
   339       else {
   340         kill(pid,SIGTERM);
   341         d(printf("mplayer: waiting for child exit (non-slave)\n"))
   342         }
   343       waitpid(pid,0,0);
   344       }
   345     ClosePipe();
   346     Cancel(2);
   347     started=slave=false;
   348     }
   349 }
   350 
   351 bool cMPlayerPlayer::Active(void)
   352 {
   353   return waitpid(pid,0,WNOHANG)==0;
   354 }
   355 
   356 bool cMPlayerPlayer::Fork(void)
   357 {
   358   if(MPlayerSetup.SlaveMode) {
   359     if(pipe(inpipe)==-1) {
   360       esyslog("ERROR: pipe failed for inpipe: (%d) %s",errno,strerror(errno));
   361       return false;
   362       }
   363     pipefl|=1+2;
   364     if(pipe(outpipe)==-1) {
   365       esyslog("ERROR: pipe failed for outpipe: (%d) %s",errno,strerror(errno));
   366       return false;
   367       }
   368     pipefl|=4+8;
   369     brokenPipe=false;
   370     }
   371 
   372   pid=fork();
   373   if(pid==-1) {
   374     esyslog("ERROR: fork failed: (%d) %s",errno,strerror(errno));
   375     return false;
   376     }
   377   if(pid==0) { // child
   378     dsyslog("mplayer: mplayer child started (pid=%d)", getpid());
   379 
   380     if(MPlayerSetup.SlaveMode) {
   381       if(dup2(inpipe[0],STDIN_FILENO)<0 ||
   382          dup2(outpipe[1],STDOUT_FILENO)<0 ||
   383          dup2(outpipe[1],STDERR_FILENO)<0) {
   384         esyslog("ERROR: dup2() failed in MPlayer child: (%d) %s",errno,strerror(errno));
   385         exit(127);
   386         }
   387       }
   388     else {
   389       int nfd=open("/dev/null",O_RDONLY);
   390       if(nfd<0 || dup2(nfd,STDIN_FILENO)<0)
   391         esyslog("ERROR: redirect of STDIN failed in MPlayer child: (%d) %s",errno,strerror(errno));
   392       }
   393     for(int i=getdtablesize()-1; i>STDERR_FILENO; i--) close(i);
   394 
   395     char cmd[64+PATH_MAX*2], aid[20];
   396     char *fname=Quote(file->FullPath());
   397     if(MPlayerAid>=0) snprintf(aid,sizeof(aid)," AID %d",MPlayerAid);
   398     else aid[0]=0;
   399     snprintf(cmd,sizeof(cmd),"%s \"%s\" %s%s",MPlayerCmd,fname,MPlayerSetup.SlaveMode?"SLAVE":"",aid);
   400     free(fname);
   401     execle("/bin/sh","sh","-c",cmd,(char *)0,environ);
   402     esyslog("ERROR: exec failed for %s: (%d) %s",cmd,errno,strerror(errno));
   403     exit(127);
   404     }
   405 
   406   if(MPlayerSetup.SlaveMode) {
   407     close(inpipe[0]); pipefl&=~1;
   408     close(outpipe[1]); pipefl&=~8;
   409     fcntl(outpipe[0],F_SETFL,O_NONBLOCK);
   410     run=slave=true;
   411     mpVolume=100; // MPlayer startup defaults
   412     mpMute=false;
   413     Start();
   414     }
   415   return true;
   416 }
   417 
   418 #define BSIZE    1024
   419 #define TIME_INT 20
   420 #define POS_INT  1
   421 
   422 void cMPlayerPlayer::Action(void)
   423 {
   424   dsyslog("mplayer: player thread started (pid=%d)", getpid());
   425 
   426   // set locale for correct parsing of MPlayer output.
   427   // I don't know if this affects other parts of VDR.
   428   const char * const oldLocale=setlocale(LC_NUMERIC,"C");
   429 
   430   pollfd pfd[1];
   431   pfd[0].fd=outpipe[0];
   432   pfd[0].events=POLLIN;
   433 
   434   float curPos=-1.0, resPos=-1.0;
   435   if(resume && !rewind) resume->GetResume(file,resPos);
   436 
   437   char buff[BSIZE+2]; // additional space for fake newline
   438   int c=0;
   439   bool force=true, slavePatch=false, trustedTotal=false, playBack=false;
   440   while(run) {
   441     if(playMode==pmPlay && playBack) {
   442       int t=time(0);
   443       if(t>=nextTime) {
   444         MPlayerControl("get_time_length");
   445         nextTime=t+(total>0 ? TIME_INT : POS_INT);
   446         }
   447       if(t>=nextPos) {
   448         if(!slavePatch) MPlayerControl("get_percent_pos");
   449         nextPos=t+POS_INT;
   450         }
   451       }
   452 
   453     poll(pfd,1,300);
   454     int r=read(outpipe[0],buff+c,BSIZE-c);
   455     if(r>0) c+=r;
   456     if(c>0) {
   457       buff[c]=0; // make sure buffer is NULL terminated
   458       char *p;
   459       do {
   460         p=strpbrk(buff,"\n\r");
   461         if(!p && c==BSIZE) { // Full buffer, but no newline found.
   462           p=&buff[c];        // Have to fake one.
   463           buff[c]='\n'; c++; buff[c]=0;
   464           }
   465         if(p) {
   466 #ifdef DEBUG
   467           char cc=*p;
   468 #endif
   469           *p++=0;
   470           float ftime=-1.0, fpos=-1.0;
   471           int itime;
   472           if(strncmp(buff,"Starting playback",17)==0 ||
   473              strncmp(buff,"Starte Wiedergabe",17)==0) {
   474             if(!playBack) {
   475               playBack=true;
   476               nextTime=nextPos=0;
   477               d(printf("PLAYBACK STARTED\n"))
   478               if(resPos>=0.0) {
   479                 if(!currentName ||
   480                    !strcmp(currentName,file->FullPath()) ||
   481                    !strcmp(currentName,file->Path()))
   482                   MPlayerControl("seek %.1f 1",resPos);
   483                 else
   484                   d(printf("mplayer: no resume, seems to be playlist\n"))
   485                 }
   486               }
   487             }
   488           else if(strncmp(buff,"Playing ",8)==0 ||
   489                   strncmp(buff,"Spiele ",7)==0) {
   490             nextTime=nextPos=0;
   491             index=saveIndex=total=-1;
   492             trustedTotal=false;
   493             LOCK_THREAD;
   494             free(currentName);
   495             currentName=strdup(::index(buff,' ')+1);
   496             if(currentName[0]) {
   497               int l=strlen(currentName);
   498               if(currentName[l-1]=='.') currentName[l-1]=0; // skip trailing dot
   499               }
   500             d(printf("PLAYING %s\n",currentName))
   501             }
   502           else if(sscanf(buff,"ANS_LENGTH=%d",&itime)==1) {
   503             if(itime>0) {
   504               total=SecondsToFrames(itime);
   505               trustedTotal=true;
   506 #ifdef DEBUG_SLAVE
   507               printf("sl: ANS_LENGTH=%s (%s)\n",IndexToHMSF(total),buff);
   508 #endif
   509               }
   510             }
   511           else if(sscanf(buff,"ANS_PERCENT_POSITION=%d",&itime)==1) {
   512             if(itime>0) {
   513               curPos=itime;
   514               if(total>=0) {
   515                 index=total*itime/100;
   516 #ifdef DEBUG_SLAVE
   517                 printf("sl: ANS_PERCENT_POS=%s (%s)\n",IndexToHMSF(index),buff);
   518 #endif
   519                 }
   520               }
   521             }
   522           else if(sscanf(buff,"SLAVE: time=%f position=%f",&ftime,&fpos)==2) {
   523             curPos=fpos;
   524             const float fr=(float)SecondsToFrames(1);
   525             itime=(int)(ftime*fr);
   526             if(saveIndex<0 || itime>saveIndex) { // prevent index jump-back
   527               saveIndex=index=itime;
   528               if(!trustedTotal) total=(int)(ftime*fr*100.0/fpos);
   529 #ifdef DEBUG_SLAVE
   530               printf("sl: SLAVE=%s/%s [%d] (%s)\n",IndexToHMSF(index),IndexToHMSF(total),trustedTotal,buff);
   531 #endif
   532               }
   533             slavePatch=playBack=true;
   534             }
   535 #ifdef DEBUG
   536           else printf("%s%c",buff,cc);
   537 #endif
   538           c-=(p-buff);
   539           memmove(buff,p,c+1);
   540           }
   541         } while(c>0 && p);
   542       }
   543     if(playBack) {
   544       SetMPlayerVolume(force);
   545       force=false;
   546       }
   547     }
   548 
   549   if(resume && curPos>=0.0) resume->SetResume(file,curPos);
   550 
   551   // restore old locale
   552   if(oldLocale) setlocale(LC_NUMERIC,oldLocale);
   553 
   554   dsyslog("mplayer: player thread ended (pid=%d)", getpid());
   555 }
   556 
   557 void cMPlayerPlayer::SetMPlayerVolume(bool force)
   558 {
   559   int volume;
   560   bool mute;
   561   Lock();
   562   if(status->GetVolume(volume,mute) || force) {
   563     if(mute) {
   564       if(!mpMute) { MPlayerControl("mute"); mpMute=true; }
   565       }
   566     else {
   567       if(mpMute) { MPlayerControl("mute"); mpMute=false; }
   568       if(volume!=mpVolume) {
   569         MPlayerControl("volume %d 1",volume);
   570         mpVolume=volume;
   571         }
   572       }
   573     d(printf("mplayer: volume=%d mpVolume=%d mpMute=%d\n",volume,mpVolume,mpMute))
   574     }
   575   Unlock();
   576 }
   577 
   578 void cMPlayerPlayer::MPlayerControl(const char *format, ...)
   579 {
   580   if(slave) {
   581     va_list ap;
   582     va_start(ap,format);
   583     char *buff=0;
   584     vasprintf(&buff,format,ap);
   585     Lock();
   586     // check for writeable pipe i.e. prevent broken pipe signal
   587     if(!brokenPipe) {
   588       struct pollfd pfd;
   589       pfd.fd=inpipe[1]; pfd.events=POLLOUT; pfd.revents=0;
   590       int r=poll(&pfd,1,50);
   591       if(r>0) {
   592         if(pfd.revents & ~POLLOUT) {
   593           d(printf("mplayer: %s%s%s%sin MPlayerControl\n",pfd.revents&POLLOUT?"POLLOUT ":"",pfd.revents&POLLERR?"POLLERR ":"",pfd.revents&POLLHUP?"POLLHUP ":"",pfd.revents&POLLNVAL?"POLLNVAL ":""))
   594           brokenPipe=true;
   595           }
   596         else if(pfd.revents & POLLOUT) {
   597           r=write(inpipe[1],buff,strlen(buff));
   598           if(r<0) {
   599             d(printf("mplayer: pipe write(1) failed: %s\n",strerror(errno)))
   600             brokenPipe=true;
   601             }
   602           else {
   603             r=write(inpipe[1],"\n",1);
   604             if(r<0) {
   605               d(printf("mplayer: pipe write(2) failed: %s\n",strerror(errno)))
   606               brokenPipe=true;
   607               }
   608             }
   609           }
   610         }
   611       else if(r==0) d(printf("mplayer: poll timed out in MPlayerControl (hugh?)\n"))
   612       else d(printf("mplayer: poll failed in MPlayerControl: %s\n",strerror(errno)))
   613       }
   614     else d(printf("mplayer: cmd pipe is broken\n"))
   615     Unlock();
   616     d(printf("mplayer: slave cmd: %s\n",buff))
   617     free(buff);
   618     va_end(ap);
   619     }
   620 }
   621 
   622 void cMPlayerPlayer::Pause(void)
   623 {
   624   if(slave) {
   625     if(playMode==pmPaused) Play();
   626     else if(playMode==pmPlay) {
   627       playMode=pmPaused;
   628       MPlayerControl("pause");
   629       }
   630     }
   631 }
   632 
   633 void cMPlayerPlayer::Play(void)
   634 {
   635   if(slave) {
   636     if(playMode==pmPaused) {
   637       playMode=pmPlay;
   638       MPlayerControl("pause");
   639       }
   640     }
   641 }
   642 
   643 void cMPlayerPlayer::Goto(int Index, bool percent, bool still)
   644 {
   645   if(slave) {
   646     if(playMode==pmPaused) Play();
   647     if(percent) MPlayerControl("seek %d 1",Index);
   648     else        MPlayerControl("seek %+d 0",Index-(index/SecondsToFrames(1)));
   649     if(still) Pause();
   650     saveIndex=-1;
   651     }
   652 }
   653 
   654 void cMPlayerPlayer::SkipSeconds(int secs)
   655 {
   656   if(slave) {
   657     bool p=false;
   658     if(playMode==pmPaused) { Play(); p=true; }
   659     MPlayerControl("seek %+d 0",secs);
   660     if(p) Pause();
   661     saveIndex=-1;
   662     }
   663 }
   664 
   665 void cMPlayerPlayer::KeyCmd(const char *cmd)
   666 {
   667   if(slave) MPlayerControl(cmd);
   668 }
   669 
   670 bool cMPlayerPlayer::GetIndex(int &Current, int &Total, bool SnapToIFrame)
   671 {
   672   Current=index; Total=total;
   673   return true;
   674 }
   675 
   676 bool cMPlayerPlayer::GetReplayMode(bool &Play, bool &Forward, int &Speed)
   677 {
   678   Play=(playMode==pmPlay);
   679   Forward=true;
   680   Speed=-1;
   681   return true;
   682 }
   683 
   684 char *cMPlayerPlayer::GetCurrentName(void)
   685 {
   686   LOCK_THREAD;
   687   return currentName ? strdup(currentName) : 0;
   688 }
   689