root/media/filters/ffmpeg_video_decoder_unittest.cc

/* [<][>][^][v][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. ACTION_P
  2. decode_cb_
  3. Initialize
  4. InitializeWithConfigAndStatus
  5. InitializeWithConfig
  6. Reinitialize
  7. Reset
  8. Stop
  9. EnterDecodingState
  10. EnterEndOfStreamState
  11. DecodeMultipleFrames
  12. DecodeSingleFrame
  13. DecodeIFrameThenTestFile
  14. Decode
  15. TEST_F
  16. TEST_F
  17. TEST_F
  18. TEST_F
  19. TEST_F
  20. TEST_F
  21. TEST_F
  22. TEST_F
  23. TEST_F
  24. TEST_F
  25. TEST_F
  26. TEST_F
  27. TEST_F
  28. TEST_F
  29. TEST_F
  30. TEST_F
  31. TEST_F
  32. TEST_F
  33. TEST_F
  34. TEST_F
  35. TEST_F
  36. TEST_F
  37. TEST_F
  38. TEST_F
  39. TEST_F
  40. TEST_F
  41. TEST_F
  42. TEST_F

// Copyright (c) 2012 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 <string>
#include <vector>

#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/memory/singleton.h"
#include "base/message_loop/message_loop.h"
#include "base/strings/string_util.h"
#include "media/base/decoder_buffer.h"
#include "media/base/gmock_callback_support.h"
#include "media/base/limits.h"
#include "media/base/mock_filters.h"
#include "media/base/test_data_util.h"
#include "media/base/test_helpers.h"
#include "media/base/video_decoder.h"
#include "media/base/video_frame.h"
#include "media/base/video_util.h"
#include "media/ffmpeg/ffmpeg_common.h"
#include "media/filters/ffmpeg_glue.h"
#include "media/filters/ffmpeg_video_decoder.h"
#include "testing/gmock/include/gmock/gmock.h"

using ::testing::_;
using ::testing::AtLeast;
using ::testing::InSequence;
using ::testing::IsNull;
using ::testing::Return;
using ::testing::SaveArg;
using ::testing::StrictMock;

namespace media {

static const VideoFrame::Format kVideoFormat = VideoFrame::YV12;
static const gfx::Size kCodedSize(320, 240);
static const gfx::Rect kVisibleRect(320, 240);
static const gfx::Size kNaturalSize(320, 240);

ACTION_P(ReturnBuffer, buffer) {
  arg0.Run(buffer.get() ? DemuxerStream::kOk : DemuxerStream::kAborted, buffer);
}

class FFmpegVideoDecoderTest : public testing::Test {
 public:
  FFmpegVideoDecoderTest()
      : decoder_(new FFmpegVideoDecoder(message_loop_.message_loop_proxy())),
        decode_cb_(base::Bind(&FFmpegVideoDecoderTest::FrameReady,
                              base::Unretained(this))) {
    FFmpegGlue::InitializeFFmpeg();

    // Initialize various test buffers.
    frame_buffer_.reset(new uint8[kCodedSize.GetArea()]);
    end_of_stream_buffer_ = DecoderBuffer::CreateEOSBuffer();
    i_frame_buffer_ = ReadTestDataFile("vp8-I-frame-320x240");
    corrupt_i_frame_buffer_ = ReadTestDataFile("vp8-corrupt-I-frame");
  }

  virtual ~FFmpegVideoDecoderTest() {
    Stop();
  }

  void Initialize() {
    InitializeWithConfig(TestVideoConfig::Normal());
  }

  void InitializeWithConfigAndStatus(const VideoDecoderConfig& config,
                                     PipelineStatus status) {
    decoder_->Initialize(config, NewExpectedStatusCB(status));
    message_loop_.RunUntilIdle();
  }

  void InitializeWithConfig(const VideoDecoderConfig& config) {
    InitializeWithConfigAndStatus(config, PIPELINE_OK);
  }

  void Reinitialize() {
    InitializeWithConfig(TestVideoConfig::Large());
  }

  void Reset() {
    decoder_->Reset(NewExpectedClosure());
    message_loop_.RunUntilIdle();
  }

  void Stop() {
    decoder_->Stop();
    message_loop_.RunUntilIdle();
  }

  // Sets up expectations and actions to put FFmpegVideoDecoder in an active
  // decoding state.
  void EnterDecodingState() {
    VideoDecoder::Status status;
    scoped_refptr<VideoFrame> video_frame;
    DecodeSingleFrame(i_frame_buffer_, &status, &video_frame);

    EXPECT_EQ(VideoDecoder::kOk, status);
    ASSERT_TRUE(video_frame.get());
    EXPECT_FALSE(video_frame->end_of_stream());
  }

  // Sets up expectations and actions to put FFmpegVideoDecoder in an end
  // of stream state.
  void EnterEndOfStreamState() {
    VideoDecoder::Status status;
    scoped_refptr<VideoFrame> video_frame;
    DecodeSingleFrame(end_of_stream_buffer_, &status, &video_frame);
    EXPECT_EQ(VideoDecoder::kOk, status);
    ASSERT_TRUE(video_frame.get());
    EXPECT_TRUE(video_frame->end_of_stream());
  }

  typedef std::vector<scoped_refptr<DecoderBuffer> > InputBuffers;
  typedef std::vector<scoped_refptr<VideoFrame> > OutputFrames;

  // Decodes all buffers in |input_buffers| and push all successfully decoded
  // output frames (excluding EOS frames) into |output_frames|.
  // Returns the last decode status returned by the decoder.
  VideoDecoder::Status DecodeMultipleFrames(const InputBuffers& input_buffers,
                                            OutputFrames* output_frames) {
    InputBuffers::const_iterator input_iter = input_buffers.begin();

    for (;;) {
      // Prepare input buffer.
      scoped_refptr<DecoderBuffer> buffer;
      if (input_iter != input_buffers.end()) {
        buffer = *input_iter;
        ++input_iter;
      } else {
        buffer = end_of_stream_buffer_;
      }

      VideoDecoder::Status status;
      scoped_refptr<VideoFrame> frame;
      Decode(buffer, &status, &frame);

      switch (status) {
        case VideoDecoder::kOk:
          DCHECK(frame);
          if (!frame->end_of_stream()) {
            output_frames->push_back(frame);
            continue;
          } else {  // EOS
            return status;
          }
        case VideoDecoder::kNotEnoughData:
          DCHECK(!frame);
          continue;
        case VideoDecoder::kAborted:
          NOTREACHED();
        case VideoDecoder::kDecodeError:
        case VideoDecoder::kDecryptError:
          DCHECK(!frame);
          return status;
      }
    }
  }

  // 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 DecodeSingleFrame(const scoped_refptr<DecoderBuffer>& buffer,
                         VideoDecoder::Status* status,
                         scoped_refptr<VideoFrame>* video_frame) {
    InputBuffers input_buffers;
    input_buffers.push_back(buffer);

    OutputFrames output_frames;
    *status = DecodeMultipleFrames(input_buffers, &output_frames);

    if (*status != VideoDecoder::kOk)
      return;

    ASSERT_LE(output_frames.size(), 1U);
    if (output_frames.size() == 1U)
      *video_frame = output_frames[0];
    else
      *video_frame = VideoFrame::CreateEOSFrame();
  }

  // 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,
                                int expected_width,
                                int expected_height) {
    Initialize();
    scoped_refptr<DecoderBuffer> buffer = ReadTestDataFile(test_file_name);

    InputBuffers input_buffers;
    input_buffers.push_back(i_frame_buffer_);
    input_buffers.push_back(buffer);

    OutputFrames output_frames;
    VideoDecoder::Status status =
        DecodeMultipleFrames(input_buffers, &output_frames);

    EXPECT_EQ(VideoDecoder::kOk, status);
    ASSERT_EQ(2U, output_frames.size());

    gfx::Size original_size = kVisibleRect.size();
    EXPECT_EQ(original_size.width(),
              output_frames[0]->visible_rect().size().width());
    EXPECT_EQ(original_size.height(),
              output_frames[0]->visible_rect().size().height());
    EXPECT_EQ(expected_width,
              output_frames[1]->visible_rect().size().width());
    EXPECT_EQ(expected_height,
              output_frames[1]->visible_rect().size().height());
  }

  void Decode(const scoped_refptr<DecoderBuffer>& buffer,
              VideoDecoder::Status* status,
              scoped_refptr<VideoFrame>* video_frame) {
    EXPECT_CALL(*this, FrameReady(_, _))
        .WillOnce(DoAll(SaveArg<0>(status), SaveArg<1>(video_frame)));

    decoder_->Decode(buffer, decode_cb_);

    message_loop_.RunUntilIdle();
  }

  MOCK_METHOD2(FrameReady, void(VideoDecoder::Status,
                                const scoped_refptr<VideoFrame>&));

  base::MessageLoop message_loop_;
  scoped_ptr<FFmpegVideoDecoder> decoder_;

  VideoDecoder::DecodeCB decode_cb_;

  // Various buffers for testing.
  scoped_ptr<uint8_t[]> frame_buffer_;
  scoped_refptr<DecoderBuffer> end_of_stream_buffer_;
  scoped_refptr<DecoderBuffer> i_frame_buffer_;
  scoped_refptr<DecoderBuffer> corrupt_i_frame_buffer_;

 private:
  DISALLOW_COPY_AND_ASSIGN(FFmpegVideoDecoderTest);
};

TEST_F(FFmpegVideoDecoderTest, Initialize_Normal) {
  Initialize();
}

TEST_F(FFmpegVideoDecoderTest, Initialize_UnsupportedDecoder) {
  // Test avcodec_find_decoder() returning NULL.
  InitializeWithConfigAndStatus(TestVideoConfig::Invalid(),
                                DECODER_ERROR_NOT_SUPPORTED);
}

TEST_F(FFmpegVideoDecoderTest, Initialize_UnsupportedPixelFormat) {
  // Ensure decoder handles unsupported pixel formats without crashing.
  VideoDecoderConfig config(kCodecVP8, VIDEO_CODEC_PROFILE_UNKNOWN,
                            VideoFrame::UNKNOWN,
                            kCodedSize, kVisibleRect, kNaturalSize,
                            NULL, 0, false);
  InitializeWithConfigAndStatus(config, DECODER_ERROR_NOT_SUPPORTED);
}

TEST_F(FFmpegVideoDecoderTest, Initialize_OpenDecoderFails) {
  // Specify Theora w/o extra data so that avcodec_open2() fails.
  VideoDecoderConfig config(kCodecTheora, VIDEO_CODEC_PROFILE_UNKNOWN,
                            kVideoFormat,
                            kCodedSize, kVisibleRect, kNaturalSize,
                            NULL, 0, false);
  InitializeWithConfigAndStatus(config, DECODER_ERROR_NOT_SUPPORTED);
}

TEST_F(FFmpegVideoDecoderTest, Initialize_AspectRatioNumeratorZero) {
  gfx::Size natural_size = GetNaturalSize(kVisibleRect.size(), 0, 1);
  VideoDecoderConfig config(kCodecVP8, VP8PROFILE_MAIN,
                            kVideoFormat,
                            kCodedSize, kVisibleRect, natural_size,
                            NULL, 0, false);
  InitializeWithConfigAndStatus(config, DECODER_ERROR_NOT_SUPPORTED);
}

TEST_F(FFmpegVideoDecoderTest, Initialize_AspectRatioDenominatorZero) {
  gfx::Size natural_size = GetNaturalSize(kVisibleRect.size(), 1, 0);
  VideoDecoderConfig config(kCodecVP8, VP8PROFILE_MAIN,
                            kVideoFormat,
                            kCodedSize, kVisibleRect, natural_size,
                            NULL, 0, false);
  InitializeWithConfigAndStatus(config, DECODER_ERROR_NOT_SUPPORTED);
}

TEST_F(FFmpegVideoDecoderTest, Initialize_AspectRatioNumeratorNegative) {
  gfx::Size natural_size = GetNaturalSize(kVisibleRect.size(), -1, 1);
  VideoDecoderConfig config(kCodecVP8, VP8PROFILE_MAIN,
                            kVideoFormat,
                            kCodedSize, kVisibleRect, natural_size,
                            NULL, 0, false);
  InitializeWithConfigAndStatus(config, DECODER_ERROR_NOT_SUPPORTED);
}

TEST_F(FFmpegVideoDecoderTest, Initialize_AspectRatioDenominatorNegative) {
  gfx::Size natural_size = GetNaturalSize(kVisibleRect.size(), 1, -1);
  VideoDecoderConfig config(kCodecVP8, VP8PROFILE_MAIN,
                            kVideoFormat,
                            kCodedSize, kVisibleRect, natural_size,
                            NULL, 0, false);
  InitializeWithConfigAndStatus(config, DECODER_ERROR_NOT_SUPPORTED);
}

TEST_F(FFmpegVideoDecoderTest, Initialize_AspectRatioNumeratorTooLarge) {
  int width = kVisibleRect.size().width();
  int num = ceil(static_cast<double>(limits::kMaxDimension + 1) / width);
  gfx::Size natural_size = GetNaturalSize(kVisibleRect.size(), num, 1);
  VideoDecoderConfig config(kCodecVP8, VP8PROFILE_MAIN,
                            kVideoFormat,
                            kCodedSize, kVisibleRect, natural_size,
                            NULL, 0, false);
  InitializeWithConfigAndStatus(config, DECODER_ERROR_NOT_SUPPORTED);
}

TEST_F(FFmpegVideoDecoderTest, Initialize_AspectRatioDenominatorTooLarge) {
  int den = kVisibleRect.size().width() + 1;
  gfx::Size natural_size = GetNaturalSize(kVisibleRect.size(), 1, den);
  VideoDecoderConfig config(kCodecVP8, VP8PROFILE_MAIN,
                            kVideoFormat,
                            kCodedSize, kVisibleRect, natural_size,
                            NULL, 0, false);
  InitializeWithConfigAndStatus(config, DECODER_ERROR_NOT_SUPPORTED);
}

TEST_F(FFmpegVideoDecoderTest, Reinitialize_Normal) {
  Initialize();
  Reinitialize();
}

TEST_F(FFmpegVideoDecoderTest, Reinitialize_Failure) {
  Initialize();
  InitializeWithConfigAndStatus(TestVideoConfig::Invalid(),
                                DECODER_ERROR_NOT_SUPPORTED);
}

TEST_F(FFmpegVideoDecoderTest, Reinitialize_AfterDecodeFrame) {
  Initialize();
  EnterDecodingState();
  Reinitialize();
}

TEST_F(FFmpegVideoDecoderTest, Reinitialize_AfterReset) {
  Initialize();
  EnterDecodingState();
  Reset();
  Reinitialize();
}

TEST_F(FFmpegVideoDecoderTest, DecodeFrame_Normal) {
  Initialize();

  // Simulate decoding a single frame.
  VideoDecoder::Status status;
  scoped_refptr<VideoFrame> video_frame;
  DecodeSingleFrame(i_frame_buffer_, &status, &video_frame);

  EXPECT_EQ(VideoDecoder::kOk, status);
  ASSERT_TRUE(video_frame.get());
  EXPECT_FALSE(video_frame->end_of_stream());
}

// Verify current behavior for 0 byte frames. FFmpeg simply ignores
// the 0 byte frames.
TEST_F(FFmpegVideoDecoderTest, DecodeFrame_0ByteFrame) {
  Initialize();

  scoped_refptr<DecoderBuffer> zero_byte_buffer = new DecoderBuffer(0);

  InputBuffers input_buffers;
  input_buffers.push_back(i_frame_buffer_);
  input_buffers.push_back(zero_byte_buffer);
  input_buffers.push_back(i_frame_buffer_);

  OutputFrames output_frames;
  VideoDecoder::Status status =
      DecodeMultipleFrames(input_buffers, &output_frames);

  EXPECT_EQ(VideoDecoder::kOk, status);
  ASSERT_EQ(2U, output_frames.size());

  EXPECT_FALSE(output_frames[0]->end_of_stream());
  EXPECT_FALSE(output_frames[1]->end_of_stream());
}

TEST_F(FFmpegVideoDecoderTest, DecodeFrame_DecodeError) {
  Initialize();

  VideoDecoder::Status status;
  scoped_refptr<VideoFrame> frame;

  // The error is only raised on the second decode attempt, so we expect at
  // least one successful decode but we don't expect valid frame to be decoded.
  // During the second decode attempt an error is raised.
  Decode(corrupt_i_frame_buffer_, &status, &frame);
  DCHECK(!frame);
  DCHECK_EQ(VideoDecoder::kNotEnoughData, status);
  Decode(i_frame_buffer_, &status, &frame);
  DCHECK(!frame);
  DCHECK_EQ(VideoDecoder::kDecodeError, status);

  // After a decode error occurred, all following decodes will return
  // kDecodeError.
  Decode(i_frame_buffer_, &status, &frame);
  DCHECK(!frame);
  DCHECK_EQ(VideoDecoder::kDecodeError, status);
}

// 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(FFmpegVideoDecoderTest, DecodeFrame_DecodeErrorAtEndOfStream) {
  Initialize();

  VideoDecoder::Status status;
  scoped_refptr<VideoFrame> video_frame;
  DecodeSingleFrame(corrupt_i_frame_buffer_, &status, &video_frame);

  EXPECT_EQ(VideoDecoder::kOk, status);
  ASSERT_TRUE(video_frame.get());
  EXPECT_TRUE(video_frame->end_of_stream());
}

// Decode |i_frame_buffer_| and then a frame with a larger width and verify
// the output size was adjusted.
TEST_F(FFmpegVideoDecoderTest, DecodeFrame_LargerWidth) {
  DecodeIFrameThenTestFile("vp8-I-frame-640x240", 640, 240);
}

// Decode |i_frame_buffer_| and then a frame with a smaller width and verify
// the output size was adjusted.
TEST_F(FFmpegVideoDecoderTest, DecodeFrame_SmallerWidth) {
  DecodeIFrameThenTestFile("vp8-I-frame-160x240", 160, 240);
}

// Decode |i_frame_buffer_| and then a frame with a larger height and verify
// the output size was adjusted.
TEST_F(FFmpegVideoDecoderTest, DecodeFrame_LargerHeight) {
  DecodeIFrameThenTestFile("vp8-I-frame-320x480", 320, 480);
}

// Decode |i_frame_buffer_| and then a frame with a smaller height and verify
// the output size was adjusted.
TEST_F(FFmpegVideoDecoderTest, DecodeFrame_SmallerHeight) {
  DecodeIFrameThenTestFile("vp8-I-frame-320x120", 320, 120);
}

// Test resetting when decoder has initialized but not decoded.
TEST_F(FFmpegVideoDecoderTest, Reset_Initialized) {
  Initialize();
  Reset();
}

// Test resetting when decoder has decoded single frame.
TEST_F(FFmpegVideoDecoderTest, Reset_Decoding) {
  Initialize();
  EnterDecodingState();
  Reset();
}

// Test resetting when decoder has hit end of stream.
TEST_F(FFmpegVideoDecoderTest, Reset_EndOfStream) {
  Initialize();
  EnterDecodingState();
  EnterEndOfStreamState();
  Reset();
}

// Test stopping when decoder has initialized but not decoded.
TEST_F(FFmpegVideoDecoderTest, Stop_Initialized) {
  Initialize();
  Stop();
}

// Test stopping when decoder has decoded single frame.
TEST_F(FFmpegVideoDecoderTest, Stop_Decoding) {
  Initialize();
  EnterDecodingState();
  Stop();
}

// Test stopping when decoder has hit end of stream.
TEST_F(FFmpegVideoDecoderTest, Stop_EndOfStream) {
  Initialize();
  EnterDecodingState();
  EnterEndOfStreamState();
  Stop();
}

}  // namespace media

/* [<][>][^][v][top][bottom][index][help] */