diff --git a/CMakeLists.txt b/CMakeLists.txt index 13b2dbd..d5d5c33 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -151,6 +151,14 @@ if (ENABLE_TESTS) "${LIBWEBM_SRC_DIR}/testing/test_util.h") target_link_libraries(parser_tests LINK_PUBLIC gtest webm) + add_executable(vp9_header_parser_tests + "${LIBWEBM_SRC_DIR}/common/vp9_header_parser_tests.cc" + "${LIBWEBM_SRC_DIR}/common/vp9_header_parser.cc" + "${LIBWEBM_SRC_DIR}/common/vp9_header_parser.h" + "${LIBWEBM_SRC_DIR}/testing/test_util.cc" + "${LIBWEBM_SRC_DIR}/testing/test_util.h") + target_link_libraries(vp9_header_parser_tests LINK_PUBLIC gtest webm) + if (ENABLE_WEBMTS) add_executable(webm2pes_tests "${LIBWEBM_SRC_DIR}/testing/test_util.cc" diff --git a/common/vp9_header_parser.cc b/common/vp9_header_parser.cc new file mode 100644 index 0000000..8590594 --- /dev/null +++ b/common/vp9_header_parser.cc @@ -0,0 +1,262 @@ +// Copyright (c) 2016 The WebM 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 "common/vp9_header_parser.h" + +#include + +namespace vp9_parser { + +bool Vp9HeaderParser::SetFrame(const uint8_t* frame, size_t length) { + if (!frame || length == 0) + return false; + + frame_ = frame; + frame_size_ = length; + bit_offset_ = 0; + profile_ = -1; + show_existing_frame_ = 0; + key_ = 0; + altref_ = 0; + error_resilient_mode_ = 0; + intra_only_ = 0; + reset_frame_context_ = 0; + color_space_ = 0; + color_range_ = 0; + subsampling_x_ = 0; + subsampling_y_ = 0; + refresh_frame_flags_ = 0; + return true; +} + +bool Vp9HeaderParser::ParseUncompressedHeader() { + const int frame_marker = VpxReadLiteral(2); + if (frame_marker != kVp9FrameMarker) { + fprintf(stderr, "Invalid VP9 frame_marker:%d\n", frame_marker); + return false; + } + + profile_ = VpxReadLiteral(2); + if (profile_ > 2) + profile_ += ReadBit(); + + // TODO(fgalligan): Decide how to handle show existing frames. + show_existing_frame_ = ReadBit(); + if (show_existing_frame_) + return true; + + key_ = !ReadBit(); + altref_ = !ReadBit(); + error_resilient_mode_ = ReadBit(); + if (key_) { + if (!ValidateVp9SyncCode()) { + fprintf(stderr, "Invalid Sync code!\n"); + return false; + } + + ParseColorSpace(); + ParseFrameResolution(); + ParseFrameParallelMode(); + ParseTileInfo(); + } else { + intra_only_ = altref_ ? ReadBit() : 0; + reset_frame_context_ = error_resilient_mode_ ? 0 : VpxReadLiteral(2); + if (intra_only_) { + if (!ValidateVp9SyncCode()) { + fprintf(stderr, "Invalid Sync code!\n"); + return false; + } + + if (profile_ > 0) { + ParseColorSpace(); + } else { + // NOTE: The intra-only frame header does not include the specification + // of either the color format or color sub-sampling in profile 0. VP9 + // specifies that the default color format should be YUV 4:2:0 in this + // case (normative). + color_space_ = kVpxCsBt601; + color_range_ = kVpxCrStudioRange; + subsampling_y_ = subsampling_x_ = 1; + bit_depth_ = 8; + } + + refresh_frame_flags_ = VpxReadLiteral(kRefFrames); + ParseFrameResolution(); + } else { + refresh_frame_flags_ = VpxReadLiteral(kRefFrames); + for (int i = 0; i < kRefsPerFrame; ++i) { + VpxReadLiteral(kRefFrames_LOG2); // Consume ref. + ReadBit(); // Consume ref sign bias. + } + + bool found = false; + for (int i = 0; i < kRefsPerFrame; ++i) { + if (ReadBit()) { + // Found previous reference, width and height did not change since + // last frame. + found = true; + break; + } + } + + if (!found) + ParseFrameResolution(); + } + } + return true; +} + +int Vp9HeaderParser::ReadBit() { + const size_t off = bit_offset_; + const size_t byte_offset = off >> 3; + const int bit_shift = 7 - static_cast(off & 0x7); + if (byte_offset < frame_size_) { + const int bit = (frame_[byte_offset] >> bit_shift) & 1; + bit_offset_++; + return bit; + } else { + return 0; + } +} + +int Vp9HeaderParser::VpxReadLiteral(int bits) { + int value = 0; + for (int bit = bits - 1; bit >= 0; --bit) + value |= ReadBit() << bit; + return value; +} + +bool Vp9HeaderParser::ValidateVp9SyncCode() { + const int sync_code_0 = VpxReadLiteral(8); + const int sync_code_1 = VpxReadLiteral(8); + const int sync_code_2 = VpxReadLiteral(8); + return (sync_code_0 == 0x49 && sync_code_1 == 0x83 && sync_code_2 == 0x42); +} + +void Vp9HeaderParser::ParseColorSpace() { + bit_depth_ = 0; + if (profile_ >= 2) + bit_depth_ = ReadBit() ? 12 : 10; + else + bit_depth_ = 8; + color_space_ = VpxReadLiteral(3); + if (color_space_ != kVpxCsSrgb) { + color_range_ = ReadBit(); + if (profile_ == 1 || profile_ == 3) { + subsampling_x_ = ReadBit(); + subsampling_y_ = ReadBit(); + ReadBit(); + } else { + subsampling_y_ = subsampling_x_ = 1; + } + } else { + color_range_ = kVpxCrFullRange; + if (profile_ == 1 || profile_ == 3) { + subsampling_y_ = subsampling_x_ = 0; + ReadBit(); + } + } +} + +void Vp9HeaderParser::ParseFrameResolution() { + width_ = VpxReadLiteral(16) + 1; + height_ = VpxReadLiteral(16) + 1; +} + +void Vp9HeaderParser::ParseFrameParallelMode() { + if (ReadBit()) { + VpxReadLiteral(16); // display width + VpxReadLiteral(16); // display height + } + if (!error_resilient_mode_) { + ReadBit(); // Consume refresh frame context + frame_parallel_mode_ = ReadBit(); + } else { + frame_parallel_mode_ = 1; + } +} + +void Vp9HeaderParser::ParseTileInfo() { + VpxReadLiteral(2); // Consume frame context index + + // loopfilter + VpxReadLiteral(6); // Consume filter level + VpxReadLiteral(3); // Consume sharpness level + + const bool mode_ref_delta_enabled = ReadBit(); + if (mode_ref_delta_enabled) { + const bool mode_ref_delta_update = ReadBit(); + if (mode_ref_delta_update) { + const int kMaxRefLFDeltas = 4; + for (int i = 0; i < kMaxRefLFDeltas; ++i) { + if (ReadBit()) + VpxReadLiteral(7); // Consume ref_deltas + sign + } + + const int kMaxModeDeltas = 2; + for (int i = 0; i < kMaxModeDeltas; ++i) { + if (ReadBit()) + VpxReadLiteral(7); // Consume mode_delta + sign + } + } + } + + // quantization + VpxReadLiteral(8); // Consume base_q + SkipDeltaQ(); // y dc + SkipDeltaQ(); // uv ac + SkipDeltaQ(); // uv dc + + // segmentation + const bool segmentation_enabled = ReadBit(); + if (!segmentation_enabled) { + const int aligned_width = AlignPowerOfTwo(width_, kMiSizeLog2); + const int mi_cols = aligned_width >> kMiSizeLog2; + const int aligned_mi_cols = AlignPowerOfTwo(mi_cols, kMiSizeLog2); + const int sb_cols = aligned_mi_cols >> 3; // to_sbs(mi_cols); + int min_log2_n_tiles, max_log2_n_tiles; + + for (max_log2_n_tiles = 0; + (sb_cols >> max_log2_n_tiles) >= kMinTileWidthB64; + max_log2_n_tiles++) { + } + max_log2_n_tiles--; + if (max_log2_n_tiles < 0) + max_log2_n_tiles = 0; + + for (min_log2_n_tiles = 0; (kMaxTileWidthB64 << min_log2_n_tiles) < sb_cols; + min_log2_n_tiles++) { + } + + // columns + const int max_log2_tile_cols = max_log2_n_tiles; + const int min_log2_tile_cols = min_log2_n_tiles; + int max_ones = max_log2_tile_cols - min_log2_tile_cols; + int log2_tile_cols = min_log2_tile_cols; + while (max_ones-- && ReadBit()) + log2_tile_cols++; + + // rows + int log2_tile_rows = ReadBit(); + if (log2_tile_rows) + log2_tile_rows += ReadBit(); + + row_tiles_ = 1 << log2_tile_rows; + column_tiles_ = 1 << log2_tile_cols; + } +} + +void Vp9HeaderParser::SkipDeltaQ() { + if (ReadBit()) + VpxReadLiteral(4); +} + +int Vp9HeaderParser::AlignPowerOfTwo(int value, int n) { + return (((value) + ((1 << (n)) - 1)) & ~((1 << (n)) - 1)); +} + +} // namespace vp9_parser diff --git a/common/vp9_header_parser.h b/common/vp9_header_parser.h new file mode 100644 index 0000000..7f2d714 --- /dev/null +++ b/common/vp9_header_parser.h @@ -0,0 +1,122 @@ +// Copyright (c) 2016 The WebM 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 LIBWEBM_COMMON_VP9_HEADER_PARSER_H_ +#define LIBWEBM_COMMON_VP9_HEADER_PARSER_H_ + +#include +#include + +namespace vp9_parser { + +const int kVp9FrameMarker = 2; +const int kMinTileWidthB64 = 4; +const int kMaxTileWidthB64 = 64; +const int kRefFrames = 8; +const int kRefsPerFrame = 3; +const int kRefFrames_LOG2 = 3; +const int kVpxCsBt601 = 1; +const int kVpxCsSrgb = 7; +const int kVpxCrStudioRange = 0; +const int kVpxCrFullRange = 1; +const int kMiSizeLog2 = 3; + +// Class to parse the header of a VP9 frame. +class Vp9HeaderParser { + public: + Vp9HeaderParser() + : frame_(NULL), + frame_size_(0), + bit_offset_(0), + profile_(-1), + show_existing_frame_(0), + key_(0), + altref_(0), + error_resilient_mode_(0), + intra_only_(0), + reset_frame_context_(0), + bit_depth_(0), + color_space_(0), + color_range_(0), + subsampling_x_(0), + subsampling_y_(0), + refresh_frame_flags_(0), + width_(0), + height_(0), + row_tiles_(0), + column_tiles_(0), + frame_parallel_mode_(0) {} + + // Set the compressed VP9 frame. This must be called before + // ParseUncompressedHeader. + bool SetFrame(const uint8_t* frame, size_t length); + + // Parse the VP9 uncompressed header. + bool ParseUncompressedHeader(); + + size_t frame_size() const { return frame_size_; } + int key() const { return key_; } + int altref() const { return altref_; } + int bit_depth() const { return bit_depth_; } + int width() const { return width_; } + int height() const { return height_; } + int refresh_frame_flags() const { return refresh_frame_flags_; } + int row_tiles() const { return row_tiles_; } + int column_tiles() const { return column_tiles_; } + int frame_parallel_mode() const { return frame_parallel_mode_; } + + private: + // Returns the next bit of the frame. + int ReadBit(); + + // Returns the next |bits| of the frame. + int VpxReadLiteral(int bits); + + // Returns true if the vp9 sync code is valid. + bool ValidateVp9SyncCode(); + + // Parses bit_depth_, color_space_, subsampling_x_, subsampling_y_, and + // color_range_. + void ParseColorSpace(); + + // Parses width and height of the frame. + void ParseFrameResolution(); + + // Parses frame_parallel_mode_. This function skips over some features. + void ParseFrameParallelMode(); + + // Parses row and column tiles. This function skips over some features. + void ParseTileInfo(); + void SkipDeltaQ(); + int AlignPowerOfTwo(int value, int n); + + const uint8_t* frame_; + size_t frame_size_; + size_t bit_offset_; + int profile_; + int show_existing_frame_; + int key_; + int altref_; + int error_resilient_mode_; + int intra_only_; + int reset_frame_context_; + int bit_depth_; + int color_space_; + int color_range_; + int subsampling_x_; + int subsampling_y_; + int refresh_frame_flags_; + int width_; + int height_; + int row_tiles_; + int column_tiles_; + int frame_parallel_mode_; +}; + +} // namespace vp9_parser + +#endif // LIBWEBM_COMMON_VP9_HEADER_PARSER_H_ diff --git a/common/vp9_header_parser_tests.cc b/common/vp9_header_parser_tests.cc new file mode 100644 index 0000000..1924f27 --- /dev/null +++ b/common/vp9_header_parser_tests.cc @@ -0,0 +1,144 @@ +// Copyright (c) 2016 The WebM 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 "common/vp9_header_parser.h" + +#include + +#include "gtest/gtest.h" + +#include "common/hdr_util.h" +#include "mkvparser/mkvparser.h" +#include "mkvparser/mkvreader.h" +#include "testing/test_util.h" + +namespace { + +class Vp9HeaderParserTests : public ::testing::Test { + public: + Vp9HeaderParserTests() : is_reader_open_(false), segment_(NULL) {} + + ~Vp9HeaderParserTests() override { + CloseReader(); + if (segment_ != NULL) { + delete segment_; + segment_ = NULL; + } + } + + void CloseReader() { + if (is_reader_open_) { + reader_.Close(); + } + is_reader_open_ = false; + } + + void CreateAndLoadSegment(const std::string& filename, + int expected_doc_type_ver) { + filename_ = test::GetTestFilePath(filename); + ASSERT_EQ(0, reader_.Open(filename_.c_str())); + is_reader_open_ = true; + pos_ = 0; + mkvparser::EBMLHeader ebml_header; + ebml_header.Parse(&reader_, pos_); + ASSERT_EQ(1, ebml_header.m_version); + ASSERT_EQ(1, ebml_header.m_readVersion); + ASSERT_STREQ("webm", ebml_header.m_docType); + ASSERT_EQ(expected_doc_type_ver, ebml_header.m_docTypeVersion); + ASSERT_EQ(2, ebml_header.m_docTypeReadVersion); + ASSERT_EQ(0, mkvparser::Segment::CreateInstance(&reader_, pos_, segment_)); + ASSERT_FALSE(HasFailure()); + ASSERT_GE(0, segment_->Load()); + } + + void CreateAndLoadSegment(const std::string& filename) { + CreateAndLoadSegment(filename, 2); + } + + void ProcessTheFrames() { + unsigned char* data = NULL; + size_t data_len = 0; + const mkvparser::Tracks* const parser_tracks = segment_->GetTracks(); + ASSERT_TRUE(parser_tracks != NULL); + const mkvparser::Cluster* cluster = segment_->GetFirst(); + ASSERT_TRUE(cluster != NULL); + + while ((cluster != NULL) && !cluster->EOS()) { + const mkvparser::BlockEntry* block_entry; + long status = cluster->GetFirst(block_entry); // NOLINT + ASSERT_EQ(0, status); + + while ((block_entry != NULL) && !block_entry->EOS()) { + const mkvparser::Block* const block = block_entry->GetBlock(); + ASSERT_TRUE(block != NULL); + const long long trackNum = block->GetTrackNumber(); // NOLINT + const mkvparser::Track* const parser_track = + parser_tracks->GetTrackByNumber( + static_cast(trackNum)); // NOLINT + ASSERT_TRUE(parser_track != NULL); + const long long track_type = parser_track->GetType(); // NOLINT + + if (track_type == mkvparser::Track::kVideo) { + const int frame_count = block->GetFrameCount(); + + for (int i = 0; i < frame_count; ++i) { + const mkvparser::Block::Frame& frame = block->GetFrame(i); + + if (static_cast(frame.len) > data_len) { + delete[] data; + data = new unsigned char[frame.len]; + ASSERT_TRUE(data != NULL); + data_len = static_cast(frame.len); + } + ASSERT_FALSE(frame.Read(&reader_, data)); + parser_.SetFrame(data, data_len); + parser_.ParseUncompressedHeader(); + } + } + + status = cluster->GetNext(block_entry, block_entry); + ASSERT_EQ(0, status); + } + + cluster = segment_->GetNext(cluster); + } + delete[] data; + } + + protected: + mkvparser::MkvReader reader_; + bool is_reader_open_; + mkvparser::Segment* segment_; + std::string filename_; + long long pos_; // NOLINT + vp9_parser::Vp9HeaderParser parser_; +}; + +TEST_F(Vp9HeaderParserTests, VideoOnlyFile) { + CreateAndLoadSegment("test_stereo_left_right.webm"); + ProcessTheFrames(); + EXPECT_EQ(256, parser_.width()); + EXPECT_EQ(144, parser_.height()); + EXPECT_EQ(1, parser_.column_tiles()); + EXPECT_EQ(0, parser_.frame_parallel_mode()); +} + +TEST_F(Vp9HeaderParserTests, Muxed) { + CreateAndLoadSegment("bbb_480p_vp9_opus_1second.webm", 4); + ProcessTheFrames(); + EXPECT_EQ(854, parser_.width()); + EXPECT_EQ(480, parser_.height()); + EXPECT_EQ(2, parser_.column_tiles()); + EXPECT_EQ(1, parser_.frame_parallel_mode()); +} + +} // namespace + +int main(int argc, char* argv[]) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +}