/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* 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/. */ #include "DecodedStream.h" #include "AudioSegment.h" #include "MediaData.h" #include "MediaDecoderStateMachine.h" #include "MediaQueue.h" #include "MediaTrackGraph.h" #include "MediaTrackListener.h" #include "SharedBuffer.h" #include "VideoSegment.h" #include "VideoUtils.h" #include "mozilla/AbstractThread.h" #include "mozilla/CheckedInt.h" #include "mozilla/SyncRunnable.h" #include "mozilla/gfx/Point.h" #include "mozilla/StaticPrefs_dom.h" #include "nsProxyRelease.h" namespace mozilla { using media::NullableTimeUnit; using media::TimeUnit; extern LazyLogModule gMediaDecoderLog; #define LOG_DS(type, fmt, ...) \ MOZ_LOG(gMediaDecoderLog, type, \ ("DecodedStream=%p " fmt, this, ##__VA_ARGS__)) /* * A container class to make it easier to pass the playback info all the * way to DecodedStreamGraphListener from DecodedStream. */ struct PlaybackInfoInit { TimeUnit mStartTime; MediaInfo mInfo; }; class DecodedStreamGraphListener; class DecodedStreamTrackListener : public MediaTrackListener { public: DecodedStreamTrackListener(DecodedStreamGraphListener* aGraphListener, SourceMediaTrack* aTrack); void NotifyOutput(MediaTrackGraph* aGraph, TrackTime aCurrentTrackTime) override; void NotifyEnded(MediaTrackGraph* aGraph) override; private: const RefPtr mGraphListener; const RefPtr mTrack; }; class DecodedStreamGraphListener { NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DecodedStreamGraphListener) public: DecodedStreamGraphListener( SourceMediaTrack* aAudioTrack, MozPromiseHolder&& aAudioEndedHolder, SourceMediaTrack* aVideoTrack, MozPromiseHolder&& aVideoEndedHolder) : mAudioTrackListener( aAudioTrack ? MakeRefPtr(this, aAudioTrack) : nullptr), mAudioEndedHolder(std::move(aAudioEndedHolder)), mVideoTrackListener( aVideoTrack ? MakeRefPtr(this, aVideoTrack) : nullptr), mVideoEndedHolder(std::move(aVideoEndedHolder)), mAudioTrack(aAudioTrack), mVideoTrack(aVideoTrack) { MOZ_ASSERT(NS_IsMainThread()); if (mAudioTrackListener) { mAudioTrack->AddListener(mAudioTrackListener); } else { mAudioEnded = true; mAudioEndedHolder.ResolveIfExists(true, __func__); } if (mVideoTrackListener) { mVideoTrack->AddListener(mVideoTrackListener); } else { mVideoEnded = true; mVideoEndedHolder.ResolveIfExists(true, __func__); } } void NotifyOutput(SourceMediaTrack* aTrack, TrackTime aCurrentTrackTime) { if (aTrack == mAudioTrack) { if (aCurrentTrackTime >= mAudioEnd) { mAudioTrack->End(); } } else if (aTrack == mVideoTrack) { if (aCurrentTrackTime >= mVideoEnd) { mVideoTrack->End(); } } else { MOZ_CRASH("Unexpected source track"); } if (aTrack != mAudioTrack && mAudioTrack && !mAudioEnded) { // Only audio playout drives the clock forward, if present and live. return; } MOZ_ASSERT_IF(aTrack == mAudioTrack, !mAudioEnded); MOZ_ASSERT_IF(aTrack == mVideoTrack, !mVideoEnded); mOnOutput.Notify(aTrack->TrackTimeToMicroseconds(aCurrentTrackTime)); } void NotifyEnded(SourceMediaTrack* aTrack) { if (aTrack == mAudioTrack) { mAudioEnded = true; } else if (aTrack == mVideoTrack) { mVideoEnded = true; } else { MOZ_CRASH("Unexpected source track"); } aTrack->Graph()->DispatchToMainThreadStableState( NewRunnableMethod>( "DecodedStreamGraphListener::DoNotifyTrackEnded", this, &DecodedStreamGraphListener::DoNotifyTrackEnded, aTrack)); } /** * Tell the graph listener to end the track sourced by the given track after * it has seen at least aEnd worth of output reported as processed by the * graph. * * A TrackTime of TRACK_TIME_MAX indicates that the track has no end and is * the default. * * This method of ending tracks is needed because the MediaTrackGraph * processes ended tracks (through SourceMediaTrack::EndTrack) at the * beginning of an iteration, but waits until the end of the iteration to * process any ControlMessages. When such a ControlMessage is a listener that * is to be added to a track that has ended in its very first iteration, the * track ends before the listener tracking this ending is added. This can lead * to a MediaStreamTrack ending on main thread (it uses another listener) * before the listeners to render the track get added, potentially meaning a * media element doesn't progress before reaching the end although data was * available. * * Callable from any thread. */ void EndTrackAt(SourceMediaTrack* aTrack, TrackTime aEnd) { if (aTrack == mAudioTrack) { mAudioEnd = aEnd; } else if (aTrack == mVideoTrack) { mVideoEnd = aEnd; } else { MOZ_CRASH("Unexpected source track"); } } void DoNotifyTrackEnded(SourceMediaTrack* aTrack) { MOZ_ASSERT(NS_IsMainThread()); if (aTrack == mAudioTrack) { mAudioEndedHolder.ResolveIfExists(true, __func__); } else if (aTrack == mVideoTrack) { mVideoEndedHolder.ResolveIfExists(true, __func__); } else { MOZ_CRASH("Unexpected source track"); } } void Forget() { MOZ_ASSERT(NS_IsMainThread()); if (mAudioTrackListener && !mAudioTrack->IsDestroyed()) { mAudioTrack->End(); mAudioTrack->RemoveListener(mAudioTrackListener); } mAudioTrackListener = nullptr; mAudioEndedHolder.ResolveIfExists(false, __func__); if (mVideoTrackListener && !mVideoTrack->IsDestroyed()) { mVideoTrack->End(); mVideoTrack->RemoveListener(mVideoTrackListener); } mVideoTrackListener = nullptr; mVideoEndedHolder.ResolveIfExists(false, __func__); } MediaEventSource& OnOutput() { return mOnOutput; } private: ~DecodedStreamGraphListener() { MOZ_ASSERT(mAudioEndedHolder.IsEmpty()); MOZ_ASSERT(mVideoEndedHolder.IsEmpty()); } MediaEventProducer mOnOutput; // Main thread only. RefPtr mAudioTrackListener; MozPromiseHolder mAudioEndedHolder; RefPtr mVideoTrackListener; MozPromiseHolder mVideoEndedHolder; // Graph thread only. bool mAudioEnded = false; bool mVideoEnded = false; // Any thread. const RefPtr mAudioTrack; const RefPtr mVideoTrack; Atomic mAudioEnd{TRACK_TIME_MAX}; Atomic mVideoEnd{TRACK_TIME_MAX}; }; DecodedStreamTrackListener::DecodedStreamTrackListener( DecodedStreamGraphListener* aGraphListener, SourceMediaTrack* aTrack) : mGraphListener(aGraphListener), mTrack(aTrack) {} void DecodedStreamTrackListener::NotifyOutput(MediaTrackGraph* aGraph, TrackTime aCurrentTrackTime) { mGraphListener->NotifyOutput(mTrack, aCurrentTrackTime); } void DecodedStreamTrackListener::NotifyEnded(MediaTrackGraph* aGraph) { mGraphListener->NotifyEnded(mTrack); } /** * All MediaStream-related data is protected by the decoder's monitor. We have * at most one DecodedStreamData per MediaDecoder. XXX Its tracks are used as * inputs for all output tracks created by OutputStreamManager after calls to * captureStream/UntilEnded. Seeking creates new source tracks, as does * replaying after the input as ended. In the latter case, the new sources are * not connected to tracks created by captureStreamUntilEnded. */ class DecodedStreamData final { public: DecodedStreamData( PlaybackInfoInit&& aInit, MediaTrackGraph* aGraph, RefPtr aAudioOutputTrack, RefPtr aVideoOutputTrack, MozPromiseHolder&& aAudioEndedPromise, MozPromiseHolder&& aVideoEndedPromise); ~DecodedStreamData(); MediaEventSource& OnOutput(); void Forget(); void GetDebugInfo(dom::DecodedStreamDataDebugInfo& aInfo); void WriteVideoToSegment(layers::Image* aImage, const TimeUnit& aStart, const TimeUnit& aEnd, const gfx::IntSize& aIntrinsicSize, const TimeStamp& aTimeStamp, VideoSegment* aOutput, const PrincipalHandle& aPrincipalHandle); /* The following group of fields are protected by the decoder's monitor * and can be read or written on any thread. */ // Count of audio frames written to the track int64_t mAudioFramesWritten; // Count of video frames written to the track in the track's rate TrackTime mVideoTrackWritten; // Count of audio frames written to the track in the track's rate TrackTime mAudioTrackWritten; // mNextAudioTime is the end timestamp for the last packet sent to the track. // Therefore audio packets starting at or after this time need to be copied // to the output track. TimeUnit mNextAudioTime; // mLastVideoStartTime is the start timestamp for the last packet sent to the // track. Therefore video packets starting after this time need to be copied // to the output track. NullableTimeUnit mLastVideoStartTime; // mLastVideoEndTime is the end timestamp for the last packet sent to the // track. It is used to adjust durations of chunks sent to the output track // when there are overlaps in VideoData. NullableTimeUnit mLastVideoEndTime; // The timestamp of the last frame, so we can ensure time never goes // backwards. TimeStamp mLastVideoTimeStamp; // The last video image sent to the track. Useful if we need to replicate // the image. RefPtr mLastVideoImage; gfx::IntSize mLastVideoImageDisplaySize; bool mHaveSentFinishAudio; bool mHaveSentFinishVideo; const RefPtr mAudioTrack; const RefPtr mVideoTrack; const RefPtr mAudioOutputTrack; const RefPtr mVideoOutputTrack; const RefPtr mAudioPort; const RefPtr mVideoPort; const RefPtr mAudioEndedPromise; const RefPtr mVideoEndedPromise; const RefPtr mListener; }; DecodedStreamData::DecodedStreamData( PlaybackInfoInit&& aInit, MediaTrackGraph* aGraph, RefPtr aAudioOutputTrack, RefPtr aVideoOutputTrack, MozPromiseHolder&& aAudioEndedPromise, MozPromiseHolder&& aVideoEndedPromise) : mAudioFramesWritten(0), mVideoTrackWritten(0), mAudioTrackWritten(0), mNextAudioTime(aInit.mStartTime), mHaveSentFinishAudio(false), mHaveSentFinishVideo(false), mAudioTrack(aInit.mInfo.HasAudio() ? aGraph->CreateSourceTrack(MediaSegment::AUDIO) : nullptr), mVideoTrack(aInit.mInfo.HasVideo() ? aGraph->CreateSourceTrack(MediaSegment::VIDEO) : nullptr), mAudioOutputTrack(std::move(aAudioOutputTrack)), mVideoOutputTrack(std::move(aVideoOutputTrack)), mAudioPort((mAudioOutputTrack && mAudioTrack) ? mAudioOutputTrack->AllocateInputPort(mAudioTrack) : nullptr), mVideoPort((mVideoOutputTrack && mVideoTrack) ? mVideoOutputTrack->AllocateInputPort(mVideoTrack) : nullptr), mAudioEndedPromise(aAudioEndedPromise.Ensure(__func__)), mVideoEndedPromise(aVideoEndedPromise.Ensure(__func__)), // DecodedStreamGraphListener will resolve these promises. mListener(MakeRefPtr( mAudioTrack, std::move(aAudioEndedPromise), mVideoTrack, std::move(aVideoEndedPromise))) { MOZ_ASSERT(NS_IsMainThread()); if (mAudioTrack) { mAudioTrack->SetAppendDataSourceRate(aInit.mInfo.mAudio.mRate); } } DecodedStreamData::~DecodedStreamData() { MOZ_ASSERT(NS_IsMainThread()); if (mAudioTrack) { mAudioTrack->Destroy(); } if (mVideoTrack) { mVideoTrack->Destroy(); } if (mAudioPort) { mAudioPort->Destroy(); } if (mVideoPort) { mVideoPort->Destroy(); } } MediaEventSource& DecodedStreamData::OnOutput() { return mListener->OnOutput(); } void DecodedStreamData::Forget() { mListener->Forget(); } void DecodedStreamData::GetDebugInfo(dom::DecodedStreamDataDebugInfo& aInfo) { CopyUTF8toUTF16(nsPrintfCString("%p", this), aInfo.mInstance); aInfo.mAudioFramesWritten = mAudioFramesWritten; aInfo.mStreamAudioWritten = mAudioTrackWritten; aInfo.mNextAudioTime = mNextAudioTime.ToMicroseconds(); aInfo.mLastVideoStartTime = mLastVideoStartTime.valueOr(TimeUnit::FromMicroseconds(-1)) .ToMicroseconds(); aInfo.mLastVideoEndTime = mLastVideoEndTime.valueOr(TimeUnit::FromMicroseconds(-1)) .ToMicroseconds(); aInfo.mHaveSentFinishAudio = mHaveSentFinishAudio; aInfo.mHaveSentFinishVideo = mHaveSentFinishVideo; } DecodedStream::DecodedStream( MediaDecoderStateMachine* aStateMachine, CopyableTArray> aOutputTracks, double aVolume, double aPlaybackRate, bool aPreservesPitch, MediaQueue& aAudioQueue, MediaQueue& aVideoQueue) : mOwnerThread(aStateMachine->OwnerThread()), mWatchManager(this, mOwnerThread), mPlaying(false, "DecodedStream::mPlaying"), mPrincipalHandle(aStateMachine->OwnerThread(), PRINCIPAL_HANDLE_NONE, "DecodedStream::mPrincipalHandle (Mirror)"), mCanonicalOutputPrincipal(aStateMachine->CanonicalOutputPrincipal()), mOutputTracks(std::move(aOutputTracks)), mVolume(aVolume), mPlaybackRate(aPlaybackRate), mPreservesPitch(aPreservesPitch), mAudioQueue(aAudioQueue), mVideoQueue(aVideoQueue) {} DecodedStream::~DecodedStream() { MOZ_ASSERT(mStartTime.isNothing(), "playback should've ended."); } RefPtr DecodedStream::OnEnded(TrackType aType) { AssertOwnerThread(); MOZ_ASSERT(mStartTime.isSome()); if (aType == TrackInfo::kAudioTrack && mInfo.HasAudio()) { return mAudioEndedPromise; } else if (aType == TrackInfo::kVideoTrack && mInfo.HasVideo()) { return mVideoEndedPromise; } return nullptr; } nsresult DecodedStream::Start(const TimeUnit& aStartTime, const MediaInfo& aInfo) { AssertOwnerThread(); MOZ_ASSERT(mStartTime.isNothing(), "playback already started."); MOZ_DIAGNOSTIC_ASSERT(!mOutputTracks.IsEmpty()); LOG_DS(LogLevel::Debug, "Start() mStartTime=%" PRId64, aStartTime.ToMicroseconds()); mStartTime.emplace(aStartTime); mLastOutputTime = TimeUnit::Zero(); mInfo = aInfo; mPlaying = true; mPrincipalHandle.Connect(mCanonicalOutputPrincipal); mWatchManager.Watch(mPlaying, &DecodedStream::PlayingChanged); mAudibilityMonitor.emplace( mInfo.mAudio.mRate, StaticPrefs::dom_media_silence_duration_for_audibility()); ConnectListener(); class R : public Runnable { public: R(PlaybackInfoInit&& aInit, nsTArray> aOutputTracks, MozPromiseHolder&& aAudioEndedPromise, MozPromiseHolder&& aVideoEndedPromise) : Runnable("CreateDecodedStreamData"), mInit(std::move(aInit)), mOutputTracks(std::move(aOutputTracks)), mAudioEndedPromise(std::move(aAudioEndedPromise)), mVideoEndedPromise(std::move(aVideoEndedPromise)) {} NS_IMETHOD Run() override { MOZ_ASSERT(NS_IsMainThread()); RefPtr audioOutputTrack; RefPtr videoOutputTrack; for (const auto& track : mOutputTracks) { if (track->mType == MediaSegment::AUDIO) { MOZ_DIAGNOSTIC_ASSERT( !audioOutputTrack, "We only support capturing to one output track per kind"); audioOutputTrack = track; } else if (track->mType == MediaSegment::VIDEO) { MOZ_DIAGNOSTIC_ASSERT( !videoOutputTrack, "We only support capturing to one output track per kind"); videoOutputTrack = track; } else { MOZ_CRASH("Unknown media type"); } } if ((!audioOutputTrack && !videoOutputTrack) || (audioOutputTrack && audioOutputTrack->IsDestroyed()) || (videoOutputTrack && videoOutputTrack->IsDestroyed())) { // No output tracks yet, or they're going away. Halt playback by not // creating DecodedStreamData. MDSM will try again with a new // DecodedStream sink when tracks are available. return NS_OK; } mData = MakeUnique( std::move(mInit), mOutputTracks[0]->Graph(), std::move(audioOutputTrack), std::move(videoOutputTrack), std::move(mAudioEndedPromise), std::move(mVideoEndedPromise)); return NS_OK; } UniquePtr ReleaseData() { return std::move(mData); } private: PlaybackInfoInit mInit; const nsTArray> mOutputTracks; MozPromiseHolder mAudioEndedPromise; MozPromiseHolder mVideoEndedPromise; UniquePtr mData; }; MozPromiseHolder audioEndedHolder; MozPromiseHolder videoEndedHolder; PlaybackInfoInit init{aStartTime, aInfo}; nsCOMPtr r = new R(std::move(init), mOutputTracks.Clone(), std::move(audioEndedHolder), std::move(videoEndedHolder)); SyncRunnable::DispatchToThread(GetMainThreadSerialEventTarget(), r); mData = static_cast(r.get())->ReleaseData(); if (mData) { mAudioEndedPromise = mData->mAudioEndedPromise; mVideoEndedPromise = mData->mVideoEndedPromise; mOutputListener = mData->OnOutput().Connect(mOwnerThread, this, &DecodedStream::NotifyOutput); if (mData->mAudioTrack) { mData->mAudioTrack->SetVolume(static_cast(mVolume)); } SendData(); } return NS_OK; } void DecodedStream::Stop() { AssertOwnerThread(); MOZ_ASSERT(mStartTime.isSome(), "playback not started."); LOG_DS(LogLevel::Debug, "Stop()"); DisconnectListener(); ResetVideo(mPrincipalHandle); ResetAudio(); mStartTime.reset(); mAudioEndedPromise = nullptr; mVideoEndedPromise = nullptr; // Clear mData immediately when this playback session ends so we won't // send data to the wrong track in SendData() in next playback session. DestroyData(std::move(mData)); mPrincipalHandle.DisconnectIfConnected(); mWatchManager.Unwatch(mPlaying, &DecodedStream::PlayingChanged); mAudibilityMonitor.reset(); } bool DecodedStream::IsStarted() const { AssertOwnerThread(); return mStartTime.isSome(); } bool DecodedStream::IsPlaying() const { AssertOwnerThread(); return IsStarted() && mPlaying; } void DecodedStream::Shutdown() { AssertOwnerThread(); mPrincipalHandle.DisconnectIfConnected(); mWatchManager.Shutdown(); } void DecodedStream::DestroyData(UniquePtr&& aData) { AssertOwnerThread(); if (!aData) { return; } mOutputListener.Disconnect(); NS_DispatchToMainThread( NS_NewRunnableFunction("DecodedStream::DestroyData", [data = std::move(aData)]() { data->Forget(); })); } void DecodedStream::SetPlaying(bool aPlaying) { AssertOwnerThread(); // Resume/pause matters only when playback started. if (mStartTime.isNothing()) { return; } LOG_DS(LogLevel::Debug, "playing (%d) -> (%d)", mPlaying.Ref(), aPlaying); mPlaying = aPlaying; } void DecodedStream::SetVolume(double aVolume) { AssertOwnerThread(); mVolume = aVolume; if (mData && mData->mAudioTrack) { mData->mAudioTrack->SetVolume(static_cast(aVolume)); } } void DecodedStream::SetPlaybackRate(double aPlaybackRate) { AssertOwnerThread(); mPlaybackRate = aPlaybackRate; } void DecodedStream::SetPreservesPitch(bool aPreservesPitch) { AssertOwnerThread(); mPreservesPitch = aPreservesPitch; } double DecodedStream::PlaybackRate() const { AssertOwnerThread(); return mPlaybackRate; } static void SendStreamAudio(DecodedStreamData* aStream, const TimeUnit& aStartTime, AudioData* aData, AudioSegment* aOutput, uint32_t aRate, const PrincipalHandle& aPrincipalHandle) { // The amount of audio frames that is used to fuzz rounding errors. static const int64_t AUDIO_FUZZ_FRAMES = 1; MOZ_ASSERT(aData); AudioData* audio = aData; // This logic has to mimic AudioSink closely to make sure we write // the exact same silences CheckedInt64 audioWrittenOffset = aStream->mAudioFramesWritten + TimeUnitToFrames(aStartTime, aRate); CheckedInt64 frameOffset = TimeUnitToFrames(audio->mTime, aRate); if (!audioWrittenOffset.isValid() || !frameOffset.isValid() || // ignore packet that we've already processed audio->GetEndTime() <= aStream->mNextAudioTime) { return; } if (audioWrittenOffset.value() + AUDIO_FUZZ_FRAMES < frameOffset.value()) { int64_t silentFrames = frameOffset.value() - audioWrittenOffset.value(); // Write silence to catch up AudioSegment silence; silence.InsertNullDataAtStart(silentFrames); aStream->mAudioFramesWritten += silentFrames; audioWrittenOffset += silentFrames; aOutput->AppendFrom(&silence); } // Always write the whole sample without truncation to be consistent with // DecodedAudioDataSink::PlayFromAudioQueue() audio->EnsureAudioBuffer(); RefPtr buffer = audio->mAudioBuffer; AudioDataValue* bufferData = static_cast(buffer->Data()); AutoTArray channels; for (uint32_t i = 0; i < audio->mChannels; ++i) { channels.AppendElement(bufferData + i * audio->Frames()); } aOutput->AppendFrames(buffer.forget(), channels, audio->Frames(), aPrincipalHandle); aStream->mAudioFramesWritten += audio->Frames(); aStream->mNextAudioTime = audio->GetEndTime(); } void DecodedStream::SendAudio(double aVolume, const PrincipalHandle& aPrincipalHandle) { AssertOwnerThread(); if (!mInfo.HasAudio()) { return; } if (mData->mHaveSentFinishAudio) { return; } AudioSegment output; uint32_t rate = mInfo.mAudio.mRate; AutoTArray, 10> audio; // It's OK to hold references to the AudioData because AudioData // is ref-counted. mAudioQueue.GetElementsAfter(mData->mNextAudioTime, &audio); for (uint32_t i = 0; i < audio.Length(); ++i) { LOG_DS(LogLevel::Verbose, "Queueing audio [%" PRId64 ",%" PRId64 "]", audio[i]->mTime.ToMicroseconds(), audio[i]->GetEndTime().ToMicroseconds()); CheckIsDataAudible(audio[i]); SendStreamAudio(mData.get(), mStartTime.ref(), audio[i], &output, rate, aPrincipalHandle); } // |mNextAudioTime| is updated as we process each audio sample in // SendStreamAudio(). if (output.GetDuration() > 0) { mData->mAudioTrackWritten += mData->mAudioTrack->AppendData(&output); } if (mAudioQueue.IsFinished() && !mData->mHaveSentFinishAudio) { mData->mListener->EndTrackAt(mData->mAudioTrack, mData->mAudioTrackWritten); mData->mHaveSentFinishAudio = true; } } void DecodedStream::CheckIsDataAudible(const AudioData* aData) { MOZ_ASSERT(aData); mAudibilityMonitor->Process(aData); bool isAudible = mAudibilityMonitor->RecentlyAudible(); if (isAudible != mIsAudioDataAudible) { mIsAudioDataAudible = isAudible; mAudibleEvent.Notify(mIsAudioDataAudible); } } void DecodedStreamData::WriteVideoToSegment( layers::Image* aImage, const TimeUnit& aStart, const TimeUnit& aEnd, const gfx::IntSize& aIntrinsicSize, const TimeStamp& aTimeStamp, VideoSegment* aOutput, const PrincipalHandle& aPrincipalHandle) { RefPtr image = aImage; auto end = mVideoTrack->MicrosecondsToTrackTimeRoundDown(aEnd.ToMicroseconds()); auto start = mVideoTrack->MicrosecondsToTrackTimeRoundDown(aStart.ToMicroseconds()); aOutput->AppendFrame(image.forget(), aIntrinsicSize, aPrincipalHandle, false, aTimeStamp); // Extend this so we get accurate durations for all frames. // Because this track is pushed, we need durations so the graph can track // when playout of the track has finished. aOutput->ExtendLastFrameBy(end - start); mLastVideoStartTime = Some(aStart); mLastVideoEndTime = Some(aEnd); mLastVideoTimeStamp = aTimeStamp; } static bool ZeroDurationAtLastChunk(VideoSegment& aInput) { // Get the last video frame's start time in VideoSegment aInput. // If the start time is equal to the duration of aInput, means the last video // frame's duration is zero. TrackTime lastVideoStratTime; aInput.GetLastFrame(&lastVideoStratTime); return lastVideoStratTime == aInput.GetDuration(); } void DecodedStream::ResetAudio() { AssertOwnerThread(); if (!mData) { return; } if (!mInfo.HasAudio()) { return; } TrackTime cleared = mData->mAudioTrack->ClearFutureData(); mData->mAudioTrackWritten -= cleared; if (const RefPtr& v = mAudioQueue.PeekFront()) { mData->mNextAudioTime = v->mTime; } if (mData->mHaveSentFinishAudio && cleared > 0) { mData->mHaveSentFinishAudio = false; mData->mListener->EndTrackAt(mData->mAudioTrack, TRACK_TIME_MAX); } } void DecodedStream::ResetVideo(const PrincipalHandle& aPrincipalHandle) { AssertOwnerThread(); if (!mData) { return; } if (!mInfo.HasVideo()) { return; } TrackTime cleared = mData->mVideoTrack->ClearFutureData(); mData->mVideoTrackWritten -= cleared; if (mData->mHaveSentFinishVideo && cleared > 0) { mData->mHaveSentFinishVideo = false; mData->mListener->EndTrackAt(mData->mVideoTrack, TRACK_TIME_MAX); } VideoSegment resetter; TimeStamp currentTime; TimeUnit currentPosition = GetPosition(¤tTime); // Giving direct consumers a frame (really *any* frame, so in this case: // nullptr) at an earlier time than the previous, will signal to that consumer // to discard any frames ahead in time of the new frame. To be honest, this is // an ugly hack because the direct listeners of the MediaTrackGraph do not // have an API that supports clearing the future frames. ImageContainer and // VideoFrameContainer do though, and we will need to move to a similar API // for video tracks as part of bug 1493618. resetter.AppendFrame(nullptr, mData->mLastVideoImageDisplaySize, aPrincipalHandle, false, currentTime); mData->mVideoTrack->AppendData(&resetter); // Consumer buffers have been reset. We now set the next time to the start // time of the current frame, so that it can be displayed again on resuming. if (RefPtr v = mVideoQueue.PeekFront()) { mData->mLastVideoStartTime = Some(v->mTime - TimeUnit::FromMicroseconds(1)); mData->mLastVideoEndTime = Some(v->mTime); } else { // There was no current frame in the queue. We set the next time to the // current time, so we at least don't resume starting in the future. mData->mLastVideoStartTime = Some(currentPosition - TimeUnit::FromMicroseconds(1)); mData->mLastVideoEndTime = Some(currentPosition); } mData->mLastVideoTimeStamp = currentTime; } void DecodedStream::SendVideo(const PrincipalHandle& aPrincipalHandle) { AssertOwnerThread(); if (!mInfo.HasVideo()) { return; } if (mData->mHaveSentFinishVideo) { return; } VideoSegment output; AutoTArray, 10> video; // It's OK to hold references to the VideoData because VideoData // is ref-counted. mVideoQueue.GetElementsAfter( mData->mLastVideoStartTime.valueOr(mStartTime.ref()), &video); TimeStamp currentTime; TimeUnit currentPosition = GetPosition(¤tTime); if (mData->mLastVideoTimeStamp.IsNull()) { mData->mLastVideoTimeStamp = currentTime; } for (uint32_t i = 0; i < video.Length(); ++i) { VideoData* v = video[i]; TimeUnit lastStart = mData->mLastVideoStartTime.valueOr( mStartTime.ref() - TimeUnit::FromMicroseconds(1)); TimeUnit lastEnd = mData->mLastVideoEndTime.valueOr(mStartTime.ref()); if (lastEnd < v->mTime) { // Write last video frame to catch up. mLastVideoImage can be null here // which is fine, it just means there's no video. // TODO: |mLastVideoImage| should come from the last image rendered // by the state machine. This will avoid the black frame when capture // happens in the middle of playback (especially in th middle of a // video frame). E.g. if we have a video frame that is 30 sec long // and capture happens at 15 sec, we'll have to append a black frame // that is 15 sec long. TimeStamp t = std::max(mData->mLastVideoTimeStamp, currentTime + (lastEnd - currentPosition).ToTimeDuration()); mData->WriteVideoToSegment(mData->mLastVideoImage, lastEnd, v->mTime, mData->mLastVideoImageDisplaySize, t, &output, aPrincipalHandle); lastEnd = v->mTime; } if (lastStart < v->mTime) { // This frame starts after the last frame's start. Note that this could be // before the last frame's end time for some videos. This only matters for // the track's lifetime in the MTG, as rendering is based on timestamps, // aka frame start times. TimeStamp t = std::max(mData->mLastVideoTimeStamp, currentTime + (lastEnd - currentPosition).ToTimeDuration()); TimeUnit end = std::max( v->GetEndTime(), lastEnd + TimeUnit::FromMicroseconds( mData->mVideoTrack->TrackTimeToMicroseconds(1) + 1)); mData->mLastVideoImage = v->mImage; mData->mLastVideoImageDisplaySize = v->mDisplay; mData->WriteVideoToSegment(v->mImage, lastEnd, end, v->mDisplay, t, &output, aPrincipalHandle); } } // Check the output is not empty. bool compensateEOS = false; bool forceBlack = false; if (output.GetLastFrame()) { compensateEOS = ZeroDurationAtLastChunk(output); } if (output.GetDuration() > 0) { mData->mVideoTrackWritten += mData->mVideoTrack->AppendData(&output); } if (mVideoQueue.IsFinished() && !mData->mHaveSentFinishVideo) { if (!mData->mLastVideoImage) { // We have video, but the video queue finished before we received any // frame. We insert a black frame to progress any consuming // HTMLMediaElement. This mirrors the behavior of VideoSink. // Force a frame - can be null compensateEOS = true; // Force frame to be black forceBlack = true; // Override the frame's size (will be 0x0 otherwise) mData->mLastVideoImageDisplaySize = mInfo.mVideo.mDisplay; } if (compensateEOS) { VideoSegment endSegment; // Calculate the deviation clock time from DecodedStream. // We round the nr of microseconds up, because WriteVideoToSegment // will round the conversion from microseconds to TrackTime down. auto deviation = TimeUnit::FromMicroseconds( mData->mVideoTrack->TrackTimeToMicroseconds(1) + 1); auto start = mData->mLastVideoEndTime.valueOr(mStartTime.ref()); mData->WriteVideoToSegment( mData->mLastVideoImage, start, start + deviation, mData->mLastVideoImageDisplaySize, currentTime + (start + deviation - currentPosition).ToTimeDuration(), &endSegment, aPrincipalHandle); MOZ_ASSERT(endSegment.GetDuration() > 0); if (forceBlack) { endSegment.ReplaceWithDisabled(); } mData->mVideoTrackWritten += mData->mVideoTrack->AppendData(&endSegment); } mData->mListener->EndTrackAt(mData->mVideoTrack, mData->mVideoTrackWritten); mData->mHaveSentFinishVideo = true; } } void DecodedStream::SendData() { AssertOwnerThread(); // Not yet created on the main thread. MDSM will try again later. if (!mData) { return; } if (!mPlaying) { return; } LOG_DS(LogLevel::Verbose, "SendData()"); SendAudio(mVolume, mPrincipalHandle); SendVideo(mPrincipalHandle); } TimeUnit DecodedStream::GetEndTime(TrackType aType) const { AssertOwnerThread(); if (aType == TrackInfo::kAudioTrack && mInfo.HasAudio() && mData) { auto t = mStartTime.ref() + FramesToTimeUnit(mData->mAudioFramesWritten, mInfo.mAudio.mRate); if (t.IsValid()) { return t; } } else if (aType == TrackInfo::kVideoTrack && mData) { return mData->mLastVideoEndTime.valueOr(mStartTime.ref()); } return TimeUnit::Zero(); } TimeUnit DecodedStream::GetPosition(TimeStamp* aTimeStamp) const { AssertOwnerThread(); // This is only called after MDSM starts playback. So mStartTime is // guaranteed to be something. MOZ_ASSERT(mStartTime.isSome()); if (aTimeStamp) { *aTimeStamp = TimeStamp::Now(); } return mStartTime.ref() + mLastOutputTime; } void DecodedStream::NotifyOutput(int64_t aTime) { AssertOwnerThread(); TimeUnit time = TimeUnit::FromMicroseconds(aTime); if (time == mLastOutputTime) { return; } MOZ_ASSERT(mLastOutputTime < time); mLastOutputTime = time; auto currentTime = GetPosition(); LOG_DS(LogLevel::Verbose, "time is now %" PRId64, currentTime.ToMicroseconds()); // Remove audio samples that have been played by MTG from the queue. RefPtr a = mAudioQueue.PeekFront(); for (; a && a->GetEndTime() <= currentTime;) { LOG_DS(LogLevel::Debug, "Dropping audio [%" PRId64 ",%" PRId64 "]", a->mTime.ToMicroseconds(), a->GetEndTime().ToMicroseconds()); RefPtr releaseMe = mAudioQueue.PopFront(); a = mAudioQueue.PeekFront(); } } void DecodedStream::PlayingChanged() { AssertOwnerThread(); if (!mPlaying) { // On seek or pause we discard future frames. ResetVideo(mPrincipalHandle); ResetAudio(); } } void DecodedStream::ConnectListener() { AssertOwnerThread(); mAudioPushListener = mAudioQueue.PushEvent().Connect( mOwnerThread, this, &DecodedStream::SendData); mAudioFinishListener = mAudioQueue.FinishEvent().Connect( mOwnerThread, this, &DecodedStream::SendData); mVideoPushListener = mVideoQueue.PushEvent().Connect( mOwnerThread, this, &DecodedStream::SendData); mVideoFinishListener = mVideoQueue.FinishEvent().Connect( mOwnerThread, this, &DecodedStream::SendData); mWatchManager.Watch(mPlaying, &DecodedStream::SendData); } void DecodedStream::DisconnectListener() { AssertOwnerThread(); mAudioPushListener.Disconnect(); mVideoPushListener.Disconnect(); mAudioFinishListener.Disconnect(); mVideoFinishListener.Disconnect(); mWatchManager.Unwatch(mPlaying, &DecodedStream::SendData); } void DecodedStream::GetDebugInfo(dom::MediaSinkDebugInfo& aInfo) { AssertOwnerThread(); int64_t startTime = mStartTime.isSome() ? mStartTime->ToMicroseconds() : -1; aInfo.mDecodedStream.mInstance = NS_ConvertUTF8toUTF16(nsPrintfCString("%p", this)); aInfo.mDecodedStream.mStartTime = startTime; aInfo.mDecodedStream.mLastOutputTime = mLastOutputTime.ToMicroseconds(); aInfo.mDecodedStream.mPlaying = mPlaying.Ref(); auto lastAudio = mAudioQueue.PeekBack(); aInfo.mDecodedStream.mLastAudio = lastAudio ? lastAudio->GetEndTime().ToMicroseconds() : -1; aInfo.mDecodedStream.mAudioQueueFinished = mAudioQueue.IsFinished(); aInfo.mDecodedStream.mAudioQueueSize = mAudioQueue.GetSize(); if (mData) { mData->GetDebugInfo(aInfo.mDecodedStream.mData); } } #undef LOG_DS } // namespace mozilla