[XBOX] fixed: Weird A/V desync at start (again)
[xbmc:xbmc-antiquated.git] / xbmc / cores / dvdplayer / DVDPlayerAudio.cpp
1 /*
2  *      Copyright (C) 2005-2008 Team XBMC
3  *      http://www.xbmc.org
4  *
5  *  This Program is free software; you can redistribute it and/or modify
6  *  it under the terms of the GNU General Public License as published by
7  *  the Free Software Foundation; either version 2, or (at your option)
8  *  any later version.
9  *
10  *  This Program is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  *  GNU General Public License for more details.
14  *
15  *  You should have received a copy of the GNU General Public License
16  *  along with XBMC; see the file COPYING.  If not, write to
17  *  the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
18  *  http://www.gnu.org/copyleft/gpl.html
19  *
20  */
21  
22 #include "stdafx.h"
23 #include "DVDPlayerAudio.h"
24 #include "DVDPlayer.h"
25 #include "DVDCodecs/Audio/DVDAudioCodec.h"
26 #include "DVDCodecs/DVDCodecs.h"
27 #include "DVDCodecs/DVDFactoryCodec.h"
28 #include "DVDPerformanceCounter.h"
29 #include "utils/TimeUtils.h"
30 #include <sstream>
31 #include <iomanip>
32
33 using namespace std;
34
35 CPTSOutputQueue::CPTSOutputQueue()
36 {
37   Flush();
38 }
39
40 void CPTSOutputQueue::Add(double pts, double delay, double duration)
41 {
42   CSingleLock lock(m_sync);
43
44   TPTSItem item;
45   item.pts = pts;
46   item.timestamp = CDVDClock::GetAbsoluteClock() + delay;
47   item.duration = duration;
48
49   // first one is applied directly
50   if(m_queue.empty() && m_current.pts == DVD_NOPTS_VALUE)
51     m_current = item;
52   else
53     m_queue.push(item);
54
55   // call function to make sure the queue
56   // doesn't grow should nobody call it
57   Current();
58 }
59 void CPTSOutputQueue::Flush()
60 {
61   CSingleLock lock(m_sync);
62
63   while( !m_queue.empty() ) m_queue.pop();
64   m_current.pts = DVD_NOPTS_VALUE;
65   m_current.timestamp = 0.0;
66   m_current.duration = 0.0;
67 }
68
69 double CPTSOutputQueue::Current()
70 {
71   CSingleLock lock(m_sync);
72
73   if(!m_queue.empty() && m_current.pts == DVD_NOPTS_VALUE)
74   {
75     m_current = m_queue.front();
76     m_queue.pop();
77   }
78
79   while( !m_queue.empty() && CDVDClock::GetAbsoluteClock() >= m_queue.front().timestamp )
80   {
81     m_current = m_queue.front();
82     m_queue.pop();
83   }
84
85   if( m_current.timestamp == 0 ) return m_current.pts;
86
87   return m_current.pts + min(m_current.duration, (CDVDClock::GetAbsoluteClock() - m_current.timestamp));
88 }
89
90 void CPTSInputQueue::Add(__int64 bytes, double pts)
91 {
92   CSingleLock lock(m_sync);
93
94   m_list.insert(m_list.begin(), make_pair(bytes, pts));
95 }
96
97 void CPTSInputQueue::Flush()
98 {
99   CSingleLock lock(m_sync);
100
101   m_list.clear();
102 }
103 double CPTSInputQueue::Get(__int64 bytes, bool consume)
104 {
105   CSingleLock lock(m_sync);
106
107   IT it = m_list.begin();
108   for(; it != m_list.end(); it++)
109   {
110     if(bytes <= it->first)
111     {
112       double pts = it->second;
113       if(consume)
114       {
115         it->second = DVD_NOPTS_VALUE;
116         m_list.erase(++it, m_list.end());
117       }
118       return pts;
119     }
120     bytes -= it->first;
121   }
122   return DVD_NOPTS_VALUE;
123 }
124
125 CDVDPlayerAudio::CDVDPlayerAudio(CDVDClock* pClock, CDVDMessageQueue& parent)
126 : CThread()
127 , m_messageQueue("audio")
128 , m_messageParent(parent)
129 , m_dvdAudio((bool&)m_bStop)
130 {
131   m_pClock = pClock;
132   m_pAudioCodec = NULL;
133   m_audioClock = 0;
134   m_droptime = 0;
135   m_speed = DVD_PLAYSPEED_NORMAL;
136   m_stalled = true;
137   m_started = false;
138   m_duration = 0.0;
139
140   m_freq = CurrentHostFrequency();
141 #ifdef _XBOX
142   m_messageQueue.SetMaxDataSize(10 * 16 * 1024);
143 #else
144   m_messageQueue.SetMaxDataSize(6 * 1024 * 1024);
145 #endif
146   m_messageQueue.SetMaxTimeSize(8.0);
147   g_dvdPerformanceCounter.EnableAudioQueue(&m_messageQueue);
148 }
149
150 CDVDPlayerAudio::~CDVDPlayerAudio()
151 {
152   StopThread();
153   g_dvdPerformanceCounter.DisableAudioQueue();
154
155   // close the stream, and don't wait for the audio to be finished
156   // CloseStream(true);
157 }
158
159 bool CDVDPlayerAudio::OpenStream( CDVDStreamInfo &hints )
160 {
161   // should alway's be NULL!!!!, it will probably crash anyway when deleting m_pAudioCodec here.
162   if (m_pAudioCodec)
163   {
164     CLog::Log(LOGFATAL, "CDVDPlayerAudio::OpenStream() m_pAudioCodec != NULL");
165     return false;
166   }
167
168   /* try to open decoder without probing, we could actually allow us to continue here */
169   if( !OpenDecoder(hints) ) return false;
170
171   m_messageQueue.Init();
172
173   m_droptime = 0;
174   m_audioClock = 0;
175   m_stalled = true;
176   m_started = false;
177
178   m_error = 0;
179   m_errorbuff = 0;
180   m_errorcount = 0;
181   m_syncclock = true;
182   m_errortime = CurrentHostCounter();
183   CLog::Log(LOGNOTICE, "Creating audio thread");
184   Create();
185
186   return true;
187 }
188
189 void CDVDPlayerAudio::CloseStream(bool bWaitForBuffers)
190 {
191   // wait until buffers are empty
192   if (bWaitForBuffers && m_speed > 0) m_messageQueue.WaitUntilEmpty();
193
194   // send abort message to the audio queue
195   m_messageQueue.Abort();
196
197   CLog::Log(LOGNOTICE, "Waiting for audio thread to exit");
198
199   // shut down the adio_decode thread and wait for it
200   StopThread(); // will set this->m_bStop to true
201
202   // destroy audio device
203   CLog::Log(LOGNOTICE, "Closing audio device");
204   if (bWaitForBuffers && m_speed > 0)
205   {
206     m_bStop = false;
207     m_dvdAudio.Drain();
208     m_bStop = true;
209   }
210   m_dvdAudio.Destroy();
211
212   // uninit queue
213   m_messageQueue.End();
214
215   CLog::Log(LOGNOTICE, "Deleting audio codec");
216   if (m_pAudioCodec)
217   {
218     m_pAudioCodec->Dispose();
219     delete m_pAudioCodec;
220     m_pAudioCodec = NULL;
221   }
222
223   // flush any remaining pts values
224   m_ptsOutput.Flush();
225 }
226
227 bool CDVDPlayerAudio::OpenDecoder(CDVDStreamInfo &hints, BYTE* buffer /* = NULL*/, unsigned int size /* = 0*/)
228 {
229   /* close current audio codec */
230   if( m_pAudioCodec )
231   {
232     CLog::Log(LOGNOTICE, "Deleting audio codec");
233     m_pAudioCodec->Dispose();
234     SAFE_DELETE(m_pAudioCodec);
235   }
236
237   /* store our stream hints */
238   m_streaminfo = hints;
239
240   CLog::Log(LOGNOTICE, "Finding audio codec for: %i", m_streaminfo.codec);
241   m_pAudioCodec = CDVDFactoryCodec::CreateAudioCodec( m_streaminfo );
242   if( !m_pAudioCodec )
243   {
244     CLog::Log(LOGERROR, "Unsupported audio codec");
245
246     m_streaminfo.Clear();
247     return false;
248   }
249
250   /* update codec information from what codec gave ut */
251   m_streaminfo.channels = m_pAudioCodec->GetChannels();
252   m_streaminfo.samplerate = m_pAudioCodec->GetSampleRate();
253
254   return true;
255 }
256
257 // decode one audio frame and returns its uncompressed size
258 int CDVDPlayerAudio::DecodeFrame(DVDAudioFrame &audioframe, bool bDropPacket)
259 {
260   int result = 0;
261
262   // make sure the sent frame is clean
263   memset(&audioframe, 0, sizeof(DVDAudioFrame));
264
265   while (!m_bStop)
266   {
267     /* NOTE: the audio packet can contain several frames */
268     while( !m_bStop && m_decode.size > 0 )
269     {
270       if( !m_pAudioCodec )
271         return DECODE_FLAG_ERROR;
272
273       /* the packet dts refers to the first audioframe that starts in the packet */
274       double dts = m_ptsInput.Get(m_decode.size + m_pAudioCodec->GetBufferSize(), true);
275       if (dts != DVD_NOPTS_VALUE)
276         m_audioClock = dts;
277
278       int len = m_pAudioCodec->Decode(m_decode.data, m_decode.size);
279       m_audioStats.AddSampleBytes(m_decode.size);
280       if (len < 0)
281       {
282         /* if error, we skip the packet */
283         CLog::Log(LOGERROR, "CDVDPlayerAudio::DecodeFrame - Decode Error. Skipping audio packet");
284         m_decode.Release();
285         m_pAudioCodec->Reset();
286         return DECODE_FLAG_ERROR;
287       }
288
289       // fix for fucked up decoders
290       if( len > m_decode.size )
291       {
292         CLog::Log(LOGERROR, "CDVDPlayerAudio:DecodeFrame - Codec tried to consume more data than available. Potential memory corruption");
293         m_decode.Release();
294         m_pAudioCodec->Reset();
295         assert(0);
296       }
297
298       m_decode.data += len;
299       m_decode.size -= len;
300
301
302       // get decoded data and the size of it
303       audioframe.size = m_pAudioCodec->GetData(&audioframe.data);
304       audioframe.pts = m_audioClock;
305       audioframe.channels = m_pAudioCodec->GetChannels();
306       audioframe.bits_per_sample = m_pAudioCodec->GetBitsPerSample();
307       audioframe.sample_rate = m_pAudioCodec->GetSampleRate();
308       audioframe.passthrough = m_pAudioCodec->NeedPassthrough();
309
310       if (audioframe.size <= 0)
311         continue;
312
313       // compute duration.
314       int n = (audioframe.channels * audioframe.bits_per_sample * audioframe.sample_rate)>>3;
315       if (n > 0)
316       {
317         // safety check, if channels == 0, n will result in 0, and that will result in a nice devide exception
318         audioframe.duration = ((double)audioframe.size * DVD_TIME_BASE) / n;
319
320         // increase audioclock to after the packet
321         m_audioClock += audioframe.duration;
322       }
323
324       if(audioframe.duration > 0)
325         m_duration = audioframe.duration;
326
327       // if demux source want's us to not display this, continue
328       if(m_decode.msg->GetPacketDrop())
329         continue;
330
331       //If we are asked to drop this packet, return a size of zero. then it won't be played
332       //we currently still decode the audio.. this is needed since we still need to know it's
333       //duration to make sure clock is updated correctly.
334       if( bDropPacket )
335         result |= DECODE_FLAG_DROP;
336
337       return result;
338     }
339     // free the current packet
340     m_decode.Release();
341
342     if (m_messageQueue.ReceivedAbortRequest()) return DECODE_FLAG_ABORT;
343
344     CDVDMsg* pMsg;
345     int priority = (m_speed == DVD_PLAYSPEED_PAUSE /* && m_started */) ? 1 : 0;
346
347     int timeout;
348     if(m_duration > 0)
349       timeout = (int)(1000 * (m_duration / DVD_TIME_BASE + m_dvdAudio.GetCacheTime()));
350     else
351       timeout = 1000;
352
353     // read next packet and return -1 on error
354     MsgQueueReturnCode ret = m_messageQueue.Get(&pMsg, timeout, priority);
355
356     if (ret == MSGQ_TIMEOUT)
357       return DECODE_FLAG_TIMEOUT;
358
359     if (MSGQ_IS_ERROR(ret) || ret == MSGQ_ABORT)
360       return DECODE_FLAG_ABORT;
361
362     if (pMsg->IsType(CDVDMsg::DEMUXER_PACKET))
363     {
364       m_decode.Attach((CDVDMsgDemuxerPacket*)pMsg);
365       m_ptsInput.Add( m_decode.size, m_decode.dts );
366     }
367     else if (pMsg->IsType(CDVDMsg::GENERAL_STREAMCHANGE))
368     {
369       CDVDMsgGeneralStreamChange* pMsgStreamChange = (CDVDMsgGeneralStreamChange*)pMsg;
370       CDVDStreamInfo* hints = pMsgStreamChange->GetStreamInfo();
371
372       /* received a stream change, reopen codec. */
373       /* we should really not do this until first packet arrives, to have a probe buffer */
374
375       /* try to open decoder, if none is found keep consuming packets */
376       OpenDecoder( *hints );
377     }
378     else if (pMsg->IsType(CDVDMsg::GENERAL_SYNCHRONIZE))
379     {
380       ((CDVDMsgGeneralSynchronize*)pMsg)->Wait( &m_bStop, SYNCSOURCE_AUDIO );
381       CLog::Log(LOGDEBUG, "CDVDPlayerAudio - CDVDMsg::GENERAL_SYNCHRONIZE");
382     }
383     else if (pMsg->IsType(CDVDMsg::GENERAL_RESYNC))
384     { //player asked us to set internal clock
385       CDVDMsgGeneralResync* pMsgGeneralResync = (CDVDMsgGeneralResync*)pMsg;
386
387       if (pMsgGeneralResync->m_timestamp != DVD_NOPTS_VALUE)
388         m_audioClock = pMsgGeneralResync->m_timestamp;
389
390       m_ptsOutput.Add(m_audioClock, m_dvdAudio.GetDelay(), 0);
391       if (pMsgGeneralResync->m_clock)
392       {
393         CLog::Log(LOGDEBUG, "CDVDPlayerAudio - CDVDMsg::GENERAL_RESYNC(%f, 1)", m_audioClock);
394         m_pClock->Discontinuity(CLOCK_DISC_NORMAL, m_ptsOutput.Current(), 0);
395       }
396       else
397         CLog::Log(LOGDEBUG, "CDVDPlayerAudio - CDVDMsg::GENERAL_RESYNC(%f, 0)", m_audioClock);
398     }
399     else if (pMsg->IsType(CDVDMsg::GENERAL_RESET))
400     {
401       if (m_pAudioCodec)
402         m_pAudioCodec->Reset();
403       m_decode.Release();
404       m_started = false;
405     }
406     else if (pMsg->IsType(CDVDMsg::GENERAL_FLUSH))
407     {
408       m_dvdAudio.Flush();
409       m_ptsOutput.Flush();
410       m_ptsInput.Flush();
411       m_syncclock = true;
412       m_stalled   = true;
413       m_started   = false;
414
415       if (m_pAudioCodec)
416         m_pAudioCodec->Reset();
417
418       m_decode.Release();
419     }
420     else if (pMsg->IsType(CDVDMsg::PLAYER_STARTED))
421     {
422       if(m_started)
423         m_messageParent.Put(new CDVDMsgInt(CDVDMsg::PLAYER_STARTED, DVDPLAYER_AUDIO));
424     }
425     else if (pMsg->IsType(CDVDMsg::GENERAL_EOF))
426     {
427       CLog::Log(LOGDEBUG, "CDVDPlayerAudio - CDVDMsg::GENERAL_EOF");
428       m_dvdAudio.Finish();
429     }
430     else if (pMsg->IsType(CDVDMsg::GENERAL_DELAY))
431     {
432       if (m_speed != DVD_PLAYSPEED_PAUSE)
433       {
434         double timeout = static_cast<CDVDMsgDouble*>(pMsg)->m_value;
435
436         CLog::Log(LOGDEBUG, "CDVDPlayerAudio - CDVDMsg::GENERAL_DELAY(%f)", timeout);
437
438         timeout *= (double)DVD_PLAYSPEED_NORMAL / abs(m_speed);
439         timeout += CDVDClock::GetAbsoluteClock();
440
441         while(!m_bStop && CDVDClock::GetAbsoluteClock() < timeout)
442           Sleep(1);
443       }
444     }
445     else if (pMsg->IsType(CDVDMsg::PLAYER_SETSPEED))
446     {
447       m_speed = static_cast<CDVDMsgInt*>(pMsg)->m_value;
448
449       if (m_speed == DVD_PLAYSPEED_PAUSE)
450       {
451         m_ptsOutput.Flush();
452         m_syncclock = true;
453         m_dvdAudio.Pause();
454       }
455       else
456         m_dvdAudio.Resume();
457     }
458     pMsg->Release();
459   }
460   return 0;
461 }
462
463 void CDVDPlayerAudio::OnStartup()
464 {
465   CThread::SetName("CDVDPlayerAudio");
466
467   m_decode.msg = NULL;
468   m_decode.Release();
469
470   g_dvdPerformanceCounter.EnableAudioDecodePerformance(ThreadHandle());
471 }
472
473 void CDVDPlayerAudio::Process()
474 {
475   CLog::Log(LOGNOTICE, "running thread: CDVDPlayerAudio::Process()");
476
477   int result;
478   bool packetadded(false);
479
480   DVDAudioFrame audioframe;
481   m_audioStats.Start();
482
483   while (!m_bStop)
484   {
485     //Don't let anybody mess with our global variables
486     result = DecodeFrame(audioframe, m_speed > DVD_PLAYSPEED_NORMAL || m_speed < 0); // blocks if no audio is available, but leaves critical section before doing so
487
488     if( result & DECODE_FLAG_ERROR )
489     {
490       CLog::Log(LOGDEBUG, "CDVDPlayerAudio::Process - Decode Error");
491       continue;
492     }
493
494     if( result & DECODE_FLAG_TIMEOUT )
495     {
496       m_stalled = true;
497       continue;
498     }
499
500     if( result & DECODE_FLAG_ABORT )
501     {
502       CLog::Log(LOGDEBUG, "CDVDPlayerAudio::Process - Abort received, exiting thread");
503       break;
504     }
505
506 #ifdef PROFILE /* during profiling we just drop all packets, after having decoded */
507     m_pClock->Discontinuity(CLOCK_DISC_NORMAL, audioframe.pts, 0);
508     continue;
509 #endif
510
511     if( audioframe.size == 0 )
512       continue;
513
514     packetadded = true;
515
516     // we have succesfully decoded an audio frame, setup renderer to match
517     if (!m_dvdAudio.IsValidFormat(audioframe))
518     {
519       m_dvdAudio.Destroy();
520       if(!m_dvdAudio.Create(audioframe, m_streaminfo.codec))
521         CLog::Log(LOGERROR, "%s - failed to create audio renderer", __FUNCTION__);
522       m_messageQueue.SetMaxTimeSize(8.0 - m_dvdAudio.GetCacheTotal());
523     }
524
525     if( result & DECODE_FLAG_DROP )
526     {
527       //frame should be dropped. Don't let audio move ahead of the current time thou
528       //we need to be able to start playing at any time
529       //when playing backwords, we try to keep as small buffers as possible
530
531       if(m_droptime == 0.0)
532         m_droptime = m_pClock->GetAbsoluteClock();
533       if(m_speed > 0)
534         m_droptime += audioframe.duration * DVD_PLAYSPEED_NORMAL / m_speed;
535       while( !m_bStop && m_droptime > m_pClock->GetAbsoluteClock() ) Sleep(1);
536
537       m_stalled = false;
538     }
539     else
540     {
541       m_droptime = 0.0;
542
543       // add any packets play
544       packetadded = OutputPacket(audioframe);
545
546       // we are not running until something is cached in output device
547       if(m_stalled && m_dvdAudio.GetCacheTime() > 0.0)
548         m_stalled = false;
549     }
550
551     // store the delay for this pts value so we can calculate the current playing
552     if(packetadded)
553     {
554       if(m_speed == DVD_PLAYSPEED_PAUSE)
555         m_ptsOutput.Add(audioframe.pts, m_dvdAudio.GetDelay() - audioframe.duration, 0);
556       else
557         m_ptsOutput.Add(audioframe.pts, m_dvdAudio.GetDelay() - audioframe.duration, audioframe.duration);
558     }
559
560     // signal to our parent that we have initialized
561     if(m_started == false)
562     {
563       m_started = true;
564       m_messageParent.Put(new CDVDMsgInt(CDVDMsg::PLAYER_STARTED, DVDPLAYER_AUDIO));
565     }
566
567     if( m_ptsOutput.Current() == DVD_NOPTS_VALUE )
568       continue;
569
570     if( m_speed != DVD_PLAYSPEED_NORMAL )
571       continue;
572
573     if (packetadded)
574       HandleSyncError(audioframe.duration);
575   }
576 }
577
578 void CDVDPlayerAudio::HandleSyncError(double duration)
579 {
580   double clock = m_pClock->GetClock();
581   double error = m_ptsOutput.Current() - clock;
582   int64_t now;
583
584   if( fabs(error) > DVD_MSEC_TO_TIME(100) || m_syncclock )
585   {
586     m_pClock->Discontinuity(CLOCK_DISC_NORMAL, clock+error, 0);
587     if(m_speed == DVD_PLAYSPEED_NORMAL)
588       CLog::Log(LOGDEBUG, "CDVDPlayerAudio:: Discontinuity - was:%f, should be:%f, error:%f", clock, clock+error, error);
589
590     m_errorbuff = 0;
591     m_errorcount = 0;
592     m_error = 0;
593     m_syncclock = false;
594     m_errortime = CurrentHostCounter();
595
596     return;
597   }
598
599   if (m_speed != DVD_PLAYSPEED_NORMAL)
600   {
601     m_errorbuff = 0;
602     m_errorcount = 0;
603     m_error = 0;
604     m_errortime = CurrentHostCounter();
605     return;
606   }
607
608   m_errorbuff += error;
609   m_errorcount++;
610
611   //check if measured error for 1 second
612   now = CurrentHostCounter();
613   if ((now - m_errortime) >= m_freq)
614   {
615     m_errortime = now;
616     m_error = m_errorbuff / m_errorcount;
617
618     m_errorbuff = 0;
619     m_errorcount = 0;
620
621     if (fabs(m_error) > DVD_MSEC_TO_TIME(10))
622     {
623       m_pClock->Discontinuity(CLOCK_DISC_NORMAL, clock+m_error, 0);
624       if(m_speed == DVD_PLAYSPEED_NORMAL)
625         CLog::Log(LOGDEBUG, "CDVDPlayerAudio:: Discontinuity - was:%f, should be:%f, error:%f", clock, clock+m_error, m_error);
626     }
627   }
628 }
629
630 bool CDVDPlayerAudio::OutputPacket(DVDAudioFrame &audioframe)
631 {
632   m_dvdAudio.AddPackets(audioframe);
633   return true;
634 }
635
636 void CDVDPlayerAudio::OnExit()
637 {
638   g_dvdPerformanceCounter.DisableAudioDecodePerformance();
639
640   CLog::Log(LOGNOTICE, "thread end: CDVDPlayerAudio::OnExit()");
641 }
642
643 void CDVDPlayerAudio::SetSpeed(int speed)
644 {
645   if(m_messageQueue.IsInited())
646     m_messageQueue.Put( new CDVDMsgInt(CDVDMsg::PLAYER_SETSPEED, speed), 1 );
647   else
648     m_speed = speed;
649 }
650
651 void CDVDPlayerAudio::Flush()
652 {
653   m_messageQueue.Flush();
654   m_messageQueue.Put( new CDVDMsg(CDVDMsg::GENERAL_FLUSH), 1);
655 }
656
657 void CDVDPlayerAudio::WaitForBuffers()
658 {
659   // make sure there are no more packets available
660   m_messageQueue.WaitUntilEmpty();
661
662   // make sure almost all has been rendered
663   // leave 500ms to avound buffer underruns
664   double delay = m_dvdAudio.GetCacheTime();
665   if(delay > 0.5)
666     Sleep((int)(1000 * (delay - 0.5)));
667 }
668
669 string CDVDPlayerAudio::GetPlayerInfo()
670 {
671   std::ostringstream s;
672   s << "aq:"     << setw(2) << min(99,m_messageQueue.GetLevel()) << "%";
673   s << ", kB/s:" << fixed << setprecision(2) << (double)GetAudioBitrate() / 1024.0;
674   return s.str();
675 }
676
677 int CDVDPlayerAudio::GetAudioBitrate()
678 {
679   return (int)m_audioStats.GetBitrate();
680 }