summaryrefslogtreecommitdiffstats
path: root/media/base
diff options
context:
space:
mode:
authortimav <timav@chromium.org>2015-07-24 18:05:13 -0700
committerCommit bot <commit-bot@chromium.org>2015-07-25 01:05:37 +0000
commit634a28cba4755cc6aaf2944b0138b7340f854c4b (patch)
treeebbfedad6b2fd3908fd0802473e13cfd80020c75 /media/base
parentc3f6e0c653bc113b52540ae695ef17830c13c6c2 (diff)
downloadchromium_src-634a28cba4755cc6aaf2944b0138b7340f854c4b.zip
chromium_src-634a28cba4755cc6aaf2944b0138b7340f854c4b.tar.gz
chromium_src-634a28cba4755cc6aaf2944b0138b7340f854c4b.tar.bz2
MediaCodecPlayer implementation (stage 2 - seek)
Implements seek operation for MediaCodecPlayer. BUG=407577 Review URL: https://codereview.chromium.org/1213583002 Cr-Commit-Position: refs/heads/master@{#340388}
Diffstat (limited to 'media/base')
-rw-r--r--media/base/android/access_unit_queue.cc2
-rw-r--r--media/base/android/demuxer_stream_player_params.cc16
-rw-r--r--media/base/android/media_codec_decoder.cc64
-rw-r--r--media/base/android/media_codec_decoder.h60
-rw-r--r--media/base/android/media_codec_player.cc181
-rw-r--r--media/base/android/media_codec_player.h101
-rw-r--r--media/base/android/media_codec_player_unittest.cc225
-rw-r--r--media/base/android/media_codec_video_decoder.cc10
-rw-r--r--media/base/android/media_codec_video_decoder.h4
-rw-r--r--media/base/android/test_data_factory.cc5
-rw-r--r--media/base/android/test_data_factory.h7
-rw-r--r--media/base/android/test_statistics.h6
12 files changed, 622 insertions, 59 deletions
diff --git a/media/base/android/access_unit_queue.cc b/media/base/android/access_unit_queue.cc
index e1973c0..38d45f5 100644
--- a/media/base/android/access_unit_queue.cc
+++ b/media/base/android/access_unit_queue.cc
@@ -39,7 +39,7 @@ void AccessUnitQueue::PushBack(const DemuxerData& data) {
for (size_t i = 0; i < data.access_units.size(); ++i) {
const AccessUnit& unit = data.access_units[i];
- // EOS must be the last unit in the chunk
+ // EOS must be the last unit in the chunk.
if (unit.is_end_of_stream) {
DCHECK(i == data.access_units.size() - 1);
}
diff --git a/media/base/android/demuxer_stream_player_params.cc b/media/base/android/demuxer_stream_player_params.cc
index 9bf443d..722eba54 100644
--- a/media/base/android/demuxer_stream_player_params.cc
+++ b/media/base/android/demuxer_stream_player_params.cc
@@ -71,6 +71,19 @@ const char* AsString(VideoCodec codec) {
return nullptr; // crash early
}
+const char* AsString(DemuxerStream::Status status) {
+ switch (status) {
+ case DemuxerStream::kOk:
+ return "kOk";
+ case DemuxerStream::kAborted:
+ return "kAborted";
+ case DemuxerStream::kConfigChanged:
+ return "kConfigChanged";
+ }
+ NOTREACHED();
+ return nullptr; // crash early
+}
+
#undef RETURN_STRING
} // namespace (anonymous)
@@ -78,7 +91,8 @@ const char* AsString(VideoCodec codec) {
} // namespace media
std::ostream& operator<<(std::ostream& os, const media::AccessUnit& au) {
- os << "status:" << au.status << (au.is_end_of_stream ? " EOS" : "")
+ os << "status:" << media::AsString(au.status)
+ << (au.is_end_of_stream ? " EOS" : "")
<< (au.is_key_frame ? " KEY_FRAME" : "") << " pts:" << au.timestamp
<< " size:" << au.data.size();
return os;
diff --git a/media/base/android/media_codec_decoder.cc b/media/base/android/media_codec_decoder.cc
index 8652d5b..dc51904 100644
--- a/media/base/android/media_codec_decoder.cc
+++ b/media/base/android/media_codec_decoder.cc
@@ -50,6 +50,9 @@ MediaCodecDecoder::MediaCodecDecoder(
last_frame_posted_(false),
is_data_request_in_progress_(false),
is_incoming_data_invalid_(false),
+#ifndef NDEBUG
+ verify_next_frame_is_key_(false),
+#endif
weak_factory_(this) {
DCHECK(media_task_runner_->BelongsToCurrentThread());
@@ -100,6 +103,15 @@ void MediaCodecDecoder::Flush() {
completed_ = false;
au_queue_.Flush();
+#ifndef NDEBUG
+ // We check and reset |verify_next_frame_is_key_| on Decoder thread.
+ // This DCHECK ensures we won't need to lock this variable.
+ DCHECK(!decoder_thread_.IsRunning());
+
+ // For video the first frame after flush must be key frame.
+ verify_next_frame_is_key_ = true;
+#endif
+
if (media_codec_bridge_) {
// MediaCodecBridge::Reset() performs MediaCodecBridge.flush()
MediaCodecStatus flush_status = media_codec_bridge_->Reset();
@@ -171,16 +183,26 @@ MediaCodecDecoder::ConfigStatus MediaCodecDecoder::Configure() {
return CONFIG_FAILURE;
}
- // Here I assume that OnDemuxerConfigsAvailable won't come
- // in the middle of demuxer data.
-
+ MediaCodecDecoder::ConfigStatus result;
if (media_codec_bridge_) {
DVLOG(1) << class_name() << "::" << __FUNCTION__
<< ": reconfiguration is not required, ignoring";
- return CONFIG_OK;
+ result = CONFIG_OK;
+ } else {
+ result = ConfigureInternal();
+
+#ifndef NDEBUG
+ // We check and reset |verify_next_frame_is_key_| on Decoder thread.
+ // This DCHECK ensures we won't need to lock this variable.
+ DCHECK(!decoder_thread_.IsRunning());
+
+ // For video the first frame after reconfiguration must be key frame.
+ if (result == CONFIG_OK)
+ verify_next_frame_is_key_ = true;
+#endif
}
- return ConfigureInternal();
+ return result;
}
bool MediaCodecDecoder::Start(base::TimeDelta current_time) {
@@ -297,25 +319,32 @@ void MediaCodecDecoder::OnLastFrameRendered(bool completed) {
void MediaCodecDecoder::OnDemuxerDataAvailable(const DemuxerData& data) {
DCHECK(media_task_runner_->BelongsToCurrentThread());
+ // If |data| contains an aborted data, the last AU will have kAborted status.
+ bool aborted_data =
+ !data.access_units.empty() &&
+ data.access_units.back().status == DemuxerStream::kAborted;
+
+#ifndef NDEBUG
const char* explain_if_skipped =
- is_incoming_data_invalid_ ? " skipped as invalid" : "";
+ is_incoming_data_invalid_ ? " skipped as invalid"
+ : (aborted_data ? " skipped as aborted" : "");
- DVLOG(2) << class_name() << "::" << __FUNCTION__ << explain_if_skipped
- << " #AUs:" << data.access_units.size()
- << " #Configs:" << data.demuxer_configs.size();
-#if !defined(NDEBUG)
for (const auto& unit : data.access_units)
- DVLOG(2) << class_name() << "::" << __FUNCTION__ << explain_if_skipped
+ DVLOG(1) << class_name() << "::" << __FUNCTION__ << explain_if_skipped
<< " au: " << unit;
+ for (const auto& configs : data.demuxer_configs)
+ DVLOG(1) << class_name() << "::" << __FUNCTION__ << " configs: " << configs;
#endif
- if (!is_incoming_data_invalid_)
+ if (!is_incoming_data_invalid_ && !aborted_data)
au_queue_.PushBack(data);
is_incoming_data_invalid_ = false;
is_data_request_in_progress_ = false;
- if (state_ == kPrefetching)
+ // Do not request data if we got kAborted. There is no point to request the
+ // data after kAborted and before the OnDemuxerSeekDone.
+ if (state_ == kPrefetching && !aborted_data)
PrefetchNextChunk();
}
@@ -472,6 +501,15 @@ bool MediaCodecDecoder::EnqueueInputBuffer() {
return false;
}
+ // We are ready to enqueue the front unit.
+
+#ifndef NDEBUG
+ if (verify_next_frame_is_key_) {
+ verify_next_frame_is_key_ = false;
+ VerifyUnitIsKeyFrame(au_info.front_unit);
+ }
+#endif
+
// Dequeue input buffer
base::TimeDelta timeout =
diff --git a/media/base/android/media_codec_decoder.h b/media/base/android/media_codec_decoder.h
index 189ebc3..a974eea 100644
--- a/media/base/android/media_codec_decoder.h
+++ b/media/base/android/media_codec_decoder.h
@@ -44,7 +44,7 @@ class MediaCodecBridge;
// v | Error recovery:
// [ Prefetched ] |
// | | (any state including Error)
-// | Start | |
+// | Configure and Start | |
// v | | ReleaseDecoderResources
// [ Running ] | v
// | | [ Stopped ]
@@ -58,6 +58,50 @@ class MediaCodecBridge;
// | Flush |
// ---------------------------
+// Here is the workflow that is expected to be maintained by a caller, which is
+// MediaCodecPlayer currently.
+//
+// [ Stopped ]
+// |
+// | Prefetch
+// v
+// [ Prefetching ]
+// |
+// | (Enough data received)
+// v
+// [ Prefetched ]
+// |
+// | <---------- SetDemuxerConfigs (*)
+// |
+// | <---------- SetVideoSurface (**)
+// |
+// | Configure --------------------------------------------+
+// | |
+// v v
+// ( Config Succeeded ) ( Key frame required )
+// | |
+// | Start |
+// v |
+// [ Running ] ------------------------------+ |
+// | | |
+// | | |
+// | RequestToStop | SyncStop | SyncStop
+// | | |
+// [ Stopping ] | |
+// | | |
+// | ( Last frame rendered ) | |
+// | | |
+// | | |
+// v | |
+// [ Stopped ] <-----------------------------+-----------------+
+//
+//
+// (*) Demuxer configs is a precondition to Configure(), but MediaCodecPlayer
+// has stricter requirements and they are set before Prefetch().
+//
+// (**) VideoSurface is a precondition to video decoder Configure(), can be set
+// any time before Configure().
+
class MediaCodecDecoder {
public:
// The result of MediaCodec configuration, used by MediaCodecPlayer.
@@ -119,8 +163,8 @@ class MediaCodecDecoder {
// Stops decoder thread, releases the MediaCodecBridge and other resources.
virtual void ReleaseDecoderResources();
- // Flushes the MediaCodec and resets the AccessUnitQueue.
- // Decoder thread should not be running.
+ // Flushes the MediaCodec, after that resets the AccessUnitQueue and blocks
+ // the input. Decoder thread should not be running.
virtual void Flush();
// Releases MediaCodecBridge.
@@ -190,6 +234,11 @@ class MediaCodecDecoder {
// because their rendering is delayed (video).
virtual void ReleaseDelayedBuffers() {}
+#ifndef NDEBUG
+ // For video, checks that access unit is the key frame or stand-alone EOS.
+ virtual void VerifyUnitIsKeyFrame(const AccessUnit* unit) const {}
+#endif
+
// Helper methods.
// Notifies the decoder if the frame is the last one.
@@ -284,6 +333,11 @@ class MediaCodecDecoder {
// Indicates whether the incoming data should be ignored.
bool is_incoming_data_invalid_;
+#ifndef NDEBUG
+ // When set, we check that the following video frame is the key frame.
+ bool verify_next_frame_is_key_;
+#endif
+
// NOTE: Weak pointers must be invalidated before all other member variables.
base::WeakPtrFactory<MediaCodecDecoder> weak_factory_;
diff --git a/media/base/android/media_codec_player.cc b/media/base/android/media_codec_player.cc
index da1fff1..27b5bca 100644
--- a/media/base/android/media_codec_player.cc
+++ b/media/base/android/media_codec_player.cc
@@ -61,6 +61,7 @@ MediaCodecPlayer::MediaCodecPlayer(
state_(STATE_PAUSED),
interpolator_(&default_tick_clock_),
pending_start_(false),
+ pending_seek_(kNoTimestamp()),
media_weak_factory_(this) {
DCHECK(ui_task_runner_->BelongsToCurrentThread());
@@ -70,6 +71,8 @@ MediaCodecPlayer::MediaCodecPlayer(
completion_cb_ =
base::Bind(&MediaPlayerManager::OnPlaybackComplete, manager, player_id);
+ seek_done_cb_ =
+ base::Bind(&MediaPlayerManager::OnSeekComplete, manager, player_id);
attach_listener_cb_ = base::Bind(&MediaPlayerAndroid::AttachListener,
WeakPtrForUIThread(), nullptr);
detach_listener_cb_ =
@@ -147,6 +150,7 @@ void MediaCodecPlayer::Start() {
switch (state_) {
case STATE_PAUSED:
+ // Prefetch or wait for initial configuration.
if (HasAudio() || HasVideo()) {
SetState(STATE_PREFETCHING);
StartPrefetchDecoders();
@@ -155,10 +159,17 @@ void MediaCodecPlayer::Start() {
}
break;
case STATE_STOPPING:
+ case STATE_WAITING_FOR_SEEK:
SetPendingStart(true);
break;
+ case STATE_WAITING_FOR_CONFIG:
+ case STATE_PREFETCHING:
+ case STATE_PLAYING:
+ case STATE_WAITING_FOR_SURFACE:
+ case STATE_ERROR:
+ break; // Ignore
default:
- // Ignore
+ NOTREACHED();
break;
}
}
@@ -168,11 +179,11 @@ void MediaCodecPlayer::Pause(bool is_media_related_action) {
DVLOG(1) << __FUNCTION__;
+ SetPendingStart(false);
+
switch (state_) {
+ case STATE_WAITING_FOR_CONFIG:
case STATE_PREFETCHING:
- SetState(STATE_PAUSED);
- StopDecoders();
- break;
case STATE_WAITING_FOR_SURFACE:
SetState(STATE_PAUSED);
StopDecoders();
@@ -181,8 +192,13 @@ void MediaCodecPlayer::Pause(bool is_media_related_action) {
SetState(STATE_STOPPING);
RequestToStopDecoders();
break;
+ case STATE_PAUSED:
+ case STATE_STOPPING:
+ case STATE_WAITING_FOR_SEEK:
+ case STATE_ERROR:
+ break; // Ignore
default:
- // Ignore
+ NOTREACHED();
break;
}
}
@@ -191,7 +207,38 @@ void MediaCodecPlayer::SeekTo(base::TimeDelta timestamp) {
RUN_ON_MEDIA_THREAD(SeekTo, timestamp);
DVLOG(1) << __FUNCTION__ << " " << timestamp;
- NOTIMPLEMENTED();
+
+ switch (state_) {
+ case STATE_PAUSED:
+ SetState(STATE_WAITING_FOR_SEEK);
+ RequestDemuxerSeek(timestamp);
+ break;
+ case STATE_WAITING_FOR_CONFIG:
+ case STATE_PREFETCHING:
+ case STATE_WAITING_FOR_SURFACE:
+ SetState(STATE_WAITING_FOR_SEEK);
+ StopDecoders();
+ SetPendingStart(true);
+ RequestDemuxerSeek(timestamp);
+ break;
+ case STATE_PLAYING:
+ SetState(STATE_STOPPING);
+ RequestToStopDecoders();
+ SetPendingStart(true);
+ SetPendingSeek(timestamp);
+ break;
+ case STATE_STOPPING:
+ SetPendingSeek(timestamp);
+ break;
+ case STATE_WAITING_FOR_SEEK:
+ SetPendingSeek(timestamp);
+ break;
+ case STATE_ERROR:
+ break; // ignore
+ default:
+ NOTREACHED();
+ break;
+ }
}
void MediaCodecPlayer::Release() {
@@ -232,7 +279,10 @@ base::TimeDelta MediaCodecPlayer::GetDuration() {
bool MediaCodecPlayer::IsPlaying() {
DCHECK(ui_task_runner_->BelongsToCurrentThread());
- return state_ == STATE_PLAYING;
+
+ // TODO(timav): Use another variable since |state_| should only be accessed on
+ // Media thread.
+ return state_ == STATE_PLAYING || state_ == STATE_STOPPING;
}
bool MediaCodecPlayer::CanPause() {
@@ -304,7 +354,53 @@ void MediaCodecPlayer::OnDemuxerSeekDone(
DVLOG(1) << __FUNCTION__ << " actual_time:" << actual_browser_seek_time;
- NOTIMPLEMENTED();
+ if (state_ != STATE_WAITING_FOR_SEEK)
+ return; // ignore
+
+ DCHECK(seek_info_.get());
+ DCHECK(seek_info_->seek_time != kNoTimestamp());
+
+ // A browser seek must not jump into the past. Ideally, it seeks to the
+ // requested time, but it might jump into the future.
+ DCHECK(!seek_info_->is_browser_seek ||
+ seek_info_->seek_time <= actual_browser_seek_time);
+
+ // Restrict the current time to be equal to seek_time
+ // for the next StartPlaybackDecoders() call.
+
+ base::TimeDelta seek_time = seek_info_->is_browser_seek
+ ? actual_browser_seek_time
+ : seek_info_->seek_time;
+
+ interpolator_.SetBounds(seek_time, seek_time);
+ audio_decoder_->SetBaseTimestamp(seek_time);
+
+ base::TimeDelta pending_seek_time = GetPendingSeek();
+ if (pending_seek_time != kNoTimestamp()) {
+ // Keep STATE_WAITING_FOR_SEEK
+ SetPendingSeek(kNoTimestamp());
+ RequestDemuxerSeek(pending_seek_time);
+ return;
+ }
+
+ if (HasPendingStart()) {
+ SetPendingStart(false);
+ // Prefetch or wait for initial configuration.
+ if (HasAudio() || HasVideo()) {
+ SetState(STATE_PREFETCHING);
+ StartPrefetchDecoders();
+ } else {
+ SetState(STATE_WAITING_FOR_CONFIG);
+ }
+ } else {
+ SetState(STATE_PAUSED);
+ }
+
+ // Notify the Renderer.
+ if (!seek_info_->is_browser_seek)
+ ui_task_runner_->PostTask(FROM_HERE, base::Bind(seek_done_cb_, seek_time));
+
+ seek_info_.reset();
}
void MediaCodecPlayer::OnDemuxerDurationChanged(
@@ -361,10 +457,14 @@ void MediaCodecPlayer::RequestDemuxerData(DemuxerStream::Type stream_type) {
void MediaCodecPlayer::OnPrefetchDone() {
DCHECK(GetMediaTaskRunner()->BelongsToCurrentThread());
- DVLOG(1) << __FUNCTION__;
- if (state_ != STATE_PREFETCHING)
+ if (state_ != STATE_PREFETCHING) {
+ DVLOG(1) << __FUNCTION__ << " wrong state " << AsString(state_)
+ << " ignoring";
return; // Ignore
+ }
+
+ DVLOG(1) << __FUNCTION__;
if (!HasAudio() && !HasVideo()) {
// No configuration at all after prefetching.
@@ -394,23 +494,33 @@ void MediaCodecPlayer::OnStopDone() {
if (interpolator_.interpolating())
interpolator_.StopInterpolating();
+ base::TimeDelta seek_time;
switch (state_) {
- case STATE_STOPPING:
- if (HasPendingStart()) {
+ case STATE_STOPPING: {
+ base::TimeDelta seek_time = GetPendingSeek();
+ if (seek_time != kNoTimestamp()) {
+ SetState(STATE_WAITING_FOR_SEEK);
+ SetPendingSeek(kNoTimestamp());
+ RequestDemuxerSeek(seek_time);
+ } else if (HasPendingStart()) {
SetPendingStart(false);
SetState(STATE_PREFETCHING);
StartPrefetchDecoders();
} else {
SetState(STATE_PAUSED);
}
- break;
+ } break;
case STATE_PLAYING:
// Unexpected stop means completion
SetState(STATE_PAUSED);
break;
default:
- DVLOG(0) << __FUNCTION__ << " illegal state: " << AsString(state_);
- NOTREACHED();
+ // DVLOG(0) << __FUNCTION__ << " illegal state: " << AsString(state_);
+ // NOTREACHED();
+ // Ignore! There can be a race condition: audio posts OnStopDone,
+ // then video posts, then first OnStopDone arrives at which point
+ // both streams are already stopped, then second OnStopDone arrives. When
+ // the second one arrives, the state us not STATE_STOPPING any more.
break;
}
@@ -502,7 +612,7 @@ void MediaCodecPlayer::SetPendingSurface(gfx::ScopedJavaSurface surface) {
video_decoder_->SetPendingSurface(surface.Pass());
}
-bool MediaCodecPlayer::HasPendingSurface() {
+bool MediaCodecPlayer::HasPendingSurface() const {
return video_decoder_->HasPendingSurface();
}
@@ -512,17 +622,28 @@ void MediaCodecPlayer::SetPendingStart(bool need_to_start) {
pending_start_ = need_to_start;
}
-bool MediaCodecPlayer::HasPendingStart() {
+bool MediaCodecPlayer::HasPendingStart() const {
DCHECK(GetMediaTaskRunner()->BelongsToCurrentThread());
return pending_start_;
}
-bool MediaCodecPlayer::HasAudio() {
+void MediaCodecPlayer::SetPendingSeek(base::TimeDelta timestamp) {
+ DCHECK(GetMediaTaskRunner()->BelongsToCurrentThread());
+ DVLOG(1) << __FUNCTION__ << ": " << timestamp;
+ pending_seek_ = timestamp;
+}
+
+base::TimeDelta MediaCodecPlayer::GetPendingSeek() const {
+ DCHECK(GetMediaTaskRunner()->BelongsToCurrentThread());
+ return pending_seek_;
+}
+
+bool MediaCodecPlayer::HasAudio() const {
DCHECK(GetMediaTaskRunner()->BelongsToCurrentThread());
return audio_decoder_->HasStream();
}
-bool MediaCodecPlayer::HasVideo() {
+bool MediaCodecPlayer::HasVideo() const {
DCHECK(GetMediaTaskRunner()->BelongsToCurrentThread());
return video_decoder_->HasStream();
}
@@ -664,6 +785,23 @@ void MediaCodecPlayer::RequestToStopDecoders() {
video_decoder_->RequestToStop();
}
+void MediaCodecPlayer::RequestDemuxerSeek(base::TimeDelta seek_time,
+ bool is_browser_seek) {
+ DCHECK(GetMediaTaskRunner()->BelongsToCurrentThread());
+ DVLOG(1) << __FUNCTION__ << " " << seek_time
+ << (is_browser_seek ? " browser_seek" : "");
+
+ // Flush decoders before requesting demuxer.
+ audio_decoder_->Flush();
+ video_decoder_->Flush();
+
+ // Save active seek data. Logically it is attached to STATE_WAITING_FOR_SEEK.
+ DCHECK(state_ == STATE_WAITING_FOR_SEEK);
+ seek_info_.reset(new SeekInfo(seek_time, is_browser_seek));
+
+ demuxer_->RequestDemuxerSeek(seek_time, is_browser_seek);
+}
+
void MediaCodecPlayer::ReleaseDecoderResources() {
DCHECK(GetMediaTaskRunner()->BelongsToCurrentThread());
DVLOG(1) << __FUNCTION__;
@@ -706,11 +844,11 @@ void MediaCodecPlayer::CreateDecoders() {
base::Bind(&MediaCodecPlayer::OnVideoCodecCreated, media_weak_this_)));
}
-bool MediaCodecPlayer::AudioFinished() {
+bool MediaCodecPlayer::AudioFinished() const {
return audio_decoder_->IsCompleted() || !audio_decoder_->HasStream();
}
-bool MediaCodecPlayer::VideoFinished() {
+bool MediaCodecPlayer::VideoFinished() const {
return video_decoder_->IsCompleted() || !video_decoder_->HasStream();
}
@@ -734,6 +872,7 @@ const char* MediaCodecPlayer::AsString(PlayerState state) {
RETURN_STRING(STATE_PLAYING);
RETURN_STRING(STATE_STOPPING);
RETURN_STRING(STATE_WAITING_FOR_SURFACE);
+ RETURN_STRING(STATE_WAITING_FOR_SEEK);
RETURN_STRING(STATE_ERROR);
}
return nullptr; // crash early
diff --git a/media/base/android/media_codec_player.h b/media/base/android/media_codec_player.h
index f9f9570..b76d543 100644
--- a/media/base/android/media_codec_player.h
+++ b/media/base/android/media_codec_player.h
@@ -31,18 +31,18 @@
// | <------------------[ WaitingForConfig ] [ Error ]
// |
// |
-// |
-// v
-// [ Prefetching ] -------------------
-// | |
-// | v
-// | <-----------------[ WaitingForSurface ]
-// v
-// [ Playing ]
-// |
-// |
-// v
-// [ Stopping ]
+// | <----------------------------------------------
+// v |
+// [ Prefetching ] ------------------- |
+// | | |
+// | v |
+// | <-----------------[ WaitingForSurface ] |
+// v |
+// [ Playing ] |
+// | |
+// | |
+// v |
+// [ Stopping ] --------------------------------> [ WaitingForSeek ]
// Events and actions for pause/resume workflow.
@@ -89,6 +89,50 @@
// | |
// ------------------------- [ Stopping ]
+
+// Events and actions for seek workflow.
+// -------------------------------------
+//
+// Seek: -- --
+// demuxer.RequestSeek | |
+// [ Paused ] -----------------------> | |
+// [ ] <----------------------- | |--
+// SeekDone: | | |
+// | | |
+// | | |
+// | | |
+// | | | Start:
+// Seek: | | | SetPendingStart
+// dec.Stop | | |
+// SetPendingSeek | | |
+// demuxer.RequestSeek | | |
+// [ Prefetching ] -----------------------> | | |
+// [ ] <---------------------- | | | Pause:
+// | SeekDone | | | RemovePendingStart
+// | w/pending start: | | |
+// | dec.Prefetch | Waiting | |
+// | | for | | Seek:
+// | | seek | | SetPendingSeek
+// | | | |
+// | PrefetchDone: dec.Start | | |
+// | | | | SeekDone
+// v | | | w/pending seek:
+// | | | demuxer.RequestSeek
+// [ Playing ] | | |
+// | | |
+// | | |<-
+// | Seek: SetPendingStart | |
+// | SetPendingSeek | |
+// | dec.RequestToStop | |
+// | | |
+// | | |
+// v | |
+// | |
+// [ Stopping ] -----------------------> | |
+// StopDone -- --
+// w/pending seek:
+// demuxer.RequestSeek
+
namespace media {
class BrowserCdm;
@@ -108,6 +152,9 @@ class MEDIA_EXPORT MediaCodecPlayer : public MediaPlayerAndroid,
typedef base::Callback<void(base::TimeDelta, base::TimeTicks)>
TimeUpdateCallback;
+ typedef base::Callback<void(const base::TimeDelta& current_timestamp)>
+ SeekDoneCallback;
+
// Constructs a player with the given ID and demuxer. |manager| must outlive
// the lifetime of this object.
MediaCodecPlayer(int player_id,
@@ -154,6 +201,7 @@ class MEDIA_EXPORT MediaCodecPlayer : public MediaPlayerAndroid,
STATE_PLAYING,
STATE_STOPPING,
STATE_WAITING_FOR_SURFACE,
+ STATE_WAITING_FOR_SEEK,
STATE_ERROR,
};
@@ -163,6 +211,14 @@ class MEDIA_EXPORT MediaCodecPlayer : public MediaPlayerAndroid,
gfx::Size video_size;
};
+ // Information about current seek in progress.
+ struct SeekInfo {
+ const base::TimeDelta seek_time;
+ const bool is_browser_seek;
+ SeekInfo(base::TimeDelta time, bool browser_seek)
+ : seek_time(time), is_browser_seek(browser_seek) {}
+ };
+
// MediaPlayerAndroid implementation.
// This method caches the data and calls manager's OnMediaMetadataChanged().
void OnMediaMetadataChanged(base::TimeDelta duration,
@@ -189,22 +245,26 @@ class MEDIA_EXPORT MediaCodecPlayer : public MediaPlayerAndroid,
// Operations called from the state machine.
void SetState(PlayerState new_state);
void SetPendingSurface(gfx::ScopedJavaSurface surface);
- bool HasPendingSurface();
+ bool HasPendingSurface() const;
void SetPendingStart(bool need_to_start);
- bool HasPendingStart();
- bool HasVideo();
- bool HasAudio();
+ bool HasPendingStart() const;
+ void SetPendingSeek(base::TimeDelta timestamp);
+ base::TimeDelta GetPendingSeek() const;
+ bool HasVideo() const;
+ bool HasAudio() const;
void SetDemuxerConfigs(const DemuxerConfigs& configs);
void StartPrefetchDecoders();
void StartPlaybackDecoders();
void StopDecoders();
void RequestToStopDecoders();
+ void RequestDemuxerSeek(base::TimeDelta seek_time,
+ bool is_browser_seek = false);
void ReleaseDecoderResources();
// Helper methods.
void CreateDecoders();
- bool AudioFinished();
- bool VideoFinished();
+ bool AudioFinished() const;
+ bool VideoFinished() const;
base::TimeDelta GetInterpolatedTime();
static const char* AsString(PlayerState state);
@@ -226,6 +286,7 @@ class MEDIA_EXPORT MediaCodecPlayer : public MediaPlayerAndroid,
base::Closure request_resources_cb_;
TimeUpdateCallback time_update_cb_;
base::Closure completion_cb_;
+ SeekDoneCallback seek_done_cb_;
// A callback that updates metadata cache and calls the manager.
MetadataChangedCallback metadata_changed_cb_;
@@ -252,6 +313,10 @@ class MEDIA_EXPORT MediaCodecPlayer : public MediaPlayerAndroid,
// Pending data to be picked up by the upcoming state.
gfx::ScopedJavaSurface pending_surface_;
bool pending_start_;
+ base::TimeDelta pending_seek_;
+
+ // Data associated with a seek in progress.
+ scoped_ptr<SeekInfo> seek_info_;
// Configuration data for the manager, accessed on the UI thread.
MediaMetadata metadata_cache_;
diff --git a/media/base/android/media_codec_player_unittest.cc b/media/base/android/media_codec_player_unittest.cc
index c917c7f..f818bac 100644
--- a/media/base/android/media_codec_player_unittest.cc
+++ b/media/base/android/media_codec_player_unittest.cc
@@ -11,6 +11,7 @@
#include "media/base/android/media_player_manager.h"
#include "media/base/android/test_data_factory.h"
#include "media/base/android/test_statistics.h"
+#include "media/base/buffers.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gl/android/surface_texture.h"
@@ -42,6 +43,16 @@ const base::TimeDelta kAudioFramePeriod =
base::TimeDelta::FromSecondsD(1024.0 / 44100); // 1024 samples @ 44100 Hz
const base::TimeDelta kVideoFramePeriod = base::TimeDelta::FromMilliseconds(20);
+// The predicate that always returns false, used for WaitForDelay implementation
+bool AlwaysFalse() {
+ return false;
+}
+
+// The method used to compare two TimeDelta values in expectations.
+bool AlmostEqual(base::TimeDelta a, base::TimeDelta b, double tolerance_ms) {
+ return (a - b).magnitude().InMilliseconds() <= tolerance_ms;
+}
+
// Mock of MediaPlayerManager for testing purpose.
class MockMediaPlayerManager : public MediaPlayerManager {
@@ -95,6 +106,10 @@ class MockMediaPlayerManager : public MediaPlayerManager {
// Conditions to wait for.
bool IsMetadataChanged() const { return media_metadata_.modified; }
bool IsPlaybackCompleted() const { return playback_completed_; }
+ bool IsPlaybackStarted() const { return pts_stat_.num_values() > 0; }
+ bool IsPlaybackBeyondPosition(const base::TimeDelta& pts) const {
+ return pts_stat_.max() > pts;
+ }
struct MediaMetadata {
base::TimeDelta duration;
@@ -209,7 +224,7 @@ class MockDemuxerAndroid : public DemuxerAndroid {
void Initialize(DemuxerAndroidClient* client) override;
void RequestDemuxerData(DemuxerStream::Type type) override;
void RequestDemuxerSeek(const base::TimeDelta& time_to_seek,
- bool is_browser_seek) override {}
+ bool is_browser_seek) override;
// Sets the audio data factory.
void SetAudioFactory(scoped_ptr<TestDataFactory> factory) {
@@ -272,6 +287,23 @@ void MockDemuxerAndroid::RequestDemuxerData(DemuxerStream::Type type) {
delay);
}
+void MockDemuxerAndroid::RequestDemuxerSeek(const base::TimeDelta& time_to_seek,
+ bool is_browser_seek) {
+ // Tell data factories to start next chunk with the new timestamp.
+ if (audio_factory_)
+ audio_factory_->SeekTo(time_to_seek);
+ if (video_factory_)
+ video_factory_->SeekTo(time_to_seek);
+
+ // Post OnDemuxerSeekDone() to the player.
+ DCHECK(client_);
+ base::TimeDelta reported_seek_time =
+ is_browser_seek ? time_to_seek : kNoTimestamp();
+ GetMediaTaskRunner()->PostTask(
+ FROM_HERE, base::Bind(&DemuxerAndroidClient::OnDemuxerSeekDone,
+ base::Unretained(client_), reported_seek_time));
+}
+
void MockDemuxerAndroid::PostConfigs(const DemuxerConfigs& configs) {
RUN_ON_MEDIA_THREAD(MockDemuxerAndroid, PostConfigs, configs);
@@ -307,6 +339,9 @@ class MediaCodecPlayerTest : public testing::Test {
MediaCodecPlayerTest();
~MediaCodecPlayerTest() override;
+ // Conditions to wait for.
+ bool IsPaused() const { return !(player_ && player_->IsPlaying()); }
+
protected:
typedef base::Callback<bool()> Predicate;
@@ -318,6 +353,16 @@ class MediaCodecPlayerTest : public testing::Test {
bool WaitForCondition(const Predicate& condition,
const base::TimeDelta& timeout = kDefaultTimeout);
+ // Waits for timeout to expire.
+ void WaitForDelay(const base::TimeDelta& timeout);
+
+ // Waits till playback position as determined by maximal reported pts
+ // reaches the given value or for timeout to expire. Returns true if the
+ // playback has passed the given position.
+ bool WaitForPlaybackBeyondPosition(
+ const base::TimeDelta& pts,
+ const base::TimeDelta& timeout = kDefaultTimeout);
+
base::MessageLoop message_loop_;
MockMediaPlayerManager manager_;
MockDemuxerAndroid* demuxer_; // owned by player_
@@ -386,6 +431,19 @@ bool MediaCodecPlayerTest::WaitForCondition(const Predicate& condition,
return false;
}
+void MediaCodecPlayerTest::WaitForDelay(const base::TimeDelta& timeout) {
+ WaitForCondition(base::Bind(&AlwaysFalse), timeout);
+}
+
+bool MediaCodecPlayerTest::WaitForPlaybackBeyondPosition(
+ const base::TimeDelta& pts,
+ const base::TimeDelta& timeout) {
+ return WaitForCondition(
+ base::Bind(&MockMediaPlayerManager::IsPlaybackBeyondPosition,
+ base::Unretained(&manager_), pts),
+ timeout);
+}
+
TEST_F(MediaCodecPlayerTest, SetAudioConfigsBeforePlayerCreation) {
// Post configuration when there is no player yet.
EXPECT_EQ(nullptr, player_);
@@ -519,4 +577,169 @@ TEST_F(MediaCodecPlayerTest, VideoPlayTillCompletion) {
EXPECT_LE(duration, manager_.pts_stat_.max());
}
+TEST_F(MediaCodecPlayerTest, AudioSeekAfterStop) {
+ SKIP_TEST_IF_MEDIA_CODEC_BRIDGE_IS_NOT_AVAILABLE();
+
+ // Play for 300 ms, then Pause, then Seek to beginning. The playback should
+ // start from the beginning.
+
+ base::TimeDelta duration = base::TimeDelta::FromMilliseconds(2000);
+
+ demuxer_->SetAudioFactory(
+ scoped_ptr<AudioFactory>(new AudioFactory(duration)));
+
+ CreatePlayer();
+
+ // Post configuration.
+ demuxer_->PostInternalConfigs();
+
+ // Start the player.
+ player_->Start();
+
+ // Wait for playback to start.
+ EXPECT_TRUE(
+ WaitForCondition(base::Bind(&MockMediaPlayerManager::IsPlaybackStarted,
+ base::Unretained(&manager_))));
+
+ // Wait for 300 ms and stop. The 300 ms interval takes into account potential
+ // audio delay: audio takes time reconfiguring after the first several packets
+ // get written to the audio track.
+ WaitForDelay(base::TimeDelta::FromMilliseconds(300));
+
+ player_->Pause(true);
+
+ // Make sure we played at least 100 ms.
+ EXPECT_LT(base::TimeDelta::FromMilliseconds(100), manager_.pts_stat_.max());
+
+ // Wait till the Pause is completed.
+ EXPECT_TRUE(WaitForCondition(
+ base::Bind(&MediaCodecPlayerTest::IsPaused, base::Unretained(this))));
+
+ // Clear statistics.
+ manager_.pts_stat_.Clear();
+
+ // Now we can seek to the beginning and start the playback.
+ player_->SeekTo(base::TimeDelta());
+
+ player_->Start();
+
+ // Wait for playback to start.
+ EXPECT_TRUE(
+ WaitForCondition(base::Bind(&MockMediaPlayerManager::IsPlaybackStarted,
+ base::Unretained(&manager_))));
+
+ // Make sure we started from the beginninig
+ EXPECT_GT(base::TimeDelta::FromMilliseconds(40), manager_.pts_stat_.min());
+}
+
+TEST_F(MediaCodecPlayerTest, AudioSeekThenPlay) {
+ SKIP_TEST_IF_MEDIA_CODEC_BRIDGE_IS_NOT_AVAILABLE();
+
+ // Issue Seek command immediately followed by Start. The playback should
+ // start at the seek position.
+
+ base::TimeDelta duration = base::TimeDelta::FromMilliseconds(2000);
+ base::TimeDelta seek_position = base::TimeDelta::FromMilliseconds(500);
+
+ demuxer_->SetAudioFactory(
+ scoped_ptr<AudioFactory>(new AudioFactory(duration)));
+
+ CreatePlayer();
+
+ // Post configuration.
+ demuxer_->PostInternalConfigs();
+
+ // Seek and immediately start.
+ player_->SeekTo(seek_position);
+ player_->Start();
+
+ // Wait for playback to start.
+ EXPECT_TRUE(
+ WaitForCondition(base::Bind(&MockMediaPlayerManager::IsPlaybackStarted,
+ base::Unretained(&manager_))));
+
+ // The playback should start at |seek_position|
+ EXPECT_TRUE(AlmostEqual(seek_position, manager_.pts_stat_.min(), 1));
+}
+
+TEST_F(MediaCodecPlayerTest, AudioSeekThenPlayThenConfig) {
+ SKIP_TEST_IF_MEDIA_CODEC_BRIDGE_IS_NOT_AVAILABLE();
+
+ // Issue Seek command immediately followed by Start but without prior demuxer
+ // configuration. Start should wait for configuration. After it has been
+ // posted the playback should start at the seek position.
+
+ base::TimeDelta duration = base::TimeDelta::FromMilliseconds(2000);
+ base::TimeDelta seek_position = base::TimeDelta::FromMilliseconds(500);
+
+ demuxer_->SetAudioFactory(
+ scoped_ptr<AudioFactory>(new AudioFactory(duration)));
+
+ CreatePlayer();
+
+ // Seek and immediately start.
+ player_->SeekTo(seek_position);
+ player_->Start();
+
+ // Make sure the player is waiting.
+ WaitForDelay(base::TimeDelta::FromMilliseconds(200));
+ EXPECT_FALSE(player_->IsPlaying());
+
+ // Post configuration.
+ demuxer_->PostInternalConfigs();
+
+ // Wait for playback to start.
+ EXPECT_TRUE(
+ WaitForCondition(base::Bind(&MockMediaPlayerManager::IsPlaybackStarted,
+ base::Unretained(&manager_))));
+
+ // The playback should start at |seek_position|
+ EXPECT_TRUE(AlmostEqual(seek_position, manager_.pts_stat_.min(), 1));
+}
+
+TEST_F(MediaCodecPlayerTest, AudioSeekWhilePlaying) {
+ SKIP_TEST_IF_MEDIA_CODEC_BRIDGE_IS_NOT_AVAILABLE();
+
+ // Play for 300 ms, then issue several Seek commands in the row.
+ // The playback should continue at the last seek position.
+
+ // To test this condition without analyzing the reported time details
+ // and without introducing dependency on implementation I make a long (10s)
+ // duration and test that the playback resumes after big time jump (5s) in a
+ // short period of time (200 ms).
+ base::TimeDelta duration = base::TimeDelta::FromSeconds(10);
+
+ demuxer_->SetAudioFactory(
+ scoped_ptr<AudioFactory>(new AudioFactory(duration)));
+
+ CreatePlayer();
+
+ // Post configuration.
+ demuxer_->PostInternalConfigs();
+
+ // Start the player.
+ player_->Start();
+
+ // Wait for playback to start.
+ EXPECT_TRUE(
+ WaitForCondition(base::Bind(&MockMediaPlayerManager::IsPlaybackStarted,
+ base::Unretained(&manager_))));
+
+ // Wait for 300 ms.
+ WaitForDelay(base::TimeDelta::FromMilliseconds(300));
+
+ // Make sure we played at least 100 ms.
+ EXPECT_LT(base::TimeDelta::FromMilliseconds(100), manager_.pts_stat_.max());
+
+ // Seek forward several times.
+ player_->SeekTo(base::TimeDelta::FromSeconds(3));
+ player_->SeekTo(base::TimeDelta::FromSeconds(4));
+ player_->SeekTo(base::TimeDelta::FromSeconds(5));
+
+ // Make sure that we reached the last timestamp within default timeout,
+ // i.e. 200 ms.
+ EXPECT_TRUE(WaitForPlaybackBeyondPosition(base::TimeDelta::FromSeconds(5)));
+ EXPECT_TRUE(player_->IsPlaying());
+}
+
} // namespace media
diff --git a/media/base/android/media_codec_video_decoder.cc b/media/base/android/media_codec_video_decoder.cc
index 2f8c11e..f44b819 100644
--- a/media/base/android/media_codec_video_decoder.cc
+++ b/media/base/android/media_codec_video_decoder.cc
@@ -244,6 +244,16 @@ void MediaCodecVideoDecoder::ReleaseDelayedBuffers() {
delayed_buffers_.clear();
}
+#ifndef NDEBUG
+void MediaCodecVideoDecoder::VerifyUnitIsKeyFrame(
+ const AccessUnit* unit) const {
+ // The first video frame in a sequence must be a key frame or stand-alone EOS.
+ DCHECK(unit);
+ bool stand_alone_eos = unit->is_end_of_stream && unit->data.empty();
+ DCHECK(stand_alone_eos || unit->is_key_frame);
+}
+#endif
+
void MediaCodecVideoDecoder::ReleaseOutputBuffer(int buffer_index,
base::TimeDelta pts,
size_t size,
diff --git a/media/base/android/media_codec_video_decoder.h b/media/base/android/media_codec_video_decoder.h
index 1bec99e..ed455c8 100644
--- a/media/base/android/media_codec_video_decoder.h
+++ b/media/base/android/media_codec_video_decoder.h
@@ -64,6 +64,10 @@ class MediaCodecVideoDecoder : public MediaCodecDecoder {
int NumDelayedRenderTasks() const override;
void ReleaseDelayedBuffers() override;
+#ifndef NDEBUG
+ void VerifyUnitIsKeyFrame(const AccessUnit* unit) const override;
+#endif
+
private:
// A helper method that releases output buffers and does
// post-release checks. Might be called by Render() or posted
diff --git a/media/base/android/test_data_factory.cc b/media/base/android/test_data_factory.cc
index c53f8bc5e..0bf3782 100644
--- a/media/base/android/test_data_factory.cc
+++ b/media/base/android/test_data_factory.cc
@@ -101,6 +101,11 @@ bool TestDataFactory::CreateChunk(DemuxerData* chunk, base::TimeDelta* delay) {
return true;
}
+void TestDataFactory::SeekTo(const base::TimeDelta& seek_time) {
+ regular_pts_ = seek_time;
+ last_pts_ = base::TimeDelta();
+}
+
void TestDataFactory::LoadPackets(const char* file_name_template) {
for (int i = 0; i < 4; ++i) {
scoped_refptr<DecoderBuffer> buffer =
diff --git a/media/base/android/test_data_factory.h b/media/base/android/test_data_factory.h
index 0287e61..f1f35a7 100644
--- a/media/base/android/test_data_factory.h
+++ b/media/base/android/test_data_factory.h
@@ -50,6 +50,11 @@ class TestDataFactory {
// In starvation mode we do not add EOS at the end.
void SetStarvationMode(bool value) { starvation_mode_ = value; }
+ // Resets the timestamp for the next access unit.
+ void SeekTo(const base::TimeDelta& seek_time);
+
+ // Returns the maximum PTS, taking into account possible modifications
+ // by subclasses. The SeekTo() resets this value.
base::TimeDelta last_pts() const { return last_pts_; }
protected:
@@ -64,7 +69,7 @@ class TestDataFactory {
base::TimeDelta frame_period_;
std::vector<uint8_t> packet_[4];
base::TimeDelta regular_pts_; // monotonically increasing PTS
- base::TimeDelta last_pts_; // subclass can modify PTS, maintains the last
+ base::TimeDelta last_pts_; // subclass can modify PTS, maintain the last
bool starvation_mode_; // true means no EOS at the end
};
diff --git a/media/base/android/test_statistics.h b/media/base/android/test_statistics.h
index 35fce46..9c889b4 100644
--- a/media/base/android/test_statistics.h
+++ b/media/base/android/test_statistics.h
@@ -28,6 +28,12 @@ class Minimax {
++num_values_;
}
+ void Clear() {
+ min_ = T();
+ max_ = T();
+ num_values_ = 0;
+ }
+
const T& min() const { return min_; }
const T& max() const { return max_; }
int num_values() const { return num_values_; }