From 5831ddad6509443a99d8c61cb2bb7cf010ea3673 Mon Sep 17 00:00:00 2001 From: Artem Titov Date: Wed, 20 Nov 2019 13:30:19 +0100 Subject: [PATCH] Introduce IVF file reader Bug: webrtc:10138 Change-Id: I97d332942f4e645527330159efefb1cb1d8034a0 Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/160008 Commit-Queue: Artem Titov Reviewed-by: Ilya Nikolaevskiy Cr-Commit-Position: refs/heads/master@{#29844} --- modules/video_coding/BUILD.gn | 3 + .../video_coding/utility/ivf_file_reader.cc | 234 ++++++++++++++++++ .../video_coding/utility/ivf_file_reader.h | 76 ++++++ .../utility/ivf_file_reader_unittest.cc | 173 +++++++++++++ 4 files changed, 486 insertions(+) create mode 100644 modules/video_coding/utility/ivf_file_reader.cc create mode 100644 modules/video_coding/utility/ivf_file_reader.h create mode 100644 modules/video_coding/utility/ivf_file_reader_unittest.cc diff --git a/modules/video_coding/BUILD.gn b/modules/video_coding/BUILD.gn index 627000d6e7..71e14fa780 100644 --- a/modules/video_coding/BUILD.gn +++ b/modules/video_coding/BUILD.gn @@ -263,6 +263,8 @@ rtc_library("video_coding_utility") { "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 @@ if (rtc_include_tests) { "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 0000000000..8703a29c37 --- /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 +#include + +#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::Create(FileWrapper file) { + auto reader = + std::unique_ptr(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 codec_type = ParseCodecType(ivf_header, 8); + if (!codec_type) { + return false; + } + codec_type_ = *codec_type; + + width_ = ByteReader::ReadLittleEndian(&ivf_header[12]); + height_ = ByteReader::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::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( + ByteReader::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 IvfFileReader::NextFrame() { + if (has_error_ || !HasMoreFrames()) { + return absl::nullopt; + } + + rtc::scoped_refptr payload = EncodedImageBuffer::Create(); + std::vector 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(90 * current_timestamp)); + } else { + image.SetTimestamp(static_cast(current_timestamp)); + } + image.SetEncodedData(payload); + image.SetSpatialIndex(static_cast(layer_sizes.size())); + for (size_t i = 0; i < layer_sizes.size(); ++i) { + image.SetSpatialLayerFrameSize(static_cast(i), layer_sizes[i]); + } + + return image; +} + +bool IvfFileReader::Close() { + if (!file_.is_open()) + return false; + + file_.Close(); + return true; +} + +absl::optional 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(&buffer[start_pos]), + kCodecTypeBytesCount); + return absl::nullopt; +} + +absl::optional +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( + ByteReader::ReadLittleEndian(&ivf_frame_header[0])); + header.timestamp = + ByteReader::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 0000000000..05b1d79cdf --- /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 +#include + +#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 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 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 ParseCodecType(uint8_t* buffer, + size_t start_pos); + absl::optional 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 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 0000000000..6ff580511b --- /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 +#include + +#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 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 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 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(frame_index)); + EXPECT_EQ(frame->Timestamp(), static_cast(90 * frame_index)); + } else { + EXPECT_EQ(frame->Timestamp(), static_cast(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( + &frame->data()[i * sizeof(kDummyPayload)]), + sizeof(kDummyPayload)); + } + } + + void ValidateContent(VideoCodecType codec_type, + bool use_capture_tims_ms, + int spatial_layers_count) { + std::unique_ptr 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(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