gecko/content/media/ogg/nsOggDecoder.cpp

2690 lines
84 KiB
C++
Raw Normal View History

/* -*- 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: MPL 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) 2007
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Chris Double <chris.double@double.co.nz>
*
* 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 "nsOggDecoder.h"
#include <limits>
#include "prmem.h"
#include "nsIFrame.h"
#include "nsIDocument.h"
#include "nsThreadUtils.h"
#include "nsIDOMHTMLMediaElement.h"
#include "nsNetUtil.h"
#include "nsAudioStream.h"
#include "nsChannelReader.h"
#include "nsHTMLVideoElement.h"
#include "nsIObserver.h"
#include "nsIObserverService.h"
#include "nsAutoLock.h"
#include "nsTArray.h"
#include "nsNetUtil.h"
using mozilla::TimeDuration;
using mozilla::TimeStamp;
using namespace mozilla::layers;
#ifdef PR_LOGGING
static PRLogModuleInfo* gOggDecoderLog;
#define LOG(type, msg) PR_LOG(gOggDecoderLog, type, msg)
#else
#define LOG(type, msg)
#endif
/*
The maximum height and width of the video. Used for
sanitizing the memory allocation of the RGB buffer.
The maximum resolution we anticipate encountering in the
wild is 2160p - 3840x2160 pixels.
*/
#define MAX_VIDEO_WIDTH 4000
#define MAX_VIDEO_HEIGHT 3000
// The number of entries in oggplay buffer list. This value is totally
// arbitrary. Note that the actual number of video/audio frames buffered is
// twice this, because the current implementation releases OggPlay's buffer
// entries and stores references or copies of the underlying data in the
// FrameQueue.
#define OGGPLAY_BUFFER_SIZE 5
// The number of frames to read before audio callback is called.
// This value is the one used by the oggplay examples.
#define OGGPLAY_FRAMES_PER_CALLBACK 2048
// Offset into Ogg buffer containing audio information. This value
// is the one used by the oggplay examples.
#define OGGPLAY_AUDIO_OFFSET 250L
// Wait this number of seconds when buffering, then leave and play
// as best as we can if the required amount of data hasn't been
// retrieved.
#define BUFFERING_WAIT 15
// The amount of data to retrieve during buffering is computed based
// on the download rate. BUFFERING_MIN_RATE is the minimum download
// rate to be used in that calculation to help avoid constant buffering
// attempts at a time when the average download rate has not stabilised.
#define BUFFERING_MIN_RATE 50000
#define BUFFERING_RATE(x) ((x)< BUFFERING_MIN_RATE ? BUFFERING_MIN_RATE : (x))
// The number of seconds of buffer data before buffering happens
// based on current playback rate.
#define BUFFERING_SECONDS_LOW_WATER_MARK 1
// The minimum size buffered byte range inside which we'll consider
// trying a bounded-seek. When we seek, we first try to seek inside all
// buffered ranges larger than this, and if they all fail we fall back to
// an unbounded seek over the whole media. 64K is approximately 16 pages.
#define MIN_BOUNDED_SEEK_SIZE (64 * 1024)
class nsOggStepDecodeEvent;
/*
All reading (including seeks) from the nsMediaStream are done on the
decoding thread. The decoder thread is informed before closing that
the stream is about to close via the Shutdown
event. oggplay_prepare_for_close is called before sending the
shutdown event to tell liboggplay to shutdown.
This call results in oggplay internally not calling any
read/write/seek/tell methods, and returns a value that results in
stopping the decoder thread.
oggplay_close is called in the destructor which results in the media
stream being closed. This is how the nsMediaStream contract that no
read/seeking must occur during or after Close is called is enforced.
This object keeps pointers to the nsOggDecoder and nsChannelReader
objects. Since the lifetime of nsOggDecodeStateMachine is
controlled by nsOggDecoder it will never have a stale reference to
these objects. The reader is destroyed by the call to oggplay_close
which is done in the destructor so again this will never be a stale
reference.
All internal state is synchronised via the decoder monitor. NotifyAll
on the monitor is called when the state of the state machine is changed
by the main thread. The following changes to state cause a notify:
mState and data related to that state changed (mSeekTime, etc)
Ogg Metadata Loaded
First Frame Loaded
Frame decoded
See nsOggDecoder.h for more details.
*/
class nsOggDecodeStateMachine : public nsRunnable
{
friend class nsOggStepDecodeEvent;
public:
// Object to hold the decoded data from a frame
class FrameData {
public:
FrameData() :
mVideoHeader(nsnull),
mVideoWidth(0),
mVideoHeight(0),
mUVWidth(0),
mUVHeight(0),
mDecodedFrameTime(0.0),
mTime(0.0)
{
MOZ_COUNT_CTOR(FrameData);
}
~FrameData()
{
MOZ_COUNT_DTOR(FrameData);
ClearVideoHeader();
}
void ClearVideoHeader() {
if (mVideoHeader) {
oggplay_callback_info_unlock_item(mVideoHeader);
mVideoHeader = nsnull;
}
}
// Write the audio data from the frame to the Audio stream.
void Write(nsAudioStream* aStream)
{
aStream->Write(mAudioData.Elements(), mAudioData.Length());
mAudioData.Clear();
}
void SetVideoHeader(OggPlayDataHeader* aVideoHeader)
{
NS_ABORT_IF_FALSE(!mVideoHeader, "Frame already owns a video header");
mVideoHeader = aVideoHeader;
oggplay_callback_info_lock_item(mVideoHeader);
}
// The position in the stream where this frame ended, in bytes
PRInt64 mEndStreamPosition;
OggPlayDataHeader* mVideoHeader;
nsTArray<float> mAudioData;
int mVideoWidth;
int mVideoHeight;
int mUVWidth;
int mUVHeight;
float mDecodedFrameTime;
float mTime;
OggPlayStreamInfo mState;
};
// A queue of decoded video frames.
class FrameQueue
{
public:
FrameQueue() :
mHead(0),
mTail(0),
mCount(0)
{
}
void Push(FrameData* frame)
{
NS_ASSERTION(!IsFull(), "FrameQueue is full");
mQueue[mTail] = frame;
mTail = (mTail+1) % OGGPLAY_BUFFER_SIZE;
++mCount;
}
FrameData* Peek() const
{
NS_ASSERTION(mCount > 0, "FrameQueue is empty");
return mQueue[mHead];
}
FrameData* Pop()
{
NS_ASSERTION(mCount, "FrameQueue is empty");
FrameData* result = mQueue[mHead];
mHead = (mHead + 1) % OGGPLAY_BUFFER_SIZE;
--mCount;
return result;
}
PRBool IsEmpty() const
{
return mCount == 0;
}
PRUint32 GetCount() const
{
return mCount;
}
PRBool IsFull() const
{
return mCount == OGGPLAY_BUFFER_SIZE;
}
PRUint32 ResetTimes(float aPeriod)
{
PRUint32 frames = 0;
if (mCount > 0) {
PRUint32 current = mHead;
do {
mQueue[current]->mTime = frames * aPeriod;
frames += 1;
current = (current + 1) % OGGPLAY_BUFFER_SIZE;
} while (current != mTail);
}
return frames;
}
private:
FrameData* mQueue[OGGPLAY_BUFFER_SIZE];
PRUint32 mHead;
PRUint32 mTail;
// This isn't redundant with mHead/mTail, since when mHead == mTail
// it's ambiguous whether the queue is full or empty
PRUint32 mCount;
};
// Enumeration for the valid states
enum State {
DECODER_STATE_DECODING_METADATA,
DECODER_STATE_DECODING,
DECODER_STATE_SEEKING,
DECODER_STATE_BUFFERING,
DECODER_STATE_COMPLETED,
DECODER_STATE_SHUTDOWN
};
nsOggDecodeStateMachine(nsOggDecoder* aDecoder);
~nsOggDecodeStateMachine();
// Cause state transitions. These methods obtain the decoder monitor
// to synchronise the change of state, and to notify other threads
// that the state has changed.
void Shutdown();
void Decode();
void Seek(float aTime);
void StopStepDecodeThread(nsAutoMonitor* aMonitor);
NS_IMETHOD Run();
PRBool HasAudio()
{
NS_ASSERTION(mState > DECODER_STATE_DECODING_METADATA, "HasAudio() called during invalid state");
PR_ASSERT_CURRENT_THREAD_IN_MONITOR(mDecoder->GetMonitor());
return mAudioTrack != -1;
}
// Decode one frame of data, returning the OggPlay error code. Must
// be called only when the current state > DECODING_METADATA. The decode
// monitor MUST NOT be locked during this call since it can take a long
// time. liboggplay internally handles locking.
// Any return value apart from those below is mean decoding cannot continue.
// E_OGGPLAY_CONTINUE = One frame decoded and put in buffer list
// E_OGGPLAY_USER_INTERRUPT = One frame decoded, buffer list is now full
// E_OGGPLAY_TIMEOUT = No frames decoded, timed out
OggPlayErrorCode DecodeFrame();
// Handle any errors returned by liboggplay when decoding a frame.
// Since this function can change the decoding state it must be called
// with the decoder lock held.
void HandleDecodeErrors(OggPlayErrorCode r);
// Returns the next decoded frame of data. The caller is responsible
// for freeing the memory returned. This function must be called
// only when the current state > DECODING_METADATA. The decode
// monitor lock does not need to be locked during this call since
// liboggplay internally handles locking.
FrameData* NextFrame();
// Play a frame of decoded video. The decode monitor is obtained
// internally by this method for synchronisation.
void PlayFrame();
// Play the video data from the given frame. The decode monitor
// must be locked when calling this method.
void PlayVideo(FrameData* aFrame);
// Plays the audio for the frame, plus any outstanding audio data
// buffered by nsAudioStream and not yet written to the
// hardware. The audio data for the frame is cleared out so
// subsequent calls with the same frame do not re-write the data.
// The decode monitor must be locked when calling this method.
void PlayAudio(FrameData* aFrame);
// Called from the main thread to get the current frame time. The decoder
// monitor must be obtained before calling this.
float GetCurrentTime();
// Called from the main thread to get the duration. The decoder monitor
// must be obtained before calling this. It is in units of milliseconds.
PRInt64 GetDuration();
// Called from the main thread to set the duration of the media resource
// if it is able to be obtained via HTTP headers. The decoder monitor
// must be obtained before calling this.
void SetDuration(PRInt64 aDuration);
// Called from the main thread to set whether the media resource can
// be seeked. The decoder monitor must be obtained before calling this.
void SetSeekable(PRBool aSeekable);
// Set the audio volume. The decoder monitor must be obtained before
// calling this.
void SetVolume(float aVolume);
// Clear the flag indicating that a playback position change event
// is currently queued. This is called from the main thread and must
// be called with the decode monitor held.
void ClearPositionChangeFlag();
// Called by decoder and main thread.
nsHTMLMediaElement::NextFrameStatus GetNextFrameStatus();
// Must be called with the decode monitor held. Can be called by main
// thread.
PRBool HaveNextFrameData() const {
return !mDecodedFrames.IsEmpty() &&
(mDecodedFrames.Peek()->mDecodedFrameTime > mCurrentFrameTime ||
mDecodedFrames.GetCount() > 1);
}
// Must be called with the decode monitor held. Can be called by main
// thread.
PRBool IsBuffering() const {
PR_ASSERT_CURRENT_THREAD_IN_MONITOR(mDecoder->GetMonitor());
return mState == nsOggDecodeStateMachine::DECODER_STATE_BUFFERING;
}
// Must be called with the decode monitor held. Can be called by main
// thread.
PRBool IsSeeking() const {
PR_ASSERT_CURRENT_THREAD_IN_MONITOR(mDecoder->GetMonitor());
return mState == nsOggDecodeStateMachine::DECODER_STATE_SEEKING;
}
protected:
// Decodes from the current position until encountering a frame with time
// greater or equal to aSeekTime.
PRBool DecodeToFrame(nsAutoMonitor& aMonitor,
float aSeekTime);
// Convert the OggPlay frame information into a format used by Gecko
// (RGB for video, float for sound, etc).The decoder monitor must be
// acquired in the scope of calls to these functions. They must be
// called only when the current state > DECODING_METADATA.
void HandleVideoData(FrameData* aFrame, int aTrackNum, OggPlayDataHeader* aVideoHeader);
void HandleAudioData(FrameData* aFrame, OggPlayAudioData* aAudioData, int aSize);
void UpdateReadyState() {
PR_ASSERT_CURRENT_THREAD_IN_MONITOR(mDecoder->GetMonitor());
nsCOMPtr<nsIRunnable> event;
switch (GetNextFrameStatus()) {
case nsHTMLMediaElement::NEXT_FRAME_UNAVAILABLE_BUFFERING:
event = NS_NEW_RUNNABLE_METHOD(nsOggDecoder, mDecoder, NextFrameUnavailableBuffering);
break;
case nsHTMLMediaElement::NEXT_FRAME_AVAILABLE:
event = NS_NEW_RUNNABLE_METHOD(nsOggDecoder, mDecoder, NextFrameAvailable);
break;
case nsHTMLMediaElement::NEXT_FRAME_UNAVAILABLE:
event = NS_NEW_RUNNABLE_METHOD(nsOggDecoder, mDecoder, NextFrameUnavailable);
break;
default:
PR_NOT_REACHED("unhandled frame state");
}
NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL);
}
// These methods can only be called on the decoding thread.
void LoadOggHeaders(nsChannelReader* aReader);
// Initializes and opens the audio stream. Called from the decode
// thread only. Must be called with the decode monitor held.
void OpenAudioStream();
// Closes and releases resources used by the audio stream. Called
// from the decode thread only. Must be called with the decode
// monitor held.
void CloseAudioStream();
// Start playback of audio, either by opening or resuming the audio
// stream. Must be called with the decode monitor held.
void StartAudio();
// Stop playback of audio, either by closing or pausing the audio
// stream. Must be called with the decode monitor held.
void StopAudio();
// Start playback of media. Must be called with the decode monitor held.
// This opens or re-opens the audio stream for playback to start.
void StartPlayback();
// Stop playback of media. Must be called with the decode monitor held.
// This actually closes the audio stream and releases any OS resources.
void StopPlayback();
// Pause playback of media. Must be called with the decode monitor held.
// This does not close the OS based audio stream - it suspends it to be
// resumed later.
void PausePlayback();
// Resume playback of media. Must be called with the decode monitor held.
// This resumes a paused audio stream.
void ResumePlayback();
// Update the playback position. This can result in a timeupdate event
// and an invalidate of the frame being dispatched asynchronously if
// there is no such event currently queued.
// Only called on the decoder thread. Must be called with
// the decode monitor held.
void UpdatePlaybackPosition(float aTime);
// Takes decoded frames from liboggplay's internal buffer and
// places them in our frame queue. Must be called with the decode
// monitor held.
void QueueDecodedFrames();
// Seeks the OggPlay to aTime, inside buffered byte ranges in aReader's
// media stream.
nsresult Seek(float aTime, nsChannelReader* aReader);
// Sets the current video and audio track to active in liboggplay.
// Called from the decoder thread only.
void SetTracksActive();
private:
// *****
// The follow fields are only accessed by the decoder thread
// *****
// The decoder object that created this state machine. The decoder
// always outlives us since it controls our lifetime.
nsOggDecoder* mDecoder;
// The OggPlay handle. Synchronisation of calls to oggplay functions
// are handled by liboggplay. We control the lifetime of this
// object, destroying it in our destructor.
OggPlay* mPlayer;
// Frame data containing decoded video/audio for the frame the
// current frame and the previous frame. Always accessed with monitor
// held. Written only via the decoder thread, but can be tested on
// main thread via HaveNextFrameData.
FrameQueue mDecodedFrames;
// The time that playback started from the system clock. This is used
// for synchronising frames. It is reset after a seek as the mTime member
// of FrameData is reset to start at 0 from the first frame after a seek.
// Accessed only via the decoder thread.
TimeStamp mPlayStartTime;
// The time that playback was most recently paused, either via
// buffering or pause. This is used to compute mPauseDuration for
// a/v sync adjustments. Accessed only via the decoder thread.
TimeStamp mPauseStartTime;
// The total time that has been spent in completed pauses (via
// 'pause' or buffering). This is used to adjust for these
// pauses when computing a/v synchronisation. Accessed only via the
// decoder thread.
TimeDuration mPauseDuration;
// PR_TRUE if the media is playing and the decoder has started
// the sound and adjusted the sync time for pauses. PR_FALSE
// if the media is paused and the decoder has stopped the sound
// and adjusted the sync time for pauses. Accessed only via the
// decoder thread.
PRPackedBool mPlaying;
// Number of seconds of data video/audio data held in a frame.
// Accessed only via the decoder thread.
double mCallbackPeriod;
// Video data. These are initially set when the metadata is loaded.
// They are only accessed from the decoder thread.
PRInt32 mVideoTrack;
float mFramerate;
float mAspectRatio;
// Audio data. These are initially set when the metadata is loaded.
// They are only accessed from the decoder thread.
PRInt32 mAudioRate;
PRInt32 mAudioChannels;
PRInt32 mAudioTrack;
// Time that buffering started. Used for buffering timeout and only
// accessed in the decoder thread.
TimeStamp mBufferingStart;
// Download position where we should stop buffering. Only
// accessed in the decoder thread.
PRInt64 mBufferingEndOffset;
// The last decoded video frame. Used for computing the sleep period
// between frames for a/v sync. Read/Write from the decode thread only.
PRUint64 mLastFrame;
// The decoder position of the end of the last decoded video frame.
// Read/Write from the decode thread only.
PRInt64 mLastFramePosition;
// Thread that steps through decoding each frame using liboggplay. Only accessed
// via the decode thread.
nsCOMPtr<nsIThread> mStepDecodeThread;
// *****
// The follow fields are accessed by the decoder thread or
// the main thread.
// *****
// The decoder monitor must be obtained before modifying this state.
// NotifyAll on the monitor must be called when the state is changed by
// the main thread so the decoder thread can wake up.
State mState;
// Position to seek to when the seek state transition occurs. The
// decoder monitor lock must be obtained before reading or writing
// this value.
float mSeekTime;
// The audio stream resource. Used on the decode thread and the
// main thread (Via the SetVolume call). Synchronisation via
// mDecoder monitor.
nsAutoPtr<nsAudioStream> mAudioStream;
// The time of the current frame in seconds. This is referenced from
// 0.0 which is the initial start of the stream. Set by the decode
// thread, and read-only from the main thread to get the current
// time value. Synchronised via decoder monitor.
float mCurrentFrameTime;
// The presentation times of the first frame that was decoded. This is
// the start time of the frame. This is subtracted from each frames'
// timestamp, so that playback appears to start at time 0 and end at
// time mDuration. Read/Written from the decode thread, read from the
// main thread. Synchronised via decoder monitor.
float mPlaybackStartTime;
// Volume of playback. 0.0 = muted. 1.0 = full volume. Read/Written
// from the decode and main threads. Synchronised via decoder
// monitor.
float mVolume;
// Duration of the media resource. It is accessed from the decoder and main
// threads. Synchronised via decoder monitor. It is in units of
// milliseconds.
PRInt64 mDuration;
// PR_TRUE if the media resource can be seeked. Accessed from the decoder
// and main threads. Synchronised via decoder monitor.
PRPackedBool mSeekable;
// PR_TRUE if an event to notify about a change in the playback
// position has been queued, but not yet run. It is set to PR_FALSE when
// the event is run. This allows coalescing of these events as they can be
// produced many times per second. Synchronised via decoder monitor.
PRPackedBool mPositionChangeQueued;
// PR_TRUE if the step decode loop thread has finished decoding. It is
// written by the step decode thread and read and written by the state
// machine thread (but only written by the state machine thread while
// the step decode thread is not running).
// Synchronised via decoder monitor.
PRPackedBool mDecodingCompleted;
// PR_TRUE if the step decode loop thread should exit now. It is
// written by the state machine thread and read by the step decode thread.
// Synchronised via decoder monitor.
PRPackedBool mExitStepDecodeThread;
// PR_TRUE if the step decode loop has indicated that we need to buffer.
// Accessed by the step decode thread and the decode state machine thread.
// Synchronised via the decoder monitor.
PRPackedBool mBufferExhausted;
// PR_TRUE if mDuration has a value obtained from an HTTP header.
// Read/Written from the decode and main threads. Synchronised via the
// decoder monitor.
PRPackedBool mGotDurationFromHeader;
};
// Event that gets posted to the thread that is responsible for decoding
// Ogg frames. Decodes each frame of an Ogg file. Locking of liboggplay
// is managed by liboggplay. The thread is created when the frames first
// need to be decoded and is shutdown when decoding is not needed (either
// completed, or seeking).
class nsOggStepDecodeEvent : public nsRunnable {
private:
// Since the lifetime of this event loop is controlled by the
// decode state machine object, it is safe to keep an
// unreferenced counted pointer to it, so we can inform
// it when we've finished decoding.
nsOggDecodeStateMachine* mDecodeStateMachine;
// The lifetime of this player is managed by the decode state
// machine thread. This event is created and destroyed before
// the mPlayer object itself is deleted.
OggPlay* mPlayer;
public:
nsOggStepDecodeEvent(nsOggDecodeStateMachine* machine, OggPlay* player) :
mDecodeStateMachine(machine), mPlayer(player) {}
// Return true if we are in a state where the decoder should not be running.
PRBool InStopDecodingState() {
PR_ASSERT_CURRENT_THREAD_IN_MONITOR(mDecodeStateMachine->mDecoder->GetMonitor());
return
mDecodeStateMachine->mState != nsOggDecodeStateMachine::DECODER_STATE_DECODING &&
mDecodeStateMachine->mState != nsOggDecodeStateMachine::DECODER_STATE_BUFFERING;
}
// This method will block on oggplay_step_decoding when oggplay's
// internal buffers are full. It is unblocked by the decode
// state machine thread via a call to oggplay_prepare_for_close
// during the shutdown protocol. It is unblocked during seeking
// by release frames from liboggplay's frame queue.
NS_IMETHOD Run() {
OggPlayErrorCode r = E_OGGPLAY_TIMEOUT;
nsAutoMonitor mon(mDecodeStateMachine->mDecoder->GetMonitor());
nsOggDecoder* decoder = mDecodeStateMachine->mDecoder;
NS_ASSERTION(!mDecodeStateMachine->mDecodingCompleted,
"State machine should have cleared this flag");
while (!mDecodeStateMachine->mExitStepDecodeThread &&
!InStopDecodingState() &&
(r == E_OGGPLAY_TIMEOUT ||
r == E_OGGPLAY_USER_INTERRUPT ||
r == E_OGGPLAY_CONTINUE)) {
if (mDecodeStateMachine->mBufferExhausted) {
mon.Wait();
} else {
// decoder and decoder->mReader are never null here because
// they are non-null through the lifetime of the state machine
// thread, which includes the lifetime of this thread.
PRInt64 initialDownloadPosition =
decoder->mReader->Stream()->GetCachedDataEnd(decoder->mDecoderPosition);
mon.Exit();
r = oggplay_step_decoding(mPlayer);
mon.Enter();
mDecodeStateMachine->HandleDecodeErrors(r);
// Check whether decoding the last frame required us to read data
// that wasn't available at the start of the frame. That means
// we should probably start buffering.
if (decoder->mDecoderPosition > initialDownloadPosition) {
mDecodeStateMachine->mBufferExhausted = PR_TRUE;
}
// If PlayFrame is waiting, wake it up so we can run the
// decoder loop and move frames from the oggplay queue to our
// queue. Also needed to wake up the decoder loop that waits
// for a frame to be ready to display.
mon.NotifyAll();
}
}
mDecodeStateMachine->mDecodingCompleted = PR_TRUE;
return NS_OK;
}
};
nsOggDecodeStateMachine::nsOggDecodeStateMachine(nsOggDecoder* aDecoder) :
mDecoder(aDecoder),
mPlayer(0),
mPlayStartTime(),
mPauseStartTime(),
mPauseDuration(0),
mPlaying(PR_FALSE),
mCallbackPeriod(1.0),
mVideoTrack(-1),
mFramerate(0.0),
mAspectRatio(1.0),
mAudioRate(0),
mAudioChannels(0),
mAudioTrack(-1),
mBufferingStart(),
mBufferingEndOffset(0),
mLastFrame(0),
mLastFramePosition(-1),
mState(DECODER_STATE_DECODING_METADATA),
mSeekTime(0.0),
mCurrentFrameTime(0.0),
mPlaybackStartTime(0.0),
mVolume(1.0),
mDuration(-1),
mSeekable(PR_TRUE),
mPositionChangeQueued(PR_FALSE),
mDecodingCompleted(PR_FALSE),
mExitStepDecodeThread(PR_FALSE),
mBufferExhausted(PR_FALSE),
mGotDurationFromHeader(PR_FALSE)
{
}
nsOggDecodeStateMachine::~nsOggDecodeStateMachine()
{
while (!mDecodedFrames.IsEmpty()) {
delete mDecodedFrames.Pop();
}
oggplay_close(mPlayer);
}
OggPlayErrorCode nsOggDecodeStateMachine::DecodeFrame()
{
OggPlayErrorCode r = oggplay_step_decoding(mPlayer);
return r;
}
void nsOggDecodeStateMachine::HandleDecodeErrors(OggPlayErrorCode aErrorCode)
{
PR_ASSERT_CURRENT_THREAD_IN_MONITOR(mDecoder->GetMonitor());
if (aErrorCode != E_OGGPLAY_TIMEOUT &&
aErrorCode != E_OGGPLAY_OK &&
aErrorCode != E_OGGPLAY_USER_INTERRUPT &&
aErrorCode != E_OGGPLAY_CONTINUE) {
mState = DECODER_STATE_SHUTDOWN;
nsCOMPtr<nsIRunnable> event =
NS_NEW_RUNNABLE_METHOD(nsOggDecoder, mDecoder, DecodeError);
NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL);
}
}
nsOggDecodeStateMachine::FrameData* nsOggDecodeStateMachine::NextFrame()
{
PR_ASSERT_CURRENT_THREAD_IN_MONITOR(mDecoder->GetMonitor());
OggPlayCallbackInfo** info = oggplay_buffer_retrieve_next(mPlayer);
if (!info)
return nsnull;
FrameData* frame = new FrameData();
if (!frame) {
return nsnull;
}
frame->mTime = mCallbackPeriod * mLastFrame;
frame->mEndStreamPosition = mDecoder->mDecoderPosition;
mLastFrame += 1;
if (mLastFramePosition >= 0) {
NS_ASSERTION(frame->mEndStreamPosition >= mLastFramePosition,
"Playback positions must not decrease without an intervening reset");
TimeStamp base = mPlayStartTime;
if (base.IsNull()) {
// It doesn't really matter what 'base' is, so just use 'now' if
// we haven't started playback.
base = TimeStamp::Now();
}
mDecoder->mPlaybackStatistics.Start(
base + TimeDuration::FromMilliseconds(NS_round(frame->mTime*1000)));
mDecoder->mPlaybackStatistics.AddBytes(frame->mEndStreamPosition - mLastFramePosition);
mDecoder->mPlaybackStatistics.Stop(
base + TimeDuration::FromMilliseconds(NS_round(mCallbackPeriod*mLastFrame*1000)));
mDecoder->UpdatePlaybackRate();
}
mLastFramePosition = frame->mEndStreamPosition;
int num_tracks = oggplay_get_num_tracks(mPlayer);
float audioTime = -1.0;
float videoTime = -1.0;
if (mVideoTrack != -1 &&
num_tracks > mVideoTrack &&
oggplay_callback_info_get_type(info[mVideoTrack]) == OGGPLAY_YUV_VIDEO) {
OggPlayDataHeader** headers = oggplay_callback_info_get_headers(info[mVideoTrack]);
if (headers[0]) {
videoTime = ((float)oggplay_callback_info_get_presentation_time(headers[0]))/1000.0;
HandleVideoData(frame, mVideoTrack, headers[0]);
}
}
// If the audio stream has finished, but there's still video frames to
// be rendered, we need to send blank audio data to the audio hardware,
// so that the audio clock, which maintains the presentation time, keeps
// incrementing.
PRBool needSilence = PR_FALSE;
if (mAudioTrack != -1 && num_tracks > mAudioTrack) {
OggPlayDataType type = oggplay_callback_info_get_type(info[mAudioTrack]);
needSilence = (type == OGGPLAY_INACTIVE);
if (type == OGGPLAY_FLOATS_AUDIO) {
OggPlayDataHeader** headers = oggplay_callback_info_get_headers(info[mAudioTrack]);
if (headers[0]) {
audioTime = ((float)oggplay_callback_info_get_presentation_time(headers[0]))/1000.0;
int required = oggplay_callback_info_get_required(info[mAudioTrack]);
for (int j = 0; j < required; ++j) {
int size = oggplay_callback_info_get_record_size(headers[j]);
OggPlayAudioData* audio_data = oggplay_callback_info_get_audio_data(headers[j]);
HandleAudioData(frame, audio_data, size);
}
}
}
}
if (needSilence) {
// Write silence to keep audio clock moving for av sync
size_t count = mAudioChannels * mAudioRate * mCallbackPeriod;
// count must be evenly divisble by number of channels.
count = mAudioChannels * PRInt32(NS_ceil(mAudioRate*mCallbackPeriod));
float* data = frame->mAudioData.AppendElements(count);
if (data) {
memset(data, 0, sizeof(float)*count);
}
}
// Pick one stream to act as the reference track to indicate if the
// stream has ended, seeked, etc.
if (videoTime >= 0) {
frame->mState = oggplay_callback_info_get_stream_info(info[mVideoTrack]);
frame->mDecodedFrameTime = videoTime;
} else if (audioTime >= 0) {
frame->mState = oggplay_callback_info_get_stream_info(info[mAudioTrack]);
frame->mDecodedFrameTime = audioTime;
} else {
NS_WARNING("Encountered frame with no audio or video data");
frame->mState = OGGPLAY_STREAM_UNINITIALISED;
frame->mDecodedFrameTime = 0.0;
}
oggplay_buffer_release(mPlayer, info);
return frame;
}
void nsOggDecodeStateMachine::HandleVideoData(FrameData* aFrame, int aTrackNum, OggPlayDataHeader* aVideoHeader) {
if (!aVideoHeader)
return;
int y_width = 0;
int y_height = 0;
oggplay_get_video_y_size(mPlayer, aTrackNum, &y_width, &y_height);
int uv_width = 0;
int uv_height = 0;
oggplay_get_video_uv_size(mPlayer, aTrackNum, &uv_width, &uv_height);
if (y_width >= MAX_VIDEO_WIDTH || y_height >= MAX_VIDEO_HEIGHT) {
return;
}
aFrame->mVideoWidth = y_width;
aFrame->mVideoHeight = y_height;
aFrame->mUVWidth = uv_width;
aFrame->mUVHeight = uv_height;
aFrame->SetVideoHeader(aVideoHeader);
}
void nsOggDecodeStateMachine::HandleAudioData(FrameData* aFrame, OggPlayAudioData* aAudioData, int aSize) {
// 'aSize' is number of samples. Multiply by number of channels to
// get the actual number of floats being sent.
int size = aSize * mAudioChannels;
aFrame->mAudioData.AppendElements(reinterpret_cast<float*>(aAudioData), size);
}
void nsOggDecodeStateMachine::PlayFrame() {
// Play a frame of video and/or audio data.
// If we are playing we open the audio stream if needed
// If there are decoded frames in the queue a single frame
// is popped off and played if it is time for that frame
// to display.
// If it is not time yet to display the frame, we either
// continue decoding frames, or wait until it is time for
// the frame to display if the queue is full.
//
// If the decode state is not PLAYING then we just exit
// so we can continue decoding frames. If the queue is
// full we wait for a state change.
nsAutoMonitor mon(mDecoder->GetMonitor());
if (mDecoder->GetState() == nsOggDecoder::PLAY_STATE_PLAYING) {
if (!mPlaying) {
ResumePlayback();
}
if (!mDecodedFrames.IsEmpty()) {
FrameData* frame = mDecodedFrames.Peek();
if (frame->mState == OGGPLAY_STREAM_JUST_SEEKED) {
// After returning from a seek all mTime members of
// FrameData start again from a time position of 0.
// Reset the play start time.
mPlayStartTime = TimeStamp::Now();
mPauseDuration = TimeDuration(0);
frame->mState = OGGPLAY_STREAM_INITIALISED;
}
double time;
PRUint32 hasAudio = frame->mAudioData.Length();
for (;;) {
// Even if the frame has had its audio data written we call
// PlayAudio to ensure that any data we have buffered in the
// nsAudioStream is written to the hardware.
PlayAudio(frame);
double hwtime = mAudioStream && hasAudio ? mAudioStream->GetPosition() : -1.0;
time = hwtime < 0.0 ?
(TimeStamp::Now() - mPlayStartTime - mPauseDuration).ToSeconds() :
hwtime;
// Resynchronize the system clock against the audio clock.
if (hwtime >= 0.0) {
mPlayStartTime = TimeStamp::Now();
mPlayStartTime -= TimeDuration::FromMilliseconds(hwtime * 1000.0);
mPauseDuration = TimeDuration(0);
}
// Is it time for the next frame? Using an integer here avoids f.p.
// rounding errors that can cause multiple 0ms waits (Bug 495352)
PRInt64 wait = PRInt64((frame->mTime - time)*1000);
if (wait <= 0)
break;
mon.Wait(PR_MillisecondsToInterval(wait));
if (mState == DECODER_STATE_SHUTDOWN)
return;
}
mDecodedFrames.Pop();
QueueDecodedFrames();
// Skip frames up to the one we should be showing.
while (!mDecodedFrames.IsEmpty() && time >= mDecodedFrames.Peek()->mTime) {
LOG(PR_LOG_DEBUG, ("%p Skipping frame time %f with audio at time %f", mDecoder, mDecodedFrames.Peek()->mTime, time));
PlayAudio(frame);
delete frame;
frame = mDecodedFrames.Peek();
mDecodedFrames.Pop();
}
if (time < frame->mTime + mCallbackPeriod) {
PlayAudio(frame);
PlayVideo(frame);
mDecoder->mPlaybackPosition = frame->mEndStreamPosition;
UpdatePlaybackPosition(frame->mDecodedFrameTime);
delete frame;
}
else {
PlayAudio(frame);
delete frame;
frame = 0;
}
}
}
else {
if (mPlaying) {
PausePlayback();
}
if (mState == DECODER_STATE_DECODING) {
mon.Wait();
if (mState == DECODER_STATE_SHUTDOWN) {
return;
}
}
}
}
static void ToARGBHook(const PlanarYCbCrImage::Data& aData, PRUint8* aOutput)
{
OggPlayYUVChannels yuv;
NS_ASSERTION(aData.mYStride == aData.mYSize.width,
"Stride not supported");
NS_ASSERTION(aData.mCbCrStride == aData.mCbCrSize.width,
"Stride not supported");
yuv.ptry = aData.mYChannel;
yuv.ptru = aData.mCbChannel;
yuv.ptrv = aData.mCrChannel;
yuv.uv_width = aData.mCbCrSize.width;
yuv.uv_height = aData.mCbCrSize.height;
yuv.y_width = aData.mYSize.width;
yuv.y_height = aData.mYSize.height;
OggPlayRGBChannels rgb;
rgb.ptro = aOutput;
rgb.rgb_width = aData.mYSize.width;
rgb.rgb_height = aData.mYSize.height;
oggplay_yuv2bgra(&yuv, &rgb);
}
void nsOggDecodeStateMachine::PlayVideo(FrameData* aFrame)
{
PR_ASSERT_CURRENT_THREAD_IN_MONITOR(mDecoder->GetMonitor());
if (aFrame && aFrame->mVideoHeader) {
ImageContainer* container = mDecoder->GetImageContainer();
// Currently our Ogg decoder only knows how to output to PLANAR_YCBCR
// format.
Image::Format format = Image::PLANAR_YCBCR;
nsRefPtr<Image> image;
if (container) {
image = container->CreateImage(&format, 1);
}
if (image) {
NS_ASSERTION(image->GetFormat() == Image::PLANAR_YCBCR,
"Wrong format?");
PlanarYCbCrImage* videoImage = static_cast<PlanarYCbCrImage*>(image.get());
// XXX this is only temporary until we get YUV code in the layer
// system.
videoImage->SetRGBConverter(ToARGBHook);
OggPlayVideoData* videoData = oggplay_callback_info_get_video_data(aFrame->mVideoHeader);
PlanarYCbCrImage::Data data;
data.mYChannel = videoData->y;
data.mYSize = gfxIntSize(aFrame->mVideoWidth, aFrame->mVideoHeight);
data.mYStride = data.mYSize.width;
data.mCbChannel = videoData->u;
data.mCrChannel = videoData->v;
data.mCbCrSize = gfxIntSize(aFrame->mUVWidth, aFrame->mUVHeight);
data.mCbCrStride = data.mCbCrSize.width;
videoImage->SetData(data);
mDecoder->SetVideoData(data.mYSize, mAspectRatio, image);
}
// Don't play the frame's video data more than once.
aFrame->ClearVideoHeader();
}
}
void nsOggDecodeStateMachine::PlayAudio(FrameData* aFrame)
{
PR_ASSERT_CURRENT_THREAD_IN_MONITOR(mDecoder->GetMonitor());
if (!mAudioStream)
return;
aFrame->Write(mAudioStream);
}
void nsOggDecodeStateMachine::OpenAudioStream()
{
PR_ASSERT_CURRENT_THREAD_IN_MONITOR(mDecoder->GetMonitor());
mAudioStream = new nsAudioStream();
if (!mAudioStream) {
LOG(PR_LOG_ERROR, ("%p Could not create audio stream", mDecoder));
}
else {
mAudioStream->Init(mAudioChannels, mAudioRate, nsAudioStream::FORMAT_FLOAT32);
mAudioStream->SetVolume(mVolume);
}
}
void nsOggDecodeStateMachine::CloseAudioStream()
{
PR_ASSERT_CURRENT_THREAD_IN_MONITOR(mDecoder->GetMonitor());
if (mAudioStream) {
mAudioStream->Shutdown();
mAudioStream = nsnull;
}
}
void nsOggDecodeStateMachine::StartAudio()
{
PR_ASSERT_CURRENT_THREAD_IN_MONITOR(mDecoder->GetMonitor());
if (HasAudio()) {
OpenAudioStream();
}
}
void nsOggDecodeStateMachine::StopAudio()
{
PR_ASSERT_CURRENT_THREAD_IN_MONITOR(mDecoder->GetMonitor());
if (HasAudio()) {
CloseAudioStream();
}
}
void nsOggDecodeStateMachine::StartPlayback()
{
PR_ASSERT_CURRENT_THREAD_IN_MONITOR(mDecoder->GetMonitor());
StartAudio();
mPlaying = PR_TRUE;
// If this is the very first play, then set the initial start time
if (mPlayStartTime.IsNull()) {
mPlayStartTime = TimeStamp::Now();
}
// If we have been paused previously, then compute duration spent paused
if (!mPauseStartTime.IsNull()) {
mPauseDuration += TimeStamp::Now() - mPauseStartTime;
// Null out mPauseStartTime
mPauseStartTime = TimeStamp();
}
}
void nsOggDecodeStateMachine::StopPlayback()
{
PR_ASSERT_CURRENT_THREAD_IN_MONITOR(mDecoder->GetMonitor());
mLastFrame = mDecodedFrames.ResetTimes(mCallbackPeriod);
StopAudio();
mPlaying = PR_FALSE;
mPauseStartTime = TimeStamp();
mPauseDuration = 0;
mPlayStartTime = TimeStamp();
}
void nsOggDecodeStateMachine::PausePlayback()
{
if (!mAudioStream) {
StopPlayback();
return;
}
mAudioStream->Pause();
mPlaying = PR_FALSE;
mPauseStartTime = TimeStamp::Now();
}
void nsOggDecodeStateMachine::ResumePlayback()
{
if (!mAudioStream) {
StartPlayback();
return;
}
mAudioStream->Resume();
mPlaying = PR_TRUE;
// Compute duration spent paused
if (!mPauseStartTime.IsNull()) {
mPauseDuration += TimeStamp::Now() - mPauseStartTime;
// Null out mPauseStartTime
mPauseStartTime = TimeStamp();
}
}
void nsOggDecodeStateMachine::UpdatePlaybackPosition(float aTime)
{
PR_ASSERT_CURRENT_THREAD_IN_MONITOR(mDecoder->GetMonitor());
mCurrentFrameTime = aTime - mPlaybackStartTime;
if (!mPositionChangeQueued) {
mPositionChangeQueued = PR_TRUE;
nsCOMPtr<nsIRunnable> event =
NS_NEW_RUNNABLE_METHOD(nsOggDecoder, mDecoder, PlaybackPositionChanged);
NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL);
}
}
void nsOggDecodeStateMachine::QueueDecodedFrames()
{
PR_ASSERT_CURRENT_THREAD_IN_MONITOR(mDecoder->GetMonitor());
FrameData* frame;
while (!mDecodedFrames.IsFull() && (frame = NextFrame())) {
PRUint32 oldFrameCount = mDecodedFrames.GetCount();
mDecodedFrames.Push(frame);
if (oldFrameCount < 2) {
// Transitioning from 0 to 1 frames or from 1 to 2 frames could
// affect HaveNextFrameData and hence what UpdateReadyStateForData does.
// This could change us from HAVE_CURRENT_DATA to HAVE_FUTURE_DATA
// (or even HAVE_ENOUGH_DATA), so we'd better trigger an
// update to the ready state.
UpdateReadyState();
}
}
}
void nsOggDecodeStateMachine::ClearPositionChangeFlag()
{
PR_ASSERT_CURRENT_THREAD_IN_MONITOR(mDecoder->GetMonitor());
mPositionChangeQueued = PR_FALSE;
}
nsHTMLMediaElement::NextFrameStatus nsOggDecodeStateMachine::GetNextFrameStatus()
{
nsAutoMonitor mon(mDecoder->GetMonitor());
if (IsBuffering() || IsSeeking()) {
return nsHTMLMediaElement::NEXT_FRAME_UNAVAILABLE_BUFFERING;
} else if (HaveNextFrameData()) {
return nsHTMLMediaElement::NEXT_FRAME_AVAILABLE;
}
return nsHTMLMediaElement::NEXT_FRAME_UNAVAILABLE;
}
void nsOggDecodeStateMachine::SetVolume(float volume)
{
PR_ASSERT_CURRENT_THREAD_IN_MONITOR(mDecoder->GetMonitor());
if (mAudioStream) {
mAudioStream->SetVolume(volume);
}
mVolume = volume;
}
float nsOggDecodeStateMachine::GetCurrentTime()
{
PR_ASSERT_CURRENT_THREAD_IN_MONITOR(mDecoder->GetMonitor());
return mCurrentFrameTime;
}
PRInt64 nsOggDecodeStateMachine::GetDuration()
{
PR_ASSERT_CURRENT_THREAD_IN_MONITOR(mDecoder->GetMonitor());
return mDuration;
}
void nsOggDecodeStateMachine::SetDuration(PRInt64 aDuration)
{
PR_ASSERT_CURRENT_THREAD_IN_MONITOR(mDecoder->GetMonitor());
mDuration = aDuration;
}
void nsOggDecodeStateMachine::SetSeekable(PRBool aSeekable)
{
PR_ASSERT_CURRENT_THREAD_IN_MONITOR(mDecoder->GetMonitor());
mSeekable = aSeekable;
}
void nsOggDecodeStateMachine::Shutdown()
{
// oggplay_prepare_for_close cannot be undone. Once called, the
// mPlayer object cannot decode any more frames. Once we've entered
// the shutdown state here there's no going back.
nsAutoMonitor mon(mDecoder->GetMonitor());
// Change state before issuing shutdown request to threads so those
// threads can start exiting cleanly during the Shutdown call.
LOG(PR_LOG_DEBUG, ("%p Changed state to SHUTDOWN", mDecoder));
mState = DECODER_STATE_SHUTDOWN;
mon.NotifyAll();
if (mPlayer) {
// This will unblock the step decode loop in the
// StepDecode thread. The thread can then be safely
// shutdown.
oggplay_prepare_for_close(mPlayer);
}
}
void nsOggDecodeStateMachine::Decode()
{
// When asked to decode, switch to decoding only if
// we are currently buffering.
nsAutoMonitor mon(mDecoder->GetMonitor());
if (mState == DECODER_STATE_BUFFERING) {
LOG(PR_LOG_DEBUG, ("%p Changed state from BUFFERING to DECODING", mDecoder));
mState = DECODER_STATE_DECODING;
mon.NotifyAll();
}
}
void nsOggDecodeStateMachine::Seek(float aTime)
{
nsAutoMonitor mon(mDecoder->GetMonitor());
// nsOggDecoder::mPlayState should be SEEKING while we seek, and
// in that case nsOggDecoder shouldn't be calling us.
NS_ASSERTION(mState != DECODER_STATE_SEEKING,
"We shouldn't already be seeking");
mSeekTime = aTime + mPlaybackStartTime;
float duration = static_cast<float>(mDuration) / 1000.0;
NS_ASSERTION(mSeekTime >= 0 && mSeekTime <= duration,
"Can only seek in range [0,duration]");
LOG(PR_LOG_DEBUG, ("%p Changed state to SEEKING (to %f)", mDecoder, aTime));
mState = DECODER_STATE_SEEKING;
}
class ByteRange {
public:
ByteRange() : mStart(-1), mEnd(-1) {}
ByteRange(PRInt64 aStart, PRInt64 aEnd) : mStart(aStart), mEnd(aEnd) {}
PRInt64 mStart, mEnd;
};
static void GetBufferedBytes(nsMediaStream* aStream, nsTArray<ByteRange>& aRanges)
{
PRInt64 startOffset = 0;
while (PR_TRUE) {
PRInt64 endOffset = aStream->GetCachedDataEnd(startOffset);
if (endOffset == startOffset) {
// Uncached at startOffset.
endOffset = aStream->GetNextCachedData(startOffset);
if (endOffset == -1) {
// Uncached at startOffset until endOffset of stream, or we're at
// the end of stream.
break;
}
} else {
// Bytes [startOffset..endOffset] are cached.
PRInt64 cachedLength = endOffset - startOffset;
// Only bother trying to seek inside ranges greater than
// MIN_BOUNDED_SEEK_SIZE, so that the bounded seek is unlikely to
// read outside of the range when finding Ogg page boundaries.
if (cachedLength > MIN_BOUNDED_SEEK_SIZE) {
aRanges.AppendElement(ByteRange(startOffset, endOffset));
}
}
startOffset = endOffset;
}
}
nsresult nsOggDecodeStateMachine::Seek(float aTime, nsChannelReader* aReader)
{
LOG(PR_LOG_DEBUG, ("%p About to seek OggPlay to %fms", mDecoder, aTime));
nsMediaStream* stream = aReader->Stream();
nsAutoTArray<ByteRange, 16> ranges;
stream->Pin();
GetBufferedBytes(stream, ranges);
PRInt64 rv = -1;
for (PRUint32 i = 0; rv < 0 && i < ranges.Length(); i++) {
rv = oggplay_seek_to_keyframe(mPlayer,
ogg_int64_t(aTime * 1000),
ranges[i].mStart,
ranges[i].mEnd);
}
stream->Unpin();
if (rv < 0) {
// Could not seek in a buffered range, fall back to seeking over the
// entire media.
rv = oggplay_seek_to_keyframe(mPlayer,
ogg_int64_t(aTime * 1000),
0,
stream->GetLength());
}
LOG(PR_LOG_DEBUG, ("%p Finished seeking OggPlay", mDecoder));
return (rv < 0) ? NS_ERROR_FAILURE : NS_OK;
}
PRBool nsOggDecodeStateMachine::DecodeToFrame(nsAutoMonitor& aMonitor,
float aTime)
{
// Drop frames before the target time.
float target = aTime - mCallbackPeriod / 2.0;
FrameData* frame = nsnull;
OggPlayErrorCode r;
mLastFrame = 0;
// Some of the audio data from previous frames actually belongs
// to this frame and later frames. So rescue that data and stuff
// it into the first frame.
float audioTime = 0;
nsTArray<float> audioData;
do {
if (frame) {
audioData.AppendElements(frame->mAudioData);
audioTime += frame->mAudioData.Length() /
(float)mAudioRate / (float)mAudioChannels;
}
do {
aMonitor.Exit();
r = DecodeFrame();
aMonitor.Enter();
} while (mState != DECODER_STATE_SHUTDOWN && r == E_OGGPLAY_TIMEOUT);
HandleDecodeErrors(r);
if (mState == DECODER_STATE_SHUTDOWN)
break;
FrameData* nextFrame = NextFrame();
if (!nextFrame)
break;
delete frame;
frame = nextFrame;
} while (frame->mDecodedFrameTime < target);
if (mState == DECODER_STATE_SHUTDOWN) {
delete frame;
return PR_TRUE;
}
NS_ASSERTION(frame != nsnull, "No frame after decode!");
if (frame) {
if (audioTime > frame->mTime) {
// liboggplay gave us more data than expected, we need to prepend
// the extra data to the current frame to keep audio in sync.
audioTime -= frame->mTime;
// numExtraSamples must be evenly divisble by number of channels.
size_t numExtraSamples = mAudioChannels *
PRInt32(NS_ceil(mAudioRate*audioTime));
float* data = audioData.Elements() + audioData.Length() - numExtraSamples;
float* dst = frame->mAudioData.InsertElementsAt(0, numExtraSamples);
memcpy(dst, data, numExtraSamples * sizeof(float));
}
mLastFrame = 0;
frame->mTime = 0;
frame->mState = OGGPLAY_STREAM_JUST_SEEKED;
mDecodedFrames.Push(frame);
UpdatePlaybackPosition(frame->mDecodedFrameTime);
PlayVideo(frame);
}
return r == E_OGGPLAY_OK;
}
void nsOggDecodeStateMachine::StopStepDecodeThread(nsAutoMonitor* aMonitor)
{
PR_ASSERT_CURRENT_THREAD_IN_MONITOR(mDecoder->GetMonitor());
if (!mStepDecodeThread)
return;
if (!mDecodingCompleted) {
// Break the step-decode thread out of the decoding loop. First
// set the exit flag so it will exit the loop.
mExitStepDecodeThread = PR_TRUE;
// Remove liboggplay frame buffer so that the step-decode thread
// can unblock in liboggplay.
delete NextFrame();
// Now notify to wake it up if it's waiting on the monitor.
aMonitor->NotifyAll();
}
aMonitor->Exit();
mStepDecodeThread->Shutdown();
aMonitor->Enter();
mStepDecodeThread = nsnull;
}
nsresult nsOggDecodeStateMachine::Run()
{
nsChannelReader* reader = mDecoder->GetReader();
NS_ENSURE_TRUE(reader, NS_ERROR_NULL_POINTER);
while (PR_TRUE) {
nsAutoMonitor mon(mDecoder->GetMonitor());
switch(mState) {
case DECODER_STATE_SHUTDOWN:
if (mPlaying) {
StopPlayback();
}
StopStepDecodeThread(&mon);
NS_ASSERTION(mState == DECODER_STATE_SHUTDOWN,
"How did we escape from the shutdown state???");
return NS_OK;
case DECODER_STATE_DECODING_METADATA:
{
mon.Exit();
LoadOggHeaders(reader);
mon.Enter();
OggPlayErrorCode r = E_OGGPLAY_TIMEOUT;
while (mState != DECODER_STATE_SHUTDOWN && r == E_OGGPLAY_TIMEOUT) {
mon.Exit();
r = DecodeFrame();
mon.Enter();
}
HandleDecodeErrors(r);
if (mState == DECODER_STATE_SHUTDOWN)
continue;
mLastFrame = 0;
FrameData* frame = NextFrame();
if (frame) {
mDecodedFrames.Push(frame);
mDecoder->mPlaybackPosition = frame->mEndStreamPosition;
mPlaybackStartTime = frame->mDecodedFrameTime;
UpdatePlaybackPosition(frame->mDecodedFrameTime);
// Now that we know the start offset, we can tell the channel
// reader the last frame time.
if (mGotDurationFromHeader) {
// Duration was in HTTP header, so the last frame time is
// start frame time + duration.
reader->SetLastFrameTime((PRInt64)(mPlaybackStartTime * 1000) + mDuration);
}
else if (mDuration != -1) {
// Got duration by seeking to end and getting timestamp of last
// page; mDuration holds the timestamp of the end of the last page.
reader->SetLastFrameTime(mDuration);
// Duration needs to be corrected so it's the length of media, not
// the last frame's end time. Note mPlaybackStartTime is
// presentation time, which is the start-time of the frame.
mDuration -= (PRInt64)(mPlaybackStartTime * 1000);
}
PlayVideo(frame);
}
// Inform the element that we've loaded the Ogg metadata and the
// first frame.
nsCOMPtr<nsIRunnable> metadataLoadedEvent =
NS_NEW_RUNNABLE_METHOD(nsOggDecoder, mDecoder, MetadataLoaded);
NS_DispatchToMainThread(metadataLoadedEvent, NS_DISPATCH_NORMAL);
if (mState == DECODER_STATE_DECODING_METADATA) {
if (r == E_OGGPLAY_OK) {
LOG(PR_LOG_DEBUG, ("%p Changed state from DECODING_METADATA to COMPLETED", mDecoder));
mState = DECODER_STATE_COMPLETED;
} else {
LOG(PR_LOG_DEBUG, ("%p Changed state from DECODING_METADATA to DECODING", mDecoder));
mState = DECODER_STATE_DECODING;
}
}
}
break;
case DECODER_STATE_DECODING:
{
// If there is no step decode thread, start it. It may not be running
// due to us having completed and then restarted playback, seeking,
// or if this is the initial play.
if (!mStepDecodeThread) {
nsresult rv = NS_NewThread(getter_AddRefs(mStepDecodeThread));
if (NS_FAILED(rv)) {
mState = DECODER_STATE_SHUTDOWN;
continue;
}
mBufferExhausted = PR_FALSE;
mDecodingCompleted = PR_FALSE;
mExitStepDecodeThread = PR_FALSE;
nsCOMPtr<nsIRunnable> event = new nsOggStepDecodeEvent(this, mPlayer);
mStepDecodeThread->Dispatch(event, NS_DISPATCH_NORMAL);
}
// Get the decoded frames and store them in our queue of decoded frames
QueueDecodedFrames();
while (mDecodedFrames.IsEmpty() && !mDecodingCompleted &&
!mBufferExhausted) {
if (mPlaying) {
PausePlayback();
}
mon.Wait();
if (mState != DECODER_STATE_DECODING)
break;
QueueDecodedFrames();
}
if (mState != DECODER_STATE_DECODING)
continue;
if (mDecodingCompleted) {
LOG(PR_LOG_DEBUG, ("%p Changed state from DECODING to COMPLETED", mDecoder));
mState = DECODER_STATE_COMPLETED;
StopStepDecodeThread(&mon);
continue;
}
// Show at least the first frame if we're not playing
// so we have a poster frame on initial load and after seek.
if (!mPlaying && !mDecodedFrames.IsEmpty()) {
PlayVideo(mDecodedFrames.Peek());
}
2009-02-05 02:51:24 -08:00
if (mBufferExhausted &&
mDecoder->GetState() == nsOggDecoder::PLAY_STATE_PLAYING &&
!mDecoder->mReader->Stream()->IsDataCachedToEndOfStream(mDecoder->mDecoderPosition) &&
!mDecoder->mReader->Stream()->IsSuspendedByCache()) {
// There is at most one frame in the queue and there's
// more data to load. Let's buffer to make sure we can play a
// decent amount of video in the future.
if (mPlaying) {
PausePlayback();
}
// We need to tell the element that buffering has started.
// We can't just directly send an asynchronous runnable that
// eventually fires the "waiting" event. The problem is that
// there might be pending main-thread events, such as "data
// received" notifications, that mean we're not actually still
// buffering by the time this runnable executes. So instead
// we just trigger UpdateReadyStateForData; when it runs, it
// will check the current state and decide whether to tell
// the element we're buffering or not.
UpdateReadyState();
mBufferingStart = TimeStamp::Now();
PRPackedBool reliable;
double playbackRate = mDecoder->ComputePlaybackRate(&reliable);
mBufferingEndOffset = mDecoder->mDecoderPosition +
BUFFERING_RATE(playbackRate) * BUFFERING_WAIT;
mState = DECODER_STATE_BUFFERING;
if (mPlaying) {
PausePlayback();
}
LOG(PR_LOG_DEBUG, ("%p Changed state from DECODING to BUFFERING", mDecoder));
} else {
if (mBufferExhausted) {
// This will wake up the step decode thread and force it to
// call oggplay_step_decoding at least once. This guarantees
// we make progress.
mBufferExhausted = PR_FALSE;
mon.NotifyAll();
}
PlayFrame();
}
}
break;
case DECODER_STATE_SEEKING:
{
// During the seek, don't have a lock on the decoder state,
// otherwise long seek operations can block the main thread.
// The events dispatched to the main thread are SYNC calls.
// These calls are made outside of the decode monitor lock so
// it is safe for the main thread to makes calls that acquire
// the lock since it won't deadlock. We check the state when
// acquiring the lock again in case shutdown has occurred
// during the time when we didn't have the lock.
StopStepDecodeThread(&mon);
if (mState == DECODER_STATE_SHUTDOWN)
continue;
float seekTime = mSeekTime;
mDecoder->StopProgressUpdates();
StopPlayback();
// Remove all frames decoded prior to seek from the queue
while (!mDecodedFrames.IsEmpty()) {
delete mDecodedFrames.Pop();
}
// SeekingStarted will do a UpdateReadyStateForData which will
// inform the element and its users that we have no frames
// to display
mon.Exit();
nsCOMPtr<nsIRunnable> startEvent =
NS_NEW_RUNNABLE_METHOD(nsOggDecoder, mDecoder, SeekingStarted);
NS_DispatchToMainThread(startEvent, NS_DISPATCH_SYNC);
nsresult res = Seek(seekTime, reader);
// Reactivate all tracks. Liboggplay deactivates tracks when it
// reads to the end of stream, but they must be reactivated in order
// to start reading from them again.
SetTracksActive();
mon.Enter();
mDecoder->StartProgressUpdates();
mLastFramePosition = mDecoder->mPlaybackPosition;
if (mState == DECODER_STATE_SHUTDOWN)
continue;
PRBool atEnd = PR_FALSE;
if (NS_SUCCEEDED(res)) {
atEnd = DecodeToFrame(mon, seekTime);
// mSeekTime should not have changed. While we seek, mPlayState
// should always be PLAY_STATE_SEEKING and no-one will call
// nsOggDecoderStateMachine::Seek.
NS_ASSERTION(seekTime == mSeekTime, "No-one should have changed mSeekTime");
if (mState == DECODER_STATE_SHUTDOWN) {
continue;
}
if (!atEnd) {
OggPlayErrorCode r;
// Now try to decode another frame to see if we're at the end.
do {
mon.Exit();
r = DecodeFrame();
mon.Enter();
} while (mState != DECODER_STATE_SHUTDOWN && r == E_OGGPLAY_TIMEOUT);
HandleDecodeErrors(r);
if (mState == DECODER_STATE_SHUTDOWN)
continue;
atEnd = r == E_OGGPLAY_OK;
}
QueueDecodedFrames();
}
// Change state to DECODING or COMPLETED now. SeekingStopped will
// call nsOggDecodeStateMachine::Seek to reset our state to SEEKING
// if we need to seek again.
nsCOMPtr<nsIRunnable> stopEvent;
if (!atEnd && mDecodedFrames.GetCount() > 1) {
LOG(PR_LOG_DEBUG, ("%p Changed state from SEEKING (to %f) to DECODING",
mDecoder, seekTime));
stopEvent = NS_NEW_RUNNABLE_METHOD(nsOggDecoder, mDecoder, SeekingStopped);
mState = DECODER_STATE_DECODING;
} else {
LOG(PR_LOG_DEBUG, ("%p Changed state from SEEKING (to %f) to COMPLETED",
mDecoder, seekTime));
stopEvent = NS_NEW_RUNNABLE_METHOD(nsOggDecoder, mDecoder, SeekingStoppedAtEnd);
mState = DECODER_STATE_COMPLETED;
}
mon.NotifyAll();
mon.Exit();
NS_DispatchToMainThread(stopEvent, NS_DISPATCH_SYNC);
mon.Enter();
}
break;
case DECODER_STATE_BUFFERING:
{
TimeStamp now = TimeStamp::Now();
if (now - mBufferingStart < TimeDuration::FromSeconds(BUFFERING_WAIT) &&
mDecoder->mReader->Stream()->GetCachedDataEnd(mDecoder->mDecoderPosition) < mBufferingEndOffset &&
!mDecoder->mReader->Stream()->IsDataCachedToEndOfStream(mDecoder->mDecoderPosition) &&
!mDecoder->mReader->Stream()->IsSuspendedByCache()) {
LOG(PR_LOG_DEBUG,
("%p In buffering: buffering data until %d bytes available or %f seconds", mDecoder,
PRUint32(mBufferingEndOffset - mDecoder->mReader->Stream()->GetCachedDataEnd(mDecoder->mDecoderPosition)),
BUFFERING_WAIT - (now - mBufferingStart).ToSeconds()));
mon.Wait(PR_MillisecondsToInterval(1000));
if (mState == DECODER_STATE_SHUTDOWN)
continue;
} else {
LOG(PR_LOG_DEBUG, ("%p Changed state from BUFFERING to DECODING", mDecoder));
mState = DECODER_STATE_DECODING;
}
if (mState != DECODER_STATE_BUFFERING) {
mBufferExhausted = PR_FALSE;
// Notify to allow blocked decoder thread to continue
mon.NotifyAll();
UpdateReadyState();
if (mDecoder->GetState() == nsOggDecoder::PLAY_STATE_PLAYING) {
if (!mPlaying) {
ResumePlayback();
}
}
}
break;
}
2009-02-05 02:51:24 -08:00
case DECODER_STATE_COMPLETED:
{
// Get all the remaining decoded frames in the liboggplay buffer and
// place them in the frame queue.
QueueDecodedFrames();
// Play the remaining frames in the frame queue
while (mState == DECODER_STATE_COMPLETED &&
!mDecodedFrames.IsEmpty()) {
PlayFrame();
if (mState == DECODER_STATE_COMPLETED) {
// Wait for the time of one frame so we don't tight loop
// and we need to release the monitor so timeupdate and
// invalidate's on the main thread can occur.
mon.Wait(PR_MillisecondsToInterval(PRInt64(mCallbackPeriod*1000)));
QueueDecodedFrames();
}
}
if (mState != DECODER_STATE_COMPLETED)
continue;
if (mAudioStream) {
mon.Exit();
LOG(PR_LOG_DEBUG, ("%p Begin nsAudioStream::Drain", mDecoder));
mAudioStream->Drain();
LOG(PR_LOG_DEBUG, ("%p End nsAudioStream::Drain", mDecoder));
mon.Enter();
// After the drain call the audio stream is unusable. Close it so that
// next time audio is used a new stream is created. The StopPlayback
// call also resets the playing flag so audio is restarted correctly.
StopPlayback();
if (mState != DECODER_STATE_COMPLETED)
continue;
}
if (mDecoder->GetState() == nsOggDecoder::PLAY_STATE_PLAYING) {
// We were playing, we need to move the current time to the end of
// media, and send an 'ended' event.
mCurrentFrameTime += mCallbackPeriod;
if (mDuration >= 0) {
mCurrentFrameTime = PR_MAX(mCurrentFrameTime, mDuration / 1000.0);
}
mon.Exit();
nsCOMPtr<nsIRunnable> event =
NS_NEW_RUNNABLE_METHOD(nsOggDecoder, mDecoder, PlaybackEnded);
NS_DispatchToMainThread(event, NS_DISPATCH_SYNC);
mon.Enter();
}
while (mState == DECODER_STATE_COMPLETED) {
mon.Wait();
}
}
break;
}
}
return NS_OK;
}
// Initialize our OggPlay struct with the specified limit on video size.
static OggPlay*
OggPlayOpen(OggPlayReader* reader,
int max_frame_pixels)
{
OggPlay *me = NULL;
int r;
if ((me = oggplay_new_with_reader(reader)) == NULL) {
return NULL;
}
r = oggplay_set_max_video_frame_pixels(me, max_frame_pixels);
if (r != E_OGGPLAY_OK) {
oggplay_close(me);
return NULL;
}
do {
r = oggplay_initialise(me, 0);
} while (r == E_OGGPLAY_TIMEOUT);
if (r != E_OGGPLAY_OK) {
oggplay_close(me);
return NULL;
}
return me;
}
void nsOggDecodeStateMachine::LoadOggHeaders(nsChannelReader* aReader)
{
LOG(PR_LOG_DEBUG, ("%p Loading Ogg Headers", mDecoder));
mPlayer = OggPlayOpen(aReader, MAX_VIDEO_WIDTH * MAX_VIDEO_HEIGHT);
if (!mPlayer) {
nsAutoMonitor mon(mDecoder->GetMonitor());
mState = DECODER_STATE_SHUTDOWN;
HandleDecodeErrors(E_OGGPLAY_UNINITIALISED);
return;
}
LOG(PR_LOG_DEBUG, ("%p There are %d tracks", mDecoder, oggplay_get_num_tracks(mPlayer)));
for (int i = 0; i < oggplay_get_num_tracks(mPlayer); ++i) {
LOG(PR_LOG_DEBUG, ("%p Tracks %d: %s", mDecoder, i, oggplay_get_track_typename(mPlayer, i)));
if (mVideoTrack == -1 && oggplay_get_track_type(mPlayer, i) == OGGZ_CONTENT_THEORA) {
oggplay_set_callback_num_frames(mPlayer, i, 1);
mVideoTrack = i;
int fpsd, fpsn;
oggplay_get_video_fps(mPlayer, i, &fpsd, &fpsn);
mFramerate = fpsd == 0 ? 0.0 : float(fpsn)/float(fpsd);
mCallbackPeriod = 1.0 / mFramerate;
LOG(PR_LOG_DEBUG, ("%p Frame rate: %f", mDecoder, mFramerate));
int aspectd, aspectn;
// this can return E_OGGPLAY_UNINITIALISED if the video has
// no aspect ratio data. We assume 1.0 in that case.
OggPlayErrorCode r =
oggplay_get_video_aspect_ratio(mPlayer, i, &aspectd, &aspectn);
mAspectRatio = r == E_OGGPLAY_OK && aspectd > 0 ?
float(aspectn)/float(aspectd) : 1.0;
int y_width;
int y_height;
oggplay_get_video_y_size(mPlayer, i, &y_width, &y_height);
mDecoder->SetVideoData(gfxIntSize(y_width, y_height), mAspectRatio,
nsnull);
}
else if (mAudioTrack == -1 && oggplay_get_track_type(mPlayer, i) == OGGZ_CONTENT_VORBIS) {
mAudioTrack = i;
oggplay_set_offset(mPlayer, i, OGGPLAY_AUDIO_OFFSET);
oggplay_get_audio_samplerate(mPlayer, i, &mAudioRate);
oggplay_get_audio_channels(mPlayer, i, &mAudioChannels);
LOG(PR_LOG_DEBUG, ("%p samplerate: %d, channels: %d", mDecoder, mAudioRate, mAudioChannels));
}
}
if (mVideoTrack == -1 && mAudioTrack == -1) {
nsAutoMonitor mon(mDecoder->GetMonitor());
HandleDecodeErrors(E_OGGPLAY_UNINITIALISED);
return;
}
SetTracksActive();
if (mVideoTrack == -1) {
oggplay_set_callback_num_frames(mPlayer, mAudioTrack, OGGPLAY_FRAMES_PER_CALLBACK);
mCallbackPeriod = 1.0 / (float(mAudioRate) / OGGPLAY_FRAMES_PER_CALLBACK);
}
LOG(PR_LOG_DEBUG, ("%p Callback Period: %f", mDecoder, mCallbackPeriod));
oggplay_use_buffer(mPlayer, OGGPLAY_BUFFER_SIZE);
// Get the duration from the Ogg file. We only do this if the
// content length of the resource is known as we need to seek
// to the end of the file to get the last time field. We also
// only do this if the resource is seekable and if we haven't
// already obtained the duration via an HTTP header.
{
nsAutoMonitor mon(mDecoder->GetMonitor());
mGotDurationFromHeader = (mDuration != -1);
if (mState != DECODER_STATE_SHUTDOWN &&
aReader->Stream()->GetLength() >= 0 &&
mSeekable &&
mDuration == -1) {
mDecoder->StopProgressUpdates();
// Don't hold the monitor during the duration
// call as it can issue seek requests
// and blocks until these are completed.
mon.Exit();
PRInt64 d = oggplay_get_duration(mPlayer);
oggplay_seek(mPlayer, 0);
mon.Enter();
mDuration = d;
mDecoder->StartProgressUpdates();
mDecoder->UpdatePlaybackRate();
}
if (mState == DECODER_STATE_SHUTDOWN)
return;
}
}
void nsOggDecodeStateMachine::SetTracksActive()
{
if (mVideoTrack != -1 &&
oggplay_set_track_active(mPlayer, mVideoTrack) < 0) {
LOG(PR_LOG_ERROR, ("%p Could not set track %d active", mDecoder, mVideoTrack));
}
if (mAudioTrack != -1 &&
oggplay_set_track_active(mPlayer, mAudioTrack) < 0) {
LOG(PR_LOG_ERROR, ("%p Could not set track %d active", mDecoder, mAudioTrack));
}
}
NS_IMPL_THREADSAFE_ISUPPORTS1(nsOggDecoder, nsIObserver)
void nsOggDecoder::Pause()
{
nsAutoMonitor mon(mMonitor);
if (mPlayState == PLAY_STATE_SEEKING || mPlayState == PLAY_STATE_ENDED) {
mNextState = PLAY_STATE_PAUSED;
return;
}
ChangeState(PLAY_STATE_PAUSED);
}
void nsOggDecoder::SetVolume(float volume)
{
nsAutoMonitor mon(mMonitor);
mInitialVolume = volume;
if (mDecodeStateMachine) {
mDecodeStateMachine->SetVolume(volume);
}
}
float nsOggDecoder::GetDuration()
{
if (mDuration >= 0) {
return static_cast<float>(mDuration) / 1000.0;
}
return std::numeric_limits<float>::quiet_NaN();
}
nsOggDecoder::nsOggDecoder() :
nsMediaDecoder(),
mDecoderPosition(0),
mPlaybackPosition(0),
mCurrentTime(0.0),
mInitialVolume(0.0),
mRequestedSeekTime(-1.0),
mDuration(-1),
mSeekable(PR_TRUE),
mReader(nsnull),
mMonitor(nsnull),
mPlayState(PLAY_STATE_PAUSED),
mNextState(PLAY_STATE_PAUSED),
mResourceLoaded(PR_FALSE),
mIgnoreProgressData(PR_FALSE)
{
MOZ_COUNT_CTOR(nsOggDecoder);
#ifdef PR_LOGGING
if (!gOggDecoderLog) {
gOggDecoderLog = PR_NewLogModule("nsOggDecoder");
}
#endif
}
PRBool nsOggDecoder::Init(nsHTMLMediaElement* aElement)
{
if (!nsMediaDecoder::Init(aElement))
return PR_FALSE;
mMonitor = nsAutoMonitor::NewMonitor("media.decoder");
if (!mMonitor)
return PR_FALSE;
nsContentUtils::RegisterShutdownObserver(this);
mReader = new nsChannelReader();
NS_ENSURE_TRUE(mReader, PR_FALSE);
mImageContainer = aElement->GetImageContainer();
return PR_TRUE;
}
void nsOggDecoder::Stop()
{
NS_ASSERTION(NS_IsMainThread(), "Should be called on main thread");
// The decode thread must die before the state machine can die.
// The state machine must die before the reader.
// The state machine must die before the decoder.
if (mDecodeThread)
mDecodeThread->Shutdown();
mDecodeThread = nsnull;
mDecodeStateMachine = nsnull;
mReader = nsnull;
}
void nsOggDecoder::Shutdown()
{
NS_ASSERTION(NS_IsMainThread(),
"nsOggDecoder::Shutdown called on non-main thread");
if (mShuttingDown)
return;
mShuttingDown = PR_TRUE;
// This changes the decoder state to SHUTDOWN and does other things
// necessary to unblock the state machine thread if it's blocked, so
// the asynchronous shutdown in nsDestroyStateMachine won't deadlock.
if (mDecodeStateMachine) {
mDecodeStateMachine->Shutdown();
}
// Force any outstanding seek and byterange requests to complete
// to prevent shutdown from deadlocking.
if (mReader) {
mReader->Stream()->Close();
}
ChangeState(PLAY_STATE_SHUTDOWN);
nsMediaDecoder::Shutdown();
// We can't destroy mDecodeStateMachine until mDecodeThread is shut down.
// It's unsafe to Shutdown() the decode thread here, as
// nsIThread::Shutdown() may run events, such as JS event handlers,
// and we could be running at an unsafe time such as during element
// destruction.
// So we destroy the decoder on the main thread in an asynchronous event.
// See bug 468721.
nsCOMPtr<nsIRunnable> event =
NS_NEW_RUNNABLE_METHOD(nsOggDecoder, this, Stop);
NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL);
nsContentUtils::UnregisterShutdownObserver(this);
}
nsOggDecoder::~nsOggDecoder()
{
MOZ_COUNT_DTOR(nsOggDecoder);
nsAutoMonitor::DestroyMonitor(mMonitor);
}
nsresult nsOggDecoder::Load(nsMediaStream* aStream,
nsIStreamListener** aStreamListener)
{
if (aStreamListener) {
*aStreamListener = nsnull;
}
{
// Hold the lock while we do this to set proper lock ordering
// expectations for dynamic deadlock detectors: decoder lock(s)
// should be grabbed before the cache lock
nsAutoMonitor mon(mMonitor);
nsresult rv = aStream->Open(aStreamListener);
if (NS_FAILED(rv)) {
delete aStream;
return rv;
}
mReader->Init(aStream);
}
nsresult rv = NS_NewThread(getter_AddRefs(mDecodeThread));
NS_ENSURE_SUCCESS(rv, rv);
mDecodeStateMachine = new nsOggDecodeStateMachine(this);
{
nsAutoMonitor mon(mMonitor);
mDecodeStateMachine->SetSeekable(mSeekable);
mDecodeStateMachine->SetDuration(mDuration);
}
ChangeState(PLAY_STATE_LOADING);
return mDecodeThread->Dispatch(mDecodeStateMachine, NS_DISPATCH_NORMAL);
}
nsresult nsOggDecoder::Play()
{
nsAutoMonitor mon(mMonitor);
if (mPlayState == PLAY_STATE_SEEKING) {
mNextState = PLAY_STATE_PLAYING;
return NS_OK;
}
if (mPlayState == PLAY_STATE_ENDED)
return Seek(0);
ChangeState(PLAY_STATE_PLAYING);
return NS_OK;
}
nsresult nsOggDecoder::Seek(float aTime)
{
nsAutoMonitor mon(mMonitor);
if (aTime < 0.0)
return NS_ERROR_FAILURE;
mRequestedSeekTime = aTime;
// If we are already in the seeking state, then setting mRequestedSeekTime
// above will result in the new seek occurring when the current seek
// completes.
if (mPlayState != PLAY_STATE_SEEKING) {
if (mPlayState == PLAY_STATE_ENDED) {
mNextState = PLAY_STATE_PLAYING;
} else {
mNextState = mPlayState;
}
ChangeState(PLAY_STATE_SEEKING);
}
return NS_OK;
}
nsresult nsOggDecoder::PlaybackRateChanged()
{
return NS_ERROR_NOT_IMPLEMENTED;
}
float nsOggDecoder::GetCurrentTime()
{
return mCurrentTime;
}
nsMediaStream* nsOggDecoder::GetCurrentStream()
{
return mReader ? mReader->Stream() : nsnull;
}
already_AddRefed<nsIPrincipal> nsOggDecoder::GetCurrentPrincipal()
{
if (!mReader)
return nsnull;
return mReader->Stream()->GetCurrentPrincipal();
}
void nsOggDecoder::MetadataLoaded()
{
if (mShuttingDown)
return;
// Only inform the element of MetadataLoaded if not doing a load() in order
// to fulfill a seek, otherwise we'll get multiple metadataloaded events.
PRBool notifyElement = PR_TRUE;
{
nsAutoMonitor mon(mMonitor);
mDuration = mDecodeStateMachine ? mDecodeStateMachine->GetDuration() : -1;
notifyElement = mNextState != PLAY_STATE_SEEKING;
}
if (mElement && notifyElement) {
// Make sure the element and the frame (if any) are told about
// our new size.
Invalidate();
mElement->MetadataLoaded();
}
if (!mResourceLoaded) {
StartProgress();
}
else if (mElement)
{
// Resource was loaded during metadata loading, when progress
// events are being ignored. Fire the final progress event.
mElement->DispatchAsyncProgressEvent(NS_LITERAL_STRING("progress"));
}
// Only inform the element of FirstFrameLoaded if not doing a load() in order
// to fulfill a seek, otherwise we'll get multiple loadedfirstframe events.
PRBool resourceIsLoaded = !mResourceLoaded && mReader &&
mReader->Stream()->IsDataCachedToEndOfStream(mDecoderPosition);
if (mElement && notifyElement) {
mElement->FirstFrameLoaded(resourceIsLoaded);
}
// The element can run javascript via events
// before reaching here, so only change the
// state if we're still set to the original
// loading state.
nsAutoMonitor mon(mMonitor);
if (mPlayState == PLAY_STATE_LOADING) {
if (mRequestedSeekTime >= 0.0) {
ChangeState(PLAY_STATE_SEEKING);
}
else {
ChangeState(mNextState);
}
}
if (resourceIsLoaded) {
ResourceLoaded();
}
}
void nsOggDecoder::ResourceLoaded()
{
// Don't handle ResourceLoaded if we are shutting down, or if
// we need to ignore progress data due to seeking (in the case
// that the seek results in reaching end of file, we get a bogus call
// to ResourceLoaded).
2009-01-06 19:33:42 -08:00
if (mShuttingDown)
return;
{
// If we are seeking or loading then the resource loaded notification we get
// should be ignored, since it represents the end of the seek request.
nsAutoMonitor mon(mMonitor);
if (mIgnoreProgressData || mResourceLoaded || mPlayState == PLAY_STATE_LOADING)
return;
Progress(PR_FALSE);
mResourceLoaded = PR_TRUE;
StopProgress();
}
2009-02-05 02:51:24 -08:00
// Ensure the final progress event gets fired
if (mElement) {
mElement->DispatchAsyncProgressEvent(NS_LITERAL_STRING("progress"));
mElement->ResourceLoaded();
}
}
void nsOggDecoder::NetworkError()
{
if (mShuttingDown)
return;
if (mElement)
mElement->NetworkError();
Shutdown();
}
void nsOggDecoder::DecodeError()
{
if (mShuttingDown)
return;
if (mElement)
mElement->DecodeError();
Shutdown();
}
PRBool nsOggDecoder::IsSeeking() const
{
return mPlayState == PLAY_STATE_SEEKING || mNextState == PLAY_STATE_SEEKING;
}
PRBool nsOggDecoder::IsEnded() const
{
return mPlayState == PLAY_STATE_ENDED || mPlayState == PLAY_STATE_SHUTDOWN;
}
void nsOggDecoder::PlaybackEnded()
{
if (mShuttingDown || mPlayState == nsOggDecoder::PLAY_STATE_SEEKING)
return;
PlaybackPositionChanged();
ChangeState(PLAY_STATE_ENDED);
if (mElement) {
UpdateReadyStateForData();
mElement->PlaybackEnded();
}
}
NS_IMETHODIMP nsOggDecoder::Observe(nsISupports *aSubjet,
const char *aTopic,
const PRUnichar *someData)
{
if (strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) {
Shutdown();
}
return NS_OK;
}
nsMediaDecoder::Statistics
nsOggDecoder::GetStatistics()
{
Statistics result;
nsAutoMonitor mon(mMonitor);
if (mReader) {
result.mDownloadRate =
mReader->Stream()->GetDownloadRate(&result.mDownloadRateReliable);
result.mDownloadPosition =
mReader->Stream()->GetCachedDataEnd(mDecoderPosition);
result.mTotalBytes = mReader->Stream()->GetLength();
result.mPlaybackRate = ComputePlaybackRate(&result.mPlaybackRateReliable);
result.mDecoderPosition = mDecoderPosition;
result.mPlaybackPosition = mPlaybackPosition;
2009-03-31 20:19:00 -07:00
} else {
result.mDownloadRate = 0;
result.mDownloadRateReliable = PR_TRUE;
result.mPlaybackRate = 0;
result.mPlaybackRateReliable = PR_TRUE;
result.mDecoderPosition = 0;
result.mPlaybackPosition = 0;
result.mDownloadPosition = 0;
result.mTotalBytes = 0;
}
return result;
}
double nsOggDecoder::ComputePlaybackRate(PRPackedBool* aReliable)
{
PRInt64 length = mReader ? mReader->Stream()->GetLength() : -1;
if (mDuration >= 0 && length >= 0) {
*aReliable = PR_TRUE;
return double(length)*1000.0/mDuration;
}
return mPlaybackStatistics.GetRateAtLastStop(aReliable);
}
void nsOggDecoder::UpdatePlaybackRate()
{
if (!mReader)
return;
PRPackedBool reliable;
PRUint32 rate = PRUint32(ComputePlaybackRate(&reliable));
if (reliable) {
// Avoid passing a zero rate
rate = PR_MAX(rate, 1);
} else {
// Set a minimum rate of 10,000 bytes per second ... sometimes we just
// don't have good data
rate = PR_MAX(rate, 10000);
}
mReader->Stream()->SetPlaybackRate(rate);
}
void nsOggDecoder::NotifySuspendedStatusChanged()
{
NS_ASSERTION(NS_IsMainThread(),
"nsOggDecoder::NotifyDownloadSuspended called on non-main thread");
if (!mReader)
return;
if (mReader->Stream()->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 nsOggDecoder::NotifyBytesDownloaded()
{
NS_ASSERTION(NS_IsMainThread(),
"nsOggDecoder::NotifyBytesDownloaded called on non-main thread");
UpdateReadyStateForData();
Progress(PR_FALSE);
}
void nsOggDecoder::NotifyDownloadEnded(nsresult aStatus)
{
if (aStatus == NS_BINDING_ABORTED)
return;
{
nsAutoMonitor mon(mMonitor);
UpdatePlaybackRate();
}
if (NS_SUCCEEDED(aStatus)) {
ResourceLoaded();
} else if (aStatus != NS_BASE_STREAM_CLOSED) {
NetworkError();
}
UpdateReadyStateForData();
}
void nsOggDecoder::NotifyBytesConsumed(PRInt64 aBytes)
{
nsAutoMonitor mon(mMonitor);
if (!mIgnoreProgressData) {
mDecoderPosition += aBytes;
}
}
void nsOggDecoder::NextFrameUnavailableBuffering()
{
NS_ASSERTION(NS_IsMainThread(), "Should be called on main thread");
if (!mElement || mShuttingDown || !mDecodeStateMachine)
return;
mElement->UpdateReadyStateForData(nsHTMLMediaElement::NEXT_FRAME_UNAVAILABLE_BUFFERING);
}
void nsOggDecoder::NextFrameAvailable()
{
NS_ASSERTION(NS_IsMainThread(), "Should be called on main thread");
if (!mElement || mShuttingDown || !mDecodeStateMachine)
return;
mElement->UpdateReadyStateForData(nsHTMLMediaElement::NEXT_FRAME_AVAILABLE);
}
void nsOggDecoder::NextFrameUnavailable()
{
NS_ASSERTION(NS_IsMainThread(), "Should be called on main thread");
if (!mElement || mShuttingDown || !mDecodeStateMachine)
return;
mElement->UpdateReadyStateForData(nsHTMLMediaElement::NEXT_FRAME_UNAVAILABLE);
}
void nsOggDecoder::UpdateReadyStateForData()
{
NS_ASSERTION(NS_IsMainThread(), "Should be called on main thread");
if (!mElement || mShuttingDown || !mDecodeStateMachine)
return;
nsHTMLMediaElement::NextFrameStatus frameStatus =
mDecodeStateMachine->GetNextFrameStatus();
mElement->UpdateReadyStateForData(frameStatus);
}
void nsOggDecoder::SeekingStopped()
{
if (mShuttingDown)
return;
{
nsAutoMonitor mon(mMonitor);
// An additional seek was requested while the current seek was
// in operation.
if (mRequestedSeekTime >= 0.0)
ChangeState(PLAY_STATE_SEEKING);
else
ChangeState(mNextState);
}
if (mElement) {
UpdateReadyStateForData();
mElement->SeekCompleted();
}
}
// This is called when seeking stopped *and* we're at the end of the
// media.
void nsOggDecoder::SeekingStoppedAtEnd()
{
if (mShuttingDown)
return;
PRBool fireEnded = PR_FALSE;
{
nsAutoMonitor mon(mMonitor);
// An additional seek was requested while the current seek was
// in operation.
if (mRequestedSeekTime >= 0.0) {
ChangeState(PLAY_STATE_SEEKING);
} else {
fireEnded = mNextState != PLAY_STATE_PLAYING;
ChangeState(fireEnded ? PLAY_STATE_ENDED : mNextState);
}
}
if (mElement) {
UpdateReadyStateForData();
mElement->SeekCompleted();
if (fireEnded) {
mElement->PlaybackEnded();
}
}
}
void nsOggDecoder::SeekingStarted()
{
if (mShuttingDown)
return;
if (mElement) {
UpdateReadyStateForData();
mElement->SeekStarted();
}
}
void nsOggDecoder::ChangeState(PlayState aState)
{
NS_ASSERTION(NS_IsMainThread(),
"nsOggDecoder::ChangeState called on non-main thread");
nsAutoMonitor mon(mMonitor);
if (mNextState == aState) {
mNextState = PLAY_STATE_PAUSED;
}
if (mPlayState == PLAY_STATE_SHUTDOWN) {
mon.NotifyAll();
return;
}
mPlayState = aState;
switch (aState) {
case PLAY_STATE_PAUSED:
/* No action needed */
break;
case PLAY_STATE_PLAYING:
mDecodeStateMachine->Decode();
break;
case PLAY_STATE_SEEKING:
mDecodeStateMachine->Seek(mRequestedSeekTime);
mRequestedSeekTime = -1.0;
break;
case PLAY_STATE_LOADING:
/* No action needed */
break;
case PLAY_STATE_START:
/* No action needed */
break;
case PLAY_STATE_ENDED:
/* No action needed */
break;
case PLAY_STATE_SHUTDOWN:
/* No action needed */
break;
}
mon.NotifyAll();
}
void nsOggDecoder::PlaybackPositionChanged()
{
if (mShuttingDown)
return;
float lastTime = mCurrentTime;
// Control the scope of the monitor so it is not
// held while the timeupdate and the invalidate is run.
{
nsAutoMonitor mon(mMonitor);
if (mDecodeStateMachine) {
mCurrentTime = mDecodeStateMachine->GetCurrentTime();
mDecodeStateMachine->ClearPositionChangeFlag();
}
}
// Invalidate the frame so any video data is displayed.
// Do this before the timeupdate event so that if that
// event runs JavaScript that queries the media size, the
// frame has reflowed and the size updated beforehand.
Invalidate();
if (mElement && lastTime != mCurrentTime) {
mElement->DispatchSimpleEvent(NS_LITERAL_STRING("timeupdate"));
}
}
void nsOggDecoder::SetDuration(PRInt64 aDuration)
{
mDuration = aDuration;
if (mDecodeStateMachine) {
nsAutoMonitor mon(mMonitor);
mDecodeStateMachine->SetDuration(mDuration);
UpdatePlaybackRate();
}
}
void nsOggDecoder::SetSeekable(PRBool aSeekable)
{
mSeekable = aSeekable;
if (mDecodeStateMachine) {
nsAutoMonitor mon(mMonitor);
mDecodeStateMachine->SetSeekable(aSeekable);
}
}
PRBool nsOggDecoder::GetSeekable()
{
return mSeekable;
}
2009-01-06 19:33:42 -08:00
void nsOggDecoder::Suspend()
{
if (mReader) {
mReader->Stream()->Suspend(PR_TRUE);
}
}
void nsOggDecoder::Resume()
{
if (mReader) {
mReader->Stream()->Resume();
}
}
void nsOggDecoder::StopProgressUpdates()
{
mIgnoreProgressData = PR_TRUE;
if (mReader) {
mReader->Stream()->SetReadMode(nsMediaCacheStream::MODE_METADATA);
}
}
void nsOggDecoder::StartProgressUpdates()
{
mIgnoreProgressData = PR_FALSE;
if (mReader) {
mReader->Stream()->SetReadMode(nsMediaCacheStream::MODE_PLAYBACK);
mDecoderPosition = mPlaybackPosition = mReader->Stream()->Tell();
}
}
void nsOggDecoder::MoveLoadsToBackground()
{
if (mReader) {
mReader->Stream()->MoveLoadsToBackground();
}
}