diff options
author | Torne (Richard Coles) <torne@google.com> | 2013-10-31 11:16:26 +0000 |
---|---|---|
committer | Torne (Richard Coles) <torne@google.com> | 2013-10-31 11:16:26 +0000 |
commit | 1e9bf3e0803691d0a228da41fc608347b6db4340 (patch) | |
tree | ab2e5565f71b4219b3da406e19f16fe306704ef5 /media/base | |
parent | f10b58d5bc6ae3e74076fc4ccca14cbc57ef805c (diff) | |
download | chromium_org-1e9bf3e0803691d0a228da41fc608347b6db4340.tar.gz |
Merge from Chromium at DEPS revision 232015
This commit was generated by merge_to_master.py.
Change-Id: If86767ad396b9e2e1a4c1e9df1427daea29703ef
Diffstat (limited to 'media/base')
21 files changed, 1520 insertions, 677 deletions
diff --git a/media/base/android/demuxer_android.h b/media/base/android/demuxer_android.h index 579154518c..865dc9d33f 100644 --- a/media/base/android/demuxer_android.h +++ b/media/base/android/demuxer_android.h @@ -32,7 +32,13 @@ class MEDIA_EXPORT DemuxerAndroid { virtual void RequestDemuxerData(media::DemuxerStream::Type type) = 0; // Called to request the demuxer to seek to a particular media time. - virtual void RequestDemuxerSeek(const base::TimeDelta& time_to_seek) = 0; + // |is_browser_seek| is true if the renderer is not previously expecting this + // seek and must coordinate with other regular seeks. Browser seek existence + // should be hidden as much as possible from the renderer player and web apps. + // TODO(wolenetz): Instead of doing browser seek, replay cached data since + // last keyframe. See http://crbug.com/304234. + virtual void RequestDemuxerSeek(const base::TimeDelta& time_to_seek, + bool is_browser_seek) = 0; }; // Defines the client callback interface. @@ -50,7 +56,13 @@ class MEDIA_EXPORT DemuxerAndroidClient { virtual void OnDemuxerDataAvailable(const DemuxerData& params) = 0; // Called in response to RequestDemuxerSeek(). - virtual void OnDemuxerSeekDone() = 0; + // If this is in response to a request with |is_browser_seek| set to true, + // then |actual_browser_seek_time| may differ from the requested + // |time_to_seek|, and reflects the actual time seeked to by the demuxer. + // For regular demuxer seeks, |actual_browser_seek_time| is kNoTimestamp() and + // should be ignored by browser player. + virtual void OnDemuxerSeekDone( + const base::TimeDelta& actual_browser_seek_time) = 0; // Called whenever the demuxer has detected a duration change. virtual void OnDemuxerDurationChanged(base::TimeDelta duration) = 0; diff --git a/media/base/android/java/src/org/chromium/media/MediaCodecBridge.java b/media/base/android/java/src/org/chromium/media/MediaCodecBridge.java index d9bb48b839..74bec56745 100644 --- a/media/base/android/java/src/org/chromium/media/MediaCodecBridge.java +++ b/media/base/android/java/src/org/chromium/media/MediaCodecBridge.java @@ -81,11 +81,13 @@ class MediaCodecBridge { * This class represents supported android codec information. */ private static class CodecInfo { - private final String mCodecType; + private final String mCodecType; // e.g. "video/x-vnd.on2.vp8". + private final String mCodecName; // e.g. "OMX.google.vp8.decoder". private final boolean mIsSecureDecoderSupported; - private CodecInfo(String codecType, boolean isSecureDecoderSupported) { + private CodecInfo(String codecType, String codecName, boolean isSecureDecoderSupported) { mCodecType = codecType; + mCodecName = codecName; mIsSecureDecoderSupported = isSecureDecoderSupported; } @@ -93,6 +95,9 @@ class MediaCodecBridge { private String codecType() { return mCodecType; } @CalledByNative("CodecInfo") + private String codecName() { return mCodecName; } + + @CalledByNative("CodecInfo") private boolean isSecureDecoderSupported() { return mIsSecureDecoderSupported; } } @@ -159,8 +164,8 @@ class MediaCodecBridge { } for (int j = 0; j < supportedTypes.length; ++j) { if (!CodecInfoMap.containsKey(supportedTypes[j]) || secureDecoderSupported) { - CodecInfoMap.put(supportedTypes[j], - new CodecInfo(supportedTypes[j], secureDecoderSupported)); + CodecInfoMap.put(supportedTypes[j], new CodecInfo( + supportedTypes[j], codecString, secureDecoderSupported)); } } } diff --git a/media/base/android/java/src/org/chromium/media/WebAudioMediaCodecBridge.java b/media/base/android/java/src/org/chromium/media/WebAudioMediaCodecBridge.java index 1de7e42b8d..17720e6671 100644 --- a/media/base/android/java/src/org/chromium/media/WebAudioMediaCodecBridge.java +++ b/media/base/android/java/src/org/chromium/media/WebAudioMediaCodecBridge.java @@ -21,7 +21,6 @@ import org.chromium.base.JNINamespace; @JNINamespace("media") class WebAudioMediaCodecBridge { - private static final boolean DEBUG = true; static final String LOG_TAG = "WebAudioMediaCodec"; // TODO(rtoy): What is the correct timeout value for reading // from a file in memory? @@ -81,18 +80,8 @@ class WebAudioMediaCodecBridge { } } - if (DEBUG) { - Log.d(LOG_TAG, "Tracks: " + extractor.getTrackCount() - + " Rate: " + sampleRate - + " Channels: " + inputChannelCount - + " Mime: " + mime - + " Duration: " + durationMicroseconds + " microsec"); - } - - nativeInitializeDestination(nativeMediaCodecBridge, - inputChannelCount, - sampleRate, - durationMicroseconds); + Log.d(LOG_TAG, "Initial: Tracks: " + extractor.getTrackCount() + + " Format: " + format); // Create decoder MediaCodec codec = MediaCodec.createDecoderByType(mime); @@ -107,6 +96,7 @@ class WebAudioMediaCodecBridge { boolean sawInputEOS = false; boolean sawOutputEOS = false; + boolean destinationInitialized = false; // Keep processing until the output is done. while (!sawOutputEOS) { @@ -145,7 +135,24 @@ class WebAudioMediaCodecBridge { if (outputBufIndex >= 0) { ByteBuffer buf = codecOutputBuffers[outputBufIndex]; - if (info.size > 0) { + if (!destinationInitialized) { + // Initialize the destination as late as possible to + // catch any changes in format. But be sure to + // initialize it BEFORE we send any decoded audio, + // and only initialize once. + Log.d(LOG_TAG, "Final: Rate: " + sampleRate + + " Channels: " + inputChannelCount + + " Mime: " + mime + + " Duration: " + durationMicroseconds + " microsec"); + + nativeInitializeDestination(nativeMediaCodecBridge, + inputChannelCount, + sampleRate, + durationMicroseconds); + destinationInitialized = true; + } + + if (destinationInitialized && info.size > 0) { nativeOnChunkDecoded(nativeMediaCodecBridge, buf, info.size, inputChannelCount, outputChannelCount); } @@ -161,6 +168,7 @@ class WebAudioMediaCodecBridge { } else if (outputBufIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { MediaFormat newFormat = codec.getOutputFormat(); outputChannelCount = newFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT); + sampleRate = newFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE); Log.d(LOG_TAG, "output format changed to " + newFormat); } } diff --git a/media/base/android/media_codec_bridge.cc b/media/base/android/media_codec_bridge.cc index 75d103c353..1c4928aad9 100644 --- a/media/base/android/media_codec_bridge.cc +++ b/media/base/android/media_codec_bridge.cc @@ -15,6 +15,7 @@ #include "base/lazy_instance.h" #include "base/logging.h" #include "base/safe_numerics.h" +#include "base/strings/string_util.h" #include "base/strings/stringprintf.h" #include "jni/MediaCodecBridge_jni.h" #include "media/base/bit_reader.h" @@ -29,7 +30,7 @@ namespace media { enum { kBufferFlagEndOfStream = 4 }; -static const std::string AudioCodecToAndroidMimeType(const AudioCodec codec) { +static const std::string AudioCodecToAndroidMimeType(const AudioCodec& codec) { switch (codec) { case kCodecMP3: return "audio/mpeg"; @@ -42,7 +43,7 @@ static const std::string AudioCodecToAndroidMimeType(const AudioCodec codec) { } } -static const std::string VideoCodecToAndroidMimeType(const VideoCodec codec) { +static const std::string VideoCodecToAndroidMimeType(const VideoCodec& codec) { switch (codec) { case kCodecH264: return "video/avc"; @@ -106,6 +107,7 @@ void MediaCodecBridge::GetCodecsInfo( return; std::string mime_type; + std::string codec_name; ScopedJavaLocalRef<jobjectArray> j_codec_info_array = Java_MediaCodecBridge_getCodecsInfo(env); jsize len = env->GetArrayLength(j_codec_info_array.obj()); @@ -115,8 +117,11 @@ void MediaCodecBridge::GetCodecsInfo( ScopedJavaLocalRef<jstring> j_codec_type = Java_CodecInfo_codecType(env, j_info.obj()); ConvertJavaStringToUTF8(env, j_codec_type.obj(), &mime_type); + ScopedJavaLocalRef<jstring> j_codec_name = + Java_CodecInfo_codecName(env, j_info.obj()); CodecsInfo info; info.codecs = AndroidMimeTypeToCodecType(mime_type); + ConvertJavaStringToUTF8(env, j_codec_name.obj(), &info.name); info.secure_decoder_supported = Java_CodecInfo_isSecureDecoderSupported(env, j_info.obj()); codecs_info->push_back(info); @@ -139,6 +144,22 @@ bool MediaCodecBridge::CanDecode(const std::string& codec, bool is_secure) { return false; } +// static +bool MediaCodecBridge::IsKnownUnaccelerated(const std::string& mime_type) { + std::string codec_type = AndroidMimeTypeToCodecType(mime_type); + std::vector<media::MediaCodecBridge::CodecsInfo> codecs_info; + media::MediaCodecBridge::GetCodecsInfo(&codecs_info); + for (size_t i = 0; i < codecs_info.size(); ++i) { + if (codecs_info[i].codecs == codec_type) { + // It would be nice if MediaCodecInfo externalized some notion of + // HW-acceleration but it doesn't. Android Media guidance is that the + // prefix below is always used for SW decoders, so that's what we use. + return StartsWithASCII(codecs_info[i].name, "OMX.google.", true); + } + } + return true; +} + MediaCodecBridge::MediaCodecBridge(const std::string& mime, bool is_secure) { JNIEnv* env = AttachCurrentThread(); CHECK(env); @@ -312,7 +333,7 @@ AudioCodecBridge::AudioCodecBridge(const std::string& mime) } bool AudioCodecBridge::Start( - const AudioCodec codec, int sample_rate, int channel_count, + const AudioCodec& codec, int sample_rate, int channel_count, const uint8* extra_data, size_t extra_data_size, bool play_audio, jobject media_crypto) { JNIEnv* env = AttachCurrentThread(); @@ -343,7 +364,7 @@ bool AudioCodecBridge::Start( } bool AudioCodecBridge::ConfigureMediaFormat( - jobject j_format, const AudioCodec codec, const uint8* extra_data, + jobject j_format, const AudioCodec& codec, const uint8* extra_data, size_t extra_data_size) { if (extra_data_size == 0) return true; @@ -470,7 +491,7 @@ VideoCodecBridge::VideoCodecBridge(const std::string& mime, bool is_secure) } bool VideoCodecBridge::Start( - const VideoCodec codec, const gfx::Size& size, jobject surface, + const VideoCodec& codec, const gfx::Size& size, jobject surface, jobject media_crypto) { JNIEnv* env = AttachCurrentThread(); @@ -495,17 +516,29 @@ bool VideoCodecBridge::Start( return StartInternal(); } -AudioCodecBridge* AudioCodecBridge::Create(const AudioCodec codec) { +AudioCodecBridge* AudioCodecBridge::Create(const AudioCodec& codec) { const std::string mime = AudioCodecToAndroidMimeType(codec); return mime.empty() ? NULL : new AudioCodecBridge(mime); } -VideoCodecBridge* VideoCodecBridge::Create(const VideoCodec codec, +// static +bool AudioCodecBridge::IsKnownUnaccelerated(const AudioCodec& codec) { + return MediaCodecBridge::IsKnownUnaccelerated( + AudioCodecToAndroidMimeType(codec)); +} + +VideoCodecBridge* VideoCodecBridge::Create(const VideoCodec& codec, bool is_secure) { const std::string mime = VideoCodecToAndroidMimeType(codec); return mime.empty() ? NULL : new VideoCodecBridge(mime, is_secure); } +// static +bool VideoCodecBridge::IsKnownUnaccelerated(const VideoCodec& codec) { + return MediaCodecBridge::IsKnownUnaccelerated( + VideoCodecToAndroidMimeType(codec)); +} + bool MediaCodecBridge::RegisterMediaCodecBridge(JNIEnv* env) { return RegisterNativesImpl(env); } diff --git a/media/base/android/media_codec_bridge.h b/media/base/android/media_codec_bridge.h index 15b707c18a..64c73571c6 100644 --- a/media/base/android/media_codec_bridge.h +++ b/media/base/android/media_codec_bridge.h @@ -54,6 +54,7 @@ class MEDIA_EXPORT MediaCodecBridge { // need more specific codecs separated by comma. (e.g. "vp8" -> "vp8, vp8.0") struct CodecsInfo { std::string codecs; + std::string name; bool secure_decoder_supported; }; @@ -137,6 +138,10 @@ class MEDIA_EXPORT MediaCodecBridge { static bool RegisterMediaCodecBridge(JNIEnv* env); protected: + // Returns true if |mime_type| is known to be unaccelerated (i.e. backed by a + // software codec instead of a hardware one). + static bool IsKnownUnaccelerated(const std::string& mime_type); + MediaCodecBridge(const std::string& mime, bool is_secure); // Calls start() against the media codec instance. Used in StartXXX() after @@ -163,10 +168,13 @@ class AudioCodecBridge : public MediaCodecBridge { public: // Returns an AudioCodecBridge instance if |codec| is supported, or a NULL // pointer otherwise. - static AudioCodecBridge* Create(const AudioCodec codec); + static AudioCodecBridge* Create(const AudioCodec& codec); + + // See MediaCodecBridge::IsKnownUnaccelerated(). + static bool IsKnownUnaccelerated(const AudioCodec& codec); // Start the audio codec bridge. - bool Start(const AudioCodec codec, int sample_rate, int channel_count, + bool Start(const AudioCodec& codec, int sample_rate, int channel_count, const uint8* extra_data, size_t extra_data_size, bool play_audio, jobject media_crypto) WARN_UNUSED_RESULT; @@ -181,7 +189,7 @@ class AudioCodecBridge : public MediaCodecBridge { explicit AudioCodecBridge(const std::string& mime); // Configure the java MediaFormat object with the extra codec data passed in. - bool ConfigureMediaFormat(jobject j_format, const AudioCodec codec, + bool ConfigureMediaFormat(jobject j_format, const AudioCodec& codec, const uint8* extra_data, size_t extra_data_size); }; @@ -189,11 +197,14 @@ class MEDIA_EXPORT VideoCodecBridge : public MediaCodecBridge { public: // Returns an VideoCodecBridge instance if |codec| is supported, or a NULL // pointer otherwise. - static VideoCodecBridge* Create(const VideoCodec codec, bool is_secure); + static VideoCodecBridge* Create(const VideoCodec& codec, bool is_secure); + + // See MediaCodecBridge::IsKnownUnaccelerated(). + static bool IsKnownUnaccelerated(const VideoCodec& codec); // Start the video codec bridge. // TODO(qinmin): Pass codec specific data if available. - bool Start(const VideoCodec codec, const gfx::Size& size, jobject surface, + bool Start(const VideoCodec& codec, const gfx::Size& size, jobject surface, jobject media_crypto); private: diff --git a/media/base/android/media_decoder_job.cc b/media/base/android/media_decoder_job.cc index 59db63a6e8..cd8d04cfe3 100644 --- a/media/base/android/media_decoder_job.cc +++ b/media/base/android/media_decoder_job.cc @@ -28,6 +28,7 @@ MediaDecoderJob::MediaDecoderJob( media_codec_bridge_(media_codec_bridge), needs_flush_(false), input_eos_encountered_(false), + prerolling_(true), weak_this_(this), request_data_cb_(request_data_cb), access_unit_index_(0), @@ -76,7 +77,7 @@ void MediaDecoderJob::Prefetch(const base::Closure& prefetch_cb) { bool MediaDecoderJob::Decode( const base::TimeTicks& start_time_ticks, const base::TimeDelta& start_presentation_timestamp, - const MediaDecoderJob::DecoderCallback& callback) { + const DecoderCallback& callback) { DCHECK(decode_cb_.is_null()); DCHECK(on_data_received_cb_.is_null()); DCHECK(ui_loop_->BelongsToCurrentThread()); @@ -121,10 +122,22 @@ void MediaDecoderJob::Flush() { on_data_received_cb_.Reset(); } +void MediaDecoderJob::BeginPrerolling( + const base::TimeDelta& preroll_timestamp) { + DVLOG(1) << __FUNCTION__ << "(" << preroll_timestamp.InSecondsF() << ")"; + DCHECK(ui_loop_->BelongsToCurrentThread()); + DCHECK(!is_decoding()); + + preroll_timestamp_ = preroll_timestamp; + prerolling_ = true; +} + void MediaDecoderJob::Release() { DCHECK(ui_loop_->BelongsToCurrentThread()); - destroy_pending_ = is_decoding(); + // If the decoder job is not waiting for data, and is still decoding, we + // cannot delete the job immediately. + destroy_pending_ = on_data_received_cb_.is_null() && is_decoding(); request_data_cb_.Reset(); on_data_received_cb_.Reset(); @@ -221,6 +234,19 @@ void MediaDecoderJob::DecodeNextAccessUnit( DCHECK(ui_loop_->BelongsToCurrentThread()); DCHECK(!decode_cb_.is_null()); + // If the first access unit is a config change, request the player to dequeue + // the input buffer again so that it can request config data. + if (received_data_.access_units[access_unit_index_].status == + DemuxerStream::kConfigChanged) { + ui_loop_->PostTask(FROM_HERE, + base::Bind(&MediaDecoderJob::OnDecodeCompleted, + base::Unretained(this), + MEDIA_CODEC_DEQUEUE_INPUT_AGAIN_LATER, + kNoTimestamp(), + 0)); + return; + } + decoder_loop_->PostTask(FROM_HERE, base::Bind( &MediaDecoderJob::DecodeInternal, base::Unretained(this), received_data_.access_units[access_unit_index_], @@ -282,11 +308,9 @@ void MediaDecoderJob::DecodeInternal( &output_eos_encountered); if (status != MEDIA_CODEC_OK) { - if (status == MEDIA_CODEC_OUTPUT_BUFFERS_CHANGED) { - if (media_codec_bridge_->GetOutputBuffers()) - status = MEDIA_CODEC_OK; - else - status = MEDIA_CODEC_ERROR; + if (status == MEDIA_CODEC_OUTPUT_BUFFERS_CHANGED && + !media_codec_bridge_->GetOutputBuffers()) { + status = MEDIA_CODEC_ERROR; } callback.Run(status, kNoTimestamp(), 0); return; @@ -299,9 +323,12 @@ void MediaDecoderJob::DecodeInternal( status = MEDIA_CODEC_INPUT_END_OF_STREAM; // Check whether we need to render the output. - // TODO(qinmin): comparing |unit.timestamp| with |preroll_timestamp_| is not - // accurate due to data reordering. Need to use the |presentation_timestamp| - // for video, and use |size| to calculate the timestamp for audio. + // TODO(qinmin): comparing most recently queued input's |unit.timestamp| with + // |preroll_timestamp_| is not accurate due to data reordering and possible + // input queueing without immediate dequeue when |input_status| != + // |MEDIA_CODEC_OK|. Need to use the |presentation_timestamp| for video, and + // use |size| to calculate the timestamp for audio. See + // http://crbug.com/310823 and b/11356652. bool render_output = unit.timestamp >= preroll_timestamp_ && (status != MEDIA_CODEC_OUTPUT_END_OF_STREAM || size != 0u); base::TimeDelta time_to_render; @@ -350,6 +377,11 @@ void MediaDecoderJob::OnDecodeCompleted( } DCHECK(!decode_cb_.is_null()); + + // If output was queued for rendering, then we have completed prerolling. + if (presentation_timestamp != kNoTimestamp()) + prerolling_ = false; + switch (status) { case MEDIA_CODEC_OK: case MEDIA_CODEC_DEQUEUE_OUTPUT_AGAIN_LATER: diff --git a/media/base/android/media_decoder_job.h b/media/base/android/media_decoder_job.h index ea3956ebec..e9530b39e0 100644 --- a/media/base/android/media_decoder_job.h +++ b/media/base/android/media_decoder_job.h @@ -27,6 +27,9 @@ class MediaDecoderJob { // Callback when a decoder job finishes its work. Args: whether decode // finished successfully, presentation time, audio output bytes. + // If the presentation time is equal to kNoTimestamp(), the decoder job + // skipped rendering of the decoded output and the callback target should + // update its clock to avoid introducing extra delays to the next frame. typedef base::Callback<void(MediaCodecStatus, const base::TimeDelta&, size_t)> DecoderCallback; // Callback when a decoder job finishes releasing the output buffer. @@ -66,9 +69,10 @@ class MediaDecoderJob { // Flush the decoder. void Flush(); - void set_preroll_timestamp(const base::TimeDelta& preroll_timestamp) { - preroll_timestamp_ = preroll_timestamp; - } + // Enter prerolling state. The job must not currently be decoding. + void BeginPrerolling(const base::TimeDelta& preroll_timestamp); + + bool prerolling() const { return prerolling_; } bool is_decoding() const { return !decode_cb_.is_null(); } @@ -120,9 +124,8 @@ class MediaDecoderJob { const DecoderCallback& callback); // Called on the UI thread to indicate that one decode cycle has completed. - // If the |presentation_timestamp| is equal to kNoTimestamp(), - // the caller should ignore the output of this frame and update its clock to - // avoid introducing extra delays to the next frame. + // Completes any pending job destruction or any pending decode stop. If + // destruction was not pending, passes its arguments to |decode_cb_|. void OnDecodeCompleted(MediaCodecStatus status, const base::TimeDelta& presentation_timestamp, size_t audio_output_bytes); @@ -149,6 +152,13 @@ class MediaDecoderJob { // is not very accurate. base::TimeDelta preroll_timestamp_; + // Indicates prerolling state. If true, this job has not yet decoded output + // that it will render, since the most recent of job construction or + // BeginPrerolling(). If false, |preroll_timestamp_| has been reached. + // TODO(qinmin): Comparing access unit's timestamp with |preroll_timestamp_| + // is not very accurate. + bool prerolling_; + // Weak pointer passed to media decoder jobs for callbacks. It is bounded to // the decoder thread. base::WeakPtrFactory<MediaDecoderJob> weak_this_; diff --git a/media/base/android/media_source_player.cc b/media/base/android/media_source_player.cc index 5f992ddc51..8e23a91629 100644 --- a/media/base/android/media_source_player.cc +++ b/media/base/android/media_source_player.cc @@ -79,7 +79,9 @@ MediaSourcePlayer::MediaSourcePlayer( is_video_encrypted_(false), volume_(-1.0), clock_(&default_tick_clock_), - surface_ever_set_(false), + next_video_data_is_iframe_(true), + doing_browser_seek_(false), + pending_seek_(false), reconfig_audio_decoder_(false), reconfig_video_decoder_(false), weak_this_(this), @@ -108,52 +110,53 @@ void MediaSourcePlayer::SetVideoSurface(gfx::ScopedJavaSurface surface) { // processed. if (IsEventPending(SURFACE_CHANGE_EVENT_PENDING)) return; - SetPendingEvent(SURFACE_CHANGE_EVENT_PENDING); - - // Setting a new surface will require a new MediaCodec to be created. - // Request a seek so that the new decoder will decode an I-frame first. - // Or otherwise, the new MediaCodec might crash. See b/8950387. - if (!surface_ever_set_) { - // Don't work-around to reach I-frame if we never previously set - // |surface_|. In this case, we never fed data to decoder, and next data - // should begin with I-frame. - surface_ever_set_ = true; - - // If seek is already pending, process of the pending surface change - // event will occur in OnDemuxerSeekDone(). Otherwise, we need to - // trigger pending event processing now. - if (!IsEventPending(SEEK_EVENT_PENDING)) - ProcessPendingEvents(); + // Eventual processing of surface change will take care of feeding the new + // video decoder initially with I-frame. See b/8950387. + SetPendingEvent(SURFACE_CHANGE_EVENT_PENDING); + // If seek is already pending, processing of the pending surface change + // event will occur in OnDemuxerSeekDone(). + if (IsEventPending(SEEK_EVENT_PENDING)) return; - } - if (IsEventPending(SEEK_EVENT_PENDING)) { - // Waiting for the seek to finish. + // If video config change is already pending, processing of the pending + // surface change event will occur in OnDemuxerConfigsAvailable(). + if (reconfig_video_decoder_ && IsEventPending(CONFIG_CHANGE_EVENT_PENDING)) return; - } - // TODO(wolenetz): We need to either coordinate seek with renderer or - // add functionality to know which data contains I-frames and only begin - // feeding a new decoder with such frames. The following will likely cause - // ChunkDemuxer to fail in renderer because it is not setup to expect seek. - ScheduleSeekEventAndStopDecoding(); + // Otherwise we need to trigger pending event processing now. + ProcessPendingEvents(); } -void MediaSourcePlayer::ScheduleSeekEventAndStopDecoding() { +void MediaSourcePlayer::ScheduleSeekEventAndStopDecoding( + const base::TimeDelta& seek_time) { + DVLOG(1) << __FUNCTION__ << "(" << seek_time.InSecondsF() << ")"; + DCHECK(!IsEventPending(SEEK_EVENT_PENDING)); + + pending_seek_ = false; + + clock_.SetTime(seek_time, seek_time); + if (audio_timestamp_helper_) + audio_timestamp_helper_->SetBaseTimestamp(seek_time); + if (audio_decoder_job_ && audio_decoder_job_->is_decoding()) audio_decoder_job_->StopDecode(); if (video_decoder_job_ && video_decoder_job_->is_decoding()) video_decoder_job_->StopDecode(); - if (IsEventPending(SEEK_EVENT_PENDING)) - return; - SetPendingEvent(SEEK_EVENT_PENDING); ProcessPendingEvents(); } +void MediaSourcePlayer::BrowserSeekToCurrentTime() { + DVLOG(1) << __FUNCTION__; + + DCHECK(!IsEventPending(SEEK_EVENT_PENDING)); + doing_browser_seek_ = true; + ScheduleSeekEventAndStopDecoding(GetCurrentTime()); +} + bool MediaSourcePlayer::Seekable() { // If the duration TimeDelta, converted to milliseconds from microseconds, // is >= 2^31, then the media is assumed to be unbounded and unseekable. @@ -201,10 +204,20 @@ int MediaSourcePlayer::GetVideoHeight() { void MediaSourcePlayer::SeekTo(const base::TimeDelta& timestamp) { DVLOG(1) << __FUNCTION__ << "(" << timestamp.InSecondsF() << ")"; - clock_.SetTime(timestamp, timestamp); - if (audio_timestamp_helper_) - audio_timestamp_helper_->SetBaseTimestamp(timestamp); - ScheduleSeekEventAndStopDecoding(); + if (IsEventPending(SEEK_EVENT_PENDING)) { + DCHECK(doing_browser_seek_) << "SeekTo while SeekTo in progress"; + DCHECK(!pending_seek_) << "SeekTo while SeekTo pending browser seek"; + + // There is a browser seek currently in progress to obtain I-frame to feed + // a newly constructed video decoder. Remember this real seek request so + // it can be initiated once OnDemuxerSeekDone() occurs for the browser seek. + pending_seek_ = true; + pending_seek_time_ = timestamp; + return; + } + + doing_browser_seek_ = false; + ScheduleSeekEventAndStopDecoding(timestamp); } base::TimeDelta MediaSourcePlayer::GetCurrentTime() { @@ -317,13 +330,8 @@ void MediaSourcePlayer::OnDemuxerConfigsAvailable( if (reconfig_audio_decoder_) ConfigureAudioDecoderJob(); - // If there is a pending surface change, we can merge it with the config - // change. - if (reconfig_video_decoder_) { - if (IsEventPending(SURFACE_CHANGE_EVENT_PENDING)) - ClearPendingEvent(SURFACE_CHANGE_EVENT_PENDING); + if (reconfig_video_decoder_) ConfigureVideoDecoderJob(); - } ClearPendingEvent(CONFIG_CHANGE_EVENT_PENDING); @@ -336,10 +344,13 @@ void MediaSourcePlayer::OnDemuxerConfigsAvailable( void MediaSourcePlayer::OnDemuxerDataAvailable(const DemuxerData& data) { DVLOG(1) << __FUNCTION__ << "(" << data.type << ")"; DCHECK_LT(0u, data.access_units.size()); - if (data.type == DemuxerStream::AUDIO && audio_decoder_job_) + if (data.type == DemuxerStream::AUDIO && audio_decoder_job_) { audio_decoder_job_->OnDataReceived(data); - else if (data.type == DemuxerStream::VIDEO && video_decoder_job_) - video_decoder_job_->OnDataReceived(data); + } else if (data.type == DemuxerStream::VIDEO) { + next_video_data_is_iframe_ = false; + if (video_decoder_job_) + video_decoder_job_->OnDataReceived(data); + } } void MediaSourcePlayer::OnDemuxerDurationChanged(base::TimeDelta duration) { @@ -384,20 +395,51 @@ void MediaSourcePlayer::SetDrmBridge(MediaDrmBridge* drm_bridge) { StartInternal(); } -void MediaSourcePlayer::OnDemuxerSeekDone() { +void MediaSourcePlayer::OnDemuxerSeekDone( + const base::TimeDelta& actual_browser_seek_time) { DVLOG(1) << __FUNCTION__; ClearPendingEvent(SEEK_EVENT_PENDING); + + next_video_data_is_iframe_ = true; + + if (pending_seek_) { + DVLOG(1) << __FUNCTION__ << "processing pending seek"; + DCHECK(doing_browser_seek_); + pending_seek_ = false; + SeekTo(pending_seek_time_); + return; + } + + // It is possible that a browser seek to I-frame had to seek to a buffered + // I-frame later than the requested one due to data removal or GC. Update + // player clock to the actual seek target. + if (doing_browser_seek_) { + DCHECK(actual_browser_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(actual_browser_seek_time >= GetCurrentTime()); + DVLOG(1) << __FUNCTION__ << " : setting clock to actual browser seek time: " + << actual_browser_seek_time.InSecondsF(); + clock_.SetTime(actual_browser_seek_time, actual_browser_seek_time); + if (audio_timestamp_helper_) + audio_timestamp_helper_->SetBaseTimestamp(actual_browser_seek_time); + } + base::TimeDelta current_time = GetCurrentTime(); - manager()->OnSeekComplete(player_id(), current_time); // TODO(qinmin): Simplify the logic by using |start_presentation_timestamp_| // to preroll media decoder jobs. Currently |start_presentation_timestamp_| // is calculated from decoder output, while preroll relies on the access // unit's timestamp. There are some differences between the two. + preroll_timestamp_ = current_time; if (audio_decoder_job_) - audio_decoder_job_->set_preroll_timestamp(current_time); + audio_decoder_job_->BeginPrerolling(preroll_timestamp_); if (video_decoder_job_) - video_decoder_job_->set_preroll_timestamp(current_time); + video_decoder_job_->BeginPrerolling(preroll_timestamp_); + + if (!doing_browser_seek_) + manager()->OnSeekComplete(player_id(), current_time); + ProcessPendingEvents(); } @@ -436,7 +478,7 @@ void MediaSourcePlayer::ProcessPendingEvents() { if (IsEventPending(SEEK_EVENT_PENDING)) { DVLOG(1) << __FUNCTION__ << " : Handling SEEK_EVENT"; ClearDecodingData(); - demuxer_->RequestDemuxerSeek(GetCurrentTime()); + demuxer_->RequestDemuxerSeek(GetCurrentTime(), doing_browser_seek_); return; } @@ -450,9 +492,15 @@ void MediaSourcePlayer::ProcessPendingEvents() { if (IsEventPending(SURFACE_CHANGE_EVENT_PENDING)) { DVLOG(1) << __FUNCTION__ << " : Handling SURFACE_CHANGE_EVENT."; + // Setting a new surface will require a new MediaCodec to be created. video_decoder_job_.reset(); ConfigureVideoDecoderJob(); - ClearPendingEvent(SURFACE_CHANGE_EVENT_PENDING); + + // Return early if we can't successfully configure a new video decoder job + // yet, except continue processing other pending events if |surface_| is + // empty. + if (!video_decoder_job_ && !surface_.IsEmpty()) + return; } if (IsEventPending(PREFETCH_REQUEST_EVENT_PENDING)) { @@ -485,7 +533,6 @@ void MediaSourcePlayer::MediaDecoderCallback( bool is_audio, MediaCodecStatus status, const base::TimeDelta& presentation_timestamp, size_t audio_output_bytes) { DVLOG(1) << __FUNCTION__ << ": " << is_audio << ", " << status; - DCHECK(!is_waiting_for_key_); // TODO(xhwang): Drop IntToString() when http://crbug.com/303899 is fixed. if (is_audio) { @@ -599,6 +646,11 @@ void MediaSourcePlayer::DecodeMoreVideo() { // Failed to start the next decode. // Wait for demuxer ready message. reconfig_video_decoder_ = true; + + // After this detection of video config change, next video data received + // will begin with I-frame. + next_video_data_is_iframe_ = true; + SetPendingEvent(CONFIG_CHANGE_EVENT_PENDING); ProcessPendingEvents(); } @@ -651,6 +703,8 @@ void MediaSourcePlayer::ConfigureAudioDecoderJob() { DCHECK(!audio_decoder_job_ || !audio_decoder_job_->is_decoding()); + DVLOG(1) << __FUNCTION__ << " : creating new audio decoder job"; + audio_decoder_job_.reset(AudioDecoderJob::Create( audio_codec_, sampling_rate_, num_channels_, &audio_extra_data_[0], audio_extra_data_.size(), media_crypto.obj(), @@ -659,6 +713,7 @@ void MediaSourcePlayer::ConfigureAudioDecoderJob() { if (audio_decoder_job_) { SetVolumeInternal(); + audio_decoder_job_->BeginPrerolling(preroll_timestamp_); reconfig_audio_decoder_ = false; } } @@ -666,19 +721,39 @@ void MediaSourcePlayer::ConfigureAudioDecoderJob() { void MediaSourcePlayer::ConfigureVideoDecoderJob() { if (!HasVideo() || surface_.IsEmpty()) { video_decoder_job_.reset(); + if (IsEventPending(SURFACE_CHANGE_EVENT_PENDING)) + ClearPendingEvent(SURFACE_CHANGE_EVENT_PENDING); return; } - // Create video decoder job only if config changes. + // Create video decoder job only if config changes or we don't have a job. if (video_decoder_job_ && !reconfig_video_decoder_) return; + if (reconfig_video_decoder_) { + // No hack browser seek should be required. I-Frame must be next. + DCHECK(next_video_data_is_iframe_) << "Received video data between " + << "detecting video config change and reconfiguring video decoder"; + } + + // If uncertain that video I-frame data is next and there is no seek already + // in process, request browser demuxer seek so the new decoder will decode + // an I-frame first. Otherwise, the new MediaCodec might crash. See b/8950387. + // TODO(wolenetz): Instead of doing hack browser seek, replay cached data + // since last keyframe. See http://crbug.com/304234. + if (!next_video_data_is_iframe_ && !IsEventPending(SEEK_EVENT_PENDING)) { + BrowserSeekToCurrentTime(); + return; + } + base::android::ScopedJavaLocalRef<jobject> media_crypto = GetMediaCrypto(); if (is_video_encrypted_ && media_crypto.is_null()) return; DCHECK(!video_decoder_job_ || !video_decoder_job_->is_decoding()); + DVLOG(1) << __FUNCTION__ << " : creating new video decoder job"; + // Release the old VideoDecoderJob first so the surface can get released. // Android does not allow 2 MediaCodec instances use the same surface. video_decoder_job_.reset(); @@ -693,8 +768,13 @@ void MediaSourcePlayer::ConfigureVideoDecoderJob() { base::Bind(&DemuxerAndroid::RequestDemuxerData, base::Unretained(demuxer_.get()), DemuxerStream::VIDEO))); - if (video_decoder_job_) + if (video_decoder_job_) { + video_decoder_job_->BeginPrerolling(preroll_timestamp_); reconfig_video_decoder_ = false; + } + + if (IsEventPending(SURFACE_CHANGE_EVENT_PENDING)) + ClearPendingEvent(SURFACE_CHANGE_EVENT_PENDING); // Inform the fullscreen view the player is ready. // TODO(qinmin): refactor MediaPlayerBridge so that we have a better way diff --git a/media/base/android/media_source_player.h b/media/base/android/media_source_player.h index 61215b4390..3c16689f7d 100644 --- a/media/base/android/media_source_player.h +++ b/media/base/android/media_source_player.h @@ -70,7 +70,8 @@ class MEDIA_EXPORT MediaSourcePlayer : public MediaPlayerAndroid, // DemuxerAndroidClient implementation. virtual void OnDemuxerConfigsAvailable(const DemuxerConfigs& params) OVERRIDE; virtual void OnDemuxerDataAvailable(const DemuxerData& params) OVERRIDE; - virtual void OnDemuxerSeekDone() OVERRIDE; + virtual void OnDemuxerSeekDone( + const base::TimeDelta& actual_browser_seek_time) OVERRIDE; virtual void OnDemuxerDurationChanged(base::TimeDelta duration) OVERRIDE; private: @@ -127,8 +128,18 @@ class MEDIA_EXPORT MediaSourcePlayer : public MediaPlayerAndroid, void StartStarvationCallback(const base::TimeDelta& presentation_timestamp); // Schedules a seek event in |pending_events_| and calls StopDecode() on all - // the MediaDecoderJobs. - void ScheduleSeekEventAndStopDecoding(); + // the MediaDecoderJobs. Sets clock to |seek_time|, and resets + // |pending_seek_|. There must not already be a seek event in + // |pending_events_|. + void ScheduleSeekEventAndStopDecoding(const base::TimeDelta& seek_time); + + // Schedules a browser seek event. We must not currently be processing any + // seek. Note that there is possibility that browser seek of renderer demuxer + // may unexpectedly stall due to lack of buffered data at or after the browser + // seek time. + // TODO(wolenetz): Instead of doing hack browser seek, replay cached data + // since last keyframe. See http://crbug.com/304234. + void BrowserSeekToCurrentTime(); // Helper function to set the volume. void SetVolumeInternal(); @@ -197,8 +208,25 @@ class MEDIA_EXPORT MediaSourcePlayer : public MediaPlayerAndroid, // The surface object currently owned by the player. gfx::ScopedJavaSurface surface_; - // Tracks whether or not the player has previously ever set |surface_|. - bool surface_ever_set_; + // Track whether or not the player has received any video data since the most + // recent of player construction, end of last seek, or receiving and + // detecting a |kConfigChanged| access unit from the demuxer. + // If no such video data has been received, the next video data begins with + // an I-frame. Otherwise, we have no such guarantee. + bool next_video_data_is_iframe_; + + // Flag that is true if doing a hack browser seek or false if doing a + // regular seek. Only valid when |SEEK_EVENT_PENDING| is pending. + // TODO(wolenetz): Instead of doing hack browser seek, replay cached data + // since last keyframe. See http://crbug.com/304234. + bool doing_browser_seek_; + + // If already doing a browser seek when a regular seek request arrives, + // these fields remember the regular seek so OnDemuxerSeekDone() can trigger + // it when the browser seek is done. These are only valid when + // |SEEK_EVENT_PENDING| is pending. + bool pending_seek_; + base::TimeDelta pending_seek_time_; // Decoder jobs. scoped_ptr<AudioDecoderJob, MediaDecoderJob::Deleter> audio_decoder_job_; @@ -207,6 +235,10 @@ class MEDIA_EXPORT MediaSourcePlayer : public MediaPlayerAndroid, bool reconfig_audio_decoder_; bool reconfig_video_decoder_; + // Track the most recent preroll target. Decoder re-creation needs this to + // resume any in-progress preroll. + base::TimeDelta preroll_timestamp_; + // A cancelable task that is posted when the audio decoder starts requesting // new data. This callback runs if no data arrives before the timeout period // elapses. diff --git a/media/base/android/media_source_player_unittest.cc b/media/base/android/media_source_player_unittest.cc index 8826848dd9..e329ac6eee 100644 --- a/media/base/android/media_source_player_unittest.cc +++ b/media/base/android/media_source_player_unittest.cc @@ -92,23 +92,31 @@ class MockDemuxerAndroid : public DemuxerAndroid { explicit MockDemuxerAndroid(base::MessageLoop* message_loop) : message_loop_(message_loop), num_data_requests_(0), - num_seek_requests_(0) {} + num_seek_requests_(0), + num_browser_seek_requests_(0), + num_config_requests_(0) {} virtual ~MockDemuxerAndroid() {} virtual void Initialize(DemuxerAndroidClient* client) OVERRIDE {} - virtual void RequestDemuxerConfigs() OVERRIDE {} + virtual void RequestDemuxerConfigs() OVERRIDE { + num_config_requests_++; + } virtual void RequestDemuxerData(DemuxerStream::Type type) OVERRIDE { num_data_requests_++; if (message_loop_->is_running()) message_loop_->Quit(); } - virtual void RequestDemuxerSeek( - const base::TimeDelta& time_to_seek) OVERRIDE { + virtual void RequestDemuxerSeek(const base::TimeDelta& time_to_seek, + bool is_browser_seek) OVERRIDE { num_seek_requests_++; + if (is_browser_seek) + num_browser_seek_requests_++; } int num_data_requests() const { return num_data_requests_; } int num_seek_requests() const { return num_seek_requests_; } + int num_browser_seek_requests() const { return num_browser_seek_requests_; } + int num_config_requests() const { return num_config_requests_; } private: base::MessageLoop* message_loop_; @@ -116,9 +124,15 @@ class MockDemuxerAndroid : public DemuxerAndroid { // The number of encoded data requests this object has seen. int num_data_requests_; - // The number of seek requests this object has seen. + // The number of regular and browser seek requests this object has seen. int num_seek_requests_; + // The number of browser seek requests this object has seen. + int num_browser_seek_requests_; + + // The number of demuxer config requests this object has seen. + int num_config_requests_; + DISALLOW_COPY_AND_ASSIGN(MockDemuxerAndroid); }; @@ -127,7 +141,8 @@ class MediaSourcePlayerTest : public testing::Test { MediaSourcePlayerTest() : manager_(&message_loop_), demuxer_(new MockDemuxerAndroid(&message_loop_)), - player_(0, &manager_, scoped_ptr<DemuxerAndroid>(demuxer_)) {} + player_(0, &manager_, scoped_ptr<DemuxerAndroid>(demuxer_)), + surface_texture_a_is_next_(true) {} virtual ~MediaSourcePlayerTest() {} protected: @@ -141,8 +156,18 @@ class MediaSourcePlayerTest : public testing::Test { player_.video_decoder_job_.get()); } - // Starts an audio decoder job. - void StartAudioDecoderJob() { + // Get the per-job prerolling status from the MediaSourcePlayer's job matching + // |is_audio|. Caller must guard against NPE if the player's job is NULL. + bool IsPrerolling(bool is_audio) { + return GetMediaDecoderJob(is_audio)->prerolling(); + } + + // Get the preroll timestamp from the MediaSourcePlayer. + base::TimeDelta GetPrerollTimestamp() { + return player_.preroll_timestamp_; + } + + DemuxerConfigs CreateAudioDemuxerConfigs() { DemuxerConfigs configs; configs.audio_codec = kCodecVorbis; configs.audio_channels = 2; @@ -153,38 +178,67 @@ class MediaSourcePlayerTest : public testing::Test { configs.audio_extra_data = std::vector<uint8>( buffer->data(), buffer->data() + buffer->data_size()); - Start(configs); + return configs; } - void StartVideoDecoderJob() { + // Starts an audio decoder job. + void StartAudioDecoderJob() { + Start(CreateAudioDemuxerConfigs()); + } + + DemuxerConfigs CreateVideoDemuxerConfigs() { DemuxerConfigs configs; configs.video_codec = kCodecVP8; configs.video_size = gfx::Size(320, 240); configs.is_video_encrypted = false; configs.duration_ms = kDefaultDurationInMs; - Start(configs); + return configs; + } + + DemuxerConfigs CreateAudioVideoDemuxerConfigs() { + DemuxerConfigs configs = CreateAudioDemuxerConfigs(); + configs.video_codec = kCodecVP8; + configs.video_size = gfx::Size(320, 240); + configs.is_video_encrypted = false; + return configs; + } + + void StartVideoDecoderJob() { + Start(CreateVideoDemuxerConfigs()); } // Starts decoding the data. void Start(const DemuxerConfigs& configs) { player_.OnDemuxerConfigsAvailable(configs); player_.Start(); + EXPECT_TRUE(player_.IsPlaying()); + } + + AccessUnit CreateAccessUnitWithData(bool is_audio, int audio_packet_id) { + AccessUnit unit; + + unit.status = DemuxerStream::kOk; + scoped_refptr<DecoderBuffer> buffer = ReadTestDataFile( + is_audio ? base::StringPrintf("vorbis-packet-%d", audio_packet_id) + : "vp8-I-frame-320x240"); + unit.data = std::vector<uint8>( + buffer->data(), buffer->data() + buffer->data_size()); + + if (is_audio) { + // Vorbis needs 4 extra bytes padding on Android to decode properly. Check + // NuMediaExtractor.cpp in Android source code. + uint8 padding[4] = { 0xff , 0xff , 0xff , 0xff }; + unit.data.insert(unit.data.end(), padding, padding + 4); + } + + return unit; } DemuxerData CreateReadFromDemuxerAckForAudio(int packet_id) { DemuxerData data; data.type = DemuxerStream::AUDIO; data.access_units.resize(1); - data.access_units[0].status = DemuxerStream::kOk; - scoped_refptr<DecoderBuffer> buffer = ReadTestDataFile( - base::StringPrintf("vorbis-packet-%d", packet_id)); - data.access_units[0].data = std::vector<uint8>( - buffer->data(), buffer->data() + buffer->data_size()); - // Vorbis needs 4 extra bytes padding on Android to decode properly. Check - // NuMediaExtractor.cpp in Android source code. - uint8 padding[4] = { 0xff , 0xff , 0xff , 0xff }; - data.access_units[0].data.insert( - data.access_units[0].data.end(), padding, padding + 4); + data.access_units[0] = CreateAccessUnitWithData(true, packet_id); return data; } @@ -192,23 +246,216 @@ class MediaSourcePlayerTest : public testing::Test { DemuxerData data; data.type = DemuxerStream::VIDEO; data.access_units.resize(1); - data.access_units[0].status = DemuxerStream::kOk; - scoped_refptr<DecoderBuffer> buffer = - ReadTestDataFile("vp8-I-frame-320x240"); - data.access_units[0].data = std::vector<uint8>( - buffer->data(), buffer->data() + buffer->data_size()); + data.access_units[0] = CreateAccessUnitWithData(false, 0); return data; } DemuxerData CreateEOSAck(bool is_audio) { - DemuxerData data; - data.type = is_audio ? DemuxerStream::AUDIO : DemuxerStream::VIDEO; - data.access_units.resize(1); - data.access_units[0].status = DemuxerStream::kOk; - data.access_units[0].end_of_stream = true; - return data; + DemuxerData data; + data.type = is_audio ? DemuxerStream::AUDIO : DemuxerStream::VIDEO; + data.access_units.resize(1); + data.access_units[0].status = DemuxerStream::kOk; + data.access_units[0].end_of_stream = true; + return data; + } + + DemuxerData CreateAbortedAck(bool is_audio) { + DemuxerData data; + data.type = is_audio ? DemuxerStream::AUDIO : DemuxerStream::VIDEO; + data.access_units.resize(1); + data.access_units[0].status = DemuxerStream::kAborted; + return data; + } + + // Seek, including simulated receipt of |kAborted| read between SeekTo() + // and OnDemuxerSeekDone(). Use this helper method only when the player + // already has created the decoder job. + void SeekPlayer(bool is_audio, const base::TimeDelta& seek_time) { + EXPECT_TRUE(GetMediaDecoderJob(is_audio)); + + int original_num_seeks = demuxer_->num_seek_requests(); + int original_num_data_requests = demuxer_->num_data_requests(); + + // Initiate a seek. Skip the round-trip of requesting seek from renderer. + // Instead behave as if the renderer has asked us to seek. + player_.SeekTo(seek_time); + + // Verify that the seek does not occur until previously outstanding data + // request is satisfied. + EXPECT_EQ(original_num_seeks, demuxer_->num_seek_requests()); + + // Simulate seeking causes the demuxer to abort the outstanding read caused + // by the seek. + player_.OnDemuxerDataAvailable(CreateAbortedAck(is_audio)); + + // Verify that the seek is requested now that the outstanding read is + // completed by aborted access unit. + EXPECT_EQ(original_num_seeks + 1, demuxer_->num_seek_requests()); + + // Send back the seek done notification. This should trigger the player to + // call OnReadFromDemuxer() again. + EXPECT_EQ(original_num_data_requests, demuxer_->num_data_requests()); + player_.OnDemuxerSeekDone(kNoTimestamp()); + EXPECT_EQ(original_num_data_requests + 1, demuxer_->num_data_requests()); + + // No other seek should have been requested. + EXPECT_EQ(original_num_seeks + 1, demuxer_->num_seek_requests()); + } + + DemuxerData CreateReadFromDemuxerAckWithConfigChanged(bool is_audio, + int config_unit_index) { + DemuxerData data; + data.type = is_audio ? DemuxerStream::AUDIO : DemuxerStream::VIDEO; + data.access_units.resize(config_unit_index + 1); + + for (int i = 0; i < config_unit_index; ++i) + data.access_units[i] = CreateAccessUnitWithData(is_audio, i); + + data.access_units[config_unit_index].status = DemuxerStream::kConfigChanged; + return data; + } + + // Valid only for video-only player tests. If |trigger_with_release_start| is + // true, triggers the browser seek with a Release() + video data received + + // Start() with a new surface. If false, triggers the browser seek by + // setting a new video surface after beginning decode of received video data. + // Such data receipt causes possibility that an I-frame is not next, and + // browser seek results once decode completes and surface change processing + // begins. + void BrowserSeekPlayer(bool trigger_with_release_start) { + int expected_num_data_requests = demuxer_->num_data_requests(); + int expected_num_seek_requests = demuxer_->num_seek_requests(); + int expected_num_browser_seek_requests = + demuxer_->num_browser_seek_requests(); + + EXPECT_FALSE(GetMediaDecoderJob(false)); + CreateNextTextureAndSetVideoSurface(); + StartVideoDecoderJob(); + EXPECT_TRUE(GetMediaDecoderJob(false)); + expected_num_data_requests++; + EXPECT_EQ(expected_num_data_requests, demuxer_->num_data_requests()); + + if (trigger_with_release_start) { + player_.Release(); + + // Simulate demuxer's response to the video data request. + player_.OnDemuxerDataAvailable(CreateReadFromDemuxerAckForVideo()); + EXPECT_FALSE(GetMediaDecoderJob(false)); + EXPECT_FALSE(player_.IsPlaying()); + EXPECT_EQ(expected_num_seek_requests, demuxer_->num_seek_requests()); + EXPECT_EQ(expected_num_data_requests, demuxer_->num_data_requests()); + + CreateNextTextureAndSetVideoSurface(); + player_.Start(); + } else { + // Simulate demuxer's response to the video data request. + player_.OnDemuxerDataAvailable(CreateReadFromDemuxerAckForVideo()); + + // While the decoder is decoding, trigger a browser seek by changing + // surface. Demuxer does not know of browser seek in advance, so no + // |kAborted| data is required (though |kAborted| can certainly occur for + // any pending read in reality due to renderer preparing for a regular + // seek). + CreateNextTextureAndSetVideoSurface(); + + // Browser seek should not begin until decoding has completed. + EXPECT_TRUE(GetMediaDecoderJob(false)); + EXPECT_EQ(expected_num_seek_requests, demuxer_->num_seek_requests()); + EXPECT_EQ(expected_num_browser_seek_requests, + demuxer_->num_browser_seek_requests()); + + // Wait for the decoder job to finish decoding and be reset pending the + // browser seek. + while (GetMediaDecoderJob(false) && + GetMediaDecoderJob(false)->is_decoding()) { + message_loop_.RunUntilIdle(); + } } + EXPECT_FALSE(GetMediaDecoderJob(false)); + EXPECT_TRUE(player_.IsPlaying()); + + // Only one browser seek should have been initiated, and no further data + // should have been requested. + expected_num_seek_requests++; + expected_num_browser_seek_requests++; + EXPECT_EQ(expected_num_seek_requests, demuxer_->num_seek_requests()); + EXPECT_EQ(expected_num_browser_seek_requests, + demuxer_->num_browser_seek_requests()); + EXPECT_EQ(expected_num_data_requests, demuxer_->num_seek_requests()); + } + + // Creates a new decoder job and feeds it data ending with a |kConfigChanged| + // access unit. If |config_unit_in_prefetch| is true, sends feeds the config + // change AU in response to the job's first read request (prefetch). If + // false, regular data is fed and decoded prior to feeding the config change + // AU in response to the second data request (after prefetch completed). + // |config_unit_index| controls which access unit is |kConfigChanged|. + void StartConfigChange(bool is_audio, + bool config_unit_in_prefetch, + int config_unit_index) { + int expected_num_data_requests = demuxer_->num_data_requests(); + int expected_num_config_requests = demuxer_->num_config_requests(); + + EXPECT_FALSE(GetMediaDecoderJob(is_audio)); + if (is_audio) { + StartAudioDecoderJob(); + } else { + CreateNextTextureAndSetVideoSurface(); + StartVideoDecoderJob(); + } + EXPECT_TRUE(GetMediaDecoderJob(is_audio)); + expected_num_data_requests++; + EXPECT_EQ(expected_num_data_requests, demuxer_->num_data_requests()); + EXPECT_EQ(expected_num_config_requests, demuxer_->num_config_requests()); + + // Feed and decode a standalone access unit so the player exits prefetch. + if (!config_unit_in_prefetch) { + if (is_audio) + player_.OnDemuxerDataAvailable(CreateReadFromDemuxerAckForAudio(0)); + else + player_.OnDemuxerDataAvailable(CreateReadFromDemuxerAckForVideo()); + + message_loop_.Run(); + + // We should have completed the prefetch phase at this point. + EXPECT_TRUE(GetMediaDecoderJob(is_audio)); + expected_num_data_requests++; + EXPECT_EQ(expected_num_data_requests, demuxer_->num_data_requests()); + EXPECT_EQ(expected_num_config_requests, demuxer_->num_config_requests()); + } + + // Feed and decode access units with data for any units prior to + // |config_unit_index|, and a |kConfigChanged| unit at that index. + // Player should prepare to reconfigure the decoder job, and should request + // new demuxer configs. + player_.OnDemuxerDataAvailable( + CreateReadFromDemuxerAckWithConfigChanged(is_audio, config_unit_index)); + while (GetMediaDecoderJob(is_audio)->is_decoding()) + message_loop_.RunUntilIdle(); + + expected_num_config_requests++; + EXPECT_TRUE(player_.IsPlaying()); + EXPECT_TRUE(GetMediaDecoderJob(is_audio)); + EXPECT_EQ(expected_num_data_requests, demuxer_->num_data_requests()); + EXPECT_EQ(expected_num_config_requests, demuxer_->num_config_requests()); + } + + void CreateNextTextureAndSetVideoSurface() { + gfx::SurfaceTexture* surface_texture; + if (surface_texture_a_is_next_) { + surface_texture_a_ = new gfx::SurfaceTexture(next_texture_id_++); + surface_texture = surface_texture_a_.get(); + } else { + surface_texture_b_ = new gfx::SurfaceTexture(next_texture_id_++); + surface_texture = surface_texture_b_.get(); + } + + surface_texture_a_is_next_ = !surface_texture_a_is_next_; + gfx::ScopedJavaSurface surface = gfx::ScopedJavaSurface(surface_texture); + player_.SetVideoSurface(surface.Pass()); + } + base::TimeTicks StartTimeTicks() { return player_.start_time_ticks_; } @@ -221,23 +468,23 @@ class MediaSourcePlayerTest : public testing::Test { scheme_uuid, security_level, container, codecs); } - void CreateAndSetVideoSurface() { - surface_texture_ = new gfx::SurfaceTexture(0); - gfx::ScopedJavaSurface surface = gfx::ScopedJavaSurface( - surface_texture_.get()); - player_.SetVideoSurface(surface.Pass()); - } - protected: base::MessageLoop message_loop_; MockMediaPlayerManager manager_; MockDemuxerAndroid* demuxer_; // Owned by |player_|. MediaSourcePlayer player_; + // We need to keep the surface texture while the decoder is actively decoding. // Otherwise, it may trigger unexpected crashes on some devices. To switch - // surfaces, tests need to create their own surface texture without releasing - // this one if they previously called CreateAndSetVideoSurface(). - scoped_refptr<gfx::SurfaceTexture> surface_texture_; + // surfaces, tests need to create a new surface texture without releasing + // their previous one. In CreateNextTextureAndSetVideoSurface(), we toggle + // between two surface textures, only replacing the N-2 texture. Assumption is + // that no more than N-1 texture is in use by decoder when + // CreateNextTextureAndSetVideoSurface() is called. + scoped_refptr<gfx::SurfaceTexture> surface_texture_a_; + scoped_refptr<gfx::SurfaceTexture> surface_texture_b_; + bool surface_texture_a_is_next_; + int next_texture_id_; DISALLOW_COPY_AND_ASSIGN(MediaSourcePlayerTest); }; @@ -249,24 +496,23 @@ TEST_F(MediaSourcePlayerTest, StartAudioDecoderWithValidConfig) { StartAudioDecoderJob(); EXPECT_TRUE(GetMediaDecoderJob(true)); EXPECT_EQ(1, demuxer_->num_data_requests()); + EXPECT_EQ(0, demuxer_->num_seek_requests()); } TEST_F(MediaSourcePlayerTest, StartAudioDecoderWithInvalidConfig) { SKIP_TEST_IF_MEDIA_CODEC_BRIDGE_IS_NOT_AVAILABLE(); // Test audio decoder job will not be created when failed to start the codec. - DemuxerConfigs configs; - configs.audio_codec = kCodecVorbis; - configs.audio_channels = 2; - configs.audio_sampling_rate = 44100; - configs.is_audio_encrypted = false; - configs.duration_ms = kDefaultDurationInMs; + DemuxerConfigs configs = CreateAudioDemuxerConfigs(); + // Replace with invalid |audio_extra_data| + configs.audio_extra_data.clear(); uint8 invalid_codec_data[] = { 0x00, 0xff, 0xff, 0xff, 0xff }; configs.audio_extra_data.insert(configs.audio_extra_data.begin(), invalid_codec_data, invalid_codec_data + 4); Start(configs); - EXPECT_EQ(NULL, GetMediaDecoderJob(true)); + EXPECT_FALSE(GetMediaDecoderJob(true)); EXPECT_EQ(0, demuxer_->num_data_requests()); + EXPECT_EQ(0, demuxer_->num_seek_requests()); } TEST_F(MediaSourcePlayerTest, StartVideoCodecWithValidSurface) { @@ -275,17 +521,27 @@ TEST_F(MediaSourcePlayerTest, StartVideoCodecWithValidSurface) { // Test video decoder job will be created when surface is valid. StartVideoDecoderJob(); // Video decoder job will not be created until surface is available. - EXPECT_EQ(NULL, GetMediaDecoderJob(false)); + EXPECT_FALSE(GetMediaDecoderJob(false)); EXPECT_EQ(0, demuxer_->num_data_requests()); - CreateAndSetVideoSurface(); + // Set both an initial and a later video surface without receiving any + // demuxed data yet. + CreateNextTextureAndSetVideoSurface(); + MediaDecoderJob* first_job = GetMediaDecoderJob(false); + EXPECT_TRUE(first_job); + CreateNextTextureAndSetVideoSurface(); - // Player should not seek the demuxer on setting initial surface. + // Setting another surface will not create a new job until any pending + // read is satisfied (and job is no longer decoding). + EXPECT_EQ(first_job, GetMediaDecoderJob(false)); + + // No seeks, even on setting surface, should have occurred. (Browser seeks can + // occur on setting surface, but only after previously receiving video data.) EXPECT_EQ(0, demuxer_->num_seek_requests()); - // The decoder job should be ready now. - EXPECT_TRUE(GetMediaDecoderJob(false)); - EXPECT_EQ(1, demuxer_->num_data_requests()); + // Note, the decoder job for the second surface set, above, will be created + // only after the pending read is satisfied and decoded, and the resulting + // browser seek is done. See BrowserSeek_* tests for this coverage. } TEST_F(MediaSourcePlayerTest, StartVideoCodecWithInvalidSurface) { @@ -297,7 +553,7 @@ TEST_F(MediaSourcePlayerTest, StartVideoCodecWithInvalidSurface) { gfx::ScopedJavaSurface surface(surface_texture.get()); StartVideoDecoderJob(); // Video decoder job will not be created until surface is available. - EXPECT_EQ(NULL, GetMediaDecoderJob(false)); + EXPECT_FALSE(GetMediaDecoderJob(false)); EXPECT_EQ(0, demuxer_->num_data_requests()); // Release the surface texture. @@ -307,7 +563,7 @@ TEST_F(MediaSourcePlayerTest, StartVideoCodecWithInvalidSurface) { // Player should not seek the demuxer on setting initial surface. EXPECT_EQ(0, demuxer_->num_seek_requests()); - EXPECT_EQ(NULL, GetMediaDecoderJob(false)); + EXPECT_FALSE(GetMediaDecoderJob(false)); EXPECT_EQ(0, demuxer_->num_data_requests()); } @@ -318,34 +574,8 @@ TEST_F(MediaSourcePlayerTest, ReadFromDemuxerAfterSeek) { StartAudioDecoderJob(); EXPECT_TRUE(GetMediaDecoderJob(true)); EXPECT_EQ(1, demuxer_->num_data_requests()); - - // Initiate a seek. Skip the round-trip of requesting seek from renderer. - // Instead behave as if the renderer has asked us to seek. - player_.SeekTo(base::TimeDelta()); - - // Verify that the seek does not occur until the initial prefetch - // completes. - EXPECT_EQ(0, demuxer_->num_seek_requests()); - - // Simulate aborted read caused by the seek. This aborts the initial - // prefetch. - DemuxerData data; - data.type = DemuxerStream::AUDIO; - data.access_units.resize(1); - data.access_units[0].status = DemuxerStream::kAborted; - player_.OnDemuxerDataAvailable(data); - - // Verify that the seek is requested now that the initial prefetch - // has completed. - EXPECT_EQ(1, demuxer_->num_seek_requests()); - - // Sending back the seek done notification. This should trigger the player to - // call OnReadFromDemuxer() again. - player_.OnDemuxerSeekDone(); + SeekPlayer(true, base::TimeDelta()); EXPECT_EQ(2, demuxer_->num_data_requests()); - - // Reconfirm exactly 1 seek request has been made of demuxer. - EXPECT_EQ(1, demuxer_->num_seek_requests()); } TEST_F(MediaSourcePlayerTest, SetSurfaceWhileSeeking) { @@ -357,35 +587,39 @@ TEST_F(MediaSourcePlayerTest, SetSurfaceWhileSeeking) { // Player is still waiting for SetVideoSurface(), so no request is sent. EXPECT_EQ(0, demuxer_->num_data_requests()); - // Initiate a seek. Skip the round-trip of requesting seek from renderer. + // Initiate a seek. Skip requesting element seek of renderer. // Instead behave as if the renderer has asked us to seek. EXPECT_EQ(0, demuxer_->num_seek_requests()); player_.SeekTo(base::TimeDelta()); EXPECT_EQ(1, demuxer_->num_seek_requests()); - CreateAndSetVideoSurface(); - EXPECT_TRUE(NULL == GetMediaDecoderJob(false)); + CreateNextTextureAndSetVideoSurface(); + EXPECT_FALSE(GetMediaDecoderJob(false)); EXPECT_EQ(1, demuxer_->num_seek_requests()); // Reconfirm player has not yet requested data. EXPECT_EQ(0, demuxer_->num_data_requests()); // Send the seek done notification. The player should start requesting data. - player_.OnDemuxerSeekDone(); + player_.OnDemuxerSeekDone(kNoTimestamp()); EXPECT_TRUE(GetMediaDecoderJob(false)); EXPECT_EQ(1, demuxer_->num_data_requests()); - // Reconfirm exactly 1 seek request has been made of demuxer. + // Reconfirm exactly 1 seek request has been made of demuxer, and that it + // was not a browser seek request. EXPECT_EQ(1, demuxer_->num_seek_requests()); + EXPECT_EQ(0, demuxer_->num_browser_seek_requests()); } TEST_F(MediaSourcePlayerTest, ChangeMultipleSurfaceWhileDecoding) { SKIP_TEST_IF_MEDIA_CODEC_BRIDGE_IS_NOT_AVAILABLE(); // Test MediaSourcePlayer can switch multiple surfaces during decoding. - CreateAndSetVideoSurface(); + CreateNextTextureAndSetVideoSurface(); StartVideoDecoderJob(); EXPECT_EQ(1, demuxer_->num_data_requests()); + EXPECT_EQ(0, demuxer_->num_seek_requests()); + EXPECT_TRUE(GetMediaDecoderJob(false)); // Send the first input chunk. player_.OnDemuxerDataAvailable(CreateReadFromDemuxerAckForVideo()); @@ -394,47 +628,49 @@ TEST_F(MediaSourcePlayerTest, ChangeMultipleSurfaceWhileDecoding) { // surface first. gfx::ScopedJavaSurface empty_surface; player_.SetVideoSurface(empty_surface.Pass()); - // Pass a new non-empty surface, don't call CreateAndSetVideoSurface() as - // it will release the old surface_texture_ and might cause unexpected - // behaviors on some devices. - scoped_refptr<gfx::SurfaceTexture> surface_texture( - new gfx::SurfaceTexture(1)); - gfx::ScopedJavaSurface surface(surface_texture.get()); - player_.SetVideoSurface(surface.Pass()); + // Next, pass a new non-empty surface. + CreateNextTextureAndSetVideoSurface(); - // Wait for the decoder job to finish decoding. - while(GetMediaDecoderJob(false)->is_decoding()) + // Wait for the decoder job to finish decoding and be reset pending a browser + // seek. + while (GetMediaDecoderJob(false) && GetMediaDecoderJob(false)->is_decoding()) message_loop_.RunUntilIdle(); - // A seek should be initiated to request Iframe. - EXPECT_EQ(1, demuxer_->num_seek_requests()); + EXPECT_FALSE(GetMediaDecoderJob(false)); + + // Only one browser seek should have been initiated. No further data request + // should have been processed on |message_loop_| before surface change event + // became pending, above. + EXPECT_EQ(1, demuxer_->num_browser_seek_requests()); EXPECT_EQ(1, demuxer_->num_data_requests()); + + // Simulate browser seek is done and confirm player requests more data for new + // video decoder job. + player_.OnDemuxerSeekDone(player_.GetCurrentTime()); + EXPECT_TRUE(GetMediaDecoderJob(false)); + EXPECT_EQ(2, demuxer_->num_data_requests()); + EXPECT_EQ(1, demuxer_->num_seek_requests()); } -TEST_F(MediaSourcePlayerTest, StartAfterSeekFinish) { +TEST_F(MediaSourcePlayerTest, AudioOnlyStartAfterSeekFinish) { SKIP_TEST_IF_MEDIA_CODEC_BRIDGE_IS_NOT_AVAILABLE(); - // Test decoder job will not start until all pending seek event is handled. - DemuxerConfigs configs; - configs.audio_codec = kCodecVorbis; - configs.audio_channels = 2; - configs.audio_sampling_rate = 44100; - configs.is_audio_encrypted = false; - configs.duration_ms = kDefaultDurationInMs; + // Test audio decoder job will not start until pending seek event is handled. + DemuxerConfigs configs = CreateAudioDemuxerConfigs(); player_.OnDemuxerConfigsAvailable(configs); - EXPECT_EQ(NULL, GetMediaDecoderJob(true)); + EXPECT_FALSE(GetMediaDecoderJob(true)); EXPECT_EQ(0, demuxer_->num_data_requests()); - // Initiate a seek. Skip the round-trip of requesting seek from renderer. + // Initiate a seek. Skip requesting element seek of renderer. // Instead behave as if the renderer has asked us to seek. player_.SeekTo(base::TimeDelta()); EXPECT_EQ(1, demuxer_->num_seek_requests()); player_.Start(); - EXPECT_EQ(NULL, GetMediaDecoderJob(true)); + EXPECT_FALSE(GetMediaDecoderJob(true)); EXPECT_EQ(0, demuxer_->num_data_requests()); // Sending back the seek done notification. - player_.OnDemuxerSeekDone(); + player_.OnDemuxerSeekDone(kNoTimestamp()); EXPECT_TRUE(GetMediaDecoderJob(true)); EXPECT_EQ(1, demuxer_->num_data_requests()); @@ -442,6 +678,33 @@ TEST_F(MediaSourcePlayerTest, StartAfterSeekFinish) { EXPECT_EQ(1, demuxer_->num_seek_requests()); } +TEST_F(MediaSourcePlayerTest, VideoOnlyStartAfterSeekFinish) { + SKIP_TEST_IF_MEDIA_CODEC_BRIDGE_IS_NOT_AVAILABLE(); + + // Test video decoder job will not start until pending seek event is handled. + CreateNextTextureAndSetVideoSurface(); + DemuxerConfigs configs = CreateVideoDemuxerConfigs(); + player_.OnDemuxerConfigsAvailable(configs); + EXPECT_FALSE(GetMediaDecoderJob(false)); + + // Initiate a seek. Skip requesting element seek of renderer. + // Instead behave as if the renderer has asked us to seek. + player_.SeekTo(base::TimeDelta()); + EXPECT_EQ(1, demuxer_->num_seek_requests()); + + player_.Start(); + EXPECT_FALSE(GetMediaDecoderJob(false)); + EXPECT_EQ(0, demuxer_->num_data_requests()); + + // Sending back the seek done notification. + player_.OnDemuxerSeekDone(kNoTimestamp()); + EXPECT_TRUE(GetMediaDecoderJob(false)); + EXPECT_EQ(1, demuxer_->num_data_requests()); + + // Reconfirm exactly 1 seek request has been made of demuxer. + EXPECT_EQ(1, demuxer_->num_seek_requests()); +} + TEST_F(MediaSourcePlayerTest, StartImmediatelyAfterPause) { SKIP_TEST_IF_MEDIA_CODEC_BRIDGE_IS_NOT_AVAILABLE(); @@ -480,23 +743,11 @@ TEST_F(MediaSourcePlayerTest, DecoderJobsCannotStartWithoutAudio) { // Test that when Start() is called, video decoder jobs will wait for audio // decoder job before start decoding the data. - DemuxerConfigs configs; - configs.audio_codec = kCodecVorbis; - configs.audio_channels = 2; - configs.audio_sampling_rate = 44100; - configs.is_audio_encrypted = false; - scoped_refptr<DecoderBuffer> buffer = ReadTestDataFile("vorbis-extradata"); - configs.audio_extra_data = std::vector<uint8>( - buffer->data(), - buffer->data() + buffer->data_size()); - configs.video_codec = kCodecVP8; - configs.video_size = gfx::Size(320, 240); - configs.is_video_encrypted = false; - configs.duration_ms = kDefaultDurationInMs; + DemuxerConfigs configs = CreateAudioVideoDemuxerConfigs(); Start(configs); EXPECT_EQ(0, demuxer_->num_data_requests()); - CreateAndSetVideoSurface(); + CreateNextTextureAndSetVideoSurface(); // Player should not seek the demuxer on setting initial surface. EXPECT_EQ(0, demuxer_->num_seek_requests()); @@ -515,6 +766,9 @@ TEST_F(MediaSourcePlayerTest, DecoderJobsCannotStartWithoutAudio) { player_.OnDemuxerDataAvailable(CreateReadFromDemuxerAckForAudio(0)); EXPECT_TRUE(audio_decoder_job->is_decoding()); EXPECT_TRUE(video_decoder_job->is_decoding()); + + // Reconfirm no seek occurred. + EXPECT_EQ(0, demuxer_->num_seek_requests()); } TEST_F(MediaSourcePlayerTest, StartTimeTicksResetAfterDecoderUnderruns) { @@ -548,7 +802,7 @@ TEST_F(MediaSourcePlayerTest, StartTimeTicksResetAfterDecoderUnderruns) { // Send new data to the decoder so it can finish the currently // pending decode. player_.OnDemuxerDataAvailable(CreateReadFromDemuxerAckForAudio(3)); - while(GetMediaDecoderJob(true)->is_decoding()) + while (GetMediaDecoderJob(true)->is_decoding()) message_loop_.RunUntilIdle(); // Verify the start time ticks is cleared at this point because the @@ -569,7 +823,7 @@ TEST_F(MediaSourcePlayerTest, NoRequestForDataAfterInputEOS) { // Test MediaSourcePlayer will not request for new data after input EOS is // reached. - CreateAndSetVideoSurface(); + CreateNextTextureAndSetVideoSurface(); StartVideoDecoderJob(); // Player should not seek the demuxer on setting initial surface. EXPECT_EQ(0, demuxer_->num_seek_requests()); @@ -585,6 +839,9 @@ TEST_F(MediaSourcePlayerTest, NoRequestForDataAfterInputEOS) { message_loop_.Run(); // No more request for data should be made. EXPECT_EQ(2, demuxer_->num_data_requests()); + + // Reconfirm no seek request has occurred. + EXPECT_EQ(0, demuxer_->num_seek_requests()); } TEST_F(MediaSourcePlayerTest, ReplayAfterInputEOS) { @@ -592,7 +849,7 @@ TEST_F(MediaSourcePlayerTest, ReplayAfterInputEOS) { // Test MediaSourcePlayer can replay after input EOS is // reached. - CreateAndSetVideoSurface(); + CreateNextTextureAndSetVideoSurface(); StartVideoDecoderJob(); // Player should not seek the demuxer on setting initial surface. @@ -610,37 +867,40 @@ TEST_F(MediaSourcePlayerTest, ReplayAfterInputEOS) { // No more request for data should be made. EXPECT_EQ(2, demuxer_->num_data_requests()); - // Initiate a seek. Skip the round-trip of requesting seek from renderer. + // Initiate a seek. Skip requesting element seek of renderer. // Instead behave as if the renderer has asked us to seek. player_.SeekTo(base::TimeDelta()); StartVideoDecoderJob(); EXPECT_EQ(1, demuxer_->num_seek_requests()); - player_.OnDemuxerSeekDone(); + player_.OnDemuxerSeekDone(kNoTimestamp()); // Seek/Play after EOS should request more data. EXPECT_EQ(3, demuxer_->num_data_requests()); + + // Reconfirm only 1 seek request has occurred. + EXPECT_EQ(1, demuxer_->num_seek_requests()); } TEST_F(MediaSourcePlayerTest, NoRequestForDataAfterAbort) { SKIP_TEST_IF_MEDIA_CODEC_BRIDGE_IS_NOT_AVAILABLE(); - // Test that the decoder will request new data after receiving an aborted + // Test that the decoder will not request new data after receiving an aborted // access unit. StartAudioDecoderJob(); EXPECT_EQ(1, demuxer_->num_data_requests()); // Send an aborted access unit. - DemuxerData data; - data.type = DemuxerStream::AUDIO; - data.access_units.resize(1); - data.access_units[0].status = DemuxerStream::kAborted; - player_.OnDemuxerDataAvailable(data); + player_.OnDemuxerDataAvailable(CreateAbortedAck(true)); + EXPECT_TRUE(GetMediaDecoderJob(true)->is_decoding()); // Wait for the decoder job to finish decoding. - while(GetMediaDecoderJob(true)->is_decoding()) + while (GetMediaDecoderJob(true)->is_decoding()) message_loop_.RunUntilIdle(); // No request will be sent for new data. EXPECT_EQ(1, demuxer_->num_data_requests()); + + // No seek requests should have occurred. + EXPECT_EQ(0, demuxer_->num_seek_requests()); } TEST_F(MediaSourcePlayerTest, DemuxerDataArrivesAfterRelease) { @@ -649,7 +909,6 @@ TEST_F(MediaSourcePlayerTest, DemuxerDataArrivesAfterRelease) { // Test that the decoder should not crash if demuxer data arrives after // Release(). StartAudioDecoderJob(); - EXPECT_TRUE(player_.IsPlaying()); EXPECT_EQ(1, demuxer_->num_data_requests()); EXPECT_TRUE(GetMediaDecoderJob(true)); @@ -659,37 +918,285 @@ TEST_F(MediaSourcePlayerTest, DemuxerDataArrivesAfterRelease) { // The decoder job should have been released. EXPECT_FALSE(player_.IsPlaying()); EXPECT_EQ(1, demuxer_->num_data_requests()); + + // No seek requests should have occurred. + EXPECT_EQ(0, demuxer_->num_seek_requests()); } -TEST_F(MediaSourcePlayerTest, PrerollAfterSeek) { - if (!MediaCodecBridge::IsAvailable()) { - LOG(INFO) << "Could not run test - not supported on device."; - return; +TEST_F(MediaSourcePlayerTest, BrowserSeek_RegularSeekPendsBrowserSeekDone) { + SKIP_TEST_IF_MEDIA_CODEC_BRIDGE_IS_NOT_AVAILABLE(); + + // Test that a browser seek, once started, delays a newly arrived regular + // SeekTo() request's demuxer seek until the browser seek is done. + BrowserSeekPlayer(false); + EXPECT_EQ(1, demuxer_->num_data_requests()); + + // Simulate renderer requesting a regular seek while browser seek in progress. + player_.SeekTo(base::TimeDelta()); + EXPECT_FALSE(GetMediaDecoderJob(false)); + + // Simulate browser seek is done. Confirm player requests the regular seek, + // still has no video decoder job configured, and has not requested any + // further data since the surface change event became pending in + // BrowserSeekPlayer(). + EXPECT_EQ(1, demuxer_->num_seek_requests()); + player_.OnDemuxerSeekDone(base::TimeDelta()); + EXPECT_FALSE(GetMediaDecoderJob(false)); + EXPECT_EQ(2, demuxer_->num_seek_requests()); + EXPECT_EQ(1, demuxer_->num_browser_seek_requests()); + EXPECT_EQ(1, demuxer_->num_data_requests()); + + // Simulate regular seek is done and confirm player requests more data for + // new video decoder job. + player_.OnDemuxerSeekDone(kNoTimestamp()); + EXPECT_TRUE(GetMediaDecoderJob(false)); + EXPECT_EQ(2, demuxer_->num_data_requests()); + EXPECT_EQ(2, demuxer_->num_seek_requests()); +} + +TEST_F(MediaSourcePlayerTest, NoSeekForInitialReleaseAndStart) { + SKIP_TEST_IF_MEDIA_CODEC_BRIDGE_IS_NOT_AVAILABLE(); + + // Test that no seek is requested if player Release() + Start() occurs prior + // to receiving any data. + CreateNextTextureAndSetVideoSurface(); + StartVideoDecoderJob(); + EXPECT_EQ(1, demuxer_->num_data_requests()); + EXPECT_TRUE(GetMediaDecoderJob(false)); + + player_.Release(); + EXPECT_FALSE(player_.IsPlaying()); + + // Pass a new non-empty surface. + CreateNextTextureAndSetVideoSurface(); + + player_.Start(); + + // TODO(wolenetz/qinmin): Multiple in-flight data requests for same stream + // should be prevented. See http://crbug.com/306314. + EXPECT_EQ(2, demuxer_->num_data_requests()); + + EXPECT_EQ(0, demuxer_->num_seek_requests()); + EXPECT_TRUE(GetMediaDecoderJob(false)); +} + +TEST_F(MediaSourcePlayerTest, BrowserSeek_MidStreamReleaseAndStart) { + SKIP_TEST_IF_MEDIA_CODEC_BRIDGE_IS_NOT_AVAILABLE(); + + // Test that one browser seek is requested if player Release() + Start(), with + // video data received between Release() and Start(). + BrowserSeekPlayer(true); + EXPECT_FALSE(GetMediaDecoderJob(false)); + EXPECT_EQ(1, demuxer_->num_data_requests()); + + // Simulate browser seek is done and confirm player requests more data. + player_.OnDemuxerSeekDone(base::TimeDelta()); + EXPECT_TRUE(GetMediaDecoderJob(false)); + EXPECT_EQ(2, demuxer_->num_data_requests()); + EXPECT_EQ(1, demuxer_->num_seek_requests()); +} + +TEST_F(MediaSourcePlayerTest, PrerollAudioAfterSeek) { + SKIP_TEST_IF_MEDIA_CODEC_BRIDGE_IS_NOT_AVAILABLE(); + + // Test decoder job will preroll the media to the seek position. + StartAudioDecoderJob(); + EXPECT_TRUE(GetMediaDecoderJob(true)); + EXPECT_EQ(1, demuxer_->num_data_requests()); + + SeekPlayer(true, base::TimeDelta::FromMilliseconds(100)); + EXPECT_TRUE(IsPrerolling(true)); + EXPECT_EQ(100.0, GetPrerollTimestamp().InMillisecondsF()); + + // Send some data before the seek position. + for (int i = 1; i < 4; ++i) { + player_.OnDemuxerDataAvailable(CreateReadFromDemuxerAckForAudio(i)); + EXPECT_TRUE(GetMediaDecoderJob(true)->is_decoding()); + message_loop_.Run(); } + EXPECT_EQ(100.0, player_.GetCurrentTime().InMillisecondsF()); + EXPECT_TRUE(IsPrerolling(true)); + + // Send data after the seek position. + DemuxerData data = CreateReadFromDemuxerAckForAudio(3); + data.access_units[0].timestamp = base::TimeDelta::FromMilliseconds(100); + player_.OnDemuxerDataAvailable(data); + message_loop_.Run(); + EXPECT_LT(100.0, player_.GetCurrentTime().InMillisecondsF()); + EXPECT_FALSE(IsPrerolling(true)); +} + +TEST_F(MediaSourcePlayerTest, PrerollVideoAfterSeek) { + SKIP_TEST_IF_MEDIA_CODEC_BRIDGE_IS_NOT_AVAILABLE(); // Test decoder job will preroll the media to the seek position. + CreateNextTextureAndSetVideoSurface(); + StartVideoDecoderJob(); + EXPECT_TRUE(GetMediaDecoderJob(false)); + EXPECT_EQ(1, demuxer_->num_data_requests()); + + SeekPlayer(false, base::TimeDelta::FromMilliseconds(100)); + EXPECT_TRUE(IsPrerolling(false)); + EXPECT_EQ(100.0, GetPrerollTimestamp().InMillisecondsF()); + + // Send some data before the seek position. + DemuxerData data; + for (int i = 1; i < 4; ++i) { + data = CreateReadFromDemuxerAckForVideo(); + data.access_units[0].timestamp = base::TimeDelta::FromMilliseconds(i * 30); + player_.OnDemuxerDataAvailable(data); + EXPECT_TRUE(GetMediaDecoderJob(false)->is_decoding()); + message_loop_.Run(); + } + EXPECT_EQ(100.0, player_.GetCurrentTime().InMillisecondsF()); + EXPECT_TRUE(IsPrerolling(false)); + + // Send data at the seek position. + data = CreateReadFromDemuxerAckForVideo(); + data.access_units[0].timestamp = base::TimeDelta::FromMilliseconds(100); + player_.OnDemuxerDataAvailable(data); + message_loop_.Run(); + + // TODO(wolenetz/qinmin): Player's maintenance of current time for video-only + // streams depends on decoder output, which may be initially inaccurate, and + // encoded video test data may also need updating. Verify at least that AU + // timestamp-based preroll logic has determined video preroll has completed. + EXPECT_FALSE(IsPrerolling(false)); +} + +TEST_F(MediaSourcePlayerTest, SeekingAfterCompletingPrerollRestartsPreroll) { + SKIP_TEST_IF_MEDIA_CODEC_BRIDGE_IS_NOT_AVAILABLE(); + + // Test decoder job will begin prerolling upon seek, when it was not + // prerolling prior to the seek. + StartAudioDecoderJob(); + MediaDecoderJob* decoder_job = GetMediaDecoderJob(true); + EXPECT_TRUE(decoder_job); + EXPECT_EQ(1, demuxer_->num_data_requests()); + EXPECT_TRUE(IsPrerolling(true)); + + // Complete the initial preroll by feeding data to the decoder. + for (int i = 0; i < 4; ++i) { + player_.OnDemuxerDataAvailable(CreateReadFromDemuxerAckForAudio(i)); + EXPECT_TRUE(GetMediaDecoderJob(true)->is_decoding()); + message_loop_.Run(); + } + EXPECT_LT(0.0, player_.GetCurrentTime().InMillisecondsF()); + EXPECT_FALSE(IsPrerolling(true)); + + SeekPlayer(true, base::TimeDelta::FromMilliseconds(500)); + + // Prerolling should have begun again. + EXPECT_TRUE(IsPrerolling(true)); + EXPECT_EQ(500.0, GetPrerollTimestamp().InMillisecondsF()); + + // Send data at and after the seek position. Prerolling should complete. + for (int i = 0; i < 4; ++i) { + DemuxerData data = CreateReadFromDemuxerAckForAudio(i); + data.access_units[0].timestamp = base::TimeDelta::FromMilliseconds( + 500 + 30 * (i - 1)); + player_.OnDemuxerDataAvailable(data); + EXPECT_TRUE(GetMediaDecoderJob(true)->is_decoding()); + message_loop_.Run(); + } + EXPECT_LT(500.0, player_.GetCurrentTime().InMillisecondsF()); + EXPECT_FALSE(IsPrerolling(true)); + + // Throughout this test, we should have not re-created the decoder job, so + // IsPrerolling() transition from false to true was not due to constructor + // initialization. It was due to BeginPrerolling(). + EXPECT_EQ(decoder_job, GetMediaDecoderJob(true)); +} + +TEST_F(MediaSourcePlayerTest, PrerollContinuesAcrossReleaseAndStart) { + SKIP_TEST_IF_MEDIA_CODEC_BRIDGE_IS_NOT_AVAILABLE(); + + // Test decoder job will resume media prerolling if interrupted by Release() + // and Start(). StartAudioDecoderJob(); EXPECT_TRUE(GetMediaDecoderJob(true)); EXPECT_EQ(1, demuxer_->num_data_requests()); - // Initiate a seek to 100 ms. This will abort the intial prefetch. - player_.SeekTo(base::TimeDelta::FromMilliseconds(100)); + SeekPlayer(true, base::TimeDelta::FromMilliseconds(100)); + EXPECT_TRUE(IsPrerolling(true)); + EXPECT_EQ(100.0, GetPrerollTimestamp().InMillisecondsF()); + + // Send some data before the seek position. + // Test uses 'large' number of iterations because decoder job may not get + // MEDIA_CODEC_OK output status until after a few dequeue output attempts. + // This allows decoder status to stabilize prior to AU timestamp reaching + // the preroll target. DemuxerData data; - data.type = DemuxerStream::AUDIO; - data.access_units.resize(1); - data.access_units[0].status = DemuxerStream::kAborted; + for (int i = 0; i < 10; ++i) { + data = CreateReadFromDemuxerAckForAudio(3); + data.access_units[0].timestamp = base::TimeDelta::FromMilliseconds(i * 10); + if (i == 1) { + // While still prerolling, Release() and Start() the player. + // TODO(qinmin): Simulation of multiple in-flight data requests (one from + // before Release(), one from after Start()) is not included here, and + // neither is any data enqueued for later decode if it arrives after + // Release() and before Start(). See http://crbug.com/306314. Assumption + // for this test, to prevent flakiness until the bug is fixed, is the + // first request's data arrives before Start(). Though that data is not + // seen by decoder, this assumption allows preroll continuation + // verification and prevents multiple in-flight data requests. + player_.Release(); + player_.OnDemuxerDataAvailable(data); + message_loop_.RunUntilIdle(); + EXPECT_FALSE(GetMediaDecoderJob(true)); + StartAudioDecoderJob(); + EXPECT_TRUE(GetMediaDecoderJob(true)); + } else { + player_.OnDemuxerDataAvailable(data); + EXPECT_TRUE(GetMediaDecoderJob(true)->is_decoding()); + message_loop_.Run(); + } + EXPECT_TRUE(IsPrerolling(true)); + } + EXPECT_EQ(100.0, player_.GetCurrentTime().InMillisecondsF()); + EXPECT_TRUE(IsPrerolling(true)); + + // Send data after the seek position. + data = CreateReadFromDemuxerAckForAudio(3); + data.access_units[0].timestamp = base::TimeDelta::FromMilliseconds(100); player_.OnDemuxerDataAvailable(data); - EXPECT_EQ(1, demuxer_->num_seek_requests()); - player_.OnDemuxerSeekDone(); - EXPECT_EQ(2, demuxer_->num_data_requests()); + message_loop_.Run(); + EXPECT_LT(100.0, player_.GetCurrentTime().InMillisecondsF()); + EXPECT_FALSE(IsPrerolling(true)); +} + +TEST_F(MediaSourcePlayerTest, PrerollContinuesAcrossConfigChange) { + SKIP_TEST_IF_MEDIA_CODEC_BRIDGE_IS_NOT_AVAILABLE(); + + // Test decoder job will resume media prerolling if interrupted by + // |kConfigChanged| and OnDemuxerConfigsAvailable(). + StartAudioDecoderJob(); + EXPECT_TRUE(GetMediaDecoderJob(true)); + EXPECT_EQ(1, demuxer_->num_data_requests()); + + SeekPlayer(true, base::TimeDelta::FromMilliseconds(100)); + EXPECT_TRUE(IsPrerolling(true)); + EXPECT_EQ(100.0, GetPrerollTimestamp().InMillisecondsF()); + + // In response to data request, simulate that demuxer signals config change by + // sending an AU with |kConfigChanged|. Player should prepare to reconfigure + // the audio decoder job, and should request new demuxer configs. + DemuxerData data = CreateReadFromDemuxerAckWithConfigChanged(true, 0); + EXPECT_EQ(0, demuxer_->num_config_requests()); + player_.OnDemuxerDataAvailable(data); + EXPECT_EQ(1, demuxer_->num_config_requests()); + + // Simulate arrival of new configs. + player_.OnDemuxerConfigsAvailable(CreateAudioDemuxerConfigs()); - // Send some data with before the seek position. + // Send some data before the seek position. for (int i = 1; i < 4; ++i) { player_.OnDemuxerDataAvailable(CreateReadFromDemuxerAckForAudio(i)); EXPECT_TRUE(GetMediaDecoderJob(true)->is_decoding()); message_loop_.Run(); } EXPECT_EQ(100.0, player_.GetCurrentTime().InMillisecondsF()); + EXPECT_TRUE(IsPrerolling(true)); // Send data after the seek position. data = CreateReadFromDemuxerAckForAudio(3); @@ -697,6 +1204,179 @@ TEST_F(MediaSourcePlayerTest, PrerollAfterSeek) { player_.OnDemuxerDataAvailable(data); message_loop_.Run(); EXPECT_LT(100.0, player_.GetCurrentTime().InMillisecondsF()); + EXPECT_FALSE(IsPrerolling(true)); +} + +TEST_F(MediaSourcePlayerTest, DemuxerConfigRequestedIfInPrefetchUnit0) { + SKIP_TEST_IF_MEDIA_CODEC_BRIDGE_IS_NOT_AVAILABLE(); + + // Test that the player detects need for and requests demuxer configs if + // the |kConfigChanged| unit is the very first unit in the set of units + // received in OnDemuxerDataAvailable() ostensibly while + // |PREFETCH_DONE_EVENT_PENDING|. + StartConfigChange(true, true, 0); +} + +TEST_F(MediaSourcePlayerTest, DemuxerConfigRequestedIfInPrefetchUnit1) { + SKIP_TEST_IF_MEDIA_CODEC_BRIDGE_IS_NOT_AVAILABLE(); + + // Test that the player detects need for and requests demuxer configs if + // the |kConfigChanged| unit is not the first unit in the set of units + // received in OnDemuxerDataAvailable() ostensibly while + // |PREFETCH_DONE_EVENT_PENDING|. + StartConfigChange(true, true, 1); +} + +TEST_F(MediaSourcePlayerTest, DemuxerConfigRequestedIfInUnit0AfterPrefetch) { + SKIP_TEST_IF_MEDIA_CODEC_BRIDGE_IS_NOT_AVAILABLE(); + + // Test that the player detects need for and requests demuxer configs if + // the |kConfigChanged| unit is the very first unit in the set of units + // received in OnDemuxerDataAvailable() from data requested ostensibly while + // not prefetching. + StartConfigChange(true, false, 0); +} + +TEST_F(MediaSourcePlayerTest, DemuxerConfigRequestedIfInUnit1AfterPrefetch) { + SKIP_TEST_IF_MEDIA_CODEC_BRIDGE_IS_NOT_AVAILABLE(); + + // Test that the player detects need for and requests demuxer configs if + // the |kConfigChanged| unit is not the first unit in the set of units + // received in OnDemuxerDataAvailable() from data requested ostensibly while + // not prefetching. + StartConfigChange(true, false, 1); +} + +TEST_F(MediaSourcePlayerTest, BrowserSeek_PrerollAfterBrowserSeek) { + SKIP_TEST_IF_MEDIA_CODEC_BRIDGE_IS_NOT_AVAILABLE(); + + // Test decoder job will preroll the media to the actual seek position + // resulting from a browser seek. + BrowserSeekPlayer(false); + + // Simulate browser seek is done, but to a later time than was requested. + EXPECT_LT(player_.GetCurrentTime().InMillisecondsF(), 100); + player_.OnDemuxerSeekDone(base::TimeDelta::FromMilliseconds(100)); + EXPECT_TRUE(GetMediaDecoderJob(false)); + EXPECT_EQ(100.0, player_.GetCurrentTime().InMillisecondsF()); + EXPECT_EQ(100.0, GetPrerollTimestamp().InMillisecondsF()); + EXPECT_EQ(2, demuxer_->num_data_requests()); + + // Send some data with access unit timestamps before the actual browser seek + // position. This is a bit unrealistic in this case where the browser seek + // jumped forward and next data from demuxer would normally begin at this + // browser seek position, immediately completing preroll. For simplicity and + // coverage, this test simulates the more common condition that AUs received + // after browser seek begin with timestamps before the seek target, and don't + // immediately complete preroll. + DemuxerData data; + for (int i = 1; i < 4; ++i) { + data = CreateReadFromDemuxerAckForVideo(); + data.access_units[0].timestamp = base::TimeDelta::FromMilliseconds(i * 30); + player_.OnDemuxerDataAvailable(data); + EXPECT_TRUE(GetMediaDecoderJob(false)->is_decoding()); + message_loop_.Run(); + EXPECT_TRUE(IsPrerolling(false)); + } + + EXPECT_EQ(100.0, player_.GetCurrentTime().InMillisecondsF()); + + // Send data after the browser seek position. + data = CreateReadFromDemuxerAckForVideo(); + data.access_units[0].timestamp = base::TimeDelta::FromMilliseconds(120); + player_.OnDemuxerDataAvailable(data); + message_loop_.Run(); + EXPECT_FALSE(IsPrerolling(false)); +} + +TEST_F(MediaSourcePlayerTest, VideoDemuxerConfigChange) { + SKIP_TEST_IF_MEDIA_CODEC_BRIDGE_IS_NOT_AVAILABLE(); + + // Test that video config change notification results in request for demuxer + // configuration, and that a video decoder job results without any browser + // seek necessary once the new demuxer config arrives. + StartConfigChange(false, true, 1); + MediaDecoderJob* first_job = GetMediaDecoderJob(false); + EXPECT_TRUE(first_job); + EXPECT_EQ(1, demuxer_->num_data_requests()); + EXPECT_EQ(1, demuxer_->num_config_requests()); + + // Simulate arrival of new configs. + player_.OnDemuxerConfigsAvailable(CreateVideoDemuxerConfigs()); + + // New video decoder job should have been created and configured, without any + // browser seek. + MediaDecoderJob* second_job = GetMediaDecoderJob(false); + EXPECT_TRUE(second_job); + EXPECT_NE(first_job, second_job); + EXPECT_EQ(2, demuxer_->num_data_requests()); + EXPECT_EQ(1, demuxer_->num_config_requests()); + EXPECT_EQ(0, demuxer_->num_seek_requests()); +} + +TEST_F(MediaSourcePlayerTest, VideoConfigChangeContinuesAcrossSeek) { + SKIP_TEST_IF_MEDIA_CODEC_BRIDGE_IS_NOT_AVAILABLE(); + + // Test if a demuxer config request is pending (due to previously receiving + // |kConfigChanged|), and a seek request arrives prior to demuxer configs, + // then seek is processed first, followed by the decoder config change. + // This assumes the demuxer sends |kConfigChanged| read response prior to + // canceling any reads pending seek; no |kAborted| is involved in this test. + StartConfigChange(false, false, 1); + MediaDecoderJob* first_job = GetMediaDecoderJob(false); + EXPECT_TRUE(first_job); + EXPECT_EQ(1, demuxer_->num_config_requests()); + EXPECT_EQ(2, demuxer_->num_data_requests()); + EXPECT_EQ(0, demuxer_->num_seek_requests()); + + player_.SeekTo(base::TimeDelta::FromMilliseconds(100)); + + // Verify that the seek is requested immediately. + EXPECT_EQ(1, demuxer_->num_seek_requests()); + + // Simulate unlikely delayed arrival of the demuxer configs, completing the + // config change. + // TODO(wolenetz): Is it even possible for requested demuxer configs to be + // delayed until after a SeekTo request arrives? + player_.OnDemuxerConfigsAvailable(CreateVideoDemuxerConfigs()); + + MediaDecoderJob* second_job = GetMediaDecoderJob(false); + EXPECT_NE(first_job, second_job); + EXPECT_TRUE(second_job); + + // Send back the seek done notification. This should finish the seek and + // trigger the player to request more data. + EXPECT_EQ(2, demuxer_->num_data_requests()); + player_.OnDemuxerSeekDone(kNoTimestamp()); + EXPECT_EQ(3, demuxer_->num_data_requests()); +} + +TEST_F(MediaSourcePlayerTest, NewSurfaceWhileChangingConfigs) { + SKIP_TEST_IF_MEDIA_CODEC_BRIDGE_IS_NOT_AVAILABLE(); + + // Test that no seek or duplicated demuxer config request results from a + // SetVideoSurface() that occurs while the player is expecting new demuxer + // configs. This test may be good to keep beyond browser seek hack. + StartConfigChange(false, false, 1); + MediaDecoderJob* first_job = GetMediaDecoderJob(false); + EXPECT_TRUE(first_job); + EXPECT_EQ(1, demuxer_->num_config_requests()); + EXPECT_EQ(2, demuxer_->num_data_requests()); + + CreateNextTextureAndSetVideoSurface(); + + // Surface change processing (including decoder job re-creation) should + // not occur until the pending video config change is completed. + EXPECT_EQ(first_job, GetMediaDecoderJob(false)); + + player_.OnDemuxerConfigsAvailable(CreateVideoDemuxerConfigs()); + MediaDecoderJob* second_job = GetMediaDecoderJob(false); + EXPECT_NE(first_job, second_job); + EXPECT_TRUE(second_job); + + EXPECT_EQ(3, demuxer_->num_data_requests()); + EXPECT_EQ(1, demuxer_->num_config_requests()); + EXPECT_EQ(0, demuxer_->num_seek_requests()); } // TODO(xhwang): Enable this test when the test devices are updated. @@ -771,7 +1451,5 @@ TEST_F(MediaSourcePlayerTest, IsTypeSupported_InvalidUUID) { // TODO(xhwang): Are these IsTypeSupported tests device specific? // TODO(xhwang): Add more IsTypeSupported tests. -// TODO(wolenetz): Add tests around second SetVideoSurface, once fix to reach -// next I-frame is correct. } // namespace media diff --git a/media/base/audio_bus_perftest.cc b/media/base/audio_bus_perftest.cc new file mode 100644 index 0000000000..e4152fdfc6 --- /dev/null +++ b/media/base/audio_bus_perftest.cc @@ -0,0 +1,52 @@ +// Copyright 2013 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/time/time.h" +#include "media/base/audio_bus.h" +#include "media/base/fake_audio_render_callback.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/perf/perf_test.h" + +namespace media { + +static const int kBenchmarkIterations = 100; + +template <typename T> +void RunInterleaveBench(AudioBus* bus, const std::string& trace_name) { + const int frame_size = bus->frames() * bus->channels(); + scoped_ptr<T> interleaved(new T[frame_size]); + const int byte_size = sizeof(*interleaved); + + base::TimeTicks start = base::TimeTicks::HighResNow(); + for (int i = 0; i < kBenchmarkIterations; ++i) { + bus->ToInterleaved(bus->frames(), byte_size, interleaved.get()); + } + double total_time_seconds = + (base::TimeTicks::HighResNow() - start).InSecondsF(); + perf_test::PrintResult( + "audio_bus_to_interleaved", "", trace_name, + kBenchmarkIterations / total_time_seconds, "runs/s", true); + + start = base::TimeTicks::HighResNow(); + for (int i = 0; i < kBenchmarkIterations; ++i) { + bus->FromInterleaved(interleaved.get(), bus->frames(), byte_size); + } + total_time_seconds = (base::TimeTicks::HighResNow() - start).InSecondsF(); + perf_test::PrintResult( + "audio_bus_from_interleaved", "", trace_name, + kBenchmarkIterations / total_time_seconds, "runs/s", true); +} + +// Benchmark the FromInterleaved() and ToInterleaved() methods. +TEST(AudioBusPerfTest, Interleave) { + scoped_ptr<AudioBus> bus = AudioBus::Create(2, 48000 * 120); + FakeAudioRenderCallback callback(0.2); + callback.Render(bus.get(), 0); + + RunInterleaveBench<int8>(bus.get(), "int8"); + RunInterleaveBench<int16>(bus.get(), "int16"); + RunInterleaveBench<int32>(bus.get(), "int32"); +} + +} // namespace media diff --git a/media/base/audio_bus_unittest.cc b/media/base/audio_bus_unittest.cc index a82090bd8e..e8c78a36b4 100644 --- a/media/base/audio_bus_unittest.cc +++ b/media/base/audio_bus_unittest.cc @@ -413,60 +413,4 @@ TEST_F(AudioBusTest, Scale) { } } -// Benchmark the FromInterleaved() and ToInterleaved() methods. -TEST_F(AudioBusTest, DISABLED_InterleaveBench) { - scoped_ptr<AudioBus> bus = AudioBus::Create(2, 48000 * 120); - const int frame_size = bus->frames() * bus->channels(); - FakeAudioRenderCallback callback(0.2); - callback.Render(bus.get(), 0); - { - SCOPED_TRACE("uint8"); - scoped_ptr<uint8> interleaved(new uint8[frame_size]); - const int byte_size = sizeof(*interleaved); - - base::TimeTicks start = base::TimeTicks::HighResNow(); - bus->ToInterleaved(bus->frames(), byte_size, interleaved.get()); - double total_time_ms = - (base::TimeTicks::HighResNow() - start).InMillisecondsF(); - printf("ToInterleaved uint8 took %.2fms.\n", total_time_ms); - - start = base::TimeTicks::HighResNow(); - bus->FromInterleaved(interleaved.get(), bus->frames(), byte_size); - total_time_ms = (base::TimeTicks::HighResNow() - start).InMillisecondsF(); - printf("FromInterleaved uint8 took %.2fms.\n", total_time_ms); - } - { - SCOPED_TRACE("int16"); - scoped_ptr<int16> interleaved(new int16[frame_size]); - const int byte_size = sizeof(*interleaved); - - base::TimeTicks start = base::TimeTicks::HighResNow(); - bus->ToInterleaved(bus->frames(), byte_size, interleaved.get()); - double total_time_ms = - (base::TimeTicks::HighResNow() - start).InMillisecondsF(); - printf("ToInterleaved int16 took %.2fms.\n", total_time_ms); - - start = base::TimeTicks::HighResNow(); - bus->FromInterleaved(interleaved.get(), bus->frames(), byte_size); - total_time_ms = (base::TimeTicks::HighResNow() - start).InMillisecondsF(); - printf("FromInterleaved int16 took %.2fms.\n", total_time_ms); - } - { - SCOPED_TRACE("int32"); - scoped_ptr<int32> interleaved(new int32[frame_size]); - const int byte_size = sizeof(*interleaved); - - base::TimeTicks start = base::TimeTicks::HighResNow(); - bus->ToInterleaved(bus->frames(), byte_size, interleaved.get()); - double total_time_ms = - (base::TimeTicks::HighResNow() - start).InMillisecondsF(); - printf("ToInterleaved int32 took %.2fms.\n", total_time_ms); - - start = base::TimeTicks::HighResNow(); - bus->FromInterleaved(interleaved.get(), bus->frames(), byte_size); - total_time_ms = (base::TimeTicks::HighResNow() - start).InMillisecondsF(); - printf("FromInterleaved int32 took %.2fms.\n", total_time_ms); - } -} - } // namespace media diff --git a/media/base/audio_converter_perftest.cc b/media/base/audio_converter_perftest.cc new file mode 100644 index 0000000000..f8570e1ec3 --- /dev/null +++ b/media/base/audio_converter_perftest.cc @@ -0,0 +1,79 @@ +// Copyright 2013 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/time/time.h" +#include "media/base/audio_converter.h" +#include "media/base/fake_audio_render_callback.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/perf/perf_test.h" + +namespace media { + +static const int kBenchmarkIterations = 500000; + +// InputCallback that zero's out the provided AudioBus. +class NullInputProvider : public AudioConverter::InputCallback { + public: + NullInputProvider() {} + virtual ~NullInputProvider() {} + + virtual double ProvideInput(AudioBus* audio_bus, + base::TimeDelta buffer_delay) OVERRIDE { + audio_bus->Zero(); + return 1; + } +}; + +void RunConvertBenchmark(const AudioParameters& in_params, + const AudioParameters& out_params, + bool fifo, + const std::string& trace_name) { + NullInputProvider fake_input1; + NullInputProvider fake_input2; + NullInputProvider fake_input3; + scoped_ptr<AudioBus> output_bus = AudioBus::Create(out_params); + + AudioConverter converter(in_params, out_params, !fifo); + converter.AddInput(&fake_input1); + converter.AddInput(&fake_input2); + converter.AddInput(&fake_input3); + + base::TimeTicks start = base::TimeTicks::HighResNow(); + for (int i = 0; i < kBenchmarkIterations; ++i) { + converter.Convert(output_bus.get()); + } + double runs_per_second = kBenchmarkIterations / + (base::TimeTicks::HighResNow() - start).InSecondsF(); + perf_test::PrintResult( + "audio_converter", "", trace_name, runs_per_second, "runs/s", true); +} + +TEST(AudioConverterPerfTest, ConvertBenchmark) { + // Create input and output parameters to convert between the two most common + // sets of parameters (as indicated via UMA data). + AudioParameters input_params( + AudioParameters::AUDIO_PCM_LINEAR, CHANNEL_LAYOUT_MONO, 48000, 16, 2048); + AudioParameters output_params( + AudioParameters::AUDIO_PCM_LINEAR, CHANNEL_LAYOUT_STEREO, 44100, 16, 440); + + RunConvertBenchmark(input_params, output_params, false, "convert"); +} + +TEST(AudioConverterPerfTest, ConvertBenchmarkFIFO) { + // Create input and output parameters to convert between common buffer sizes + // without any resampling for the FIFO vs no FIFO benchmarks. + AudioParameters input_params(AudioParameters::AUDIO_PCM_LINEAR, + CHANNEL_LAYOUT_STEREO, + 44100, + 16, + 2048); + AudioParameters output_params( + AudioParameters::AUDIO_PCM_LINEAR, CHANNEL_LAYOUT_STEREO, 44100, 16, 440); + + RunConvertBenchmark(input_params, output_params, true, "convert_fifo_only"); + RunConvertBenchmark(input_params, output_params, false, + "convert_pass_through"); +} + +} // namespace media diff --git a/media/base/audio_converter_unittest.cc b/media/base/audio_converter_unittest.cc index d218ac882f..aeb021c311 100644 --- a/media/base/audio_converter_unittest.cc +++ b/media/base/audio_converter_unittest.cc @@ -7,12 +7,9 @@ #include <cmath> -#include "base/command_line.h" -#include "base/logging.h" #include "base/memory/scoped_ptr.h" #include "base/memory/scoped_vector.h" #include "base/strings/string_number_conversions.h" -#include "base/time/time.h" #include "media/base/audio_converter.h" #include "media/base/fake_audio_render_callback.h" #include "testing/gmock/include/gmock/gmock.h" @@ -20,10 +17,6 @@ namespace media { -// Command line switch for runtime adjustment of benchmark iterations. -static const char kBenchmarkIterations[] = "audio-converter-iterations"; -static const int kDefaultIterations = 10; - // Parameters which control the many input case tests. static const int kConvertInputs = 8; static const int kConvertCycles = 3; @@ -234,107 +227,6 @@ TEST(AudioConverterTest, AudioDelay) { callback.last_audio_delay_milliseconds()); } -// InputCallback that zero's out the provided AudioBus. Used for benchmarking. -class NullInputProvider : public AudioConverter::InputCallback { - public: - NullInputProvider() {} - virtual ~NullInputProvider() {} - - virtual double ProvideInput(AudioBus* audio_bus, - base::TimeDelta buffer_delay) OVERRIDE { - audio_bus->Zero(); - return 1; - } -}; - -// Benchmark for audio conversion. Original benchmarks were run with -// --audio-converter-iterations=50000. -TEST(AudioConverterTest, ConvertBenchmark) { - int benchmark_iterations = kDefaultIterations; - std::string iterations(CommandLine::ForCurrentProcess()->GetSwitchValueASCII( - kBenchmarkIterations)); - base::StringToInt(iterations, &benchmark_iterations); - if (benchmark_iterations < kDefaultIterations) - benchmark_iterations = kDefaultIterations; - - NullInputProvider fake_input1; - NullInputProvider fake_input2; - NullInputProvider fake_input3; - - printf("Benchmarking %d iterations:\n", benchmark_iterations); - - { - // Create input and output parameters to convert between the two most common - // sets of parameters (as indicated via UMA data). - AudioParameters input_params( - AudioParameters::AUDIO_PCM_LINEAR, CHANNEL_LAYOUT_MONO, - 48000, 16, 2048); - AudioParameters output_params( - AudioParameters::AUDIO_PCM_LINEAR, CHANNEL_LAYOUT_STEREO, - 44100, 16, 440); - scoped_ptr<AudioBus> output_bus = AudioBus::Create(output_params); - - scoped_ptr<AudioConverter> converter( - new AudioConverter(input_params, output_params, true)); - converter->AddInput(&fake_input1); - converter->AddInput(&fake_input2); - converter->AddInput(&fake_input3); - - // Benchmark Convert() w/ FIFO. - base::TimeTicks start = base::TimeTicks::HighResNow(); - for (int i = 0; i < benchmark_iterations; ++i) { - converter->Convert(output_bus.get()); - } - double total_time_ms = - (base::TimeTicks::HighResNow() - start).InMillisecondsF(); - printf("Convert() w/ Resampling took %.2fms.\n", total_time_ms); - } - - // Create input and output parameters to convert between common buffer sizes - // without any resampling for the FIFO vs no FIFO benchmarks. - AudioParameters input_params( - AudioParameters::AUDIO_PCM_LINEAR, CHANNEL_LAYOUT_STEREO, - 44100, 16, 2048); - AudioParameters output_params( - AudioParameters::AUDIO_PCM_LINEAR, CHANNEL_LAYOUT_STEREO, - 44100, 16, 440); - scoped_ptr<AudioBus> output_bus = AudioBus::Create(output_params); - - { - scoped_ptr<AudioConverter> converter( - new AudioConverter(input_params, output_params, false)); - converter->AddInput(&fake_input1); - converter->AddInput(&fake_input2); - converter->AddInput(&fake_input3); - - // Benchmark Convert() w/ FIFO. - base::TimeTicks start = base::TimeTicks::HighResNow(); - for (int i = 0; i < benchmark_iterations; ++i) { - converter->Convert(output_bus.get()); - } - double total_time_ms = - (base::TimeTicks::HighResNow() - start).InMillisecondsF(); - printf("Convert() w/ FIFO took %.2fms.\n", total_time_ms); - } - - { - scoped_ptr<AudioConverter> converter( - new AudioConverter(input_params, output_params, true)); - converter->AddInput(&fake_input1); - converter->AddInput(&fake_input2); - converter->AddInput(&fake_input3); - - // Benchmark Convert() w/o FIFO. - base::TimeTicks start = base::TimeTicks::HighResNow(); - for (int i = 0; i < benchmark_iterations; ++i) { - converter->Convert(output_bus.get()); - } - double total_time_ms = - (base::TimeTicks::HighResNow() - start).InMillisecondsF(); - printf("Convert() w/o FIFO took %.2fms.\n", total_time_ms); - } -} - TEST_P(AudioConverterTest, NoInputs) { FillAudioData(1.0f); EXPECT_TRUE(RenderAndValidateAudioData(0.0f)); diff --git a/media/base/media_switches.cc b/media/base/media_switches.cc index c295a0d991..4e75216e0c 100644 --- a/media/base/media_switches.cc +++ b/media/base/media_switches.cc @@ -21,10 +21,6 @@ const char kDisableVp8AlphaPlayback[] = "disable-vp8-alpha-playback"; // Set number of threads to use for video decoding. const char kVideoThreads[] = "video-threads"; -// Override suppressed responses to canPlayType(). -const char kOverrideEncryptedMediaCanPlayType[] = - "override-encrypted-media-canplaytype"; - // Enables MP3 stream parser for Media Source Extensions. const char kEnableMP3StreamParser[] = "enable-mp3-stream-parser"; @@ -33,9 +29,6 @@ const char kEnableMP3StreamParser[] = "enable-mp3-stream-parser"; const char kDisableInfobarForProtectedMediaIdentifier[] = "disable-infobar-for-protected-media-identifier"; -// Enables use of MediaDrm for Encrypted Media Extensions implementation. -const char kEnableMediaDrm[] = "enable-mediadrm"; - // Enables use of non-compositing MediaDrm decoding by default for Encrypted // Media Extensions implementation. const char kMediaDrmEnableNonCompositing[] = "mediadrm-enable-non-compositing"; @@ -63,6 +56,13 @@ const char kAlsaOutputDevice[] = "alsa-output-device"; // tested. See http://crbug.com/158170. // TODO(dalecurtis): Remove this once we're sure nothing has exploded. const char kDisableMainThreadAudio[] = "disable-main-thread-audio"; +// AVFoundation is available in versions 10.7 and onwards, and is to be used +// http://crbug.com/288562 for both audio and video device monitoring and for +// video capture. Being a dynamically loaded NSBundle and library, it hits the +// Chrome startup time (http://crbug.com/311325 and http://crbug.com/311437); +// until development is finished and the library load time issue is solved, the +// usage of this library is hidden behind this flag. +const char kEnableAVFoundation[] = "enable-avfoundation"; #endif #if defined(OS_WIN) diff --git a/media/base/media_switches.h b/media/base/media_switches.h index 963a351cba..69fb10e260 100644 --- a/media/base/media_switches.h +++ b/media/base/media_switches.h @@ -22,13 +22,10 @@ MEDIA_EXPORT extern const char kDisableVp8AlphaPlayback[]; MEDIA_EXPORT extern const char kVideoThreads[]; -MEDIA_EXPORT extern const char kOverrideEncryptedMediaCanPlayType[]; - MEDIA_EXPORT extern const char kEnableMP3StreamParser[]; #if defined(OS_ANDROID) MEDIA_EXPORT extern const char kDisableInfobarForProtectedMediaIdentifier[]; -MEDIA_EXPORT extern const char kEnableMediaDrm[]; MEDIA_EXPORT extern const char kMediaDrmEnableNonCompositing[]; #endif @@ -43,6 +40,7 @@ MEDIA_EXPORT extern const char kAlsaOutputDevice[]; #if defined(OS_MACOSX) MEDIA_EXPORT extern const char kDisableMainThreadAudio[]; +MEDIA_EXPORT extern const char kEnableAVFoundation[]; #endif #if defined(OS_WIN) diff --git a/media/base/sinc_resampler.h b/media/base/sinc_resampler.h index 77f331bf38..217077830c 100644 --- a/media/base/sinc_resampler.h +++ b/media/base/sinc_resampler.h @@ -75,7 +75,7 @@ class MEDIA_EXPORT SincResampler { private: FRIEND_TEST_ALL_PREFIXES(SincResamplerTest, Convolve); - FRIEND_TEST_ALL_PREFIXES(SincResamplerTest, ConvolveBenchmark); + FRIEND_TEST_ALL_PREFIXES(SincResamplerPerfTest, Convolve); void InitializeKernel(); void UpdateRegions(bool second_load); diff --git a/media/base/sinc_resampler_perftest.cc b/media/base/sinc_resampler_perftest.cc new file mode 100644 index 0000000000..c7e75170e6 --- /dev/null +++ b/media/base/sinc_resampler_perftest.cc @@ -0,0 +1,76 @@ +// Copyright 2013 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/cpu.h" +#include "base/time/time.h" +#include "media/base/sinc_resampler.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/perf/perf_test.h" + +namespace media { + +static const int kBenchmarkIterations = 50000000; + +static const double kSampleRateRatio = 192000.0 / 44100.0; +static const double kKernelInterpolationFactor = 0.5; + +// Helper function to provide no input to SincResampler's Convolve benchmark. +static void DoNothing(int frames, float* destination) {} + +// Define platform independent function name for Convolve* tests. +#if defined(ARCH_CPU_X86_FAMILY) +#define CONVOLVE_FUNC Convolve_SSE +#elif defined(ARCH_CPU_ARM_FAMILY) && defined(USE_NEON) +#define CONVOLVE_FUNC Convolve_NEON +#endif + +static void RunConvolveBenchmark( + SincResampler* resampler, + float (*convolve_fn)(const float*, const float*, const float*, double), + bool aligned, + const std::string& trace_name) { + base::TimeTicks start = base::TimeTicks::HighResNow(); + for (int i = 0; i < kBenchmarkIterations; ++i) { + convolve_fn(resampler->get_kernel_for_testing() + (aligned ? 0 : 1), + resampler->get_kernel_for_testing(), + resampler->get_kernel_for_testing(), + kKernelInterpolationFactor); + } + double total_time_seconds = + (base::TimeTicks::HighResNow() - start).InSecondsF(); + perf_test::PrintResult("sinc_resampler_convolve", + "", + trace_name, + kBenchmarkIterations / total_time_seconds, + "runs/s", + true); +} + +// Benchmark for the various Convolve() methods. Make sure to build with +// branding=Chrome so that DCHECKs are compiled out when benchmarking. +TEST(SincResamplerPerfTest, Convolve) { + SincResampler resampler(kSampleRateRatio, + SincResampler::kDefaultRequestSize, + base::Bind(&DoNothing)); + + RunConvolveBenchmark( + &resampler, SincResampler::Convolve_C, true, "unoptimized_aligned"); + +#if defined(CONVOLVE_FUNC) +#if defined(ARCH_CPU_X86_FAMILY) + ASSERT_TRUE(base::CPU().has_sse()); +#endif + RunConvolveBenchmark( + &resampler, SincResampler::CONVOLVE_FUNC, true, "optimized_aligned"); + RunConvolveBenchmark( + &resampler, SincResampler::CONVOLVE_FUNC, false, "optimized_unaligned"); +#endif +} + +#undef CONVOLVE_FUNC + +} // namespace media diff --git a/media/base/sinc_resampler_unittest.cc b/media/base/sinc_resampler_unittest.cc index 8b89a5d380..3b460a39c3 100644 --- a/media/base/sinc_resampler_unittest.cc +++ b/media/base/sinc_resampler_unittest.cc @@ -9,11 +9,8 @@ #include "base/bind.h" #include "base/bind_helpers.h" -#include "base/command_line.h" #include "base/cpu.h" -#include "base/logging.h" #include "base/strings/string_number_conversions.h" -#include "base/strings/stringize_macros.h" #include "base/time/time.h" #include "build/build_config.h" #include "media/base/sinc_resampler.h" @@ -25,10 +22,6 @@ using testing::_; namespace media { static const double kSampleRateRatio = 192000.0 / 44100.0; -static const double kKernelInterpolationFactor = 0.5; - -// Command line switch for runtime adjustment of ConvolveBenchmark iterations. -static const char kConvolveIterations[] = "convolve-iterations"; // Helper class to ensure ChunkedResample() functions properly. class MockSource { @@ -125,6 +118,8 @@ TEST(SincResamplerTest, DISABLED_SetRatioBench) { // this test if other optimized methods exist, otherwise the default Convolve() // will be tested by the parameterized SincResampler tests below. #if defined(CONVOLVE_FUNC) +static const double kKernelInterpolationFactor = 0.5; + TEST(SincResamplerTest, Convolve) { #if defined(ARCH_CPU_X86_FAMILY) ASSERT_TRUE(base::CPU().has_sse()); @@ -161,74 +156,6 @@ TEST(SincResamplerTest, Convolve) { } #endif -// Benchmark for the various Convolve() methods. Make sure to build with -// branding=Chrome so that DCHECKs are compiled out when benchmarking. Original -// benchmarks were run with --convolve-iterations=50000000. -TEST(SincResamplerTest, ConvolveBenchmark) { - // Initialize a dummy resampler. - MockSource mock_source; - SincResampler resampler( - kSampleRateRatio, SincResampler::kDefaultRequestSize, - base::Bind(&MockSource::ProvideInput, base::Unretained(&mock_source))); - - // Retrieve benchmark iterations from command line. - int convolve_iterations = 10; - std::string iterations(CommandLine::ForCurrentProcess()->GetSwitchValueASCII( - kConvolveIterations)); - if (!iterations.empty()) - base::StringToInt(iterations, &convolve_iterations); - - printf("Benchmarking %d iterations:\n", convolve_iterations); - - // Benchmark Convolve_C(). - base::TimeTicks start = base::TimeTicks::HighResNow(); - for (int i = 0; i < convolve_iterations; ++i) { - resampler.Convolve_C( - resampler.kernel_storage_.get(), resampler.kernel_storage_.get(), - resampler.kernel_storage_.get(), kKernelInterpolationFactor); - } - double total_time_c_ms = - (base::TimeTicks::HighResNow() - start).InMillisecondsF(); - printf("Convolve_C took %.2fms.\n", total_time_c_ms); - -#if defined(CONVOLVE_FUNC) -#if defined(ARCH_CPU_X86_FAMILY) - ASSERT_TRUE(base::CPU().has_sse()); -#endif - - // Benchmark with unaligned input pointer. - start = base::TimeTicks::HighResNow(); - for (int j = 0; j < convolve_iterations; ++j) { - resampler.CONVOLVE_FUNC( - resampler.kernel_storage_.get() + 1, resampler.kernel_storage_.get(), - resampler.kernel_storage_.get(), kKernelInterpolationFactor); - } - double total_time_optimized_unaligned_ms = - (base::TimeTicks::HighResNow() - start).InMillisecondsF(); - printf(STRINGIZE(CONVOLVE_FUNC) " (unaligned) took %.2fms; which is %.2fx " - "faster than Convolve_C.\n", total_time_optimized_unaligned_ms, - total_time_c_ms / total_time_optimized_unaligned_ms); - - // Benchmark with aligned input pointer. - start = base::TimeTicks::HighResNow(); - for (int j = 0; j < convolve_iterations; ++j) { - resampler.CONVOLVE_FUNC( - resampler.kernel_storage_.get(), resampler.kernel_storage_.get(), - resampler.kernel_storage_.get(), kKernelInterpolationFactor); - } - double total_time_optimized_aligned_ms = - (base::TimeTicks::HighResNow() - start).InMillisecondsF(); - printf(STRINGIZE(CONVOLVE_FUNC) " (aligned) took %.2fms; which is %.2fx " - "faster than Convolve_C and %.2fx faster than " - STRINGIZE(CONVOLVE_FUNC) " (unaligned).\n", - total_time_optimized_aligned_ms, - total_time_c_ms / total_time_optimized_aligned_ms, - total_time_optimized_unaligned_ms / total_time_optimized_aligned_ms); -#endif -} - -#undef CONVOLVE_FUNC - // Fake audio source for testing the resampler. Generates a sinusoidal linear // chirp (http://en.wikipedia.org/wiki/Chirp) which can be tuned to stress the // resampler for the specific sample rate conversion being used. diff --git a/media/base/vector_math_perftest.cc b/media/base/vector_math_perftest.cc new file mode 100644 index 0000000000..78699c3d70 --- /dev/null +++ b/media/base/vector_math_perftest.cc @@ -0,0 +1,124 @@ +// Copyright 2013 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/cpu.h" +#include "base/memory/aligned_memory.h" +#include "base/memory/scoped_ptr.h" +#include "base/time/time.h" +#include "media/base/vector_math.h" +#include "media/base/vector_math_testing.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/perf/perf_test.h" + +using base::TimeTicks; +using std::fill; + +namespace media { + +static const int kBenchmarkIterations = 200000; +static const float kScale = 0.5; +static const int kVectorSize = 8192; + +class VectorMathPerfTest : public testing::Test { + public: + VectorMathPerfTest() { + // Initialize input and output vectors. + input_vector_.reset(static_cast<float*>(base::AlignedAlloc( + sizeof(float) * kVectorSize, vector_math::kRequiredAlignment))); + output_vector_.reset(static_cast<float*>(base::AlignedAlloc( + sizeof(float) * kVectorSize, vector_math::kRequiredAlignment))); + fill(input_vector_.get(), input_vector_.get() + kVectorSize, 1.0f); + fill(output_vector_.get(), output_vector_.get() + kVectorSize, 0.0f); + } + + void RunBenchmark(void (*fn)(const float[], float, int, float[]), + bool aligned, + const std::string& test_name, + const std::string& trace_name) { + TimeTicks start = TimeTicks::HighResNow(); + for (int i = 0; i < kBenchmarkIterations; ++i) { + fn(input_vector_.get(), + kScale, + kVectorSize - (aligned ? 0 : 1), + output_vector_.get()); + } + double total_time_seconds = (TimeTicks::HighResNow() - start).InSecondsF(); + perf_test::PrintResult(test_name, + "", + trace_name, + kBenchmarkIterations / total_time_seconds, + "runs/s", + true); + } + + protected: + scoped_ptr_malloc<float, base::ScopedPtrAlignedFree> input_vector_; + scoped_ptr_malloc<float, base::ScopedPtrAlignedFree> output_vector_; + + DISALLOW_COPY_AND_ASSIGN(VectorMathPerfTest); +}; + +// Define platform independent function name for FMAC* perf tests. +#if defined(ARCH_CPU_X86_FAMILY) +#define FMAC_FUNC FMAC_SSE +#elif defined(ARCH_CPU_ARM_FAMILY) && defined(USE_NEON) +#define FMAC_FUNC FMAC_NEON +#endif + +// Benchmark for each optimized vector_math::FMAC() method. +TEST_F(VectorMathPerfTest, FMAC) { + // Benchmark FMAC_C(). + RunBenchmark( + vector_math::FMAC_C, true, "vector_math_fmac", "unoptimized"); +#if defined(FMAC_FUNC) +#if defined(ARCH_CPU_X86_FAMILY) + ASSERT_TRUE(base::CPU().has_sse()); +#endif + // Benchmark FMAC_FUNC() with unaligned size. + ASSERT_NE((kVectorSize - 1) % (vector_math::kRequiredAlignment / + sizeof(float)), 0U); + RunBenchmark( + vector_math::FMAC_FUNC, false, "vector_math_fmac", "optimized_unaligned"); + // Benchmark FMAC_FUNC() with aligned size. + ASSERT_EQ(kVectorSize % (vector_math::kRequiredAlignment / sizeof(float)), + 0U); + RunBenchmark( + vector_math::FMAC_FUNC, true, "vector_math_fmac", "optimized_aligned"); +#endif +} + +#undef FMAC_FUNC + +// Define platform independent function name for FMULBenchmark* tests. +#if defined(ARCH_CPU_X86_FAMILY) +#define FMUL_FUNC FMUL_SSE +#elif defined(ARCH_CPU_ARM_FAMILY) && defined(USE_NEON) +#define FMUL_FUNC FMUL_NEON +#endif + +// Benchmark for each optimized vector_math::FMUL() method. +TEST_F(VectorMathPerfTest, FMUL) { + // Benchmark FMUL_C(). + RunBenchmark( + vector_math::FMUL_C, true, "vector_math_fmul", "unoptimized"); +#if defined(FMUL_FUNC) +#if defined(ARCH_CPU_X86_FAMILY) + ASSERT_TRUE(base::CPU().has_sse()); +#endif + // Benchmark FMUL_FUNC() with unaligned size. + ASSERT_NE((kVectorSize - 1) % (vector_math::kRequiredAlignment / + sizeof(float)), 0U); + RunBenchmark( + vector_math::FMUL_FUNC, false, "vector_math_fmac", "optimized_unaligned"); + // Benchmark FMUL_FUNC() with aligned size. + ASSERT_EQ(kVectorSize % (vector_math::kRequiredAlignment / sizeof(float)), + 0U); + RunBenchmark( + vector_math::FMUL_FUNC, true, "vector_math_fmac", "optimized_aligned"); +#endif +} + +#undef FMUL_FUNC + +} // namespace media diff --git a/media/base/vector_math_unittest.cc b/media/base/vector_math_unittest.cc index 2c7740142c..32e5ea468c 100644 --- a/media/base/vector_math_unittest.cc +++ b/media/base/vector_math_unittest.cc @@ -6,68 +6,50 @@ #define _USE_MATH_DEFINES #include <cmath> -#include "base/command_line.h" #include "base/cpu.h" #include "base/memory/aligned_memory.h" #include "base/memory/scoped_ptr.h" #include "base/strings/string_number_conversions.h" #include "base/strings/stringize_macros.h" -#include "base/time/time.h" #include "media/base/vector_math.h" #include "media/base/vector_math_testing.h" #include "testing/gtest/include/gtest/gtest.h" -using base::TimeTicks; using std::fill; -// Command line switch for runtime adjustment of benchmark iterations. -static const char kBenchmarkIterations[] = "vector-math-iterations"; -static const int kDefaultIterations = 10; +namespace media { // Default test values. static const float kScale = 0.5; static const float kInputFillValue = 1.0; static const float kOutputFillValue = 3.0; - -namespace media { +static const int kVectorSize = 8192; class VectorMathTest : public testing::Test { public: - static const int kVectorSize = 8192; VectorMathTest() { // Initialize input and output vectors. - input_vector.reset(static_cast<float*>(base::AlignedAlloc( + input_vector_.reset(static_cast<float*>(base::AlignedAlloc( sizeof(float) * kVectorSize, vector_math::kRequiredAlignment))); - output_vector.reset(static_cast<float*>(base::AlignedAlloc( + output_vector_.reset(static_cast<float*>(base::AlignedAlloc( sizeof(float) * kVectorSize, vector_math::kRequiredAlignment))); } void FillTestVectors(float input, float output) { // Setup input and output vectors. - fill(input_vector.get(), input_vector.get() + kVectorSize, input); - fill(output_vector.get(), output_vector.get() + kVectorSize, output); + fill(input_vector_.get(), input_vector_.get() + kVectorSize, input); + fill(output_vector_.get(), output_vector_.get() + kVectorSize, output); } void VerifyOutput(float value) { for (int i = 0; i < kVectorSize; ++i) - ASSERT_FLOAT_EQ(output_vector.get()[i], value); - } - - int BenchmarkIterations() { - int vector_math_iterations = kDefaultIterations; - std::string iterations( - CommandLine::ForCurrentProcess()->GetSwitchValueASCII( - kBenchmarkIterations)); - if (!iterations.empty()) - base::StringToInt(iterations, &vector_math_iterations); - return vector_math_iterations; + ASSERT_FLOAT_EQ(output_vector_.get()[i], value); } protected: - int benchmark_iterations; - scoped_ptr_malloc<float, base::ScopedPtrAlignedFree> input_vector; - scoped_ptr_malloc<float, base::ScopedPtrAlignedFree> output_vector; + scoped_ptr_malloc<float, base::ScopedPtrAlignedFree> input_vector_; + scoped_ptr_malloc<float, base::ScopedPtrAlignedFree> output_vector_; DISALLOW_COPY_AND_ASSIGN(VectorMathTest); }; @@ -80,7 +62,7 @@ TEST_F(VectorMathTest, FMAC) { SCOPED_TRACE("FMAC"); FillTestVectors(kInputFillValue, kOutputFillValue); vector_math::FMAC( - input_vector.get(), kScale, kVectorSize, output_vector.get()); + input_vector_.get(), kScale, kVectorSize, output_vector_.get()); VerifyOutput(kResult); } @@ -88,7 +70,7 @@ TEST_F(VectorMathTest, FMAC) { SCOPED_TRACE("FMAC_C"); FillTestVectors(kInputFillValue, kOutputFillValue); vector_math::FMAC_C( - input_vector.get(), kScale, kVectorSize, output_vector.get()); + input_vector_.get(), kScale, kVectorSize, output_vector_.get()); VerifyOutput(kResult); } @@ -98,7 +80,7 @@ TEST_F(VectorMathTest, FMAC) { SCOPED_TRACE("FMAC_SSE"); FillTestVectors(kInputFillValue, kOutputFillValue); vector_math::FMAC_SSE( - input_vector.get(), kScale, kVectorSize, output_vector.get()); + input_vector_.get(), kScale, kVectorSize, output_vector_.get()); VerifyOutput(kResult); } #endif @@ -108,7 +90,7 @@ TEST_F(VectorMathTest, FMAC) { SCOPED_TRACE("FMAC_NEON"); FillTestVectors(kInputFillValue, kOutputFillValue); vector_math::FMAC_NEON( - input_vector.get(), kScale, kVectorSize, output_vector.get()); + input_vector_.get(), kScale, kVectorSize, output_vector_.get()); VerifyOutput(kResult); } #endif @@ -122,7 +104,7 @@ TEST_F(VectorMathTest, FMUL) { SCOPED_TRACE("FMUL"); FillTestVectors(kInputFillValue, kOutputFillValue); vector_math::FMUL( - input_vector.get(), kScale, kVectorSize, output_vector.get()); + input_vector_.get(), kScale, kVectorSize, output_vector_.get()); VerifyOutput(kResult); } @@ -130,7 +112,7 @@ TEST_F(VectorMathTest, FMUL) { SCOPED_TRACE("FMUL_C"); FillTestVectors(kInputFillValue, kOutputFillValue); vector_math::FMUL_C( - input_vector.get(), kScale, kVectorSize, output_vector.get()); + input_vector_.get(), kScale, kVectorSize, output_vector_.get()); VerifyOutput(kResult); } @@ -140,7 +122,7 @@ TEST_F(VectorMathTest, FMUL) { SCOPED_TRACE("FMUL_SSE"); FillTestVectors(kInputFillValue, kOutputFillValue); vector_math::FMUL_SSE( - input_vector.get(), kScale, kVectorSize, output_vector.get()); + input_vector_.get(), kScale, kVectorSize, output_vector_.get()); VerifyOutput(kResult); } #endif @@ -150,142 +132,10 @@ TEST_F(VectorMathTest, FMUL) { SCOPED_TRACE("FMUL_NEON"); FillTestVectors(kInputFillValue, kOutputFillValue); vector_math::FMUL_NEON( - input_vector.get(), kScale, kVectorSize, output_vector.get()); + input_vector_.get(), kScale, kVectorSize, output_vector_.get()); VerifyOutput(kResult); } #endif } -// Define platform independent function name for FMACBenchmark* tests. -#if defined(ARCH_CPU_X86_FAMILY) -#define FMAC_FUNC FMAC_SSE -#elif defined(ARCH_CPU_ARM_FAMILY) && defined(USE_NEON) -#define FMAC_FUNC FMAC_NEON -#endif - -// Benchmark for each optimized vector_math::FMAC() method. Original benchmarks -// were run with --vector-fmac-iterations=200000. -TEST_F(VectorMathTest, FMACBenchmark) { - static const int kBenchmarkIterations = BenchmarkIterations(); - - printf("Benchmarking %d iterations:\n", kBenchmarkIterations); - - // Benchmark FMAC_C(). - FillTestVectors(kInputFillValue, kOutputFillValue); - TimeTicks start = TimeTicks::HighResNow(); - for (int i = 0; i < kBenchmarkIterations; ++i) { - vector_math::FMAC_C( - input_vector.get(), kScale, kVectorSize, output_vector.get()); - } - double total_time_c_ms = (TimeTicks::HighResNow() - start).InMillisecondsF(); - printf("FMAC_C took %.2fms.\n", total_time_c_ms); - -#if defined(FMAC_FUNC) -#if defined(ARCH_CPU_X86_FAMILY) - ASSERT_TRUE(base::CPU().has_sse()); -#endif - - // Benchmark FMAC_FUNC() with unaligned size. - ASSERT_NE((kVectorSize - 1) % (vector_math::kRequiredAlignment / - sizeof(float)), 0U); - FillTestVectors(kInputFillValue, kOutputFillValue); - start = TimeTicks::HighResNow(); - for (int j = 0; j < kBenchmarkIterations; ++j) { - vector_math::FMAC_FUNC( - input_vector.get(), kScale, kVectorSize - 1, output_vector.get()); - } - double total_time_optimized_unaligned_ms = - (TimeTicks::HighResNow() - start).InMillisecondsF(); - printf(STRINGIZE(FMAC_FUNC) " (unaligned size) took %.2fms; which is %.2fx " - "faster than FMAC_C.\n", total_time_optimized_unaligned_ms, - total_time_c_ms / total_time_optimized_unaligned_ms); - - // Benchmark FMAC_FUNC() with aligned size. - ASSERT_EQ(kVectorSize % (vector_math::kRequiredAlignment / sizeof(float)), - 0U); - FillTestVectors(kInputFillValue, kOutputFillValue); - start = TimeTicks::HighResNow(); - for (int j = 0; j < kBenchmarkIterations; ++j) { - vector_math::FMAC_FUNC( - input_vector.get(), kScale, kVectorSize, output_vector.get()); - } - double total_time_optimized_aligned_ms = - (TimeTicks::HighResNow() - start).InMillisecondsF(); - printf(STRINGIZE(FMAC_FUNC) " (aligned) took %.2fms; which is %.2fx " - "faster than FMAC_C and %.2fx faster than " - STRINGIZE(FMAC_FUNC) " (unaligned).\n", - total_time_optimized_aligned_ms, - total_time_c_ms / total_time_optimized_aligned_ms, - total_time_optimized_unaligned_ms / total_time_optimized_aligned_ms); -#endif -} - -#undef FMAC_FUNC - -// Define platform independent function name for FMULBenchmark* tests. -#if defined(ARCH_CPU_X86_FAMILY) -#define FMUL_FUNC FMUL_SSE -#elif defined(ARCH_CPU_ARM_FAMILY) && defined(USE_NEON) -#define FMUL_FUNC FMUL_NEON -#endif - -// Benchmark for each optimized vector_math::FMUL() method. Original benchmarks -// were run with --vector-math-iterations=200000. -TEST_F(VectorMathTest, FMULBenchmark) { - static const int kBenchmarkIterations = BenchmarkIterations(); - - printf("Benchmarking %d iterations:\n", kBenchmarkIterations); - - // Benchmark FMUL_C(). - FillTestVectors(kInputFillValue, kOutputFillValue); - TimeTicks start = TimeTicks::HighResNow(); - for (int i = 0; i < kBenchmarkIterations; ++i) { - vector_math::FMUL_C( - input_vector.get(), kScale, kVectorSize, output_vector.get()); - } - double total_time_c_ms = (TimeTicks::HighResNow() - start).InMillisecondsF(); - printf("FMUL_C took %.2fms.\n", total_time_c_ms); - -#if defined(FMUL_FUNC) -#if defined(ARCH_CPU_X86_FAMILY) - ASSERT_TRUE(base::CPU().has_sse()); -#endif - - // Benchmark FMUL_SSE() with unaligned size. - ASSERT_NE((kVectorSize - 1) % (vector_math::kRequiredAlignment / - sizeof(float)), 0U); - FillTestVectors(kInputFillValue, kOutputFillValue); - start = TimeTicks::HighResNow(); - for (int j = 0; j < kBenchmarkIterations; ++j) { - vector_math::FMUL_FUNC( - input_vector.get(), kScale, kVectorSize - 1, output_vector.get()); - } - double total_time_optimized_unaligned_ms = - (TimeTicks::HighResNow() - start).InMillisecondsF(); - printf(STRINGIZE(FMUL_FUNC) " (unaligned size) took %.2fms; which is %.2fx " - "faster than FMUL_C.\n", total_time_optimized_unaligned_ms, - total_time_c_ms / total_time_optimized_unaligned_ms); - - // Benchmark FMUL_SSE() with aligned size. - ASSERT_EQ(kVectorSize % (vector_math::kRequiredAlignment / sizeof(float)), - 0U); - FillTestVectors(kInputFillValue, kOutputFillValue); - start = TimeTicks::HighResNow(); - for (int j = 0; j < kBenchmarkIterations; ++j) { - vector_math::FMUL_FUNC( - input_vector.get(), kScale, kVectorSize, output_vector.get()); - } - double total_time_optimized_aligned_ms = - (TimeTicks::HighResNow() - start).InMillisecondsF(); - printf(STRINGIZE(FMUL_FUNC) " (aligned) took %.2fms; which is %.2fx " - "faster than FMUL_C and %.2fx faster than " - STRINGIZE(FMUL_FUNC) " (unaligned).\n", - total_time_optimized_aligned_ms, - total_time_c_ms / total_time_optimized_aligned_ms, - total_time_optimized_unaligned_ms / total_time_optimized_aligned_ms); -#endif -} - -#undef FMUL_FUNC - } // namespace media |