diff -r 000000000000 -r 6474c204b198 content/media/MediaDecoderStateMachine.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/content/media/MediaDecoderStateMachine.cpp Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,2857 @@ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifdef XP_WIN +// Include Windows headers required for enabling high precision timers. +#include "windows.h" +#include "mmsystem.h" +#endif + +#include "mozilla/DebugOnly.h" +#include + +#include "MediaDecoderStateMachine.h" +#include "AudioStream.h" +#include "nsTArray.h" +#include "MediaDecoder.h" +#include "MediaDecoderReader.h" +#include "mozilla/mozalloc.h" +#include "VideoUtils.h" +#include "mozilla/dom/TimeRanges.h" +#include "nsDeque.h" +#include "AudioSegment.h" +#include "VideoSegment.h" +#include "ImageContainer.h" +#include "nsComponentManagerUtils.h" +#include "nsITimer.h" +#include "nsContentUtils.h" +#include "MediaShutdownManager.h" +#include "SharedThreadPool.h" +#include "MediaTaskQueue.h" +#include "nsIEventTarget.h" +#include "prenv.h" +#include "mozilla/Preferences.h" +#include "gfx2DGlue.h" + +#include + +namespace mozilla { + +using namespace mozilla::layers; +using namespace mozilla::dom; +using namespace mozilla::gfx; + +// avoid redefined macro in unified build +#undef DECODER_LOG +#undef VERBOSE_LOG + +#ifdef PR_LOGGING +extern PRLogModuleInfo* gMediaDecoderLog; +#define DECODER_LOG(type, msg, ...) \ + PR_LOG(gMediaDecoderLog, type, ("Decoder=%p " msg, mDecoder.get(), ##__VA_ARGS__)) +#define VERBOSE_LOG(msg, ...) \ + PR_BEGIN_MACRO \ + if (!PR_GetEnv("MOZ_QUIET")) { \ + DECODER_LOG(PR_LOG_DEBUG, msg, ##__VA_ARGS__); \ + } \ + PR_END_MACRO +#else +#define DECODER_LOG(type, msg, ...) +#define VERBOSE_LOG(msg, ...) +#endif + +// GetCurrentTime is defined in winbase.h as zero argument macro forwarding to +// GetTickCount() and conflicts with MediaDecoderStateMachine::GetCurrentTime +// implementation. With unified builds, putting this in headers is not enough. +#ifdef GetCurrentTime +#undef GetCurrentTime +#endif + +// 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. +static const uint32_t BUFFERING_WAIT_S = 30; + +// If audio queue has less than this many usecs of decoded audio, we won't risk +// trying to decode the video, we'll skip decoding video up to the next +// keyframe. We may increase this value for an individual decoder if we +// encounter video frames which take a long time to decode. +static const uint32_t LOW_AUDIO_USECS = 300000; + +// If more than this many usecs of decoded audio is queued, we'll hold off +// decoding more audio. If we increase the low audio threshold (see +// LOW_AUDIO_USECS above) we'll also increase this value to ensure it's not +// less than the low audio threshold. +const int64_t AMPLE_AUDIO_USECS = 1000000; + +// When we're only playing audio and we don't have a video stream, we divide +// AMPLE_AUDIO_USECS and LOW_AUDIO_USECS by the following value. This reduces +// the amount of decoded audio we buffer, reducing our memory usage. We only +// need to decode far ahead when we're decoding video using software decoding, +// as otherwise a long video decode could cause an audio underrun. +const int64_t NO_VIDEO_AMPLE_AUDIO_DIVISOR = 8; + +// Maximum number of bytes we'll allocate and write at once to the audio +// hardware when the audio stream contains missing frames and we're +// writing silence in order to fill the gap. We limit our silence-writes +// to 32KB in order to avoid allocating an impossibly large chunk of +// memory if we encounter a large chunk of silence. +const uint32_t SILENCE_BYTES_CHUNK = 32 * 1024; + +// If we have fewer than LOW_VIDEO_FRAMES decoded frames, and +// we're not "prerolling video", we'll skip the video up to the next keyframe +// which is at or after the current playback position. +static const uint32_t LOW_VIDEO_FRAMES = 1; + +// Arbitrary "frame duration" when playing only audio. +static const int AUDIO_DURATION_USECS = 40000; + +// If we increase our "low audio threshold" (see LOW_AUDIO_USECS above), we +// use this as a factor in all our calculations. Increasing this will cause +// us to be more likely to increase our low audio threshold, and to +// increase it by more. +static const int THRESHOLD_FACTOR = 2; + +// If we have less than this much undecoded data available, we'll consider +// ourselves to be running low on undecoded data. We determine how much +// undecoded data we have remaining using the reader's GetBuffered() +// implementation. +static const int64_t LOW_DATA_THRESHOLD_USECS = 5000000; + +// LOW_DATA_THRESHOLD_USECS needs to be greater than AMPLE_AUDIO_USECS, otherwise +// the skip-to-keyframe logic can activate when we're running low on data. +static_assert(LOW_DATA_THRESHOLD_USECS > AMPLE_AUDIO_USECS, + "LOW_DATA_THRESHOLD_USECS is too small"); + +// Amount of excess usecs of data to add in to the "should we buffer" calculation. +static const uint32_t EXHAUSTED_DATA_MARGIN_USECS = 60000; + +// If we enter buffering within QUICK_BUFFER_THRESHOLD_USECS seconds of starting +// decoding, we'll enter "quick buffering" mode, which exits a lot sooner than +// normal buffering mode. This exists so that if the decode-ahead exhausts the +// downloaded data while decode/playback is just starting up (for example +// after a seek while the media is still playing, or when playing a media +// as soon as it's load started), we won't necessarily stop for 30s and wait +// for buffering. We may actually be able to playback in this case, so exit +// buffering early and try to play. If it turns out we can't play, we'll fall +// back to buffering normally. +static const uint32_t QUICK_BUFFER_THRESHOLD_USECS = 2000000; + +// If we're quick buffering, we'll remain in buffering mode while we have less than +// QUICK_BUFFERING_LOW_DATA_USECS of decoded data available. +static const uint32_t QUICK_BUFFERING_LOW_DATA_USECS = 1000000; + +// If QUICK_BUFFERING_LOW_DATA_USECS is > AMPLE_AUDIO_USECS, we won't exit +// quick buffering in a timely fashion, as the decode pauses when it +// reaches AMPLE_AUDIO_USECS decoded data, and thus we'll never reach +// QUICK_BUFFERING_LOW_DATA_USECS. +static_assert(QUICK_BUFFERING_LOW_DATA_USECS <= AMPLE_AUDIO_USECS, + "QUICK_BUFFERING_LOW_DATA_USECS is too large"); + +// This value has been chosen empirically. +static const uint32_t AUDIOSTREAM_MIN_WRITE_BEFORE_START_USECS = 200000; + +// The amount of instability we tollerate in calls to +// MediaDecoderStateMachine::UpdateEstimatedDuration(); changes of duration +// less than this are ignored, as they're assumed to be the result of +// instability in the duration estimation. +static const int64_t ESTIMATED_DURATION_FUZZ_FACTOR_USECS = USECS_PER_S / 2; + +static TimeDuration UsecsToDuration(int64_t aUsecs) { + return TimeDuration::FromMilliseconds(static_cast(aUsecs) / USECS_PER_MS); +} + +static int64_t DurationToUsecs(TimeDuration aDuration) { + return static_cast(aDuration.ToSeconds() * USECS_PER_S); +} + +MediaDecoderStateMachine::MediaDecoderStateMachine(MediaDecoder* aDecoder, + MediaDecoderReader* aReader, + bool aRealTime) : + mDecoder(aDecoder), + mState(DECODER_STATE_DECODING_METADATA), + mInRunningStateMachine(false), + mSyncPointInMediaStream(-1), + mSyncPointInDecodedStream(-1), + mResetPlayStartTime(false), + mPlayDuration(0), + mStartTime(-1), + mEndTime(-1), + mFragmentEndTime(-1), + mReader(aReader), + mCurrentFrameTime(0), + mAudioStartTime(-1), + mAudioEndTime(-1), + mVideoFrameEndTime(-1), + mVolume(1.0), + mPlaybackRate(1.0), + mPreservesPitch(true), + mBasePosition(0), + mAmpleVideoFrames(2), + mLowAudioThresholdUsecs(LOW_AUDIO_USECS), + mAmpleAudioThresholdUsecs(AMPLE_AUDIO_USECS), + mDispatchedAudioDecodeTask(false), + mDispatchedVideoDecodeTask(false), + mIsReaderIdle(false), + mAudioCaptured(false), + mTransportSeekable(true), + mMediaSeekable(true), + mPositionChangeQueued(false), + mAudioCompleted(false), + mGotDurationFromMetaData(false), + mDispatchedEventToDecode(false), + mStopAudioThread(true), + mQuickBuffering(false), + mMinimizePreroll(false), + mDecodeThreadWaiting(false), + mRealTime(aRealTime), + mLastFrameStatus(MediaDecoderOwner::NEXT_FRAME_UNINITIALIZED), + mTimerId(0) +{ + MOZ_COUNT_CTOR(MediaDecoderStateMachine); + NS_ASSERTION(NS_IsMainThread(), "Should be on main thread."); + + // Only enable realtime mode when "media.realtime_decoder.enabled" is true. + if (Preferences::GetBool("media.realtime_decoder.enabled", false) == false) + mRealTime = false; + + mAmpleVideoFrames = + std::max(Preferences::GetUint("media.video-queue.default-size", 10), 3); + + mBufferingWait = mRealTime ? 0 : BUFFERING_WAIT_S; + mLowDataThresholdUsecs = mRealTime ? 0 : LOW_DATA_THRESHOLD_USECS; + + mVideoPrerollFrames = mRealTime ? 0 : mAmpleVideoFrames / 2; + mAudioPrerollUsecs = mRealTime ? 0 : LOW_AUDIO_USECS * 2; + +#ifdef XP_WIN + // Ensure high precision timers are enabled on Windows, otherwise the state + // machine thread isn't woken up at reliable intervals to set the next frame, + // and we drop frames while painting. Note that multiple calls to this + // function per-process is OK, provided each call is matched by a corresponding + // timeEndPeriod() call. + timeBeginPeriod(1); +#endif +} + +MediaDecoderStateMachine::~MediaDecoderStateMachine() +{ + MOZ_ASSERT(NS_IsMainThread(), "Should be on main thread."); + MOZ_COUNT_DTOR(MediaDecoderStateMachine); + NS_ASSERTION(!mPendingWakeDecoder.get(), + "WakeDecoder should have been revoked already"); + + MOZ_ASSERT(!mDecodeTaskQueue, "Should be released in SHUTDOWN"); + // No need to cancel the timer here for we've done that in SHUTDOWN. + MOZ_ASSERT(!mTimer, "Should be released in SHUTDOWN"); + mReader = nullptr; + +#ifdef XP_WIN + timeEndPeriod(1); +#endif +} + +bool MediaDecoderStateMachine::HasFutureAudio() { + AssertCurrentThreadInMonitor(); + NS_ASSERTION(HasAudio(), "Should only call HasFutureAudio() when we have audio"); + // We've got audio ready to play if: + // 1. We've not completed playback of audio, and + // 2. we either have more than the threshold of decoded audio available, or + // we've completely decoded all audio (but not finished playing it yet + // as per 1). + return !mAudioCompleted && + (AudioDecodedUsecs() > LOW_AUDIO_USECS * mPlaybackRate || AudioQueue().IsFinished()); +} + +bool MediaDecoderStateMachine::HaveNextFrameData() { + AssertCurrentThreadInMonitor(); + return (!HasAudio() || HasFutureAudio()) && + (!HasVideo() || VideoQueue().GetSize() > 0); +} + +int64_t MediaDecoderStateMachine::GetDecodedAudioDuration() { + NS_ASSERTION(OnDecodeThread() || OnStateMachineThread(), + "Should be on decode thread or state machine thread"); + AssertCurrentThreadInMonitor(); + int64_t audioDecoded = AudioQueue().Duration(); + if (mAudioEndTime != -1) { + audioDecoded += mAudioEndTime - GetMediaTime(); + } + return audioDecoded; +} + +void MediaDecoderStateMachine::SendStreamAudio(AudioData* aAudio, + DecodedStreamData* aStream, + AudioSegment* aOutput) +{ + NS_ASSERTION(OnDecodeThread() || + OnStateMachineThread(), "Should be on decode thread or state machine thread"); + AssertCurrentThreadInMonitor(); + + if (aAudio->mTime <= aStream->mLastAudioPacketTime) { + // ignore packet that we've already processed + return; + } + aStream->mLastAudioPacketTime = aAudio->mTime; + aStream->mLastAudioPacketEndTime = aAudio->GetEndTime(); + + // This logic has to mimic AudioLoop closely to make sure we write + // the exact same silences + CheckedInt64 audioWrittenOffset = UsecsToFrames(mInfo.mAudio.mRate, + aStream->mInitialTime + mStartTime) + aStream->mAudioFramesWritten; + CheckedInt64 frameOffset = UsecsToFrames(mInfo.mAudio.mRate, aAudio->mTime); + if (!audioWrittenOffset.isValid() || !frameOffset.isValid()) + return; + if (audioWrittenOffset.value() < frameOffset.value()) { + // Write silence to catch up + VERBOSE_LOG("writing %d frames of silence to MediaStream", + int32_t(frameOffset.value() - audioWrittenOffset.value())); + AudioSegment silence; + silence.InsertNullDataAtStart(frameOffset.value() - audioWrittenOffset.value()); + aStream->mAudioFramesWritten += silence.GetDuration(); + aOutput->AppendFrom(&silence); + } + + int64_t offset; + if (aStream->mAudioFramesWritten == 0) { + NS_ASSERTION(frameOffset.value() <= audioWrittenOffset.value(), + "Otherwise we'd have taken the write-silence path"); + // We're starting in the middle of a packet. Split the packet. + offset = audioWrittenOffset.value() - frameOffset.value(); + } else { + // Write the entire packet. + offset = 0; + } + + if (offset >= aAudio->mFrames) + return; + + aAudio->EnsureAudioBuffer(); + nsRefPtr buffer = aAudio->mAudioBuffer; + AudioDataValue* bufferData = static_cast(buffer->Data()); + nsAutoTArray channels; + for (uint32_t i = 0; i < aAudio->mChannels; ++i) { + channels.AppendElement(bufferData + i*aAudio->mFrames + offset); + } + aOutput->AppendFrames(buffer.forget(), channels, aAudio->mFrames); + VERBOSE_LOG("writing %d frames of data to MediaStream for AudioData at %lld", + aAudio->mFrames - int32_t(offset), aAudio->mTime); + aStream->mAudioFramesWritten += aAudio->mFrames - int32_t(offset); +} + +static void WriteVideoToMediaStream(layers::Image* aImage, + int64_t aDuration, + const IntSize& aIntrinsicSize, + VideoSegment* aOutput) +{ + nsRefPtr image = aImage; + aOutput->AppendFrame(image.forget(), aDuration, aIntrinsicSize); +} + +static const TrackID TRACK_AUDIO = 1; +static const TrackID TRACK_VIDEO = 2; +static const TrackRate RATE_VIDEO = USECS_PER_S; + +void MediaDecoderStateMachine::SendStreamData() +{ + NS_ASSERTION(OnDecodeThread() || + OnStateMachineThread(), "Should be on decode thread or state machine thread"); + AssertCurrentThreadInMonitor(); + + DecodedStreamData* stream = mDecoder->GetDecodedStream(); + if (!stream) + return; + + if (mState == DECODER_STATE_DECODING_METADATA) + return; + + // If there's still an audio thread alive, then we can't send any stream + // data yet since both SendStreamData and the audio thread want to be in + // charge of popping the audio queue. We're waiting for the audio thread + // to die before sending anything to our stream. + if (mAudioThread) + return; + + int64_t minLastAudioPacketTime = INT64_MAX; + bool finished = + (!mInfo.HasAudio() || AudioQueue().IsFinished()) && + (!mInfo.HasVideo() || VideoQueue().IsFinished()); + if (mDecoder->IsSameOriginMedia()) { + SourceMediaStream* mediaStream = stream->mStream; + StreamTime endPosition = 0; + + if (!stream->mStreamInitialized) { + if (mInfo.HasAudio()) { + AudioSegment* audio = new AudioSegment(); + mediaStream->AddTrack(TRACK_AUDIO, mInfo.mAudio.mRate, 0, audio); + stream->mStream->DispatchWhenNotEnoughBuffered(TRACK_AUDIO, + GetStateMachineThread(), GetWakeDecoderRunnable()); + } + if (mInfo.HasVideo()) { + VideoSegment* video = new VideoSegment(); + mediaStream->AddTrack(TRACK_VIDEO, RATE_VIDEO, 0, video); + stream->mStream->DispatchWhenNotEnoughBuffered(TRACK_VIDEO, + GetStateMachineThread(), GetWakeDecoderRunnable()); + } + stream->mStreamInitialized = true; + } + + if (mInfo.HasAudio()) { + nsAutoTArray audio; + // It's OK to hold references to the AudioData because while audio + // is captured, only the decoder thread pops from the queue (see below). + AudioQueue().GetElementsAfter(stream->mLastAudioPacketTime, &audio); + AudioSegment output; + for (uint32_t i = 0; i < audio.Length(); ++i) { + SendStreamAudio(audio[i], stream, &output); + } + if (output.GetDuration() > 0) { + mediaStream->AppendToTrack(TRACK_AUDIO, &output); + } + if (AudioQueue().IsFinished() && !stream->mHaveSentFinishAudio) { + mediaStream->EndTrack(TRACK_AUDIO); + stream->mHaveSentFinishAudio = true; + } + minLastAudioPacketTime = std::min(minLastAudioPacketTime, stream->mLastAudioPacketTime); + endPosition = std::max(endPosition, + TicksToTimeRoundDown(mInfo.mAudio.mRate, stream->mAudioFramesWritten)); + } + + if (mInfo.HasVideo()) { + nsAutoTArray video; + // It's OK to hold references to the VideoData only the decoder thread + // pops from the queue. + VideoQueue().GetElementsAfter(stream->mNextVideoTime, &video); + VideoSegment output; + for (uint32_t i = 0; i < video.Length(); ++i) { + VideoData* v = video[i]; + if (stream->mNextVideoTime < v->mTime) { + VERBOSE_LOG("writing last video to MediaStream %p for %lldus", + mediaStream, v->mTime - stream->mNextVideoTime); + // Write last video frame to catch up. mLastVideoImage can be null here + // which is fine, it just means there's no video. + WriteVideoToMediaStream(stream->mLastVideoImage, + v->mTime - stream->mNextVideoTime, stream->mLastVideoImageDisplaySize, + &output); + stream->mNextVideoTime = v->mTime; + } + if (stream->mNextVideoTime < v->GetEndTime()) { + VERBOSE_LOG("writing video frame %lldus to MediaStream %p for %lldus", + v->mTime, mediaStream, v->GetEndTime() - stream->mNextVideoTime); + WriteVideoToMediaStream(v->mImage, + v->GetEndTime() - stream->mNextVideoTime, v->mDisplay, + &output); + stream->mNextVideoTime = v->GetEndTime(); + stream->mLastVideoImage = v->mImage; + stream->mLastVideoImageDisplaySize = v->mDisplay; + } else { + VERBOSE_LOG("skipping writing video frame %lldus (end %lldus) to MediaStream", + v->mTime, v->GetEndTime()); + } + } + if (output.GetDuration() > 0) { + mediaStream->AppendToTrack(TRACK_VIDEO, &output); + } + if (VideoQueue().IsFinished() && !stream->mHaveSentFinishVideo) { + mediaStream->EndTrack(TRACK_VIDEO); + stream->mHaveSentFinishVideo = true; + } + endPosition = std::max(endPosition, + TicksToTimeRoundDown(RATE_VIDEO, stream->mNextVideoTime - stream->mInitialTime)); + } + + if (!stream->mHaveSentFinish) { + stream->mStream->AdvanceKnownTracksTime(endPosition); + } + + if (finished && !stream->mHaveSentFinish) { + stream->mHaveSentFinish = true; + stream->mStream->Finish(); + } + } + + if (mAudioCaptured) { + // Discard audio packets that are no longer needed. + while (true) { + const AudioData* a = AudioQueue().PeekFront(); + // Packet times are not 100% reliable so this may discard packets that + // actually contain data for mCurrentFrameTime. This means if someone might + // create a new output stream and we actually don't have the audio for the + // very start. That's OK, we'll play silence instead for a brief moment. + // That's OK. Seeking to this time would have a similar issue for such + // badly muxed resources. + if (!a || a->GetEndTime() >= minLastAudioPacketTime) + break; + mAudioEndTime = std::max(mAudioEndTime, a->GetEndTime()); + delete AudioQueue().PopFront(); + } + + if (finished) { + mAudioCompleted = true; + UpdateReadyState(); + } + } +} + +MediaDecoderStateMachine::WakeDecoderRunnable* +MediaDecoderStateMachine::GetWakeDecoderRunnable() +{ + AssertCurrentThreadInMonitor(); + + if (!mPendingWakeDecoder.get()) { + mPendingWakeDecoder = new WakeDecoderRunnable(this); + } + return mPendingWakeDecoder.get(); +} + +bool MediaDecoderStateMachine::HaveEnoughDecodedAudio(int64_t aAmpleAudioUSecs) +{ + AssertCurrentThreadInMonitor(); + + if (AudioQueue().GetSize() == 0 || + GetDecodedAudioDuration() < aAmpleAudioUSecs) { + return false; + } + if (!mAudioCaptured) { + return true; + } + + DecodedStreamData* stream = mDecoder->GetDecodedStream(); + if (stream && stream->mStreamInitialized && !stream->mHaveSentFinishAudio) { + if (!stream->mStream->HaveEnoughBuffered(TRACK_AUDIO)) { + return false; + } + stream->mStream->DispatchWhenNotEnoughBuffered(TRACK_AUDIO, + GetStateMachineThread(), GetWakeDecoderRunnable()); + } + + return true; +} + +bool MediaDecoderStateMachine::HaveEnoughDecodedVideo() +{ + AssertCurrentThreadInMonitor(); + + if (static_cast(VideoQueue().GetSize()) < mAmpleVideoFrames * mPlaybackRate) { + return false; + } + + DecodedStreamData* stream = mDecoder->GetDecodedStream(); + if (stream && stream->mStreamInitialized && !stream->mHaveSentFinishVideo) { + if (!stream->mStream->HaveEnoughBuffered(TRACK_VIDEO)) { + return false; + } + stream->mStream->DispatchWhenNotEnoughBuffered(TRACK_VIDEO, + GetStateMachineThread(), GetWakeDecoderRunnable()); + } + + return true; +} + +bool +MediaDecoderStateMachine::NeedToDecodeVideo() +{ + AssertCurrentThreadInMonitor(); + NS_ASSERTION(OnStateMachineThread() || OnDecodeThread(), + "Should be on state machine or decode thread."); + return mIsVideoDecoding && + !mMinimizePreroll && + !HaveEnoughDecodedVideo(); +} + +void +MediaDecoderStateMachine::DecodeVideo() +{ + ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); + NS_ASSERTION(OnDecodeThread(), "Should be on decode thread."); + + if (mState != DECODER_STATE_DECODING && mState != DECODER_STATE_BUFFERING) { + mDispatchedVideoDecodeTask = false; + return; + } + EnsureActive(); + + // We don't want to consider skipping to the next keyframe if we've + // only just started up the decode loop, so wait until we've decoded + // some frames before enabling the keyframe skip logic on video. + if (mIsVideoPrerolling && + (static_cast(VideoQueue().GetSize()) + >= mVideoPrerollFrames * mPlaybackRate)) + { + mIsVideoPrerolling = false; + } + + // We'll skip the video decode to the nearest keyframe if we're low on + // audio, or if we're low on video, provided we're not running low on + // data to decode. If we're running low on downloaded data to decode, + // we won't start keyframe skipping, as we'll be pausing playback to buffer + // soon anyway and we'll want to be able to display frames immediately + // after buffering finishes. + if (mState == DECODER_STATE_DECODING && + !mSkipToNextKeyFrame && + mIsVideoDecoding && + ((!mIsAudioPrerolling && mIsAudioDecoding && + GetDecodedAudioDuration() < mLowAudioThresholdUsecs * mPlaybackRate) || + (!mIsVideoPrerolling && mIsVideoDecoding && + // don't skip frame when |clock time| <= |mVideoFrameEndTime| for + // we are still in the safe range without underrunning video frames + GetClock() > mVideoFrameEndTime && + (static_cast(VideoQueue().GetSize()) + < LOW_VIDEO_FRAMES * mPlaybackRate))) && + !HasLowUndecodedData()) + { + mSkipToNextKeyFrame = true; + DECODER_LOG(PR_LOG_DEBUG, "Skipping video decode to the next keyframe"); + } + + // Time the video decode, so that if it's slow, we can increase our low + // audio threshold to reduce the chance of an audio underrun while we're + // waiting for a video decode to complete. + TimeDuration decodeTime; + { + int64_t currentTime = GetMediaTime(); + ReentrantMonitorAutoExit exitMon(mDecoder->GetReentrantMonitor()); + TimeStamp start = TimeStamp::Now(); + mIsVideoDecoding = mReader->DecodeVideoFrame(mSkipToNextKeyFrame, currentTime); + decodeTime = TimeStamp::Now() - start; + } + if (!mIsVideoDecoding) { + // Playback ended for this stream, close the sample queue. + VideoQueue().Finish(); + CheckIfDecodeComplete(); + } + + if (THRESHOLD_FACTOR * DurationToUsecs(decodeTime) > mLowAudioThresholdUsecs && + !HasLowUndecodedData()) + { + mLowAudioThresholdUsecs = + std::min(THRESHOLD_FACTOR * DurationToUsecs(decodeTime), AMPLE_AUDIO_USECS); + mAmpleAudioThresholdUsecs = std::max(THRESHOLD_FACTOR * mLowAudioThresholdUsecs, + mAmpleAudioThresholdUsecs); + DECODER_LOG(PR_LOG_DEBUG, "Slow video decode, set mLowAudioThresholdUsecs=%lld mAmpleAudioThresholdUsecs=%lld", + mLowAudioThresholdUsecs, mAmpleAudioThresholdUsecs); + } + + SendStreamData(); + + // The ready state can change when we've decoded data, so update the + // ready state, so that DOM events can fire. + UpdateReadyState(); + + mDispatchedVideoDecodeTask = false; + DispatchDecodeTasksIfNeeded(); +} + +bool +MediaDecoderStateMachine::NeedToDecodeAudio() +{ + AssertCurrentThreadInMonitor(); + NS_ASSERTION(OnStateMachineThread() || OnDecodeThread(), + "Should be on state machine or decode thread."); + return mIsAudioDecoding && + !mMinimizePreroll && + !HaveEnoughDecodedAudio(mAmpleAudioThresholdUsecs * mPlaybackRate); +} + +void +MediaDecoderStateMachine::DecodeAudio() +{ + ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); + NS_ASSERTION(OnDecodeThread(), "Should be on decode thread."); + + if (mState != DECODER_STATE_DECODING && mState != DECODER_STATE_BUFFERING) { + mDispatchedAudioDecodeTask = false; + return; + } + EnsureActive(); + + // We don't want to consider skipping to the next keyframe if we've + // only just started up the decode loop, so wait until we've decoded + // some audio data before enabling the keyframe skip logic on audio. + if (mIsAudioPrerolling && + GetDecodedAudioDuration() >= mAudioPrerollUsecs * mPlaybackRate) { + mIsAudioPrerolling = false; + } + + { + ReentrantMonitorAutoExit exitMon(mDecoder->GetReentrantMonitor()); + mIsAudioDecoding = mReader->DecodeAudioData(); + } + if (!mIsAudioDecoding) { + // Playback ended for this stream, close the sample queue. + AudioQueue().Finish(); + CheckIfDecodeComplete(); + } + + SendStreamData(); + + // Notify to ensure that the AudioLoop() is not waiting, in case it was + // waiting for more audio to be decoded. + mDecoder->GetReentrantMonitor().NotifyAll(); + + // The ready state can change when we've decoded data, so update the + // ready state, so that DOM events can fire. + UpdateReadyState(); + + mDispatchedAudioDecodeTask = false; + DispatchDecodeTasksIfNeeded(); +} + +void +MediaDecoderStateMachine::CheckIfDecodeComplete() +{ + AssertCurrentThreadInMonitor(); + if (mState == DECODER_STATE_SHUTDOWN || + mState == DECODER_STATE_SEEKING || + mState == DECODER_STATE_COMPLETED) { + // Don't change our state if we've already been shutdown, or we're seeking, + // since we don't want to abort the shutdown or seek processes. + return; + } + MOZ_ASSERT(!AudioQueue().IsFinished() || !mIsAudioDecoding); + MOZ_ASSERT(!VideoQueue().IsFinished() || !mIsVideoDecoding); + if (!mIsVideoDecoding && !mIsAudioDecoding) { + // We've finished decoding all active streams, + // so move to COMPLETED state. + mState = DECODER_STATE_COMPLETED; + DispatchDecodeTasksIfNeeded(); + ScheduleStateMachine(); + } + DECODER_LOG(PR_LOG_DEBUG, "CheckIfDecodeComplete %scompleted", + ((mState == DECODER_STATE_COMPLETED) ? "" : "NOT ")); +} + +bool MediaDecoderStateMachine::IsPlaying() +{ + AssertCurrentThreadInMonitor(); + + return !mPlayStartTime.IsNull(); +} + +// If we have already written enough frames to the AudioStream, start the +// playback. +static void +StartAudioStreamPlaybackIfNeeded(AudioStream* aStream) +{ + // We want to have enough data in the buffer to start the stream. + if (static_cast(aStream->GetWritten()) / aStream->GetRate() >= + static_cast(AUDIOSTREAM_MIN_WRITE_BEFORE_START_USECS) / USECS_PER_S) { + aStream->Start(); + } +} + +static void WriteSilence(AudioStream* aStream, uint32_t aFrames) +{ + uint32_t numSamples = aFrames * aStream->GetChannels(); + nsAutoTArray buf; + buf.SetLength(numSamples); + memset(buf.Elements(), 0, numSamples * sizeof(AudioDataValue)); + aStream->Write(buf.Elements(), aFrames); + + StartAudioStreamPlaybackIfNeeded(aStream); +} + +void MediaDecoderStateMachine::AudioLoop() +{ + NS_ASSERTION(OnAudioThread(), "Should be on audio thread."); + DECODER_LOG(PR_LOG_DEBUG, "Begun audio thread/loop"); + int64_t audioDuration = 0; + int64_t audioStartTime = -1; + uint32_t channels, rate; + double volume = -1; + bool setVolume; + double playbackRate = -1; + bool setPlaybackRate; + bool preservesPitch; + bool setPreservesPitch; + AudioChannel audioChannel; + + { + ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); + mAudioCompleted = false; + audioStartTime = mAudioStartTime; + NS_ASSERTION(audioStartTime != -1, "Should have audio start time by now"); + channels = mInfo.mAudio.mChannels; + rate = mInfo.mAudio.mRate; + + audioChannel = mDecoder->GetAudioChannel(); + volume = mVolume; + preservesPitch = mPreservesPitch; + playbackRate = mPlaybackRate; + } + + { + // AudioStream initialization can block for extended periods in unusual + // circumstances, so we take care to drop the decoder monitor while + // initializing. + RefPtr audioStream(new AudioStream()); + audioStream->Init(channels, rate, audioChannel, AudioStream::HighLatency); + audioStream->SetVolume(volume); + if (audioStream->SetPreservesPitch(preservesPitch) != NS_OK) { + NS_WARNING("Setting the pitch preservation failed at AudioLoop start."); + } + if (playbackRate != 1.0) { + NS_ASSERTION(playbackRate != 0, + "Don't set the playbackRate to 0 on an AudioStream."); + if (audioStream->SetPlaybackRate(playbackRate) != NS_OK) { + NS_WARNING("Setting the playback rate failed at AudioLoop start."); + } + } + + { + ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); + mAudioStream = audioStream.forget(); + } + } + + while (1) { + // Wait while we're not playing, and we're not shutting down, or we're + // playing and we've got no audio to play. + { + ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); + NS_ASSERTION(mState != DECODER_STATE_DECODING_METADATA, + "Should have meta data before audio started playing."); + while (mState != DECODER_STATE_SHUTDOWN && + !mStopAudioThread && + (!IsPlaying() || + mState == DECODER_STATE_BUFFERING || + (AudioQueue().GetSize() == 0 && + !AudioQueue().AtEndOfStream()))) + { + if (!IsPlaying() && !mAudioStream->IsPaused()) { + mAudioStream->Pause(); + } + mon.Wait(); + } + + // If we're shutting down, break out and exit the audio thread. + // Also break out if audio is being captured. + if (mState == DECODER_STATE_SHUTDOWN || + mStopAudioThread || + AudioQueue().AtEndOfStream()) + { + break; + } + + // We only want to go to the expense of changing the volume if + // the volume has changed. + setVolume = volume != mVolume; + volume = mVolume; + + // Same for the playbackRate. + setPlaybackRate = playbackRate != mPlaybackRate; + playbackRate = mPlaybackRate; + + // Same for the pitch preservation. + setPreservesPitch = preservesPitch != mPreservesPitch; + preservesPitch = mPreservesPitch; + + if (IsPlaying() && mAudioStream->IsPaused()) { + mAudioStream->Resume(); + } + } + + if (setVolume) { + mAudioStream->SetVolume(volume); + } + if (setPlaybackRate) { + NS_ASSERTION(playbackRate != 0, + "Don't set the playbackRate to 0 in the AudioStreams"); + if (mAudioStream->SetPlaybackRate(playbackRate) != NS_OK) { + NS_WARNING("Setting the playback rate failed in AudioLoop."); + } + } + if (setPreservesPitch) { + if (mAudioStream->SetPreservesPitch(preservesPitch) != NS_OK) { + NS_WARNING("Setting the pitch preservation failed in AudioLoop."); + } + } + NS_ASSERTION(AudioQueue().GetSize() > 0, + "Should have data to play"); + // See if there's a gap in the audio. If there is, push silence into the + // audio hardware, so we can play across the gap. + const AudioData* s = AudioQueue().PeekFront(); + + // Calculate the number of frames that have been pushed onto the audio + // hardware. + CheckedInt64 playedFrames = UsecsToFrames(audioStartTime, rate) + + audioDuration; + // Calculate the timestamp of the next chunk of audio in numbers of + // samples. + CheckedInt64 sampleTime = UsecsToFrames(s->mTime, rate); + CheckedInt64 missingFrames = sampleTime - playedFrames; + if (!missingFrames.isValid() || !sampleTime.isValid()) { + NS_WARNING("Int overflow adding in AudioLoop()"); + break; + } + + int64_t framesWritten = 0; + if (missingFrames.value() > 0) { + // The next audio chunk begins some time after the end of the last chunk + // we pushed to the audio hardware. We must push silence into the audio + // hardware so that the next audio chunk begins playback at the correct + // time. + missingFrames = std::min(UINT32_MAX, missingFrames.value()); + VERBOSE_LOG("playing %d frames of silence", int32_t(missingFrames.value())); + framesWritten = PlaySilence(static_cast(missingFrames.value()), + channels, playedFrames.value()); + } else { + framesWritten = PlayFromAudioQueue(sampleTime.value(), channels); + } + audioDuration += framesWritten; + { + ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); + CheckedInt64 playedUsecs = FramesToUsecs(audioDuration, rate) + audioStartTime; + if (!playedUsecs.isValid()) { + NS_WARNING("Int overflow calculating audio end time"); + break; + } + mAudioEndTime = playedUsecs.value(); + } + } + { + ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); + if (AudioQueue().AtEndOfStream() && + mState != DECODER_STATE_SHUTDOWN && + !mStopAudioThread) + { + // If the media was too short to trigger the start of the audio stream, + // start it now. + mAudioStream->Start(); + // Last frame pushed to audio hardware, wait for the audio to finish, + // before the audio thread terminates. + bool seeking = false; + { + int64_t oldPosition = -1; + int64_t position = GetMediaTime(); + while (oldPosition != position && + mAudioEndTime - position > 0 && + mState != DECODER_STATE_SEEKING && + mState != DECODER_STATE_SHUTDOWN) + { + const int64_t DRAIN_BLOCK_USECS = 100000; + Wait(std::min(mAudioEndTime - position, DRAIN_BLOCK_USECS)); + oldPosition = position; + position = GetMediaTime(); + } + seeking = mState == DECODER_STATE_SEEKING; + } + + if (!seeking && !mAudioStream->IsPaused()) { + { + ReentrantMonitorAutoExit exit(mDecoder->GetReentrantMonitor()); + mAudioStream->Drain(); + } + } + } + } + DECODER_LOG(PR_LOG_DEBUG, "Reached audio stream end."); + { + // Must hold lock while shutting down and anulling the audio stream to prevent + // state machine thread trying to use it while we're destroying it. + ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); + mAudioStream->Shutdown(); + mAudioStream = nullptr; + if (!mAudioCaptured) { + mAudioCompleted = true; + UpdateReadyState(); + // Kick the decode thread; it may be sleeping waiting for this to finish. + mDecoder->GetReentrantMonitor().NotifyAll(); + } + } + + DECODER_LOG(PR_LOG_DEBUG, "Audio stream finished playing, audio thread exit"); +} + +uint32_t MediaDecoderStateMachine::PlaySilence(uint32_t aFrames, + uint32_t aChannels, + uint64_t aFrameOffset) + +{ + NS_ASSERTION(OnAudioThread(), "Only call on audio thread."); + NS_ASSERTION(!mAudioStream->IsPaused(), "Don't play when paused"); + uint32_t maxFrames = SILENCE_BYTES_CHUNK / aChannels / sizeof(AudioDataValue); + uint32_t frames = std::min(aFrames, maxFrames); + WriteSilence(mAudioStream, frames); + return frames; +} + +uint32_t MediaDecoderStateMachine::PlayFromAudioQueue(uint64_t aFrameOffset, + uint32_t aChannels) +{ + NS_ASSERTION(OnAudioThread(), "Only call on audio thread."); + NS_ASSERTION(!mAudioStream->IsPaused(), "Don't play when paused"); + nsAutoPtr audio(AudioQueue().PopFront()); + { + ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); + NS_WARN_IF_FALSE(IsPlaying(), "Should be playing"); + // Awaken the decode loop if it's waiting for space to free up in the + // audio queue. + mDecoder->GetReentrantMonitor().NotifyAll(); + } + int64_t offset = -1; + uint32_t frames = 0; + VERBOSE_LOG("playing %d frames of data to stream for AudioData at %lld", + audio->mFrames, audio->mTime); + mAudioStream->Write(audio->mAudioData, + audio->mFrames); + + aChannels = mAudioStream->GetOutChannels(); + + StartAudioStreamPlaybackIfNeeded(mAudioStream); + + offset = audio->mOffset; + frames = audio->mFrames; + + if (offset != -1) { + mDecoder->UpdatePlaybackOffset(offset); + } + return frames; +} + +nsresult MediaDecoderStateMachine::Init(MediaDecoderStateMachine* aCloneDonor) +{ + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr decodePool( + SharedThreadPool::Get(NS_LITERAL_CSTRING("Media Decode"), + Preferences::GetUint("media.num-decode-threads", 25))); + NS_ENSURE_TRUE(decodePool, NS_ERROR_FAILURE); + + RefPtr stateMachinePool( + SharedThreadPool::Get(NS_LITERAL_CSTRING("Media State Machine"), 1)); + NS_ENSURE_TRUE(stateMachinePool, NS_ERROR_FAILURE); + + mDecodeTaskQueue = new MediaTaskQueue(decodePool.forget()); + NS_ENSURE_TRUE(mDecodeTaskQueue, NS_ERROR_FAILURE); + + MediaDecoderReader* cloneReader = nullptr; + if (aCloneDonor) { + cloneReader = aCloneDonor->mReader; + } + + mStateMachineThreadPool = stateMachinePool; + + nsresult rv; + mTimer = do_CreateInstance("@mozilla.org/timer;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = mTimer->SetTarget(GetStateMachineThread()); + NS_ENSURE_SUCCESS(rv, rv); + + return mReader->Init(cloneReader); +} + +void MediaDecoderStateMachine::StopPlayback() +{ + DECODER_LOG(PR_LOG_DEBUG, "StopPlayback()"); + + AssertCurrentThreadInMonitor(); + + mDecoder->NotifyPlaybackStopped(); + + if (IsPlaying()) { + mPlayDuration = GetClock(); + mPlayStartTime = TimeStamp(); + } + // Notify the audio thread, so that it notices that we've stopped playing, + // so it can pause audio playback. + mDecoder->GetReentrantMonitor().NotifyAll(); + NS_ASSERTION(!IsPlaying(), "Should report not playing at end of StopPlayback()"); + mDecoder->UpdateStreamBlockingForStateMachinePlaying(); + + DispatchDecodeTasksIfNeeded(); +} + +void MediaDecoderStateMachine::SetSyncPointForMediaStream() +{ + AssertCurrentThreadInMonitor(); + + DecodedStreamData* stream = mDecoder->GetDecodedStream(); + if (!stream) { + return; + } + + mSyncPointInMediaStream = stream->GetLastOutputTime(); + mSyncPointInDecodedStream = mStartTime + mPlayDuration; +} + +int64_t MediaDecoderStateMachine::GetCurrentTimeViaMediaStreamSync() +{ + AssertCurrentThreadInMonitor(); + NS_ASSERTION(mSyncPointInDecodedStream >= 0, "Should have set up sync point"); + DecodedStreamData* stream = mDecoder->GetDecodedStream(); + StreamTime streamDelta = stream->GetLastOutputTime() - mSyncPointInMediaStream; + return mSyncPointInDecodedStream + MediaTimeToMicroseconds(streamDelta); +} + +void MediaDecoderStateMachine::StartPlayback() +{ + DECODER_LOG(PR_LOG_DEBUG, "StartPlayback()"); + + NS_ASSERTION(!IsPlaying(), "Shouldn't be playing when StartPlayback() is called"); + AssertCurrentThreadInMonitor(); + + mDecoder->NotifyPlaybackStarted(); + mPlayStartTime = TimeStamp::Now(); + + NS_ASSERTION(IsPlaying(), "Should report playing by end of StartPlayback()"); + if (NS_FAILED(StartAudioThread())) { + NS_WARNING("Failed to create audio thread"); + } + mDecoder->GetReentrantMonitor().NotifyAll(); + mDecoder->UpdateStreamBlockingForStateMachinePlaying(); +} + +void MediaDecoderStateMachine::UpdatePlaybackPositionInternal(int64_t aTime) +{ + NS_ASSERTION(OnStateMachineThread() || OnDecodeThread(), + "Should be on state machine thread."); + AssertCurrentThreadInMonitor(); + + NS_ASSERTION(mStartTime >= 0, "Should have positive mStartTime"); + mCurrentFrameTime = aTime - mStartTime; + NS_ASSERTION(mCurrentFrameTime >= 0, "CurrentTime should be positive!"); + if (aTime > mEndTime) { + NS_ASSERTION(mCurrentFrameTime > GetDuration(), + "CurrentTime must be after duration if aTime > endTime!"); + mEndTime = aTime; + nsCOMPtr event = + NS_NewRunnableMethod(mDecoder, &MediaDecoder::DurationChanged); + NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL); + } +} + +void MediaDecoderStateMachine::UpdatePlaybackPosition(int64_t aTime) +{ + UpdatePlaybackPositionInternal(aTime); + + bool fragmentEnded = mFragmentEndTime >= 0 && GetMediaTime() >= mFragmentEndTime; + if (!mPositionChangeQueued || fragmentEnded) { + mPositionChangeQueued = true; + nsCOMPtr event = + NS_NewRunnableMethod(mDecoder, &MediaDecoder::PlaybackPositionChanged); + NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL); + } + + mMetadataManager.DispatchMetadataIfNeeded(mDecoder, aTime); + + if (fragmentEnded) { + StopPlayback(); + } +} + +void MediaDecoderStateMachine::ClearPositionChangeFlag() +{ + NS_ASSERTION(NS_IsMainThread(), "Should be on main thread."); + AssertCurrentThreadInMonitor(); + + mPositionChangeQueued = false; +} + +MediaDecoderOwner::NextFrameStatus MediaDecoderStateMachine::GetNextFrameStatus() +{ + ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); + if (IsBuffering() || IsSeeking()) { + return MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE_BUFFERING; + } else if (HaveNextFrameData()) { + return MediaDecoderOwner::NEXT_FRAME_AVAILABLE; + } + return MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE; +} + +void MediaDecoderStateMachine::SetVolume(double volume) +{ + NS_ASSERTION(NS_IsMainThread(), "Should be on main thread."); + ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); + mVolume = volume; +} + +void MediaDecoderStateMachine::SetAudioCaptured(bool aCaptured) +{ + NS_ASSERTION(NS_IsMainThread(), "Should be on main thread."); + ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); + if (!mAudioCaptured && aCaptured && !mStopAudioThread) { + // Make sure the state machine runs as soon as possible. That will + // stop the audio thread. + // If mStopAudioThread is true then we're already stopping the audio thread + // and since we set mAudioCaptured to true, nothing can start it again. + ScheduleStateMachine(); + } + mAudioCaptured = aCaptured; +} + +double MediaDecoderStateMachine::GetCurrentTime() const +{ + NS_ASSERTION(NS_IsMainThread() || + OnStateMachineThread() || + OnDecodeThread(), + "Should be on main, decode, or state machine thread."); + + return static_cast(mCurrentFrameTime) / static_cast(USECS_PER_S); +} + +int64_t MediaDecoderStateMachine::GetDuration() +{ + AssertCurrentThreadInMonitor(); + + if (mEndTime == -1 || mStartTime == -1) + return -1; + return mEndTime - mStartTime; +} + +void MediaDecoderStateMachine::SetDuration(int64_t aDuration) +{ + NS_ASSERTION(NS_IsMainThread() || OnDecodeThread(), + "Should be on main or decode thread."); + AssertCurrentThreadInMonitor(); + + if (aDuration == -1) { + return; + } + + if (mStartTime != -1) { + mEndTime = mStartTime + aDuration; + } else { + mStartTime = 0; + mEndTime = aDuration; + } +} + +void MediaDecoderStateMachine::UpdateEstimatedDuration(int64_t aDuration) +{ + AssertCurrentThreadInMonitor(); + int64_t duration = GetDuration(); + if (aDuration != duration && + abs(aDuration - duration) > ESTIMATED_DURATION_FUZZ_FACTOR_USECS) { + SetDuration(aDuration); + nsCOMPtr event = + NS_NewRunnableMethod(mDecoder, &MediaDecoder::DurationChanged); + NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL); + } +} + +void MediaDecoderStateMachine::SetMediaEndTime(int64_t aEndTime) +{ + NS_ASSERTION(OnDecodeThread(), "Should be on decode thread"); + AssertCurrentThreadInMonitor(); + + mEndTime = aEndTime; +} + +void MediaDecoderStateMachine::SetFragmentEndTime(int64_t aEndTime) +{ + AssertCurrentThreadInMonitor(); + + mFragmentEndTime = aEndTime < 0 ? aEndTime : aEndTime + mStartTime; +} + +void MediaDecoderStateMachine::SetTransportSeekable(bool aTransportSeekable) +{ + NS_ASSERTION(NS_IsMainThread() || OnDecodeThread(), + "Should be on main thread or the decoder thread."); + AssertCurrentThreadInMonitor(); + + mTransportSeekable = aTransportSeekable; +} + +void MediaDecoderStateMachine::SetMediaSeekable(bool aMediaSeekable) +{ + NS_ASSERTION(NS_IsMainThread() || OnDecodeThread(), + "Should be on main thread or the decoder thread."); + + mMediaSeekable = aMediaSeekable; +} + +bool MediaDecoderStateMachine::IsDormantNeeded() +{ + return mReader->IsDormantNeeded(); +} + +void MediaDecoderStateMachine::SetDormant(bool aDormant) +{ + NS_ASSERTION(NS_IsMainThread(), "Should be on main thread."); + AssertCurrentThreadInMonitor(); + + if (!mReader) { + return; + } + + if (aDormant) { + ScheduleStateMachine(); + mState = DECODER_STATE_DORMANT; + mDecoder->GetReentrantMonitor().NotifyAll(); + } else if ((aDormant != true) && (mState == DECODER_STATE_DORMANT)) { + ScheduleStateMachine(); + mStartTime = 0; + mCurrentFrameTime = 0; + mState = DECODER_STATE_DECODING_METADATA; + mDecoder->GetReentrantMonitor().NotifyAll(); + } +} + +void MediaDecoderStateMachine::Shutdown() +{ + NS_ASSERTION(NS_IsMainThread(), "Should be on main thread."); + + // Once we've entered the shutdown state here there's no going back. + ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); + + // Change state before issuing shutdown request to threads so those + // threads can start exiting cleanly during the Shutdown call. + DECODER_LOG(PR_LOG_DEBUG, "Changed state to SHUTDOWN"); + ScheduleStateMachine(); + mState = DECODER_STATE_SHUTDOWN; + mDecoder->GetReentrantMonitor().NotifyAll(); +} + +void MediaDecoderStateMachine::StartDecoding() +{ + NS_ASSERTION(OnStateMachineThread() || OnDecodeThread(), + "Should be on state machine or decode thread."); + ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); + if (mState == DECODER_STATE_DECODING) { + return; + } + mState = DECODER_STATE_DECODING; + + mDecodeStartTime = TimeStamp::Now(); + + // Reset our "stream finished decoding" flags, so we try to decode all + // streams that we have when we start decoding. + mIsVideoDecoding = HasVideo() && !VideoQueue().IsFinished(); + mIsAudioDecoding = HasAudio() && !AudioQueue().IsFinished(); + + CheckIfDecodeComplete(); + if (mState == DECODER_STATE_COMPLETED) { + return; + } + + // Reset other state to pristine values before starting decode. + mSkipToNextKeyFrame = false; + mIsAudioPrerolling = true; + mIsVideoPrerolling = true; + + // Ensure that we've got tasks enqueued to decode data if we need to. + DispatchDecodeTasksIfNeeded(); + + ScheduleStateMachine(); +} + +void MediaDecoderStateMachine::StartWaitForResources() +{ + NS_ASSERTION(OnStateMachineThread() || OnDecodeThread(), + "Should be on state machine or decode thread."); + AssertCurrentThreadInMonitor(); + mState = DECODER_STATE_WAIT_FOR_RESOURCES; +} + +void MediaDecoderStateMachine::NotifyWaitingForResourcesStatusChanged() +{ + AssertCurrentThreadInMonitor(); + if (mState != DECODER_STATE_WAIT_FOR_RESOURCES || + mReader->IsWaitingMediaResources()) { + return; + } + // The reader is no longer waiting for resources (say a hardware decoder), + // we can now proceed to decode metadata. + mState = DECODER_STATE_DECODING_METADATA; + EnqueueDecodeMetadataTask(); +} + +void MediaDecoderStateMachine::Play() +{ + NS_ASSERTION(NS_IsMainThread(), "Should be on main thread."); + // When asked to play, switch to decoding state only if + // we are currently buffering. In other cases, we'll start playing anyway + // when the state machine notices the decoder's state change to PLAYING. + ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); + if (mState == DECODER_STATE_BUFFERING) { + DECODER_LOG(PR_LOG_DEBUG, "Changed state from BUFFERING to DECODING"); + mState = DECODER_STATE_DECODING; + mDecodeStartTime = TimeStamp::Now(); + } + // Once we start playing, we don't want to minimize our prerolling, as we + // assume the user is likely to want to keep playing in future. + mMinimizePreroll = false; + ScheduleStateMachine(); +} + +void MediaDecoderStateMachine::ResetPlayback() +{ + NS_ASSERTION(OnDecodeThread(), "Should be on decode thread."); + mVideoFrameEndTime = -1; + mAudioStartTime = -1; + mAudioEndTime = -1; + mAudioCompleted = false; +} + +void MediaDecoderStateMachine::NotifyDataArrived(const char* aBuffer, + uint32_t aLength, + int64_t aOffset) +{ + NS_ASSERTION(NS_IsMainThread(), "Only call on main thread"); + mReader->NotifyDataArrived(aBuffer, aLength, aOffset); + + // While playing an unseekable stream of unknown duration, mEndTime is + // updated (in AdvanceFrame()) as we play. But if data is being downloaded + // faster than played, mEndTime won't reflect the end of playable data + // since we haven't played the frame at the end of buffered data. So update + // mEndTime here as new data is downloaded to prevent such a lag. + dom::TimeRanges buffered; + if (mDecoder->IsInfinite() && + NS_SUCCEEDED(mDecoder->GetBuffered(&buffered))) + { + uint32_t length = 0; + buffered.GetLength(&length); + if (length) { + double end = 0; + buffered.End(length - 1, &end); + ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); + mEndTime = std::max(mEndTime, end * USECS_PER_S); + } + } +} + +void MediaDecoderStateMachine::Seek(const SeekTarget& aTarget) +{ + NS_ASSERTION(NS_IsMainThread(), "Should be on main thread."); + ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); + + // We need to be able to seek both at a transport level and at a media level + // to seek. + if (!mMediaSeekable) { + return; + } + // MediaDecoder::mPlayState should be SEEKING while we seek, and + // in that case MediaDecoder shouldn't be calling us. + NS_ASSERTION(mState != DECODER_STATE_SEEKING, + "We shouldn't already be seeking"); + NS_ASSERTION(mState >= DECODER_STATE_DECODING, + "We should have loaded metadata"); + + // Bound the seek time to be inside the media range. + NS_ASSERTION(mStartTime != -1, "Should know start time by now"); + NS_ASSERTION(mEndTime != -1, "Should know end time by now"); + int64_t seekTime = aTarget.mTime + mStartTime; + seekTime = std::min(seekTime, mEndTime); + seekTime = std::max(mStartTime, seekTime); + NS_ASSERTION(seekTime >= mStartTime && seekTime <= mEndTime, + "Can only seek in range [0,duration]"); + mSeekTarget = SeekTarget(seekTime, aTarget.mType); + + mBasePosition = seekTime - mStartTime; + DECODER_LOG(PR_LOG_DEBUG, "Changed state to SEEKING (to %lld)", mSeekTarget.mTime); + mState = DECODER_STATE_SEEKING; + if (mDecoder->GetDecodedStream()) { + mDecoder->RecreateDecodedStream(seekTime - mStartTime); + } + ScheduleStateMachine(); +} + +void MediaDecoderStateMachine::StopAudioThread() +{ + NS_ASSERTION(OnDecodeThread() || + OnStateMachineThread(), "Should be on decode thread or state machine thread"); + AssertCurrentThreadInMonitor(); + + if (mStopAudioThread) { + // Nothing to do, since the thread is already stopping + return; + } + + mStopAudioThread = true; + mDecoder->GetReentrantMonitor().NotifyAll(); + if (mAudioThread) { + DECODER_LOG(PR_LOG_DEBUG, "Shutdown audio thread"); + { + ReentrantMonitorAutoExit exitMon(mDecoder->GetReentrantMonitor()); + mAudioThread->Shutdown(); + } + mAudioThread = nullptr; + // Now that the audio thread is dead, try sending data to our MediaStream(s). + // That may have been waiting for the audio thread to stop. + SendStreamData(); + } +} + +nsresult +MediaDecoderStateMachine::EnqueueDecodeMetadataTask() +{ + AssertCurrentThreadInMonitor(); + + if (mState != DECODER_STATE_DECODING_METADATA) { + return NS_OK; + } + nsresult rv = mDecodeTaskQueue->Dispatch( + NS_NewRunnableMethod(this, &MediaDecoderStateMachine::CallDecodeMetadata)); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +void +MediaDecoderStateMachine::EnsureActive() +{ + AssertCurrentThreadInMonitor(); + MOZ_ASSERT(OnDecodeThread()); + if (!mIsReaderIdle) { + return; + } + mIsReaderIdle = false; + { + ReentrantMonitorAutoExit exitMon(mDecoder->GetReentrantMonitor()); + SetReaderActive(); + } +} + +void +MediaDecoderStateMachine::SetReaderIdle() +{ +#ifdef PR_LOGGING + { + ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); + DECODER_LOG(PR_LOG_DEBUG, "SetReaderIdle() audioQueue=%lld videoQueue=%lld", + GetDecodedAudioDuration(), + VideoQueue().Duration()); + } +#endif + MOZ_ASSERT(OnDecodeThread()); + mReader->SetIdle(); +} + +void +MediaDecoderStateMachine::SetReaderActive() +{ + DECODER_LOG(PR_LOG_DEBUG, "SetReaderActive()"); + MOZ_ASSERT(OnDecodeThread()); + mReader->SetActive(); +} + +void +MediaDecoderStateMachine::DispatchDecodeTasksIfNeeded() +{ + AssertCurrentThreadInMonitor(); + + // NeedToDecodeAudio() can go from false to true while we hold the + // monitor, but it can't go from true to false. This can happen because + // NeedToDecodeAudio() takes into account the amount of decoded audio + // that's been written to the AudioStream but not played yet. So if we + // were calling NeedToDecodeAudio() twice and we thread-context switch + // between the calls, audio can play, which can affect the return value + // of NeedToDecodeAudio() giving inconsistent results. So we cache the + // value returned by NeedToDecodeAudio(), and make decisions + // based on the cached value. If NeedToDecodeAudio() has + // returned false, and then subsequently returns true and we're not + // playing, it will probably be OK since we don't need to consume data + // anyway. + + const bool needToDecodeAudio = NeedToDecodeAudio(); + const bool needToDecodeVideo = NeedToDecodeVideo(); + + // If we're in completed state, we should not need to decode anything else. + MOZ_ASSERT(mState != DECODER_STATE_COMPLETED || + (!needToDecodeAudio && !needToDecodeVideo)); + + bool needIdle = !mDecoder->IsLogicallyPlaying() && + mState != DECODER_STATE_SEEKING && + !needToDecodeAudio && + !needToDecodeVideo && + !IsPlaying(); + + if (needToDecodeAudio) { + EnsureAudioDecodeTaskQueued(); + } + if (needToDecodeVideo) { + EnsureVideoDecodeTaskQueued(); + } + + if (mIsReaderIdle == needIdle) { + return; + } + mIsReaderIdle = needIdle; + RefPtr event; + if (mIsReaderIdle) { + event = NS_NewRunnableMethod(this, &MediaDecoderStateMachine::SetReaderIdle); + } else { + event = NS_NewRunnableMethod(this, &MediaDecoderStateMachine::SetReaderActive); + } + if (NS_FAILED(mDecodeTaskQueue->Dispatch(event.forget())) && + mState != DECODER_STATE_SHUTDOWN) { + NS_WARNING("Failed to dispatch event to set decoder idle state"); + } +} + +nsresult +MediaDecoderStateMachine::EnqueueDecodeSeekTask() +{ + NS_ASSERTION(OnStateMachineThread() || OnDecodeThread(), + "Should be on state machine or decode thread."); + AssertCurrentThreadInMonitor(); + + if (mState != DECODER_STATE_SEEKING) { + return NS_OK; + } + nsresult rv = mDecodeTaskQueue->Dispatch( + NS_NewRunnableMethod(this, &MediaDecoderStateMachine::DecodeSeek)); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult +MediaDecoderStateMachine::DispatchAudioDecodeTaskIfNeeded() +{ + ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); + NS_ASSERTION(OnStateMachineThread() || OnDecodeThread(), + "Should be on state machine or decode thread."); + + if (NeedToDecodeAudio()) { + return EnsureAudioDecodeTaskQueued(); + } + + return NS_OK; +} + +nsresult +MediaDecoderStateMachine::EnsureAudioDecodeTaskQueued() +{ + ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); + NS_ASSERTION(OnStateMachineThread() || OnDecodeThread(), + "Should be on state machine or decode thread."); + + if (mState >= DECODER_STATE_COMPLETED) { + return NS_OK; + } + + MOZ_ASSERT(mState > DECODER_STATE_DECODING_METADATA); + + if (mIsAudioDecoding && !mDispatchedAudioDecodeTask) { + nsresult rv = mDecodeTaskQueue->Dispatch( + NS_NewRunnableMethod(this, &MediaDecoderStateMachine::DecodeAudio)); + if (NS_SUCCEEDED(rv)) { + mDispatchedAudioDecodeTask = true; + } else { + NS_WARNING("Failed to dispatch task to decode audio"); + } + } + + return NS_OK; +} + +nsresult +MediaDecoderStateMachine::DispatchVideoDecodeTaskIfNeeded() +{ + ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); + NS_ASSERTION(OnStateMachineThread() || OnDecodeThread(), + "Should be on state machine or decode thread."); + + if (NeedToDecodeVideo()) { + return EnsureVideoDecodeTaskQueued(); + } + + return NS_OK; +} + +nsresult +MediaDecoderStateMachine::EnsureVideoDecodeTaskQueued() +{ + ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); + NS_ASSERTION(OnStateMachineThread() || OnDecodeThread(), + "Should be on state machine or decode thread."); + + if (mState >= DECODER_STATE_COMPLETED) { + return NS_OK; + } + + MOZ_ASSERT(mState > DECODER_STATE_DECODING_METADATA); + + if (mIsVideoDecoding && !mDispatchedVideoDecodeTask) { + nsresult rv = mDecodeTaskQueue->Dispatch( + NS_NewRunnableMethod(this, &MediaDecoderStateMachine::DecodeVideo)); + if (NS_SUCCEEDED(rv)) { + mDispatchedVideoDecodeTask = true; + } else { + NS_WARNING("Failed to dispatch task to decode video"); + } + } + + return NS_OK; +} + +nsresult +MediaDecoderStateMachine::StartAudioThread() +{ + NS_ASSERTION(OnStateMachineThread() || OnDecodeThread(), + "Should be on state machine or decode thread."); + AssertCurrentThreadInMonitor(); + if (mAudioCaptured) { + NS_ASSERTION(mStopAudioThread, "mStopAudioThread must always be true if audio is captured"); + return NS_OK; + } + + mStopAudioThread = false; + if (HasAudio() && !mAudioThread) { + nsresult rv = NS_NewNamedThread("Media Audio", + getter_AddRefs(mAudioThread), + nullptr, + MEDIA_THREAD_STACK_SIZE); + if (NS_FAILED(rv)) { + DECODER_LOG(PR_LOG_WARNING, "Changed state to SHUTDOWN because failed to create audio thread"); + mState = DECODER_STATE_SHUTDOWN; + return rv; + } + + nsCOMPtr event = + NS_NewRunnableMethod(this, &MediaDecoderStateMachine::AudioLoop); + mAudioThread->Dispatch(event, NS_DISPATCH_NORMAL); + } + return NS_OK; +} + +int64_t MediaDecoderStateMachine::AudioDecodedUsecs() +{ + NS_ASSERTION(HasAudio(), + "Should only call AudioDecodedUsecs() when we have audio"); + // The amount of audio we have decoded is the amount of audio data we've + // already decoded and pushed to the hardware, plus the amount of audio + // data waiting to be pushed to the hardware. + int64_t pushed = (mAudioEndTime != -1) ? (mAudioEndTime - GetMediaTime()) : 0; + return pushed + AudioQueue().Duration(); +} + +bool MediaDecoderStateMachine::HasLowDecodedData(int64_t aAudioUsecs) +{ + AssertCurrentThreadInMonitor(); + // We consider ourselves low on decoded data if we're low on audio, + // provided we've not decoded to the end of the audio stream, or + // if we're low on video frames, provided + // we've not decoded to the end of the video stream. + return ((HasAudio() && + !AudioQueue().IsFinished() && + AudioDecodedUsecs() < aAudioUsecs) + || + (HasVideo() && + !VideoQueue().IsFinished() && + static_cast(VideoQueue().GetSize()) < LOW_VIDEO_FRAMES)); +} + +bool MediaDecoderStateMachine::HasLowUndecodedData() +{ + return HasLowUndecodedData(mLowDataThresholdUsecs); +} + +bool MediaDecoderStateMachine::HasLowUndecodedData(double aUsecs) +{ + AssertCurrentThreadInMonitor(); + NS_ASSERTION(mState > DECODER_STATE_DECODING_METADATA, + "Must have loaded metadata for GetBuffered() to work"); + + bool reliable; + double bytesPerSecond = mDecoder->ComputePlaybackRate(&reliable); + if (!reliable) { + // Default to assuming we have enough + return false; + } + + MediaResource* stream = mDecoder->GetResource(); + int64_t currentPos = stream->Tell(); + int64_t requiredPos = currentPos + int64_t((aUsecs/1000000.0)*bytesPerSecond); + int64_t length = stream->GetLength(); + if (length >= 0) { + requiredPos = std::min(requiredPos, length); + } + + return stream->GetCachedDataEnd(currentPos) < requiredPos; +} + +void +MediaDecoderStateMachine::DecodeError() +{ + AssertCurrentThreadInMonitor(); + NS_ASSERTION(OnDecodeThread(), "Should be on decode thread."); + + // Change state to shutdown before sending error report to MediaDecoder + // and the HTMLMediaElement, so that our pipeline can start exiting + // cleanly during the sync dispatch below. + DECODER_LOG(PR_LOG_WARNING, "Decode error, changed state to SHUTDOWN"); + ScheduleStateMachine(); + mState = DECODER_STATE_SHUTDOWN; + mDecoder->GetReentrantMonitor().NotifyAll(); + + // Dispatch the event to call DecodeError synchronously. This ensures + // we're in shutdown state by the time we exit the decode thread. + // If we just moved to shutdown state here on the decode thread, we may + // cause the state machine to shutdown/free memory without closing its + // media stream properly, and we'll get callbacks from the media stream + // causing a crash. + { + nsCOMPtr event = + NS_NewRunnableMethod(mDecoder, &MediaDecoder::DecodeError); + ReentrantMonitorAutoExit exitMon(mDecoder->GetReentrantMonitor()); + NS_DispatchToMainThread(event, NS_DISPATCH_SYNC); + } +} + +void +MediaDecoderStateMachine::CallDecodeMetadata() +{ + ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); + if (mState != DECODER_STATE_DECODING_METADATA) { + return; + } + if (NS_FAILED(DecodeMetadata())) { + DECODER_LOG(PR_LOG_WARNING, "Decode metadata failed, shutting down decoder"); + DecodeError(); + } +} + +nsresult MediaDecoderStateMachine::DecodeMetadata() +{ + AssertCurrentThreadInMonitor(); + NS_ASSERTION(OnDecodeThread(), "Should be on decode thread."); + DECODER_LOG(PR_LOG_DEBUG, "Decoding Media Headers"); + if (mState != DECODER_STATE_DECODING_METADATA) { + return NS_ERROR_FAILURE; + } + EnsureActive(); + + nsresult res; + MediaInfo info; + MetadataTags* tags; + { + ReentrantMonitorAutoExit exitMon(mDecoder->GetReentrantMonitor()); + res = mReader->ReadMetadata(&info, &tags); + } + if (NS_SUCCEEDED(res) && + mState == DECODER_STATE_DECODING_METADATA && + mReader->IsWaitingMediaResources()) { + // change state to DECODER_STATE_WAIT_FOR_RESOURCES + StartWaitForResources(); + return NS_OK; + } + + mInfo = info; + + if (NS_FAILED(res) || (!info.HasValidMedia())) { + return NS_ERROR_FAILURE; + } + mDecoder->StartProgressUpdates(); + mGotDurationFromMetaData = (GetDuration() != -1); + + VideoData* videoData = FindStartTime(); + if (videoData) { + ReentrantMonitorAutoExit exitMon(mDecoder->GetReentrantMonitor()); + RenderVideoFrame(videoData, TimeStamp::Now()); + } + + if (mState == DECODER_STATE_SHUTDOWN) { + return NS_ERROR_FAILURE; + } + + NS_ASSERTION(mStartTime != -1, "Must have start time"); + MOZ_ASSERT((!HasVideo() && !HasAudio()) || + !(mMediaSeekable && mTransportSeekable) || mEndTime != -1, + "Active seekable media should have end time"); + MOZ_ASSERT(!(mMediaSeekable && mTransportSeekable) || + GetDuration() != -1, "Seekable media should have duration"); + DECODER_LOG(PR_LOG_DEBUG, "Media goes from %lld to %lld (duration %lld) " + "transportSeekable=%d, mediaSeekable=%d", + mStartTime, mEndTime, GetDuration(), mTransportSeekable, mMediaSeekable); + + if (HasAudio() && !HasVideo()) { + // We're playing audio only. We don't need to worry about slow video + // decodes causing audio underruns, so don't buffer so much audio in + // order to reduce memory usage. + mAmpleAudioThresholdUsecs /= NO_VIDEO_AMPLE_AUDIO_DIVISOR; + mLowAudioThresholdUsecs /= NO_VIDEO_AMPLE_AUDIO_DIVISOR; + } + + // Inform the element that we've loaded the metadata and the first frame. + nsCOMPtr metadataLoadedEvent = + new AudioMetadataEventRunner(mDecoder, + mInfo.mAudio.mChannels, + mInfo.mAudio.mRate, + HasAudio(), + HasVideo(), + tags); + NS_DispatchToMainThread(metadataLoadedEvent, NS_DISPATCH_NORMAL); + + if (HasAudio()) { + RefPtr decodeTask( + NS_NewRunnableMethod(this, &MediaDecoderStateMachine::DispatchAudioDecodeTaskIfNeeded)); + AudioQueue().AddPopListener(decodeTask, mDecodeTaskQueue); + } + if (HasVideo()) { + RefPtr decodeTask( + NS_NewRunnableMethod(this, &MediaDecoderStateMachine::DispatchVideoDecodeTaskIfNeeded)); + VideoQueue().AddPopListener(decodeTask, mDecodeTaskQueue); + } + + if (mState == DECODER_STATE_DECODING_METADATA) { + DECODER_LOG(PR_LOG_DEBUG, "Changed state from DECODING_METADATA to DECODING"); + StartDecoding(); + } + + // For very short media FindStartTime() can decode the entire media. + // So we need to check if this has occurred, else our decode pipeline won't + // run (since it doesn't need to) and we won't detect end of stream. + CheckIfDecodeComplete(); + + if ((mState == DECODER_STATE_DECODING || mState == DECODER_STATE_COMPLETED) && + mDecoder->GetState() == MediaDecoder::PLAY_STATE_PLAYING && + !IsPlaying()) + { + StartPlayback(); + } + + return NS_OK; +} + +void MediaDecoderStateMachine::DecodeSeek() +{ + ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); + NS_ASSERTION(OnDecodeThread(), "Should be on decode thread."); + if (mState != DECODER_STATE_SEEKING) { + return; + } + EnsureActive(); + + // 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. + int64_t seekTime = mSeekTarget.mTime; + mDecoder->StopProgressUpdates(); + + bool currentTimeChanged = false; + const int64_t mediaTime = GetMediaTime(); + if (mediaTime != seekTime) { + currentTimeChanged = true; + // Stop playback now to ensure that while we're outside the monitor + // dispatching SeekingStarted, playback doesn't advance and mess with + // mCurrentFrameTime that we've setting to seekTime here. + StopPlayback(); + UpdatePlaybackPositionInternal(seekTime); + } + + // SeekingStarted will do a UpdateReadyStateForData which will + // inform the element and its users that we have no frames + // to display + { + ReentrantMonitorAutoExit exitMon(mDecoder->GetReentrantMonitor()); + nsCOMPtr startEvent = + NS_NewRunnableMethod(mDecoder, &MediaDecoder::SeekingStarted); + NS_DispatchToMainThread(startEvent, NS_DISPATCH_SYNC); + } + + int64_t newCurrentTime = seekTime; + if (currentTimeChanged) { + // The seek target is different than the current playback position, + // we'll need to seek the playback position, so shutdown our decode + // and audio threads. + StopAudioThread(); + ResetPlayback(); + nsresult res; + { + ReentrantMonitorAutoExit exitMon(mDecoder->GetReentrantMonitor()); + // Now perform the seek. We must not hold the state machine monitor + // while we seek, since the seek reads, which could block on I/O. + res = mReader->Seek(seekTime, + mStartTime, + mEndTime, + mediaTime); + + if (NS_SUCCEEDED(res) && mSeekTarget.mType == SeekTarget::Accurate) { + res = mReader->DecodeToTarget(seekTime); + } + } + + if (NS_SUCCEEDED(res)) { + int64_t nextSampleStartTime = 0; + VideoData* video = nullptr; + { + ReentrantMonitorAutoExit exitMon(mDecoder->GetReentrantMonitor()); + video = mReader->FindStartTime(nextSampleStartTime); + } + + // Setup timestamp state. + if (seekTime == mEndTime) { + newCurrentTime = mAudioStartTime = seekTime; + } else if (HasAudio()) { + AudioData* audio = AudioQueue().PeekFront(); + newCurrentTime = mAudioStartTime = audio ? audio->mTime : seekTime; + } else { + newCurrentTime = video ? video->mTime : seekTime; + } + mPlayDuration = newCurrentTime - mStartTime; + + if (HasVideo()) { + if (video) { + { + ReentrantMonitorAutoExit exitMon(mDecoder->GetReentrantMonitor()); + RenderVideoFrame(video, TimeStamp::Now()); + } + nsCOMPtr event = + NS_NewRunnableMethod(mDecoder, &MediaDecoder::Invalidate); + NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL); + } + } + } else { + DecodeError(); + } + } + mDecoder->StartProgressUpdates(); + if (mState == DECODER_STATE_DECODING_METADATA || + mState == DECODER_STATE_DORMANT || + mState == DECODER_STATE_SHUTDOWN) { + return; + } + + // Change state to DECODING or COMPLETED now. SeekingStopped will + // call MediaDecoderStateMachine::Seek to reset our state to SEEKING + // if we need to seek again. + + nsCOMPtr stopEvent; + bool isLiveStream = mDecoder->GetResource()->GetLength() == -1; + if (GetMediaTime() == mEndTime && !isLiveStream) { + // Seeked to end of media, move to COMPLETED state. Note we don't do + // this if we're playing a live stream, since the end of media will advance + // once we download more data! + DECODER_LOG(PR_LOG_DEBUG, "Changed state from SEEKING (to %lld) to COMPLETED", seekTime); + stopEvent = NS_NewRunnableMethod(mDecoder, &MediaDecoder::SeekingStoppedAtEnd); + // Explicitly set our state so we don't decode further, and so + // we report playback ended to the media element. + mState = DECODER_STATE_COMPLETED; + mIsAudioDecoding = false; + mIsVideoDecoding = false; + DispatchDecodeTasksIfNeeded(); + } else { + DECODER_LOG(PR_LOG_DEBUG, "Changed state from SEEKING (to %lld) to DECODING", seekTime); + stopEvent = NS_NewRunnableMethod(mDecoder, &MediaDecoder::SeekingStopped); + StartDecoding(); + } + + if (newCurrentTime != mediaTime) { + UpdatePlaybackPositionInternal(newCurrentTime); + if (mDecoder->GetDecodedStream()) { + SetSyncPointForMediaStream(); + } + } + + // Try to decode another frame to detect if we're at the end... + DECODER_LOG(PR_LOG_DEBUG, "Seek completed, mCurrentFrameTime=%lld", mCurrentFrameTime); + + { + ReentrantMonitorAutoExit exitMon(mDecoder->GetReentrantMonitor()); + NS_DispatchToMainThread(stopEvent, NS_DISPATCH_SYNC); + } + + // Reset quick buffering status. This ensures that if we began the + // seek while quick-buffering, we won't bypass quick buffering mode + // if we need to buffer after the seek. + mQuickBuffering = false; + + ScheduleStateMachine(); +} + +// Runnable to dispose of the decoder and state machine on the main thread. +class nsDecoderDisposeEvent : public nsRunnable { +public: + nsDecoderDisposeEvent(already_AddRefed aDecoder, + already_AddRefed aStateMachine) + : mDecoder(aDecoder), mStateMachine(aStateMachine) {} + NS_IMETHOD Run() { + NS_ASSERTION(NS_IsMainThread(), "Must be on main thread."); + mStateMachine->ReleaseDecoder(); + mDecoder->ReleaseStateMachine(); + mStateMachine = nullptr; + mDecoder = nullptr; + return NS_OK; + } +private: + nsRefPtr mDecoder; + nsRefPtr mStateMachine; +}; + +// Runnable which dispatches an event to the main thread to dispose of the +// decoder and state machine. This runs on the state machine thread after +// the state machine has shutdown, and all events for that state machine have +// finished running. +class nsDispatchDisposeEvent : public nsRunnable { +public: + nsDispatchDisposeEvent(MediaDecoder* aDecoder, + MediaDecoderStateMachine* aStateMachine) + : mDecoder(aDecoder), mStateMachine(aStateMachine) {} + NS_IMETHOD Run() { + NS_DispatchToMainThread(new nsDecoderDisposeEvent(mDecoder.forget(), + mStateMachine.forget())); + return NS_OK; + } +private: + nsRefPtr mDecoder; + nsRefPtr mStateMachine; +}; + +nsresult MediaDecoderStateMachine::RunStateMachine() +{ + AssertCurrentThreadInMonitor(); + + MediaResource* resource = mDecoder->GetResource(); + NS_ENSURE_TRUE(resource, NS_ERROR_NULL_POINTER); + + switch (mState) { + case DECODER_STATE_SHUTDOWN: { + if (IsPlaying()) { + StopPlayback(); + } + StopAudioThread(); + // If mAudioThread is non-null after StopAudioThread completes, we are + // running in a nested event loop waiting for Shutdown() on + // mAudioThread to complete. Return to the event loop and let it + // finish processing before continuing with shutdown. + if (mAudioThread) { + MOZ_ASSERT(mStopAudioThread); + return NS_OK; + } + + // The reader's listeners hold references to the state machine, + // creating a cycle which keeps the state machine and its shared + // thread pools alive. So break it here. + AudioQueue().ClearListeners(); + VideoQueue().ClearListeners(); + + { + ReentrantMonitorAutoExit exitMon(mDecoder->GetReentrantMonitor()); + // Wait for the thread decoding to exit. + mDecodeTaskQueue->Shutdown(); + mDecodeTaskQueue = nullptr; + mReader->ReleaseMediaResources(); + } + // Now that those threads are stopped, there's no possibility of + // mPendingWakeDecoder being needed again. Revoke it. + mPendingWakeDecoder = nullptr; + + MOZ_ASSERT(mState == DECODER_STATE_SHUTDOWN, + "How did we escape from the shutdown state?"); + // We must daisy-chain these events to destroy the decoder. We must + // destroy the decoder on the main thread, but we can't destroy the + // decoder while this thread holds the decoder monitor. We can't + // dispatch an event to the main thread to destroy the decoder from + // here, as the event may run before the dispatch returns, and we + // hold the decoder monitor here. We also want to guarantee that the + // state machine is destroyed on the main thread, and so the + // event runner running this function (which holds a reference to the + // state machine) needs to finish and be released in order to allow + // that. So we dispatch an event to run after this event runner has + // finished and released its monitor/references. That event then will + // dispatch an event to the main thread to release the decoder and + // state machine. + GetStateMachineThread()->Dispatch( + new nsDispatchDisposeEvent(mDecoder, this), NS_DISPATCH_NORMAL); + + mTimer->Cancel(); + mTimer = nullptr; + return NS_OK; + } + + case DECODER_STATE_DORMANT: { + if (IsPlaying()) { + StopPlayback(); + } + StopAudioThread(); + // Now that those threads are stopped, there's no possibility of + // mPendingWakeDecoder being needed again. Revoke it. + mPendingWakeDecoder = nullptr; + { + ReentrantMonitorAutoExit exitMon(mDecoder->GetReentrantMonitor()); + // Wait for the thread decoding, if any, to exit. + mDecodeTaskQueue->AwaitIdle(); + mReader->ReleaseMediaResources(); + } + return NS_OK; + } + + case DECODER_STATE_WAIT_FOR_RESOURCES: { + return NS_OK; + } + + case DECODER_STATE_DECODING_METADATA: { + // Ensure we have a decode thread to decode metadata. + return EnqueueDecodeMetadataTask(); + } + + case DECODER_STATE_DECODING: { + if (mDecoder->GetState() != MediaDecoder::PLAY_STATE_PLAYING && + IsPlaying()) + { + // We're playing, but the element/decoder is in paused state. Stop + // playing! + StopPlayback(); + } + + if (mDecoder->GetState() == MediaDecoder::PLAY_STATE_PLAYING && + !IsPlaying()) { + // We are playing, but the state machine does not know it yet. Tell it + // that it is, so that the clock can be properly queried. + StartPlayback(); + } + + AdvanceFrame(); + NS_ASSERTION(mDecoder->GetState() != MediaDecoder::PLAY_STATE_PLAYING || + IsStateMachineScheduled() || + mPlaybackRate == 0.0, "Must have timer scheduled"); + return NS_OK; + } + + case DECODER_STATE_BUFFERING: { + 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; + bool isLiveStream = resource->GetLength() == -1; + if ((isLiveStream || !mDecoder->CanPlayThrough()) && + elapsed < TimeDuration::FromSeconds(mBufferingWait * mPlaybackRate) && + (mQuickBuffering ? HasLowDecodedData(QUICK_BUFFERING_LOW_DATA_USECS) + : HasLowUndecodedData(mBufferingWait * USECS_PER_S)) && + !mDecoder->IsDataCachedToEndOfResource() && + !resource->IsSuspended()) + { + DECODER_LOG(PR_LOG_DEBUG, "Buffering: wait %ds, timeout in %.3lfs %s", + mBufferingWait, mBufferingWait - elapsed.ToSeconds(), + (mQuickBuffering ? "(quick exit)" : "")); + ScheduleStateMachine(USECS_PER_S); + return NS_OK; + } else { + DECODER_LOG(PR_LOG_DEBUG, "Changed state from BUFFERING to DECODING"); + DECODER_LOG(PR_LOG_DEBUG, "Buffered for %.3lfs", (now - mBufferingStart).ToSeconds()); + StartDecoding(); + } + + // Notify to allow blocked decoder thread to continue + mDecoder->GetReentrantMonitor().NotifyAll(); + UpdateReadyState(); + if (mDecoder->GetState() == MediaDecoder::PLAY_STATE_PLAYING && + !IsPlaying()) + { + StartPlayback(); + } + 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 EnqueueDecodeSeekTask(); + } + + case DECODER_STATE_COMPLETED: { + // 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 (VideoQueue().GetSize() > 0 || + (HasAudio() && !mAudioCompleted) || + (mDecoder->GetDecodedStream() && !mDecoder->GetDecodedStream()->IsFinished())) + { + AdvanceFrame(); + NS_ASSERTION(mDecoder->GetState() != MediaDecoder::PLAY_STATE_PLAYING || + mPlaybackRate == 0 || + 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) { + // While we're presenting a frame we can change state. Whatever changed + // our state should have scheduled another state machine run. + NS_ASSERTION(IsStateMachineScheduled(), "Must have timer scheduled"); + return NS_OK; + } + + StopAudioThread(); + // When we're decoding to a stream, the stream's main-thread finish signal + // will take care of calling MediaDecoder::PlaybackEnded. + if (mDecoder->GetState() == MediaDecoder::PLAY_STATE_PLAYING && + !mDecoder->GetDecodedStream()) { + int64_t videoTime = HasVideo() ? mVideoFrameEndTime : 0; + int64_t clockTime = std::max(mEndTime, std::max(videoTime, GetAudioClock())); + UpdatePlaybackPosition(clockTime); + + { + // Wait for the state change is completed in the main thread, + // otherwise we might see |mDecoder->GetState() == MediaDecoder::PLAY_STATE_PLAYING| + // in next loop and send |MediaDecoder::PlaybackEnded| again to trigger 'ended' + // event twice in the media element. + ReentrantMonitorAutoExit exitMon(mDecoder->GetReentrantMonitor()); + nsCOMPtr event = + NS_NewRunnableMethod(mDecoder, &MediaDecoder::PlaybackEnded); + NS_DispatchToMainThread(event, NS_DISPATCH_SYNC); + } + } + return NS_OK; + } + } + + return NS_OK; +} + +void MediaDecoderStateMachine::RenderVideoFrame(VideoData* aData, + TimeStamp aTarget) +{ + NS_ASSERTION(OnStateMachineThread() || OnDecodeThread(), + "Should be on state machine or decode thread."); + mDecoder->GetReentrantMonitor().AssertNotCurrentThreadIn(); + + if (aData->mDuplicate) { + return; + } + + VERBOSE_LOG("playing video frame %lld", aData->mTime); + + VideoFrameContainer* container = mDecoder->GetVideoFrameContainer(); + if (container) { + container->SetCurrentFrame(ThebesIntSize(aData->mDisplay), aData->mImage, + aTarget); + } +} + +int64_t +MediaDecoderStateMachine::GetAudioClock() +{ + // We must hold the decoder monitor while using the audio stream off the + // audio thread to ensure that it doesn't get destroyed on the audio thread + // while we're using it. + AssertCurrentThreadInMonitor(); + if (!HasAudio() || mAudioCaptured) + return -1; + if (!mAudioStream) { + // Audio thread hasn't played any data yet. + return mAudioStartTime; + } + int64_t t = mAudioStream->GetPosition(); + return (t == -1) ? -1 : t + mAudioStartTime; +} + +int64_t MediaDecoderStateMachine::GetVideoStreamPosition() +{ + AssertCurrentThreadInMonitor(); + + if (!IsPlaying()) { + return mPlayDuration + mStartTime; + } + + // The playbackRate has been just been changed, reset the playstartTime. + if (mResetPlayStartTime) { + mPlayStartTime = TimeStamp::Now(); + mResetPlayStartTime = false; + } + + int64_t pos = DurationToUsecs(TimeStamp::Now() - mPlayStartTime) + mPlayDuration; + pos -= mBasePosition; + NS_ASSERTION(pos >= 0, "Video stream position should be positive."); + return mBasePosition + pos * mPlaybackRate + mStartTime; +} + +int64_t MediaDecoderStateMachine::GetClock() +{ + AssertCurrentThreadInMonitor(); + + // 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. If our output is being + // fed to a MediaStream, use that stream as the source of the clock. + int64_t clock_time = -1; + DecodedStreamData* stream = mDecoder->GetDecodedStream(); + if (!IsPlaying()) { + clock_time = mPlayDuration + mStartTime; + } else if (stream) { + clock_time = GetCurrentTimeViaMediaStreamSync(); + } else { + int64_t 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 { + // Audio is disabled on this system. Sync to the system clock. + clock_time = GetVideoStreamPosition(); + // Ensure the clock can never go backwards. + NS_ASSERTION(mCurrentFrameTime <= clock_time || mPlaybackRate <= 0, + "Clock should go forwards if the playback rate is > 0."); + } + } + return clock_time; +} + +void MediaDecoderStateMachine::AdvanceFrame() +{ + NS_ASSERTION(OnStateMachineThread(), "Should be on state machine thread."); + AssertCurrentThreadInMonitor(); + NS_ASSERTION(!HasAudio() || mAudioStartTime != -1, + "Should know audio start time if we have audio."); + + if (mDecoder->GetState() != MediaDecoder::PLAY_STATE_PLAYING) { + return; + } + + // If playbackRate is 0.0, we should stop the progress, but not be in paused + // state, per spec. + if (mPlaybackRate == 0.0) { + return; + } + + int64_t clock_time = GetClock(); + // 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. + int64_t remainingTime = AUDIO_DURATION_USECS; + NS_ASSERTION(clock_time >= mStartTime, "Should have positive clock time."); + nsAutoPtr currentFrame; +#ifdef PR_LOGGING + int32_t droppedFrames = 0; +#endif + if (VideoQueue().GetSize() > 0) { + VideoData* frame = VideoQueue().PeekFront(); + while (mRealTime || clock_time >= frame->mTime) { + mVideoFrameEndTime = frame->GetEndTime(); + currentFrame = frame; +#ifdef PR_LOGGING + VERBOSE_LOG("discarding video frame %lld", frame->mTime); + if (droppedFrames++) { + VERBOSE_LOG("discarding video frame %lld (%d so far)", frame->mTime, droppedFrames-1); + } +#endif + VideoQueue().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 (VideoQueue().GetSize() == 0) + break; + frame = VideoQueue().PeekFront(); + } + // Current frame has already been presented, wait until it's time to + // present the next frame. + if (frame && !currentFrame) { + int64_t now = IsPlaying() ? clock_time : mPlayDuration; + + remainingTime = frame->mTime - 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. + MediaResource* resource = mDecoder->GetResource(); + if (mState == DECODER_STATE_DECODING && + mDecoder->GetState() == MediaDecoder::PLAY_STATE_PLAYING && + HasLowDecodedData(remainingTime + EXHAUSTED_DATA_MARGIN_USECS) && + !mDecoder->IsDataCachedToEndOfResource() && + !resource->IsSuspended()) { + if (JustExitedQuickBuffering() || HasLowUndecodedData()) { + if (currentFrame) { + VideoQueue().PushFront(currentFrame.forget()); + } + StartBuffering(); + // Don't go straight back to the state machine loop since that might + // cause us to start decoding again and we could flip-flop between + // decoding and quick-buffering. + ScheduleStateMachine(USECS_PER_S); + return; + } + } + + // We've got enough data to keep playing until at least the next frame. + // Start playing now if need be. + if (!IsPlaying() && ((mFragmentEndTime >= 0 && clock_time < mFragmentEndTime) || mFragmentEndTime < 0)) { + StartPlayback(); + } + + 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); + } + // If we're no longer playing after dropping and reacquiring the lock, + // playback must've been stopped on the decode thread (by a seek, for + // example). In that case, the current frame is probably out of date. + if (!IsPlaying()) { + ScheduleStateMachine(); + return; + } + MediaDecoder::FrameStatistics& frameStats = mDecoder->GetFrameStatistics(); + frameStats.NotifyPresentedFrame(); + remainingTime = currentFrame->GetEndTime() - clock_time; + currentFrame = nullptr; + } + + // 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 frame. + clock_time = std::min(clock_time, std::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 frames queued has changed, either by + // this function popping and playing a video frame, or by the audio + // thread popping and playing an audio frame, we may need to update our + // ready state. Post an update to do so. + UpdateReadyState(); + + ScheduleStateMachine(remainingTime); +} + +void MediaDecoderStateMachine::Wait(int64_t aUsecs) { + NS_ASSERTION(OnAudioThread(), "Only call on the audio thread"); + AssertCurrentThreadInMonitor(); + TimeStamp end = TimeStamp::Now() + UsecsToDuration(std::max(USECS_PER_MS, aUsecs)); + TimeStamp now; + while ((now = TimeStamp::Now()) < end && + mState != DECODER_STATE_SHUTDOWN && + mState != DECODER_STATE_SEEKING && + !mStopAudioThread && + IsPlaying()) + { + int64_t ms = static_cast(NS_round((end - now).ToSeconds() * 1000)); + if (ms == 0 || ms > UINT32_MAX) { + break; + } + mDecoder->GetReentrantMonitor().Wait(PR_MillisecondsToInterval(static_cast(ms))); + } +} + +VideoData* MediaDecoderStateMachine::FindStartTime() +{ + NS_ASSERTION(OnDecodeThread(), "Should be on decode thread."); + AssertCurrentThreadInMonitor(); + int64_t startTime = 0; + mStartTime = 0; + VideoData* v = nullptr; + { + ReentrantMonitorAutoExit exitMon(mDecoder->GetReentrantMonitor()); + v = mReader->FindStartTime(startTime); + } + if (startTime != 0) { + mStartTime = startTime; + if (mGotDurationFromMetaData) { + NS_ASSERTION(mEndTime != -1, + "We should have mEndTime as supplied duration here"); + // We were specified a duration from a Content-Duration HTTP header. + // Adjust mEndTime so that mEndTime-mStartTime matches the specified + // duration. + mEndTime = mStartTime + mEndTime; + } + } + // Set the audio start time to be start of media. If this lies before the + // first actual audio frame we have, we'll inject silence during playback + // to ensure the audio starts at the correct time. + mAudioStartTime = mStartTime; + DECODER_LOG(PR_LOG_DEBUG, "Media start time is %lld", mStartTime); + return v; +} + +void MediaDecoderStateMachine::UpdateReadyState() { + AssertCurrentThreadInMonitor(); + + MediaDecoderOwner::NextFrameStatus nextFrameStatus = GetNextFrameStatus(); + if (nextFrameStatus == mLastFrameStatus) { + return; + } + mLastFrameStatus = nextFrameStatus; + + /* This is a bit tricky. MediaDecoder::UpdateReadyStateForData will run on + * the main thread and re-evaluate GetNextFrameStatus there, passing it to + * HTMLMediaElement::UpdateReadyStateForData. It doesn't use the value of + * GetNextFrameStatus we computed here, because what we're computing here + * could be stale by the time MediaDecoder::UpdateReadyStateForData runs. + * We only compute GetNextFrameStatus here to avoid posting runnables to the main + * thread unnecessarily. + */ + nsCOMPtr event; + event = NS_NewRunnableMethod(mDecoder, &MediaDecoder::UpdateReadyStateForData); + NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL); +} + +bool MediaDecoderStateMachine::JustExitedQuickBuffering() +{ + return !mDecodeStartTime.IsNull() && + mQuickBuffering && + (TimeStamp::Now() - mDecodeStartTime) < TimeDuration::FromMicroseconds(QUICK_BUFFER_THRESHOLD_USECS); +} + +void MediaDecoderStateMachine::StartBuffering() +{ + AssertCurrentThreadInMonitor(); + + if (mState != DECODER_STATE_DECODING) { + // We only move into BUFFERING state if we're actually decoding. + // If we're currently doing something else, we don't need to buffer, + // and more importantly, we shouldn't overwrite mState to interrupt + // the current operation, as that could leave us in an inconsistent + // state! + return; + } + + if (IsPlaying()) { + StopPlayback(); + } + + TimeDuration decodeDuration = TimeStamp::Now() - mDecodeStartTime; + // Go into quick buffering mode provided we've not just left buffering using + // a "quick exit". This stops us flip-flopping between playing and buffering + // when the download speed is similar to the decode speed. + mQuickBuffering = + !JustExitedQuickBuffering() && + decodeDuration < UsecsToDuration(QUICK_BUFFER_THRESHOLD_USECS); + mBufferingStart = TimeStamp::Now(); + + // 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(); + mState = DECODER_STATE_BUFFERING; + DECODER_LOG(PR_LOG_DEBUG, "Changed state from DECODING to BUFFERING, decoded for %.3lfs", + decodeDuration.ToSeconds()); +#ifdef PR_LOGGING + MediaDecoder::Statistics stats = mDecoder->GetStatistics(); + DECODER_LOG(PR_LOG_DEBUG, "Playback rate: %.1lfKB/s%s download rate: %.1lfKB/s%s", + stats.mPlaybackRate/1024, stats.mPlaybackRateReliable ? "" : " (unreliable)", + stats.mDownloadRate/1024, stats.mDownloadRateReliable ? "" : " (unreliable)"); +#endif +} + +nsresult MediaDecoderStateMachine::GetBuffered(dom::TimeRanges* aBuffered) { + MediaResource* resource = mDecoder->GetResource(); + NS_ENSURE_TRUE(resource, NS_ERROR_FAILURE); + resource->Pin(); + nsresult res = mReader->GetBuffered(aBuffered, mStartTime); + resource->Unpin(); + return res; +} + +nsresult MediaDecoderStateMachine::CallRunStateMachine() +{ + AssertCurrentThreadInMonitor(); + NS_ASSERTION(OnStateMachineThread(), "Should be on state machine thread."); + + // If audio is being captured, stop the audio thread if it's running + if (mAudioCaptured) { + StopAudioThread(); + } + + MOZ_ASSERT(!mInRunningStateMachine, "State machine cycles must run in sequence!"); + mTimeout = TimeStamp(); + mInRunningStateMachine = true; + nsresult res = RunStateMachine(); + mInRunningStateMachine = false; + return res; +} + +nsresult MediaDecoderStateMachine::TimeoutExpired(int aTimerId) +{ + ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); + NS_ASSERTION(OnStateMachineThread(), "Must be on state machine thread"); + mTimer->Cancel(); + if (mTimerId == aTimerId) { + return CallRunStateMachine(); + } else { + return NS_OK; + } +} + +void MediaDecoderStateMachine::ScheduleStateMachineWithLockAndWakeDecoder() { + ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); + DispatchAudioDecodeTaskIfNeeded(); + DispatchVideoDecodeTaskIfNeeded(); +} + +class TimerEvent : public nsITimerCallback, public nsRunnable { + NS_DECL_THREADSAFE_ISUPPORTS +public: + TimerEvent(MediaDecoderStateMachine* aStateMachine, int aTimerId) + : mStateMachine(aStateMachine), mTimerId(aTimerId) {} + + NS_IMETHOD Run() MOZ_OVERRIDE { + return mStateMachine->TimeoutExpired(mTimerId); + } + + NS_IMETHOD Notify(nsITimer* aTimer) { + return mStateMachine->TimeoutExpired(mTimerId); + } +private: + const nsRefPtr mStateMachine; + int mTimerId; +}; + +NS_IMPL_ISUPPORTS(TimerEvent, nsITimerCallback, nsIRunnable); + +nsresult MediaDecoderStateMachine::ScheduleStateMachine(int64_t aUsecs) { + AssertCurrentThreadInMonitor(); + NS_ABORT_IF_FALSE(GetStateMachineThread(), + "Must have a state machine thread to schedule"); + + if (mState == DECODER_STATE_SHUTDOWN) { + return NS_ERROR_FAILURE; + } + aUsecs = std::max(aUsecs, 0); + + TimeStamp timeout = TimeStamp::Now() + UsecsToDuration(aUsecs); + if (!mTimeout.IsNull() && 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; + } + + uint32_t ms = static_cast((aUsecs / USECS_PER_MS) & 0xFFFFFFFF); + if (mRealTime && ms > 40) { + ms = 40; + } + + // Don't cancel the timer here for this function will be called from + // different threads. + + nsresult rv = NS_ERROR_FAILURE; + nsRefPtr event = new TimerEvent(this, mTimerId+1); + + if (ms == 0) { + // Dispatch a runnable to the state machine thread when delay is 0. + // It will has less latency than dispatching a runnable to the state + // machine thread which will then schedule a zero-delay timer. + rv = GetStateMachineThread()->Dispatch(event, NS_DISPATCH_NORMAL); + } else if (OnStateMachineThread()) { + rv = mTimer->InitWithCallback(event, ms, nsITimer::TYPE_ONE_SHOT); + } else { + MOZ_ASSERT(false, "non-zero delay timer should be only scheduled in state machine thread"); + } + + if (NS_SUCCEEDED(rv)) { + mTimeout = timeout; + ++mTimerId; + } else { + NS_WARNING("Failed to schedule state machine"); + } + + return rv; +} + +bool MediaDecoderStateMachine::OnDecodeThread() const +{ + return mDecodeTaskQueue->IsCurrentThreadIn(); +} + +bool MediaDecoderStateMachine::OnStateMachineThread() const +{ + bool rv = false; + mStateMachineThreadPool->IsOnCurrentThread(&rv); + return rv; +} + +nsIEventTarget* MediaDecoderStateMachine::GetStateMachineThread() +{ + return mStateMachineThreadPool->GetEventTarget(); +} + +void MediaDecoderStateMachine::SetPlaybackRate(double aPlaybackRate) +{ + NS_ASSERTION(NS_IsMainThread(), "Should be on main thread."); + NS_ASSERTION(aPlaybackRate != 0, + "PlaybackRate == 0 should be handled before this function."); + ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); + + if (mPlaybackRate == aPlaybackRate) { + return; + } + + // Get position of the last time we changed the rate. + if (!HasAudio()) { + // mBasePosition is a position in the video stream, not an absolute time. + if (mState == DECODER_STATE_SEEKING) { + mBasePosition = mSeekTarget.mTime - mStartTime; + } else { + mBasePosition = GetVideoStreamPosition(); + } + mPlayDuration = mBasePosition; + mResetPlayStartTime = true; + mPlayStartTime = TimeStamp::Now(); + } + + mPlaybackRate = aPlaybackRate; +} + +void MediaDecoderStateMachine::SetPreservesPitch(bool aPreservesPitch) +{ + NS_ASSERTION(NS_IsMainThread(), "Should be on main thread."); + ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); + + mPreservesPitch = aPreservesPitch; +} + +void +MediaDecoderStateMachine::SetMinimizePrerollUntilPlaybackStarts() +{ + AssertCurrentThreadInMonitor(); + mMinimizePreroll = true; +} + +bool MediaDecoderStateMachine::IsShutdown() +{ + AssertCurrentThreadInMonitor(); + return GetState() == DECODER_STATE_SHUTDOWN; +} + +void MediaDecoderStateMachine::QueueMetadata(int64_t aPublishTime, + int aChannels, + int aRate, + bool aHasAudio, + bool aHasVideo, + MetadataTags* aTags) +{ + NS_ASSERTION(OnDecodeThread(), "Should be on decode thread."); + AssertCurrentThreadInMonitor(); + TimedMetadata* metadata = new TimedMetadata; + metadata->mPublishTime = aPublishTime; + metadata->mChannels = aChannels; + metadata->mRate = aRate; + metadata->mHasAudio = aHasAudio; + metadata->mHasVideo = aHasVideo; + metadata->mTags = aTags; + mMetadataManager.QueueMetadata(metadata); +} + +} // namespace mozilla + +// avoid redefined macro in unified build +#undef DECODER_LOG +#undef VERBOSE_LOG