1// Copyright 2014 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include <string>
6
7// This has to be included first.
8// See http://code.google.com/p/googletest/issues/detail?id=371
9#include "testing/gtest/include/gtest/gtest.h"
10
11#include "base/bind.h"
12#include "base/command_line.h"
13#include "base/files/file_util.h"
14#include "base/logging.h"
15#include "content/common/gpu/media/vaapi_h264_decoder.h"
16#include "media/base/video_decoder_config.h"
17#include "third_party/libyuv/include/libyuv.h"
18
19// This program is run like this:
20// DISPLAY=:0 ./vaapi_h264_decoder_unittest --input_file input.h264
21// [--output_file output.i420] [--md5sum expected_md5_hex]
22//
23// The input is read from input.h264. The output is written to output.i420 if it
24// is given. It also verifies the MD5 sum of the decoded I420 data if the
25// expected MD5 sum is given.
26
27namespace content {
28namespace {
29
30// These are the command line parameters
31base::FilePath g_input_file;
32base::FilePath g_output_file;
33std::string g_md5sum;
34
35// These default values are used if nothing is specified in the command line.
36const base::FilePath::CharType* kDefaultInputFile =
37    FILE_PATH_LITERAL("test-25fps.h264");
38const char* kDefaultMD5Sum = "3af866863225b956001252ebeccdb71d";
39
40// This class encapsulates the use of VaapiH264Decoder to a simpler interface.
41// It reads an H.264 Annex B bytestream from a file and outputs the decoded
42// frames (in I420 format) to another file. The output file can be played by
43// $ mplayer test_dec.yuv -demuxer rawvideo -rawvideo w=1920:h=1080
44//
45// To use the class, construct an instance, call Initialize() to specify the
46// input and output file paths, then call Run() to decode the whole stream and
47// output the frames.
48//
49// This class must be created, called and destroyed on a single thread, and
50// does nothing internally on any other thread.
51class VaapiH264DecoderLoop {
52 public:
53  VaapiH264DecoderLoop();
54  ~VaapiH264DecoderLoop();
55
56  // Initialize the decoder. Return true if successful.
57  bool Initialize(base::FilePath input_file, base::FilePath output_file);
58
59  // Run the decode loop. The decoded data is written to the file specified by
60  // output_file in Initialize().  Return true if all decoding is successful.
61  bool Run();
62
63  // Get the MD5 sum of the decoded data.
64  std::string GetMD5Sum();
65
66 private:
67  // Callback from the decoder when a picture is decoded.
68  void OutputPicture(int32 input_id,
69                     const scoped_refptr<VASurface>& va_surface);
70
71  // Recycle one surface and put it on available_surfaces_ list.
72  void RecycleSurface(VASurfaceID va_surface_id);
73
74  // Give all surfaces in available_surfaces_ to the decoder.
75  void RefillSurfaces();
76
77  // Free the current set of surfaces and allocate a new set of
78  // surfaces. Returns true when successful.
79  bool AllocateNewSurfaces();
80
81  // Use the data in the frame: write to file and update MD5 sum.
82  bool ProcessVideoFrame(const scoped_refptr<media::VideoFrame>& frame);
83
84  scoped_ptr<VaapiWrapper> wrapper_;
85  scoped_ptr<VaapiH264Decoder> decoder_;
86  std::string data_;            // data read from input_file
87  base::FilePath output_file_;  // output data is written to this file
88  std::vector<VASurfaceID> available_surfaces_;
89
90  // These members (x_display_, num_outputted_pictures_, num_surfaces_)
91  // need to be initialized and possibly freed manually.
92  Display* x_display_;
93  int num_outputted_pictures_;  // number of pictures already outputted
94  size_t num_surfaces_;  // number of surfaces in the current set of surfaces
95  base::MD5Context md5_context_;
96};
97
98VaapiH264DecoderLoop::VaapiH264DecoderLoop()
99    : x_display_(NULL), num_outputted_pictures_(0), num_surfaces_(0) {
100  base::MD5Init(&md5_context_);
101}
102
103VaapiH264DecoderLoop::~VaapiH264DecoderLoop() {
104  // We need to destruct decoder and wrapper first because:
105  // (1) The decoder has a reference to the wrapper.
106  // (2) The wrapper has a reference to x_display_.
107  decoder_.reset();
108  wrapper_.reset();
109
110  if (x_display_) {
111    XCloseDisplay(x_display_);
112  }
113}
114
115void LogOnError(VaapiH264Decoder::VAVDAH264DecoderFailure error) {
116  LOG(FATAL) << "Oh noes! Decoder failed: " << error;
117}
118
119bool VaapiH264DecoderLoop::Initialize(base::FilePath input_file,
120                                      base::FilePath output_file) {
121  x_display_ = XOpenDisplay(NULL);
122  if (!x_display_) {
123    LOG(ERROR) << "Can't open X display";
124    return false;
125  }
126
127  media::VideoCodecProfile profile = media::H264PROFILE_BASELINE;
128  base::Closure report_error_cb =
129      base::Bind(&LogOnError, VaapiH264Decoder::VAAPI_ERROR);
130  wrapper_ = VaapiWrapper::Create(
131      VaapiWrapper::kDecode, profile, x_display_, report_error_cb);
132  if (!wrapper_.get()) {
133    LOG(ERROR) << "Can't create vaapi wrapper";
134    return false;
135  }
136
137  decoder_.reset(new VaapiH264Decoder(
138      wrapper_.get(),
139      base::Bind(&VaapiH264DecoderLoop::OutputPicture, base::Unretained(this)),
140      base::Bind(&LogOnError)));
141
142  if (!base::ReadFileToString(input_file, &data_)) {
143    LOG(ERROR) << "failed to read input data from " << input_file.value();
144    return false;
145  }
146
147  const int input_id = 0;  // We don't use input_id in this class.
148  decoder_->SetStream(
149      reinterpret_cast<const uint8*>(data_.c_str()), data_.size(), input_id);
150
151  // This creates or truncates output_file.
152  // Without it, AppendToFile() will not work.
153  if (!output_file.empty()) {
154    if (base::WriteFile(output_file, NULL, 0) != 0) {
155      return false;
156    }
157    output_file_ = output_file;
158  }
159
160  return true;
161}
162
163bool VaapiH264DecoderLoop::Run() {
164  while (1) {
165    switch (decoder_->Decode()) {
166      case VaapiH264Decoder::kDecodeError:
167        LOG(ERROR) << "Decode Error";
168        return false;
169      case VaapiH264Decoder::kAllocateNewSurfaces:
170        VLOG(1) << "Allocate new surfaces";
171        if (!AllocateNewSurfaces()) {
172          LOG(ERROR) << "Failed to allocate new surfaces";
173          return false;
174        }
175        break;
176      case VaapiH264Decoder::kRanOutOfStreamData: {
177        bool rc = decoder_->Flush();
178        VLOG(1) << "Flush returns " << rc;
179        return rc;
180      }
181      case VaapiH264Decoder::kRanOutOfSurfaces:
182        VLOG(1) << "Ran out of surfaces";
183        RefillSurfaces();
184        break;
185    }
186  }
187}
188
189std::string VaapiH264DecoderLoop::GetMD5Sum() {
190  base::MD5Digest digest;
191  base::MD5Final(&digest, &md5_context_);
192  return MD5DigestToBase16(digest);
193}
194
195scoped_refptr<media::VideoFrame> CopyNV12ToI420(VAImage* image, void* mem) {
196  int width = image->width;
197  int height = image->height;
198
199  DVLOG(1) << "CopyNV12ToI420 width=" << width << ", height=" << height;
200
201  const gfx::Size coded_size(width, height);
202  const gfx::Rect visible_rect(width, height);
203  const gfx::Size natural_size(width, height);
204
205  scoped_refptr<media::VideoFrame> frame =
206      media::VideoFrame::CreateFrame(media::VideoFrame::I420,
207                                     coded_size,
208                                     visible_rect,
209                                     natural_size,
210                                     base::TimeDelta());
211
212  uint8_t* mem_byte_ptr = static_cast<uint8_t*>(mem);
213  uint8_t* src_y = mem_byte_ptr + image->offsets[0];
214  uint8_t* src_uv = mem_byte_ptr + image->offsets[1];
215  int src_stride_y = image->pitches[0];
216  int src_stride_uv = image->pitches[1];
217
218  uint8_t* dst_y = frame->data(media::VideoFrame::kYPlane);
219  uint8_t* dst_u = frame->data(media::VideoFrame::kUPlane);
220  uint8_t* dst_v = frame->data(media::VideoFrame::kVPlane);
221  int dst_stride_y = frame->stride(media::VideoFrame::kYPlane);
222  int dst_stride_u = frame->stride(media::VideoFrame::kUPlane);
223  int dst_stride_v = frame->stride(media::VideoFrame::kVPlane);
224
225  int rc = libyuv::NV12ToI420(src_y,
226                              src_stride_y,
227                              src_uv,
228                              src_stride_uv,
229                              dst_y,
230                              dst_stride_y,
231                              dst_u,
232                              dst_stride_u,
233                              dst_v,
234                              dst_stride_v,
235                              width,
236                              height);
237  CHECK_EQ(0, rc);
238  return frame;
239}
240
241bool VaapiH264DecoderLoop::ProcessVideoFrame(
242    const scoped_refptr<media::VideoFrame>& frame) {
243  frame->HashFrameForTesting(&md5_context_);
244
245  if (output_file_.empty())
246    return true;
247
248  for (size_t i = 0; i < media::VideoFrame::NumPlanes(frame->format()); i++) {
249    int to_write = media::VideoFrame::PlaneAllocationSize(
250        frame->format(), i, frame->coded_size());
251    const char* buf = reinterpret_cast<const char*>(frame->data(i));
252    int written = base::AppendToFile(output_file_, buf, to_write);
253    if (written != to_write)
254      return false;
255  }
256  return true;
257}
258
259void VaapiH264DecoderLoop::OutputPicture(
260    int32 input_id,
261    const scoped_refptr<VASurface>& va_surface) {
262  VLOG(1) << "OutputPicture: picture " << num_outputted_pictures_++;
263
264  VAImage image;
265  void* mem;
266
267  if (!wrapper_->GetVaImageForTesting(va_surface->id(), &image, &mem)) {
268    LOG(ERROR) << "Cannot get VAImage.";
269    return;
270  }
271
272  if (image.format.fourcc != VA_FOURCC_NV12) {
273    LOG(ERROR) << "Unexpected image format: " << image.format.fourcc;
274    wrapper_->ReturnVaImageForTesting(&image);
275    return;
276  }
277
278  // Convert NV12 to I420 format.
279  scoped_refptr<media::VideoFrame> frame = CopyNV12ToI420(&image, mem);
280
281  if (frame.get()) {
282    if (!ProcessVideoFrame(frame)) {
283      LOG(ERROR) << "Write to file failed";
284    }
285  } else {
286    LOG(ERROR) << "Cannot convert image to I420.";
287  }
288
289  wrapper_->ReturnVaImageForTesting(&image);
290}
291
292void VaapiH264DecoderLoop::RecycleSurface(VASurfaceID va_surface_id) {
293  available_surfaces_.push_back(va_surface_id);
294}
295
296void VaapiH264DecoderLoop::RefillSurfaces() {
297  for (size_t i = 0; i < available_surfaces_.size(); i++) {
298    VASurface::ReleaseCB release_cb = base::Bind(
299        &VaapiH264DecoderLoop::RecycleSurface, base::Unretained(this));
300    scoped_refptr<VASurface> surface(
301        new VASurface(available_surfaces_[i], release_cb));
302    decoder_->ReuseSurface(surface);
303  }
304  available_surfaces_.clear();
305}
306
307bool VaapiH264DecoderLoop::AllocateNewSurfaces() {
308  CHECK_EQ(num_surfaces_, available_surfaces_.size())
309      << "not all surfaces are returned";
310
311  available_surfaces_.clear();
312  wrapper_->DestroySurfaces();
313
314  gfx::Size size = decoder_->GetPicSize();
315  num_surfaces_ = decoder_->GetRequiredNumOfPictures();
316  return wrapper_->CreateSurfaces(size, num_surfaces_, &available_surfaces_);
317}
318
319TEST(VaapiH264DecoderTest, TestDecode) {
320  base::FilePath input_file = g_input_file;
321  base::FilePath output_file = g_output_file;
322  std::string md5sum = g_md5sum;
323
324  // If nothing specified, use the default file in the source tree.
325  if (input_file.empty() && output_file.empty() && md5sum.empty()) {
326    input_file = base::FilePath(kDefaultInputFile);
327    md5sum = kDefaultMD5Sum;
328  } else {
329    ASSERT_FALSE(input_file.empty()) << "Need to specify --input_file";
330  }
331
332  VLOG(1) << "Input File: " << input_file.value();
333  VLOG(1) << "Output File: " << output_file.value();
334  VLOG(1) << "Expected MD5 sum: " << md5sum;
335
336  content::VaapiH264DecoderLoop loop;
337  ASSERT_TRUE(loop.Initialize(input_file, output_file))
338      << "initialize decoder loop failed";
339  ASSERT_TRUE(loop.Run()) << "run decoder loop failed";
340
341  if (!md5sum.empty()) {
342    std::string actual = loop.GetMD5Sum();
343    VLOG(1) << "Actual MD5 sum: " << actual;
344    EXPECT_EQ(md5sum, actual);
345  }
346}
347
348}  // namespace
349}  // namespace content
350
351int main(int argc, char** argv) {
352  testing::InitGoogleTest(&argc, argv);  // Removes gtest-specific args.
353  base::CommandLine::Init(argc, argv);
354
355  // Needed to enable DVLOG through --vmodule.
356  logging::LoggingSettings settings;
357  settings.logging_dest = logging::LOG_TO_SYSTEM_DEBUG_LOG;
358  CHECK(logging::InitLogging(settings));
359
360  // Process command line.
361  base::CommandLine* cmd_line = base::CommandLine::ForCurrentProcess();
362  CHECK(cmd_line);
363
364  base::CommandLine::SwitchMap switches = cmd_line->GetSwitches();
365  for (base::CommandLine::SwitchMap::const_iterator it = switches.begin();
366       it != switches.end();
367       ++it) {
368    if (it->first == "input_file") {
369      content::g_input_file = base::FilePath(it->second);
370      continue;
371    }
372    if (it->first == "output_file") {
373      content::g_output_file = base::FilePath(it->second);
374      continue;
375    }
376    if (it->first == "md5sum") {
377      content::g_md5sum = it->second;
378      continue;
379    }
380    if (it->first == "v" || it->first == "vmodule")
381      continue;
382    LOG(FATAL) << "Unexpected switch: " << it->first << ":" << it->second;
383  }
384
385  return RUN_ALL_TESTS();
386}
387