diff options
| author | sandersd <sandersd@chromium.org> | 2016-03-04 15:14:08 -0800 |
|---|---|---|
| committer | Commit bot <commit-bot@chromium.org> | 2016-03-04 23:15:04 +0000 |
| commit | 1c0bba0d8cbd1390822796b22bfa9bc353d4b5c8 (patch) | |
| tree | 2203cafb70968bd3bb382a32bdf2a58319097506 | |
| parent | da5bc9563816fed084050d20e3413ab12ed2a4de (diff) | |
| download | chromium_src-1c0bba0d8cbd1390822796b22bfa9bc353d4b5c8.zip chromium_src-1c0bba0d8cbd1390822796b22bfa9bc353d4b5c8.tar.gz chromium_src-1c0bba0d8cbd1390822796b22bfa9bc353d4b5c8.tar.bz2 | |
Extract state management from WebMediaPlayerImpl.
WMPI recieves operations at any time but is backed by Pipeline which can
only perform one operation at a time. This CL creates a helper class
(PipelineController) to translate between the two so that WMPI does not need
to handle the full cross product of states.
Review URL: https://codereview.chromium.org/1641423002
Cr-Commit-Position: refs/heads/master@{#379386}
| -rw-r--r-- | media/BUILD.gn | 3 | ||||
| -rw-r--r-- | media/base/mock_filters.cc | 25 | ||||
| -rw-r--r-- | media/base/mock_filters.h | 67 | ||||
| -rw-r--r-- | media/blink/webmediaplayer_cast_android.cc | 2 | ||||
| -rw-r--r-- | media/blink/webmediaplayer_impl.cc | 403 | ||||
| -rw-r--r-- | media/blink/webmediaplayer_impl.h | 81 | ||||
| -rw-r--r-- | media/filters/pipeline_controller.cc | 268 | ||||
| -rw-r--r-- | media/filters/pipeline_controller.h | 177 | ||||
| -rw-r--r-- | media/filters/pipeline_controller_unittest.cc | 212 | ||||
| -rw-r--r-- | media/media.gyp | 3 |
10 files changed, 890 insertions, 351 deletions
diff --git a/media/BUILD.gn b/media/BUILD.gn index 0c2674d..01d09d1 100644 --- a/media/BUILD.gn +++ b/media/BUILD.gn @@ -163,6 +163,8 @@ component("media") { "filters/opus_audio_decoder.h", "filters/opus_constants.cc", "filters/opus_constants.h", + "filters/pipeline_controller.cc", + "filters/pipeline_controller.h", "filters/source_buffer_range.cc", "filters/source_buffer_range.h", "filters/source_buffer_stream.cc", @@ -554,6 +556,7 @@ test("media_unittests") { "filters/h264_parser_unittest.cc", "filters/ivf_parser_unittest.cc", "filters/jpeg_parser_unittest.cc", + "filters/pipeline_controller_unittest.cc", "filters/source_buffer_stream_unittest.cc", "filters/video_cadence_estimator_unittest.cc", "filters/video_decoder_selector_unittest.cc", diff --git a/media/base/mock_filters.cc b/media/base/mock_filters.cc index 86b882c..1c783bd 100644 --- a/media/base/mock_filters.cc +++ b/media/base/mock_filters.cc @@ -14,6 +14,31 @@ using ::testing::Return; namespace media { +MockPipeline::MockPipeline() {} + +MockPipeline::~MockPipeline() {} + +void MockPipeline::Start(Demuxer* demuxer, + scoped_ptr<Renderer> renderer, + const base::Closure& ended_cb, + const PipelineStatusCB& error_cb, + const PipelineStatusCB& seek_cb, + const PipelineMetadataCB& metadata_cb, + const BufferingStateCB& buffering_state_cb, + const base::Closure& duration_change_cb, + const AddTextTrackCB& add_text_track_cb, + const base::Closure& waiting_for_decryption_key_cb) { + Start(demuxer, &renderer, ended_cb, error_cb, seek_cb, metadata_cb, + buffering_state_cb, duration_change_cb, add_text_track_cb, + waiting_for_decryption_key_cb); +} + +void MockPipeline::Resume(scoped_ptr<Renderer> renderer, + base::TimeDelta timestamp, + const PipelineStatusCB& seek_cb) { + Resume(&renderer, timestamp, seek_cb); +} + MockDemuxer::MockDemuxer() {} MockDemuxer::~MockDemuxer() {} diff --git a/media/base/mock_filters.h b/media/base/mock_filters.h index 368a542..482f159 100644 --- a/media/base/mock_filters.h +++ b/media/base/mock_filters.h @@ -18,6 +18,7 @@ #include "media/base/decoder_buffer.h" #include "media/base/decryptor.h" #include "media/base/demuxer.h" +#include "media/base/pipeline.h" #include "media/base/pipeline_status.h" #include "media/base/renderer.h" #include "media/base/text_track.h" @@ -30,6 +31,72 @@ namespace media { +class MockPipeline : public Pipeline { + public: + MockPipeline(); + virtual ~MockPipeline(); + + // Note: Start() and Resume() declarations are not actually overrides; they + // take scoped_ptr* instead of scoped_ptr so that they can be mock methods. + // Private stubs for Start() and Resume() implement the actual Pipeline + // interface by forwarding to these mock methods. + MOCK_METHOD10(Start, + void(Demuxer*, + scoped_ptr<Renderer>*, + const base::Closure&, + const PipelineStatusCB&, + const PipelineStatusCB&, + const PipelineMetadataCB&, + const BufferingStateCB&, + const base::Closure&, + const AddTextTrackCB&, + const base::Closure&)); + MOCK_METHOD1(Stop, void(const base::Closure&)); + MOCK_METHOD2(Seek, void(base::TimeDelta, const PipelineStatusCB&)); + MOCK_METHOD1(Suspend, void(const PipelineStatusCB&)); + MOCK_METHOD3(Resume, + void(scoped_ptr<Renderer>*, + base::TimeDelta, + const PipelineStatusCB&)); + + // TODO(sandersd): This should automatically return true between Start() and + // Stop(). (Or better, remove it from the interface entirely.) + MOCK_CONST_METHOD0(IsRunning, bool()); + + // TODO(sandersd): These should be regular getters/setters. + MOCK_CONST_METHOD0(GetPlaybackRate, double()); + MOCK_METHOD1(SetPlaybackRate, void(double)); + MOCK_CONST_METHOD0(GetVolume, float()); + MOCK_METHOD1(SetVolume, void(float)); + + // TODO(sandersd): These should probably have setters too. + MOCK_CONST_METHOD0(GetMediaTime, base::TimeDelta()); + MOCK_CONST_METHOD0(GetBufferedTimeRanges, Ranges<base::TimeDelta>()); + MOCK_CONST_METHOD0(GetMediaDuration, base::TimeDelta()); + MOCK_METHOD0(DidLoadingProgress, bool()); + MOCK_CONST_METHOD0(GetStatistics, PipelineStatistics()); + + MOCK_METHOD2(SetCdm, void(CdmContext*, const CdmAttachedCB&)); + + private: + // Forwarding stubs (see comment above). + void Start(Demuxer* demuxer, + scoped_ptr<Renderer> renderer, + const base::Closure& ended_cb, + const PipelineStatusCB& error_cb, + const PipelineStatusCB& seek_cb, + const PipelineMetadataCB& metadata_cb, + const BufferingStateCB& buffering_state_cb, + const base::Closure& duration_change_cb, + const AddTextTrackCB& add_text_track_cb, + const base::Closure& waiting_for_decryption_key_cb) override; + void Resume(scoped_ptr<Renderer> renderer, + base::TimeDelta timestamp, + const PipelineStatusCB& seek_cb) override; + + DISALLOW_COPY_AND_ASSIGN(MockPipeline); +}; + class MockDemuxer : public Demuxer { public: MockDemuxer(); diff --git a/media/blink/webmediaplayer_cast_android.cc b/media/blink/webmediaplayer_cast_android.cc index 993af2c..ec94419 100644 --- a/media/blink/webmediaplayer_cast_android.cc +++ b/media/blink/webmediaplayer_cast_android.cc @@ -221,7 +221,7 @@ void WebMediaPlayerCast::OnSeekComplete(const base::TimeDelta& current_time) { DVLOG(1) << __FUNCTION__; remote_time_at_ = base::TimeTicks::Now(); remote_time_ = current_time; - webmediaplayer_->OnPipelineSeeked(true, PIPELINE_OK); + webmediaplayer_->OnPipelineSeeked(true); } void WebMediaPlayerCast::OnMediaError(int error_type) { diff --git a/media/blink/webmediaplayer_impl.cc b/media/blink/webmediaplayer_impl.cc index 84bcfff..ea60463 100644 --- a/media/blink/webmediaplayer_impl.cc +++ b/media/blink/webmediaplayer_impl.cc @@ -11,6 +11,7 @@ #include <utility> #include "base/bind.h" +#include "base/bind_helpers.h" #include "base/callback.h" #include "base/callback_helpers.h" #include "base/command_line.h" @@ -143,20 +144,21 @@ WebMediaPlayerImpl::WebMediaPlayerImpl( worker_task_runner_(params.worker_task_runner()), media_log_(params.media_log()), pipeline_(media_task_runner_, media_log_.get()), + pipeline_controller_( + &pipeline_, + base::Bind(&WebMediaPlayerImpl::CreateRenderer, + base::Unretained(this)), + base::Bind(&WebMediaPlayerImpl::OnPipelineSeeked, AsWeakPtr()), + base::Bind(&WebMediaPlayerImpl::OnPipelineSuspended, AsWeakPtr()), + base::Bind(&WebMediaPlayerImpl::OnPipelineResumed, AsWeakPtr()), + base::Bind(&WebMediaPlayerImpl::OnPipelineError, AsWeakPtr())), load_type_(LoadTypeURL), opaque_(false), playback_rate_(0.0), paused_(true), seeking_(false), - pending_suspend_(false), - pending_time_change_(false), - pending_resume_(false), - suspending_(false), - suspended_(false), - resuming_(false), pending_suspend_resume_cycle_(false), ended_(false), - pending_seek_(false), should_notify_time_changed_(false), fullscreen_(false), decoder_requires_restart_for_fullscreen_(false), @@ -230,7 +232,7 @@ WebMediaPlayerImpl::~WebMediaPlayerImpl() { data_source_->Abort(); if (chunk_demuxer_) { chunk_demuxer_->Shutdown(); - chunk_demuxer_ = NULL; + chunk_demuxer_ = nullptr; } renderer_factory_.reset(); @@ -342,21 +344,21 @@ void WebMediaPlayerImpl::play() { #endif paused_ = false; - pipeline_.SetPlaybackRate(playback_rate_); + if (data_source_) data_source_->MediaIsPlaying(); media_log_->AddEvent(media_log_->CreateEvent(MediaLogEvent::PLAY)); if (playback_rate_ > 0) { - // Resume the player if playback was initiated in the foreground. - if (suspended_ && !resuming_ && delegate_ && !delegate_->IsHidden()) { - ScheduleResume(); - return; - } - NotifyPlaybackStarted(); + + // Resume the player if playback was initiated in the foreground. Resume() + // will do nothing if the pipeline is not suspended state, but will clear + // some internal pending state, so it should always be called. + if (delegate_ && !delegate_->IsHidden()) + pipeline_controller_.Resume(); } } @@ -375,7 +377,13 @@ void WebMediaPlayerImpl::pause() { #endif pipeline_.SetPlaybackRate(0.0); - UpdatePausedTime(); + + // pause() may be called after playback has ended and the HTMLMediaElement + // requires that currentTime() == duration() after ending. We want to ensure + // |paused_time_| matches currentTime() in this case or a future seek() may + // incorrectly discard what it thinks is a seek to the existing time. + paused_time_ = + ended_ ? pipeline_.GetMediaDuration() : pipeline_.GetMediaTime(); media_log_->AddEvent(media_log_->CreateEvent(MediaLogEvent::PAUSE)); @@ -391,14 +399,17 @@ bool WebMediaPlayerImpl::supportsSave() const { void WebMediaPlayerImpl::seek(double seconds) { DVLOG(1) << __FUNCTION__ << "(" << seconds << "s)"; DCHECK(main_task_runner_->BelongsToCurrentThread()); + DoSeek(base::TimeDelta::FromSecondsD(seconds), true); +} - ended_ = false; +void WebMediaPlayerImpl::DoSeek(base::TimeDelta time, bool time_updated) { + DCHECK(main_task_runner_->BelongsToCurrentThread()); - base::TimeDelta new_seek_time = base::TimeDelta::FromSecondsD(seconds); + ended_ = false; #if defined(OS_ANDROID) // WMPI_CAST if (isRemote()) { - cast_impl_.seek(new_seek_time); + cast_impl_.seek(time); return; } #endif @@ -407,86 +418,39 @@ void WebMediaPlayerImpl::seek(double seconds) { if (ready_state_ > WebMediaPlayer::ReadyStateHaveMetadata) SetReadyState(WebMediaPlayer::ReadyStateHaveMetadata); - if (seeking_ || suspended_) { - // Once resuming, it's too late to change the resume time and so the - // implementation is a little different. - bool is_suspended = suspended_ && !resuming_; - - // If we are currently seeking or resuming to |new_seek_time|, skip the - // seek (except for MSE, which always seeks). - if (!is_suspended && new_seek_time == seek_time_) { - if (chunk_demuxer_) { - // Don't suppress any redundant in-progress MSE seek. There could have - // been changes to the underlying buffers after seeking the demuxer and - // before receiving OnPipelineSeeked() for the currently in-progress - // seek. - MEDIA_LOG(DEBUG, media_log_) - << "Detected MediaSource seek to same time as in-progress seek to " - << seek_time_ << "."; - } else { - // Suppress all redundant seeks if unrestricted by media source demuxer - // API. - pending_seek_ = false; - pending_seek_time_ = base::TimeDelta(); - return; - } - } - - // If |chunk_demuxer_| is already seeking, cancel that seek and schedule the - // new one. - if (!is_suspended && chunk_demuxer_) - chunk_demuxer_->CancelPendingSeek(new_seek_time); - - // Schedule a seek once the current suspend or seek finishes. - pending_seek_ = true; - pending_seek_time_ = new_seek_time; - - // In the case of seeking while suspended, the seek is considered to have - // started immediately (but won't complete until the pipeline is resumed). - if (is_suspended) { - seeking_ = true; - seek_time_ = new_seek_time; - - // Resume the pipeline if the seek is initiated in the foreground so that - // the correct frame is displayed. - if (delegate_ && !delegate_->IsHidden()) - ScheduleResume(); - } - - return; - } - - media_log_->AddEvent(media_log_->CreateSeekEvent(seconds)); - - // Update our paused time. - // For non-MSE playbacks, in paused state ignore the seek operations to - // current time if the loading is completed and generate - // OnPipelineBufferingStateChanged event to eventually fire seeking and seeked - // events. We don't short-circuit MSE seeks in this logic because the - // underlying buffers around the seek time might have changed (or even been - // removed) since previous seek/preroll/pause action, and the pipeline might - // need to flush so the new buffers are decoded and rendered instead of the - // old ones. - if (paused_) { - if (paused_time_ != new_seek_time || chunk_demuxer_) { - paused_time_ = new_seek_time; - } else if (old_state == ReadyStateHaveEnoughData) { + // When paused, we know exactly what the current time is and can elide seeks + // to it. However, there are two cases that are not elided: + // 1) When the pipeline state is not stable. + // In this case we just let |pipeline_controller_| decide what to do, as + // it has complete information. + // 2) For MSE. + // Because the buffers may have changed between seeks, MSE seeks are + // never elided. + if (paused_ && pipeline_controller_.IsStable() && paused_time_ == time && + !chunk_demuxer_) { + // If the ready state was high enough before, we can indicate that the seek + // completed just by restoring it. Otherwise we will just wait for the real + // ready state change to eventually happen. + if (old_state == ReadyStateHaveEnoughData) { main_task_runner_->PostTask( FROM_HERE, base::Bind(&WebMediaPlayerImpl::OnPipelineBufferingStateChanged, AsWeakPtr(), BUFFERING_HAVE_ENOUGH)); - return; } + return; } seeking_ = true; - seek_time_ = new_seek_time; - - if (chunk_demuxer_) - chunk_demuxer_->StartWaitingForSeek(seek_time_); + seek_time_ = time; + if (paused_) + paused_time_ = time; + pipeline_controller_.Seek(time, time_updated); - pipeline_.Seek(seek_time_, BIND_TO_RENDER_LOOP1( - &WebMediaPlayerImpl::OnPipelineSeeked, true)); + // Resume the pipeline if the seek is initiated in the foreground so that + // the correct frame is displayed. If the pipeline is not suspended, Resume() + // will do nothing but clear some pending state. + if (delegate_ && !delegate_->IsHidden()) + pipeline_controller_.Resume(); } void WebMediaPlayerImpl::setRate(double rate) { @@ -635,23 +599,16 @@ double WebMediaPlayerImpl::currentTime() const { if (ended_) return duration(); - // We know the current seek time better than pipeline: pipeline may processing - // an earlier seek before a pending seek has been started, or it might not yet - // have the current seek time returnable via GetMediaTime(). - if (seeking()) { - return pending_seek_ ? pending_seek_time_.InSecondsF() - : seek_time_.InSecondsF(); - } + if (seeking()) + return seek_time_.InSecondsF(); #if defined(OS_ANDROID) // WMPI_CAST - if (isRemote()) { + if (isRemote()) return cast_impl_.currentTime(); - } #endif - if (paused_) { + if (paused_) return paused_time_.InSecondsF(); - } return pipeline_.GetMediaTime().InSecondsF(); } @@ -884,68 +841,16 @@ void WebMediaPlayerImpl::OnCdmAttached(bool success) { set_cdm_result_.reset(); } -void WebMediaPlayerImpl::OnPipelineSeeked(bool time_changed, - PipelineStatus status) { - DVLOG(1) << __FUNCTION__ << "(" << time_changed << ", " << status << ")"; - DCHECK(main_task_runner_->BelongsToCurrentThread()); - - if (status != PIPELINE_OK) { - OnPipelineError(status); - return; - } - - // Whether or not the seek was caused by a resume, we're not suspended now. - const bool was_resuming = resuming_; - resuming_ = false; - suspended_ = false; - - // If we we're resuming into the playing state, notify the delegate. - if (was_resuming && playback_rate_ > 0 && !paused_) - NotifyPlaybackStarted(); - - // If there is a pending suspend, the seek does not complete until after the - // next resume. - if (pending_suspend_) { - pending_suspend_ = false; - pending_time_change_ = time_changed; - Suspend(); - return; - } - - // Clear seek state. Note that if the seek was caused by a resume, then - // |seek_time_| is always set but |seeking_| is only set if there was a - // pending seek at the time. +void WebMediaPlayerImpl::OnPipelineSeeked(bool time_updated) { seeking_ = false; seek_time_ = base::TimeDelta(); - - if (pending_seek_) { - double pending_seek_seconds = pending_seek_time_.InSecondsF(); - pending_seek_ = false; - pending_seek_time_ = base::TimeDelta(); - seek(pending_seek_seconds); - return; - } - - // Update our paused time. if (paused_) - UpdatePausedTime(); - - should_notify_time_changed_ = time_changed; + paused_time_ = pipeline_.GetMediaTime(); + if (time_updated) + should_notify_time_changed_ = true; } -void WebMediaPlayerImpl::OnPipelineSuspended(PipelineStatus status) { - DVLOG(1) << __FUNCTION__ << "(" << status << ")"; - DCHECK(main_task_runner_->BelongsToCurrentThread()); - - if (status != PIPELINE_OK) { - OnPipelineError(status); - return; - } - - suspending_ = false; - if (delegate_) - delegate_->PlayerGone(delegate_id_); - +void WebMediaPlayerImpl::OnPipelineSuspended() { #if defined(OS_ANDROID) if (isRemote()) { scoped_refptr<VideoFrame> frame = cast_impl_.GetCastingBanner(); @@ -955,20 +860,27 @@ void WebMediaPlayerImpl::OnPipelineSuspended(PipelineStatus status) { } #endif - if (pending_resume_ || pending_suspend_resume_cycle_) { - pending_resume_ = false; + if (delegate_) + delegate_->PlayerGone(delegate_id_); + + if (pending_suspend_resume_cycle_) { pending_suspend_resume_cycle_ = false; - Resume(); + pipeline_controller_.Resume(); return; } } +void WebMediaPlayerImpl::OnPipelineResumed() { + if (playback_rate_ > 0 && !paused_) + NotifyPlaybackStarted(); +} + void WebMediaPlayerImpl::OnPipelineEnded() { DVLOG(1) << __FUNCTION__; DCHECK(main_task_runner_->BelongsToCurrentThread()); - // Ignore state changes until we've completed all outstanding seeks. - if (seeking_ || pending_seek_) + // Ignore state changes until we've completed all outstanding operations. + if (!pipeline_controller_.IsStable()) return; ended_ = true; @@ -1032,8 +944,9 @@ void WebMediaPlayerImpl::OnPipelineBufferingStateChanged( BufferingState buffering_state) { DVLOG(1) << __FUNCTION__ << "(" << buffering_state << ")"; - // Ignore buffering state changes until we've completed all outstanding seeks. - if (seeking_ || pending_seek_) + // Ignore buffering state changes until we've completed all outstanding + // operations. + if (!pipeline_controller_.IsStable()) return; // TODO(scherkus): Handle other buffering states when Pipeline starts using @@ -1107,38 +1020,7 @@ void WebMediaPlayerImpl::OnHidden(bool must_suspend) { #endif if (must_suspend || (paused_ && ended_) || hasVideo()) - ScheduleSuspend(); -} - -void WebMediaPlayerImpl::ScheduleSuspend() { - if (!pipeline_.IsRunning()) - return; - - if (resuming_ || seeking_) { - pending_suspend_ = true; - return; - } - - if (pending_resume_) { - pending_resume_ = false; - return; - } - - Suspend(); -} - -void WebMediaPlayerImpl::Suspend() { - DCHECK(main_task_runner_->BelongsToCurrentThread()); - - // Since Pipeline::IsRunning() may be set on the media thread there are cases - // where two suspends might be issued concurrently. - if (suspended_) - return; - - suspended_ = true; - suspending_ = true; - pipeline_.Suspend( - BIND_TO_RENDER_LOOP(&WebMediaPlayerImpl::OnPipelineSuspended)); + pipeline_controller_.Suspend(); } void WebMediaPlayerImpl::OnShown() { @@ -1164,26 +1046,7 @@ void WebMediaPlayerImpl::OnShown() { #endif if (!ended_ && !paused_) - ScheduleResume(); -} - -void WebMediaPlayerImpl::ScheduleResume() { - if (!pipeline_.IsRunning()) - return; - - if (suspending_) { - pending_resume_ = true; - return; - } - - if (pending_suspend_) { - pending_suspend_ = false; - return; - } - - // Might already be resuming iff we came back from remote playback recently. - if (suspended_ && !resuming_) - Resume(); + pipeline_controller_.Resume(); } void WebMediaPlayerImpl::OnPlay() { @@ -1201,57 +1064,14 @@ void WebMediaPlayerImpl::OnVolumeMultiplierUpdate(double multiplier) { setVolume(volume_); } -void WebMediaPlayerImpl::Resume() { - DCHECK(main_task_runner_->BelongsToCurrentThread()); - CHECK(suspended_); - CHECK(!resuming_); - - // If there was a time change pending when we suspended (which can happen when - // we suspend immediately after a seek), surface it after resuming. - bool time_changed = pending_time_change_; - pending_time_change_ = false; - - if (seeking_ || pending_seek_) { - if (pending_seek_) { - seek_time_ = pending_seek_time_; - pending_seek_ = false; - pending_seek_time_ = base::TimeDelta(); - } - time_changed = true; - } else { - // It is safe to call GetCurrentFrameTimestamp() because VFC is stopped - // during Suspend(). It won't be started again until after Resume() is - // called. Use the pipeline time if there's no video. - if (!data_source_ || !data_source_->IsStreaming()) { - seek_time_ = hasVideo() ? compositor_->GetCurrentFrameTimestamp() - : pipeline_.GetMediaTime(); - } else { - // Resume from zero if a resource does not support range requests; this - // avoids a painful "read-the-whole-file" seek penalty. - seek_time_ = base::TimeDelta(); - } - } - - if (chunk_demuxer_) - chunk_demuxer_->StartWaitingForSeek(seek_time_); - - resuming_ = true; - pipeline_.Resume(CreateRenderer(), seek_time_, - BIND_TO_RENDER_LOOP1(&WebMediaPlayerImpl::OnPipelineSeeked, - time_changed)); -} - void WebMediaPlayerImpl::ScheduleRestart() { - // If we're suspended but not resuming there is no need to restart because - // there is no renderer to kill. - if (!suspended_ || resuming_) { + if (!pipeline_controller_.IsSuspended()) { pending_suspend_resume_cycle_ = true; - ScheduleSuspend(); + pipeline_controller_.Suspend(); } } #if defined(OS_ANDROID) // WMPI_CAST - bool WebMediaPlayerImpl::isRemote() const { return cast_impl_.isRemote(); } @@ -1278,28 +1098,26 @@ void WebMediaPlayerImpl::OnRemotePlaybackEnded() { } void WebMediaPlayerImpl::OnDisconnectedFromRemoteDevice(double t) { - paused_time_ = base::TimeDelta::FromSecondsD(t); - pending_seek_ = true; - pending_seek_time_ = paused_time_; + DoSeek(base::TimeDelta::FromSecondsD(t), false); + if (delegate_ && !delegate_->IsHidden()) + pipeline_controller_.Resume(); - ScheduleResume(); - - if (paused_time_ == pipeline_.GetMediaDuration()) { - ended_ = true; - } // We already told the delegate we're paused when remoting started. client_->playbackStateChanged(); client_->disconnectedFromRemoteDevice(); } void WebMediaPlayerImpl::SuspendForRemote() { - if (suspended_ && !suspending_) { + if (!pipeline_controller_.IsSuspended()) { + pipeline_controller_.Suspend(); + } else { + // TODO(sandersd): If PipelineController::Suspend() called |suspended_cb| + // when already suspended, we wouldn't need this case. scoped_refptr<VideoFrame> frame = cast_impl_.GetCastingBanner(); if (frame) { compositor_->PaintFrameUsingOldRenderingPath(frame); } } - ScheduleSuspend(); } gfx::Size WebMediaPlayerImpl::GetCanvasSize() const { @@ -1410,11 +1228,10 @@ void WebMediaPlayerImpl::StartPipeline() { seeking_ = true; // TODO(sandersd): On Android, defer Start() if the tab is not visible. - pipeline_.Start( - demuxer_.get(), CreateRenderer(), + bool is_streaming = (data_source_ && data_source_->IsStreaming()); + pipeline_controller_.Start( + chunk_demuxer_, demuxer_.get(), is_streaming, BIND_TO_RENDER_LOOP(&WebMediaPlayerImpl::OnPipelineEnded), - BIND_TO_RENDER_LOOP(&WebMediaPlayerImpl::OnPipelineError), - BIND_TO_RENDER_LOOP1(&WebMediaPlayerImpl::OnPipelineSeeked, false), BIND_TO_RENDER_LOOP(&WebMediaPlayerImpl::OnPipelineMetadata), BIND_TO_RENDER_LOOP(&WebMediaPlayerImpl::OnPipelineBufferingStateChanged), BIND_TO_RENDER_LOOP(&WebMediaPlayerImpl::OnDurationChanged), @@ -1522,17 +1339,6 @@ WebMediaPlayerImpl::GetCurrentFrameFromCompositor() { return video_frame; } -void WebMediaPlayerImpl::UpdatePausedTime() { - DCHECK(main_task_runner_->BelongsToCurrentThread()); - - // pause() may be called after playback has ended and the HTMLMediaElement - // requires that currentTime() == duration() after ending. We want to ensure - // |paused_time_| matches currentTime() in this case or a future seek() may - // incorrectly discard what it thinks is a seek to the existing time. - paused_time_ = - ended_ ? pipeline_.GetMediaDuration() : pipeline_.GetMediaTime(); -} - void WebMediaPlayerImpl::NotifyPlaybackStarted() { #if defined(OS_ANDROID) // WMPI_CAST // We do not tell our delegates about remote playback, because that would @@ -1541,10 +1347,13 @@ void WebMediaPlayerImpl::NotifyPlaybackStarted() { return; #endif - // Don't send delegate notifications when suspended; upon suspend we send - // PlayerGone() to the delegate -- no more notifications should be sent until - // after resume. - if (suspended_) + // NotifyPlaybackStarted() may be called by interactions while suspended, + // (play/pause in particular). Those actions won't have any effect until the + // pipeline is resumed. + // TODO(dalecurtis): Should these be dropped at the call sites instead? + // Alternatively, rename this method to include Maybe or Changed, and handle + // multiple calls safely. + if (pipeline_controller_.IsSuspended()) return; if (delegate_) { @@ -1563,10 +1372,14 @@ void WebMediaPlayerImpl::NotifyPlaybackPaused() { if (isRemote()) return; #endif - // Don't send delegate notifications when suspended; upon suspend we send - // PlayerGone() to the delegate -- no more notifications should be sent until - // after resume. - if (!suspended_ && delegate_) + + // Same as above, NotifyPlaybackPaused() may be called by interactions while + // suspended, but those actions won't have any effect until the pipeline is + // resumed. + if (pipeline_controller_.IsSuspended()) + return; + + if (delegate_) delegate_->DidPause(delegate_id_, ended_); memory_usage_reporting_timer_.Stop(); ReportMemoryUsage(); diff --git a/media/blink/webmediaplayer_impl.h b/media/blink/webmediaplayer_impl.h index ec55ca1..0283efb 100644 --- a/media/blink/webmediaplayer_impl.h +++ b/media/blink/webmediaplayer_impl.h @@ -30,6 +30,7 @@ #include "media/blink/webmediaplayer_delegate.h" #include "media/blink/webmediaplayer_params.h" #include "media/blink/webmediaplayer_util.h" +#include "media/filters/pipeline_controller.h" #include "media/renderers/skcanvas_video_renderer.h" #include "third_party/WebKit/public/platform/WebAudioSourceProvider.h" #include "third_party/WebKit/public/platform/WebContentDecryptionModuleResult.h" @@ -165,16 +166,6 @@ class MEDIA_BLINK_EXPORT WebMediaPlayerImpl void enteredFullscreen() override; void exitedFullscreen() override; - void OnPipelineSeeked(bool time_changed, PipelineStatus status); - void OnPipelineSuspended(PipelineStatus status); - void OnPipelineEnded(); - void OnPipelineError(PipelineStatus error); - void OnPipelineMetadata(PipelineMetadata metadata); - void OnPipelineBufferingStateChanged(BufferingState buffering_state); - void OnDemuxerOpened(); - void OnAddTextTrack(const TextTrackConfig& config, - const AddTextTrackDoneCB& done_cb); - // WebMediaPlayerDelegate::Observer implementation. void OnHidden(bool must_suspend) override; void OnShown() override; @@ -198,20 +189,24 @@ class MEDIA_BLINK_EXPORT WebMediaPlayerImpl void SetDeviceScaleFactor(float scale_factor); #endif - private: - // Ask for the pipeline to be suspended, will call Suspend() when ready. - // (Possibly immediately.) - void ScheduleSuspend(); - - // Initiate suspending the pipeline. - void Suspend(); + // Called from WebMediaPlayerCast. + // TODO(hubbe): WMPI_CAST make private. + void OnPipelineSeeked(bool time_updated); - // Ask for the pipeline to be resumed, will call Resume() when ready. - // (Possibly immediately.) - void ScheduleResume(); + private: + void OnPipelineSuspended(); + void OnPipelineResumed(); + void OnPipelineEnded(); + void OnPipelineError(PipelineStatus error); + void OnPipelineMetadata(PipelineMetadata metadata); + void OnPipelineBufferingStateChanged(BufferingState buffering_state); + void OnDemuxerOpened(); + void OnAddTextTrack(const TextTrackConfig& config, + const AddTextTrackDoneCB& done_cb); - // Initiate resuming the pipeline. - void Resume(); + // Actually seek. Avoids causing |should_notify_time_changed_| to be set when + // |time_updated| is false. + void DoSeek(base::TimeDelta time, bool time_updated); // Ask for the renderer to be restarted (destructed and recreated). void ScheduleRestart(); @@ -272,10 +267,6 @@ class MEDIA_BLINK_EXPORT WebMediaPlayerImpl // Called when a CDM has been attached to the |pipeline_|. void OnCdmAttached(bool success); - // Updates |paused_time_| to the current media time with consideration for the - // |ended_| state by clamping current time to duration upon |ended_|. - void UpdatePausedTime(); - // Notifies |delegate_| that playback has started or was paused; also starts // or stops the memory usage reporting timer respectively. void NotifyPlaybackStarted(); @@ -308,7 +299,11 @@ class MEDIA_BLINK_EXPORT WebMediaPlayerImpl scoped_refptr<base::SingleThreadTaskRunner> media_task_runner_; scoped_refptr<base::TaskRunner> worker_task_runner_; scoped_refptr<MediaLog> media_log_; + + // |pipeline_controller_| references |pipeline_| and therefore must be + // constructed after and destructed before |pipeline_|. PipelineImpl pipeline_; + PipelineController pipeline_controller_; // The LoadType passed in the |load_type| parameter of the load() call. LoadType load_type_; @@ -332,32 +327,16 @@ class MEDIA_BLINK_EXPORT WebMediaPlayerImpl // clock can creep forward a little bit while the asynchronous // SetPlaybackRate(0) is being executed. double playback_rate_; + + // Set while paused. |paused_time_| is only valid when |paused_| is true. bool paused_; base::TimeDelta paused_time_; - bool seeking_; - // Set when seeking (|seeking_| is true) or resuming. + // Set when starting, seeking, and resuming (all of which require a Pipeline + // seek). |seek_time_| is only valid when |seeking_| is true. + bool seeking_; base::TimeDelta seek_time_; - // Set when a suspend is required but another suspend or seek is in progress. - bool pending_suspend_; - - // Set when suspending immediately after a seek. The time change will happen - // after Resume(). - bool pending_time_change_; - - // Set when a resume is required but suspending is in progress. - bool pending_resume_; - - // Set for the entire period between suspend starting and resume completing. - bool suspending_; - - // Set while suspending to detect double-suspend. - bool suspended_; - - // Set while resuming to detect double-resume. - bool resuming_; - // Set when doing a restart (a suspend and resume in sequence) of the pipeline // in order to destruct and reinitialize the decoders. This is separate from // |pending_resume_| and |pending_suspend_| because they can be elided in @@ -369,14 +348,6 @@ class MEDIA_BLINK_EXPORT WebMediaPlayerImpl // see http://crbug.com/409280 bool ended_; - // Indicates that a seek is queued after the current seek completes or, if the - // pipeline is suspended, after it resumes. Only the last queued seek will - // have any effect. - bool pending_seek_; - - // |pending_seek_time_| is meaningless when |pending_seek_| is false. - base::TimeDelta pending_seek_time_; - // Tracks whether to issue time changed notifications during buffering state // changes. bool should_notify_time_changed_; diff --git a/media/filters/pipeline_controller.cc b/media/filters/pipeline_controller.cc new file mode 100644 index 0000000..1e218f9 --- /dev/null +++ b/media/filters/pipeline_controller.cc @@ -0,0 +1,268 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "media/filters/pipeline_controller.h" + +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "media/base/bind_to_current_loop.h" +#include "media/filters/chunk_demuxer.h" + +namespace media { + +PipelineController::PipelineController( + Pipeline* pipeline, + const RendererFactoryCB& renderer_factory_cb, + const SeekedCB& seeked_cb, + const SuspendedCB& suspended_cb, + const ResumedCB& resumed_cb, + const PipelineStatusCB& error_cb) + : pipeline_(pipeline), + renderer_factory_cb_(renderer_factory_cb), + seeked_cb_(seeked_cb), + suspended_cb_(suspended_cb), + resumed_cb_(resumed_cb), + error_cb_(error_cb), + weak_factory_(this) { + DCHECK(pipeline_); + DCHECK(!renderer_factory_cb_.is_null()); + DCHECK(!seeked_cb_.is_null()); + DCHECK(!suspended_cb_.is_null()); + DCHECK(!resumed_cb_.is_null()); + DCHECK(!error_cb_.is_null()); +} + +PipelineController::~PipelineController() { + DCHECK(thread_checker_.CalledOnValidThread()); +} + +// TODO(sandersd): Move ChunkDemuxer API to Demuxer so that Pipeline can +// implement all of this. +// TODO(sandersd): If there is a pending suspend, don't call pipeline_.Start() +// until Resume(). +void PipelineController::Start( + ChunkDemuxer* chunk_demuxer, + Demuxer* demuxer, + bool is_streaming, + const base::Closure& ended_cb, + const PipelineMetadataCB& metadata_cb, + const BufferingStateCB& buffering_state_cb, + const base::Closure& duration_change_cb, + const AddTextTrackCB& add_text_track_cb, + const base::Closure& waiting_for_decryption_key_cb) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(state_ == State::CREATED); + + if (chunk_demuxer) + DCHECK_EQ(demuxer, chunk_demuxer); + + // Once the pipeline is started, we want to call the seeked callback but + // without a time update. + pending_seeked_cb_ = true; + state_ = State::STARTING; + + chunk_demuxer_ = chunk_demuxer; + is_streaming_ = is_streaming; + pipeline_->Start( + demuxer, renderer_factory_cb_.Run(), ended_cb, + BindToCurrentLoop(error_cb_), + BindToCurrentLoop(base::Bind(&PipelineController::OnPipelineStatus, + weak_factory_.GetWeakPtr(), State::PLAYING)), + metadata_cb, buffering_state_cb, duration_change_cb, add_text_track_cb, + waiting_for_decryption_key_cb); +} + +void PipelineController::Seek(base::TimeDelta time, bool time_updated) { + DCHECK(thread_checker_.CalledOnValidThread()); + + // It would be slightly more clear to set this in Dispatch(), but we want to + // be sure it gets updated even if the seek is elided. + if (time_updated) + pending_time_updated_ = true; + pending_seeked_cb_ = true; + + // If we are already seeking to |time|, just clear any pending seek. This does + // not apply to MSE because the underlying buffer could have been changed + // between the seek calls. + // TODO(sandersd): The underlying buffer could also have changed for + // File objects, but WMPI is also broken in that case (because it caches). + if ((state_ == State::SEEKING || state_ == State::RESUMING) && + seek_time_ == time && !chunk_demuxer_) { + pending_seek_ = false; + return; + } + + pending_seek_time_ = time; + pending_seek_ = true; + Dispatch(); +} + +// TODO(sandersd): It may be easier to use this interface if |suspended_cb_| is +// executed when Suspend() is called while already suspended. +void PipelineController::Suspend() { + DCHECK(thread_checker_.CalledOnValidThread()); + pending_resume_ = false; + if (state_ != State::SUSPENDING && state_ != State::SUSPENDED) { + pending_suspend_ = true; + Dispatch(); + } +} + +void PipelineController::Resume() { + DCHECK(thread_checker_.CalledOnValidThread()); + pending_suspend_ = false; + if (state_ == State::SUSPENDING || state_ == State::SUSPENDED) { + pending_resume_ = true; + Dispatch(); + } +} + +bool PipelineController::IsStable() { + DCHECK(thread_checker_.CalledOnValidThread()); + return (state_ == State::PLAYING); +} + +bool PipelineController::IsSuspended() { + DCHECK(thread_checker_.CalledOnValidThread()); + return (state_ == State::SUSPENDED); +} + +void PipelineController::OnPipelineStatus(State state, + PipelineStatus pipeline_status) { + DCHECK(thread_checker_.CalledOnValidThread()); + + if (pipeline_status != PIPELINE_OK) { + error_cb_.Run(pipeline_status); + return; + } + + state_ = state; + + if (state == State::PLAYING) { + // Start(), Seek(), or Resume() completed; we can be sure that + // |chunk_demuxer_| got the seek it was waiting for. + waiting_for_seek_ = false; + if (pending_resumed_cb_) { + pending_resumed_cb_ = false; + + // Warning: possibly reentrant. The state may change inside this callback. + // It must be safe to call Dispatch() twice in a row here. + resumed_cb_.Run(); + } + } else if (state == State::SUSPENDED) { + pending_resumed_cb_ = true; + + // Warning: possibly reentrant. The state may change inside this callback. + // It must be safe to call Dispatch() twice in a row here. + suspended_cb_.Run(); + } + + Dispatch(); +} + +// Note: Dispatch() may be called re-entrantly (by callbacks internally) or +// twice in a row (by OnPipelineStatus()). +void PipelineController::Dispatch() { + DCHECK(thread_checker_.CalledOnValidThread()); + + // Suspend/resume transitions take priority because seeks before a suspend + // are wasted, and seeks after can be merged into the resume operation. + if (pending_suspend_ && state_ == State::PLAYING) { + pending_suspend_ = false; + state_ = State::SUSPENDING; + pipeline_->Suspend(BindToCurrentLoop( + base::Bind(&PipelineController::OnPipelineStatus, + weak_factory_.GetWeakPtr(), State::SUSPENDED))); + return; + } + + if (pending_resume_ && state_ == State::SUSPENDED) { + // If there is a pending seek, resume to that time instead... + if (pending_seek_) { + seek_time_ = pending_seek_time_; + pending_seek_ = false; + } else { + seek_time_ = pipeline_->GetMediaTime(); + } + + // ...unless the media is streaming, in which case we resume at the start + // because seeking doesn't work well. + if (is_streaming_ && !seek_time_.is_zero()) { + seek_time_ = base::TimeDelta(); + + // In this case we want to make sure that the controls get updated + // immediately, so we don't try to hide the seek. + pending_time_updated_ = true; + } + + // Tell |chunk_demuxer_| to expect our resume. + if (chunk_demuxer_) { + DCHECK(!waiting_for_seek_); + chunk_demuxer_->StartWaitingForSeek(seek_time_); + waiting_for_seek_ = true; + } + + pending_resume_ = false; + state_ = State::RESUMING; + pipeline_->Resume(renderer_factory_cb_.Run(), seek_time_, + BindToCurrentLoop(base::Bind( + &PipelineController::OnPipelineStatus, + weak_factory_.GetWeakPtr(), State::PLAYING))); + return; + } + + // |chunk_demuxer_| supports aborting seeks. Make use of that when we have + // other pending operations. + if ((pending_seek_ || pending_suspend_) && waiting_for_seek_) { + CHECK(chunk_demuxer_); + + // If there is no pending seek, return the current seek to pending status. + if (!pending_seek_) { + pending_seek_time_ = seek_time_; + pending_seek_ = true; + } + + // CancelPendingSeek() may be reentrant, so update state first and return + // immediately. + waiting_for_seek_ = false; + chunk_demuxer_->CancelPendingSeek(pending_seek_time_); + return; + } + + // Ordinary seeking. + if (pending_seek_ && state_ == State::PLAYING) { + seek_time_ = pending_seek_time_; + + // Tell |chunk_demuxer_| to expect our seek. + if (chunk_demuxer_) { + DCHECK(!waiting_for_seek_); + waiting_for_seek_ = true; + chunk_demuxer_->StartWaitingForSeek(seek_time_); + } + + pending_seek_ = false; + state_ = State::SEEKING; + pipeline_->Seek(seek_time_, + BindToCurrentLoop(base::Bind( + &PipelineController::OnPipelineStatus, + weak_factory_.GetWeakPtr(), State::PLAYING))); + return; + } + + // If |state_| is PLAYING and we didn't trigger an operation above then we + // are in a stable state. If there is a seeked callback pending, emit it. + if (state_ == State::PLAYING) { + if (pending_seeked_cb_) { + // |seeked_cb_| may be reentrant, so update state first and return + // immediately. + pending_seeked_cb_ = false; + bool was_pending_time_updated = pending_time_updated_; + pending_time_updated_ = false; + seeked_cb_.Run(was_pending_time_updated); + return; + } + } +} + +} // namespace media diff --git a/media/filters/pipeline_controller.h b/media/filters/pipeline_controller.h new file mode 100644 index 0000000..3491bd2 --- /dev/null +++ b/media/filters/pipeline_controller.h @@ -0,0 +1,177 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef MEDIA_FILTERS_PIPELINE_CONTROLLER_H_ +#define MEDIA_FILTERS_PIPELINE_CONTROLLER_H_ + +#include "base/callback.h" +#include "base/macros.h" +#include "base/memory/weak_ptr.h" +#include "base/threading/thread_checker.h" +#include "base/time/time.h" +#include "media/base/media_export.h" +#include "media/base/pipeline.h" +#include "media/base/renderer.h" + +namespace media { + +class ChunkDemuxer; +class Demuxer; + +// PipelineController wraps a Pipeline to expose the one-at-a-time operations +// (Seek(), Suspend(), and Resume()) with a simpler API. Internally it tracks +// pending operations and dispatches them when possible. Duplicate requests +// (such as seeking twice to the same time) may be elided. +// +// TODO(sandersd): +// - Expose an operation that restarts via suspend+resume. +// - Block invalid calls after an error occurs. +class MEDIA_EXPORT PipelineController { + public: + enum class State { + CREATED, + STARTING, + PLAYING, + SEEKING, + SUSPENDING, + SUSPENDED, + RESUMING, + }; + + using RendererFactoryCB = base::Callback<scoped_ptr<Renderer>(void)>; + using SeekedCB = base::Callback<void(bool time_updated)>; + using SuspendedCB = base::Callback<void()>; + using ResumedCB = base::Callback<void()>; + + // Construct a PipelineController wrapping |pipeline_|. |pipeline_| must + // outlive the resulting PipelineController. The callbacks are: + // - |renderer_factory_cb| is called by PipelineController to create new + // renderers when starting and resuming. + // - |seeked_cb| is called upon reaching a stable state if a seek occured. + // - |suspended_cb| is called immediately after suspendeding. + // - |resumed_cb| is called immediately after resuming. + // - |error_cb| is called if any operation on |pipeline_| does not result + // in PIPELINE_OK or its error callback is called. + PipelineController(Pipeline* pipeline, + const RendererFactoryCB& renderer_factory_cb, + const SeekedCB& seeked_cb, + const SuspendedCB& suspended_cb, + const ResumedCB& resumed_cb, + const PipelineStatusCB& error_cb); + ~PipelineController(); + + // Start |pipeline_|. If provided, |chunk_demuxer| will be stored and + // StartWaitingForSeek()/CancelPendingSeek() will be issued to it as + // necessary. + // + // When |is_streaming| is true, Resume() will always start at the + // beginning of the stream, rather than attempting to seek to the current + // time. + // + // The other parameters are just passed directly to pipeline_.Start(). + void Start(ChunkDemuxer* chunk_demuxer, + Demuxer* demuxer, + bool is_streaming, + const base::Closure& ended_cb, + const PipelineMetadataCB& metadata_cb, + const BufferingStateCB& buffering_state_cb, + const base::Closure& duration_change_cb, + const AddTextTrackCB& add_text_track_cb, + const base::Closure& waiting_for_decryption_key_cb); + + // Request a seek to |time|. If |time_updated| is true, then the eventual + // |seeked_cb| callback will also have |time_updated| set to true; it + // indicates that the seek was requested by Blink and a time update is + // expected so that Blink can fire the seeked event. + void Seek(base::TimeDelta time, bool time_updated); + + // Request that |pipeline_| be suspended. This is a no-op if |pipeline_| has + // been suspended. + void Suspend(); + + // Request that |pipeline_| be resumed. This is a no-op if |pipeline_| has not + // been suspended. + void Resume(); + + // Returns true if the current state is stable. This means that |state_| is + // PLAYING and there are no pending operations. Requests are processed + // immediately when the state is stable, otherwise they are queued. + // + // Exceptions to the above: + // - Start() is processed immediately while in the CREATED state. + // - Resume() is processed immediately while in the SUSPENDED state. + bool IsStable(); + + // Returns true if |pipeline_| is suspended. + bool IsSuspended(); + + private: + // Attempts to make progress from the current state to the target state. + void Dispatch(); + + // PipelineStaus callback that also carries the target state. + void OnPipelineStatus(State state, PipelineStatus pipeline_status); + + // The Pipeline we are managing state for. + Pipeline* pipeline_ = nullptr; + + // Factory for Renderers, used for Start() and Resume(). + RendererFactoryCB renderer_factory_cb_; + + // Called after seeks (which includes Start()) upon reaching a stable state. + // Multiple seeks result in only one callback if no stable state occurs + // between them. + SeekedCB seeked_cb_; + + // Called immediately when |pipeline_| completes a suspend operation. + SuspendedCB suspended_cb_; + + // Called immediately when |pipeline_| completes a resume operation. + ResumedCB resumed_cb_; + + // Called immediately when any operation on |pipeline_| results in an error. + PipelineStatusCB error_cb_; + + // State for handling StartWaitingForSeek()/CancelPendingSeek(). + ChunkDemuxer* chunk_demuxer_ = nullptr; + bool waiting_for_seek_ = false; + + // When true, Resume() will start at time zero instead of seeking to the + // current time. + bool is_streaming_ = false; + + // Tracks the current state of |pipeline_|. + State state_ = State::CREATED; + + // Indicates that a seek has occurred. When set, a seeked callback will be + // issued at the next stable state. + bool pending_seeked_cb_ = false; + + // Indicates that time has been changed by a seek, which will be reported at + // the next seeked callback. + bool pending_time_updated_ = false; + + // Indicates that the |pipeline_| was suspended, and therefore that a resumed + // callback should be issued the next time we enter State::PLAYING. + bool pending_resumed_cb_ = false; + + // The target time of the active seek; valid while SEEKING or RESUMING. + base::TimeDelta seek_time_; + + // Target state which we will work to achieve. |pending_seek_time_| is only + // valid when |pending_seek_| is true. + bool pending_seek_ = false; + base::TimeDelta pending_seek_time_; + bool pending_suspend_ = false; + bool pending_resume_ = false; + + base::ThreadChecker thread_checker_; + base::WeakPtrFactory<PipelineController> weak_factory_; + + DISALLOW_COPY_AND_ASSIGN(PipelineController); +}; + +} // namespace media + +#endif // MEDIA_FILTERS_PIPELINE_CONTROLLER_H_ diff --git a/media/filters/pipeline_controller_unittest.cc b/media/filters/pipeline_controller_unittest.cc new file mode 100644 index 0000000..561c7b0 --- /dev/null +++ b/media/filters/pipeline_controller_unittest.cc @@ -0,0 +1,212 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/logging.h" +#include "base/macros.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/message_loop/message_loop.h" +#include "base/time/time.h" +#include "media/base/mock_filters.h" +#include "media/base/pipeline.h" +#include "media/filters/pipeline_controller.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using ::testing::_; +using ::testing::DoAll; +using ::testing::Mock; +using ::testing::Return; +using ::testing::SaveArg; +using ::testing::StrictMock; + +namespace media { + +class PipelineControllerTest : public ::testing::Test { + public: + PipelineControllerTest() + : pipeline_controller_(&pipeline_, + base::Bind(&PipelineControllerTest::CreateRenderer, + base::Unretained(this)), + base::Bind(&PipelineControllerTest::OnSeeked, + base::Unretained(this)), + base::Bind(&PipelineControllerTest::OnSuspended, + base::Unretained(this)), + base::Bind(&PipelineControllerTest::OnResumed, + base::Unretained(this)), + base::Bind(&PipelineControllerTest::OnError, + base::Unretained(this))) {} + + ~PipelineControllerTest() override {} + + PipelineStatusCB StartPipeline() { + EXPECT_FALSE(pipeline_controller_.IsStable()); + PipelineStatusCB start_cb; + EXPECT_CALL(pipeline_, Start(_, _, _, _, _, _, _, _, _, _)) + .WillOnce(SaveArg<4>(&start_cb)); + pipeline_controller_.Start( + nullptr, nullptr, false, base::Closure(), PipelineMetadataCB(), + BufferingStateCB(), base::Closure(), AddTextTrackCB(), base::Closure()); + Mock::VerifyAndClear(&pipeline_); + EXPECT_FALSE(pipeline_controller_.IsStable()); + return start_cb; + } + + PipelineStatusCB SeekPipeline(base::TimeDelta time) { + EXPECT_TRUE(pipeline_controller_.IsStable()); + PipelineStatusCB seek_cb; + EXPECT_CALL(pipeline_, Seek(time, _)).WillOnce(SaveArg<1>(&seek_cb)); + pipeline_controller_.Seek(time, true); + Mock::VerifyAndClear(&pipeline_); + EXPECT_FALSE(pipeline_controller_.IsStable()); + return seek_cb; + } + + PipelineStatusCB SuspendPipeline() { + EXPECT_TRUE(pipeline_controller_.IsStable()); + PipelineStatusCB suspend_cb; + EXPECT_CALL(pipeline_, Suspend(_)).WillOnce(SaveArg<0>(&suspend_cb)); + pipeline_controller_.Suspend(); + Mock::VerifyAndClear(&pipeline_); + EXPECT_FALSE(pipeline_controller_.IsStable()); + EXPECT_FALSE(pipeline_controller_.IsSuspended()); + return suspend_cb; + } + + PipelineStatusCB ResumePipeline() { + EXPECT_TRUE(pipeline_controller_.IsSuspended()); + PipelineStatusCB resume_cb; + EXPECT_CALL(pipeline_, Resume(_, _, _)) + .WillOnce( + DoAll(SaveArg<1>(&last_resume_time_), SaveArg<2>(&resume_cb))); + EXPECT_CALL(pipeline_, GetMediaTime()) + .WillRepeatedly(Return(base::TimeDelta())); + pipeline_controller_.Resume(); + Mock::VerifyAndClear(&pipeline_); + EXPECT_FALSE(pipeline_controller_.IsStable()); + EXPECT_FALSE(pipeline_controller_.IsSuspended()); + return resume_cb; + } + + void Complete(const PipelineStatusCB& cb) { + cb.Run(PIPELINE_OK); + message_loop_.RunUntilIdle(); + } + + protected: + scoped_ptr<Renderer> CreateRenderer() { return scoped_ptr<Renderer>(); } + + void OnSeeked(bool time_updated) { + was_seeked_ = true; + last_seeked_time_updated_ = time_updated; + } + + void OnSuspended() { was_suspended_ = true; } + + void OnResumed() { was_resumed_ = true; } + + void OnError(PipelineStatus status) { NOTREACHED(); } + + base::MessageLoop message_loop_; + + StrictMock<MockPipeline> pipeline_; + PipelineController pipeline_controller_; + + bool was_seeked_ = false; + bool last_seeked_time_updated_ = false; + bool was_suspended_ = false; + bool was_resumed_ = false; + base::TimeDelta last_resume_time_; + + DISALLOW_COPY_AND_ASSIGN(PipelineControllerTest); +}; + +TEST_F(PipelineControllerTest, Startup) { + PipelineStatusCB start_cb = StartPipeline(); + EXPECT_FALSE(was_seeked_); + + Complete(start_cb); + EXPECT_TRUE(was_seeked_); + EXPECT_FALSE(last_seeked_time_updated_); + EXPECT_FALSE(was_suspended_); + EXPECT_FALSE(was_resumed_); + EXPECT_TRUE(pipeline_controller_.IsStable()); +} + +TEST_F(PipelineControllerTest, SuspendResume) { + Complete(StartPipeline()); + EXPECT_TRUE(was_seeked_); + was_seeked_ = false; + + Complete(SuspendPipeline()); + EXPECT_TRUE(was_suspended_); + EXPECT_FALSE(was_resumed_); + EXPECT_FALSE(pipeline_controller_.IsStable()); + + Complete(ResumePipeline()); + EXPECT_TRUE(was_resumed_); + EXPECT_TRUE(pipeline_controller_.IsStable()); + + // |was_seeked_| should not be affected by Suspend()/Resume() at all. + EXPECT_FALSE(was_seeked_); +} + +TEST_F(PipelineControllerTest, PendingSuspend) { + Complete(StartPipeline()); + + base::TimeDelta seek_time = base::TimeDelta::FromSeconds(5); + PipelineStatusCB seek_cb = SeekPipeline(seek_time); + message_loop_.RunUntilIdle(); + + // While the seek is ongoing, request a suspend. + // It will be a mock failure if pipeline_.Suspend() is called. + pipeline_controller_.Suspend(); + message_loop_.RunUntilIdle(); + + // Expect the suspend to trigger when the seek is completed. + EXPECT_CALL(pipeline_, Suspend(_)); + Complete(seek_cb); +} + +TEST_F(PipelineControllerTest, SeekMergesWithResume) { + Complete(StartPipeline()); + Complete(SuspendPipeline()); + + // Request a seek while suspended. + // It will be a mock failure if pipeline_.Seek() is called. + base::TimeDelta seek_time = base::TimeDelta::FromSeconds(5); + pipeline_controller_.Seek(seek_time, true); + message_loop_.RunUntilIdle(); + + // Resume and verify the resume time includes the seek. + Complete(ResumePipeline()); + EXPECT_EQ(seek_time, last_resume_time_); + EXPECT_TRUE(last_seeked_time_updated_); +} + +TEST_F(PipelineControllerTest, SeekMergesWithSeek) { + Complete(StartPipeline()); + + base::TimeDelta seek_time_1 = base::TimeDelta::FromSeconds(5); + PipelineStatusCB seek_cb_1 = SeekPipeline(seek_time_1); + message_loop_.RunUntilIdle(); + + // Request another seek while the first is ongoing. + base::TimeDelta seek_time_2 = base::TimeDelta::FromSeconds(10); + pipeline_controller_.Seek(seek_time_2, true); + message_loop_.RunUntilIdle(); + + // Request a third seek. (It should replace the second.) + base::TimeDelta seek_time_3 = base::TimeDelta::FromSeconds(15); + pipeline_controller_.Seek(seek_time_3, true); + message_loop_.RunUntilIdle(); + + // Expect the third seek to trigger when the first seek completes. + EXPECT_CALL(pipeline_, Seek(seek_time_3, _)); + Complete(seek_cb_1); +} + +} // namespace media diff --git a/media/media.gyp b/media/media.gyp index 5c72613..bdc7d5c 100644 --- a/media/media.gyp +++ b/media/media.gyp @@ -536,6 +536,8 @@ 'filters/opus_audio_decoder.h', 'filters/opus_constants.cc', 'filters/opus_constants.h', + 'filters/pipeline_controller.cc', + 'filters/pipeline_controller.h', 'filters/source_buffer_range.cc', 'filters/source_buffer_range.h', 'filters/source_buffer_stream.cc', @@ -1258,6 +1260,7 @@ 'filters/ivf_parser_unittest.cc', 'filters/jpeg_parser_unittest.cc', 'filters/memory_data_source_unittest.cc', + 'filters/pipeline_controller_unittest.cc', 'filters/source_buffer_stream_unittest.cc', 'filters/video_cadence_estimator_unittest.cc', 'filters/video_decoder_selector_unittest.cc', |
