// Copyright (c) 2011 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/memory/scoped_ptr.h" #include "base/message_loop.h" #include "media/base/data_buffer.h" #include "media/base/pipeline.h" #include "media/base/test_data_util.h" #include "media/filters/ffmpeg_glue.h" #include "media/video/ffmpeg_video_decode_engine.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::SaveArg; using ::testing::SetArgumentPointee; using ::testing::StrictMock; namespace media { static const gfx::Size kCodedSize(320, 240); static const gfx::Rect kVisibleRect(320, 240); static const gfx::Size kNaturalSize(522, 288); static const AVRational kFrameRate = { 100, 1 }; ACTION_P2(DemuxComplete, engine, buffer) { engine->ConsumeVideoSample(buffer); } class FFmpegVideoDecodeEngineTest : public testing::Test, public VideoDecodeEngine::EventHandler { public: FFmpegVideoDecodeEngineTest() : config_(kCodecVP8, kCodedSize, kVisibleRect, kNaturalSize, kFrameRate.num, kFrameRate.den, NULL, 0) { CHECK(FFmpegGlue::GetInstance()); // Setup FFmpeg structures. frame_buffer_.reset(new uint8[kCodedSize.GetArea()]); test_engine_.reset(new FFmpegVideoDecodeEngine()); ReadTestDataFile("vp8-I-frame-320x240", &i_frame_buffer_); ReadTestDataFile("vp8-corrupt-I-frame", &corrupt_i_frame_buffer_); end_of_stream_buffer_ = new DataBuffer(0); } ~FFmpegVideoDecodeEngineTest() { test_engine_.reset(); } void Initialize() { VideoCodecInfo info; EXPECT_CALL(*this, OnInitializeComplete(_)) .WillOnce(SaveArg<0>(&info)); test_engine_->Initialize(MessageLoop::current(), this, NULL, config_); EXPECT_TRUE(info.success); } // Decodes the single compressed frame in |buffer| and writes the // uncompressed output to |video_frame|. This method works with single // and multithreaded decoders. End of stream buffers are used to trigger // the frame to be returned in the multithreaded decoder case. void DecodeASingleFrame(const scoped_refptr<Buffer>& buffer, scoped_refptr<VideoFrame>* video_frame) { EXPECT_CALL(*this, ProduceVideoSample(_)) .WillOnce(DemuxComplete(test_engine_.get(), buffer)) .WillRepeatedly(DemuxComplete(test_engine_.get(), end_of_stream_buffer_)); EXPECT_CALL(*this, ConsumeVideoFrame(_, _)) .WillOnce(SaveArg<0>(video_frame)); CallProduceVideoFrame(); } // Decodes |i_frame_buffer_| and then decodes the data contained in // the file named |test_file_name|. This function expects both buffers // to decode to frames that are the same size. void DecodeIFrameThenTestFile(const std::string& test_file_name) { Initialize(); scoped_refptr<VideoFrame> video_frame_a; scoped_refptr<VideoFrame> video_frame_b; scoped_refptr<Buffer> buffer; ReadTestDataFile(test_file_name, &buffer); EXPECT_CALL(*this, ProduceVideoSample(_)) .WillOnce(DemuxComplete(test_engine_.get(), i_frame_buffer_)) .WillOnce(DemuxComplete(test_engine_.get(), buffer)) .WillRepeatedly(DemuxComplete(test_engine_.get(), end_of_stream_buffer_)); EXPECT_CALL(*this, ConsumeVideoFrame(_, _)) .WillOnce(SaveArg<0>(&video_frame_a)) .WillOnce(SaveArg<0>(&video_frame_b)); CallProduceVideoFrame(); CallProduceVideoFrame(); size_t expected_width = static_cast<size_t>(kVisibleRect.width()); size_t expected_height = static_cast<size_t>(kVisibleRect.height()); EXPECT_EQ(expected_width, video_frame_a->width()); EXPECT_EQ(expected_height, video_frame_a->height()); EXPECT_EQ(expected_width, video_frame_b->width()); EXPECT_EQ(expected_height, video_frame_b->height()); } // VideoDecodeEngine::EventHandler implementation. MOCK_METHOD2(ConsumeVideoFrame, void(scoped_refptr<VideoFrame> video_frame, const PipelineStatistics& statistics)); MOCK_METHOD1(ProduceVideoSample, void(scoped_refptr<Buffer> buffer)); MOCK_METHOD1(OnInitializeComplete, void(const VideoCodecInfo& info)); MOCK_METHOD0(OnUninitializeComplete, void()); MOCK_METHOD0(OnFlushComplete, void()); MOCK_METHOD0(OnSeekComplete, void()); MOCK_METHOD0(OnError, void()); void CallProduceVideoFrame() { test_engine_->ProduceVideoFrame(VideoFrame::CreateFrame( VideoFrame::YV12, kVisibleRect.width(), kVisibleRect.height(), kNoTimestamp, kNoTimestamp)); } protected: VideoDecoderConfig config_; scoped_ptr<FFmpegVideoDecodeEngine> test_engine_; scoped_array<uint8_t> frame_buffer_; scoped_refptr<Buffer> i_frame_buffer_; scoped_refptr<Buffer> corrupt_i_frame_buffer_; scoped_refptr<Buffer> end_of_stream_buffer_; private: DISALLOW_COPY_AND_ASSIGN(FFmpegVideoDecodeEngineTest); }; TEST_F(FFmpegVideoDecodeEngineTest, Initialize_Normal) { Initialize(); } TEST_F(FFmpegVideoDecodeEngineTest, Initialize_FindDecoderFails) { VideoDecoderConfig config(kUnknownVideoCodec, kCodedSize, kVisibleRect, kNaturalSize, kFrameRate.num, kFrameRate.den, NULL, 0); // Test avcodec_find_decoder() returning NULL. VideoCodecInfo info; EXPECT_CALL(*this, OnInitializeComplete(_)) .WillOnce(SaveArg<0>(&info)); test_engine_->Initialize(MessageLoop::current(), this, NULL, config); EXPECT_FALSE(info.success); } TEST_F(FFmpegVideoDecodeEngineTest, Initialize_OpenDecoderFails) { // Specify Theora w/o extra data so that avcodec_open() fails. VideoDecoderConfig config(kCodecTheora, kCodedSize, kVisibleRect, kNaturalSize, kFrameRate.num, kFrameRate.den, NULL, 0); VideoCodecInfo info; EXPECT_CALL(*this, OnInitializeComplete(_)) .WillOnce(SaveArg<0>(&info)); test_engine_->Initialize(MessageLoop::current(), this, NULL, config); EXPECT_FALSE(info.success); } TEST_F(FFmpegVideoDecodeEngineTest, DecodeFrame_Normal) { Initialize(); // We rely on FFmpeg for timestamp and duration reporting. const base::TimeDelta kTimestamp = base::TimeDelta::FromMicroseconds(0); const base::TimeDelta kDuration = base::TimeDelta::FromMicroseconds(10000); // Simulate decoding a single frame. scoped_refptr<VideoFrame> video_frame; DecodeASingleFrame(i_frame_buffer_, &video_frame); // |video_frame| timestamp is 0 because we set the timestamp based off // the buffer timestamp. ASSERT_TRUE(video_frame); EXPECT_EQ(0, video_frame->GetTimestamp().ToInternalValue()); EXPECT_EQ(kDuration.ToInternalValue(), video_frame->GetDuration().ToInternalValue()); } // Verify current behavior for 0 byte frames. FFmpeg simply ignores // the 0 byte frames. TEST_F(FFmpegVideoDecodeEngineTest, DecodeFrame_0ByteFrame) { Initialize(); scoped_refptr<DataBuffer> zero_byte_buffer = new DataBuffer(1); scoped_refptr<VideoFrame> video_frame_a; scoped_refptr<VideoFrame> video_frame_b; scoped_refptr<VideoFrame> video_frame_c; EXPECT_CALL(*this, ProduceVideoSample(_)) .WillOnce(DemuxComplete(test_engine_.get(), i_frame_buffer_)) .WillOnce(DemuxComplete(test_engine_.get(), zero_byte_buffer)) .WillOnce(DemuxComplete(test_engine_.get(), i_frame_buffer_)) .WillRepeatedly(DemuxComplete(test_engine_.get(), end_of_stream_buffer_)); EXPECT_CALL(*this, ConsumeVideoFrame(_, _)) .WillOnce(SaveArg<0>(&video_frame_a)) .WillOnce(SaveArg<0>(&video_frame_b)) .WillOnce(SaveArg<0>(&video_frame_c)); CallProduceVideoFrame(); CallProduceVideoFrame(); CallProduceVideoFrame(); EXPECT_TRUE(video_frame_a); EXPECT_TRUE(video_frame_b); EXPECT_FALSE(video_frame_c); } TEST_F(FFmpegVideoDecodeEngineTest, DecodeFrame_DecodeError) { Initialize(); EXPECT_CALL(*this, ProduceVideoSample(_)) .WillOnce(DemuxComplete(test_engine_.get(), corrupt_i_frame_buffer_)) .WillRepeatedly(DemuxComplete(test_engine_.get(), i_frame_buffer_)); EXPECT_CALL(*this, OnError()); CallProduceVideoFrame(); } // Multi-threaded decoders have different behavior than single-threaded // decoders at the end of the stream. Multithreaded decoders hide errors // that happen on the last |codec_context_->thread_count| frames to avoid // prematurely signalling EOS. This test just exposes that behavior so we can // detect if it changes. TEST_F(FFmpegVideoDecodeEngineTest, DecodeFrame_DecodeErrorAtEndOfStream) { Initialize(); EXPECT_CALL(*this, ProduceVideoSample(_)) .WillOnce(DemuxComplete(test_engine_.get(), corrupt_i_frame_buffer_)) .WillRepeatedly(DemuxComplete(test_engine_.get(), end_of_stream_buffer_)); scoped_refptr<VideoFrame> video_frame; EXPECT_CALL(*this, ConsumeVideoFrame(_, _)) .WillOnce(SaveArg<0>(&video_frame)); CallProduceVideoFrame(); EXPECT_FALSE(video_frame); } // Decode |i_frame_buffer_| and then a frame with a larger width and verify // the output size didn't change. // TODO(acolwell): Fix InvalidRead detected by Valgrind //TEST_F(FFmpegVideoDecodeEngineTest, DecodeFrame_LargerWidth) { // DecodeIFrameThenTestFile("vp8-I-frame-640x240"); //} // Decode |i_frame_buffer_| and then a frame with a smaller width and verify // the output size didn't change. TEST_F(FFmpegVideoDecodeEngineTest, DecodeFrame_SmallerWidth) { DecodeIFrameThenTestFile("vp8-I-frame-160x240"); } // Decode |i_frame_buffer_| and then a frame with a larger height and verify // the output size didn't change. // TODO(acolwell): Fix InvalidRead detected by Valgrind //TEST_F(FFmpegVideoDecodeEngineTest, DecodeFrame_LargerHeight) { // DecodeIFrameThenTestFile("vp8-I-frame-320x480"); //} // Decode |i_frame_buffer_| and then a frame with a smaller height and verify // the output size didn't change. TEST_F(FFmpegVideoDecodeEngineTest, DecodeFrame_SmallerHeight) { DecodeIFrameThenTestFile("vp8-I-frame-320x120"); } } // namespace media