// Copyright (c) 2010 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/file_path.h" #include "base/file_util.h" #include "base/message_loop.h" #include "base/path_service.h" #include "base/ref_counted.h" #include "base/scoped_ptr.h" #include "base/string_util.h" #include "base/time.h" #include "media/base/data_buffer.h" #include "media/base/video_frame.h" #include "media/tools/mft_h264_example/file_reader_util.h" #include "media/video/mft_h264_decode_engine.h" #include "media/video/mft_h264_decode_engine_context.h" #include "testing/gtest/include/gtest/gtest.h" using base::TimeDelta; namespace media { static const int kDecoderMaxWidth = 1920; static const int kDecoderMaxHeight = 1088; // Helper classes class BaseMftReader : public base::RefCountedThreadSafe { public: virtual ~BaseMftReader() {} virtual void ReadCallback(scoped_refptr* input) = 0; }; class FakeMftReader : public BaseMftReader { public: FakeMftReader() : frames_remaining_(20) {} explicit FakeMftReader(int count) : frames_remaining_(count) {} virtual ~FakeMftReader() {} // Provides garbage input to the decoder. virtual void ReadCallback(scoped_refptr* input) { if (frames_remaining_ > 0) { int sz = 4096; uint8* buf = new uint8[sz]; memset(buf, 42, sz); *input = new DataBuffer(buf, sz); (*input)->SetDuration(base::TimeDelta::FromMicroseconds(5000)); (*input)->SetTimestamp( base::TimeDelta::FromMicroseconds( 50000000 - frames_remaining_ * 10000)); --frames_remaining_; } else { // Emulate end of stream on the last "frame". *input = new DataBuffer(0); } } int frames_remaining() const { return frames_remaining_; } private: int frames_remaining_; }; class SimpleMftH264DecodeEngineHandler : public VideoDecodeEngine::EventHandler { public: SimpleMftH264DecodeEngineHandler() : init_count_(0), uninit_count_(0), flush_count_(0), format_change_count_(0), empty_buffer_callback_count_(0), fill_buffer_callback_count_(0) { memset(&info_, 0, sizeof(info_)); } virtual ~SimpleMftH264DecodeEngineHandler() {} virtual void OnInitializeComplete(const VideoCodecInfo& info) { info_ = info; init_count_++; } virtual void OnUninitializeComplete() { uninit_count_++; } virtual void OnFlushComplete() { flush_count_++; } virtual void OnSeekComplete() {} virtual void OnError() {} virtual void OnFormatChange(VideoStreamInfo stream_info) { format_change_count_++; info_.stream_info = stream_info; } virtual void ProduceVideoSample(scoped_refptr buffer) { if (reader_.get() && decoder_) { empty_buffer_callback_count_++; scoped_refptr input; reader_->ReadCallback(&input); decoder_->ConsumeVideoSample(input); } } virtual void ConsumeVideoFrame(scoped_refptr frame) { fill_buffer_callback_count_++; current_frame_ = frame; } void SetReader(scoped_refptr reader) { reader_ = reader; } void SetDecodeEngine(MftH264DecodeEngine* decoder) { decoder_ = decoder; } int init_count_; int uninit_count_; int flush_count_; int format_change_count_; int empty_buffer_callback_count_; int fill_buffer_callback_count_; VideoCodecInfo info_; scoped_refptr reader_; MftH264DecodeEngine* decoder_; scoped_refptr current_frame_; }; class FFmpegFileReaderWrapper : public BaseMftReader { public: FFmpegFileReaderWrapper() {} virtual ~FFmpegFileReaderWrapper() {} bool InitReader(const std::string& filename) { reader_.reset(new FFmpegFileReader(filename)); if (!reader_.get() || !reader_->Initialize()) { reader_.reset(); return false; } return true; } virtual void ReadCallback(scoped_refptr* input) { if (reader_.get()) { reader_->Read(input); } } bool GetWidth(int* width) { if (!reader_.get()) return false; return reader_->GetWidth(width); } bool GetHeight(int* height) { if (!reader_.get()) return false; return reader_->GetHeight(height); } scoped_ptr reader_; }; // Helper functions static FilePath GetVideoFilePath(const std::string& file_name) { FilePath path; PathService::Get(base::DIR_SOURCE_ROOT, &path); path = path.AppendASCII("media") .AppendASCII("test") .AppendASCII("data") .AppendASCII(file_name.c_str()); return path; } class MftH264DecodeEngineTest : public testing::Test { protected: MftH264DecodeEngineTest() : loop_(), window_(NULL), handler_(NULL), engine_(NULL), context_(NULL) { } virtual ~MftH264DecodeEngineTest() {} virtual void SetUp() { handler_.reset(new SimpleMftH264DecodeEngineHandler()); } virtual void TearDown() { if (context_.get()) { context_->ReleaseAllVideoFrames(); context_->Destroy(NULL); } if (window_) DestroyWindow(window_); } void GetDecodeEngine(bool dxva) { if (dxva) { if (!window_) CreateDrawWindow(); context_.reset(new MftH264DecodeEngineContext(window_)); ASSERT_TRUE(context_.get()); context_->Initialize(NULL); ASSERT_TRUE(context_->initialized()); } engine_.reset(new MftH264DecodeEngine(dxva)); ASSERT_TRUE(engine_.get()); } void InitDecodeEngine(int width, int height) { VideoCodecConfig config; config.width = width; config.height = height; // Note that although |config| is passed as reference, |config| is copied // into the decode engine, so it is okay to make |config| a local variable. engine_->Initialize(&loop_, handler_.get(), context_.get(), config); EXPECT_EQ(1, handler_->init_count_); EXPECT_EQ(MftH264DecodeEngine::kNormal, engine_->state()); } void InitDecodeEngine() { InitDecodeEngine(800, 600); } void TestInitAndUninit(bool dxva) { GetDecodeEngine(dxva); InitDecodeEngine(); engine_->Uninitialize(); } void DecodeAll(scoped_refptr reader) { handler_->SetReader(reader); handler_->SetDecodeEngine(engine_.get()); while (MftH264DecodeEngine::kStopped != engine_->state()) { scoped_refptr frame; engine_->ProduceVideoFrame(frame); } } void DecodeValidVideo(const std::string& filename, int num_frames, bool dxva) { scoped_refptr reader( new FFmpegFileReaderWrapper()); ASSERT_TRUE(reader.get()); FilePath path = GetVideoFilePath(filename); ASSERT_TRUE(file_util::PathExists(path)); ASSERT_TRUE(reader->InitReader(WideToASCII(path.value()))); int actual_width; int actual_height; ASSERT_TRUE(reader->GetWidth(&actual_width)); ASSERT_TRUE(reader->GetHeight(&actual_height)); VideoCodecConfig config; CreateDrawWindow(config.width, config.height); GetDecodeEngine(dxva); InitDecodeEngine(); DecodeAll(reader); // We expect a format change when decoder receives enough data to determine // the actual frame width/height. EXPECT_GT(handler_->format_change_count_, 0); EXPECT_EQ(actual_width, handler_->info_.stream_info.surface_width); EXPECT_EQ(actual_height, handler_->info_.stream_info.surface_height); EXPECT_GE(handler_->empty_buffer_callback_count_, num_frames); EXPECT_EQ(num_frames, handler_->fill_buffer_callback_count_ - 1); engine_->Uninitialize(); } void ExpectDefaultDimensionsOnInput(int width, int height) { GetDecodeEngine(false); InitDecodeEngine(width, height); EXPECT_EQ(kDecoderMaxWidth, handler_->info_.stream_info.surface_width); EXPECT_EQ(kDecoderMaxHeight, handler_->info_.stream_info.surface_height); engine_->Uninitialize(); } scoped_ptr handler_; scoped_ptr engine_; scoped_ptr context_; private: void CreateDrawWindow(int width, int height) { static const wchar_t kClassName[] = L"Test"; static const wchar_t kWindowTitle[] = L"MFT Unittest Draw Window"; WNDCLASS window_class = {0}; window_class.lpszClassName = kClassName; window_class.hInstance = NULL; window_class.hbrBackground = 0; window_class.lpfnWndProc = DefWindowProc; window_class.hCursor = 0; RegisterClass(&window_class); window_ = CreateWindow(kClassName, kWindowTitle, (WS_OVERLAPPEDWINDOW | WS_VISIBLE) & ~(WS_MAXIMIZEBOX | WS_THICKFRAME), 100, 100, width, height, NULL, NULL, NULL, NULL); ASSERT_TRUE(window_); } void CreateDrawWindow() { CreateDrawWindow(800, 600); } MessageLoop loop_; HWND window_; }; // A simple test case for init/deinit of MF/COM libraries. TEST_F(MftH264DecodeEngineTest, LibraryInit) { EXPECT_TRUE(MftH264DecodeEngine::StartupComLibraries()); MftH264DecodeEngine::ShutdownComLibraries(); } TEST_F(MftH264DecodeEngineTest, DecoderUninitializedAtFirst) { GetDecodeEngine(true); EXPECT_EQ(MftH264DecodeEngine::kUninitialized, engine_->state()); } TEST_F(MftH264DecodeEngineTest, DecoderInitMissingArgs) { VideoCodecConfig config; GetDecodeEngine(false); engine_->Initialize(NULL, NULL, NULL, config); EXPECT_EQ(MftH264DecodeEngine::kUninitialized, engine_->state()); } TEST_F(MftH264DecodeEngineTest, DecoderInitNoDxva) { TestInitAndUninit(false); } TEST_F(MftH264DecodeEngineTest, DecoderInitDxva) { TestInitAndUninit(true); } TEST_F(MftH264DecodeEngineTest, DecoderUninit) { TestInitAndUninit(false); EXPECT_EQ(1, handler_->uninit_count_); EXPECT_EQ(MftH264DecodeEngine::kUninitialized, engine_->state()); } TEST_F(MftH264DecodeEngineTest, UninitBeforeInit) { GetDecodeEngine(false); engine_->Uninitialize(); EXPECT_EQ(0, handler_->uninit_count_); } TEST_F(MftH264DecodeEngineTest, InitWithNegativeDimensions) { ExpectDefaultDimensionsOnInput(-123, -456); } TEST_F(MftH264DecodeEngineTest, InitWithTooHighDimensions) { ExpectDefaultDimensionsOnInput(kDecoderMaxWidth + 1, kDecoderMaxHeight + 1); } TEST_F(MftH264DecodeEngineTest, DrainOnEmptyBuffer) { GetDecodeEngine(false); InitDecodeEngine(); // Decoder should switch to drain mode because of this NULL buffer, and then // switch to kStopped when it says it needs more input during drain mode. scoped_refptr buffer(new DataBuffer(0)); engine_->ConsumeVideoSample(buffer); EXPECT_EQ(MftH264DecodeEngine::kStopped, engine_->state()); // Should have called back with one empty frame. EXPECT_EQ(1, handler_->fill_buffer_callback_count_); ASSERT_TRUE(handler_->current_frame_.get()); EXPECT_EQ(VideoFrame::EMPTY, handler_->current_frame_->format()); engine_->Uninitialize(); } TEST_F(MftH264DecodeEngineTest, NoOutputOnGarbageInput) { // 100 samples of garbage. const int kNumFrames = 100; scoped_refptr reader(new FakeMftReader(kNumFrames)); ASSERT_TRUE(reader.get()); GetDecodeEngine(false); InitDecodeEngine(); DecodeAll(reader); // Output callback should only be invoked once - the empty frame to indicate // end of stream. EXPECT_EQ(1, handler_->fill_buffer_callback_count_); ASSERT_TRUE(handler_->current_frame_.get()); EXPECT_EQ(VideoFrame::EMPTY, handler_->current_frame_->format()); // One extra count because of the end of stream NULL sample. EXPECT_EQ(kNumFrames, handler_->empty_buffer_callback_count_ - 1); engine_->Uninitialize(); } TEST_F(MftH264DecodeEngineTest, FlushAtStart) { GetDecodeEngine(false); InitDecodeEngine(); engine_->Flush(); // Flush should succeed even if input/output are empty. EXPECT_EQ(1, handler_->flush_count_); engine_->Uninitialize(); } TEST_F(MftH264DecodeEngineTest, NoFlushAtStopped) { scoped_refptr reader(new FakeMftReader()); ASSERT_TRUE(reader.get()); GetDecodeEngine(false); InitDecodeEngine(); DecodeAll(reader); EXPECT_EQ(0, handler_->flush_count_); int old_flush_count = handler_->flush_count_; engine_->Flush(); EXPECT_EQ(old_flush_count, handler_->flush_count_); engine_->Uninitialize(); } TEST_F(MftH264DecodeEngineTest, DecodeValidVideoDxva) { DecodeValidVideo("bear.1280x720.mp4", 82, true); } TEST_F(MftH264DecodeEngineTest, DecodeValidVideoNoDxva) { DecodeValidVideo("bear.1280x720.mp4", 82, false); } } // namespace media