diff options
author | ajwong@chromium.org <ajwong@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-12-10 00:05:39 +0000 |
---|---|---|
committer | ajwong@chromium.org <ajwong@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-12-10 00:05:39 +0000 |
commit | e7a557c64a933835da445cda865a8f81bd92b8b0 (patch) | |
tree | e5fdf02f91d06f17aa34e32c6a94c82703e4c374 /media | |
parent | e3149dbeafdcb286e24aa676b125eb9b0134ab11 (diff) | |
download | chromium_src-e7a557c64a933835da445cda865a8f81bd92b8b0.zip chromium_src-e7a557c64a933835da445cda865a8f81bd92b8b0.tar.gz chromium_src-e7a557c64a933835da445cda865a8f81bd92b8b0.tar.bz2 |
Refactor FFmpegVideoDecoder to try and generalize code common to all video decoders.
This changes the DecoderBase API to be fully asynchronous when invoking its subclass's actions.
Review URL: http://codereview.chromium.org/465044
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@34208 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'media')
-rw-r--r-- | media/base/callback.h | 62 | ||||
-rw-r--r-- | media/base/mock_task.h | 110 | ||||
-rw-r--r-- | media/ffmpeg/ffmpeg_util.cc | 18 | ||||
-rw-r--r-- | media/ffmpeg/ffmpeg_util.h | 18 | ||||
-rw-r--r-- | media/filters/decoder_base.h | 80 | ||||
-rw-r--r-- | media/filters/ffmpeg_audio_decoder.cc | 31 | ||||
-rw-r--r-- | media/filters/ffmpeg_audio_decoder.h | 9 | ||||
-rw-r--r-- | media/filters/ffmpeg_demuxer.cc | 18 | ||||
-rw-r--r-- | media/filters/ffmpeg_demuxer.h | 4 | ||||
-rw-r--r-- | media/filters/ffmpeg_video_decode_engine_unittest.cc | 184 | ||||
-rw-r--r-- | media/filters/ffmpeg_video_decoder.cc | 301 | ||||
-rw-r--r-- | media/filters/ffmpeg_video_decoder.h | 78 | ||||
-rw-r--r-- | media/filters/ffmpeg_video_decoder_unittest.cc | 297 | ||||
-rw-r--r-- | media/filters/video_decode_engine.h | 57 | ||||
-rw-r--r-- | media/media.gyp | 6 |
15 files changed, 913 insertions, 360 deletions
diff --git a/media/base/callback.h b/media/base/callback.h new file mode 100644 index 0000000..553c842 --- /dev/null +++ b/media/base/callback.h @@ -0,0 +1,62 @@ +// Copyright (c) 2009 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. + +// Some basic utilities for aiding in the management of Tasks and Callbacks. +// AutoTaskRunner, and its brother AutoCallbackRunner are the scoped_ptr +// equivalents for callbacks. They are useful for ensuring a callback is +// executed and delete in the face of multiple return points in a function. + +#ifndef MEDIA_BASE_CALLBACK_ +#define MEDIA_BASE_CALLBACK_ + +#include "base/scoped_ptr.h" +#include "base/task.h" + +namespace media { + +class AutoTaskRunner { + public: + // Takes ownership of the task. + explicit AutoTaskRunner(Task* task) + : task_(task) { + } + + ~AutoTaskRunner() { + if (task_.get()) { + task_->Run(); + } + } + + Task* release() { return task_.release(); } + + private: + scoped_ptr<Task> task_; + + DISALLOW_COPY_AND_ASSIGN(AutoTaskRunner); +}; + +class AutoCallbackRunner { + public: + // Takes ownership of the callback. + explicit AutoCallbackRunner(Callback0::Type* callback) + : callback_(callback) { + } + + ~AutoCallbackRunner() { + if (callback_.get()) { + callback_->Run(); + } + } + + Callback0::Type* release() { return callback_.release(); } + + private: + scoped_ptr<Callback0::Type> callback_; + + DISALLOW_COPY_AND_ASSIGN(AutoCallbackRunner); +}; + +} // namespace media + +#endif // MEDIA_BASE_CALLBACK_ diff --git a/media/base/mock_task.h b/media/base/mock_task.h new file mode 100644 index 0000000..cb6e0de --- /dev/null +++ b/media/base/mock_task.h @@ -0,0 +1,110 @@ +// Copyright (c) 2009 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. + +// This file provides some utility classes that help with testing APIs which use +// callbacks. +// +// -- InvokeRunnable -- +// The InvokeRunnable is an action that can be used a gMock mock object to +// invoke the Run() method on mock argument. Example: +// +// class MockFoo : public Foo { +// public: +// MOCK_METHOD0(DoSomething, void(Task* done_cb)); +// }; +// +// EXPECT_CALL(foo, DoSomething(_)).WillOnce(WithArg<0>(InvokeRunnable())); +// +// Then you pass "foo" to something that will eventually call DoSomething(). +// The mock action will ensure that passed in done_cb is invoked. +// +// +// -- TaskMocker -- +// The TaskMocker class lets you create mock callbacks. Callbacks are +// difficult to mock because ownership of the callback object is often passed +// to the funciton being invoked. TaskMocker solves this by providing a +// GetTask() function that creates a new, single-use task that delegates to +// the originating TaskMocker object. Expectations are placed on the +// originating TaskMocker object. Each callback retrieved by GetTask() is +// tracked to ensure that it is properly deleted. The TaskMocker expects to +// outlive all the callbacks retrieved by GetTask(). +// +// Example: +// +// TaskMocker done_cb; +// EXPECT_CALL(done_cb, Run()).Times(3); +// +// func1(done_cb.GetTask()); +// func2(done_cb.GetTask()); +// func3(done_cb.GetTask()); +// +// // All 3 callbacks from GetTask() should be deleted before done_cb goes out +// // of scope. +// +// This class is not threadsafe. +// +// TODO(ajwong): Is it even worth bothering with gmock here? +// TODO(ajwong): Move MockFilterCallback here and merge the implementation +// differences. + +#ifndef MEDIA_BASE_MOCK_TASK_H_ +#define MEDIA_BASE_MOCK_TASK_H_ + +#include "base/task.h" +#include "testing/gmock/include/gmock/gmock.h" + +namespace media { + +ACTION(InvokeRunnable) { + arg0->Run(); + delete arg0; +} + +class TaskMocker { + public: + TaskMocker() + : outstanding_tasks_(0) { + } + ~TaskMocker() { + CHECK(outstanding_tasks_ == 0) + << "If outstanding_tasks_ is not zero, tasks have been leaked."; + } + + Task* CreateTask() { + return new CountingTask(this); + } + + MOCK_METHOD0(Run, void()); + + private: + friend class CountingTask; + class CountingTask : public Task { + public: + CountingTask(TaskMocker* origin) + : origin_(origin) { + origin_->outstanding_tasks_++; + } + + virtual void Run() { + origin_->Run(); + } + + virtual ~CountingTask() { + origin_->outstanding_tasks_--; + } + + private: + TaskMocker* origin_; + + DISALLOW_COPY_AND_ASSIGN(CountingTask); + }; + + int outstanding_tasks_; + + DISALLOW_COPY_AND_ASSIGN(TaskMocker); +}; + +} // namespace media + +#endif //MEDIA_BASE_MOCK_TASK_H_ diff --git a/media/ffmpeg/ffmpeg_util.cc b/media/ffmpeg/ffmpeg_util.cc new file mode 100644 index 0000000..1be3b12 --- /dev/null +++ b/media/ffmpeg/ffmpeg_util.cc @@ -0,0 +1,18 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. Use of this +// source code is governed by a BSD-style license that can be found in the +// LICENSE file. + +#include "media/ffmpeg/ffmpeg_util.h" + +#include "media/filters/ffmpeg_common.h" + +namespace media { + +static const AVRational kMicrosBase = { 1, base::Time::kMicrosecondsPerSecond }; + +base::TimeDelta ConvertTimestamp(const AVRational& time_base, int64 timestamp) { + int64 microseconds = av_rescale_q(timestamp, time_base, kMicrosBase); + return base::TimeDelta::FromMicroseconds(microseconds); +} + +} // namespace media diff --git a/media/ffmpeg/ffmpeg_util.h b/media/ffmpeg/ffmpeg_util.h new file mode 100644 index 0000000..6799cc4 --- /dev/null +++ b/media/ffmpeg/ffmpeg_util.h @@ -0,0 +1,18 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. Use of this +// source code is governed by a BSD-style license that can be found in the +// LICENSE file. + +#ifndef MEDIA_FFMPEG_FFMPEG_UTIL_H_ +#define MEDIA_FFMPEG_FFMPEG_UTIL_H_ + +#include "base/time.h" + +struct AVRational; + +namespace media { + +base::TimeDelta ConvertTimestamp(const AVRational& time_base, int64 timestamp); + +} // namespace media + +#endif // MEDIA_FFMPEG_FFMPEG_UTIL_H_ diff --git a/media/filters/decoder_base.h b/media/filters/decoder_base.h index a228958..cc403bc 100644 --- a/media/filters/decoder_base.h +++ b/media/filters/decoder_base.h @@ -14,6 +14,7 @@ #include "base/task.h" #include "base/thread.h" #include "media/base/buffers.h" +#include "media/base/callback.h" #include "media/base/filters.h" #include "media/base/filter_host.h" @@ -55,17 +56,6 @@ class DecoderBase : public Decoder { NewRunnableMethod(this, &DecoderBase::ReadTask, read_callback)); } - void OnReadComplete(Buffer* buffer) { - // Little bit of magic here to get NewRunnableMethod() to generate a Task - // that holds onto a reference via scoped_refptr<>. - // - // TODO(scherkus): change the callback format to pass a scoped_refptr<> or - // better yet see if we can get away with not using reference counting. - scoped_refptr<Buffer> buffer_ref = buffer; - this->message_loop()->PostTask(FROM_HERE, - NewRunnableMethod(this, &DecoderBase::ReadCompleteTask, buffer_ref)); - } - protected: DecoderBase() : pending_reads_(0), @@ -89,6 +79,12 @@ class DecoderBase : public Decoder { } } + // TODO(ajwong): All these "Task*" used as completion callbacks should be + // FilterCallbacks. However, since NewCallback() cannot prebind parameters, + // we use NewRunnableMethod() instead which causes an unfortunate refcount. + // We should move stoyan's Mutant code into base/task.h and turn these + // back into FilterCallbacks. + // Method that must be implemented by the derived class. Called from within // the DecoderBase::Initialize() method before any reads are submitted to // the demuxer stream. Returns true if successful, otherwise false indicates @@ -96,32 +92,46 @@ class DecoderBase : public Decoder { // InitializationComplete() method. If this method returns true, then the // base class will call the host to complete initialization. During this // call, the derived class must fill in the media_format_ member. - virtual bool OnInitialize(DemuxerStream* demuxer_stream) = 0; + virtual void DoInitialize(DemuxerStream* demuxer_stream, bool* success, + Task* done_cb) = 0; // Method that may be implemented by the derived class if desired. It will // be called from within the MediaFilter::Stop() method prior to stopping the // base class. - virtual void OnStop() {} + // + // TODO(ajwong): Make this asynchronous. + virtual void DoStop() {} // Derived class can implement this method and perform seeking logic prior // to the base class. - virtual void OnSeek(base::TimeDelta time) {} + virtual void DoSeek(base::TimeDelta time, Task* done_cb) = 0; // Method that must be implemented by the derived class. If the decode // operation produces one or more outputs, the derived class should call // the EnequeueResult() method from within this method. - virtual void OnDecode(Buffer* input) = 0; + virtual void DoDecode(Buffer* input, Task* done_cb) = 0; MediaFormat media_format_; private: bool IsStopped() { return state_ == kStopped; } + void OnReadComplete(Buffer* buffer) { + // Little bit of magic here to get NewRunnableMethod() to generate a Task + // that holds onto a reference via scoped_refptr<>. + // + // TODO(scherkus): change the callback format to pass a scoped_refptr<> or + // better yet see if we can get away with not using reference counting. + scoped_refptr<Buffer> buffer_ref = buffer; + this->message_loop()->PostTask(FROM_HERE, + NewRunnableMethod(this, &DecoderBase::ReadCompleteTask, buffer_ref)); + } + void StopTask() { DCHECK_EQ(MessageLoop::current(), this->message_loop()); // Delegate to the subclass first. - OnStop(); + DoStop(); // Throw away all buffers in all queues. result_queue_.clear(); @@ -133,7 +143,6 @@ class DecoderBase : public Decoder { DCHECK_EQ(MessageLoop::current(), this->message_loop()); DCHECK_EQ(0u, pending_reads_) << "Pending reads should have completed"; DCHECK(read_queue_.empty()) << "Read requests should be empty"; - scoped_ptr<FilterCallback> c(callback); // Delegate to the subclass first. // @@ -142,8 +151,11 @@ class DecoderBase : public Decoder { // either flush their buffers here or wait for IsDiscontinuous(). I'm // inclined to say that they should still wait for IsDiscontinuous() so they // don't have duplicated logic for Seek() and actual discontinuous frames. - OnSeek(time); + DoSeek(time, + NewRunnableMethod(this, &DecoderBase::OnSeekComplete, callback)); + } + void OnSeekComplete(FilterCallback* callback) { // Flush our decoded results. We'll set a boolean that we can DCHECK to // verify our assertion that the first buffer received after a Seek() should // always be discontinuous. @@ -158,20 +170,30 @@ class DecoderBase : public Decoder { DCHECK_EQ(MessageLoop::current(), this->message_loop()); CHECK(kUninitialized == state_); CHECK(!demuxer_stream_); - scoped_ptr<FilterCallback> c(callback); demuxer_stream_ = demuxer_stream; + bool* success = new bool; + DoInitialize(demuxer_stream, + success, + NewRunnableMethod(this, &DecoderBase::OnInitializeComplete, + success, callback)); + } + + void OnInitializeComplete(bool* success, FilterCallback* done_cb) { + // Note: The done_runner must be declared *last* to ensure proper + // destruction order. + scoped_ptr<bool> success_deleter(success); + AutoCallbackRunner done_runner(done_cb); + + DCHECK_EQ(MessageLoop::current(), this->message_loop()); // Delegate to subclass first. - if (!OnInitialize(demuxer_stream_)) { + if (!*success) { this->host()->SetError(PIPELINE_ERROR_DECODE); - callback->Run(); - return; + } else { + // TODO(scherkus): subclass shouldn't mutate superclass media format. + DCHECK(!media_format_.empty()) << "Subclass did not set media_format_"; + state_ = kInitialized; } - - // TODO(scherkus): subclass shouldn't mutate superclass media format. - DCHECK(!media_format_.empty()) << "Subclass did not set media_format_"; - state_ = kInitialized; - callback->Run(); } void ReadTask(ReadCallback* read_callback) { @@ -210,8 +232,10 @@ class DecoderBase : public Decoder { } // Decode the frame right away. - OnDecode(buffer); + DoDecode(buffer, NewRunnableMethod(this, &DecoderBase::OnDecodeComplete)); + } + void OnDecodeComplete() { // Attempt to fulfill a pending read callback and schedule additional reads // if necessary. FulfillPendingRead(); diff --git a/media/filters/ffmpeg_audio_decoder.cc b/media/filters/ffmpeg_audio_decoder.cc index 8a7991a..884cde9 100644 --- a/media/filters/ffmpeg_audio_decoder.cc +++ b/media/filters/ffmpeg_audio_decoder.cc @@ -4,6 +4,7 @@ #include "media/filters/ffmpeg_audio_decoder.h" +#include "media/base/callback.h" #include "media/base/data_buffer.h" #include "media/base/limits.h" #include "media/filters/ffmpeg_common.h" @@ -29,11 +30,16 @@ bool FFmpegAudioDecoder::IsMediaFormatSupported(const MediaFormat& format) { mime_type::kFFmpegAudio == mime_type; } -bool FFmpegAudioDecoder::OnInitialize(DemuxerStream* demuxer_stream) { +void FFmpegAudioDecoder::DoInitialize(DemuxerStream* demuxer_stream, + bool* success, + Task* done_cb) { + AutoTaskRunner done_runner(done_cb); + *success = false; + // Get the AVStream by querying for the provider interface. AVStreamProvider* av_stream_provider; if (!demuxer_stream->QueryInterface(&av_stream_provider)) { - return false; + return; } AVStream* av_stream = av_stream_provider->GetAVStream(); @@ -48,15 +54,17 @@ bool FFmpegAudioDecoder::OnInitialize(DemuxerStream* demuxer_stream) { bps == 0 || static_cast<size_t>(bps) > Limits::kMaxBPS || codec_context_->sample_rate == 0 || - static_cast<size_t>(codec_context_->sample_rate) > Limits::kMaxSampleRate) - return false; + (static_cast<size_t>(codec_context_->sample_rate) > + Limits::kMaxSampleRate)) { + return; + } // Serialize calls to avcodec_open(). AVCodec* codec = avcodec_find_decoder(codec_context_->codec_id); { AutoLock auto_lock(FFmpegLock::get()->lock()); if (!codec || avcodec_open(codec_context_, codec) < 0) { - return false; + return; } } @@ -78,20 +86,21 @@ bool FFmpegAudioDecoder::OnInitialize(DemuxerStream* demuxer_stream) { output_buffer_.reset(static_cast<uint8*>(av_malloc(kOutputBufferSize))); if (!output_buffer_.get()) { host()->SetError(PIPELINE_ERROR_OUT_OF_MEMORY); - return false; + return; } - return true; + *success = true; } -void FFmpegAudioDecoder::OnSeek(base::TimeDelta time) { +void FFmpegAudioDecoder::DoSeek(base::TimeDelta time, Task* done_cb) { avcodec_flush_buffers(codec_context_); estimated_next_timestamp_ = StreamSample::kInvalidTimestamp; + done_cb->Run(); + delete done_cb; } -void FFmpegAudioDecoder::OnStop() { -} +void FFmpegAudioDecoder::DoDecode(Buffer* input, Task* done_cb) { + AutoTaskRunner done_runner(done_cb); -void FFmpegAudioDecoder::OnDecode(Buffer* input) { // Due to FFmpeg API changes we no longer have const read-only pointers. AVPacket packet; av_init_packet(&packet); diff --git a/media/filters/ffmpeg_audio_decoder.h b/media/filters/ffmpeg_audio_decoder.h index f7adc33..f5ff8ac 100644 --- a/media/filters/ffmpeg_audio_decoder.h +++ b/media/filters/ffmpeg_audio_decoder.h @@ -24,13 +24,12 @@ class FFmpegAudioDecoder : public DecoderBase<AudioDecoder, Buffer> { static bool IsMediaFormatSupported(const MediaFormat& media_format); protected: - virtual bool OnInitialize(DemuxerStream* demuxer_stream); + virtual void DoInitialize(DemuxerStream* demuxer_stream, bool* success, + Task* done_cb); - virtual void OnSeek(base::TimeDelta time); + virtual void DoSeek(base::TimeDelta time, Task* done_cb); - virtual void OnStop(); - - virtual void OnDecode(Buffer* input); + virtual void DoDecode(Buffer* input, Task* done_cb); private: friend class FilterFactoryImpl0<FFmpegAudioDecoder>; diff --git a/media/filters/ffmpeg_demuxer.cc b/media/filters/ffmpeg_demuxer.cc index dd08e40..3eab9f6 100644 --- a/media/filters/ffmpeg_demuxer.cc +++ b/media/filters/ffmpeg_demuxer.cc @@ -7,6 +7,7 @@ #include "base/string_util.h" #include "base/time.h" #include "media/base/filter_host.h" +#include "media/ffmpeg/ffmpeg_util.h" #include "media/filters/ffmpeg_common.h" #include "media/filters/ffmpeg_demuxer.h" #include "media/filters/ffmpeg_glue.h" @@ -76,7 +77,7 @@ FFmpegDemuxerStream::FFmpegDemuxerStream(FFmpegDemuxer* demuxer, } // Calculate the duration. - duration_ = ConvertTimestamp(stream->duration); + duration_ = ConvertStreamTimestamp(stream->time_base, stream->duration); } FFmpegDemuxerStream::~FFmpegDemuxerStream() { @@ -103,8 +104,10 @@ bool FFmpegDemuxerStream::HasPendingReads() { base::TimeDelta FFmpegDemuxerStream::EnqueuePacket(AVPacket* packet) { DCHECK_EQ(MessageLoop::current(), demuxer_->message_loop()); - base::TimeDelta timestamp = ConvertTimestamp(packet->pts); - base::TimeDelta duration = ConvertTimestamp(packet->duration); + base::TimeDelta timestamp = + ConvertStreamTimestamp(stream_->time_base, packet->pts); + base::TimeDelta duration = + ConvertStreamTimestamp(stream_->time_base, packet->duration); if (stopped_) { NOTREACHED() << "Attempted to enqueue packet on a stopped stream"; return timestamp; @@ -191,12 +194,13 @@ void FFmpegDemuxerStream::FulfillPendingRead() { read_callback->Run(buffer); } -base::TimeDelta FFmpegDemuxerStream::ConvertTimestamp(int64 timestamp) { +// static +base::TimeDelta FFmpegDemuxerStream::ConvertStreamTimestamp( + const AVRational& time_base, int64 timestamp) { if (timestamp == static_cast<int64>(AV_NOPTS_VALUE)) return StreamSample::kInvalidTimestamp; - AVRational time_base = { 1, base::Time::kMicrosecondsPerSecond }; - int64 microseconds = av_rescale_q(timestamp, stream_->time_base, time_base); - return base::TimeDelta::FromMicroseconds(microseconds); + + return ConvertTimestamp(time_base, timestamp); } // diff --git a/media/filters/ffmpeg_demuxer.h b/media/filters/ffmpeg_demuxer.h index f6af38c..f1b13a0 100644 --- a/media/filters/ffmpeg_demuxer.h +++ b/media/filters/ffmpeg_demuxer.h @@ -39,6 +39,7 @@ struct AVCodecContext; struct AVBitStreamFilterContext; struct AVFormatContext; struct AVPacket; +struct AVRational; struct AVStream; namespace media { @@ -93,7 +94,8 @@ class FFmpegDemuxerStream : public DemuxerStream, public AVStreamProvider { void FulfillPendingRead(); // Converts an FFmpeg stream timestamp into a base::TimeDelta. - base::TimeDelta ConvertTimestamp(int64 timestamp); + static base::TimeDelta ConvertStreamTimestamp(const AVRational& time_base, + int64 timestamp); FFmpegDemuxer* demuxer_; AVStream* stream_; diff --git a/media/filters/ffmpeg_video_decode_engine_unittest.cc b/media/filters/ffmpeg_video_decode_engine_unittest.cc new file mode 100644 index 0000000..76c43fd --- /dev/null +++ b/media/filters/ffmpeg_video_decode_engine_unittest.cc @@ -0,0 +1,184 @@ +// Copyright (c) 2009 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/scoped_ptr.h" +#include "media/base/data_buffer.h" +#include "media/base/mock_ffmpeg.h" +#include "media/base/mock_task.h" +#include "media/filters/ffmpeg_video_decoder.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using ::testing::_; +using ::testing::DoAll; +using ::testing::Return; +using ::testing::ReturnNull; +using ::testing::SetArgumentPointee; +using ::testing::StrictMock; + +namespace media { + +class FFmpegVideoDecodeEngineTest : public testing::Test { + protected: + FFmpegVideoDecodeEngineTest() { + // Setup FFmpeg structures. + memset(&yuv_frame_, 0, sizeof(yuv_frame_)); + memset(&codec_context_, 0, sizeof(codec_context_)); + memset(&codec_, 0, sizeof(codec_)); + memset(&stream_, 0, sizeof(stream_)); + stream_.codec = &codec_context_; + + buffer_ = new DataBuffer(1); + + // Initialize MockFFmpeg. + MockFFmpeg::set(&mock_ffmpeg_); + + test_engine_.reset(new FFmpegVideoDecodeEngine()); + test_engine_->SetCodecContextForTest(&codec_context_); + } + + scoped_ptr<FFmpegVideoDecodeEngine> test_engine_; + + StrictMock<MockFFmpeg> mock_ffmpeg_; + + AVFrame yuv_frame_; + AVCodecContext codec_context_; + AVStream stream_; + AVCodec codec_; + scoped_refptr<DataBuffer> buffer_; +}; + +TEST_F(FFmpegVideoDecodeEngineTest, Construction) { + FFmpegVideoDecodeEngine engine; + EXPECT_FALSE(engine.codec_context()); + EXPECT_EQ(FFmpegVideoDecodeEngine::kCreated, engine.state()); +} + +TEST_F(FFmpegVideoDecodeEngineTest, Initialize_Normal) { + // Test avcodec_open() failing. + EXPECT_CALL(*MockFFmpeg::get(), AVCodecFindDecoder(CODEC_ID_NONE)) + .WillOnce(Return(&codec_)); + EXPECT_CALL(*MockFFmpeg::get(), AVCodecThreadInit(&codec_context_, 2)) + .WillOnce(Return(0)); + EXPECT_CALL(*MockFFmpeg::get(), AVCodecOpen(&codec_context_, &codec_)) + .WillOnce(Return(0)); + + TaskMocker done_cb; + EXPECT_CALL(done_cb, Run()); + + test_engine_->Initialize(&stream_, done_cb.CreateTask()); + EXPECT_EQ(VideoDecodeEngine::kNormal, test_engine_->state()); +} + +TEST_F(FFmpegVideoDecodeEngineTest, Initialize_FindDecoderFails) { + // Test avcodec_find_decoder() returning NULL. + EXPECT_CALL(*MockFFmpeg::get(), AVCodecFindDecoder(CODEC_ID_NONE)) + .WillOnce(ReturnNull()); + + TaskMocker done_cb; + EXPECT_CALL(done_cb, Run()); + + test_engine_->Initialize(&stream_, done_cb.CreateTask()); + EXPECT_EQ(VideoDecodeEngine::kError, test_engine_->state()); +} + +TEST_F(FFmpegVideoDecodeEngineTest, Initialize_InitThreadFails) { + // Test avcodec_thread_init() failing. + EXPECT_CALL(*MockFFmpeg::get(), AVCodecFindDecoder(CODEC_ID_NONE)) + .WillOnce(Return(&codec_)); + EXPECT_CALL(*MockFFmpeg::get(), AVCodecThreadInit(&codec_context_, 2)) + .WillOnce(Return(-1)); + + TaskMocker done_cb; + EXPECT_CALL(done_cb, Run()); + + test_engine_->Initialize(&stream_, done_cb.CreateTask()); + EXPECT_EQ(VideoDecodeEngine::kError, test_engine_->state()); +} + +TEST_F(FFmpegVideoDecodeEngineTest, Initialize_OpenDecoderFails) { + // Test avcodec_open() failing. + EXPECT_CALL(*MockFFmpeg::get(), AVCodecFindDecoder(CODEC_ID_NONE)) + .WillOnce(Return(&codec_)); + EXPECT_CALL(*MockFFmpeg::get(), AVCodecThreadInit(&codec_context_, 2)) + .WillOnce(Return(0)); + EXPECT_CALL(*MockFFmpeg::get(), AVCodecOpen(&codec_context_, &codec_)) + .WillOnce(Return(-1)); + + TaskMocker done_cb; + EXPECT_CALL(done_cb, Run()); + + test_engine_->Initialize(&stream_, done_cb.CreateTask()); + EXPECT_EQ(VideoDecodeEngine::kError, test_engine_->state()); +} + +TEST_F(FFmpegVideoDecodeEngineTest, DecodeFrame_Normal) { + // Expect a bunch of avcodec calls. + EXPECT_CALL(mock_ffmpeg_, AVInitPacket(_)); + EXPECT_CALL(mock_ffmpeg_, + AVCodecDecodeVideo2(&codec_context_, &yuv_frame_, _, _)) + .WillOnce(DoAll(SetArgumentPointee<2>(1), // Simulate 1 byte frame. + Return(0))); + + TaskMocker done_cb; + EXPECT_CALL(done_cb, Run()); + + bool got_result; + test_engine_->DecodeFrame(*buffer_, &yuv_frame_, &got_result, + done_cb.CreateTask()); + EXPECT_TRUE(got_result); +} + +TEST_F(FFmpegVideoDecodeEngineTest, DecodeFrame_0ByteFrame) { + // Expect a bunch of avcodec calls. + EXPECT_CALL(mock_ffmpeg_, AVInitPacket(_)); + EXPECT_CALL(mock_ffmpeg_, + AVCodecDecodeVideo2(&codec_context_, &yuv_frame_, _, _)) + .WillOnce(DoAll(SetArgumentPointee<2>(0), // Simulate 0 byte frame. + Return(0))); + + TaskMocker done_cb; + EXPECT_CALL(done_cb, Run()); + + bool got_result; + test_engine_->DecodeFrame(*buffer_, &yuv_frame_, &got_result, + done_cb.CreateTask()); + EXPECT_FALSE(got_result); +} + +TEST_F(FFmpegVideoDecodeEngineTest, DecodeFrame_DecodeError) { + // Expect a bunch of avcodec calls. + EXPECT_CALL(mock_ffmpeg_, AVInitPacket(_)); + EXPECT_CALL(mock_ffmpeg_, + AVCodecDecodeVideo2(&codec_context_, &yuv_frame_, _, _)) + .WillOnce(Return(-1)); + + TaskMocker done_cb; + EXPECT_CALL(done_cb, Run()); + + bool got_result; + test_engine_->DecodeFrame(*buffer_, &yuv_frame_, &got_result, + done_cb.CreateTask()); + EXPECT_FALSE(got_result); +} + +TEST_F(FFmpegVideoDecodeEngineTest, GetSurfaceFormat) { + // YV12 formats. + codec_context_.pix_fmt = PIX_FMT_YUV420P; + EXPECT_EQ(VideoSurface::YV12, test_engine_->GetSurfaceFormat()); + codec_context_.pix_fmt = PIX_FMT_YUVJ420P; + EXPECT_EQ(VideoSurface::YV12, test_engine_->GetSurfaceFormat()); + + // YV16 formats. + codec_context_.pix_fmt = PIX_FMT_YUV422P; + EXPECT_EQ(VideoSurface::YV16, test_engine_->GetSurfaceFormat()); + codec_context_.pix_fmt = PIX_FMT_YUVJ422P; + EXPECT_EQ(VideoSurface::YV16, test_engine_->GetSurfaceFormat()); + + // Invalid value. + codec_context_.pix_fmt = PIX_FMT_NONE; + EXPECT_EQ(VideoSurface::INVALID, test_engine_->GetSurfaceFormat()); +} + +} // namespace media diff --git a/media/filters/ffmpeg_video_decoder.cc b/media/filters/ffmpeg_video_decoder.cc index 474d30d..6390216 100644 --- a/media/filters/ffmpeg_video_decoder.cc +++ b/media/filters/ffmpeg_video_decoder.cc @@ -2,47 +2,143 @@ // source code is governed by a BSD-style license that can be found in the // LICENSE file. +#include "media/filters/ffmpeg_video_decoder.h" + +#include "base/task.h" +#include "base/waitable_event.h" +#include "media/base/callback.h" #include "media/base/limits.h" #include "media/base/video_frame_impl.h" +#include "media/ffmpeg/ffmpeg_util.h" #include "media/filters/ffmpeg_common.h" #include "media/filters/ffmpeg_demuxer.h" -#include "media/filters/ffmpeg_video_decoder.h" -namespace { +namespace media { -const AVRational kMicrosBase = { 1, base::Time::kMicrosecondsPerSecond }; +FFmpegVideoDecodeEngine::FFmpegVideoDecodeEngine() + : codec_context_(NULL), + state_(kCreated) { +} -// TODO(ajwong): Move this into a utility function file and dedup with -// FFmpegDemuxer ConvertTimestamp. -base::TimeDelta ConvertTimestamp(const AVRational& time_base, int64 timestamp) { - int64 microseconds = av_rescale_q(timestamp, time_base, kMicrosBase); - return base::TimeDelta::FromMicroseconds(microseconds); +FFmpegVideoDecodeEngine::~FFmpegVideoDecodeEngine() { } -} // namespace +void FFmpegVideoDecodeEngine::Initialize(AVStream* stream, Task* done_cb) { + AutoTaskRunner done_runner(done_cb); -namespace media { + // Always try to use two threads for video decoding. There is little reason + // not to since current day CPUs tend to be multi-core and we measured + // performance benefits on older machines such as P4s with hyperthreading. + // + // Handling decoding on separate threads also frees up the pipeline thread to + // continue processing. Although it'd be nice to have the option of a single + // decoding thread, FFmpeg treats having one thread the same as having zero + // threads (i.e., avcodec_decode_video() will execute on the calling thread). + // Yet another reason for having two threads :) + // + // TODO(scherkus): some video codecs might not like avcodec_thread_init() + // being called on them... should attempt to find out which ones those are! + static const int kDecodeThreads = 2; + + CHECK(state_ == kCreated); + + codec_context_ = stream->codec; + codec_context_->flags2 |= CODEC_FLAG2_FAST; // Enable faster H264 decode. + // Enable motion vector search (potentially slow), strong deblocking filter + // for damaged macroblocks, and set our error detection sensitivity. + codec_context_->error_concealment = FF_EC_GUESS_MVS | FF_EC_DEBLOCK; + codec_context_->error_recognition = FF_ER_CAREFUL; + + // Serialize calls to avcodec_open(). + AVCodec* codec = avcodec_find_decoder(codec_context_->codec_id); + { + AutoLock auto_lock(FFmpegLock::get()->lock()); + if (codec && + avcodec_thread_init(codec_context_, kDecodeThreads) >= 0 && + avcodec_open(codec_context_, codec) >= 0) { + state_ = kNormal; + } else { + state_ = kError; + } + } +} + +// Decodes one frame of video with the given buffer. +void FFmpegVideoDecodeEngine::DecodeFrame(const Buffer& buffer, + AVFrame* yuv_frame, + bool* got_frame, + Task* done_cb) { + AutoTaskRunner done_runner(done_cb); + + // Create a packet for input data. + // Due to FFmpeg API changes we no longer have const read-only pointers. + // + // TODO(ajwong): This is dangerous since AVPacket may change size with + // different ffmpeg versions. Use the alloca verison. + AVPacket packet; + av_init_packet(&packet); + packet.data = const_cast<uint8*>(buffer.GetData()); + packet.size = buffer.GetDataSize(); + + // We don't allocate AVFrame on the stack since different versions of FFmpeg + // may change the size of AVFrame, causing stack corruption. The solution is + // to let FFmpeg allocate the structure via avcodec_alloc_frame(). + int frame_decoded = 0; + int result = + avcodec_decode_video2(codec_context_, yuv_frame, &frame_decoded, &packet); + + // Log the problem if we can't decode a video frame and exit early. + if (result < 0) { + LOG(INFO) << "Error decoding a video frame with timestamp: " + << buffer.GetTimestamp().InMicroseconds() << " us" + << " , duration: " + << buffer.GetDuration().InMicroseconds() << " us" + << " , packet size: " + << buffer.GetDataSize() << " bytes"; + *got_frame = false; + } else { + // If frame_decoded == 0, then no frame was produced. + *got_frame = frame_decoded != 0; + } +} + +void FFmpegVideoDecodeEngine::Flush(Task* done_cb) { + AutoTaskRunner done_runner(done_cb); + + avcodec_flush_buffers(codec_context_); +} + +VideoSurface::Format FFmpegVideoDecodeEngine::GetSurfaceFormat() const { + // J (Motion JPEG) versions of YUV are full range 0..255. + // Regular (MPEG) YUV is 16..240. + // For now we will ignore the distinction and treat them the same. + switch (codec_context_->pix_fmt) { + case PIX_FMT_YUV420P: + case PIX_FMT_YUVJ420P: + return VideoSurface::YV12; + break; + case PIX_FMT_YUV422P: + case PIX_FMT_YUVJ422P: + return VideoSurface::YV16; + break; + default: + // TODO(scherkus): More formats here? + return VideoSurface::INVALID; + } +} + +// static +FilterFactory* FFmpegVideoDecoder::CreateFactory() { + return new FilterFactoryImpl1<FFmpegVideoDecoder, VideoDecodeEngine*>( + new FFmpegVideoDecodeEngine()); +} -// Always try to use two threads for video decoding. There is little reason -// not to since current day CPUs tend to be multi-core and we measured -// performance benefits on older machines such as P4s with hyperthreading. -// -// Handling decoding on separate threads also frees up the pipeline thread to -// continue processing. Although it'd be nice to have the option of a single -// decoding thread, FFmpeg treats having one thread the same as having zero -// threads (i.e., avcodec_decode_video() will execute on the calling thread). -// Yet another reason for having two threads :) -// -// TODO(scherkus): some video codecs might not like avcodec_thread_init() being -// called on them... should attempt to find out which ones those are! -static const int kDecodeThreads = 2; - -FFmpegVideoDecoder::FFmpegVideoDecoder() +FFmpegVideoDecoder::FFmpegVideoDecoder(VideoDecodeEngine* engine) : width_(0), height_(0), time_base_(new AVRational()), state_(kNormal), - codec_context_(NULL) { + decode_engine_(engine) { } FFmpegVideoDecoder::~FFmpegVideoDecoder() { @@ -55,59 +151,70 @@ bool FFmpegVideoDecoder::IsMediaFormatSupported(const MediaFormat& format) { mime_type::kFFmpegVideo == mime_type; } -bool FFmpegVideoDecoder::OnInitialize(DemuxerStream* demuxer_stream) { +void FFmpegVideoDecoder::DoInitialize(DemuxerStream* demuxer_stream, + bool* success, + Task* done_cb) { + AutoTaskRunner done_runner(done_cb); + *success = false; + // Get the AVStream by querying for the provider interface. AVStreamProvider* av_stream_provider; if (!demuxer_stream->QueryInterface(&av_stream_provider)) { - return false; + return; } AVStream* av_stream = av_stream_provider->GetAVStream(); + *time_base_ = av_stream->time_base; + + // TODO(ajwong): We don't need these extra variables if |media_format_| has + // them. Remove. width_ = av_stream->codec->width; height_ = av_stream->codec->height; - *time_base_ = av_stream->time_base; if (width_ > Limits::kMaxDimension || height_ > Limits::kMaxDimension || width_ * height_ > Limits::kMaxCanvas) - return false; + return; media_format_.SetAsString(MediaFormat::kMimeType, mime_type::kUncompressedVideo); media_format_.SetAsInteger(MediaFormat::kWidth, width_); media_format_.SetAsInteger(MediaFormat::kHeight, height_); - codec_context_ = av_stream->codec; - codec_context_->flags2 |= CODEC_FLAG2_FAST; // Enable faster H264 decode. - // Enable motion vector search (potentially slow), strong deblocking filter - // for damaged macroblocks, and set our error detection sensitivity. - codec_context_->error_concealment = FF_EC_GUESS_MVS | FF_EC_DEBLOCK; - codec_context_->error_recognition = FF_ER_CAREFUL; + decode_engine_->Initialize( + av_stream, + NewRunnableMethod(this, + &FFmpegVideoDecoder::OnInitializeComplete, + success, + done_runner.release())); +} - // Serialize calls to avcodec_open(). - AVCodec* codec = avcodec_find_decoder(codec_context_->codec_id); - { - AutoLock auto_lock(FFmpegLock::get()->lock()); - if (!codec || - avcodec_thread_init(codec_context_, kDecodeThreads) < 0 || - avcodec_open(codec_context_, codec) < 0) { - return false; - } - } - return true; +void FFmpegVideoDecoder::OnInitializeComplete(bool* success, Task* done_cb) { + AutoTaskRunner done_runner(done_cb); + + *success = decode_engine_->state() == FFmpegVideoDecodeEngine::kNormal; } -void FFmpegVideoDecoder::OnSeek(base::TimeDelta time) { +void FFmpegVideoDecoder::DoSeek(base::TimeDelta time, Task* done_cb) { // Everything in the presentation time queue is invalid, clear the queue. while (!pts_heap_.IsEmpty()) pts_heap_.Pop(); // We're back where we started. It should be completely safe to flush here // since DecoderBase uses |expecting_discontinuous_| to verify that the next - // time OnDecode() is called we will have a discontinuous buffer. + // time DoDecode() is called we will have a discontinuous buffer. + // + // TODO(ajwong): Should we put a guard here to prevent leaving kError. state_ = kNormal; - avcodec_flush_buffers(codec_context_); + + decode_engine_->Flush(done_cb); } -void FFmpegVideoDecoder::OnDecode(Buffer* buffer) { +void FFmpegVideoDecoder::DoDecode(Buffer* buffer, Task* done_cb) { + AutoTaskRunner done_runner(done_cb); + + // TODO(ajwong): This DoDecode and OnDecodeComplete set of functions is too + // complicated to easily unittest. The test becomes fragile. Try to find a + // way to reorganize into smaller units for testing. + // During decode, because reads are issued asynchronously, it is possible to // receive multiple end of stream buffers since each read is acked. When the // first end of stream buffer is read, FFmpeg may still have frames queued @@ -155,12 +262,31 @@ void FFmpegVideoDecoder::OnDecode(Buffer* buffer) { } // Otherwise, attempt to decode a single frame. - scoped_ptr_malloc<AVFrame, ScopedPtrAVFree> yuv_frame(avcodec_alloc_frame()); - if (DecodeFrame(*buffer, codec_context_, yuv_frame.get())) { - last_pts_ = FindPtsAndDuration(*time_base_, - pts_heap_, - last_pts_, - yuv_frame.get()); + AVFrame* yuv_frame = avcodec_alloc_frame(); + bool* got_frame = new bool; + decode_engine_->DecodeFrame( + *buffer, + yuv_frame, + got_frame, + NewRunnableMethod(this, + &FFmpegVideoDecoder::OnDecodeComplete, + yuv_frame, + got_frame, + done_runner.release())); +} + +void FFmpegVideoDecoder::OnDecodeComplete(AVFrame* yuv_frame, bool* got_frame, + Task* done_cb) { + // Note: The |done_runner| must be declared *last* to ensure proper + // destruction order. + scoped_ptr_malloc<AVFrame, ScopedPtrAVFree> yuv_frame_deleter(yuv_frame); + scoped_ptr<bool> got_frame_deleter(got_frame); + AutoTaskRunner done_runner(done_cb); + + // If we actually got data back, enqueue a frame. + if (*got_frame) { + last_pts_ = FindPtsAndDuration(*time_base_, pts_heap_, last_pts_, + yuv_frame); // Pop off a pts on a successful decode since we are "using up" one // timestamp. @@ -177,7 +303,7 @@ void FFmpegVideoDecoder::OnDecode(Buffer* buffer) { } if (!EnqueueVideoFrame( - GetSurfaceFormat(*codec_context_), last_pts_, yuv_frame.get())) { + decode_engine_->GetSurfaceFormat(), last_pts_, yuv_frame)) { // On an EnqueueEmptyFrame error, error out the whole pipeline and // set the state to kDecodeFinished. SignalPipelineError(); @@ -258,38 +384,6 @@ void FFmpegVideoDecoder::EnqueueEmptyFrame() { EnqueueResult(video_frame); } -bool FFmpegVideoDecoder::DecodeFrame(const Buffer& buffer, - AVCodecContext* codec_context, - AVFrame* yuv_frame) { - // Create a packet for input data. - // Due to FFmpeg API changes we no longer have const read-only pointers. - AVPacket packet; - av_init_packet(&packet); - packet.data = const_cast<uint8*>(buffer.GetData()); - packet.size = buffer.GetDataSize(); - - // We don't allocate AVFrame on the stack since different versions of FFmpeg - // may change the size of AVFrame, causing stack corruption. The solution is - // to let FFmpeg allocate the structure via avcodec_alloc_frame(). - int frame_decoded = 0; - int result = - avcodec_decode_video2(codec_context, yuv_frame, &frame_decoded, &packet); - - // Log the problem if we can't decode a video frame and exit early. - if (result < 0) { - LOG(INFO) << "Error decoding a video frame with timestamp: " - << buffer.GetTimestamp().InMicroseconds() << " us" - << " , duration: " - << buffer.GetDuration().InMicroseconds() << " us" - << " , packet size: " - << buffer.GetDataSize() << " bytes"; - return false; - } - - // If frame_decoded == 0, then no frame was produced. - return frame_decoded != 0; -} - FFmpegVideoDecoder::TimeTuple FFmpegVideoDecoder::FindPtsAndDuration( const AVRational& time_base, const PtsHeap& pts_heap, @@ -333,29 +427,14 @@ FFmpegVideoDecoder::TimeTuple FFmpegVideoDecoder::FindPtsAndDuration( return pts; } -VideoSurface::Format FFmpegVideoDecoder::GetSurfaceFormat( - const AVCodecContext& codec_context) { - // J (Motion JPEG) versions of YUV are full range 0..255. - // Regular (MPEG) YUV is 16..240. - // For now we will ignore the distinction and treat them the same. - switch (codec_context.pix_fmt) { - case PIX_FMT_YUV420P: - case PIX_FMT_YUVJ420P: - return VideoSurface::YV12; - break; - case PIX_FMT_YUV422P: - case PIX_FMT_YUVJ422P: - return VideoSurface::YV16; - break; - default: - // TODO(scherkus): More formats here? - return VideoSurface::INVALID; - } -} - void FFmpegVideoDecoder::SignalPipelineError() { host()->SetError(PIPELINE_ERROR_DECODE); state_ = kDecodeFinished; } -} // namespace +void FFmpegVideoDecoder::SetVideoDecodeEngineForTest( + VideoDecodeEngine* engine) { + decode_engine_.reset(engine); +} + +} // namespace media diff --git a/media/filters/ffmpeg_video_decoder.h b/media/filters/ffmpeg_video_decoder.h index fba1ae7..b35e173 100644 --- a/media/filters/ffmpeg_video_decoder.h +++ b/media/filters/ffmpeg_video_decoder.h @@ -10,43 +10,58 @@ #include "media/base/factory.h" #include "media/base/pts_heap.h" #include "media/filters/decoder_base.h" +#include "media/filters/video_decode_engine.h" #include "testing/gtest/include/gtest/gtest_prod.h" // FFmpeg types. struct AVCodecContext; struct AVFrame; struct AVRational; +struct AVStream; namespace media { -class FFmpegVideoDecoder : public DecoderBase<VideoDecoder, VideoFrame> { +class FFmpegVideoDecodeEngine : public VideoDecodeEngine { public: - static FilterFactory* CreateFactory() { - return new FilterFactoryImpl0<FFmpegVideoDecoder>(); - } + FFmpegVideoDecodeEngine(); + virtual ~FFmpegVideoDecodeEngine(); - static bool IsMediaFormatSupported(const MediaFormat& media_format); + // Implementation of the VideoDecodeEngine Interface. + virtual void Initialize(AVStream* stream, Task* done_cb); + virtual void DecodeFrame(const Buffer& buffer, AVFrame* yuv_frame, + bool* got_result, Task* done_cb); + virtual void Flush(Task* done_cb); + virtual VideoSurface::Format GetSurfaceFormat() const; - protected: - virtual bool OnInitialize(DemuxerStream* demuxer_stream); + virtual State state() const { return state_; } - virtual void OnSeek(base::TimeDelta time); + virtual AVCodecContext* codec_context() const { return codec_context_; } - virtual void OnDecode(Buffer* buffer); + virtual void SetCodecContextForTest(AVCodecContext* context) { + codec_context_ = context; + } private: - friend class FilterFactoryImpl0<FFmpegVideoDecoder>; + AVCodecContext* codec_context_; + State state_; + + DISALLOW_COPY_AND_ASSIGN(FFmpegVideoDecodeEngine); +}; + +class FFmpegVideoDecoder : public DecoderBase<VideoDecoder, VideoFrame> { + public: + static FilterFactory* CreateFactory(); + static bool IsMediaFormatSupported(const MediaFormat& media_format); + + private: + friend class FilterFactoryImpl1<FFmpegVideoDecoder, VideoDecodeEngine*>; friend class DecoderPrivateMock; friend class FFmpegVideoDecoderTest; - FRIEND_TEST(FFmpegVideoDecoderTest, DecodeFrame_0ByteFrame); - FRIEND_TEST(FFmpegVideoDecoderTest, DecodeFrame_DecodeError); - FRIEND_TEST(FFmpegVideoDecoderTest, DecodeFrame_Normal); FRIEND_TEST(FFmpegVideoDecoderTest, FindPtsAndDuration); - FRIEND_TEST(FFmpegVideoDecoderTest, GetSurfaceFormat); - FRIEND_TEST(FFmpegVideoDecoderTest, OnDecode_EnqueueVideoFrameError); - FRIEND_TEST(FFmpegVideoDecoderTest, OnDecode_FinishEnqueuesEmptyFrames); - FRIEND_TEST(FFmpegVideoDecoderTest, OnDecode_TestStateTransition); - FRIEND_TEST(FFmpegVideoDecoderTest, OnSeek); + FRIEND_TEST(FFmpegVideoDecoderTest, DoDecode_EnqueueVideoFrameError); + FRIEND_TEST(FFmpegVideoDecoderTest, DoDecode_FinishEnqueuesEmptyFrames); + FRIEND_TEST(FFmpegVideoDecoderTest, DoDecode_TestStateTransition); + FRIEND_TEST(FFmpegVideoDecoderTest, DoSeek); // The TimeTuple struct is used to hold the needed timestamp data needed for // enqueuing a video frame. @@ -55,9 +70,15 @@ class FFmpegVideoDecoder : public DecoderBase<VideoDecoder, VideoFrame> { base::TimeDelta duration; }; - FFmpegVideoDecoder(); + FFmpegVideoDecoder(VideoDecodeEngine* engine); virtual ~FFmpegVideoDecoder(); + // Implement DecoderBase template methods. + virtual void DoInitialize(DemuxerStream* demuxer_stream, bool* success, + Task* done_cb); + virtual void DoSeek(base::TimeDelta time, Task* done_cb); + virtual void DoDecode(Buffer* buffer, Task* done_cb); + virtual bool EnqueueVideoFrame(VideoSurface::Format surface_format, const TimeTuple& time, const AVFrame* frame); @@ -68,14 +89,11 @@ class FFmpegVideoDecoder : public DecoderBase<VideoDecoder, VideoFrame> { virtual void CopyPlane(size_t plane, const VideoSurface& surface, const AVFrame* frame); - // Converts a AVCodecContext |pix_fmt| to a VideoSurface::Format. - virtual VideoSurface::Format GetSurfaceFormat( - const AVCodecContext& codec_context); - - // Decodes one frame of video with the given buffer. Returns false if there - // was a decode error, or a zero-byte frame was produced. - virtual bool DecodeFrame(const Buffer& buffer, AVCodecContext* codec_context, - AVFrame* yuv_frame); + // Methods that pickup after the decode engine has finished its action. + virtual void OnInitializeComplete(bool* success /* Not owned */, + Task* done_cb); + virtual void OnDecodeComplete(AVFrame* yuv_frame, bool* got_frame, + Task* done_cb); // Attempt to get the PTS and Duration for this frame by examining the time // info provided via packet stream (stored in |pts_heap|), or the info @@ -94,6 +112,10 @@ class FFmpegVideoDecoder : public DecoderBase<VideoDecoder, VideoFrame> { // into the kDecodeFinished state. virtual void SignalPipelineError(); + // Injection point for unittest to provide a mock engine. Takes ownership of + // the provided engine. + virtual void SetVideoDecodeEngineForTest(VideoDecodeEngine* engine); + size_t width_; size_t height_; @@ -109,7 +131,7 @@ class FFmpegVideoDecoder : public DecoderBase<VideoDecoder, VideoFrame> { DecoderState state_; - AVCodecContext* codec_context_; + scoped_ptr<VideoDecodeEngine> decode_engine_; DISALLOW_COPY_AND_ASSIGN(FFmpegVideoDecoder); }; diff --git a/media/filters/ffmpeg_video_decoder_unittest.cc b/media/filters/ffmpeg_video_decoder_unittest.cc index 9c0963b..7bb05a9 100644 --- a/media/filters/ffmpeg_video_decoder_unittest.cc +++ b/media/filters/ffmpeg_video_decoder_unittest.cc @@ -5,11 +5,13 @@ #include <deque> #include "base/singleton.h" +#include "base/string_util.h" #include "media/base/data_buffer.h" #include "media/base/filters.h" #include "media/base/mock_ffmpeg.h" #include "media/base/mock_filter_host.h" #include "media/base/mock_filters.h" +#include "media/base/mock_task.h" #include "media/filters/ffmpeg_common.h" #include "media/filters/ffmpeg_interfaces.h" #include "media/filters/ffmpeg_video_decoder.h" @@ -17,10 +19,12 @@ using ::testing::_; using ::testing::DoAll; +using ::testing::Message; using ::testing::Return; using ::testing::ReturnNull; using ::testing::SetArgumentPointee; using ::testing::StrictMock; +using ::testing::WithArg; namespace media { @@ -39,20 +43,29 @@ class MockFFmpegDemuxerStream : public MockDemuxerStream, DISALLOW_COPY_AND_ASSIGN(MockFFmpegDemuxerStream); }; +class MockVideoDecodeEngine : public VideoDecodeEngine { + public: + MOCK_METHOD2(Initialize, void(AVStream* stream, Task* done_cb)); + MOCK_METHOD4(DecodeFrame, void(const Buffer& buffer, AVFrame* yuv_frame, + bool* got_result, Task* done_cb)); + MOCK_METHOD1(Flush, void(Task* done_cb)); + MOCK_CONST_METHOD0(state, State()); + MOCK_CONST_METHOD0(GetSurfaceFormat, VideoSurface::Format()); +}; + // Class that just mocks the private functions. class DecoderPrivateMock : public FFmpegVideoDecoder { public: + DecoderPrivateMock(VideoDecodeEngine* engine) + : FFmpegVideoDecoder(engine) { + } + MOCK_METHOD3(EnqueueVideoFrame, bool(VideoSurface::Format surface_format, const TimeTuple& time, const AVFrame* frame)); MOCK_METHOD0(EnqueueEmptyFrame, void()); MOCK_METHOD3(CopyPlane, void(size_t plane, const VideoSurface& surface, const AVFrame* frame)); - MOCK_METHOD1(GetSurfaceFormat, - VideoSurface::Format(const AVCodecContext& codec_context)); - MOCK_METHOD3(DecodeFrame, bool(const Buffer& buffer, - AVCodecContext* codec_context, - AVFrame* yuv_frame)); MOCK_METHOD4(FindPtsAndDuration, TimeTuple(const AVRational& time_base, const PtsHeap& pts_heap, const TimeTuple& last_pts, @@ -73,14 +86,17 @@ class FFmpegVideoDecoderTest : public testing::Test { MediaFormat media_format; media_format.SetAsString(MediaFormat::kMimeType, mime_type::kFFmpegVideo); - // Create an FFmpegVideoDecoder. + // Create an FFmpegVideoDecoder, and MockVideoDecodeEngine. factory_ = FFmpegVideoDecoder::CreateFactory(); decoder_ = factory_->Create<FFmpegVideoDecoder>(media_format); + engine_ = new StrictMock<MockVideoDecodeEngine>(); + DCHECK(decoder_); - // Inject a filter host and message loop and prepare a demuxer stream. + // Inject mocks and prepare a demuxer stream. decoder_->set_host(&host_); decoder_->set_message_loop(&message_loop_); + decoder_->SetVideoDecodeEngineForTest(engine_); demuxer_ = new StrictMock<MockFFmpegDemuxerStream>(); // Initialize FFmpeg fixtures. @@ -112,6 +128,7 @@ class FFmpegVideoDecoderTest : public testing::Test { // Fixture members. scoped_refptr<FilterFactory> factory_; + MockVideoDecodeEngine* engine_; // Owned by |decoder_|. scoped_refptr<FFmpegVideoDecoder> decoder_; scoped_refptr<StrictMock<MockFFmpegDemuxerStream> > demuxer_; scoped_refptr<DataBuffer> buffer_; @@ -169,56 +186,21 @@ TEST_F(FFmpegVideoDecoderTest, Initialize_QueryInterfaceFails) { message_loop_.RunAllPending(); } -TEST_F(FFmpegVideoDecoderTest, Initialize_FindDecoderFails) { - // Test avcodec_find_decoder() returning NULL. +TEST_F(FFmpegVideoDecoderTest, Initialize_EngineFails) { + // Test successful initialization. AVStreamProvider* av_stream_provider = demuxer_; EXPECT_CALL(*demuxer_, QueryInterface(AVStreamProvider::interface_id())) .WillOnce(Return(av_stream_provider)); EXPECT_CALL(*demuxer_, GetAVStream()) .WillOnce(Return(&stream_)); - EXPECT_CALL(*MockFFmpeg::get(), AVCodecFindDecoder(CODEC_ID_NONE)) - .WillOnce(ReturnNull()); - EXPECT_CALL(host_, SetError(PIPELINE_ERROR_DECODE)); - EXPECT_CALL(callback_, OnFilterCallback()); - EXPECT_CALL(callback_, OnCallbackDestroyed()); - decoder_->Initialize(demuxer_, callback_.NewCallback()); - message_loop_.RunAllPending(); -} + EXPECT_CALL(*engine_, Initialize(_, _)) + .WillOnce(WithArg<1>(InvokeRunnable())); + EXPECT_CALL(*engine_, state()) + .WillOnce(Return(VideoDecodeEngine::kError)); -TEST_F(FFmpegVideoDecoderTest, Initialize_InitThreadFails) { - // Test avcodec_thread_init() failing. - AVStreamProvider* av_stream_provider = demuxer_; - EXPECT_CALL(*demuxer_, QueryInterface(AVStreamProvider::interface_id())) - .WillOnce(Return(av_stream_provider)); - EXPECT_CALL(*demuxer_, GetAVStream()) - .WillOnce(Return(&stream_)); - EXPECT_CALL(*MockFFmpeg::get(), AVCodecFindDecoder(CODEC_ID_NONE)) - .WillOnce(Return(&codec_)); - EXPECT_CALL(*MockFFmpeg::get(), AVCodecThreadInit(&codec_context_, 2)) - .WillOnce(Return(-1)); EXPECT_CALL(host_, SetError(PIPELINE_ERROR_DECODE)); - EXPECT_CALL(callback_, OnFilterCallback()); - EXPECT_CALL(callback_, OnCallbackDestroyed()); - - decoder_->Initialize(demuxer_, callback_.NewCallback()); - message_loop_.RunAllPending(); -} -TEST_F(FFmpegVideoDecoderTest, Initialize_OpenDecoderFails) { - // Test avcodec_open() failing. - AVStreamProvider* av_stream_provider = demuxer_; - EXPECT_CALL(*demuxer_, QueryInterface(AVStreamProvider::interface_id())) - .WillOnce(Return(av_stream_provider)); - EXPECT_CALL(*demuxer_, GetAVStream()) - .WillOnce(Return(&stream_)); - EXPECT_CALL(*MockFFmpeg::get(), AVCodecFindDecoder(CODEC_ID_NONE)) - .WillOnce(Return(&codec_)); - EXPECT_CALL(*MockFFmpeg::get(), AVCodecThreadInit(&codec_context_, 2)) - .WillOnce(Return(0)); - EXPECT_CALL(*MockFFmpeg::get(), AVCodecOpen(&codec_context_, &codec_)) - .WillOnce(Return(-1)); - EXPECT_CALL(host_, SetError(PIPELINE_ERROR_DECODE)); EXPECT_CALL(callback_, OnFilterCallback()); EXPECT_CALL(callback_, OnCallbackDestroyed()); @@ -233,12 +215,12 @@ TEST_F(FFmpegVideoDecoderTest, Initialize_Successful) { .WillOnce(Return(av_stream_provider)); EXPECT_CALL(*demuxer_, GetAVStream()) .WillOnce(Return(&stream_)); - EXPECT_CALL(*MockFFmpeg::get(), AVCodecFindDecoder(CODEC_ID_NONE)) - .WillOnce(Return(&codec_)); - EXPECT_CALL(*MockFFmpeg::get(), AVCodecThreadInit(&codec_context_, 2)) - .WillOnce(Return(0)); - EXPECT_CALL(*MockFFmpeg::get(), AVCodecOpen(&codec_context_, &codec_)) - .WillOnce(Return(0)); + + EXPECT_CALL(*engine_, Initialize(_, _)) + .WillOnce(WithArg<1>(InvokeRunnable())); + EXPECT_CALL(*engine_, state()) + .WillOnce(Return(VideoDecodeEngine::kNormal)); + EXPECT_CALL(callback_, OnFilterCallback()); EXPECT_CALL(callback_, OnCallbackDestroyed()); @@ -259,66 +241,9 @@ TEST_F(FFmpegVideoDecoderTest, Initialize_Successful) { EXPECT_EQ(kHeight, height); } -TEST_F(FFmpegVideoDecoderTest, DecodeFrame_Normal) { - // Expect a bunch of avcodec calls. - EXPECT_CALL(mock_ffmpeg_, AVInitPacket(_)); - EXPECT_CALL(mock_ffmpeg_, - AVCodecDecodeVideo2(&codec_context_, &yuv_frame_, _, _)) - .WillOnce(DoAll(SetArgumentPointee<2>(1), // Simulate 1 byte frame. - Return(0))); - - scoped_refptr<FFmpegVideoDecoder> decoder = new FFmpegVideoDecoder(); - EXPECT_TRUE(decoder->DecodeFrame(*buffer_, &codec_context_, &yuv_frame_)); -} - -TEST_F(FFmpegVideoDecoderTest, DecodeFrame_0ByteFrame) { - // Expect a bunch of avcodec calls. - EXPECT_CALL(mock_ffmpeg_, AVInitPacket(_)); - EXPECT_CALL(mock_ffmpeg_, - AVCodecDecodeVideo2(&codec_context_, &yuv_frame_, _, _)) - .WillOnce(DoAll(SetArgumentPointee<2>(0), // Simulate 0 byte frame. - Return(0))); - - scoped_refptr<FFmpegVideoDecoder> decoder = new FFmpegVideoDecoder(); - EXPECT_FALSE(decoder->DecodeFrame(*buffer_, &codec_context_, &yuv_frame_)); -} - -TEST_F(FFmpegVideoDecoderTest, DecodeFrame_DecodeError) { - // Expect a bunch of avcodec calls. - EXPECT_CALL(mock_ffmpeg_, AVInitPacket(_)); - EXPECT_CALL(mock_ffmpeg_, - AVCodecDecodeVideo2(&codec_context_, &yuv_frame_, _, _)) - .WillOnce(Return(-1)); - - scoped_refptr<FFmpegVideoDecoder> decoder = new FFmpegVideoDecoder(); - EXPECT_FALSE(decoder->DecodeFrame(*buffer_, &codec_context_, &yuv_frame_)); -} - -TEST_F(FFmpegVideoDecoderTest, GetSurfaceFormat) { - AVCodecContext context; - scoped_refptr<FFmpegVideoDecoder> decoder = new FFmpegVideoDecoder(); - - // YV12 formats. - context.pix_fmt = PIX_FMT_YUV420P; - EXPECT_EQ(VideoSurface::YV12, decoder->GetSurfaceFormat(context)); - context.pix_fmt = PIX_FMT_YUVJ420P; - EXPECT_EQ(VideoSurface::YV12, decoder->GetSurfaceFormat(context)); - - // YV16 formats. - context.pix_fmt = PIX_FMT_YUV422P; - EXPECT_EQ(VideoSurface::YV16, decoder->GetSurfaceFormat(context)); - context.pix_fmt = PIX_FMT_YUVJ422P; - EXPECT_EQ(VideoSurface::YV16, decoder->GetSurfaceFormat(context)); - - // Invalid value. - context.pix_fmt = PIX_FMT_NONE; - EXPECT_EQ(VideoSurface::INVALID, decoder->GetSurfaceFormat(context)); -} - TEST_F(FFmpegVideoDecoderTest, FindPtsAndDuration) { // Start with an empty timestamp queue. PtsHeap pts_heap; - scoped_refptr<FFmpegVideoDecoder> decoder = new FFmpegVideoDecoder(); // Use 1/2 second for simple results. Thus, calculated Durations should be // 500000 microseconds. @@ -333,16 +258,16 @@ TEST_F(FFmpegVideoDecoderTest, FindPtsAndDuration) { // Simulate an uninitialized yuv_frame. yuv_frame_.pts = AV_NOPTS_VALUE; FFmpegVideoDecoder::TimeTuple result_pts = - decoder->FindPtsAndDuration(time_base, pts_heap, last_pts, &yuv_frame_); + decoder_->FindPtsAndDuration(time_base, pts_heap, last_pts, &yuv_frame_); EXPECT_EQ(116, result_pts.timestamp.InMicroseconds()); EXPECT_EQ(500000, result_pts.duration.InMicroseconds()); // Test that providing no frame has the same result as an uninitialized // frame. - result_pts = decoder->FindPtsAndDuration(time_base, - pts_heap, - last_pts, - NULL); + result_pts = decoder_->FindPtsAndDuration(time_base, + pts_heap, + last_pts, + NULL); EXPECT_EQ(116, result_pts.timestamp.InMicroseconds()); EXPECT_EQ(500000, result_pts.duration.InMicroseconds()); @@ -351,33 +276,33 @@ TEST_F(FFmpegVideoDecoderTest, FindPtsAndDuration) { // data for the frame, which means that value is useless to us. yuv_frame_.pts = 0; result_pts = - decoder->FindPtsAndDuration(time_base, pts_heap, last_pts, &yuv_frame_); + decoder_->FindPtsAndDuration(time_base, pts_heap, last_pts, &yuv_frame_); EXPECT_EQ(116, result_pts.timestamp.InMicroseconds()); EXPECT_EQ(500000, result_pts.duration.InMicroseconds()); // Add a pts to the timequeue and make sure it overrides estimation. pts_heap.Push(base::TimeDelta::FromMicroseconds(123)); - result_pts = decoder->FindPtsAndDuration(time_base, - pts_heap, - last_pts, - &yuv_frame_); + result_pts = decoder_->FindPtsAndDuration(time_base, + pts_heap, + last_pts, + &yuv_frame_); EXPECT_EQ(123, result_pts.timestamp.InMicroseconds()); EXPECT_EQ(500000, result_pts.duration.InMicroseconds()); // Add a pts into the frame and make sure it overrides the timequeue. yuv_frame_.pts = 333; yuv_frame_.repeat_pict = 2; - result_pts = decoder->FindPtsAndDuration(time_base, - pts_heap, - last_pts, - &yuv_frame_); + result_pts = decoder_->FindPtsAndDuration(time_base, + pts_heap, + last_pts, + &yuv_frame_); EXPECT_EQ(166500000, result_pts.timestamp.InMicroseconds()); EXPECT_EQ(1500000, result_pts.duration.InMicroseconds()); } -TEST_F(FFmpegVideoDecoderTest, OnDecode_TestStateTransition) { +TEST_F(FFmpegVideoDecoderTest, DoDecode_TestStateTransition) { // Simulates a input sequence of three buffers, and six decode requests to - // exercise the state transitions, and bookkeeping logic of OnDecode. + // exercise the state transitions, and bookkeeping logic of DoDecode. // // We try to verify the following: // 1) Non-EoS buffer timestamps are pushed into the pts_heap. @@ -386,28 +311,38 @@ TEST_F(FFmpegVideoDecoderTest, OnDecode_TestStateTransition) { // 4) kDecodeFinished is never left regardless of what kind of buffer is // given. // 5) All state transitions happen as expected. + MockVideoDecodeEngine* mock_engine = new StrictMock<MockVideoDecodeEngine>(); scoped_refptr<DecoderPrivateMock> mock_decoder = - new StrictMock<DecoderPrivateMock>(); + new StrictMock<DecoderPrivateMock>(mock_engine); // Setup decoder to buffer one frame, decode one frame, fail one frame, // decode one more, and then fail the last one to end decoding. - EXPECT_CALL(*mock_decoder, DecodeFrame(_, _, _)) - .WillOnce(Return(false)) - .WillOnce(DoAll(SetArgumentPointee<2>(yuv_frame_), Return(true))) - .WillOnce(Return(false)) - .WillOnce(DoAll(SetArgumentPointee<2>(yuv_frame_), Return(true))) - .WillOnce(DoAll(SetArgumentPointee<2>(yuv_frame_), Return(true))) - .WillOnce(Return(false)); - EXPECT_CALL(*mock_decoder, GetSurfaceFormat(_)) + EXPECT_CALL(*mock_engine, DecodeFrame(_, _, _,_)) + .WillOnce(DoAll(SetArgumentPointee<2>(false), + WithArg<3>(InvokeRunnable()))) + .WillOnce(DoAll(SetArgumentPointee<1>(yuv_frame_), + SetArgumentPointee<2>(true), + WithArg<3>(InvokeRunnable()))) + .WillOnce(DoAll(SetArgumentPointee<2>(false), + WithArg<3>(InvokeRunnable()))) + .WillOnce(DoAll(SetArgumentPointee<1>(yuv_frame_), + SetArgumentPointee<2>(true), + WithArg<3>(InvokeRunnable()))) + .WillOnce(DoAll(SetArgumentPointee<1>(yuv_frame_), + SetArgumentPointee<2>(true), + WithArg<3>(InvokeRunnable()))) + .WillOnce(DoAll(SetArgumentPointee<2>(false), + WithArg<3>(InvokeRunnable()))); + EXPECT_CALL(*mock_engine, GetSurfaceFormat()) .Times(3) .WillRepeatedly(Return(VideoSurface::YV16)); - EXPECT_CALL(*mock_decoder, EnqueueVideoFrame(_, _, _)) - .Times(3) - .WillRepeatedly(Return(true)); EXPECT_CALL(*mock_decoder, FindPtsAndDuration(_, _, _, _)) .WillOnce(Return(kTestPts1)) .WillOnce(Return(kTestPts2)) .WillOnce(Return(kTestPts1)); + EXPECT_CALL(*mock_decoder, EnqueueVideoFrame(_, _, _)) + .Times(3) + .WillRepeatedly(Return(true)); EXPECT_CALL(*mock_decoder, EnqueueEmptyFrame()) .Times(1); @@ -419,21 +354,24 @@ TEST_F(FFmpegVideoDecoderTest, OnDecode_TestStateTransition) { EXPECT_CALL(mock_ffmpeg_, AVFree(&yuv_frame_)) .Times(6); + // Setup callbacks to be executed 6 times. + TaskMocker done_cb; + EXPECT_CALL(done_cb, Run()).Times(6); + // Setup initial state and check that it is sane. - mock_decoder->codec_context_ = &codec_context_; ASSERT_EQ(FFmpegVideoDecoder::kNormal, mock_decoder->state_); ASSERT_TRUE(base::TimeDelta() == mock_decoder->last_pts_.timestamp); ASSERT_TRUE(base::TimeDelta() == mock_decoder->last_pts_.duration); // Decode once, which should simulate a buffering call. - mock_decoder->OnDecode(buffer_); + mock_decoder->DoDecode(buffer_, done_cb.CreateTask()); EXPECT_EQ(FFmpegVideoDecoder::kNormal, mock_decoder->state_); ASSERT_TRUE(base::TimeDelta() == mock_decoder->last_pts_.timestamp); ASSERT_TRUE(base::TimeDelta() == mock_decoder->last_pts_.duration); EXPECT_FALSE(mock_decoder->pts_heap_.IsEmpty()); // Decode a second time, which should yield the first frame. - mock_decoder->OnDecode(buffer_); + mock_decoder->DoDecode(buffer_, done_cb.CreateTask()); EXPECT_EQ(FFmpegVideoDecoder::kNormal, mock_decoder->state_); EXPECT_TRUE(kTestPts1.timestamp == mock_decoder->last_pts_.timestamp); EXPECT_TRUE(kTestPts1.duration == mock_decoder->last_pts_.duration); @@ -441,7 +379,7 @@ TEST_F(FFmpegVideoDecoderTest, OnDecode_TestStateTransition) { // Decode a third time, with a regular buffer. The decode will error // out, but the state should be the same. - mock_decoder->OnDecode(buffer_); + mock_decoder->DoDecode(buffer_, done_cb.CreateTask()); EXPECT_EQ(FFmpegVideoDecoder::kNormal, mock_decoder->state_); EXPECT_TRUE(kTestPts1.timestamp == mock_decoder->last_pts_.timestamp); EXPECT_TRUE(kTestPts1.duration == mock_decoder->last_pts_.duration); @@ -449,7 +387,7 @@ TEST_F(FFmpegVideoDecoderTest, OnDecode_TestStateTransition) { // Decode a fourth time, with an end of stream buffer. This should // yield the second frame, and stay in flushing mode. - mock_decoder->OnDecode(end_of_stream_buffer_); + mock_decoder->DoDecode(end_of_stream_buffer_, done_cb.CreateTask()); EXPECT_EQ(FFmpegVideoDecoder::kFlushCodec, mock_decoder->state_); EXPECT_TRUE(kTestPts2.timestamp == mock_decoder->last_pts_.timestamp); EXPECT_TRUE(kTestPts2.duration == mock_decoder->last_pts_.duration); @@ -457,7 +395,7 @@ TEST_F(FFmpegVideoDecoderTest, OnDecode_TestStateTransition) { // Decode a fifth time with an end of stream buffer. this should // yield the third frame. - mock_decoder->OnDecode(end_of_stream_buffer_); + mock_decoder->DoDecode(end_of_stream_buffer_, done_cb.CreateTask()); EXPECT_EQ(FFmpegVideoDecoder::kFlushCodec, mock_decoder->state_); EXPECT_TRUE(kTestPts1.timestamp == mock_decoder->last_pts_.timestamp); EXPECT_TRUE(kTestPts1.duration == mock_decoder->last_pts_.duration); @@ -465,61 +403,71 @@ TEST_F(FFmpegVideoDecoderTest, OnDecode_TestStateTransition) { // Decode a sixth time with an end of stream buffer. This should // Move into kDecodeFinished. - mock_decoder->OnDecode(end_of_stream_buffer_); + mock_decoder->DoDecode(end_of_stream_buffer_, done_cb.CreateTask()); EXPECT_EQ(FFmpegVideoDecoder::kDecodeFinished, mock_decoder->state_); EXPECT_TRUE(kTestPts1.timestamp == mock_decoder->last_pts_.timestamp); EXPECT_TRUE(kTestPts1.duration == mock_decoder->last_pts_.duration); EXPECT_TRUE(mock_decoder->pts_heap_.IsEmpty()); } -TEST_F(FFmpegVideoDecoderTest, OnDecode_EnqueueVideoFrameError) { +TEST_F(FFmpegVideoDecoderTest, DoDecode_EnqueueVideoFrameError) { + MockVideoDecodeEngine* mock_engine = new StrictMock<MockVideoDecodeEngine>(); scoped_refptr<DecoderPrivateMock> mock_decoder = - new StrictMock<DecoderPrivateMock>(); + new StrictMock<DecoderPrivateMock>(mock_engine); // Setup decoder to decode one frame, but then fail on enqueue. - EXPECT_CALL(*mock_decoder, DecodeFrame(_, _, _)) - .WillOnce(DoAll(SetArgumentPointee<2>(yuv_frame_), Return(true))); - EXPECT_CALL(*mock_decoder, GetSurfaceFormat(_)) + EXPECT_CALL(*mock_engine, DecodeFrame(_, _, _,_)) + .WillOnce(DoAll(SetArgumentPointee<1>(yuv_frame_), + SetArgumentPointee<2>(true), + WithArg<3>(InvokeRunnable()))); + EXPECT_CALL(*mock_engine, GetSurfaceFormat()) .WillOnce(Return(VideoSurface::YV16)); - EXPECT_CALL(*mock_decoder, EnqueueVideoFrame(_, _, _)) - .WillOnce(Return(false)); EXPECT_CALL(*mock_decoder, FindPtsAndDuration(_, _, _, _)) .WillOnce(Return(kTestPts1)); + EXPECT_CALL(*mock_decoder, EnqueueVideoFrame(_, _, _)) + .WillOnce(Return(false)); EXPECT_CALL(*mock_decoder, SignalPipelineError()); + // Count the callback invoked. + TaskMocker done_cb; + EXPECT_CALL(done_cb, Run()).Times(1); + // Setup FFmpeg expectations for frame allocations. EXPECT_CALL(mock_ffmpeg_, AVCodecAllocFrame()) .WillOnce(Return(&yuv_frame_)); EXPECT_CALL(mock_ffmpeg_, AVFree(&yuv_frame_)); // Attempt the decode. - mock_decoder->codec_context_ = &codec_context_; - mock_decoder->OnDecode(buffer_); + mock_decoder->DoDecode(buffer_, done_cb.CreateTask()); } -TEST_F(FFmpegVideoDecoderTest, OnDecode_FinishEnqueuesEmptyFrames) { +TEST_F(FFmpegVideoDecoderTest, DoDecode_FinishEnqueuesEmptyFrames) { + MockVideoDecodeEngine* mock_engine = new StrictMock<MockVideoDecodeEngine>(); scoped_refptr<DecoderPrivateMock> mock_decoder = - new StrictMock<DecoderPrivateMock>(); + new StrictMock<DecoderPrivateMock>(mock_engine); // Move the decoder into the finished state for this test. mock_decoder->state_ = FFmpegVideoDecoder::kDecodeFinished; - // Expect 2 calls, make two calls. + // Expect 2 calls, make two calls. If kDecodeFinished is set, the buffer is + // not even examined. EXPECT_CALL(*mock_decoder, EnqueueEmptyFrame()).Times(3); - mock_decoder->OnDecode(NULL); - mock_decoder->OnDecode(buffer_); - mock_decoder->OnDecode(end_of_stream_buffer_); + + // Setup callbacks to be executed 3 times. + TaskMocker done_cb; + EXPECT_CALL(done_cb, Run()).Times(3); + + mock_decoder->DoDecode(NULL, done_cb.CreateTask()); + mock_decoder->DoDecode(buffer_, done_cb.CreateTask()); + mock_decoder->DoDecode(end_of_stream_buffer_, done_cb.CreateTask()); + EXPECT_EQ(FFmpegVideoDecoder::kDecodeFinished, mock_decoder->state_); } -TEST_F(FFmpegVideoDecoderTest, OnSeek) { - // Simulates receiving a call to OnSeek() while in every possible state. In +TEST_F(FFmpegVideoDecoderTest, DoSeek) { + // Simulates receiving a call to DoSeek() while in every possible state. In // every case, it should clear the timestamp queue, flush the decoder and // reset the state to kNormal. - scoped_refptr<DecoderPrivateMock> mock_decoder = - new StrictMock<DecoderPrivateMock>(); - mock_decoder->codec_context_ = &codec_context_; - const base::TimeDelta kZero; const FFmpegVideoDecoder::DecoderState kStates[] = { FFmpegVideoDecoder::kNormal, @@ -528,6 +476,13 @@ TEST_F(FFmpegVideoDecoderTest, OnSeek) { }; for (size_t i = 0; i < ARRAYSIZE_UNSAFE(kStates); ++i) { + SCOPED_TRACE(Message() << "Iteration " << i); + + MockVideoDecodeEngine* mock_engine = + new StrictMock<MockVideoDecodeEngine>(); + scoped_refptr<DecoderPrivateMock> mock_decoder = + new StrictMock<DecoderPrivateMock>(mock_engine); + // Push in some timestamps. mock_decoder->pts_heap_.Push(kTestPts1.timestamp); mock_decoder->pts_heap_.Push(kTestPts2.timestamp); @@ -535,10 +490,14 @@ TEST_F(FFmpegVideoDecoderTest, OnSeek) { // Expect a flush. mock_decoder->state_ = kStates[i]; - EXPECT_CALL(mock_ffmpeg_, AVCodecFlushBuffers(&codec_context_)); + EXPECT_CALL(*mock_engine, Flush(_)) + .WillOnce(WithArg<0>(InvokeRunnable())); + + TaskMocker done_cb; + EXPECT_CALL(done_cb, Run()).Times(1); // Seek and verify the results. - mock_decoder->OnSeek(kZero); + mock_decoder->DoSeek(kZero, done_cb.CreateTask()); EXPECT_TRUE(mock_decoder->pts_heap_.IsEmpty()); EXPECT_EQ(FFmpegVideoDecoder::kNormal, mock_decoder->state_); } diff --git a/media/filters/video_decode_engine.h b/media/filters/video_decode_engine.h new file mode 100644 index 0000000..f7db194 --- /dev/null +++ b/media/filters/video_decode_engine.h @@ -0,0 +1,57 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. Use of this +// source code is governed by a BSD-style license that can be found in the +// LICENSE file. + +#ifndef MEDIA_FILTERS_VIDEO_DECODE_ENGINE_H_ +#define MEDIA_FILTERS_VIDEO_DECODE_ENGINE_H_ + +// FFmpeg types. +// +// TODO(ajwong): Try to cut the dependency on the FFmpeg types. +struct AVFrame; +struct AVStream; + +class Task; + +namespace media { + +class Buffer; + +class VideoDecodeEngine { + public: + enum State { + kCreated, + kNormal, + kError, + }; + + VideoDecodeEngine() {} + virtual ~VideoDecodeEngine() {} + + // Initialized the engine. On successful Initialization, state() should + // return kNormal. + virtual void Initialize(AVStream* stream, Task* done_cb) = 0; + + // Decodes one frame of video with the given buffer. Returns false if there + // was a decode error, or a zero-byte frame was produced. + // + // TODO(ajwong): Should this function just allocate a new yuv_frame and return + // it via a "GetNextFrame()" method or similar? + virtual void DecodeFrame(const Buffer& buffer, AVFrame* yuv_frame, + bool* got_result, Task* done_cb) = 0; + + // Flushes the decode engine of any buffered input packets. + virtual void Flush(Task* done_cb) = 0; + + // Returns the VideoSurface::Format of the resulting |yuv_frame| from + // DecodeFrame(). + virtual VideoSurface::Format GetSurfaceFormat() const = 0; + + // Returns the current state of the decode engine. + virtual State state() const = 0; + +}; + +} // namespace media + +#endif // MEDIA_FILTERS_VIDEO_DECODE_ENGINE_H_ diff --git a/media/media.gyp b/media/media.gyp index ef7d524..bc071f4 100644 --- a/media/media.gyp +++ b/media/media.gyp @@ -52,6 +52,7 @@ 'base/buffer_queue.h', 'base/buffers.cc', 'base/buffers.h', + 'base/callback.h', 'base/clock.h', 'base/clock_impl.cc', 'base/clock_impl.h', @@ -85,6 +86,8 @@ 'base/yuv_row_mac.cc', 'base/yuv_row_linux.cc', 'base/yuv_row.h', + 'ffmpeg/ffmpeg_util.cc', + 'ffmpeg/ffmpeg_util.h', 'filters/audio_renderer_algorithm_base.cc', 'filters/audio_renderer_algorithm_base.h', 'filters/audio_renderer_algorithm_default.cc', @@ -112,6 +115,7 @@ 'filters/file_data_source.h', 'filters/null_audio_renderer.cc', 'filters/null_audio_renderer.h', + 'filters/video_decode_engine.h', 'filters/video_renderer_base.cc', 'filters/video_renderer_base.h', ], @@ -182,6 +186,7 @@ 'base/mock_filters.cc', 'base/mock_filters.h', 'base/mock_reader.h', + 'base/mock_task.h', 'base/pipeline_impl_unittest.cc', 'base/pts_heap_unittest.cc', 'base/run_all_unittests.cc', @@ -192,6 +197,7 @@ 'filters/audio_renderer_base_unittest.cc', 'filters/ffmpeg_demuxer_unittest.cc', 'filters/ffmpeg_glue_unittest.cc', + 'filters/ffmpeg_video_decode_engine_unittest.cc', 'filters/ffmpeg_video_decoder_unittest.cc', 'filters/file_data_source_unittest.cc', 'filters/video_renderer_base_unittest.cc', |