/* -*- 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 "AudioDecoderInputTrack.h" #include "AudioSegment.h" #include "MediaData.h" #include "MediaDecoderStateMachine.h" #include "MediaQueue.h" #include "MediaTrackGraph.h" #include "MediaTrackListener.h" #include "SharedBuffer.h" #include "Tracing.h" #include "VideoSegment.h" #include "VideoUtils.h" #include "mozilla/AbstractThread.h" #include "mozilla/CheckedInt.h" #include "mozilla/ProfilerLabels.h" #include "mozilla/ProfilerMarkerTypes.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__)) #define PLAYBACK_PROFILER_MARKER(markerString) \ PROFILER_MARKER_TEXT(FUNCTION_SIGNATURE, MEDIA_PLAYBACK, {}, markerString) /* * 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 SourceVideoTrackListener : public MediaTrackListener { public: SourceVideoTrackListener(DecodedStreamGraphListener* aGraphListener, SourceMediaTrack* aVideoTrack, MediaTrack* aAudioTrack, nsISerialEventTarget* aDecoderThread); void NotifyOutput(MediaTrackGraph* aGraph, TrackTime aCurrentTrackTime) override; void NotifyEnded(MediaTrackGraph* aGraph) override; private: const RefPtr mGraphListener; const RefPtr mVideoTrack; const RefPtr mAudioTrack; const RefPtr mDecoderThread; TrackTime mLastVideoOutputTime = 0; }; class DecodedStreamGraphListener { NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DecodedStreamGraphListener) public: DecodedStreamGraphListener( nsISerialEventTarget* aDecoderThread, AudioDecoderInputTrack* aAudioTrack, MozPromiseHolder&& aAudioEndedHolder, SourceMediaTrack* aVideoTrack, MozPromiseHolder&& aVideoEndedHolder) : mDecoderThread(aDecoderThread), mVideoTrackListener( aVideoTrack ? MakeRefPtr( this, aVideoTrack, aAudioTrack, aDecoderThread) : nullptr), mAudioEndedHolder(std::move(aAudioEndedHolder)), mVideoEndedHolder(std::move(aVideoEndedHolder)), mAudioTrack(aAudioTrack), mVideoTrack(aVideoTrack) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mDecoderThread); if (mAudioTrack) { mOnAudioOutput = mAudioTrack->OnOutput().Connect( mDecoderThread, [self = RefPtr(this)](TrackTime aTime) { self->NotifyOutput(MediaSegment::AUDIO, aTime); }); mOnAudioEnd = mAudioTrack->OnEnd().Connect( mDecoderThread, [self = RefPtr(this)]() { self->NotifyEnded(MediaSegment::AUDIO); }); } else { mAudioEnded = true; mAudioEndedHolder.ResolveIfExists(true, __func__); } if (mVideoTrackListener) { mVideoTrack->AddListener(mVideoTrackListener); } else { mVideoEnded = true; mVideoEndedHolder.ResolveIfExists(true, __func__); } } void Close() { AssertOnDecoderThread(); if (mAudioTrack) { mAudioTrack->Close(); } if (mVideoTrack) { mVideoTrack->End(); } mAudioEndedHolder.ResolveIfExists(false, __func__); mVideoEndedHolder.ResolveIfExists(false, __func__); mOnAudioOutput.DisconnectIfExists(); mOnAudioEnd.DisconnectIfExists(); } void NotifyOutput(MediaSegment::Type aType, TrackTime aCurrentTrackTime) { AssertOnDecoderThread(); if (aType == MediaSegment::AUDIO) { mAudioOutputFrames = aCurrentTrackTime; } else if (aType == MediaSegment::VIDEO) { if (aCurrentTrackTime >= mVideoEndTime) { mVideoTrack->End(); } } else { MOZ_CRASH("Unexpected track type"); } MOZ_ASSERT_IF(aType == MediaSegment::AUDIO, !mAudioEnded); MOZ_ASSERT_IF(aType == MediaSegment::VIDEO, !mVideoEnded); // This situation would happen when playing audio in >1x playback rate, // because the audio output clock isn't align the graph time and would go // forward faster. Eg. playback rate=2, when the graph time passes 10s, the // audio clock time actually already goes forward 20s. After audio track // ended, video track would tirgger the clock, but the video time still // follows the graph time, which is smaller than the preivous audio clock // time and should be ignored. if (aCurrentTrackTime <= mLastOutputTime) { MOZ_ASSERT(aType == MediaSegment::VIDEO); return; } MOZ_ASSERT(aCurrentTrackTime > mLastOutputTime); mLastOutputTime = aCurrentTrackTime; // Only when audio track doesn't exists or has reached the end, video // track should drive the clock. MOZ_ASSERT_IF(aType == MediaSegment::VIDEO, mAudioEnded); const MediaTrack* track = aType == MediaSegment::VIDEO ? static_cast(mVideoTrack) : static_cast(mAudioTrack); mOnOutput.Notify(track->TrackTimeToMicroseconds(aCurrentTrackTime)); } void NotifyEnded(MediaSegment::Type aType) { AssertOnDecoderThread(); if (aType == MediaSegment::AUDIO) { MOZ_ASSERT(!mAudioEnded); mAudioEnded = true; mAudioEndedHolder.ResolveIfExists(true, __func__); } else if (aType == MediaSegment::VIDEO) { MOZ_ASSERT(!mVideoEnded); mVideoEnded = true; mVideoEndedHolder.ResolveIfExists(true, __func__); } else { MOZ_CRASH("Unexpected track type"); } } /** * 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. */ void EndVideoTrackAt(MediaTrack* aTrack, TrackTime aEnd) { AssertOnDecoderThread(); MOZ_DIAGNOSTIC_ASSERT(aTrack == mVideoTrack); mVideoEndTime = aEnd; } void Forget() { MOZ_ASSERT(NS_IsMainThread()); if (mVideoTrackListener && !mVideoTrack->IsDestroyed()) { mVideoTrack->RemoveListener(mVideoTrackListener); } mVideoTrackListener = nullptr; } TrackTime GetAudioFramesPlayed() { AssertOnDecoderThread(); return mAudioOutputFrames; } MediaEventSource& OnOutput() { return mOnOutput; } private: ~DecodedStreamGraphListener() { MOZ_ASSERT(mAudioEndedHolder.IsEmpty()); MOZ_ASSERT(mVideoEndedHolder.IsEmpty()); } inline void AssertOnDecoderThread() const { MOZ_ASSERT(mDecoderThread->IsOnCurrentThread()); } const RefPtr mDecoderThread; // Accessible on any thread, but only notify on the decoder thread. MediaEventProducer mOnOutput; RefPtr mVideoTrackListener; // These can be resolved on the main thread on creation if there is no // corresponding track, otherwise they are resolved on the decoder thread. MozPromiseHolder mAudioEndedHolder; MozPromiseHolder mVideoEndedHolder; // Decoder thread only. TrackTime mAudioOutputFrames = 0; TrackTime mLastOutputTime = 0; bool mAudioEnded = false; bool mVideoEnded = false; // Any thread. const RefPtr mAudioTrack; const RefPtr mVideoTrack; MediaEventListener mOnAudioOutput; MediaEventListener mOnAudioEnd; Atomic mVideoEndTime{TRACK_TIME_MAX}; }; SourceVideoTrackListener::SourceVideoTrackListener( DecodedStreamGraphListener* aGraphListener, SourceMediaTrack* aVideoTrack, MediaTrack* aAudioTrack, nsISerialEventTarget* aDecoderThread) : mGraphListener(aGraphListener), mVideoTrack(aVideoTrack), mAudioTrack(aAudioTrack), mDecoderThread(aDecoderThread) {} void SourceVideoTrackListener::NotifyOutput(MediaTrackGraph* aGraph, TrackTime aCurrentTrackTime) { aGraph->AssertOnGraphThreadOrNotRunning(); if (mAudioTrack && !mAudioTrack->Ended()) { // Only audio playout drives the clock forward, if present and live. return; } // The graph can iterate without time advancing, but the invariant is that // time can never go backwards. if (aCurrentTrackTime <= mLastVideoOutputTime) { MOZ_ASSERT(aCurrentTrackTime == mLastVideoOutputTime); return; } mLastVideoOutputTime = aCurrentTrackTime; mDecoderThread->Dispatch(NS_NewRunnableFunction( "SourceVideoTrackListener::NotifyOutput", [self = RefPtr(this), aCurrentTrackTime]() { self->mGraphListener->NotifyOutput(MediaSegment::VIDEO, aCurrentTrackTime); })); } void SourceVideoTrackListener::NotifyEnded(MediaTrackGraph* aGraph) { aGraph->AssertOnGraphThreadOrNotRunning(); mDecoderThread->Dispatch(NS_NewRunnableFunction( "SourceVideoTrackListener::NotifyEnded", [self = RefPtr(this)]() { self->mGraphListener->NotifyEnded(MediaSegment::VIDEO); })); } /** * 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, float aPlaybackRate, float aVolume, bool aPreservesPitch, nsISerialEventTarget* aDecoderThread); ~DecodedStreamData(); MediaEventSource& OnOutput(); // This is used to mark track as closed and should be called before Forget(). // Decoder thread only. void Close(); // After calling this function, the DecodedStreamData would be destroyed. // Main thread only. 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, double aPlaybackRate); /* 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; // 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, float aPlaybackRate, float aVolume, bool aPreservesPitch, nsISerialEventTarget* aDecoderThread) : mAudioFramesWritten(0), mVideoTrackWritten(0), mNextAudioTime(aInit.mStartTime), mHaveSentFinishAudio(false), mHaveSentFinishVideo(false), mAudioTrack(aInit.mInfo.HasAudio() ? AudioDecoderInputTrack::Create( aGraph, aDecoderThread, aInit.mInfo.mAudio, aPlaybackRate, aVolume, aPreservesPitch) : 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( aDecoderThread, mAudioTrack, std::move(aAudioEndedPromise), mVideoTrack, std::move(aVideoEndedPromise))) { MOZ_ASSERT(NS_IsMainThread()); } 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::Close() { mListener->Close(); } void DecodedStreamData::Forget() { mListener->Forget(); } void DecodedStreamData::GetDebugInfo(dom::DecodedStreamDataDebugInfo& aInfo) { CopyUTF8toUTF16(nsPrintfCString("%p", this), aInfo.mInstance); aInfo.mAudioFramesWritten = mAudioFramesWritten; aInfo.mStreamAudioWritten = mListener->GetAudioFramesPlayed(); 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, nsMainThreadPtrHandle aDummyTrack, CopyableTArray> aOutputTracks, double aVolume, double aPlaybackRate, bool aPreservesPitch, MediaQueue& aAudioQueue, MediaQueue& aVideoQueue, RefPtr aAudioDevice) : mOwnerThread(aStateMachine->OwnerThread()), mDummyTrack(std::move(aDummyTrack)), 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), mAudioDevice(std::move(aAudioDevice)) {} 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; } 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."); AUTO_PROFILER_LABEL(FUNCTION_SIGNATURE, MEDIA_PLAYBACK); if (profiler_thread_is_being_profiled_for_markers()) { nsPrintfCString markerString("StartTime=%" PRId64, aStartTime.ToMicroseconds()); PLAYBACK_PROFILER_MARKER(markerString); } 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, nsMainThreadPtrHandle aDummyTrack, nsTArray> aOutputTracks, MozPromiseHolder&& aAudioEndedPromise, MozPromiseHolder&& aVideoEndedPromise, float aPlaybackRate, float aVolume, bool aPreservesPitch, nsISerialEventTarget* aDecoderThread) : Runnable("CreateDecodedStreamData"), mInit(std::move(aInit)), mDummyTrack(std::move(aDummyTrack)), mOutputTracks(std::move(aOutputTracks)), mAudioEndedPromise(std::move(aAudioEndedPromise)), mVideoEndedPromise(std::move(aVideoEndedPromise)), mPlaybackRate(aPlaybackRate), mVolume(aVolume), mPreservesPitch(aPreservesPitch), mDecoderThread(aDecoderThread) {} 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 (!mDummyTrack) { // No dummy track - no graph. This could be intentional as the owning // media element needs access to the tracks on main thread to set up // forwarding of them before playback starts. MDSM will re-create // DecodedStream once a dummy track is available. This effectively halts // playback for this DecodedStream. return NS_OK; } if ((audioOutputTrack && audioOutputTrack->IsDestroyed()) || (videoOutputTrack && videoOutputTrack->IsDestroyed())) { // A track has been destroyed and we'll soon get re-created with a // proper one. This effectively halts playback for this DecodedStream. return NS_OK; } mData = MakeUnique( std::move(mInit), mDummyTrack->mTrack->Graph(), std::move(audioOutputTrack), std::move(videoOutputTrack), std::move(mAudioEndedPromise), std::move(mVideoEndedPromise), mPlaybackRate, mVolume, mPreservesPitch, mDecoderThread); return NS_OK; } UniquePtr ReleaseData() { return std::move(mData); } private: PlaybackInfoInit mInit; nsMainThreadPtrHandle mDummyTrack; const nsTArray> mOutputTracks; MozPromiseHolder mAudioEndedPromise; MozPromiseHolder mVideoEndedPromise; UniquePtr mData; const float mPlaybackRate; const float mVolume; const bool mPreservesPitch; const RefPtr mDecoderThread; }; MozPromiseHolder audioEndedHolder; MozPromiseHolder videoEndedHolder; PlaybackInfoInit init{aStartTime, aInfo}; nsCOMPtr r = new R(std::move(init), mDummyTrack, mOutputTracks.Clone(), std::move(audioEndedHolder), std::move(videoEndedHolder), static_cast(mPlaybackRate), static_cast(mVolume), mPreservesPitch, mOwnerThread); 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); SendData(); } return NS_OK; } void DecodedStream::Stop() { AssertOwnerThread(); MOZ_ASSERT(mStartTime.isSome(), "playback not started."); TRACE("DecodedStream::Stop"); 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; } TRACE("DecodedStream::DestroyData"); mOutputListener.Disconnect(); aData->Close(); 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; } if (profiler_thread_is_being_profiled_for_markers()) { nsPrintfCString markerString("Playing=%s", aPlaying ? "true" : "false"); PLAYBACK_PROFILER_MARKER(markerString); } LOG_DS(LogLevel::Debug, "playing (%d) -> (%d)", mPlaying.Ref(), aPlaying); mPlaying = aPlaying; } void DecodedStream::SetVolume(double aVolume) { AssertOwnerThread(); if (profiler_thread_is_being_profiled_for_markers()) { nsPrintfCString markerString("Volume=%f", aVolume); PLAYBACK_PROFILER_MARKER(markerString); } if (mVolume == aVolume) { return; } mVolume = aVolume; if (mData && mData->mAudioTrack) { mData->mAudioTrack->SetVolume(static_cast(aVolume)); } } void DecodedStream::SetPlaybackRate(double aPlaybackRate) { AssertOwnerThread(); if (profiler_thread_is_being_profiled_for_markers()) { nsPrintfCString markerString("PlaybackRate=%f", aPlaybackRate); PLAYBACK_PROFILER_MARKER(markerString); } if (mPlaybackRate == aPlaybackRate) { return; } mPlaybackRate = aPlaybackRate; if (mData && mData->mAudioTrack) { mData->mAudioTrack->SetPlaybackRate(static_cast(aPlaybackRate)); } } void DecodedStream::SetPreservesPitch(bool aPreservesPitch) { AssertOwnerThread(); if (profiler_thread_is_being_profiled_for_markers()) { nsPrintfCString markerString("PreservesPitch=%s", aPreservesPitch ? "true" : "false"); PLAYBACK_PROFILER_MARKER(markerString); } if (mPreservesPitch == aPreservesPitch) { return; } mPreservesPitch = aPreservesPitch; if (mData && mData->mAudioTrack) { mData->mAudioTrack->SetPreservesPitch(aPreservesPitch); } } double DecodedStream::PlaybackRate() const { AssertOwnerThread(); return mPlaybackRate; } void DecodedStream::SendAudio(const PrincipalHandle& aPrincipalHandle) { AssertOwnerThread(); if (!mInfo.HasAudio()) { return; } if (mData->mHaveSentFinishAudio) { return; } TRACE("DecodedStream::SendAudio"); // It's OK to hold references to the AudioData because AudioData // is ref-counted. AutoTArray, 10> audio; mAudioQueue.GetElementsAfter(mData->mNextAudioTime, &audio); // This will happen everytime when the media sink switches from `AudioSink` to // `DecodedStream`. If we don't insert the silence then the A/V will be out of // sync. RefPtr nextAudio = audio.IsEmpty() ? nullptr : audio[0]; if (RefPtr silence = CreateSilenceDataIfGapExists(nextAudio)) { LOG_DS(LogLevel::Verbose, "Detect a gap in audio, insert silence=%u", silence->Frames()); audio.InsertElementAt(0, silence); } // Append data which hasn't been sent to audio track before. mData->mAudioTrack->AppendData(audio, aPrincipalHandle); for (uint32_t i = 0; i < audio.Length(); ++i) { CheckIsDataAudible(audio[i]); mData->mNextAudioTime = audio[i]->GetEndTime(); mData->mAudioFramesWritten += audio[i]->Frames(); } if (mAudioQueue.IsFinished() && !mData->mHaveSentFinishAudio) { mData->mAudioTrack->NotifyEndOfStream(); mData->mHaveSentFinishAudio = true; } } already_AddRefed DecodedStream::CreateSilenceDataIfGapExists( RefPtr& aNextAudio) { AssertOwnerThread(); if (!aNextAudio) { return nullptr; } CheckedInt64 audioWrittenOffset = mData->mAudioFramesWritten + TimeUnitToFrames(*mStartTime, aNextAudio->mRate); CheckedInt64 frameOffset = TimeUnitToFrames(aNextAudio->mTime, aNextAudio->mRate); if (audioWrittenOffset.value() >= frameOffset.value()) { return nullptr; } // We've written less audio than our frame offset, return a silence data so we // have enough audio to be at the correct offset for our current frames. CheckedInt64 missingFrames = frameOffset - audioWrittenOffset; AlignedAudioBuffer silenceBuffer(missingFrames.value() * aNextAudio->mChannels); if (!silenceBuffer) { NS_WARNING("OOM in DecodedStream::CreateSilenceDataIfGapExists"); return nullptr; } auto duration = media::TimeUnit(missingFrames.value(), aNextAudio->mRate); if (!duration.IsValid()) { NS_WARNING("Int overflow in DecodedStream::CreateSilenceDataIfGapExists"); return nullptr; } RefPtr silenceData = new AudioData( aNextAudio->mOffset, aNextAudio->mTime, std::move(silenceBuffer), aNextAudio->mChannels, aNextAudio->mRate); MOZ_DIAGNOSTIC_ASSERT(duration == silenceData->mDuration, "must be equal"); return silenceData.forget(); } 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, double aPlaybackRate) { RefPtr image = aImage; 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. MOZ_ASSERT(aPlaybackRate > 0); TrackTime start = aStart.ToTicksAtRate(mVideoTrack->mSampleRate); TrackTime end = aEnd.ToTicksAtRate(mVideoTrack->mSampleRate); aOutput->ExtendLastFrameBy( static_cast((float)(end - start) / aPlaybackRate)); 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; } TRACE("DecodedStream::ResetAudio"); mData->mAudioTrack->ClearFutureData(); if (const RefPtr& v = mAudioQueue.PeekFront()) { mData->mNextAudioTime = v->mTime; mData->mHaveSentFinishAudio = false; } } void DecodedStream::ResetVideo(const PrincipalHandle& aPrincipalHandle) { AssertOwnerThread(); if (!mData) { return; } if (!mInfo.HasVideo()) { return; } TRACE("DecodedStream::ResetVideo"); TrackTime cleared = mData->mVideoTrack->ClearFutureData(); mData->mVideoTrackWritten -= cleared; if (mData->mHaveSentFinishVideo && cleared > 0) { mData->mHaveSentFinishVideo = false; mData->mListener->EndVideoTrackAt(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; } TRACE("DecodedStream::SendVideo"); 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, mPlaybackRate); 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, mPlaybackRate); } } // 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; LOG_DS(LogLevel::Debug, "No mLastVideoImage"); } if (compensateEOS) { VideoSegment endSegment; auto start = mData->mLastVideoEndTime.valueOr(mStartTime.ref()); mData->WriteVideoToSegment( mData->mLastVideoImage, start, start, mData->mLastVideoImageDisplaySize, currentTime + (start - currentPosition).ToTimeDuration(), &endSegment, aPrincipalHandle, mPlaybackRate); // ForwardedInputTrack drops zero duration frames, even at the end of // the track. Give the frame a minimum duration so that it is not // dropped. endSegment.ExtendLastFrameBy(1); LOG_DS(LogLevel::Debug, "compensateEOS: start %s, duration %" PRId64 ", mPlaybackRate %lf, sample rate %" PRId32, start.ToString().get(), endSegment.GetDuration(), mPlaybackRate, mData->mVideoTrack->mSampleRate); MOZ_ASSERT(endSegment.GetDuration() > 0); if (forceBlack) { endSegment.ReplaceWithDisabled(); } mData->mVideoTrackWritten += mData->mVideoTrack->AppendData(&endSegment); } mData->mListener->EndVideoTrackAt(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(mPrincipalHandle); SendVideo(mPrincipalHandle); } TimeUnit DecodedStream::GetEndTime(TrackType aType) const { AssertOwnerThread(); TRACE("DecodedStream::GetEndTime"); if (aType == TrackInfo::kAudioTrack && mInfo.HasAudio() && mData) { auto t = mStartTime.ref() + media::TimeUnit(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) { AssertOwnerThread(); TRACE("DecodedStream::GetPosition"); // 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(); if (profiler_thread_is_being_profiled_for_markers()) { nsPrintfCString markerString("OutputTime=%" PRId64, currentTime.ToMicroseconds()); PLAYBACK_PROFILER_MARKER(markerString); } 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(); TRACE("DecodedStream::PlayingChanged"); 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 = AssertedCast(mAudioQueue.GetSize()); if (mData) { mData->GetDebugInfo(aInfo.mDecodedStream.mData); } } #undef LOG_DS } // namespace mozilla