diff options
author | timav <timav@chromium.org> | 2015-07-24 18:05:13 -0700 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2015-07-25 01:05:37 +0000 |
commit | 634a28cba4755cc6aaf2944b0138b7340f854c4b (patch) | |
tree | ebbfedad6b2fd3908fd0802473e13cfd80020c75 /media/base | |
parent | c3f6e0c653bc113b52540ae695ef17830c13c6c2 (diff) | |
download | chromium_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.cc | 2 | ||||
-rw-r--r-- | media/base/android/demuxer_stream_player_params.cc | 16 | ||||
-rw-r--r-- | media/base/android/media_codec_decoder.cc | 64 | ||||
-rw-r--r-- | media/base/android/media_codec_decoder.h | 60 | ||||
-rw-r--r-- | media/base/android/media_codec_player.cc | 181 | ||||
-rw-r--r-- | media/base/android/media_codec_player.h | 101 | ||||
-rw-r--r-- | media/base/android/media_codec_player_unittest.cc | 225 | ||||
-rw-r--r-- | media/base/android/media_codec_video_decoder.cc | 10 | ||||
-rw-r--r-- | media/base/android/media_codec_video_decoder.h | 4 | ||||
-rw-r--r-- | media/base/android/test_data_factory.cc | 5 | ||||
-rw-r--r-- | media/base/android/test_data_factory.h | 7 | ||||
-rw-r--r-- | media/base/android/test_statistics.h | 6 |
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_; } |