/* -*- 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 "VideoSink.h" namespace mozilla { extern LazyLogModule gMediaDecoderLog; #define VSINK_LOG(msg, ...) \ MOZ_LOG(gMediaDecoderLog, LogLevel::Debug, \ ("VideoSink=%p " msg, this, ##__VA_ARGS__)) #define VSINK_LOG_V(msg, ...) \ MOZ_LOG(gMediaDecoderLog, LogLevel::Verbose, \ ("VideoSink=%p " msg, this, ##__VA_ARGS__)) using namespace mozilla::layers; namespace media { VideoSink::VideoSink(AbstractThread* aThread, MediaSink* aAudioSink, MediaQueue& aVideoQueue, VideoFrameContainer* aContainer, bool aRealTime, FrameStatistics& aFrameStats, uint32_t aVQueueSentToCompositerSize) : mOwnerThread(aThread) , mAudioSink(aAudioSink) , mVideoQueue(aVideoQueue) , mContainer(aContainer) , mProducerID(ImageContainer::AllocateProducerID()) , mRealTime(aRealTime) , mFrameStats(aFrameStats) , mVideoFrameEndTime(-1) , mHasVideo(false) , mUpdateScheduler(aThread) , mVideoQueueSendToCompositorSize(aVQueueSentToCompositerSize) { MOZ_ASSERT(mAudioSink, "AudioSink should exist."); } VideoSink::~VideoSink() { } const MediaSink::PlaybackParams& VideoSink::GetPlaybackParams() const { AssertOwnerThread(); return mAudioSink->GetPlaybackParams(); } void VideoSink::SetPlaybackParams(const PlaybackParams& aParams) { AssertOwnerThread(); mAudioSink->SetPlaybackParams(aParams); } RefPtr VideoSink::OnEnded(TrackType aType) { AssertOwnerThread(); MOZ_ASSERT(mAudioSink->IsStarted(), "Must be called after playback starts."); if (aType == TrackInfo::kAudioTrack) { return mAudioSink->OnEnded(aType); } else if (aType == TrackInfo::kVideoTrack) { return mEndPromise; } return nullptr; } int64_t VideoSink::GetEndTime(TrackType aType) const { AssertOwnerThread(); MOZ_ASSERT(mAudioSink->IsStarted(), "Must be called after playback starts."); if (aType == TrackInfo::kVideoTrack) { return mVideoFrameEndTime; } else if (aType == TrackInfo::kAudioTrack) { return mAudioSink->GetEndTime(aType); } return -1; } int64_t VideoSink::GetPosition(TimeStamp* aTimeStamp) const { AssertOwnerThread(); return mAudioSink->GetPosition(aTimeStamp); } bool VideoSink::HasUnplayedFrames(TrackType aType) const { AssertOwnerThread(); MOZ_ASSERT(aType == TrackInfo::kAudioTrack, "Not implemented for non audio tracks."); return mAudioSink->HasUnplayedFrames(aType); } void VideoSink::SetPlaybackRate(double aPlaybackRate) { AssertOwnerThread(); mAudioSink->SetPlaybackRate(aPlaybackRate); } void VideoSink::SetVolume(double aVolume) { AssertOwnerThread(); mAudioSink->SetVolume(aVolume); } void VideoSink::SetPreservesPitch(bool aPreservesPitch) { AssertOwnerThread(); mAudioSink->SetPreservesPitch(aPreservesPitch); } void VideoSink::SetPlaying(bool aPlaying) { AssertOwnerThread(); VSINK_LOG_V(" playing (%d) -> (%d)", mAudioSink->IsPlaying(), aPlaying); if (!aPlaying) { // Reset any update timer if paused. mUpdateScheduler.Reset(); // Since playback is paused, tell compositor to render only current frame. RenderVideoFrames(1); } mAudioSink->SetPlaying(aPlaying); if (mHasVideo && aPlaying) { // There's no thread in VideoSink for pulling video frames, need to trigger // rendering while becoming playing status. because the VideoQueue may be // full already. TryUpdateRenderedVideoFrames(); } } void VideoSink::Start(int64_t aStartTime, const MediaInfo& aInfo) { AssertOwnerThread(); VSINK_LOG("[%s]", __func__); mAudioSink->Start(aStartTime, aInfo); mHasVideo = aInfo.HasVideo(); if (mHasVideo) { mEndPromise = mEndPromiseHolder.Ensure(__func__); ConnectListener(); TryUpdateRenderedVideoFrames(); } } void VideoSink::Stop() { AssertOwnerThread(); MOZ_ASSERT(mAudioSink->IsStarted(), "playback not started."); VSINK_LOG("[%s]", __func__); mAudioSink->Stop(); mUpdateScheduler.Reset(); if (mHasVideo) { DisconnectListener(); mEndPromiseHolder.Resolve(true, __func__); mEndPromise = nullptr; } mVideoFrameEndTime = -1; } bool VideoSink::IsStarted() const { AssertOwnerThread(); return mAudioSink->IsStarted(); } bool VideoSink::IsPlaying() const { AssertOwnerThread(); return mAudioSink->IsPlaying(); } void VideoSink::Shutdown() { AssertOwnerThread(); MOZ_ASSERT(!mAudioSink->IsStarted(), "must be called after playback stops."); VSINK_LOG("[%s]", __func__); mAudioSink->Shutdown(); } void VideoSink::OnVideoQueueEvent(RefPtr&& aSample) { AssertOwnerThread(); // Listen to push event, VideoSink should try rendering ASAP if first frame // arrives but update scheduler is not triggered yet. VideoData* v = aSample->As(); if (!v->mSentToCompositor) { // Since we push rendered frames back to the queue, we will receive // push events for them. We only need to trigger render loop // when this frame is not rendered yet. TryUpdateRenderedVideoFrames(); } } void VideoSink::Redraw() { AssertOwnerThread(); RenderVideoFrames(1); } void VideoSink::TryUpdateRenderedVideoFrames() { AssertOwnerThread(); if (!mUpdateScheduler.IsScheduled() && VideoQueue().GetSize() >= 1 && mAudioSink->IsPlaying()) { UpdateRenderedVideoFrames(); } } void VideoSink::UpdateRenderedVideoFramesByTimer() { AssertOwnerThread(); mUpdateScheduler.CompleteRequest(); UpdateRenderedVideoFrames(); } void VideoSink::ConnectListener() { AssertOwnerThread(); mPushListener = VideoQueue().PushEvent().Connect( mOwnerThread, this, &VideoSink::OnVideoQueueEvent); } void VideoSink::DisconnectListener() { AssertOwnerThread(); mPushListener.Disconnect(); } void VideoSink::RenderVideoFrames(int32_t aMaxFrames, int64_t aClockTime, const TimeStamp& aClockTimeStamp) { AssertOwnerThread(); nsAutoTArray,16> frames; VideoQueue().GetFirstElements(aMaxFrames, &frames); if (frames.IsEmpty() || !mContainer) { return; } nsAutoTArray images; TimeStamp lastFrameTime; MediaSink::PlaybackParams params = mAudioSink->GetPlaybackParams(); for (uint32_t i = 0; i < frames.Length(); ++i) { VideoData* frame = frames[i]->As(); frame->mSentToCompositor = true; // This frame is behind the current time. Let's report it as dropped. // bug 1299018 if (aClockTime >= frame->GetEndTime()) { frame->mIsDropped = true; } if (!frame->mImage || !frame->mImage->IsValid()) { continue; } int64_t frameTime = frame->mTime; if (frameTime < 0) { // Frame times before the start time are invalid; drop such frames continue; } TimeStamp t; if (aMaxFrames > 1) { MOZ_ASSERT(!aClockTimeStamp.IsNull()); int64_t delta = frame->mTime - aClockTime; t = aClockTimeStamp + TimeDuration::FromMicroseconds(delta / params.mPlaybackRate); if (!lastFrameTime.IsNull() && t <= lastFrameTime) { // Timestamps out of order; drop the new frame. In theory we should // probably replace the previous frame with the new frame if the // timestamps are equal, but this is a corrupt video file already so // never mind. continue; } lastFrameTime = t; } ImageContainer::NonOwningImage* img = images.AppendElement(); img->mTimeStamp = t; img->mImage = frame->mImage; img->mFrameID = frame->mFrameID; img->mProducerID = mProducerID; VSINK_LOG_V("playing video frame %lld (id=%x) (vq-queued=%i)", frame->mTime, frame->mFrameID, VideoQueue().GetSize()); } mContainer->SetCurrentFrames(frames[0]->As()->mDisplay, images); } void VideoSink::UpdateRenderedVideoFrames() { AssertOwnerThread(); MOZ_ASSERT(mAudioSink->IsPlaying(), "should be called while playing."); TimeStamp nowTime; const int64_t clockTime = mAudioSink->GetPosition(&nowTime); // 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 and drop // the current frame. NS_ASSERTION(clockTime >= 0, "Should have positive clock time."); #if(0) int64_t remainingTime = -1; if (VideoQueue().GetSize() > 0) { RefPtr currentFrame = VideoQueue().PopFront(); int32_t framesRemoved = 0; while (VideoQueue().GetSize() > 0) { MediaData* nextFrame = VideoQueue().PeekFront(); if (!mRealTime && nextFrame->mTime > clockTime) { remainingTime = nextFrame->mTime - clockTime; break; } ++framesRemoved; if (!currentFrame->As()->mSentToCompositor || currentFrame->As()->mIsDropped /* bug 1299018 */) { mFrameStats.NotifyDecodedFrames(0, 0, 1); VSINK_LOG_V("discarding video frame mTime=%lld clock_time=%lld", currentFrame->mTime, clockTime); } currentFrame = VideoQueue().PopFront(); } VideoQueue().PushFront(currentFrame); if (framesRemoved > 0) { mVideoFrameEndTime = currentFrame->GetEndTime(); mFrameStats.NotifyPresentedFrame(); } } RenderVideoFrames(mVideoQueueSendToCompositorSize, clockTime, nowTime); // No next fame to render. There is no need to schedule next render // loop. We will run render loops again upon incoming frames. if (remainingTime < 0) { return; } TimeStamp target = nowTime + TimeDuration::FromMicroseconds( remainingTime / mAudioSink->GetPlaybackParams().mPlaybackRate); #else // bug 1258870 // Skip frames up to the playback position. int64_t lastDisplayedFrameEndTime = 0; while (VideoQueue().GetSize() > 0 && clockTime >= VideoQueue().PeekFront()->GetEndTime()) { // bug 1298594 RefPtr frame = VideoQueue().PopFront(); if ( frame->As()->mSentToCompositor && !frame->As()->mIsDropped /* bug 1299018 */) { lastDisplayedFrameEndTime = frame->GetEndTime(); mFrameStats.NotifyPresentedFrame(); } else { mFrameStats.NotifyDecodedFrames(0, 0, 1); VSINK_LOG_V("discarding video frame mTime=%lld clock_time=%lld", frame->mTime, clockTime); } } // The presentation end time of the last video frame displayed is either // the end time of the current frame, or if we dropped all frames in the // queue, the end time of the last frame we removed from the queue. RefPtr currentFrame = VideoQueue().PeekFront(); mVideoFrameEndTime = currentFrame ? currentFrame->GetEndTime() : lastDisplayedFrameEndTime; RenderVideoFrames(mVideoQueueSendToCompositorSize, clockTime, nowTime); // Get the timestamp of the next frame. Schedule the next update at // the start time of the next frame. If we don't have a next frame, // we will run render loops again upon incoming frames. nsTArray> frames; VideoQueue().GetFirstElements(2, &frames); if (frames.Length() < 2) { return; } int64_t nextFrameTime = frames[1]->mTime; TimeStamp target = nowTime + TimeDuration::FromMicroseconds((nextFrameTime - clockTime) / mAudioSink->GetPlaybackParams().mPlaybackRate); #endif RefPtr self = this; mUpdateScheduler.Ensure(target, [self] () { self->UpdateRenderedVideoFramesByTimer(); }, [self] () { self->UpdateRenderedVideoFramesByTimer(); }); } } // namespace media } // namespace mozilla