зеркало из https://github.com/mozilla/gecko-dev.git
1627 строки
44 KiB
C++
1627 строки
44 KiB
C++
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* vim:set ts=2 sw=2 sts=2 et cindent: */
|
|
/* ***** BEGIN LICENSE BLOCK *****
|
|
* Version: ML 1.1/GPL 2.0/LGPL 2.1
|
|
*
|
|
* The contents of this file are subject to the Mozilla Public License Version
|
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
|
* the License. You may obtain a copy of the License at
|
|
* http://www.mozilla.org/MPL/
|
|
*
|
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
|
* for the specific language governing rights and limitations under the
|
|
* License.
|
|
*
|
|
* The Original Code is Mozilla code.
|
|
*
|
|
* The Initial Developer of the Original Code is the Mozilla Corporation.
|
|
* Portions created by the Initial Developer are Copyright (C) 2008
|
|
* the Initial Developer. All Rights Reserved.
|
|
*
|
|
* Contributor(s):
|
|
* Matthew Gregan <kinetik@flim.org>
|
|
*
|
|
* Alternatively, the contents of this file may be used under the terms of
|
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
|
* of those above. If you wish to allow use of your version of this file only
|
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
|
* use your version of this file under the terms of the MPL, indicate your
|
|
* decision by deleting the provisions above and replace them with the notice
|
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
|
* the provisions above, a recipient may use your version of this file under
|
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
|
*
|
|
* ***** END LICENSE BLOCK ***** */
|
|
#include "limits"
|
|
#include "prlog.h"
|
|
#include "prmem.h"
|
|
#include "nsIDOMHTMLMediaElement.h"
|
|
#include "nsIDocument.h"
|
|
#include "nsIFrame.h"
|
|
#include "nsIObserver.h"
|
|
#include "nsIObserverService.h"
|
|
#include "nsISeekableStream.h"
|
|
#include "nsAudioStream.h"
|
|
#include "nsAutoLock.h"
|
|
#include "nsHTMLMediaElement.h"
|
|
#include "nsNetUtil.h"
|
|
#include "nsThreadUtils.h"
|
|
#include "nsWaveDecoder.h"
|
|
|
|
using mozilla::TimeDuration;
|
|
using mozilla::TimeStamp;
|
|
|
|
#ifdef PR_LOGGING
|
|
static PRLogModuleInfo* gWaveDecoderLog;
|
|
#define LOG(type, msg) PR_LOG(gWaveDecoderLog, type, msg)
|
|
#else
|
|
#define LOG(type, msg)
|
|
#endif
|
|
|
|
// Maximum number of seconds to wait when buffering.
|
|
#define BUFFERING_TIMEOUT 3
|
|
|
|
// 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
|
|
#define WAVE_CHUNK_MAGIC 0x57415645
|
|
#define FRMT_CHUNK_MAGIC 0x666d7420
|
|
#define DATA_CHUNK_MAGIC 0x64617461
|
|
|
|
// Size of RIFF chunk header. 4 byte chunk header type and 4 byte size field.
|
|
#define RIFF_CHUNK_HEADER_SIZE 8
|
|
|
|
// Size of RIFF header. RIFF chunk and 4 byte RIFF type.
|
|
#define RIFF_INITIAL_SIZE (RIFF_CHUNK_HEADER_SIZE + 4)
|
|
|
|
// Size of required part of format chunk. Actual format chunks may be
|
|
// extended (for non-PCM encodings), but we skip any extended data.
|
|
#define WAVE_FORMAT_CHUNK_SIZE 16
|
|
|
|
// 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
|
|
dispatches the state machine event to the decode thread to begin
|
|
execution. Once running, metadata loading begins immediately. If this
|
|
completes successfully, the state machine will move into a paused state
|
|
awaiting further commands. The state machine provides a small set of
|
|
threadsafe methods enabling the main thread to play, pause, seek, and
|
|
query parameters.
|
|
|
|
An weak (raw) pointer to the decoder's nsMediaStream is used by the state
|
|
machine to read data, seek, and query stream information. The decoder is
|
|
responsible for creating and opening the stream, and may also cancel it.
|
|
Every other stream operation is performed on the playback thread by the
|
|
state machine. A cancel from the main thread will force any in-flight
|
|
stream operations to abort.
|
|
*/
|
|
class nsWaveStateMachine : public nsRunnable
|
|
{
|
|
public:
|
|
nsWaveStateMachine(nsWaveDecoder* aDecoder,
|
|
TimeDuration aBufferWaitTime, float aInitialVolume);
|
|
~nsWaveStateMachine();
|
|
|
|
void SetStream(nsMediaStream* aStream) { mStream = aStream; }
|
|
|
|
// Set specified volume. aVolume must be in range [0.0, 1.0].
|
|
// Threadsafe.
|
|
void SetVolume(float aVolume);
|
|
|
|
/*
|
|
The following four member functions initiate the appropriate state
|
|
transition suggested by the function name. Threadsafe.
|
|
*/
|
|
void Play();
|
|
void Pause();
|
|
void Seek(float aTime);
|
|
void Shutdown();
|
|
|
|
// Returns the playback length of the audio data in seconds, calculated
|
|
// from the length extracted from the metadata. Returns NaN if called
|
|
// before metadata validation has completed. Threadsafe.
|
|
float GetDuration();
|
|
|
|
// Returns true if the state machine is seeking. Threadsafe.
|
|
PRBool IsSeeking();
|
|
|
|
// Returns true if the state machine has reached the end of playback. Threadsafe.
|
|
PRBool IsEnded();
|
|
|
|
// Main state machine loop. Runs forever, until shutdown state is reached.
|
|
NS_IMETHOD Run();
|
|
|
|
// Called by the decoder, on the main thread.
|
|
nsMediaDecoder::Statistics GetStatistics();
|
|
|
|
// Called on the decoder thread
|
|
void NotifyBytesConsumed(PRInt64 aBytes);
|
|
|
|
// Called by the main thread only
|
|
nsHTMLMediaElement::NextFrameStatus GetNextFrameStatus();
|
|
|
|
// 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. 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,
|
|
// so must hold mMonitor. Threadsafe.
|
|
void ChangeState(State aState);
|
|
|
|
// Create and initialize audio stream using current audio parameters.
|
|
void OpenAudioStream();
|
|
|
|
// Shut down and dispose audio stream.
|
|
void CloseAudioStream();
|
|
|
|
// Read RIFF_INITIAL_SIZE from the beginning of the stream and verify that
|
|
// 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.
|
|
PRBool LoadFormatChunk();
|
|
|
|
// Scan forward in the stream looking for the start of the PCM data. If
|
|
// found, record the data length and offset in mWaveLength and
|
|
// 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(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.
|
|
PRInt64 TimeToBytes(float aTime) const
|
|
{
|
|
NS_ABORT_IF_FALSE(mMetadataValid, "Requires valid metadata");
|
|
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.
|
|
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);
|
|
}
|
|
|
|
// Weak (raw) pointer to our decoder instance. The decoder manages the
|
|
// lifetime of the state machine object, so it is guaranteed that the
|
|
// state machine will not outlive the decoder. The decoder is not
|
|
// threadsafe, so this pointer must only be used to create runnable events
|
|
// targeted at the main thread.
|
|
nsWaveDecoder* mDecoder;
|
|
|
|
// Weak (raw) pointer to a media stream. The decoder manages the lifetime
|
|
// of the stream, so it is guaranteed that the stream will live as long as
|
|
// the state machine. The stream is threadsafe, but is only used on the
|
|
// playback thread except for create, open, and cancel, which are called
|
|
// from the main thread.
|
|
nsMediaStream* mStream;
|
|
|
|
// Our audio stream. Created on demand when entering playback state. It
|
|
// is destroyed when seeking begins and will not be reinitialized until
|
|
// playback resumes, so it is possible for this to be null.
|
|
nsAutoPtr<nsAudioStream> mAudioStream;
|
|
|
|
// Maximum time to spend waiting for data during buffering.
|
|
TimeDuration mBufferingWait;
|
|
|
|
// Machine time that buffering began, used with mBufferingWait to time out
|
|
// buffering.
|
|
TimeStamp mBufferingStart;
|
|
|
|
// 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
|
|
stream, and for byte<->time domain conversions.
|
|
*/
|
|
|
|
// Number of samples per second. Limited to range [100, 96000] in LoadFormatChunk.
|
|
PRUint32 mSampleRate;
|
|
|
|
// Number of channels. Limited to range [1, 2] in LoadFormatChunk.
|
|
PRUint32 mChannels;
|
|
|
|
// Size of a single sample segment, which includes a sample for each
|
|
// channel (interleaved).
|
|
PRUint32 mSampleSize;
|
|
|
|
// The sample format of the PCM data.
|
|
nsAudioStream::SampleFormat mSampleFormat;
|
|
|
|
// Size of PCM data stored in the WAVE as reported by the data chunk in
|
|
// the media.
|
|
PRInt64 mWaveLength;
|
|
|
|
// Start offset of the PCM data in the media stream. Extends mWaveLength
|
|
// bytes.
|
|
PRInt64 mWavePCMOffset;
|
|
|
|
/*
|
|
All member variables following this comment are accessed by both
|
|
threads and must be synchronized via mMonitor.
|
|
*/
|
|
PRMonitor* mMonitor;
|
|
|
|
// The state to enter when the state machine loop iterates next.
|
|
State mState;
|
|
|
|
// A queued state transition. This is used to record the next state
|
|
// transition when play or pause is requested during seeking or metadata
|
|
// loading to ensure a completed metadata load or seek returns to the most
|
|
// recently requested state on completion.
|
|
State mNextState;
|
|
|
|
// Current playback position in the stream.
|
|
PRInt64 mPlaybackPosition;
|
|
|
|
// Volume that the audio backend will be initialized with.
|
|
float mInitialVolume;
|
|
|
|
// Time position (in seconds) to seek to. Set by Seek(float).
|
|
float mSeekTime;
|
|
|
|
// True once metadata has been parsed and validated. Users of mSampleRate,
|
|
// 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;
|
|
|
|
// True if paused. Tracks only the play/paused state.
|
|
PRPackedBool mPaused;
|
|
};
|
|
|
|
nsWaveStateMachine::nsWaveStateMachine(nsWaveDecoder* aDecoder,
|
|
TimeDuration aBufferWaitTime,
|
|
float aInitialVolume)
|
|
: mDecoder(aDecoder),
|
|
mStream(nsnull),
|
|
mBufferingWait(aBufferWaitTime),
|
|
mBufferingStart(),
|
|
mBufferingEndOffset(0),
|
|
mSampleRate(0),
|
|
mChannels(0),
|
|
mSampleSize(0),
|
|
mSampleFormat(nsAudioStream::FORMAT_S16_LE),
|
|
mWaveLength(0),
|
|
mWavePCMOffset(0),
|
|
mMonitor(nsnull),
|
|
mState(STATE_LOADING_METADATA),
|
|
mNextState(STATE_PAUSED),
|
|
mPlaybackPosition(0),
|
|
mInitialVolume(aInitialVolume),
|
|
mSeekTime(0.0),
|
|
mMetadataValid(PR_FALSE),
|
|
mPositionChangeQueued(PR_FALSE),
|
|
mPaused(mNextState == STATE_PAUSED)
|
|
{
|
|
mMonitor = nsAutoMonitor::NewMonitor("nsWaveStateMachine");
|
|
}
|
|
|
|
nsWaveStateMachine::~nsWaveStateMachine()
|
|
{
|
|
nsAutoMonitor::DestroyMonitor(mMonitor);
|
|
}
|
|
|
|
void
|
|
nsWaveStateMachine::Shutdown()
|
|
{
|
|
ChangeState(STATE_SHUTDOWN);
|
|
}
|
|
|
|
void
|
|
nsWaveStateMachine::Play()
|
|
{
|
|
nsAutoMonitor monitor(mMonitor);
|
|
mPaused = PR_FALSE;
|
|
if (mState == STATE_ENDED) {
|
|
Seek(0);
|
|
return;
|
|
}
|
|
if (mState == STATE_LOADING_METADATA || mState == STATE_SEEKING) {
|
|
mNextState = STATE_PLAYING;
|
|
} else {
|
|
ChangeState(STATE_PLAYING);
|
|
}
|
|
}
|
|
|
|
void
|
|
nsWaveStateMachine::SetVolume(float aVolume)
|
|
{
|
|
nsAutoMonitor monitor(mMonitor);
|
|
mInitialVolume = aVolume;
|
|
if (mAudioStream) {
|
|
mAudioStream->SetVolume(aVolume);
|
|
}
|
|
}
|
|
|
|
void
|
|
nsWaveStateMachine::Pause()
|
|
{
|
|
nsAutoMonitor monitor(mMonitor);
|
|
mPaused = PR_TRUE;
|
|
if (mState == STATE_LOADING_METADATA || mState == STATE_SEEKING ||
|
|
mState == STATE_BUFFERING || mState == STATE_ENDED) {
|
|
mNextState = STATE_PAUSED;
|
|
} else if (mState == STATE_PLAYING) {
|
|
ChangeState(STATE_PAUSED);
|
|
}
|
|
}
|
|
|
|
void
|
|
nsWaveStateMachine::Seek(float aTime)
|
|
{
|
|
nsAutoMonitor monitor(mMonitor);
|
|
mSeekTime = aTime;
|
|
if (mSeekTime < 0.0) {
|
|
mSeekTime = 0.0;
|
|
}
|
|
if (mState == STATE_LOADING_METADATA) {
|
|
mNextState = STATE_SEEKING;
|
|
} else if (mState != STATE_SEEKING) {
|
|
if (mState == STATE_ENDED) {
|
|
mNextState = mPaused ? STATE_PAUSED : STATE_PLAYING;
|
|
} else {
|
|
mNextState = mState;
|
|
}
|
|
ChangeState(STATE_SEEKING);
|
|
}
|
|
}
|
|
|
|
float
|
|
nsWaveStateMachine::GetDuration()
|
|
{
|
|
nsAutoMonitor monitor(mMonitor);
|
|
if (mMetadataValid) {
|
|
return BytesToTime(GetDataLength());
|
|
}
|
|
return std::numeric_limits<float>::quiet_NaN();
|
|
}
|
|
|
|
PRBool
|
|
nsWaveStateMachine::IsSeeking()
|
|
{
|
|
nsAutoMonitor monitor(mMonitor);
|
|
return mState == STATE_SEEKING || mNextState == STATE_SEEKING;
|
|
}
|
|
|
|
PRBool
|
|
nsWaveStateMachine::IsEnded()
|
|
{
|
|
nsAutoMonitor monitor(mMonitor);
|
|
return mState == STATE_ENDED || mState == STATE_SHUTDOWN;
|
|
}
|
|
|
|
nsHTMLMediaElement::NextFrameStatus
|
|
nsWaveStateMachine::GetNextFrameStatus()
|
|
{
|
|
nsAutoMonitor monitor(mMonitor);
|
|
if (mState == STATE_BUFFERING)
|
|
return nsHTMLMediaElement::NEXT_FRAME_UNAVAILABLE_BUFFERING;
|
|
// If mMetadataValid is false then we can't call GetDataLength because
|
|
// we haven't got the length from the Wave header yet. But we know that
|
|
// if we haven't read the metadata then we don't have playable data.
|
|
if (mMetadataValid &&
|
|
mPlaybackPosition < mStream->GetCachedDataEnd(mPlaybackPosition) &&
|
|
mPlaybackPosition < mWavePCMOffset + GetDataLength())
|
|
return nsHTMLMediaElement::NEXT_FRAME_AVAILABLE;
|
|
return nsHTMLMediaElement::NEXT_FRAME_UNAVAILABLE;
|
|
}
|
|
|
|
float
|
|
nsWaveStateMachine::GetTimeForPositionChange()
|
|
{
|
|
nsAutoMonitor monitor(mMonitor);
|
|
mPositionChangeQueued = PR_FALSE;
|
|
return BytesToTime(mPlaybackPosition - mWavePCMOffset);
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
nsWaveStateMachine::Run()
|
|
{
|
|
// Monitor is held by this thread almost permanently, but must be manually
|
|
// dropped during long operations to prevent the main thread from blocking
|
|
// when calling methods on the state machine object.
|
|
nsAutoMonitor monitor(mMonitor);
|
|
|
|
for (;;) {
|
|
switch (mState) {
|
|
case STATE_LOADING_METADATA:
|
|
{
|
|
monitor.Exit();
|
|
PRBool loaded = LoadRIFFChunk() && LoadFormatChunk() && FindDataOffset();
|
|
monitor.Enter();
|
|
|
|
if (!loaded) {
|
|
ChangeState(STATE_ERROR);
|
|
}
|
|
|
|
if (mState == STATE_LOADING_METADATA) {
|
|
mMetadataValid = PR_TRUE;
|
|
if (mNextState != STATE_SEEKING) {
|
|
nsCOMPtr<nsIRunnable> event = NS_NEW_RUNNABLE_METHOD(nsWaveDecoder, mDecoder, MetadataLoaded);
|
|
NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL);
|
|
}
|
|
ChangeState(mNextState);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case STATE_BUFFERING: {
|
|
TimeStamp now = TimeStamp::Now();
|
|
if (now - mBufferingStart < mBufferingWait &&
|
|
mStream->GetCachedDataEnd(mPlaybackPosition) < mBufferingEndOffset &&
|
|
!mStream->IsDataCachedToEndOfStream(mPlaybackPosition) &&
|
|
!mStream->IsSuspendedByCache()) {
|
|
LOG(PR_LOG_DEBUG,
|
|
("In buffering: buffering data until %d bytes available or %f seconds\n",
|
|
PRUint32(mBufferingEndOffset - mStream->GetCachedDataEnd(mPlaybackPosition)),
|
|
(mBufferingWait - (now - mBufferingStart)).ToSeconds()));
|
|
monitor.Wait(PR_MillisecondsToInterval(1000));
|
|
} else {
|
|
ChangeState(mNextState);
|
|
nsCOMPtr<nsIRunnable> event =
|
|
NS_NEW_RUNNABLE_METHOD(nsWaveDecoder, mDecoder, UpdateReadyStateForData);
|
|
NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case STATE_PLAYING: {
|
|
if (!mAudioStream) {
|
|
OpenAudioStream();
|
|
if (!mAudioStream) {
|
|
ChangeState(STATE_ERROR);
|
|
break;
|
|
}
|
|
}
|
|
|
|
TimeStamp now = TimeStamp::Now();
|
|
TimeStamp lastWakeup = now -
|
|
TimeDuration::FromMilliseconds(AUDIO_BUFFER_LENGTH);
|
|
|
|
do {
|
|
TimeDuration sleepTime = now - lastWakeup;
|
|
lastWakeup = now;
|
|
|
|
// 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.
|
|
TimeDuration targetTime =
|
|
TimeDuration::FromMilliseconds(AUDIO_BUFFER_LENGTH);
|
|
if (sleepTime < targetTime) {
|
|
targetTime = sleepTime;
|
|
}
|
|
|
|
PRInt64 len = TimeToBytes(targetTime.ToSeconds());
|
|
|
|
PRInt64 leftToPlay =
|
|
GetDataLength() - (mPlaybackPosition - mWavePCMOffset);
|
|
if (leftToPlay <= len) {
|
|
len = leftToPlay;
|
|
ChangeState(STATE_ENDED);
|
|
}
|
|
|
|
PRInt64 availableOffset = mStream->GetCachedDataEnd(mPlaybackPosition);
|
|
|
|
// don't buffer if we're at the end of the stream, or if the
|
|
// load has been suspended by the cache (in the latter case
|
|
// we need to advance playback to free up cache space)
|
|
if (mState != STATE_ENDED &&
|
|
availableOffset < mPlaybackPosition + len &&
|
|
!mStream->IsSuspendedByCache()) {
|
|
mBufferingStart = now;
|
|
mBufferingEndOffset = mPlaybackPosition +
|
|
TimeToBytes(mBufferingWait.ToSeconds());
|
|
mBufferingEndOffset = PR_MAX(mPlaybackPosition + len, mBufferingEndOffset);
|
|
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();
|
|
PRBool ok = ReadAll(buf.get(), len, &got);
|
|
monitor.Enter();
|
|
|
|
// Reached EOF.
|
|
if (!ok) {
|
|
ChangeState(STATE_ENDED);
|
|
if (got == 0) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Calculate difference between the current media stream position
|
|
// and the expected end of the PCM data.
|
|
PRInt64 endDelta = mWavePCMOffset + mWaveLength - mPlaybackPosition;
|
|
if (endDelta < 0) {
|
|
// Read past the end of PCM data. Adjust got to avoid playing
|
|
// back trailing data.
|
|
got -= -endDelta;
|
|
ChangeState(STATE_ENDED);
|
|
}
|
|
|
|
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();
|
|
|
|
FirePositionChanged(PR_FALSE);
|
|
}
|
|
|
|
if (mState == STATE_PLAYING) {
|
|
monitor.Wait(PR_MillisecondsToInterval(AUDIO_BUFFER_WAKEUP));
|
|
now = TimeStamp::Now();
|
|
}
|
|
} while (mState == STATE_PLAYING);
|
|
break;
|
|
}
|
|
|
|
case STATE_SEEKING:
|
|
{
|
|
CloseAudioStream();
|
|
|
|
mSeekTime = NS_MIN(mSeekTime, GetDuration());
|
|
float seekTime = mSeekTime;
|
|
|
|
monitor.Exit();
|
|
nsCOMPtr<nsIRunnable> startEvent =
|
|
NS_NEW_RUNNABLE_METHOD(nsWaveDecoder, mDecoder, SeekingStarted);
|
|
NS_DispatchToMainThread(startEvent, NS_DISPATCH_SYNC);
|
|
monitor.Enter();
|
|
|
|
if (mState == STATE_SHUTDOWN) {
|
|
break;
|
|
}
|
|
|
|
// Calculate relative offset within PCM data.
|
|
PRInt64 position = RoundDownToSample(TimeToBytes(seekTime));
|
|
NS_ABORT_IF_FALSE(position >= 0 && position <= GetDataLength(), "Invalid seek position");
|
|
// Convert to absolute offset within stream.
|
|
position += mWavePCMOffset;
|
|
|
|
monitor.Exit();
|
|
nsresult rv;
|
|
rv = mStream->Seek(nsISeekableStream::NS_SEEK_SET, position);
|
|
if (NS_FAILED(rv)) {
|
|
NS_WARNING("Seek failed");
|
|
}
|
|
monitor.Enter();
|
|
if (NS_SUCCEEDED(rv)) {
|
|
mPlaybackPosition = position;
|
|
}
|
|
|
|
if (mState == STATE_SHUTDOWN) {
|
|
break;
|
|
}
|
|
|
|
if (mState == STATE_SEEKING && mSeekTime == seekTime) {
|
|
// Special case #1: if a seek was requested during metadata load,
|
|
// mNextState will have been clobbered. This can only happen when
|
|
// we're instantiating a decoder to service a seek request after
|
|
// playback has ended, so we know that the clobbered mNextState
|
|
// was PAUSED.
|
|
// Special case #2: if a seek is requested after the state machine
|
|
// entered STATE_ENDED but before the user has seen the ended
|
|
// event, playback has not ended as far as the user's
|
|
// concerned--the state machine needs to return to the last
|
|
// playback state.
|
|
// Special case #3: if seeking to the end of the media, transition
|
|
// directly into STATE_ENDED.
|
|
State nextState = mNextState;
|
|
if (nextState == STATE_SEEKING) {
|
|
nextState = STATE_PAUSED;
|
|
} else if (nextState == STATE_ENDED) {
|
|
nextState = mPaused ? STATE_PAUSED : STATE_PLAYING;
|
|
} else if (GetDuration() == seekTime) {
|
|
nextState = STATE_ENDED;
|
|
}
|
|
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:
|
|
monitor.Wait();
|
|
break;
|
|
|
|
case STATE_ENDED:
|
|
FirePositionChanged(PR_TRUE);
|
|
|
|
if (mAudioStream) {
|
|
monitor.Exit();
|
|
mAudioStream->Drain();
|
|
monitor.Enter();
|
|
|
|
// After the drain call the audio stream is unusable. Close it so that
|
|
// next time audio is used a new stream is created.
|
|
CloseAudioStream();
|
|
}
|
|
|
|
if (mState == STATE_ENDED) {
|
|
nsCOMPtr<nsIRunnable> event =
|
|
NS_NEW_RUNNABLE_METHOD(nsWaveDecoder, mDecoder, PlaybackEnded);
|
|
NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL);
|
|
|
|
do {
|
|
monitor.Wait();
|
|
} while (mState == STATE_ENDED);
|
|
}
|
|
break;
|
|
|
|
case STATE_ERROR:
|
|
{
|
|
nsCOMPtr<nsIRunnable> event = NS_NEW_RUNNABLE_METHOD(nsWaveDecoder, mDecoder, DecodeError);
|
|
NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL);
|
|
|
|
monitor.Wait();
|
|
|
|
if (mState != STATE_SHUTDOWN) {
|
|
NS_WARNING("Invalid state transition");
|
|
ChangeState(STATE_ERROR);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case STATE_SHUTDOWN:
|
|
CloseAudioStream();
|
|
return NS_OK;
|
|
}
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
#if defined(DEBUG)
|
|
static PRBool
|
|
IsValidStateTransition(State aStartState, State aEndState)
|
|
{
|
|
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 ||
|
|
aEndState == STATE_ENDED)
|
|
return PR_TRUE;
|
|
break;
|
|
case STATE_PAUSED:
|
|
if (aEndState == STATE_PLAYING || aEndState == STATE_SEEKING)
|
|
return PR_TRUE;
|
|
break;
|
|
case STATE_ENDED:
|
|
if (aEndState == STATE_SEEKING)
|
|
return PR_TRUE;
|
|
/* fallthrough */
|
|
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 (mState == STATE_SHUTDOWN) {
|
|
LOG(PR_LOG_WARNING, ("In shutdown, state transition ignored"));
|
|
return;
|
|
}
|
|
#if defined(DEBUG)
|
|
NS_ABORT_IF_FALSE(IsValidStateTransition(mState, aState), "Invalid state transition");
|
|
#endif
|
|
mState = aState;
|
|
monitor.NotifyAll();
|
|
}
|
|
|
|
void
|
|
nsWaveStateMachine::OpenAudioStream()
|
|
{
|
|
mAudioStream = new nsAudioStream();
|
|
if (!mAudioStream) {
|
|
LOG(PR_LOG_ERROR, ("Could not create audio stream"));
|
|
} else {
|
|
NS_ABORT_IF_FALSE(mMetadataValid,
|
|
"Attempting to initialize audio stream with invalid metadata");
|
|
mAudioStream->Init(mChannels, mSampleRate, mSampleFormat);
|
|
mAudioStream->SetVolume(mInitialVolume);
|
|
}
|
|
}
|
|
|
|
void
|
|
nsWaveStateMachine::CloseAudioStream()
|
|
{
|
|
if (mAudioStream) {
|
|
mAudioStream->Shutdown();
|
|
mAudioStream = nsnull;
|
|
}
|
|
}
|
|
|
|
nsMediaDecoder::Statistics
|
|
nsWaveStateMachine::GetStatistics()
|
|
{
|
|
nsMediaDecoder::Statistics result;
|
|
nsAutoMonitor monitor(mMonitor);
|
|
result.mDownloadRate = mStream->GetDownloadRate(&result.mDownloadRateReliable);
|
|
result.mPlaybackRate = mSampleRate*mChannels*mSampleSize;
|
|
result.mPlaybackRateReliable = PR_TRUE;
|
|
result.mTotalBytes = mStream->GetLength();
|
|
result.mDownloadPosition = mStream->GetCachedDataEnd(mPlaybackPosition);
|
|
result.mDecoderPosition = mPlaybackPosition;
|
|
result.mPlaybackPosition = mPlaybackPosition;
|
|
return result;
|
|
}
|
|
|
|
void
|
|
nsWaveStateMachine::NotifyBytesConsumed(PRInt64 aBytes)
|
|
{
|
|
nsAutoMonitor monitor(mMonitor);
|
|
mPlaybackPosition += aBytes;
|
|
}
|
|
|
|
static PRUint32
|
|
ReadUint32BE(const char** aBuffer)
|
|
{
|
|
PRUint32 result =
|
|
PRUint8((*aBuffer)[0]) << 24 |
|
|
PRUint8((*aBuffer)[1]) << 16 |
|
|
PRUint8((*aBuffer)[2]) << 8 |
|
|
PRUint8((*aBuffer)[3]);
|
|
*aBuffer += sizeof(PRUint32);
|
|
return result;
|
|
}
|
|
|
|
static PRUint32
|
|
ReadUint32LE(const char** aBuffer)
|
|
{
|
|
PRUint32 result =
|
|
PRUint8((*aBuffer)[3]) << 24 |
|
|
PRUint8((*aBuffer)[2]) << 16 |
|
|
PRUint8((*aBuffer)[1]) << 8 |
|
|
PRUint8((*aBuffer)[0]);
|
|
*aBuffer += sizeof(PRUint32);
|
|
return result;
|
|
}
|
|
|
|
static PRUint16
|
|
ReadUint16LE(const char** aBuffer)
|
|
{
|
|
PRUint16 result =
|
|
PRUint8((*aBuffer)[1]) << 8 |
|
|
PRUint8((*aBuffer)[0]) << 0;
|
|
*aBuffer += sizeof(PRUint16);
|
|
return result;
|
|
}
|
|
|
|
PRBool
|
|
nsWaveStateMachine::IsShutdown()
|
|
{
|
|
nsAutoMonitor monitor(mMonitor);
|
|
return mState == STATE_SHUTDOWN;
|
|
}
|
|
|
|
PRBool
|
|
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() || read == 0) {
|
|
return PR_FALSE;
|
|
}
|
|
NotifyBytesConsumed(read);
|
|
got += read;
|
|
if (aBytesRead) {
|
|
*aBytesRead = got;
|
|
}
|
|
} while (got != aSize);
|
|
return PR_TRUE;
|
|
}
|
|
|
|
PRBool
|
|
nsWaveStateMachine::LoadRIFFChunk()
|
|
{
|
|
char riffHeader[RIFF_INITIAL_SIZE];
|
|
const char* p = riffHeader;
|
|
|
|
NS_ABORT_IF_FALSE(mStream->Tell() == 0,
|
|
"LoadRIFFChunk called when stream in invalid state");
|
|
|
|
if (!ReadAll(riffHeader, sizeof(riffHeader))) {
|
|
return PR_FALSE;
|
|
}
|
|
|
|
if (ReadUint32BE(&p) != RIFF_CHUNK_MAGIC) {
|
|
NS_WARNING("Stream data not in RIFF format");
|
|
return PR_FALSE;
|
|
}
|
|
|
|
// Skip over RIFF size field.
|
|
p += 4;
|
|
|
|
if (ReadUint32BE(&p) != WAVE_CHUNK_MAGIC) {
|
|
NS_WARNING("Expected WAVE chunk");
|
|
return PR_FALSE;
|
|
}
|
|
|
|
return PR_TRUE;
|
|
}
|
|
|
|
PRBool
|
|
nsWaveStateMachine::ScanForwardUntil(PRUint32 aWantedChunk, PRUint32* aChunkSize)
|
|
{
|
|
NS_ABORT_IF_FALSE(aChunkSize, "Require aChunkSize argument");
|
|
*aChunkSize = 0;
|
|
|
|
for (;;) {
|
|
char chunkHeader[8];
|
|
const char* p = chunkHeader;
|
|
|
|
if (!ReadAll(chunkHeader, sizeof(chunkHeader))) {
|
|
return PR_FALSE;
|
|
}
|
|
|
|
PRUint32 magic = ReadUint32BE(&p);
|
|
PRUint32 chunkSize = ReadUint32LE(&p);
|
|
|
|
if (magic == aWantedChunk) {
|
|
*aChunkSize = chunkSize;
|
|
return PR_TRUE;
|
|
}
|
|
|
|
// RIFF chunks are two-byte aligned, so round up if necessary.
|
|
chunkSize += chunkSize % 2;
|
|
|
|
while (chunkSize > 0) {
|
|
PRUint32 size = PR_MIN(chunkSize, 1 << 16);
|
|
nsAutoArrayPtr<char> chunk(new char[size]);
|
|
if (!ReadAll(chunk.get(), size)) {
|
|
return PR_FALSE;
|
|
}
|
|
chunkSize -= size;
|
|
}
|
|
}
|
|
}
|
|
|
|
PRBool
|
|
nsWaveStateMachine::LoadFormatChunk()
|
|
{
|
|
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 (ReadUint16LE(&p) != WAVE_FORMAT_ENCODING_PCM) {
|
|
NS_WARNING("WAVE is not uncompressed PCM, compressed encodings are not supported");
|
|
return PR_FALSE;
|
|
}
|
|
|
|
channels = ReadUint16LE(&p);
|
|
rate = ReadUint32LE(&p);
|
|
|
|
// Skip over average bytes per second field.
|
|
p += 4;
|
|
|
|
sampleSize = ReadUint16LE(&p);
|
|
|
|
sampleFormat = ReadUint16LE(&p);
|
|
|
|
// PCM encoded WAVEs are not expected to have an extended "format" chunk,
|
|
// but I have found WAVEs that have a extended "format" chunk with an
|
|
// 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) {
|
|
char extLength[2];
|
|
const char* p = extLength;
|
|
|
|
if (!ReadAll(extLength, sizeof(extLength))) {
|
|
return PR_FALSE;
|
|
}
|
|
|
|
PRUint16 extra = ReadUint16LE(&p);
|
|
if (fmtSize - (WAVE_FORMAT_CHUNK_SIZE + 2) != extra) {
|
|
NS_WARNING("Invalid extended format chunk size");
|
|
return PR_FALSE;
|
|
}
|
|
extra += extra % 2;
|
|
|
|
if (extra > 0) {
|
|
nsAutoArrayPtr<char> chunkExtension(new char[extra]);
|
|
if (!ReadAll(chunkExtension.get(), extra)) {
|
|
return PR_FALSE;
|
|
}
|
|
}
|
|
}
|
|
|
|
// RIFF chunks are always word (two byte) aligned.
|
|
NS_ABORT_IF_FALSE(mStream->Tell() % 2 == 0,
|
|
"LoadFormatChunk left stream unaligned");
|
|
|
|
// Make sure metadata is fairly sane. The rate check is fairly arbitrary,
|
|
// but the channels check is intentionally limited to mono or stereo
|
|
// because that's what the audio backend currently supports.
|
|
if (rate < 100 || rate > 96000 ||
|
|
channels < 1 || channels > 2 ||
|
|
(sampleSize != 1 && sampleSize != 2 && sampleSize != 4) ||
|
|
(sampleFormat != 8 && sampleFormat != 16)) {
|
|
NS_WARNING("Invalid WAVE metadata");
|
|
return PR_FALSE;
|
|
}
|
|
|
|
nsAutoMonitor monitor(mMonitor);
|
|
mSampleRate = rate;
|
|
mChannels = channels;
|
|
mSampleSize = sampleSize;
|
|
if (sampleFormat == 8) {
|
|
mSampleFormat = nsAudioStream::FORMAT_U8;
|
|
} else {
|
|
mSampleFormat = nsAudioStream::FORMAT_S16_LE;
|
|
}
|
|
return PR_TRUE;
|
|
}
|
|
|
|
PRBool
|
|
nsWaveStateMachine::FindDataOffset()
|
|
{
|
|
// RIFF chunks are always word (two byte) aligned.
|
|
NS_ABORT_IF_FALSE(mStream->Tell() % 2 == 0,
|
|
"FindDataOffset called with unaligned stream");
|
|
|
|
// The "data" chunk may not directly follow the "format" chunk, so skip
|
|
// over any intermediate chunks.
|
|
PRUint32 length;
|
|
if (!ScanForwardUntil(DATA_CHUNK_MAGIC, &length)) {
|
|
return PR_FALSE;
|
|
}
|
|
|
|
PRInt64 offset = mStream->Tell();
|
|
if (offset <= 0 || offset > PR_UINT32_MAX) {
|
|
NS_WARNING("PCM data offset out of range");
|
|
return PR_FALSE;
|
|
}
|
|
|
|
nsAutoMonitor monitor(mMonitor);
|
|
mWaveLength = length;
|
|
mWavePCMOffset = PRUint32(offset);
|
|
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.
|
|
PRInt64 streamLength = mStream->GetLength();
|
|
if (streamLength >= 0) {
|
|
PRInt64 dataLength = PR_MAX(0, streamLength - mWavePCMOffset);
|
|
length = PR_MIN(dataLength, length);
|
|
}
|
|
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),
|
|
mCurrentTime(0.0),
|
|
mEndedDuration(std::numeric_limits<float>::quiet_NaN()),
|
|
mEnded(PR_FALSE),
|
|
mSeekable(PR_TRUE),
|
|
mResourceLoaded(PR_FALSE),
|
|
mMetadataLoadedReported(PR_FALSE),
|
|
mResourceLoadedReported(PR_FALSE)
|
|
{
|
|
MOZ_COUNT_CTOR(nsWaveDecoder);
|
|
|
|
#ifdef PR_LOGGING
|
|
if (!gWaveDecoderLog) {
|
|
gWaveDecoderLog = PR_NewLogModule("nsWaveDecoder");
|
|
}
|
|
#endif
|
|
}
|
|
|
|
nsWaveDecoder::~nsWaveDecoder()
|
|
{
|
|
MOZ_COUNT_DTOR(nsWaveDecoder);
|
|
}
|
|
|
|
PRBool
|
|
nsWaveDecoder::Init(nsHTMLMediaElement* aElement)
|
|
{
|
|
nsMediaDecoder::Init(aElement);
|
|
|
|
RegisterShutdownObserver();
|
|
|
|
mPlaybackStateMachine = new nsWaveStateMachine(this,
|
|
TimeDuration::FromMilliseconds(BUFFERING_TIMEOUT),
|
|
mInitialVolume);
|
|
NS_ENSURE_TRUE(mPlaybackStateMachine, PR_FALSE);
|
|
|
|
return PR_TRUE;
|
|
}
|
|
|
|
nsMediaStream*
|
|
nsWaveDecoder::GetCurrentStream()
|
|
{
|
|
return mStream;
|
|
}
|
|
|
|
already_AddRefed<nsIPrincipal>
|
|
nsWaveDecoder::GetCurrentPrincipal()
|
|
{
|
|
if (!mStream) {
|
|
return nsnull;
|
|
}
|
|
return mStream->GetCurrentPrincipal();
|
|
}
|
|
|
|
float
|
|
nsWaveDecoder::GetCurrentTime()
|
|
{
|
|
return mCurrentTime;
|
|
}
|
|
|
|
nsresult
|
|
nsWaveDecoder::Seek(float aTime)
|
|
{
|
|
if (mPlaybackStateMachine) {
|
|
mPlaybackStateMachine->Seek(aTime);
|
|
return NS_OK;
|
|
}
|
|
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
nsresult
|
|
nsWaveDecoder::PlaybackRateChanged()
|
|
{
|
|
return NS_ERROR_NOT_IMPLEMENTED;
|
|
}
|
|
|
|
float
|
|
nsWaveDecoder::GetDuration()
|
|
{
|
|
if (mPlaybackStateMachine) {
|
|
return mPlaybackStateMachine->GetDuration();
|
|
}
|
|
return mEndedDuration;
|
|
}
|
|
|
|
void
|
|
nsWaveDecoder::Pause()
|
|
{
|
|
if (mPlaybackStateMachine) {
|
|
mPlaybackStateMachine->Pause();
|
|
}
|
|
}
|
|
|
|
void
|
|
nsWaveDecoder::SetVolume(float aVolume)
|
|
{
|
|
mInitialVolume = aVolume;
|
|
if (mPlaybackStateMachine) {
|
|
mPlaybackStateMachine->SetVolume(aVolume);
|
|
}
|
|
}
|
|
|
|
nsresult
|
|
nsWaveDecoder::Play()
|
|
{
|
|
if (mPlaybackStateMachine) {
|
|
mPlaybackStateMachine->Play();
|
|
return NS_OK;
|
|
}
|
|
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
void
|
|
nsWaveDecoder::Stop()
|
|
{
|
|
if (mPlaybackStateMachine) {
|
|
mPlaybackStateMachine->Shutdown();
|
|
}
|
|
|
|
if (mStream) {
|
|
mStream->Close();
|
|
}
|
|
|
|
if (mPlaybackThread) {
|
|
mPlaybackThread->Shutdown();
|
|
}
|
|
|
|
if (mPlaybackStateMachine) {
|
|
mEndedDuration = mPlaybackStateMachine->GetDuration();
|
|
mEnded = mPlaybackStateMachine->IsEnded();
|
|
}
|
|
|
|
mPlaybackThread = nsnull;
|
|
mPlaybackStateMachine = nsnull;
|
|
mStream = nsnull;
|
|
|
|
UnregisterShutdownObserver();
|
|
}
|
|
|
|
nsresult
|
|
nsWaveDecoder::Load(nsMediaStream* aStream, nsIStreamListener** aStreamListener)
|
|
{
|
|
NS_ASSERTION(aStream, "A stream should be provided");
|
|
|
|
if (aStreamListener) {
|
|
*aStreamListener = nsnull;
|
|
}
|
|
|
|
mStream = aStream;
|
|
|
|
nsresult rv = mStream->Open(aStreamListener);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
mPlaybackStateMachine->SetStream(mStream);
|
|
|
|
rv = NS_NewThread(getter_AddRefs(mPlaybackThread));
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
rv = mPlaybackThread->Dispatch(mPlaybackStateMachine, NS_DISPATCH_NORMAL);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
void
|
|
nsWaveDecoder::MetadataLoaded()
|
|
{
|
|
if (mShuttingDown) {
|
|
return;
|
|
}
|
|
|
|
if (mElement) {
|
|
mElement->MetadataLoaded();
|
|
mElement->FirstFrameLoaded(mResourceLoaded);
|
|
}
|
|
|
|
mMetadataLoadedReported = PR_TRUE;
|
|
|
|
if (mResourceLoaded) {
|
|
ResourceLoaded();
|
|
} else {
|
|
StartProgress();
|
|
}
|
|
}
|
|
|
|
void
|
|
nsWaveDecoder::PlaybackEnded()
|
|
{
|
|
if (mShuttingDown) {
|
|
return;
|
|
}
|
|
|
|
if (!mPlaybackStateMachine->IsEnded()) {
|
|
return;
|
|
}
|
|
|
|
// Update ready state; now that we've finished playback, we should
|
|
// switch to HAVE_CURRENT_DATA.
|
|
UpdateReadyStateForData();
|
|
if (mElement) {
|
|
mElement->PlaybackEnded();
|
|
}
|
|
}
|
|
|
|
void
|
|
nsWaveDecoder::ResourceLoaded()
|
|
{
|
|
if (mShuttingDown) {
|
|
return;
|
|
}
|
|
|
|
mResourceLoaded = PR_TRUE;
|
|
|
|
if (!mMetadataLoadedReported || mResourceLoadedReported)
|
|
return;
|
|
|
|
StopProgress();
|
|
|
|
if (mElement) {
|
|
// Ensure the final progress event gets fired
|
|
mElement->DispatchAsyncProgressEvent(NS_LITERAL_STRING("progress"));
|
|
mElement->ResourceLoaded();
|
|
}
|
|
|
|
mResourceLoadedReported = PR_TRUE;
|
|
}
|
|
|
|
void
|
|
nsWaveDecoder::NetworkError()
|
|
{
|
|
if (mShuttingDown) {
|
|
return;
|
|
}
|
|
if (mElement) {
|
|
mElement->NetworkError();
|
|
}
|
|
Shutdown();
|
|
}
|
|
|
|
PRBool
|
|
nsWaveDecoder::IsSeeking() const
|
|
{
|
|
if (mPlaybackStateMachine) {
|
|
return mPlaybackStateMachine->IsSeeking();
|
|
}
|
|
return PR_FALSE;
|
|
}
|
|
|
|
PRBool
|
|
nsWaveDecoder::IsEnded() const
|
|
{
|
|
if (mPlaybackStateMachine) {
|
|
return mPlaybackStateMachine->IsEnded();
|
|
}
|
|
return mEnded;
|
|
}
|
|
|
|
nsMediaDecoder::Statistics
|
|
nsWaveDecoder::GetStatistics()
|
|
{
|
|
if (!mPlaybackStateMachine)
|
|
return Statistics();
|
|
return mPlaybackStateMachine->GetStatistics();
|
|
}
|
|
|
|
void
|
|
nsWaveDecoder::NotifySuspendedStatusChanged()
|
|
{
|
|
if (mStream->IsSuspendedByCache() && mElement) {
|
|
// if this is an autoplay element, we need to kick off its autoplaying
|
|
// now so we consume data and hopefully free up cache space
|
|
mElement->NotifyAutoplayDataReady();
|
|
}
|
|
}
|
|
|
|
void
|
|
nsWaveDecoder::NotifyBytesDownloaded()
|
|
{
|
|
UpdateReadyStateForData();
|
|
Progress(PR_FALSE);
|
|
}
|
|
|
|
void
|
|
nsWaveDecoder::NotifyDownloadEnded(nsresult aStatus)
|
|
{
|
|
if (NS_SUCCEEDED(aStatus)) {
|
|
ResourceLoaded();
|
|
} else if (aStatus != NS_BASE_STREAM_CLOSED &&
|
|
aStatus != NS_BINDING_ABORTED) {
|
|
NetworkError();
|
|
}
|
|
UpdateReadyStateForData();
|
|
}
|
|
|
|
void
|
|
nsWaveDecoder::Shutdown()
|
|
{
|
|
if (mShuttingDown)
|
|
return;
|
|
|
|
mShuttingDown = PR_TRUE;
|
|
|
|
nsMediaDecoder::Shutdown();
|
|
|
|
// An event that gets posted to the main thread, when the media element is
|
|
// being destroyed, to destroy the decoder. Since the decoder shutdown can
|
|
// block and post events this cannot be done inside destructor calls. So
|
|
// this event is posted asynchronously to the main thread to perform the
|
|
// shutdown.
|
|
nsCOMPtr<nsIRunnable> event =
|
|
NS_NEW_RUNNABLE_METHOD(nsWaveDecoder, this, Stop);
|
|
NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL);
|
|
}
|
|
|
|
nsresult
|
|
nsWaveDecoder::Observe(nsISupports* aSubject, const char* aTopic, const PRUnichar* aData)
|
|
{
|
|
if (strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) {
|
|
Shutdown();
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
void
|
|
nsWaveDecoder::UpdateReadyStateForData()
|
|
{
|
|
if (!mElement || mShuttingDown || !mPlaybackStateMachine)
|
|
return;
|
|
|
|
nsHTMLMediaElement::NextFrameStatus frameStatus =
|
|
mPlaybackStateMachine->GetNextFrameStatus();
|
|
if (frameStatus == nsHTMLMediaElement::NEXT_FRAME_AVAILABLE &&
|
|
!mMetadataLoadedReported) {
|
|
frameStatus = nsHTMLMediaElement::NEXT_FRAME_UNAVAILABLE;
|
|
}
|
|
mElement->UpdateReadyStateForData(frameStatus);
|
|
}
|
|
|
|
void
|
|
nsWaveDecoder::SeekingStarted()
|
|
{
|
|
if (mShuttingDown) {
|
|
return;
|
|
}
|
|
|
|
if (mElement) {
|
|
UpdateReadyStateForData();
|
|
mElement->SeekStarted();
|
|
}
|
|
}
|
|
|
|
void
|
|
nsWaveDecoder::SeekingStopped()
|
|
{
|
|
if (mShuttingDown) {
|
|
return;
|
|
}
|
|
|
|
if (mElement) {
|
|
UpdateReadyStateForData();
|
|
mElement->SeekCompleted();
|
|
}
|
|
}
|
|
|
|
void
|
|
nsWaveDecoder::RegisterShutdownObserver()
|
|
{
|
|
nsCOMPtr<nsIObserverService> observerService =
|
|
do_GetService("@mozilla.org/observer-service;1");
|
|
if (observerService) {
|
|
observerService->AddObserver(this,
|
|
NS_XPCOM_SHUTDOWN_OBSERVER_ID,
|
|
PR_FALSE);
|
|
} else {
|
|
NS_WARNING("Could not get an observer service. Audio playback may not shutdown cleanly.");
|
|
}
|
|
}
|
|
|
|
void
|
|
nsWaveDecoder::UnregisterShutdownObserver()
|
|
{
|
|
nsCOMPtr<nsIObserverService> observerService =
|
|
do_GetService("@mozilla.org/observer-service;1");
|
|
if (observerService) {
|
|
observerService->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
|
|
}
|
|
}
|
|
|
|
void
|
|
nsWaveDecoder::DecodeError()
|
|
{
|
|
if (mShuttingDown) {
|
|
return;
|
|
}
|
|
if (mElement) {
|
|
mElement->DecodeError();
|
|
}
|
|
Shutdown();
|
|
}
|
|
|
|
void
|
|
nsWaveDecoder::PlaybackPositionChanged()
|
|
{
|
|
if (mShuttingDown) {
|
|
return;
|
|
}
|
|
|
|
float lastTime = mCurrentTime;
|
|
|
|
if (mPlaybackStateMachine) {
|
|
mCurrentTime = mPlaybackStateMachine->GetTimeForPositionChange();
|
|
}
|
|
|
|
if (mElement && lastTime != mCurrentTime) {
|
|
UpdateReadyStateForData();
|
|
mElement->DispatchSimpleEvent(NS_LITERAL_STRING("timeupdate"));
|
|
}
|
|
}
|
|
|
|
void
|
|
nsWaveDecoder::SetDuration(PRInt64 /* aDuration */)
|
|
{
|
|
// Ignored by the wave decoder since we can compute the
|
|
// duration directly from the wave data itself.
|
|
}
|
|
|
|
void
|
|
nsWaveDecoder::SetSeekable(PRBool aSeekable)
|
|
{
|
|
mSeekable = aSeekable;
|
|
}
|
|
|
|
PRBool
|
|
nsWaveDecoder::GetSeekable()
|
|
{
|
|
return mSeekable;
|
|
}
|
|
|
|
void
|
|
nsWaveDecoder::Suspend()
|
|
{
|
|
if (mStream) {
|
|
mStream->Suspend(PR_TRUE);
|
|
}
|
|
}
|
|
|
|
void
|
|
nsWaveDecoder::Resume()
|
|
{
|
|
if (mStream) {
|
|
mStream->Resume();
|
|
}
|
|
}
|
|
|
|
void
|
|
nsWaveDecoder::MoveLoadsToBackground()
|
|
{
|
|
if (mStream) {
|
|
mStream->MoveLoadsToBackground();
|
|
}
|
|
}
|