media/gpu/image_processor_test: Initial version of image_processor_test

image_processor_test is a test for ImageProcessor. The tested
image processor is platform-dependent and decided on the capability.
The initial version of image_processor_test tests one video frame conversion
from I420 (Memory) to NV12 (Memory).

Bug: 917951
Test: ImageProcessorTest on Linux and Chrome OS
Change-Id: Ie81a6728ab073a164daa18411d45093caf872efb
Reviewed-on: https://chromium-review.googlesource.com/c/1391664
Commit-Queue: Hirokazu Honda <hiroh@chromium.org>
Reviewed-by: David Staessens <dstaessens@chromium.org>
Cr-Commit-Position: refs/heads/master@{#625069}
diff --git a/media/gpu/BUILD.gn b/media/gpu/BUILD.gn
index 9537104..8cde0b7 100644
--- a/media/gpu/BUILD.gn
+++ b/media/gpu/BUILD.gn
@@ -650,3 +650,18 @@
     }
   }
 }
+
+test("image_processor_test") {
+  sources = [
+    "image_processor_test.cc",
+  ]
+  deps = [
+    ":buildflags",
+    ":gpu",
+    "test:image_processor",
+    "//base/test:test_support",
+    "//media:test_support",
+    "//mojo/core/embedder",
+    "//testing/gtest",
+  ]
+}
diff --git a/media/gpu/image_processor_test.cc b/media/gpu/image_processor_test.cc
new file mode 100644
index 0000000..8480373
--- /dev/null
+++ b/media/gpu/image_processor_test.cc
@@ -0,0 +1,129 @@
+// Copyright 2019 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 <tuple>
+
+#include "base/files/file_path.h"
+#include "base/md5.h"
+#include "base/test/launcher/unit_test_launcher.h"
+#include "base/test/test_suite.h"
+#include "build/build_config.h"
+#include "media/base/video_frame.h"
+#include "media/base/video_types.h"
+#include "media/gpu/image_processor.h"
+#include "media/gpu/test/image_processor/image_processor_client.h"
+#include "media/gpu/test/video_image_info.h"
+#include "mojo/core/embedder/embedder.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "ui/gfx/geometry/size.h"
+
+namespace media {
+namespace {
+
+// I420 formatted 320x192 video frame. (bear)
+// TODO(crbug.com/917951): Dynamically load this info from json file.
+constexpr test::VideoImageInfo kI420Image(
+    FILE_PATH_LITERAL("bear_320x192.i420.yuv"),
+    "962820755c74b28f9385fd67219cc04a",
+    PIXEL_FORMAT_I420,
+    gfx::Size(320, 192));
+
+// NV12 formatted 320x192 video frame. (bear)
+// TODO(crbug.com/917951): Dynamically load this info from json file.
+constexpr test::VideoImageInfo kNV12Image(
+    FILE_PATH_LITERAL("bear_320x192.i420.nv12.yuv"),
+    "ce21986434743d3671056719136d46ff",
+    PIXEL_FORMAT_NV12,
+    gfx::Size(320, 192));
+
+class ImageProcessorSimpleParamTest
+    : public ::testing::Test,
+      public ::testing::WithParamInterface<
+          std::tuple<test::VideoImageInfo, test::VideoImageInfo>> {
+ public:
+  // TODO(crbug.com/917951): Initialize Ozone once.
+  void SetUp() override {}
+  void TearDown() override {}
+
+  std::unique_ptr<test::ImageProcessorClient> CreateImageProcessorClient(
+      const test::VideoImageInfo& input_image_info,
+      const test::VideoImageInfo& output_image_info) {
+    // TODO(crbug.com/917951): Pass VideoFrameProcessor.
+    auto input_config_layout = input_image_info.VideoFrameLayout();
+    auto output_config_layout = output_image_info.VideoFrameLayout();
+    LOG_ASSERT(input_config_layout);
+    LOG_ASSERT(output_config_layout);
+    ImageProcessor::PortConfig input_config(*input_config_layout,
+                                            input_image_info.visible_size,
+                                            {VideoFrame::STORAGE_OWNED_MEMORY});
+    ImageProcessor::PortConfig output_config(
+        *output_config_layout, output_image_info.visible_size,
+        {VideoFrame::STORAGE_OWNED_MEMORY});
+    // TODO(crbug.com/917951): Select more appropriate number of buffers.
+    constexpr size_t kNumBuffers = 1;
+    auto ip_client = test::ImageProcessorClient::Create(
+        input_config, output_config, kNumBuffers, true);
+    LOG_ASSERT(ip_client) << "Failed to create ImageProcessorClient";
+    return ip_client;
+  }
+};
+
+TEST_P(ImageProcessorSimpleParamTest, ConvertOneTimeFromMemToMem) {
+  test::VideoImageInfo input_image_info = std::get<0>(GetParam());
+  test::VideoImageInfo output_image_info = std::get<1>(GetParam());
+  auto ip_client =
+      CreateImageProcessorClient(input_image_info, output_image_info);
+
+  ip_client->Process(input_image_info, output_image_info);
+  EXPECT_TRUE(ip_client->WaitUntilNumImageProcessed(1u));
+  EXPECT_EQ(ip_client->GetErrorCount(), 0u);
+  EXPECT_EQ(ip_client->GetNumOfProcessedImages(), 1u);
+
+  // TODO(crbug.com/917951): Replace this checker with VideoFrameProcessor
+  // interface and get results by ImageProcessorClient function like
+  // ImageProcessorClient::GetProcessResults().
+  const auto output_frames = ip_client->GetProcessedImages();
+  ASSERT_EQ(output_frames.size(), 1u);
+  auto processed_frame = output_frames[0];
+  ASSERT_TRUE(processed_frame->IsMappable());
+  base::MD5Context context;
+  base::MD5Init(&context);
+  VideoFrame::HashFrameForTesting(&context, processed_frame);
+  base::MD5Digest digest;
+  base::MD5Final(&digest, &context);
+  std::string expected_md5 = output_image_info.md5sum;
+  std::string computed_md5 = MD5DigestToBase16(digest);
+  EXPECT_EQ(expected_md5, computed_md5);
+};
+
+// I420->NV12
+INSTANTIATE_TEST_CASE_P(ConvertI420ToNV12,
+                        ImageProcessorSimpleParamTest,
+                        ::testing::Values(std::make_tuple(kI420Image,
+                                                          kNV12Image)));
+
+#if defined(OS_CHROMEOS)
+// TODO(hiroh): Add more tests.
+// MEM->DMABUF (V4L2VideoEncodeAccelerator),
+// DMABUF->DMABUF (GpuArcVideoEncodeAccelerator),
+#endif
+
+}  // namespace
+}  // namespace media
+
+int main(int argc, char** argv) {
+  testing::InitGoogleTest(&argc, argv);
+  base::CommandLine::Init(argc, argv);
+  // Using shared memory requires mojo to be initialized (crbug.com/849207).
+  mojo::core::Init();
+  base::ShadowingAtExitManager at_exit_manager;
+
+  // Needed to enable DVLOG through --vmodule.
+  logging::LoggingSettings settings;
+  settings.logging_dest = logging::LOG_TO_SYSTEM_DEBUG_LOG;
+  LOG_ASSERT(logging::InitLogging(settings));
+
+  return RUN_ALL_TESTS();
+}
diff --git a/media/gpu/test/BUILD.gn b/media/gpu/test/BUILD.gn
index f0f94af..8b5b8d4a 100644
--- a/media/gpu/test/BUILD.gn
+++ b/media/gpu/test/BUILD.gn
@@ -140,3 +140,21 @@
     }
   }
 }
+
+static_library("image_processor") {
+  testonly = true
+  sources = [
+    "image_processor/image_processor_client.cc",
+    "image_processor/image_processor_client.h",
+    "video_image_info.h",
+  ]
+  deps = [
+    "//media:test_support",
+    "//media/gpu",
+    "//testing/gtest",
+    "//third_party/libyuv",
+  ]
+  data = [
+    "//media/test/data/",
+  ]
+}
diff --git a/media/gpu/test/image_processor/image_processor_client.cc b/media/gpu/test/image_processor/image_processor_client.cc
new file mode 100644
index 0000000..e54d4aa
--- /dev/null
+++ b/media/gpu/test/image_processor/image_processor_client.cc
@@ -0,0 +1,335 @@
+// Copyright 2019 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/gpu/test/image_processor/image_processor_client.h"
+
+#include <utility>
+
+#include "third_party/libyuv/include/libyuv/planar_functions.h"
+
+#include "base/bind_helpers.h"
+#include "base/files/file_util.h"
+#include "base/files/memory_mapped_file.h"
+#include "base/logging.h"
+#include "base/memory/ptr_util.h"
+#include "base/synchronization/waitable_event.h"
+#include "build/build_config.h"
+#include "media/base/bind_to_current_loop.h"
+#include "media/base/test_data_util.h"
+#include "media/base/video_frame.h"
+#include "media/base/video_frame_layout.h"
+#include "media/gpu/image_processor_factory.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "ui/gfx/geometry/rect.h"
+
+namespace media {
+namespace test {
+
+namespace {
+// TODO(crbug.com/917951): Move these functions to video_frame_helpers.h
+
+// Find the file path for |file_name|.
+base::FilePath GetFilePath(const base::FilePath::CharType* const file_name) {
+  // 1. Try to find |file_name| in the current directory.
+  base::FilePath file_path =
+      base::FilePath(base::FilePath::kCurrentDirectory).Append(file_name);
+  if (base::PathExists(file_path)) {
+    return file_path;
+  }
+
+  // 2. Try media::GetTestDataFilePath(|file_name|), that is,
+  // media/test/data/file_name. This is mainly for Try bot.
+  file_path = media::GetTestDataPath().Append(file_name);
+  LOG_ASSERT(base::PathExists(file_path)) << " Cannot find " << file_name;
+  return file_path;
+}
+
+// Copy |src_frame| into a new VideoFrame with |dst_layout|. The created
+// VideoFrame's content is the same as |src_frame|. Returns nullptr on failure.
+scoped_refptr<VideoFrame> CloneVideoFrameWithLayout(
+    const VideoFrame* const src_frame,
+    const VideoFrameLayout& dst_layout) {
+  LOG_ASSERT(src_frame->IsMappable());
+  LOG_ASSERT(src_frame->format() == dst_layout.format());
+  // Create VideoFrame, which allocates and owns data.
+  auto dst_frame = VideoFrame::CreateFrameWithLayout(
+      dst_layout, src_frame->visible_rect(), src_frame->natural_size(),
+      src_frame->timestamp(), false /* zero_initialize_memory*/);
+  if (!dst_frame) {
+    LOG(ERROR) << "Failed to create VideoFrame";
+    return nullptr;
+  }
+
+  // Copy every plane's content from |src_frame| to |dst_frame|.
+  const size_t num_planes = VideoFrame::NumPlanes(dst_layout.format());
+  LOG_ASSERT(dst_layout.planes().size() == num_planes);
+  LOG_ASSERT(src_frame->layout().planes().size() == num_planes);
+  for (size_t i = 0; i < num_planes; ++i) {
+    libyuv::CopyPlane(
+        src_frame->data(i), src_frame->layout().planes()[i].stride,
+        dst_frame->data(i), dst_frame->layout().planes()[i].stride,
+        VideoFrame::Columns(i, dst_frame->format(),
+                            dst_frame->natural_size().width()),
+        VideoFrame::Rows(i, dst_frame->format(),
+                         dst_frame->natural_size().height()));
+  }
+
+  return dst_frame;
+}
+
+// Create VideoFrame from |info| loading |info.file_name|. Return nullptr on
+// failure.
+scoped_refptr<VideoFrame> ReadVideoFrame(const VideoImageInfo& info) {
+  auto path = GetFilePath(info.file_name);
+  // First read file.
+  auto mapped_file = std::make_unique<base::MemoryMappedFile>();
+  if (!mapped_file->Initialize(path)) {
+    LOG(ERROR) << "Failed to read file: " << path;
+    return nullptr;
+  }
+
+  const auto format = info.pixel_format;
+  const auto visible_size = info.visible_size;
+  // Check the file length and md5sum.
+  LOG_ASSERT(mapped_file->length() ==
+             VideoFrame::AllocationSize(format, visible_size));
+  base::MD5Digest digest;
+  base::MD5Sum(mapped_file->data(), mapped_file->length(), &digest);
+  LOG_ASSERT(base::MD5DigestToBase16(digest) == info.md5sum);
+
+  // Create planes for layout. We cannot use WrapExternalData() because it calls
+  // GetDefaultLayout() and it supports only a few pixel formats.
+  const size_t num_planes = VideoFrame::NumPlanes(format);
+  std::vector<VideoFrameLayout::Plane> planes(num_planes);
+  const auto strides = VideoFrame::ComputeStrides(format, visible_size);
+  size_t offset = 0;
+  for (size_t i = 0; i < num_planes; ++i) {
+    planes[i].stride = strides[i];
+    planes[i].offset = offset;
+    offset += VideoFrame::PlaneSize(format, i, visible_size).GetArea();
+  }
+
+  auto layout = VideoFrameLayout::CreateWithPlanes(
+      format, visible_size, std::move(planes), {mapped_file->length()});
+  if (!layout) {
+    LOG(ERROR) << "Failed to create VideoFrameLayout";
+    return nullptr;
+  }
+
+  auto frame = VideoFrame::WrapExternalDataWithLayout(
+      *layout, gfx::Rect(visible_size), visible_size, mapped_file->data(),
+      mapped_file->length(), base::TimeDelta());
+  if (!frame) {
+    LOG(ERROR) << "Failed to create VideoFrame";
+    return nullptr;
+  }
+
+  // Automatically unmap the memory mapped file when the video frame is
+  // destroyed.
+  frame->AddDestructionObserver(base::BindOnce(
+      base::DoNothing::Once<std::unique_ptr<base::MemoryMappedFile>>(),
+      std::move(mapped_file)));
+  return frame;
+}
+
+}  // namespace
+
+// static
+std::unique_ptr<ImageProcessorClient> ImageProcessorClient::Create(
+    const ImageProcessor::PortConfig& input_config,
+    const ImageProcessor::PortConfig& output_config,
+    size_t num_buffers,
+    bool store_processed_video_frames) {
+  auto ip_client =
+      base::WrapUnique(new ImageProcessorClient(store_processed_video_frames));
+  if (!ip_client->CreateImageProcessor(input_config, output_config,
+                                       num_buffers)) {
+    LOG(ERROR) << "Failed to create ImageProcessor";
+    return nullptr;
+  }
+  return ip_client;
+}
+
+ImageProcessorClient::ImageProcessorClient(bool store_processed_video_frames)
+    : image_processor_client_thread_("ImageProcessorClientThread"),
+      store_processed_video_frames_(store_processed_video_frames),
+      output_cv_(&output_lock_),
+      num_processed_frames_(0),
+      image_processor_error_count_(0) {
+  CHECK(image_processor_client_thread_.Start());
+  DETACH_FROM_THREAD(image_processor_client_thread_checker_);
+}
+
+ImageProcessorClient::~ImageProcessorClient() {
+  DCHECK_CALLED_ON_VALID_THREAD(test_main_thread_checker_);
+  CHECK(image_processor_client_thread_.IsRunning());
+  // Destroys |image_processor_| on |image_processor_client_thread_|.
+  image_processor_client_thread_.task_runner()->DeleteSoon(
+      FROM_HERE, image_processor_.release());
+  image_processor_client_thread_.Stop();
+}
+
+bool ImageProcessorClient::CreateImageProcessor(
+    const ImageProcessor::PortConfig& input_config,
+    const ImageProcessor::PortConfig& output_config,
+    size_t num_buffers) {
+  DCHECK_CALLED_ON_VALID_THREAD(test_main_thread_checker_);
+  base::WaitableEvent done(base::WaitableEvent::ResetPolicy::AUTOMATIC,
+                           base::WaitableEvent::InitialState::NOT_SIGNALED);
+  // base::Unretained(this) and base::ConstRef() are safe here because |this|,
+  // |input_config| and |output_config| must outlive because this task is
+  // blocking.
+  image_processor_client_thread_.task_runner()->PostTask(
+      FROM_HERE,
+      base::BindOnce(&ImageProcessorClient::CreateImageProcessorTask,
+                     base::Unretained(this), base::ConstRef(input_config),
+                     base::ConstRef(output_config), num_buffers, &done));
+  done.Wait();
+  if (!image_processor_) {
+    LOG(ERROR) << "Failed to create ImageProcessor";
+    return false;
+  }
+  return true;
+}
+
+void ImageProcessorClient::CreateImageProcessorTask(
+    const ImageProcessor::PortConfig& input_config,
+    const ImageProcessor::PortConfig& output_config,
+    size_t num_buffers,
+    base::WaitableEvent* done) {
+  DCHECK_CALLED_ON_VALID_THREAD(image_processor_client_thread_checker_);
+  // base::Unretained(this) for ErrorCB is safe here because the callback is
+  // executed on |image_processor_client_thread_| which is owned by this class.
+  image_processor_ = ImageProcessorFactory::Create(
+      input_config, output_config, {ImageProcessor::OutputMode::IMPORT},
+      num_buffers,
+      base::BindRepeating(&ImageProcessorClient::NotifyError,
+                          base::Unretained(this)));
+  done->Signal();
+}
+
+scoped_refptr<VideoFrame> ImageProcessorClient::CreateInputFrame(
+    const VideoImageInfo& input_image_info) const {
+  DCHECK_CALLED_ON_VALID_THREAD(test_main_thread_checker_);
+  LOG_ASSERT(image_processor_);
+
+  auto mapped_frame = ReadVideoFrame(input_image_info);
+  const auto& input_layout = image_processor_->input_layout();
+  if (VideoFrame::IsStorageTypeMappable(
+          image_processor_->input_storage_type())) {
+    return CloneVideoFrameWithLayout(mapped_frame.get(), input_layout);
+  } else {
+#if defined(OS_CHROMEOS)
+    LOG_ASSERT(image_processor_->input_storage_type() ==
+               VideoFrame::STORAGE_DMABUFS);
+#endif
+    // TODO(crbug.com/917951): Support Dmabuf.
+    NOTIMPLEMENTED();
+    return nullptr;
+  }
+}
+
+scoped_refptr<VideoFrame> ImageProcessorClient::CreateOutputFrame(
+    const VideoImageInfo& output_image_info) const {
+  DCHECK_CALLED_ON_VALID_THREAD(test_main_thread_checker_);
+  LOG_ASSERT(image_processor_);
+
+  const auto& output_layout = image_processor_->output_layout();
+  if (VideoFrame::IsStorageTypeMappable(
+          image_processor_->input_storage_type())) {
+    return VideoFrame::CreateFrameWithLayout(
+        output_layout, gfx::Rect(output_image_info.visible_size),
+        output_image_info.visible_size, base::TimeDelta(),
+        false /* zero_initialize_memory*/
+    );
+  } else {
+#if defined(OS_CHROMEOS)
+    LOG_ASSERT(image_processor_->input_storage_type() ==
+               VideoFrame::STORAGE_DMABUFS);
+#endif
+    // TODO(crbug.com/917951): Support Dmabuf.
+    NOTIMPLEMENTED();
+    return nullptr;
+  }
+}
+
+void ImageProcessorClient::FrameReady(scoped_refptr<VideoFrame> frame) {
+  DCHECK_CALLED_ON_VALID_THREAD(image_processor_client_thread_checker_);
+
+  base::AutoLock auto_lock_(output_lock_);
+  if (store_processed_video_frames_)
+    output_video_frames_.push_back(std::move(frame));
+  num_processed_frames_++;
+  output_cv_.Signal();
+}
+
+bool ImageProcessorClient::WaitUntilNumImageProcessed(
+    size_t num_processed,
+    base::TimeDelta max_wait) {
+  base::TimeDelta time_waiting;
+  // NOTE: Acquire lock here does not matter, because
+  // base::ConditionVariable::TimedWait() unlocks output_lock_ at the start and
+  // locks again at the end.
+  base::AutoLock auto_lock_(output_lock_);
+  while (time_waiting < max_wait) {
+    const base::TimeTicks start_time = base::TimeTicks::Now();
+    output_cv_.TimedWait(max_wait);
+    time_waiting += base::TimeTicks::Now() - start_time;
+
+    if (num_processed_frames_ >= num_processed)
+      return true;
+  }
+  return false;
+}
+
+size_t ImageProcessorClient::GetNumOfProcessedImages() const {
+  base::AutoLock auto_lock_(output_lock_);
+  return num_processed_frames_;
+}
+
+std::vector<scoped_refptr<VideoFrame>>
+ImageProcessorClient::GetProcessedImages() const {
+  LOG_ASSERT(store_processed_video_frames_);
+  base::AutoLock auto_lock_(output_lock_);
+  return output_video_frames_;
+}
+
+size_t ImageProcessorClient::GetErrorCount() const {
+  base::AutoLock auto_lock_(output_lock_);
+  return image_processor_error_count_;
+}
+
+void ImageProcessorClient::NotifyError() {
+  DCHECK_CALLED_ON_VALID_THREAD(image_processor_client_thread_checker_);
+  base::AutoLock auto_lock_(output_lock_);
+  image_processor_error_count_++;
+}
+
+void ImageProcessorClient::Process(const VideoImageInfo& input_info,
+                                   const VideoImageInfo& output_info) {
+  DCHECK_CALLED_ON_VALID_THREAD(test_main_thread_checker_);
+  auto input_frame = CreateInputFrame(input_info);
+  ASSERT_TRUE(input_frame);
+  auto output_frame = CreateOutputFrame(input_info);
+  ASSERT_TRUE(output_frame);
+  image_processor_client_thread_.task_runner()->PostTask(
+      FROM_HERE,
+      base::BindOnce(&ImageProcessorClient::ProcessTask, base::Unretained(this),
+                     std::move(input_frame), std::move(output_frame)));
+}
+
+void ImageProcessorClient::ProcessTask(scoped_refptr<VideoFrame> input_frame,
+                                       scoped_refptr<VideoFrame> output_frame) {
+  DCHECK_CALLED_ON_VALID_THREAD(image_processor_client_thread_checker_);
+  // base::Unretained(this) and base::ConstRef() for FrameReadyCB is safe here
+  // because the callback is executed on |image_processor_client_thread_| which
+  // is owned by this class.
+  image_processor_->Process(
+      std::move(input_frame), std::move(output_frame),
+      BindToCurrentLoop(base::BindOnce(&ImageProcessorClient::FrameReady,
+                                       base::Unretained(this))));
+}
+
+}  // namespace test
+}  // namespace media
diff --git a/media/gpu/test/image_processor/image_processor_client.h b/media/gpu/test/image_processor/image_processor_client.h
new file mode 100644
index 0000000..999bbc3
--- /dev/null
+++ b/media/gpu/test/image_processor/image_processor_client.h
@@ -0,0 +1,141 @@
+// Copyright 2019 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_GPU_TEST_IMAGE_PROCESSOR_IMAGE_PROCESSOR_CLIENT_H_
+#define MEDIA_GPU_TEST_IMAGE_PROCESSOR_IMAGE_PROCESSOR_CLIENT_H_
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "base/atomicops.h"
+#include "base/macros.h"
+#include "base/synchronization/condition_variable.h"
+#include "base/synchronization/lock.h"
+#include "base/threading/thread.h"
+#include "base/threading/thread_checker.h"
+#include "base/time/time.h"
+#include "media/gpu/image_processor.h"
+#include "media/gpu/test/video_image_info.h"
+
+namespace base {
+
+class WaitableEvent;
+
+}  // namespace base
+
+namespace media {
+
+class VideoFrame;
+
+namespace test {
+
+// ImageProcessorClient is a client of ImageProcessor for testing purpose.
+// All the public functions must be called on the same thread, usually the test
+// main thread.
+class ImageProcessorClient {
+ public:
+  // |store_processed_video_frames| is whether ImageProcessorClient will store
+  // VideoFrame in FrameReady().
+  // TODO(crbug.com/917951): Get VideoFrameProcessor and does not store
+  // VideoFrame.
+  static std::unique_ptr<ImageProcessorClient> Create(
+      const ImageProcessor::PortConfig& input_config,
+      const ImageProcessor::PortConfig& output_config,
+      size_t num_buffers,
+      bool store_processed_video_frames = true);
+
+  // Destruct |image_processor_| if it is created.
+  ~ImageProcessorClient();
+
+  // Process |input_frame| and |output_frame| with |image_processor_|.
+  void Process(const VideoImageInfo& input_info,
+               const VideoImageInfo& output_info);
+
+  // TODO(crbug.com/917951): Add Reset() when we test Reset() test case.
+
+  // Wait until |num_processed| frames are processed.
+  bool WaitUntilNumImageProcessed(
+      size_t num_processed,
+      base::TimeDelta max_wait = base::TimeDelta::FromSeconds(5));
+
+  // Get the number of processed VideoFrames.
+  size_t GetNumOfProcessedImages() const;
+
+  // Get processed VideoFrames.
+  // This always returns empty unless |store_processed_video_frames_|
+  // TODO(hiroh): Remove this function and instead check md5sum and order
+  // interactively inside of ImageProcessorClient by passing some
+  // VideoFrameProcessor interface class.
+  std::vector<scoped_refptr<VideoFrame>> GetProcessedImages() const;
+
+  // Return whether |image_processor_| invokes ImageProcessor::ErrorCB.
+  size_t GetErrorCount() const;
+
+ private:
+  ImageProcessorClient(bool store_processed_video_frames);
+
+  // Create ImageProcessor with |input_config|, |output_config| and
+  // |num_buffers|.
+  bool CreateImageProcessor(const ImageProcessor::PortConfig& input_config,
+                            const ImageProcessor::PortConfig& output_config,
+                            size_t num_buffers);
+
+  // Create |image_processor_| on |my_thread_|.
+  void CreateImageProcessorTask(const ImageProcessor::PortConfig& input_config,
+                                const ImageProcessor::PortConfig& output_config,
+                                size_t num_buffers,
+                                base::WaitableEvent* done);
+
+  // Call ImageProcessor::Process() on |my_thread_|.
+  void ProcessTask(scoped_refptr<VideoFrame> input_frame,
+                   scoped_refptr<VideoFrame> output_frame);
+
+  // FrameReadyCB for ImageProcessor::Process().
+  void FrameReady(scoped_refptr<VideoFrame> frame);
+  // ErrorCB for ImageProcessor.
+  void NotifyError();
+
+  // These are test helper functions to create a VideoFrame from VideoImageInfo,
+  // which will be input in Process().
+  // Create a VideoFrame using the input layout required by |image_processor_|.
+  scoped_refptr<VideoFrame> CreateInputFrame(
+      const VideoImageInfo& input_image_info) const;
+  // Create a VideoFrame using the output layout required by |image_processor_|.
+  scoped_refptr<VideoFrame> CreateOutputFrame(
+      const VideoImageInfo& output_image_info) const;
+
+  std::unique_ptr<ImageProcessor> image_processor_;
+
+  // The thread on which the |image_processor_| is created and destroyed. From
+  // the specification of ImageProcessor, ImageProcessor::Process(),
+  // FrameReady() and NotifyError() must be run on
+  // |image_processor_client_thread_|.
+  base::Thread image_processor_client_thread_;
+
+  // If true, ImageProcessorClient stores processed VideoFrame in
+  // |output_video_frames_|.
+  const bool store_processed_video_frames_;
+
+  mutable base::Lock output_lock_;
+  // This is signaled in FrameReady().
+  base::ConditionVariable output_cv_;
+  // The number of processed VideoFrame.
+  size_t num_processed_frames_ GUARDED_BY(output_lock_);
+  // The collection of processed VideoFrame. It is stored in FrameReady() call
+  // order.
+  std::vector<scoped_refptr<VideoFrame>> output_video_frames_
+      GUARDED_BY(output_lock_);
+  // The number of times ImageProcessor::ErrorCB called.
+  size_t image_processor_error_count_ GUARDED_BY(output_lock_);
+
+  THREAD_CHECKER(image_processor_client_thread_checker_);
+  THREAD_CHECKER(test_main_thread_checker_);
+  DISALLOW_COPY_AND_ASSIGN(ImageProcessorClient);
+};
+
+}  // namespace test
+}  // namespace media
+
+#endif  // MEDIA_GPU_TEST_IMAGE_PROCESSOR_IMAGE_PROCESSOR_CLIENT_H_
diff --git a/media/gpu/test/video_image_info.h b/media/gpu/test/video_image_info.h
new file mode 100644
index 0000000..3de0da2
--- /dev/null
+++ b/media/gpu/test/video_image_info.h
@@ -0,0 +1,60 @@
+
+// Copyright 2019 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_GPU_TEST_VIDEO_IMAGE_INFO_H_
+#define MEDIA_GPU_TEST_VIDEO_IMAGE_INFO_H_
+
+#include <vector>
+
+#include "base/files/file_path.h"
+#include "base/optional.h"
+#include "media/base/video_frame.h"
+#include "media/base/video_frame_layout.h"
+#include "media/base/video_types.h"
+#include "ui/gfx/geometry/size.h"
+
+namespace media {
+namespace test {
+
+// VideoImageInfo is the information about raw video frame in file.
+struct VideoImageInfo {
+  // TODO(crbug.com/917951): Deprecate this constructor once we load these info
+  // from json file.
+  constexpr VideoImageInfo(const base::FilePath::CharType* const file_name,
+                           const char* const md5sum,
+                           VideoPixelFormat pixel_format,
+                           gfx::Size size)
+      : file_name(file_name),
+        md5sum(md5sum),
+        pixel_format(pixel_format),
+        visible_size(size.width(), size.height()) {}
+  VideoImageInfo() = delete;
+  ~VideoImageInfo() = default;
+
+  base::Optional<VideoFrameLayout> VideoFrameLayout() const {
+    return VideoFrameLayout::CreateWithStrides(
+        pixel_format, visible_size,
+        VideoFrame::ComputeStrides(pixel_format, visible_size),
+        std::vector<size_t>(VideoFrame::NumPlanes(pixel_format),
+                            0) /* buffer_sizes */);
+  }
+
+  // |file_name| is a file name to be read(e.g. "bear_320x192.i420.yuv"), not
+  // file path.
+  const base::FilePath::CharType* const file_name;
+  //| md5sum| is the md5sum value of the video frame, whose coded_size is the
+  // same as visible size.
+  const char* const md5sum;
+
+  // |pixel_format| and |visible_size| of the video frame in file.
+  // NOTE: visible_size should be the same as coded_size, i.e., there is no
+  // extra padding in the file.
+  const VideoPixelFormat pixel_format;
+  const gfx::Size visible_size;
+};
+
+}  // namespace test
+}  // namespace media
+#endif  // MEDIA_GPU_TEST_VIDEO_IMAGE_INFO_H_