summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsandersd <sandersd@chromium.org>2016-03-04 15:14:08 -0800
committerCommit bot <commit-bot@chromium.org>2016-03-04 23:15:04 +0000
commit1c0bba0d8cbd1390822796b22bfa9bc353d4b5c8 (patch)
tree2203cafb70968bd3bb382a32bdf2a58319097506
parentda5bc9563816fed084050d20e3413ab12ed2a4de (diff)
downloadchromium_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.gn3
-rw-r--r--media/base/mock_filters.cc25
-rw-r--r--media/base/mock_filters.h67
-rw-r--r--media/blink/webmediaplayer_cast_android.cc2
-rw-r--r--media/blink/webmediaplayer_impl.cc403
-rw-r--r--media/blink/webmediaplayer_impl.h81
-rw-r--r--media/filters/pipeline_controller.cc268
-rw-r--r--media/filters/pipeline_controller.h177
-rw-r--r--media/filters/pipeline_controller_unittest.cc212
-rw-r--r--media/media.gyp3
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',