Introduce IVF file reader

Bug: webrtc:10138
Change-Id: I97d332942f4e645527330159efefb1cb1d8034a0
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/160008
Commit-Queue: Artem Titov <titovartem@webrtc.org>
Reviewed-by: Ilya Nikolaevskiy <ilnik@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#29844}
diff --git a/modules/video_coding/BUILD.gn b/modules/video_coding/BUILD.gn
index 627000d..71e14fa 100644
--- a/modules/video_coding/BUILD.gn
+++ b/modules/video_coding/BUILD.gn
@@ -263,6 +263,8 @@
     "utility/frame_dropper.h",
     "utility/framerate_controller.cc",
     "utility/framerate_controller.h",
+    "utility/ivf_file_reader.cc",
+    "utility/ivf_file_reader.h",
     "utility/ivf_file_writer.cc",
     "utility/ivf_file_writer.h",
     "utility/quality_scaler.cc",
@@ -844,6 +846,7 @@
       "utility/default_video_bitrate_allocator_unittest.cc",
       "utility/frame_dropper_unittest.cc",
       "utility/framerate_controller_unittest.cc",
+      "utility/ivf_file_reader_unittest.cc",
       "utility/ivf_file_writer_unittest.cc",
       "utility/quality_scaler_unittest.cc",
       "utility/simulcast_rate_allocator_unittest.cc",
diff --git a/modules/video_coding/utility/ivf_file_reader.cc b/modules/video_coding/utility/ivf_file_reader.cc
new file mode 100644
index 0000000..8703a29
--- /dev/null
+++ b/modules/video_coding/utility/ivf_file_reader.cc
@@ -0,0 +1,234 @@
+/*
+ *  Copyright (c) 2019 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#include "modules/video_coding/utility/ivf_file_reader.h"
+
+#include <string>
+#include <vector>
+
+#include "api/video_codecs/video_codec.h"
+#include "modules/rtp_rtcp/source/byte_io.h"
+#include "rtc_base/logging.h"
+
+namespace webrtc {
+namespace {
+
+constexpr size_t kIvfHeaderSize = 32;
+constexpr size_t kIvfFrameHeaderSize = 12;
+constexpr int kCodecTypeBytesCount = 4;
+
+constexpr uint8_t kFileHeaderStart[kCodecTypeBytesCount] = {'D', 'K', 'I', 'F'};
+constexpr uint8_t kVp8Header[kCodecTypeBytesCount] = {'V', 'P', '8', '0'};
+constexpr uint8_t kVp9Header[kCodecTypeBytesCount] = {'V', 'P', '9', '0'};
+constexpr uint8_t kH264Header[kCodecTypeBytesCount] = {'H', '2', '6', '4'};
+
+}  // namespace
+
+std::unique_ptr<IvfFileReader> IvfFileReader::Create(FileWrapper file) {
+  auto reader =
+      std::unique_ptr<IvfFileReader>(new IvfFileReader(std::move(file)));
+  if (!reader->Reset()) {
+    return nullptr;
+  }
+  return reader;
+}
+IvfFileReader::~IvfFileReader() {
+  Close();
+}
+
+bool IvfFileReader::Reset() {
+  // Set error to true while initialization.
+  has_error_ = true;
+  if (!file_.Rewind()) {
+    RTC_LOG(LS_ERROR) << "Failed to rewind IVF file";
+    return false;
+  }
+
+  uint8_t ivf_header[kIvfHeaderSize] = {0};
+  size_t read = file_.Read(&ivf_header, kIvfHeaderSize);
+  if (read != kIvfHeaderSize) {
+    RTC_LOG(LS_ERROR) << "Failed to read IVF header";
+    return false;
+  }
+
+  if (memcmp(&ivf_header[0], kFileHeaderStart, 4) != 0) {
+    RTC_LOG(LS_ERROR) << "File is not in IVF format: DKIF header expected";
+    return false;
+  }
+
+  absl::optional<VideoCodecType> codec_type = ParseCodecType(ivf_header, 8);
+  if (!codec_type) {
+    return false;
+  }
+  codec_type_ = *codec_type;
+
+  width_ = ByteReader<uint16_t>::ReadLittleEndian(&ivf_header[12]);
+  height_ = ByteReader<uint16_t>::ReadLittleEndian(&ivf_header[14]);
+  if (width_ == 0 || height_ == 0) {
+    RTC_LOG(LS_ERROR) << "Invalid IVF header: width or height is 0";
+    return false;
+  }
+
+  uint32_t time_scale = ByteReader<uint32_t>::ReadLittleEndian(&ivf_header[16]);
+  if (time_scale == 1000) {
+    using_capture_timestamps_ = true;
+  } else if (time_scale == 90000) {
+    using_capture_timestamps_ = false;
+  } else {
+    RTC_LOG(LS_ERROR) << "Invalid IVF header: Unknown time scale";
+    return false;
+  }
+
+  num_frames_ = static_cast<size_t>(
+      ByteReader<uint32_t>::ReadLittleEndian(&ivf_header[24]));
+  if (num_frames_ <= 0) {
+    RTC_LOG(LS_ERROR) << "Invalid IVF header: number of frames 0 or negative";
+    return false;
+  }
+
+  num_read_frames_ = 0;
+  next_frame_header_ = ReadNextFrameHeader();
+  if (!next_frame_header_) {
+    RTC_LOG(LS_ERROR) << "Failed to read 1st frame header";
+    return false;
+  }
+  // Initialization succeed: reset error.
+  has_error_ = false;
+
+  const char* codec_name = CodecTypeToPayloadString(codec_type_);
+  RTC_LOG(INFO) << "Opened IVF file with codec data of type " << codec_name
+                << " at resolution " << width_ << " x " << height_ << ", using "
+                << (using_capture_timestamps_ ? "1" : "90")
+                << "kHz clock resolution.";
+
+  return true;
+}
+
+absl::optional<EncodedImage> IvfFileReader::NextFrame() {
+  if (has_error_ || !HasMoreFrames()) {
+    return absl::nullopt;
+  }
+
+  rtc::scoped_refptr<EncodedImageBuffer> payload = EncodedImageBuffer::Create();
+  std::vector<size_t> layer_sizes;
+  // next_frame_header_ have to be presented by the way how it was loaded. If it
+  // is missing it means there is a bug in error handling.
+  RTC_DCHECK(next_frame_header_);
+  int64_t current_timestamp = next_frame_header_->timestamp;
+  while (next_frame_header_ &&
+         current_timestamp == next_frame_header_->timestamp) {
+    // Resize payload to fit next spatial layer.
+    size_t current_layer_size = next_frame_header_->frame_size;
+    size_t current_layer_start_pos = payload->size();
+    payload->Realloc(payload->size() + current_layer_size);
+    layer_sizes.push_back(current_layer_size);
+
+    // Read next layer into payload
+    size_t read = file_.Read(&payload->data()[current_layer_start_pos],
+                             current_layer_size);
+    if (read != current_layer_size) {
+      RTC_LOG(LS_ERROR) << "Frame #" << num_read_frames_
+                        << ": failed to read frame payload";
+      has_error_ = true;
+      return absl::nullopt;
+    }
+    num_read_frames_++;
+
+    current_timestamp = next_frame_header_->timestamp;
+    next_frame_header_ = ReadNextFrameHeader();
+  }
+  if (!next_frame_header_) {
+    // If EOF was reached, we need to check that all frames were met.
+    if (!has_error_ && num_read_frames_ != num_frames_) {
+      RTC_LOG(LS_ERROR) << "Unexpected EOF";
+      has_error_ = true;
+      return absl::nullopt;
+    }
+  }
+
+  EncodedImage image;
+  if (using_capture_timestamps_) {
+    image.capture_time_ms_ = current_timestamp;
+    image.SetTimestamp(static_cast<uint32_t>(90 * current_timestamp));
+  } else {
+    image.SetTimestamp(static_cast<uint32_t>(current_timestamp));
+  }
+  image.SetEncodedData(payload);
+  image.SetSpatialIndex(static_cast<int>(layer_sizes.size()));
+  for (size_t i = 0; i < layer_sizes.size(); ++i) {
+    image.SetSpatialLayerFrameSize(static_cast<int>(i), layer_sizes[i]);
+  }
+
+  return image;
+}
+
+bool IvfFileReader::Close() {
+  if (!file_.is_open())
+    return false;
+
+  file_.Close();
+  return true;
+}
+
+absl::optional<VideoCodecType> IvfFileReader::ParseCodecType(uint8_t* buffer,
+                                                             size_t start_pos) {
+  if (memcmp(&buffer[start_pos], kVp8Header, kCodecTypeBytesCount) == 0) {
+    return VideoCodecType::kVideoCodecVP8;
+  }
+  if (memcmp(&buffer[start_pos], kVp9Header, kCodecTypeBytesCount) == 0) {
+    return VideoCodecType::kVideoCodecVP9;
+  }
+  if (memcmp(&buffer[start_pos], kH264Header, kCodecTypeBytesCount) == 0) {
+    return VideoCodecType::kVideoCodecH264;
+  }
+  has_error_ = true;
+  RTC_LOG(LS_ERROR) << "Unknown codec type: "
+                    << std::string(
+                           reinterpret_cast<char const*>(&buffer[start_pos]),
+                           kCodecTypeBytesCount);
+  return absl::nullopt;
+}
+
+absl::optional<IvfFileReader::FrameHeader>
+IvfFileReader::ReadNextFrameHeader() {
+  uint8_t ivf_frame_header[kIvfFrameHeaderSize] = {0};
+  size_t read = file_.Read(&ivf_frame_header, kIvfFrameHeaderSize);
+  if (read != kIvfFrameHeaderSize) {
+    if (read != 0 || !file_.ReadEof()) {
+      has_error_ = true;
+      RTC_LOG(LS_ERROR) << "Frame #" << num_read_frames_
+                        << ": failed to read IVF frame header";
+    }
+    return absl::nullopt;
+  }
+  FrameHeader header;
+  header.frame_size = static_cast<size_t>(
+      ByteReader<uint32_t>::ReadLittleEndian(&ivf_frame_header[0]));
+  header.timestamp =
+      ByteReader<uint64_t>::ReadLittleEndian(&ivf_frame_header[4]);
+
+  if (header.frame_size == 0) {
+    has_error_ = true;
+    RTC_LOG(LS_ERROR) << "Frame #" << num_read_frames_
+                      << ": invalid frame size";
+    return absl::nullopt;
+  }
+
+  if (header.timestamp < 0) {
+    has_error_ = true;
+    RTC_LOG(LS_ERROR) << "Frame #" << num_read_frames_
+                      << ": negative timestamp";
+    return absl::nullopt;
+  }
+
+  return header;
+}
+
+}  // namespace webrtc
diff --git a/modules/video_coding/utility/ivf_file_reader.h b/modules/video_coding/utility/ivf_file_reader.h
new file mode 100644
index 0000000..05b1d79
--- /dev/null
+++ b/modules/video_coding/utility/ivf_file_reader.h
@@ -0,0 +1,76 @@
+/*
+ *  Copyright (c) 2019 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#ifndef MODULES_VIDEO_CODING_UTILITY_IVF_FILE_READER_H_
+#define MODULES_VIDEO_CODING_UTILITY_IVF_FILE_READER_H_
+
+#include <memory>
+#include <utility>
+
+#include "absl/types/optional.h"
+#include "api/video/encoded_image.h"
+#include "rtc_base/system/file_wrapper.h"
+
+namespace webrtc {
+
+class IvfFileReader {
+ public:
+  // Creates IvfFileReader. Returns nullptr if error acquired.
+  static std::unique_ptr<IvfFileReader> Create(FileWrapper file);
+  ~IvfFileReader();
+  // Reinitializes reader. Returns false if any error acquired.
+  bool Reset();
+
+  // Returns codec type which was used to create this IVF file and which should
+  // be used to decode EncodedImages from this file.
+  VideoCodecType GetVideoCodecType() const { return codec_type_; }
+  // Returns count of frames in this file.
+  size_t GetFramesCount() const { return num_frames_; }
+
+  // Returns next frame or absl::nullopt if any error acquired. Always returns
+  // absl::nullopt after first error was spotted.
+  absl::optional<EncodedImage> NextFrame();
+  bool HasMoreFrames() const { return num_read_frames_ < num_frames_; }
+  bool HasError() const { return has_error_; }
+
+  bool Close();
+
+ private:
+  struct FrameHeader {
+    size_t frame_size;
+    int64_t timestamp;
+  };
+
+  explicit IvfFileReader(FileWrapper file) : file_(std::move(file)) {}
+
+  // Parses codec type from specified position of the buffer. Codec type
+  // contains kCodecTypeBytesCount bytes and caller has to ensure that buffer
+  // won't overflow.
+  absl::optional<VideoCodecType> ParseCodecType(uint8_t* buffer,
+                                                size_t start_pos);
+  absl::optional<FrameHeader> ReadNextFrameHeader();
+
+  VideoCodecType codec_type_;
+  size_t num_frames_;
+  size_t num_read_frames_;
+  uint16_t width_;
+  uint16_t height_;
+  bool using_capture_timestamps_;
+  FileWrapper file_;
+
+  absl::optional<FrameHeader> next_frame_header_;
+  bool has_error_;
+
+  RTC_DISALLOW_COPY_AND_ASSIGN(IvfFileReader);
+};
+
+}  // namespace webrtc
+
+#endif  // MODULES_VIDEO_CODING_UTILITY_IVF_FILE_READER_H_
diff --git a/modules/video_coding/utility/ivf_file_reader_unittest.cc b/modules/video_coding/utility/ivf_file_reader_unittest.cc
new file mode 100644
index 0000000..6ff5805
--- /dev/null
+++ b/modules/video_coding/utility/ivf_file_reader_unittest.cc
@@ -0,0 +1,173 @@
+/*
+ *  Copyright (c) 2019 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#include "modules/video_coding/utility/ivf_file_reader.h"
+#include "modules/video_coding/utility/ivf_file_writer.h"
+
+#include <memory>
+#include <string>
+
+#include "test/gtest.h"
+#include "test/testsupport/file_utils.h"
+
+namespace webrtc {
+namespace {
+
+constexpr int kWidth = 320;
+constexpr int kHeight = 240;
+constexpr int kNumFrames = 3;
+constexpr uint8_t kDummyPayload[4] = {'0', '1', '2', '3'};
+
+}  // namespace
+
+class IvfFileReaderTest : public ::testing::Test {
+ protected:
+  void SetUp() override {
+    file_name_ =
+        webrtc::test::TempFilename(webrtc::test::OutputPath(), "test_file.ivf");
+  }
+  void TearDown() override { webrtc::test::RemoveFile(file_name_); }
+
+  bool WriteDummyTestFrames(IvfFileWriter* file_writer,
+                            VideoCodecType codec_type,
+                            int width,
+                            int height,
+                            int num_frames,
+                            bool use_capture_tims_ms,
+                            int spatial_layers_count) {
+    EncodedImage frame;
+    frame.SetSpatialIndex(spatial_layers_count);
+    rtc::scoped_refptr<EncodedImageBuffer> payload = EncodedImageBuffer::Create(
+        sizeof(kDummyPayload) * spatial_layers_count);
+    for (int i = 0; i < spatial_layers_count; ++i) {
+      memcpy(&payload->data()[i * sizeof(kDummyPayload)], kDummyPayload,
+             sizeof(kDummyPayload));
+      frame.SetSpatialLayerFrameSize(i, sizeof(kDummyPayload));
+    }
+    frame.SetEncodedData(payload);
+    frame._encodedWidth = width;
+    frame._encodedHeight = height;
+    for (int i = 1; i <= num_frames; ++i) {
+      if (use_capture_tims_ms) {
+        frame.capture_time_ms_ = i;
+      } else {
+        frame.SetTimestamp(i);
+      }
+      if (!file_writer->WriteFrame(frame, codec_type))
+        return false;
+    }
+    return true;
+  }
+
+  void CreateTestFile(VideoCodecType codec_type,
+                      bool use_capture_tims_ms,
+                      int spatial_layers_count) {
+    std::unique_ptr<IvfFileWriter> file_writer =
+        IvfFileWriter::Wrap(FileWrapper::OpenWriteOnly(file_name_), 0);
+    ASSERT_TRUE(file_writer.get());
+    ASSERT_TRUE(WriteDummyTestFrames(file_writer.get(), codec_type, kWidth,
+                                     kHeight, kNumFrames, use_capture_tims_ms,
+                                     spatial_layers_count));
+    ASSERT_TRUE(file_writer->Close());
+  }
+
+  void ValidateFrame(absl::optional<EncodedImage> frame,
+                     int frame_index,
+                     bool use_capture_tims_ms,
+                     int spatial_layers_count) {
+    ASSERT_TRUE(frame);
+    EXPECT_EQ(frame->SpatialIndex(), spatial_layers_count);
+    if (use_capture_tims_ms) {
+      EXPECT_EQ(frame->capture_time_ms_, static_cast<int64_t>(frame_index));
+      EXPECT_EQ(frame->Timestamp(), static_cast<int64_t>(90 * frame_index));
+    } else {
+      EXPECT_EQ(frame->Timestamp(), static_cast<int64_t>(frame_index));
+    }
+    ASSERT_EQ(frame->size(), sizeof(kDummyPayload) * spatial_layers_count);
+    for (int i = 0; i < spatial_layers_count; ++i) {
+      EXPECT_EQ(memcmp(&frame->data()[i * sizeof(kDummyPayload)], kDummyPayload,
+                       sizeof(kDummyPayload)),
+                0)
+          << std::string(reinterpret_cast<char const*>(
+                             &frame->data()[i * sizeof(kDummyPayload)]),
+                         sizeof(kDummyPayload));
+    }
+  }
+
+  void ValidateContent(VideoCodecType codec_type,
+                       bool use_capture_tims_ms,
+                       int spatial_layers_count) {
+    std::unique_ptr<IvfFileReader> reader =
+        IvfFileReader::Create(FileWrapper::OpenReadOnly(file_name_));
+    ASSERT_TRUE(reader.get());
+    EXPECT_EQ(reader->GetVideoCodecType(), codec_type);
+    EXPECT_EQ(reader->GetFramesCount(),
+              spatial_layers_count * static_cast<size_t>(kNumFrames));
+    for (int i = 1; i <= kNumFrames; ++i) {
+      ASSERT_TRUE(reader->HasMoreFrames());
+      ValidateFrame(reader->NextFrame(), i, use_capture_tims_ms,
+                    spatial_layers_count);
+      EXPECT_FALSE(reader->HasError());
+    }
+    EXPECT_FALSE(reader->HasMoreFrames());
+    EXPECT_FALSE(reader->NextFrame());
+    EXPECT_FALSE(reader->HasError());
+    ASSERT_TRUE(reader->Close());
+  }
+
+  std::string file_name_;
+};
+
+TEST_F(IvfFileReaderTest, BasicVp8FileNtpTimestamp) {
+  CreateTestFile(kVideoCodecVP8, false, 1);
+  ValidateContent(kVideoCodecVP8, false, 1);
+}
+
+TEST_F(IvfFileReaderTest, BasicVP8FileMsTimestamp) {
+  CreateTestFile(kVideoCodecVP8, true, 1);
+  ValidateContent(kVideoCodecVP8, true, 1);
+}
+
+TEST_F(IvfFileReaderTest, BasicVP9FileNtpTimestamp) {
+  CreateTestFile(kVideoCodecVP9, false, 1);
+  ValidateContent(kVideoCodecVP9, false, 1);
+}
+
+TEST_F(IvfFileReaderTest, BasicVP9FileMsTimestamp) {
+  CreateTestFile(kVideoCodecVP9, true, 1);
+  ValidateContent(kVideoCodecVP9, true, 1);
+}
+
+TEST_F(IvfFileReaderTest, BasicH264FileNtpTimestamp) {
+  CreateTestFile(kVideoCodecH264, false, 1);
+  ValidateContent(kVideoCodecH264, false, 1);
+}
+
+TEST_F(IvfFileReaderTest, BasicH264FileMsTimestamp) {
+  CreateTestFile(kVideoCodecH264, true, 1);
+  ValidateContent(kVideoCodecH264, true, 1);
+}
+
+TEST_F(IvfFileReaderTest, MultilayerVp8FileNtpTimestamp) {
+  CreateTestFile(kVideoCodecVP8, false, 3);
+  ValidateContent(kVideoCodecVP8, false, 3);
+}
+
+TEST_F(IvfFileReaderTest, MultilayerVP9FileNtpTimestamp) {
+  CreateTestFile(kVideoCodecVP9, false, 3);
+  ValidateContent(kVideoCodecVP9, false, 3);
+}
+
+TEST_F(IvfFileReaderTest, MultilayerH264FileNtpTimestamp) {
+  CreateTestFile(kVideoCodecH264, false, 3);
+  ValidateContent(kVideoCodecH264, false, 3);
+}
+
+}  // namespace webrtc