Bug 469266 - Rewrite Wave decoder playback loop to reduce latency, improve accuracy of playback position reporting, and fire timeupdate events.

--HG--
extra : rebase_source : 2c2faa5af34abe2b5fe14d150207d9c375f7d567
This commit is contained in:
Matthew Gregan 2009-02-17 17:56:16 +13:00
parent a2bc6f0ca1
commit f48db6edc8
8 changed files with 337 additions and 396 deletions

View File

@ -92,30 +92,12 @@ class nsAudioStream
// Block until buffered audio data has been consumed.
void Drain();
// Pause sound playback.
void Pause();
// Resume sound playback.
void Resume();
// Return the position (in seconds) of the audio sample currently being
// played by the audio hardware.
double GetTime();
private:
double mVolume;
void* mAudioHandle;
int mRate;
int mChannels;
// The byte position in the audio buffer where playback was last paused.
PRInt64 mSavedPauseBytes;
PRInt64 mPauseBytes;
float mStartTime;
float mPauseTime;
PRInt64 mSamplesBuffered;
SampleFormat mFormat;
// When a Write() request is made, and the number of samples
@ -123,7 +105,5 @@ class nsAudioStream
// backend, the remaining samples are stored in this variable. They
// will be written on the next Write() request.
nsTArray<short> mBufferOverflow;
PRPackedBool mPaused;
};
#endif

View File

@ -67,10 +67,6 @@ public:
// request to return an error. Call on main thread only.
void Cancel();
// Return the number of bytes buffered from the file. This can safely
// be read without blocking.
PRUint32 Available();
// Suspend any downloads that are in progress.
void Suspend();

View File

@ -88,7 +88,6 @@ public:
virtual nsresult Read(char* aBuffer, PRUint32 aCount, PRUint32* aBytes) = 0;
virtual nsresult Seek(PRInt32 aWhence, PRInt64 aOffset) = 0;
virtual PRInt64 Tell() = 0;
virtual PRUint32 Available() = 0;
virtual void Cancel() { }
virtual nsIPrincipal* GetCurrentPrincipal() = 0;
virtual void Suspend() = 0;
@ -172,10 +171,6 @@ class nsMediaStream
// Can be called from any thread.
PRInt64 Tell();
// Return the number of bytes available in the stream that can be
// read without blocking. Can be called from any thread.
PRUint32 Available();
// Cancels any currently blocking request and forces that request to
// return an error. Call on main thread only.
void Cancel();

View File

@ -200,6 +200,8 @@ class nsWaveDecoder : public nsMediaDecoder
virtual void SetTotalBytes(PRInt64 aBytes);
void PlaybackPositionChanged();
// Setter for the duration. This is ignored by the wave decoder since it can
// compute the duration directly from the wave data.
virtual void SetDuration(PRInt64 aDuration);
@ -268,6 +270,12 @@ private:
// state machine will validate the offset against the current media.
float mTimeOffset;
// The current playback position of the media resource in units of
// seconds. This is updated every time a block of audio is passed to the
// backend (unless an prior update is still pending). It is read and
// written from the main thread only.
float mCurrentTime;
// Copy of the current time, duration, and ended state when the state
// machine was disposed. Used to respond to time and duration queries
// with sensible values after the state machine is destroyed.

View File

@ -52,11 +52,6 @@ PRLogModuleInfo* gAudioStreamLog = nsnull;
#define FAKE_BUFFER_SIZE 176400
static float CurrentTimeInSeconds()
{
return PR_IntervalToMilliseconds(PR_IntervalNow()) / 1000.0;
}
void nsAudioStream::InitLibrary()
{
#ifdef PR_LOGGING
@ -73,12 +68,7 @@ nsAudioStream::nsAudioStream() :
mAudioHandle(0),
mRate(0),
mChannels(0),
mSavedPauseBytes(0),
mPauseBytes(0),
mPauseTime(0.0),
mSamplesBuffered(0),
mFormat(FORMAT_S16_LE),
mPaused(PR_FALSE)
mFormat(FORMAT_S16_LE)
{
}
@ -87,7 +77,6 @@ void nsAudioStream::Init(PRInt32 aNumChannels, PRInt32 aRate, SampleFormat aForm
mRate = aRate;
mChannels = aNumChannels;
mFormat = aFormat;
mStartTime = CurrentTimeInSeconds();
if (sa_stream_create_pcm(reinterpret_cast<sa_stream_t**>(&mAudioHandle),
NULL,
SA_MODE_WRONLY,
@ -121,7 +110,6 @@ void nsAudioStream::Write(const void* aBuf, PRUint32 aCount)
NS_ABORT_IF_FALSE(aCount % mChannels == 0,
"Buffer size must be divisible by channel count");
mSamplesBuffered += aCount;
PRUint32 offset = mBufferOverflow.Length();
PRInt32 count = aCount + offset;
@ -214,13 +202,8 @@ void nsAudioStream::SetVolume(float aVolume)
void nsAudioStream::Drain()
{
if (!mAudioHandle) {
// mSamplesBuffered already accounts for the data in the
// mBufferOverflow array.
PRUint32 drainTime = (float(mSamplesBuffered) / mRate / mChannels - GetTime()) * 1000.0;
PR_Sleep(PR_MillisecondsToInterval(drainTime));
if (!mAudioHandle)
return;
}
// Write any remaining unwritten sound data in the overflow buffer
if (!mBufferOverflow.IsEmpty()) {
@ -235,69 +218,3 @@ void nsAudioStream::Drain()
Shutdown();
}
}
void nsAudioStream::Pause()
{
if (mPaused)
return;
// Save the elapsed playback time. Used to offset the wall-clock time
// when resuming.
mPauseTime = CurrentTimeInSeconds() - mStartTime;
mPaused = PR_TRUE;
if (!mAudioHandle)
return;
int64_t bytes = 0;
#if !defined(WIN32)
sa_stream_get_position(static_cast<sa_stream_t*>(mAudioHandle), SA_POSITION_WRITE_SOFTWARE, &bytes);
#endif
mSavedPauseBytes = bytes;
sa_stream_pause(static_cast<sa_stream_t*>(mAudioHandle));
}
void nsAudioStream::Resume()
{
if (!mPaused)
return;
// Reset the start time to the current time offset backwards by the
// elapsed time saved when the stream paused.
mStartTime = CurrentTimeInSeconds() - mPauseTime;
mPaused = PR_FALSE;
if (!mAudioHandle)
return;
sa_stream_resume(static_cast<sa_stream_t*>(mAudioHandle));
#if !defined(WIN32)
mPauseBytes += mSavedPauseBytes;
#endif
}
double nsAudioStream::GetTime()
{
// If the audio backend failed to open, emulate the current playback
// position using the system clock.
if (!mAudioHandle) {
if (mPaused) {
return mPauseTime;
}
float curTime = CurrentTimeInSeconds() - mStartTime;
float maxTime = float(mSamplesBuffered) / mRate / mChannels;
return NS_MIN(curTime, maxTime);
}
int64_t bytes = 0;
#if defined(WIN32)
sa_stream_get_position(static_cast<sa_stream_t*>(mAudioHandle), SA_POSITION_WRITE_HARDWARE, &bytes);
#else
sa_stream_get_position(static_cast<sa_stream_t*>(mAudioHandle), SA_POSITION_WRITE_SOFTWARE, &bytes);
#endif
return double(bytes + mPauseBytes) / (sizeof(short) * mChannels * mRate);
}

View File

@ -48,11 +48,6 @@ void nsChannelReader::Cancel()
mStream.Cancel();
}
PRUint32 nsChannelReader::Available()
{
return mStream.Available();
}
OggPlayErrorCode nsChannelReader::initialise(int aBlock)
{
return E_OGGPLAY_OK;

View File

@ -71,7 +71,6 @@ public:
virtual nsresult Read(char* aBuffer, PRUint32 aCount, PRUint32* aBytes);
virtual nsresult Seek(PRInt32 aWhence, PRInt64 aOffset);
virtual PRInt64 Tell();
virtual PRUint32 Available();
virtual void Cancel();
virtual nsIPrincipal* GetCurrentPrincipal();
virtual void Suspend();
@ -186,20 +185,6 @@ PRInt64 nsDefaultStreamStrategy::Tell()
return mPosition;
}
PRUint32 nsDefaultStreamStrategy::Available()
{
// The request pulls from the pipe, not the channels input
// stream. This allows calling from any thread as the pipe is
// threadsafe.
nsAutoLock lock(mLock);
if (!mPipeInput)
return 0;
PRUint32 count = 0;
mPipeInput->Available(&count);
return count;
}
void nsDefaultStreamStrategy::Cancel()
{
if (mListener)
@ -239,7 +224,6 @@ public:
virtual nsresult Read(char* aBuffer, PRUint32 aCount, PRUint32* aBytes);
virtual nsresult Seek(PRInt32 aWhence, PRInt64 aOffset);
virtual PRInt64 Tell();
virtual PRUint32 Available();
virtual nsIPrincipal* GetCurrentPrincipal();
virtual void Suspend();
virtual void Resume();
@ -419,17 +403,6 @@ PRInt64 nsFileStreamStrategy::Tell()
return offset;
}
PRUint32 nsFileStreamStrategy::Available()
{
nsAutoLock lock(mLock);
if (!mInput)
return 0;
PRUint32 count = 0;
mInput->Available(&count);
return count;
}
nsIPrincipal* nsFileStreamStrategy::GetCurrentPrincipal()
{
return mPrincipal;
@ -463,7 +436,6 @@ public:
virtual nsresult Read(char* aBuffer, PRUint32 aCount, PRUint32* aBytes);
virtual nsresult Seek(PRInt32 aWhence, PRInt64 aOffset);
virtual PRInt64 Tell();
virtual PRUint32 Available();
virtual void Cancel();
virtual nsIPrincipal* GetCurrentPrincipal();
virtual void Suspend();
@ -763,12 +735,13 @@ nsresult nsHttpStreamStrategy::Seek(PRInt32 aWhence, PRInt64 aOffset)
bytesRead += bytes;
} while (bytesRead != bytesAhead);
// We don't need to notify the decoder here that we seeked. It will
// look like we just read ahead a bit. In fact, we mustn't tell
// the decoder that we seeked, since the seek notification might
// race with the "data downloaded" notification after the data was
// written into the pipe, so that the seek notification
// happens *first*, hopelessly confusing the decoder.
// We don't need to notify the decoder here that we seeked, just that
// we read ahead. In fact, we mustn't tell the decoder that we seeked,
// since the seek notification might race with the "data downloaded"
// notification after the data was written into the pipe, so that the
// seek notification happens *first*, hopelessly confusing the
// decoder.
mDecoder->NotifyBytesConsumed(bytesRead);
return rv;
}
}
@ -795,20 +768,6 @@ PRInt64 nsHttpStreamStrategy::Tell()
return mAtEOF ? mDecoder->GetStatistics().mTotalBytes : mPosition;
}
PRUint32 nsHttpStreamStrategy::Available()
{
// The request pulls from the pipe, not the channels input
// stream. This allows calling from any thread as the pipe is
// threadsafe.
nsAutoLock lock(mLock);
if (!mPipeInput)
return 0;
PRUint32 count = 0;
mPipeInput->Available(&count);
return count;
}
void nsHttpStreamStrategy::Cancel()
{
mCancelled = PR_TRUE;
@ -909,11 +868,6 @@ PRInt64 nsMediaStream::Tell()
return mStreamStrategy->Tell();
}
PRUint32 nsMediaStream::Available()
{
return mStreamStrategy->Available();
}
void nsMediaStream::Cancel()
{
NS_ASSERTION(NS_IsMainThread(),

View File

@ -54,9 +54,13 @@
// Maximum number of seconds to wait when buffering.
#define BUFFERING_TIMEOUT 3
// The number of seconds of buffer data before buffering happens
// based on current playback rate.
#define BUFFERING_SECONDS_LOW_WATER_MARK 1
// Duration the playback loop will sleep after refilling the backend's audio
// buffers. The loop's goal is to keep AUDIO_BUFFER_LENGTH milliseconds of
// audio buffered to allow time to refill before the backend underruns.
// Should be a multiple of 10 to deal with poor timer granularity on some
// platforms.
#define AUDIO_BUFFER_WAKEUP 100
#define AUDIO_BUFFER_LENGTH (2 * AUDIO_BUFFER_WAKEUP)
// Magic values that identify RIFF chunks we're interested in.
#define RIFF_CHUNK_MAGIC 0x52494646
@ -74,13 +78,21 @@
// extended (for non-PCM encodings), but we skip any extended data.
#define WAVE_FORMAT_CHUNK_SIZE 16
// Size of format chunk including RIFF chunk header.
#define WAVE_FORMAT_SIZE (RIFF_CHUNK_HEADER_SIZE + WAVE_FORMAT_CHUNK_SIZE)
// PCM encoding type from format chunk. Linear PCM is the only encoding
// supported by nsAudioStream.
#define WAVE_FORMAT_ENCODING_PCM 1
enum State {
STATE_LOADING_METADATA,
STATE_BUFFERING,
STATE_PLAYING,
STATE_SEEKING,
STATE_PAUSED,
STATE_ENDED,
STATE_ERROR,
STATE_SHUTDOWN
};
/*
A single nsWaveStateMachine instance is owned by the decoder, created
on-demand at load time. Upon creation, the decoder immediately
@ -101,17 +113,6 @@
class nsWaveStateMachine : public nsRunnable
{
public:
enum State {
STATE_LOADING_METADATA,
STATE_BUFFERING,
STATE_PLAYING,
STATE_SEEKING,
STATE_PAUSED,
STATE_ENDED,
STATE_ERROR,
STATE_SHUTDOWN
};
nsWaveStateMachine(nsWaveDecoder* aDecoder, nsMediaStream* aStream,
PRUint32 aBufferWaitTime, float aInitialVolume);
~nsWaveStateMachine();
@ -148,18 +149,9 @@ public:
// Returns true if the state machine has reached the end of playback. Threadsafe.
PRBool IsEnded();
// Called by the decoder to indicate that the media stream has closed.
// aAtEnd is true if we read to the end of the file.
void StreamEnded(PRBool aAtEnd);
// Main state machine loop. Runs forever, until shutdown state is reached.
NS_IMETHOD Run();
// Called by the decoder when the SeekStarted event runs. This ensures
// the current time offset of the state machine is updated to the new seek
// position at the appropriate time.
void UpdateTimeOffset(float aTime);
// Called by the decoder, on the main thread.
nsMediaDecoder::Statistics GetStatistics();
@ -177,13 +169,19 @@ public:
// Called by the main thread only
nsHTMLMediaElement::NextFrameStatus GetNextFrameStatus();
private:
// Clear the flag indicating that a playback position change event is
// currently queued and return the current time. This is called from the
// main thread.
float GetTimeForPositionChange();
private:
// Returns PR_TRUE if we're in shutdown state. Threadsafe.
PRBool IsShutdown();
// Reads from the media stream. Returns PR_FALSE on failure or EOF.
PRBool ReadAll(char* aBuf, PRUint32 aSize);
// Reads from the media stream. Returns PR_FALSE on failure or EOF. If
// aBytesRead is non-null, the number of bytes read will be returned via
// this.
PRBool ReadAll(char* aBuf, PRInt64 aSize, PRInt64* aBytesRead);
// Change the current state and wake the playback thread if it is waiting
// on mMonitor. Used by public member functions called from both threads,
@ -200,6 +198,11 @@ private:
// the stream data is a RIFF bitstream containing WAVE data.
PRBool LoadRIFFChunk();
// Read forward in the stream until aWantedChunk is found. Return chunk
// size in aChunkSize. aChunkSize will not be rounded up if the chunk
// size is odd.
PRBool ScanForwardUntil(PRUint32 aWantedChunk, PRUint32* aChunkSize);
// Scan forward in the stream looking for the WAVE format chunk. If
// found, parse and validate required metadata, then use it to set
// mSampleRate, mChannels, mSampleSize, and mSampleFormat.
@ -210,29 +213,40 @@ private:
// mWavePCMOffset.
PRBool FindDataOffset();
// Return the length of the PCM data.
PRInt64 GetDataLength();
// Fire a PlaybackPositionChanged event. If aCoalesce is true and a
// PlaybackPositionChanged event is already pending, an event is not
// fired.
void FirePositionChanged(PRBool aCoalesce);
// Returns the number of seconds that aBytes represents based on the
// current audio parameters. e.g. 176400 bytes is 1 second at 16-bit
// stereo 44.1kHz.
float BytesToTime(PRUint32 aBytes) const
float BytesToTime(PRInt64 aBytes) const
{
NS_ABORT_IF_FALSE(mMetadataValid, "Requires valid metadata");
NS_ABORT_IF_FALSE(aBytes >= 0, "Must be >= 0");
return float(aBytes) / mSampleRate / mSampleSize;
}
// Returns the number of bytes that aTime represents based on the current
// audio parameters. e.g. 1 second is 176400 bytes at 16-bit stereo
// 44.1kHz.
PRUint32 TimeToBytes(float aTime) const
PRInt64 TimeToBytes(float aTime) const
{
NS_ABORT_IF_FALSE(mMetadataValid, "Requires valid metadata");
return PRUint32(aTime * mSampleRate * mSampleSize);
NS_ABORT_IF_FALSE(aTime >= 0.0, "Must be >= 0");
return RoundDownToSample(PRInt64(aTime * mSampleRate * mSampleSize));
}
// Rounds aBytes down to the nearest complete sample. Assumes beginning
// of byte range is already sample aligned by caller.
PRUint32 RoundDownToSample(PRUint32 aBytes) const
PRInt64 RoundDownToSample(PRInt64 aBytes) const
{
NS_ABORT_IF_FALSE(mMetadataValid, "Requires valid metadata");
NS_ABORT_IF_FALSE(aBytes >= 0, "Must be >= 0");
return aBytes - (aBytes % mSampleSize);
}
@ -258,16 +272,13 @@ private:
// Maximum time in milliseconds to spend waiting for data during buffering.
PRUint32 mBufferingWait;
// Maximum number of bytes to wait for during buffering.
PRUint32 mBufferingBytes;
// Machine time that buffering began, used with mBufferingWait to time out
// buffering.
PRIntervalTime mBufferingStart;
// Maximum number of bytes mAudioStream buffers internally. Used to
// calculate next wakeup time after refilling audio buffers.
PRUint32 mAudioBufferSize;
// Download position where we should stop buffering. Only accessed
// in the decoder thread.
PRInt64 mBufferingEndOffset;
/*
Metadata extracted from the WAVE header. Used to initialize the audio
@ -289,11 +300,11 @@ private:
// Size of PCM data stored in the WAVE as reported by the data chunk in
// the media.
PRUint32 mWaveLength;
PRInt64 mWaveLength;
// Start offset of the PCM data in the media stream. Extends mWaveLength
// bytes.
PRUint32 mWavePCMOffset;
PRInt64 mWavePCMOffset;
/*
All member variables following this comment are accessed by both
@ -326,15 +337,9 @@ private:
// Volume that the audio backend will be initialized with.
float mInitialVolume;
// Time position (in seconds) to offset current time from audio stream.
// Set when the seek started event runs and when the stream is closed
// during shutdown.
float mTimeOffset;
// Set when StreamEnded has fired to indicate that we should not expect
// any more data from mStream than what is already buffered (i.e. what
// Available() reports).
PRPackedBool mExpectMoreData;
// Playback position (in bytes), updated as the playback loop runs and
// upon seeking.
PRInt64 mTimeOffset;
// Time position (in seconds) to seek to. Set by Seek(float).
float mSeekTime;
@ -343,6 +348,12 @@ private:
// mChannels, mSampleSize, mSampleFormat, mWaveLength, mWavePCMOffset must
// check this flag before assuming the values are valid.
PRPackedBool mMetadataValid;
// True if an event to notify about a change in the playback position has
// been queued, but not yet run. It is set to false when the event is
// run. This allows coalescing of these events as they can be produced
// many times per second.
PRPackedBool mPositionChangeQueued;
};
nsWaveStateMachine::nsWaveStateMachine(nsWaveDecoder* aDecoder, nsMediaStream* aStream,
@ -350,9 +361,8 @@ nsWaveStateMachine::nsWaveStateMachine(nsWaveDecoder* aDecoder, nsMediaStream* a
: mDecoder(aDecoder),
mStream(aStream),
mBufferingWait(aBufferWaitTime),
mBufferingBytes(0),
mBufferingStart(0),
mAudioBufferSize(0),
mBufferingEndOffset(0),
mSampleRate(0),
mChannels(0),
mSampleSize(0),
@ -366,10 +376,10 @@ nsWaveStateMachine::nsWaveStateMachine(nsWaveDecoder* aDecoder, nsMediaStream* a
mDownloadPosition(0),
mPlaybackPosition(0),
mInitialVolume(aInitialVolume),
mTimeOffset(0.0),
mExpectMoreData(PR_TRUE),
mTimeOffset(0),
mSeekTime(0.0),
mMetadataValid(PR_FALSE)
mMetadataValid(PR_FALSE),
mPositionChangeQueued(PR_FALSE)
{
mMonitor = nsAutoMonitor::NewMonitor("nsWaveStateMachine");
mDownloadStatistics.Start(PR_IntervalNow());
@ -449,14 +459,7 @@ nsWaveStateMachine::GetDuration()
{
nsAutoMonitor monitor(mMonitor);
if (mMetadataValid) {
PRUint32 length = mWaveLength;
// If the decoder has a valid content length, and it's shorter than the
// expected length of the PCM data, calculate the playback duration from
// the content length rather than the expected PCM data length.
if (mTotalBytes >= 0 && mTotalBytes - mWavePCMOffset < length) {
length = mTotalBytes - mWavePCMOffset;
}
return BytesToTime(length);
return BytesToTime(GetDataLength());
}
return std::numeric_limits<float>::quiet_NaN();
}
@ -465,11 +468,10 @@ float
nsWaveStateMachine::GetCurrentTime()
{
nsAutoMonitor monitor(mMonitor);
double time = 0.0;
if (mAudioStream) {
time = mAudioStream->GetTime();
if (mMetadataValid) {
return BytesToTime(mTimeOffset);
}
return float(time + mTimeOffset);
return std::numeric_limits<float>::quiet_NaN();
}
PRBool
@ -486,30 +488,25 @@ nsWaveStateMachine::IsEnded()
return mState == STATE_ENDED || mState == STATE_SHUTDOWN;
}
void
nsWaveStateMachine::StreamEnded(PRBool aAtEnd)
{
nsAutoMonitor monitor(mMonitor);
mExpectMoreData = PR_FALSE;
// If we know the content length, set the bytes downloaded to this
// so the final progress event gets the correct final value.
if (mTotalBytes >= 0) {
mDownloadPosition = mTotalBytes;
}
}
nsHTMLMediaElement::NextFrameStatus
nsWaveStateMachine::GetNextFrameStatus()
{
nsAutoMonitor monitor(mMonitor);
if (mPlaybackPosition < mDownloadPosition)
return nsHTMLMediaElement::NEXT_FRAME_AVAILABLE;
if (mState == STATE_BUFFERING)
return nsHTMLMediaElement::NEXT_FRAME_UNAVAILABLE_BUFFERING;
if (mPlaybackPosition < mDownloadPosition)
return nsHTMLMediaElement::NEXT_FRAME_AVAILABLE;
return nsHTMLMediaElement::NEXT_FRAME_UNAVAILABLE;
}
float
nsWaveStateMachine::GetTimeForPositionChange()
{
nsAutoMonitor monitor(mMonitor);
mPositionChangeQueued = PR_FALSE;
return GetCurrentTime();
}
NS_IMETHODIMP
nsWaveStateMachine::Run()
{
@ -552,10 +549,12 @@ nsWaveStateMachine::Run()
case STATE_BUFFERING: {
PRIntervalTime now = PR_IntervalNow();
if ((PR_IntervalToMilliseconds(now - mBufferingStart) < mBufferingWait) &&
mStream->Available() < mBufferingBytes) {
LOG(PR_LOG_DEBUG, ("Buffering data until %d bytes or %d milliseconds\n",
mBufferingBytes - mStream->Available(),
mBufferingWait - (now - mBufferingStart)));
mDownloadPosition < mBufferingEndOffset &&
(mTotalBytes < 0 || mDownloadPosition < mTotalBytes)) {
LOG(PR_LOG_DEBUG,
("In buffering: buffering data until %d bytes available or %d milliseconds\n",
PRUint32(mBufferingEndOffset - mDownloadPosition),
mBufferingWait - (PR_IntervalToMilliseconds(now - mBufferingStart))));
monitor.Wait(PR_MillisecondsToInterval(1000));
} else {
ChangeState(mNextState);
@ -570,86 +569,102 @@ nsWaveStateMachine::Run()
case STATE_PLAYING: {
if (!mAudioStream) {
OpenAudioStream();
} else {
mAudioStream->Resume();
if (!mAudioStream) {
mState = STATE_ERROR;
break;
}
}
if (mStream->Available() < mSampleSize) {
if (mExpectMoreData) {
// Buffer until mBufferingWait milliseconds of data is available.
mBufferingBytes = TimeToBytes(float(mBufferingWait) / 1000.0);
mBufferingStart = PR_IntervalNow();
PRUint32 startTime = PR_IntervalToMilliseconds(PR_IntervalNow());
startTime -= AUDIO_BUFFER_LENGTH;
PRIntervalTime lastWakeup = PR_MillisecondsToInterval(startTime);
nsCOMPtr<nsIRunnable> event =
NS_NEW_RUNNABLE_METHOD(nsWaveDecoder, mDecoder, UpdateReadyStateForData);
NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL);
do {
PRIntervalTime now = PR_IntervalNow();
PRInt32 sleepTime = PR_IntervalToMilliseconds(now - lastWakeup);
lastWakeup = now;
ChangeState(STATE_BUFFERING);
} else {
// Media stream has ended and there is less data available than a
// single sample so end playback.
// We aim to have AUDIO_BUFFER_LENGTH milliseconds of audio
// buffered, but only sleep for AUDIO_BUFFER_WAKEUP milliseconds
// (waking early to refill before the backend underruns). Since we
// wake early, we only buffer sleepTime milliseconds of audio since
// there is still AUDIO_BUFFER_LENGTH - sleepTime milliseconds of
// audio buffered.
PRInt32 targetTime = AUDIO_BUFFER_LENGTH;
if (sleepTime < targetTime) {
targetTime = sleepTime;
}
PRInt64 len = TimeToBytes(float(targetTime) / 1000.0f);
PRInt64 leftToPlay = GetDataLength() - mTimeOffset;
if (leftToPlay <= len) {
len = leftToPlay;
ChangeState(STATE_ENDED);
}
} else {
// Assuming enough data is available from the network, we aim to
// completely fill the audio backend's buffers with data. This
// allows us plenty of time to wake up and refill the buffers
// without an underrun occurring.
PRUint32 sampleSize = mSampleFormat == nsAudioStream::FORMAT_U8 ? 1 : 2;
PRUint32 len = RoundDownToSample(NS_MIN(mStream->Available(),
PRUint32(mAudioStream->Available() * sampleSize)));
if (len) {
nsAutoArrayPtr<char> buf(new char[len]);
PRUint32 got = 0;
PRInt64 available = mDownloadPosition - mPlaybackPosition;
// don't buffer if we're at the end of the stream
if (mState != STATE_ENDED && available < len) {
mBufferingStart = PR_IntervalNow();
mBufferingEndOffset = mDownloadPosition + TimeToBytes(float(mBufferingWait) / 1000.0f);
mNextState = mState;
ChangeState(STATE_BUFFERING);
nsCOMPtr<nsIRunnable> event =
NS_NEW_RUNNABLE_METHOD(nsWaveDecoder, mDecoder, UpdateReadyStateForData);
NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL);
break;
}
if (len > 0) {
nsAutoArrayPtr<char> buf(new char[size_t(len)]);
PRInt64 got = 0;
monitor.Exit();
if (NS_FAILED(mStream->Read(buf.get(), len, &got))) {
NS_WARNING("Stream read failed");
}
PRBool ok = ReadAll(buf.get(), len, &got);
PRInt64 streamPos = mStream->Tell();
monitor.Enter();
if (got == 0) {
// Reached EOF.
if (!ok) {
ChangeState(STATE_ENDED);
if (got == 0) {
break;
}
}
// If we got less data than requested, go ahead and write what we
// got to the audio hardware. It's unlikely that this can happen
// since we never attempt to read more data than what is already
// buffered.
len = RoundDownToSample(got);
// Calculate difference between the current media stream position
// and the expected end of the PCM data.
PRInt64 endDelta = mWavePCMOffset + mWaveLength - mStream->Tell();
PRInt64 endDelta = mWavePCMOffset + mWaveLength - streamPos;
if (endDelta < 0) {
// Read past the end of PCM data. Adjust len to avoid playing
// Read past the end of PCM data. Adjust got to avoid playing
// back trailing data.
len -= -endDelta;
if (RoundDownToSample(len) != len) {
NS_WARNING("PCM data does not end with complete sample");
len = RoundDownToSample(len);
}
got -= -endDelta;
ChangeState(STATE_ENDED);
}
PRUint32 lengthInSamples = len;
if (mSampleFormat == nsAudioStream::FORMAT_S16_LE) {
lengthInSamples /= sizeof(short);
if (mState == STATE_ENDED) {
got = RoundDownToSample(got);
}
PRUint32 sampleSize = mSampleFormat == nsAudioStream::FORMAT_U8 ? 1 : 2;
NS_ABORT_IF_FALSE(got % sampleSize == 0, "Must write complete samples");
PRUint32 lengthInSamples = got / sampleSize;
monitor.Exit();
mAudioStream->Write(buf.get(), lengthInSamples);
monitor.Enter();
mTimeOffset += got;
FirePositionChanged(PR_FALSE);
}
// To avoid waking up too frequently to top up these buffers,
// calculate the duration of the currently buffered data and sleep
// until most of the buffered data has been consumed. We can't
// sleep for the entire duration because we might not wake up in
// time to refill the buffers, causing an underrun. To avoid this,
// wake up when approximately half the buffered data has been
// consumed. This could be made smarter, but at least avoids waking
// up frequently to perform small buffer refills.
float nextWakeup = BytesToTime(mAudioBufferSize - mAudioStream->Available() * sizeof(short)) * 1000.0 / 2.0;
monitor.Wait(PR_MillisecondsToInterval(PRUint32(nextWakeup)));
}
if (mState == STATE_PLAYING) {
monitor.Wait(PR_MillisecondsToInterval(AUDIO_BUFFER_WAKEUP));
}
} while (mState == STATE_PLAYING);
break;
}
@ -672,7 +687,9 @@ nsWaveStateMachine::Run()
// Calculate relative offset within PCM data.
PRInt64 position = RoundDownToSample(TimeToBytes(seekTime));
NS_ABORT_IF_FALSE(position >= 0 && position <= mWaveLength, "Invalid seek position");
NS_ABORT_IF_FALSE(position >= 0 && position <= GetDataLength(), "Invalid seek position");
mTimeOffset = position;
// If position==0, instead of seeking to position+mWavePCMOffset,
// we want to first seek to 0 before seeking to
@ -704,12 +721,6 @@ nsWaveStateMachine::Run()
break;
}
monitor.Exit();
nsCOMPtr<nsIRunnable> stopEvent =
NS_NEW_RUNNABLE_METHOD(nsWaveDecoder, mDecoder, SeekingStopped);
NS_DispatchToMainThread(stopEvent, NS_DISPATCH_SYNC);
monitor.Enter();
if (mState == STATE_SEEKING && mSeekTime == seekTime) {
// Special case: if a seek was requested during metadata load,
// mNextState will have been clobbered. This can only happen when
@ -722,29 +733,30 @@ nsWaveStateMachine::Run()
}
ChangeState(nextState);
}
FirePositionChanged(PR_TRUE);
monitor.Exit();
nsCOMPtr<nsIRunnable> stopEvent =
NS_NEW_RUNNABLE_METHOD(nsWaveDecoder, mDecoder, SeekingStopped);
NS_DispatchToMainThread(stopEvent, NS_DISPATCH_SYNC);
monitor.Enter();
}
break;
case STATE_PAUSED:
if (mAudioStream) {
mAudioStream->Pause();
}
monitor.Wait();
break;
case STATE_ENDED:
FirePositionChanged(PR_TRUE);
if (mAudioStream) {
monitor.Exit();
mAudioStream->Drain();
monitor.Enter();
mTimeOffset += mAudioStream->GetTime();
}
// Dispose the audio stream early (before SHUTDOWN) so that
// GetCurrentTime no longer attempts to query the audio backend for
// stream time.
CloseAudioStream();
if (mState != STATE_SHUTDOWN) {
nsCOMPtr<nsIRunnable> event =
NS_NEW_RUNNABLE_METHOD(nsWaveDecoder, mDecoder, PlaybackEnded);
@ -765,9 +777,6 @@ nsWaveStateMachine::Run()
break;
case STATE_SHUTDOWN:
if (mAudioStream) {
mTimeOffset += mAudioStream->GetTime();
}
CloseAudioStream();
return NS_OK;
}
@ -776,20 +785,61 @@ nsWaveStateMachine::Run()
return NS_OK;
}
void
nsWaveStateMachine::UpdateTimeOffset(float aTime)
#if defined(DEBUG)
static PRBool
IsValidStateTransition(State aStartState, State aEndState)
{
nsAutoMonitor monitor(mMonitor);
mTimeOffset = NS_MIN(aTime, GetDuration());
if (mTimeOffset < 0.0) {
mTimeOffset = 0.0;
if (aEndState == STATE_SHUTDOWN) {
return PR_TRUE;
}
if (aStartState == aEndState) {
LOG(PR_LOG_WARNING, ("Transition to current state requested"));
return PR_TRUE;
}
switch (aStartState) {
case STATE_LOADING_METADATA:
if (aEndState == STATE_PLAYING || aEndState == STATE_SEEKING ||
aEndState == STATE_PAUSED || aEndState == STATE_ERROR)
return PR_TRUE;
break;
case STATE_BUFFERING:
if (aEndState == STATE_PLAYING || aEndState == STATE_PAUSED ||
aEndState == STATE_SEEKING)
return PR_TRUE;
break;
case STATE_PLAYING:
if (aEndState == STATE_BUFFERING || aEndState == STATE_SEEKING ||
aEndState == STATE_ENDED || aEndState == STATE_PAUSED)
return PR_TRUE;
break;
case STATE_SEEKING:
if (aEndState == STATE_PLAYING || aEndState == STATE_PAUSED)
return PR_TRUE;
break;
case STATE_PAUSED:
if (aEndState == STATE_PLAYING || aEndState == STATE_SEEKING)
return PR_TRUE;
break;
case STATE_ENDED:
case STATE_ERROR:
case STATE_SHUTDOWN:
break;
}
LOG(PR_LOG_ERROR, ("Invalid state transition from %d to %d", aStartState, aEndState));
return PR_FALSE;
}
#endif
void
nsWaveStateMachine::ChangeState(State aState)
{
nsAutoMonitor monitor(mMonitor);
#if defined(DEBUG)
NS_ABORT_IF_FALSE(IsValidStateTransition(mState, aState), "Invalid state transition");
#endif
mState = aState;
monitor.NotifyAll();
}
@ -805,7 +855,6 @@ nsWaveStateMachine::OpenAudioStream()
"Attempting to initialize audio stream with invalid metadata");
mAudioStream->Init(mChannels, mSampleRate, mSampleFormat);
mAudioStream->SetVolume(mInitialVolume);
mAudioBufferSize = mAudioStream->Available() * sizeof(short);
}
}
@ -835,7 +884,8 @@ nsWaveStateMachine::GetStatistics()
}
void
nsWaveStateMachine::SetTotalBytes(PRInt64 aBytes) {
nsWaveStateMachine::SetTotalBytes(PRInt64 aBytes)
{
nsAutoMonitor monitor(mMonitor);
mTotalBytes = aBytes;
}
@ -913,18 +963,25 @@ nsWaveStateMachine::IsShutdown()
}
PRBool
nsWaveStateMachine::ReadAll(char* aBuf, PRUint32 aSize)
nsWaveStateMachine::ReadAll(char* aBuf, PRInt64 aSize, PRInt64* aBytesRead = nsnull)
{
PRUint32 got = 0;
if (aBytesRead) {
*aBytesRead = 0;
}
do {
PRUint32 read = 0;
if (NS_FAILED(mStream->Read(aBuf + got, aSize - got, &read))) {
NS_WARNING("Stream read failed");
return PR_FALSE;
}
if (IsShutdown())
if (IsShutdown() || read == 0) {
return PR_FALSE;
}
got += read;
if (aBytesRead) {
*aBytesRead = got;
}
} while (got != aSize);
return PR_TRUE;
}
@ -958,28 +1015,59 @@ nsWaveStateMachine::LoadRIFFChunk()
return PR_TRUE;
}
PRBool
nsWaveStateMachine::ScanForwardUntil(PRUint32 aWantedChunk, PRUint32* aChunkSize)
{
NS_ENSURE_ARG_POINTER(aChunkSize);
*aChunkSize = 0;
for (;;) {
char chunkHeader[8];
const char* p = chunkHeader;
if (!ReadAll(chunkHeader, sizeof(chunkHeader))) {
return PR_FALSE;
}
PRUint32 magic = ReadUint32BE(&p);
PRUint32 size = ReadUint32LE(&p);
if (magic == aWantedChunk) {
*aChunkSize = size;
return PR_TRUE;
}
// RIFF chunks are two-byte aligned, so round up if necessary.
size += size % 2;
nsAutoArrayPtr<char> chunk(new char[size]);
if (!ReadAll(chunk.get(), size)) {
return PR_FALSE;
}
}
}
PRBool
nsWaveStateMachine::LoadFormatChunk()
{
PRUint32 rate, channels, sampleSize, sampleFormat;
char waveFormat[WAVE_FORMAT_SIZE];
PRUint32 fmtSize, rate, channels, sampleSize, sampleFormat;
char waveFormat[WAVE_FORMAT_CHUNK_SIZE];
const char* p = waveFormat;
// RIFF chunks are always word (two byte) aligned.
NS_ABORT_IF_FALSE(mStream->Tell() % 2 == 0,
"LoadFormatChunk called with unaligned stream");
// The "format" chunk may not directly follow the "riff" chunk, so skip
// over any intermediate chunks.
if (!ScanForwardUntil(FRMT_CHUNK_MAGIC, &fmtSize)) {
return PR_FALSE;
}
if (!ReadAll(waveFormat, sizeof(waveFormat))) {
return PR_FALSE;
}
if (ReadUint32BE(&p) != FRMT_CHUNK_MAGIC) {
NS_WARNING("Expected format chunk");
return PR_FALSE;
}
PRUint32 fmtsize = ReadUint32LE(&p);
if (ReadUint16LE(&p) != WAVE_FORMAT_ENCODING_PCM) {
NS_WARNING("WAVE is not uncompressed PCM, compressed encodings are not supported");
return PR_FALSE;
@ -1000,7 +1088,7 @@ nsWaveStateMachine::LoadFormatChunk()
// extension size of 0 bytes. Be polite and handle this rather than
// considering the file invalid. This code skips any extension of the
// "format" chunk.
if (fmtsize > WAVE_FORMAT_CHUNK_SIZE) {
if (fmtSize > WAVE_FORMAT_CHUNK_SIZE) {
char extLength[2];
const char* p = extLength;
@ -1009,7 +1097,7 @@ nsWaveStateMachine::LoadFormatChunk()
}
PRUint16 extra = ReadUint16LE(&p);
if (fmtsize - (WAVE_FORMAT_CHUNK_SIZE + 2) != extra) {
if (fmtSize - (WAVE_FORMAT_CHUNK_SIZE + 2) != extra) {
NS_WARNING("Invalid extended format chunk size");
return PR_FALSE;
}
@ -1062,33 +1150,8 @@ nsWaveStateMachine::FindDataOffset()
// The "data" chunk may not directly follow the "format" chunk, so skip
// over any intermediate chunks.
for (;;) {
char chunkHeader[8];
const char* p = chunkHeader;
if (!ReadAll(chunkHeader, sizeof(chunkHeader))) {
return PR_FALSE;
}
PRUint32 magic = ReadUint32BE(&p);
if (magic == DATA_CHUNK_MAGIC) {
length = ReadUint32LE(&p);
break;
}
if (magic == FRMT_CHUNK_MAGIC) {
LOG(PR_LOG_ERROR, ("Invalid WAVE: expected \"data\" chunk but found \"format\" chunk"));
return PR_FALSE;
}
PRUint32 size = ReadUint32LE(&p);
size += size % 2;
nsAutoArrayPtr<char> chunk(new char[size]);
if (!ReadAll(chunk.get(), size)) {
return PR_FALSE;
}
if (!ScanForwardUntil(DATA_CHUNK_MAGIC, &length)) {
return PR_FALSE;
}
offset = mStream->Tell();
@ -1108,12 +1171,41 @@ nsWaveStateMachine::FindDataOffset()
return PR_TRUE;
}
PRInt64
nsWaveStateMachine::GetDataLength()
{
NS_ABORT_IF_FALSE(mMetadataValid,
"Attempting to initialize audio stream with invalid metadata");
PRInt64 length = mWaveLength;
// If the decoder has a valid content length, and it's shorter than the
// expected length of the PCM data, calculate the playback duration from
// the content length rather than the expected PCM data length.
if (mTotalBytes >= 0 && mTotalBytes - mWavePCMOffset < length) {
length = mTotalBytes - mWavePCMOffset;
}
return length;
}
void
nsWaveStateMachine::FirePositionChanged(PRBool aCoalesce)
{
if (aCoalesce && mPositionChangeQueued) {
return;
}
mPositionChangeQueued = PR_TRUE;
nsCOMPtr<nsIRunnable> event = NS_NEW_RUNNABLE_METHOD(nsWaveDecoder, mDecoder, PlaybackPositionChanged);
NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL);
}
NS_IMPL_THREADSAFE_ISUPPORTS1(nsWaveDecoder, nsIObserver)
nsWaveDecoder::nsWaveDecoder()
: mInitialVolume(1.0),
mStream(nsnull),
mTimeOffset(0.0),
mCurrentTime(0.0),
mEndedCurrentTime(0.0),
mEndedDuration(std::numeric_limits<float>::quiet_NaN()),
mEnded(PR_FALSE),
@ -1149,10 +1241,7 @@ nsWaveDecoder::GetCurrentPrincipal()
float
nsWaveDecoder::GetCurrentTime()
{
if (mPlaybackStateMachine) {
return mPlaybackStateMachine->GetCurrentTime();
}
return mEndedCurrentTime;
return mCurrentTime;
}
nsresult
@ -1351,10 +1440,6 @@ nsWaveDecoder::ResourceLoaded()
if (mShuttingDown) {
return;
}
if (mPlaybackStateMachine) {
mPlaybackStateMachine->StreamEnded(PR_TRUE);
}
mResourceLoaded = PR_TRUE;
@ -1381,9 +1466,6 @@ nsWaveDecoder::NetworkError()
if (mElement) {
mElement->NetworkError();
}
if (mPlaybackStateMachine) {
mPlaybackStateMachine->StreamEnded(PR_FALSE);
}
Stop();
}
@ -1530,10 +1612,6 @@ nsWaveDecoder::SeekingStarted()
return;
}
if (mPlaybackStateMachine) {
mPlaybackStateMachine->UpdateTimeOffset(mTimeOffset);
}
if (mElement) {
mElement->SeekStarted();
}
@ -1597,6 +1675,24 @@ nsWaveDecoder::MediaErrorDecode()
#endif
}
void
nsWaveDecoder::PlaybackPositionChanged()
{
if (mShuttingDown) {
return;
}
float lastTime = mCurrentTime;
if (mPlaybackStateMachine) {
mCurrentTime = mPlaybackStateMachine->GetTimeForPositionChange();
}
if (mElement && lastTime != mCurrentTime) {
mElement->DispatchSimpleEvent(NS_LITERAL_STRING("timeupdate"));
}
}
void
nsWaveDecoder::SetDuration(PRInt64 /* aDuration */)
{