diff --git a/content/media/video/public/nsAudioStream.h b/content/media/video/public/nsAudioStream.h index 38e0525e678..b87ffb1e527 100644 --- a/content/media/video/public/nsAudioStream.h +++ b/content/media/video/public/nsAudioStream.h @@ -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 mBufferOverflow; - - PRPackedBool mPaused; }; #endif diff --git a/content/media/video/public/nsChannelReader.h b/content/media/video/public/nsChannelReader.h index 3246cdaf75d..eea457ea9e0 100644 --- a/content/media/video/public/nsChannelReader.h +++ b/content/media/video/public/nsChannelReader.h @@ -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(); diff --git a/content/media/video/public/nsMediaStream.h b/content/media/video/public/nsMediaStream.h index 2f67ebadea6..c4562472a89 100644 --- a/content/media/video/public/nsMediaStream.h +++ b/content/media/video/public/nsMediaStream.h @@ -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(); diff --git a/content/media/video/public/nsWaveDecoder.h b/content/media/video/public/nsWaveDecoder.h index 8321f90fb89..af27d1307b1 100644 --- a/content/media/video/public/nsWaveDecoder.h +++ b/content/media/video/public/nsWaveDecoder.h @@ -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. diff --git a/content/media/video/src/nsAudioStream.cpp b/content/media/video/src/nsAudioStream.cpp index 750db830762..c4be7ccb525 100644 --- a/content/media/video/src/nsAudioStream.cpp +++ b/content/media/video/src/nsAudioStream.cpp @@ -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(&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(mAudioHandle), SA_POSITION_WRITE_SOFTWARE, &bytes); -#endif - mSavedPauseBytes = bytes; - - sa_stream_pause(static_cast(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(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(mAudioHandle), SA_POSITION_WRITE_HARDWARE, &bytes); -#else - sa_stream_get_position(static_cast(mAudioHandle), SA_POSITION_WRITE_SOFTWARE, &bytes); -#endif - return double(bytes + mPauseBytes) / (sizeof(short) * mChannels * mRate); -} diff --git a/content/media/video/src/nsChannelReader.cpp b/content/media/video/src/nsChannelReader.cpp index 10f4950aa9f..06cf78a7c4f 100644 --- a/content/media/video/src/nsChannelReader.cpp +++ b/content/media/video/src/nsChannelReader.cpp @@ -48,11 +48,6 @@ void nsChannelReader::Cancel() mStream.Cancel(); } -PRUint32 nsChannelReader::Available() -{ - return mStream.Available(); -} - OggPlayErrorCode nsChannelReader::initialise(int aBlock) { return E_OGGPLAY_OK; diff --git a/content/media/video/src/nsMediaStream.cpp b/content/media/video/src/nsMediaStream.cpp index dda7e997cb0..436fde4862f 100644 --- a/content/media/video/src/nsMediaStream.cpp +++ b/content/media/video/src/nsMediaStream.cpp @@ -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(), diff --git a/content/media/video/src/nsWaveDecoder.cpp b/content/media/video/src/nsWaveDecoder.cpp index 679a0730955..807c58f29ac 100644 --- a/content/media/video/src/nsWaveDecoder.cpp +++ b/content/media/video/src/nsWaveDecoder.cpp @@ -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::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::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 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 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 event = + NS_NEW_RUNNABLE_METHOD(nsWaveDecoder, mDecoder, UpdateReadyStateForData); + NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL); + break; + } + + if (len > 0) { + nsAutoArrayPtr 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 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 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 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 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 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 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::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 */) {