From c5544bacacf18806f1193c6a7c6267b9a4850030 Mon Sep 17 00:00:00 2001 From: Chris Pearce Date: Wed, 6 Jul 2011 10:05:24 +1200 Subject: [PATCH] Bug 592833 - Run media state machine as a series of events. r=roc --- content/media/nsBuiltinDecoder.cpp | 24 +- content/media/nsBuiltinDecoder.h | 5 + .../media/nsBuiltinDecoderStateMachine.cpp | 624 +++++++++--------- content/media/nsBuiltinDecoderStateMachine.h | 22 + content/media/test/seek1.js | 5 +- 5 files changed, 375 insertions(+), 305 deletions(-) diff --git a/content/media/nsBuiltinDecoder.cpp b/content/media/nsBuiltinDecoder.cpp index c94017b23f6..1326e2ca5fd 100644 --- a/content/media/nsBuiltinDecoder.cpp +++ b/content/media/nsBuiltinDecoder.cpp @@ -244,16 +244,32 @@ nsresult nsBuiltinDecoder::RequestFrameBufferLength(PRUint32 aLength) return res; } -nsresult nsBuiltinDecoder::StartStateMachineThread() +nsresult nsBuiltinDecoder::CreateStateMachineThread() { + if (mShuttingDown) + return NS_OK; NS_ASSERTION(mDecoderStateMachine, "Must have state machine to start state machine thread"); if (mStateMachineThread) { return NS_OK; } - nsresult rv = NS_NewThread(getter_AddRefs(mStateMachineThread)); - NS_ENSURE_SUCCESS(rv, rv); - return mStateMachineThread->Dispatch(mDecoderStateMachine, NS_DISPATCH_NORMAL); + nsresult res = NS_NewThread(getter_AddRefs(mStateMachineThread)); + if (NS_FAILED(res)) { + Shutdown(); + return res; + } + return NS_OK; +} + +nsresult nsBuiltinDecoder::StartStateMachineThread() +{ + if (mShuttingDown) + return NS_OK; + NS_ASSERTION(mDecoderStateMachine, + "Must have state machine to start state machine thread"); + nsresult res = CreateStateMachineThread(); + if (NS_FAILED(res)) return res; + return mStateMachineThread->Dispatch(mDecoderStateMachine, NS_DISPATCH_NORMAL); } nsresult nsBuiltinDecoder::Play() diff --git a/content/media/nsBuiltinDecoder.h b/content/media/nsBuiltinDecoder.h index 449839c85be..4c78d8a8fee 100644 --- a/content/media/nsBuiltinDecoder.h +++ b/content/media/nsBuiltinDecoder.h @@ -571,6 +571,11 @@ public: // if necessary. nsresult StartStateMachineThread(); + // Creates the state machine thread. The state machine may wish to create + // the state machine thread without running it immediately if it needs to + // schedule it to run in future. + nsresult CreateStateMachineThread(); + /****** * The following members should be accessed with the decoder lock held. ******/ diff --git a/content/media/nsBuiltinDecoderStateMachine.cpp b/content/media/nsBuiltinDecoderStateMachine.cpp index e0a60b1d336..3677d19e85e 100644 --- a/content/media/nsBuiltinDecoderStateMachine.cpp +++ b/content/media/nsBuiltinDecoderStateMachine.cpp @@ -202,6 +202,9 @@ nsBuiltinDecoderStateMachine::nsBuiltinDecoderStateMachine(nsBuiltinDecoder* aDe nsBuiltinDecoderStateMachine::~nsBuiltinDecoderStateMachine() { MOZ_COUNT_DTOR(nsBuiltinDecoderStateMachine); + if (mTimer) + mTimer->Cancel(); + mTimer = nsnull; } PRBool nsBuiltinDecoderStateMachine::HasFutureAudio() const { @@ -243,7 +246,6 @@ void nsBuiltinDecoderStateMachine::DecodeThreadRun() "Should be in shutdown state if metadata loading fails."); LOG(PR_LOG_DEBUG, ("Decode metadata failed, shutting down decode thread")); } - mDecoder->GetReentrantMonitor().NotifyAll(); } while (mState != DECODER_STATE_SHUTDOWN && mState != DECODER_STATE_COMPLETED) { @@ -251,7 +253,6 @@ void nsBuiltinDecoderStateMachine::DecodeThreadRun() DecodeLoop(); } else if (mState == DECODER_STATE_SEEKING) { DecodeSeek(); - mDecoder->GetReentrantMonitor().NotifyAll(); } } @@ -411,7 +412,7 @@ void nsBuiltinDecoderStateMachine::DecodeLoop() mState != DECODER_STATE_SEEKING) { mState = DECODER_STATE_COMPLETED; - mDecoder->GetReentrantMonitor().NotifyAll(); + ScheduleStateMachine(); } LOG(PR_LOG_DEBUG, ("%p Exiting DecodeLoop", mDecoder)); @@ -612,8 +613,7 @@ void nsBuiltinDecoderStateMachine::AudioLoop() mEventManager.Clear(); mAudioCompleted = PR_TRUE; UpdateReadyState(); - // Kick the decode and state machine threads; they may be sleeping waiting - // for this to finish. + // Kick the decode thread; it may be sleeping waiting for this to finish. mDecoder->GetReentrantMonitor().NotifyAll(); } LOG(PR_LOG_DEBUG, ("%p Audio stream finished playing, audio thread exit", mDecoder)); @@ -845,6 +845,7 @@ void nsBuiltinDecoderStateMachine::Shutdown() // 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)); + ScheduleStateMachine(); mState = DECODER_STATE_SHUTDOWN; mDecoder->GetReentrantMonitor().NotifyAll(); } @@ -858,6 +859,7 @@ void nsBuiltinDecoderStateMachine::StartDecoding() mDecodeStartTime = TimeStamp::Now(); } mState = DECODER_STATE_DECODING; + ScheduleStateMachine(); } void nsBuiltinDecoderStateMachine::Play() @@ -871,8 +873,8 @@ void nsBuiltinDecoderStateMachine::Play() LOG(PR_LOG_DEBUG, ("%p Changed state from BUFFERING to DECODING", mDecoder)); mState = DECODER_STATE_DECODING; mDecodeStartTime = TimeStamp::Now(); - mDecoder->GetReentrantMonitor().NotifyAll(); } + ScheduleStateMachine(); } void nsBuiltinDecoderStateMachine::ResetPlayback() @@ -911,6 +913,7 @@ void nsBuiltinDecoderStateMachine::Seek(double aTime) mSeekTime = NS_MAX(mStartTime, mSeekTime); LOG(PR_LOG_DEBUG, ("%p Changed state to SEEKING (to %f)", mDecoder, aTime)); mState = DECODER_STATE_SEEKING; + ScheduleStateMachine(); } void nsBuiltinDecoderStateMachine::StopDecodeThread() @@ -973,8 +976,8 @@ nsBuiltinDecoderStateMachine::StartDecodeThread() nsresult nsBuiltinDecoderStateMachine::StartAudioThread() { - NS_ASSERTION(IsCurrentThread(mDecoder->mStateMachineThread), - "Should be on state machine thread."); + NS_ASSERTION(OnStateMachineThread() || OnDecodeThread(), + "Should be on state machine or decode thread."); mDecoder->GetReentrantMonitor().AssertCurrentThreadIn(); mStopAudioThread = PR_FALSE; if (HasAudio() && !mAudioThread) { @@ -1127,6 +1130,14 @@ nsresult nsBuiltinDecoderStateMachine::DecodeMetadata() StartDecoding(); } + if ((mState == DECODER_STATE_DECODING || mState == DECODER_STATE_COMPLETED) && + mDecoder->GetState() == nsBuiltinDecoder::PLAY_STATE_PLAYING && + !IsPlaying()) + { + StartPlayback(); + StartAudioThread(); + } + return NS_OK; } @@ -1230,8 +1241,6 @@ void nsBuiltinDecoderStateMachine::DecodeSeek() stopEvent = NS_NewRunnableMethod(mDecoder, &nsBuiltinDecoder::SeekingStopped); StartDecoding(); } - mDecoder->GetReentrantMonitor().NotifyAll(); - { ReentrantMonitorAutoExit exitMon(mDecoder->GetReentrantMonitor()); NS_DispatchToMainThread(stopEvent, NS_DISPATCH_SYNC); @@ -1241,19 +1250,22 @@ void nsBuiltinDecoderStateMachine::DecodeSeek() // seek while quick-buffering, we won't bypass quick buffering mode // if we need to buffer after the seek. mQuickBuffering = PR_FALSE; + + ScheduleStateMachine(); } nsresult nsBuiltinDecoderStateMachine::Run() { + ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); NS_ASSERTION(IsCurrentThread(mDecoder->mStateMachineThread), "Should be on state machine thread."); + + mTimeout = TimeStamp(); nsMediaStream* stream = mDecoder->GetCurrentStream(); NS_ENSURE_TRUE(stream, NS_ERROR_NULL_POINTER); - while (PR_TRUE) { - ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); - switch (mState) { - case DECODER_STATE_SHUTDOWN: + switch (mState) { + case DECODER_STATE_SHUTDOWN: { if (IsPlaying()) { StopPlayback(); } @@ -1262,156 +1274,118 @@ nsresult nsBuiltinDecoderStateMachine::Run() NS_ASSERTION(mState == DECODER_STATE_SHUTDOWN, "How did we escape from the shutdown state???"); return NS_OK; + } - case DECODER_STATE_DECODING_METADATA: - { - // Start the decode threads, so that metadata decoding begins. - if (NS_FAILED(StartDecodeThread())) { - continue; - } + case DECODER_STATE_DECODING_METADATA: { + // Ensure we have a decode thread to decode metadata. + return StartDecodeThread(); + } + + case DECODER_STATE_DECODING: { + nsresult res = StartDecodeThread(); + if (NS_FAILED(res)) return res; + AdvanceFrame(); + NS_ASSERTION(mDecoder->GetState() != nsBuiltinDecoder::PLAY_STATE_PLAYING || + IsStateMachineScheduled(), "Must have timer scheduled"); + return NS_OK; + } - while (mState == DECODER_STATE_DECODING_METADATA) { - mon.Wait(); - } - - if ((mState == DECODER_STATE_DECODING || mState == DECODER_STATE_COMPLETED) && - mDecoder->GetState() == nsBuiltinDecoder::PLAY_STATE_PLAYING && - !IsPlaying()) - { - StartPlayback(); - StartAudioThread(); - } - - } - break; - - case DECODER_STATE_DECODING: - { - if (NS_FAILED(StartDecodeThread())) { - continue; - } - - AdvanceFrame(); - } - break; - - case DECODER_STATE_SEEKING: - { - // Ensure decode thread is alive and well... - if (NS_FAILED(StartDecodeThread())) { - continue; - } - - // Wait until seeking finishes... - while (mState == DECODER_STATE_SEEKING) { - mon.Wait(); - } - } - break; - - case DECODER_STATE_BUFFERING: - { - if (IsPlaying()) { - StopPlayback(); - mDecoder->GetReentrantMonitor().NotifyAll(); - } - - TimeStamp now = TimeStamp::Now(); - NS_ASSERTION(!mBufferingStart.IsNull(), "Must know buffering start time."); - - // We will remain in the buffering state if we've not decoded enough - // data to begin playback, or if we've not downloaded a reasonable - // amount of data inside our buffering time. - TimeDuration elapsed = now - mBufferingStart; - PRBool isLiveStream = mDecoder->GetCurrentStream()->GetLength() == -1; - if ((isLiveStream || !mDecoder->CanPlayThrough()) && - elapsed < TimeDuration::FromSeconds(BUFFERING_WAIT) && - (mQuickBuffering ? HasLowDecodedData(QUICK_BUFFERING_LOW_DATA_USECS) - : (GetUndecodedData() < BUFFERING_WAIT * USECS_PER_S)) && - !stream->IsDataCachedToEndOfStream(mDecoder->mDecoderPosition) && - !stream->IsSuspended()) - { - LOG(PR_LOG_DEBUG, - ("Buffering: %.3lfs/%ds, timeout in %.3lfs %s", - GetUndecodedData() / static_cast(USECS_PER_S), - BUFFERING_WAIT, - BUFFERING_WAIT - elapsed.ToSeconds(), - (mQuickBuffering ? "(quick exit)" : ""))); - Wait(USECS_PER_S); - if (mState == DECODER_STATE_SHUTDOWN) - continue; - } else { - LOG(PR_LOG_DEBUG, ("%p Changed state from BUFFERING to DECODING", mDecoder)); - LOG(PR_LOG_DEBUG, ("%p Buffered for %.3lfs", - mDecoder, - (now - mBufferingStart).ToSeconds())); - StartDecoding(); - } - - if (mState != DECODER_STATE_BUFFERING) { - // Notify to allow blocked decoder thread to continue - mDecoder->GetReentrantMonitor().NotifyAll(); - UpdateReadyState(); - if (mDecoder->GetState() == nsBuiltinDecoder::PLAY_STATE_PLAYING && - !IsPlaying()) - { - StartPlayback(); - StartAudioThread(); - } - } - break; - } - - case DECODER_STATE_COMPLETED: - { - StopDecodeThread(); - - if (NS_FAILED(StartAudioThread())) { - continue; - } - - // Play the remaining media. We want to run AdvanceFrame() at least - // once to ensure the current playback position is advanced to the - // end of the media, and so that we update the readyState. - do { - AdvanceFrame(); - } while (mState == DECODER_STATE_COMPLETED && - (mReader->mVideoQueue.GetSize() > 0 || - (HasAudio() && !mAudioCompleted))); - - // StopPlayback in order to reset the IsPlaying() state so audio - // is restarted correctly. + case DECODER_STATE_BUFFERING: { + if (IsPlaying()) { StopPlayback(); - - if (mState != DECODER_STATE_COMPLETED) - continue; - - StopAudioThread(); - - if (mDecoder->GetState() == nsBuiltinDecoder::PLAY_STATE_PLAYING) { - PRInt64 videoTime = HasVideo() ? mVideoFrameEndTime : 0; - PRInt64 clockTime = NS_MAX(mEndTime, NS_MAX(videoTime, GetAudioClock())); - UpdatePlaybackPosition(clockTime); - { - ReentrantMonitorAutoExit exitMon(mDecoder->GetReentrantMonitor()); - nsCOMPtr event = - NS_NewRunnableMethod(mDecoder, &nsBuiltinDecoder::PlaybackEnded); - NS_DispatchToMainThread(event, NS_DISPATCH_SYNC); - } - } - - if (mState == DECODER_STATE_COMPLETED) { - // We've finished playback. Shutdown the state machine thread, - // in order to save memory on thread stacks, particuarly on Linux. - LOG(PR_LOG_DEBUG, ("%p Shutting down the state machine thread", mDecoder)); - nsCOMPtr event = - new ShutdownThreadEvent(mDecoder->mStateMachineThread); - NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL); - mDecoder->mStateMachineThread = nsnull; - return NS_OK; - } + mDecoder->GetReentrantMonitor().NotifyAll(); } - break; + + TimeStamp now = TimeStamp::Now(); + NS_ASSERTION(!mBufferingStart.IsNull(), "Must know buffering start time."); + + // We will remain in the buffering state if we've not decoded enough + // data to begin playback, or if we've not downloaded a reasonable + // amount of data inside our buffering time. + TimeDuration elapsed = now - mBufferingStart; + PRBool isLiveStream = mDecoder->GetCurrentStream()->GetLength() == -1; + if ((isLiveStream || !mDecoder->CanPlayThrough()) && + elapsed < TimeDuration::FromSeconds(BUFFERING_WAIT) && + (mQuickBuffering ? HasLowDecodedData(QUICK_BUFFERING_LOW_DATA_USECS) + : (GetUndecodedData() < BUFFERING_WAIT * USECS_PER_S)) && + !stream->IsDataCachedToEndOfStream(mDecoder->mDecoderPosition) && + !stream->IsSuspended()) + { + LOG(PR_LOG_DEBUG, + ("Buffering: %.3lfs/%ds, timeout in %.3lfs %s", + GetUndecodedData() / static_cast(USECS_PER_S), + BUFFERING_WAIT, + BUFFERING_WAIT - elapsed.ToSeconds(), + (mQuickBuffering ? "(quick exit)" : ""))); + ScheduleStateMachine(USECS_PER_S); + return NS_OK; + } else { + LOG(PR_LOG_DEBUG, ("%p Changed state from BUFFERING to DECODING", mDecoder)); + LOG(PR_LOG_DEBUG, ("%p Buffered for %.3lfs", + mDecoder, + (now - mBufferingStart).ToSeconds())); + StartDecoding(); + } + + // Notify to allow blocked decoder thread to continue + mDecoder->GetReentrantMonitor().NotifyAll(); + UpdateReadyState(); + if (mDecoder->GetState() == nsBuiltinDecoder::PLAY_STATE_PLAYING && + !IsPlaying()) + { + StartPlayback(); + StartAudioThread(); + } + NS_ASSERTION(IsStateMachineScheduled(), "Must have timer scheduled"); + return NS_OK; + } + + case DECODER_STATE_SEEKING: { + // Ensure we have a decode thread to perform the seek. + return StartDecodeThread(); + } + + case DECODER_STATE_COMPLETED: { + StopDecodeThread(); + + nsresult res = StartAudioThread(); + if (NS_FAILED(res)) return res; + + // Play the remaining media. We want to run AdvanceFrame() at least + // once to ensure the current playback position is advanced to the + // end of the media, and so that we update the readyState. + if (mState == DECODER_STATE_COMPLETED && + (mReader->mVideoQueue.GetSize() > 0 || + (HasAudio() && !mAudioCompleted))) + { + AdvanceFrame(); + NS_ASSERTION(mDecoder->GetState() == nsBuiltinDecoder::PLAY_STATE_PAUSED || + IsStateMachineScheduled(), + "Must have timer scheduled"); + return NS_OK; + } + + // StopPlayback in order to reset the IsPlaying() state so audio + // is restarted correctly. + StopPlayback(); + + if (mState != DECODER_STATE_COMPLETED) { + // We've changed state. Whatever changed our state should have + // scheduled another state machine run. + NS_ASSERTION(IsStateMachineScheduled(), "Must have timer scheduled"); + return NS_OK; + } + + StopAudioThread(); + if (mDecoder->GetState() == nsBuiltinDecoder::PLAY_STATE_PLAYING) { + PRInt64 videoTime = HasVideo() ? mVideoFrameEndTime : 0; + PRInt64 clockTime = NS_MAX(mEndTime, NS_MAX(videoTime, GetAudioClock())); + UpdatePlaybackPosition(clockTime); + nsCOMPtr event = + NS_NewRunnableMethod(mDecoder, &nsBuiltinDecoder::PlaybackEnded); + NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL); + } + return NS_OK; } } @@ -1454,156 +1428,149 @@ void nsBuiltinDecoderStateMachine::AdvanceFrame() NS_ASSERTION(IsCurrentThread(mDecoder->mStateMachineThread), "Should be on state machine thread."); mDecoder->GetReentrantMonitor().AssertCurrentThreadIn(); - // When it's time to display a frame, decode the frame and display it. - if (mDecoder->GetState() == nsBuiltinDecoder::PLAY_STATE_PLAYING) { - if (HasAudio() && mAudioStartTime == -1 && !mAudioCompleted) { - // We've got audio (so we should sync off the audio clock), but we've not - // played a sample on the audio thread, so we can't get a time from the - // audio clock. Just wait and then return, to give the audio clock time - // to tick. This should really wait for a specific signal from the audio - // thread rather than polling after a sleep. See bug 568431 comment 4. - Wait(AUDIO_DURATION_USECS); - return; - } - - // Determine the clock time. If we've got audio, and we've not reached - // the end of the audio, use the audio clock. However if we've finished - // audio, or don't have audio, use the system clock. - PRInt64 clock_time = -1; - if (!IsPlaying()) { - clock_time = mPlayDuration + mStartTime; - } else { - PRInt64 audio_time = GetAudioClock(); - if (HasAudio() && !mAudioCompleted && audio_time != -1) { - clock_time = audio_time; - // Resync against the audio clock, while we're trusting the - // audio clock. This ensures no "drift", particularly on Linux. - mPlayDuration = clock_time - mStartTime; - mPlayStartTime = TimeStamp::Now(); - } else { - // Sound is disabled on this system. Sync to the system clock. - clock_time = DurationToUsecs(TimeStamp::Now() - mPlayStartTime) + mPlayDuration; - // Ensure the clock can never go backwards. - NS_ASSERTION(mCurrentFrameTime <= clock_time, "Clock should go forwards"); - clock_time = NS_MAX(mCurrentFrameTime, clock_time) + mStartTime; - } - } - - // Skip frames up to the frame at the playback position, and figure out - // the time remaining until it's time to display the next frame. - PRInt64 remainingTime = AUDIO_DURATION_USECS; - NS_ASSERTION(clock_time >= mStartTime, "Should have positive clock time."); - nsAutoPtr currentFrame; - if (mReader->mVideoQueue.GetSize() > 0) { - VideoData* frame = mReader->mVideoQueue.PeekFront(); - while (clock_time >= frame->mTime) { - mVideoFrameEndTime = frame->mEndTime; - currentFrame = frame; - mReader->mVideoQueue.PopFront(); - // Notify the decode thread that the video queue's buffers may have - // free'd up space for more frames. - mDecoder->GetReentrantMonitor().NotifyAll(); - mDecoder->UpdatePlaybackOffset(frame->mOffset); - if (mReader->mVideoQueue.GetSize() == 0) - break; - frame = mReader->mVideoQueue.PeekFront(); - } - // Current frame has already been presented, wait until it's time to - // present the next frame. - if (frame && !currentFrame) { - PRInt64 now = IsPlaying() - ? (DurationToUsecs(TimeStamp::Now() - mPlayStartTime) + mPlayDuration) - : mPlayDuration; - remainingTime = frame->mTime - mStartTime - now; - } - } - - // Check to see if we don't have enough data to play up to the next frame. - // If we don't, switch to buffering mode. - nsMediaStream* stream = mDecoder->GetCurrentStream(); - if (mState == DECODER_STATE_DECODING && - mDecoder->GetState() == nsBuiltinDecoder::PLAY_STATE_PLAYING && - HasLowDecodedData(remainingTime + EXHAUSTED_DATA_MARGIN_USECS) && - !stream->IsDataCachedToEndOfStream(mDecoder->mDecoderPosition) && - !stream->IsSuspended() && - (JustExitedQuickBuffering() || HasLowUndecodedData())) - { - if (currentFrame) { - mReader->mVideoQueue.PushFront(currentFrame.forget()); - } - StartBuffering(); - return; - } - - // We've got enough data to keep playing until at least the next frame. - // Start playing now if need be. - if (!IsPlaying()) { - StartPlayback(); - StartAudioThread(); - mDecoder->GetReentrantMonitor().NotifyAll(); - } - - if (currentFrame) { - // Decode one frame and display it. - TimeStamp presTime = mPlayStartTime - UsecsToDuration(mPlayDuration) + - UsecsToDuration(currentFrame->mTime - mStartTime); - NS_ASSERTION(currentFrame->mTime >= mStartTime, "Should have positive frame time"); - { - ReentrantMonitorAutoExit exitMon(mDecoder->GetReentrantMonitor()); - // If we have video, we want to increment the clock in steps of the frame - // duration. - RenderVideoFrame(currentFrame, presTime); - } - mDecoder->GetFrameStatistics().NotifyPresentedFrame(); - PRInt64 now = DurationToUsecs(TimeStamp::Now() - mPlayStartTime) + mPlayDuration; - remainingTime = currentFrame->mEndTime - mStartTime - now; - currentFrame = nsnull; - } - - // Cap the current time to the larger of the audio and video end time. - // This ensures that if we're running off the system clock, we don't - // advance the clock to after the media end time. - if (mVideoFrameEndTime != -1 || mAudioEndTime != -1) { - // These will be non -1 if we've displayed a video frame, or played an audio sample. - clock_time = NS_MIN(clock_time, NS_MAX(mVideoFrameEndTime, mAudioEndTime)); - if (clock_time > GetMediaTime()) { - // Only update the playback position if the clock time is greater - // than the previous playback position. The audio clock can - // sometimes report a time less than its previously reported in - // some situations, and we need to gracefully handle that. - UpdatePlaybackPosition(clock_time); - } - } - - // If the number of audio/video samples queued has changed, either by - // this function popping and playing a video sample, or by the audio - // thread popping and playing an audio sample, we may need to update our - // ready state. Post an update to do so. - UpdateReadyState(); - - if (remainingTime > 0) { - Wait(remainingTime); - } - } else if (mState == DECODER_STATE_DECODING || - mState == DECODER_STATE_COMPLETED) - { - StopPlayback(); - mDecoder->GetReentrantMonitor().Wait(); + if (mDecoder->GetState() != nsBuiltinDecoder::PLAY_STATE_PLAYING) { + return; } + + if (HasAudio() && mAudioStartTime == -1 && !mAudioCompleted) { + // We've got audio (so we should sync off the audio clock), but we've not + // played a sample on the audio thread, so we can't get a time from the + // audio clock. Just wait and then return, to give the audio clock time + // to tick. This should really wait for a specific signal from the audio + // thread rather than polling after a sleep. See bug 568431 comment 4. + ScheduleStateMachine(AUDIO_DURATION_USECS); + return; + } + + // Determine the clock time. If we've got audio, and we've not reached + // the end of the audio, use the audio clock. However if we've finished + // audio, or don't have audio, use the system clock. + PRInt64 clock_time = -1; + if (!IsPlaying()) { + clock_time = mPlayDuration + mStartTime; + } else { + PRInt64 audio_time = GetAudioClock(); + if (HasAudio() && !mAudioCompleted && audio_time != -1) { + clock_time = audio_time; + // Resync against the audio clock, while we're trusting the + // audio clock. This ensures no "drift", particularly on Linux. + mPlayDuration = clock_time - mStartTime; + mPlayStartTime = TimeStamp::Now(); + } else { + // Sound is disabled on this system. Sync to the system clock. + clock_time = DurationToUsecs(TimeStamp::Now() - mPlayStartTime) + mPlayDuration; + // Ensure the clock can never go backwards. + NS_ASSERTION(mCurrentFrameTime <= clock_time, "Clock should go forwards"); + clock_time = NS_MAX(mCurrentFrameTime, clock_time) + mStartTime; + } + } + + // Skip frames up to the frame at the playback position, and figure out + // the time remaining until it's time to display the next frame. + PRInt64 remainingTime = AUDIO_DURATION_USECS; + NS_ASSERTION(clock_time >= mStartTime, "Should have positive clock time."); + nsAutoPtr currentFrame; + if (mReader->mVideoQueue.GetSize() > 0) { + VideoData* frame = mReader->mVideoQueue.PeekFront(); + while (clock_time >= frame->mTime) { + mVideoFrameEndTime = frame->mEndTime; + currentFrame = frame; + mReader->mVideoQueue.PopFront(); + // Notify the decode thread that the video queue's buffers may have + // free'd up space for more frames. + mDecoder->GetReentrantMonitor().NotifyAll(); + mDecoder->UpdatePlaybackOffset(frame->mOffset); + if (mReader->mVideoQueue.GetSize() == 0) + break; + frame = mReader->mVideoQueue.PeekFront(); + } + // Current frame has already been presented, wait until it's time to + // present the next frame. + if (frame && !currentFrame) { + PRInt64 now = IsPlaying() + ? (DurationToUsecs(TimeStamp::Now() - mPlayStartTime) + mPlayDuration) + : mPlayDuration; + remainingTime = frame->mTime - mStartTime - now; + } + } + + // Check to see if we don't have enough data to play up to the next frame. + // If we don't, switch to buffering mode. + nsMediaStream* stream = mDecoder->GetCurrentStream(); + if (mState == DECODER_STATE_DECODING && + mDecoder->GetState() == nsBuiltinDecoder::PLAY_STATE_PLAYING && + HasLowDecodedData(remainingTime + EXHAUSTED_DATA_MARGIN_USECS) && + !stream->IsDataCachedToEndOfStream(mDecoder->mDecoderPosition) && + !stream->IsSuspended() && + (JustExitedQuickBuffering() || HasLowUndecodedData())) + { + if (currentFrame) { + mReader->mVideoQueue.PushFront(currentFrame.forget()); + } + StartBuffering(); + ScheduleStateMachine(); + return; + } + + // We've got enough data to keep playing until at least the next frame. + // Start playing now if need be. + if (!IsPlaying()) { + StartPlayback(); + StartAudioThread(); + } + + if (currentFrame) { + // Decode one frame and display it. + TimeStamp presTime = mPlayStartTime - UsecsToDuration(mPlayDuration) + + UsecsToDuration(currentFrame->mTime - mStartTime); + NS_ASSERTION(currentFrame->mTime >= mStartTime, "Should have positive frame time"); + { + ReentrantMonitorAutoExit exitMon(mDecoder->GetReentrantMonitor()); + // If we have video, we want to increment the clock in steps of the frame + // duration. + RenderVideoFrame(currentFrame, presTime); + } + mDecoder->GetFrameStatistics().NotifyPresentedFrame(); + PRInt64 now = DurationToUsecs(TimeStamp::Now() - mPlayStartTime) + mPlayDuration; + remainingTime = currentFrame->mEndTime - mStartTime - now; + currentFrame = nsnull; + } + + // Cap the current time to the larger of the audio and video end time. + // This ensures that if we're running off the system clock, we don't + // advance the clock to after the media end time. + if (mVideoFrameEndTime != -1 || mAudioEndTime != -1) { + // These will be non -1 if we've displayed a video frame, or played an audio sample. + clock_time = NS_MIN(clock_time, NS_MAX(mVideoFrameEndTime, mAudioEndTime)); + if (clock_time > GetMediaTime()) { + // Only update the playback position if the clock time is greater + // than the previous playback position. The audio clock can + // sometimes report a time less than its previously reported in + // some situations, and we need to gracefully handle that. + UpdatePlaybackPosition(clock_time); + } + } + + // If the number of audio/video samples queued has changed, either by + // this function popping and playing a video sample, or by the audio + // thread popping and playing an audio sample, we may need to update our + // ready state. Post an update to do so. + UpdateReadyState(); + + ScheduleStateMachine(remainingTime); } void nsBuiltinDecoderStateMachine::Wait(PRInt64 aUsecs) { + NS_ASSERTION(OnAudioThread(), "Only call on the audio thread"); mDecoder->GetReentrantMonitor().AssertCurrentThreadIn(); TimeStamp end = TimeStamp::Now() + UsecsToDuration(NS_MAX(USECS_PER_MS, aUsecs)); TimeStamp now; while ((now = TimeStamp::Now()) < end && mState != DECODER_STATE_SHUTDOWN && mState != DECODER_STATE_SEEKING && - (!OnAudioThread() || !mStopAudioThread)) + !mStopAudioThread && + IsPlaying()) { - if (OnAudioThread() && !IsPlaying()) { - break; - } PRInt64 ms = static_cast(NS_round((end - now).ToSeconds() * 1000)); if (ms == 0 || ms > PR_UINT32_MAX) { break; @@ -1711,3 +1678,62 @@ nsresult nsBuiltinDecoderStateMachine::GetBuffered(nsTimeRanges* aBuffered) { stream->Unpin(); return res; } + +static void RunStateMachine(nsITimer *aTimer, void *aClosure) { + nsBuiltinDecoderStateMachine *machine = + static_cast(aClosure); + NS_ASSERTION(machine, "Must have been passed state machine"); + machine->Run(); +} + +nsresult nsBuiltinDecoderStateMachine::ScheduleStateMachine() { + return ScheduleStateMachine(0); +} + +nsresult nsBuiltinDecoderStateMachine::ScheduleStateMachine(PRInt64 aUsecs) { + mDecoder->GetReentrantMonitor().AssertCurrentThreadIn(); + + if (mState == DECODER_STATE_SHUTDOWN) { + return NS_ERROR_FAILURE; + } + aUsecs = PR_MAX(aUsecs, 0); + + TimeStamp timeout = TimeStamp::Now() + UsecsToDuration(aUsecs); + if (!mTimeout.IsNull()) { + if (timeout >= mTimeout) { + // We've already scheduled a timer set to expire at or before this time, + // or have an event dispatched to run the state machine. + return NS_OK; + } else if (timeout < mTimeout && mTimer) { + // We've been asked to schedule a timer to run before an existing timer. + // Cancel the existing timer. + mTimer->Cancel(); + } + } + + // Ensure the state machine thread is alive; we'll be running on it! + nsresult res = mDecoder->CreateStateMachineThread(); + if (NS_FAILED(res)) return res; + + mTimeout = timeout; + + PRUint32 ms = + static_cast((aUsecs / USECS_PER_MS) & 0xFFFFFFFF); + if (ms == 0) { + // We've been asked to schedule a timer to run ASAP, so just dispatch an + // event rather than using a timer. + return mDecoder->mStateMachineThread->Dispatch(this, NS_DISPATCH_NORMAL); + } + + if (!mTimer) { + mTimer = do_CreateInstance("@mozilla.org/timer;1", &res); + if (NS_FAILED(res)) return res; + mTimer->SetTarget(mDecoder->mStateMachineThread); + } + + res = mTimer->InitWithFuncCallback(RunStateMachine, + this, + ms, + nsITimer::TYPE_ONE_SHOT); + return res; +} diff --git a/content/media/nsBuiltinDecoderStateMachine.h b/content/media/nsBuiltinDecoderStateMachine.h index 8d893b4d13a..c11074c823d 100644 --- a/content/media/nsBuiltinDecoderStateMachine.h +++ b/content/media/nsBuiltinDecoderStateMachine.h @@ -119,6 +119,7 @@ not yet time to display the next frame. #include "nsAudioAvailableEventManager.h" #include "nsHTMLMediaElement.h" #include "mozilla/ReentrantMonitor.h" +#include "nsITimer.h" /* The playback state machine class. This manages the decoding in the @@ -249,6 +250,14 @@ public: // Accessed on the main and state machine threads. virtual void SetFrameBufferLength(PRUint32 aLength); + // Schedules the state machine thread to run the state machine. + nsresult ScheduleStateMachine(); + + // Schedules the state machine thread to run the state machine + // in aUsecs microseconds from now, if it's not already scheduled to run + // earlier, in which case the request is discarded. + nsresult ScheduleStateMachine(PRInt64 aUsecs); + protected: // Returns PR_TRUE if we've got less than aAudioUsecs microseconds of decoded @@ -409,6 +418,10 @@ protected: // to call. void DecodeThreadRun(); + PRBool IsStateMachineScheduled() const { + return !mTimeout.IsNull(); + } + // The size of the decoded YCbCr frame. // Accessed on state machine thread. PRUint32 mCbCrSize; @@ -423,6 +436,15 @@ protected: // Thread for decoding video in background. The "decode thread". nsCOMPtr mDecodeThread; + // Timer to call the state machine Run() method. Used by + // ScheduleStateMachine(). Access protected by decoder monitor. + nsCOMPtr mTimer; + + // Timestamp at which the next state machine Run() method will be called. + // If this is non-null, a call to Run() is scheduled, either by a timer, + // or via an event. Access protected by decoder monitor. + TimeStamp mTimeout; + // The time that playback started from the system clock. This is used for // timing the presentation of video frames when there's no audio. // Accessed only via the state machine thread. diff --git a/content/media/test/seek1.js b/content/media/test/seek1.js index 2fbd25c61db..8340a1bb4f5 100644 --- a/content/media/test/seek1.js +++ b/content/media/test/seek1.js @@ -29,7 +29,8 @@ function startTest() { function seekStarted() { if (completed) return false; - ok(v.currentTime >= seekTime - 0.1, "Video currentTime should be around " + seekTime + ": " + v.currentTime); + ok(v.currentTime >= seekTime - 0.1, + "Video currentTime should be around " + seekTime + ": " + v.currentTime + " (seeking)"); v.pause(); startPassed = true; return false; @@ -43,7 +44,7 @@ function seekEnded() { // Since we were playing, and we only paused asynchronously, we can't be // sure that we paused before the seek finished, so we may have played // ahead arbitrarily far. - ok(t >= seekTime - 0.1, "Video currentTime should be around " + seekTime + ": " + t); + ok(t >= seekTime - 0.1, "Video currentTime should be around " + seekTime + ": " + t + " (seeked)"); v.play(); endPassed = true; seekFlagEnd = v.seeking;