| /* | |
| * Copyright (c) 2026 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 "test/testsupport/y4m_header_parser.h" | |
| #include <stdio.h> | |
| #include <cstdio> | |
| #include <optional> | |
| #include <string> | |
| #include <vector> | |
| #include "api/units/data_size.h" | |
| #include "api/units/frequency.h" | |
| #include "api/video/color_space.h" | |
| #include "api/video/resolution.h" | |
| #include "test/gmock.h" | |
| #include "test/gtest.h" | |
| #include "test/testsupport/file_utils.h" | |
| namespace webrtc { | |
| namespace test { | |
| using ::testing::AllOf; | |
| using ::testing::ElementsAre; | |
| using ::testing::Eq; | |
| using ::testing::Field; | |
| TEST(Y4mHeaderParserTest, TokenizeValidHeader) { | |
| const std::string header_str = "YUV4MPEG2 W1920 H1080 F30:1 C420 Ip A1:1\n"; | |
| std::optional<std::vector<Y4mHeaderToken>> tokens = | |
| TokenizeY4mHeader(header_str); | |
| ASSERT_TRUE(tokens.has_value()); | |
| EXPECT_THAT(*tokens, ElementsAre(Y4mHeaderToken{.tag = 'W', .value = "1920"}, | |
| Y4mHeaderToken{.tag = 'H', .value = "1080"}, | |
| Y4mHeaderToken{.tag = 'F', .value = "30:1"}, | |
| Y4mHeaderToken{.tag = 'C', .value = "420"}, | |
| Y4mHeaderToken{.tag = 'I', .value = "p"}, | |
| Y4mHeaderToken{.tag = 'A', .value = "1:1"})); | |
| } | |
| TEST(Y4mHeaderParserTest, TokenizeEmptyTagsReturnsEmptyTokensList) { | |
| const std::string header_str = "YUV4MPEG2\n"; | |
| std::optional<std::vector<Y4mHeaderToken>> tokens = | |
| TokenizeY4mHeader(header_str); | |
| ASSERT_TRUE(tokens.has_value()); | |
| EXPECT_TRUE(tokens->empty()); | |
| } | |
| TEST(Y4mHeaderParserTest, RejectsDuplicateWidth) { | |
| EXPECT_FALSE(ParseY4mHeader("YUV4MPEG2 W640 H480 W1280 F30:1\n").has_value()); | |
| } | |
| TEST(Y4mHeaderParserTest, RejectsDuplicateHeight) { | |
| EXPECT_FALSE(ParseY4mHeader("YUV4MPEG2 W640 H480 H720 F30:1\n").has_value()); | |
| } | |
| TEST(Y4mHeaderParserTest, RejectsDuplicateFramerate) { | |
| EXPECT_FALSE(ParseY4mHeader("YUV4MPEG2 W640 H480 F30:1 F60:1\n").has_value()); | |
| } | |
| TEST(Y4mHeaderParserTest, RejectsDuplicateColorSpace) { | |
| EXPECT_FALSE( | |
| ParseY4mHeader("YUV4MPEG2 W640 H480 C420 C420jpeg\n").has_value()); | |
| } | |
| TEST(Y4mHeaderParserTest, TokenizeInvalidMagicReturnsNullopt) { | |
| EXPECT_FALSE(TokenizeY4mHeader("NOT_Y4M W10 H10\n").has_value()); | |
| } | |
| TEST(Y4mHeaderParserTest, MinimalHeaderWithoutFps) { | |
| const std::string header_str = "YUV4MPEG2 W1 H1\n"; | |
| std::optional<Y4mHeader> header = ParseY4mHeader(header_str); | |
| ASSERT_TRUE(header.has_value()); | |
| EXPECT_THAT(*header, AllOf(Field(&Y4mHeader::resolution, | |
| Eq(Resolution{.width = 1, .height = 1})), | |
| Field(&Y4mHeader::header_size, | |
| Eq(DataSize::Bytes(header_str.size()))), | |
| Field(&Y4mHeader::framerate, Eq(std::nullopt)), | |
| Field(&Y4mHeader::color_space, Eq(std::nullopt)))); | |
| } | |
| TEST(Y4mHeaderParserTest, MinimalHeaderWithFps) { | |
| const std::string header_str = "YUV4MPEG2 W2 H2 F2:1\n"; | |
| std::optional<Y4mHeader> header = ParseY4mHeader(header_str); | |
| ASSERT_TRUE(header.has_value()); | |
| EXPECT_THAT(*header, | |
| AllOf(Field(&Y4mHeader::resolution, | |
| Eq(Resolution{.width = 2, .height = 2})), | |
| Field(&Y4mHeader::header_size, | |
| Eq(DataSize::Bytes(header_str.size()))), | |
| Field(&Y4mHeader::framerate, Eq(Frequency::Hertz(2))))); | |
| } | |
| TEST(Y4mHeaderParserTest, ArbitraryTagOrderingAndRecognizedColorSpace) { | |
| // Colour space (C420) comes before width and height, interlace and aspect | |
| // ratio tags included. | |
| const std::string header_str = "YUV4MPEG2 C420 W640 H360 Ip F30:1 A1:1\n"; | |
| std::optional<Y4mHeader> header = ParseY4mHeader(header_str); | |
| ASSERT_TRUE(header.has_value()); | |
| EXPECT_THAT(*header, | |
| AllOf(Field(&Y4mHeader::resolution, | |
| Eq(Resolution{.width = 640, .height = 360})), | |
| Field(&Y4mHeader::header_size, | |
| Eq(DataSize::Bytes(header_str.size()))), | |
| Field(&Y4mHeader::framerate, Eq(Frequency::Hertz(30))))); | |
| ASSERT_TRUE(header->color_space.has_value()); | |
| EXPECT_EQ(header->color_space->chroma_siting_horizontal(), | |
| ColorSpace::ChromaSiting::kHalf); | |
| EXPECT_EQ(header->color_space->chroma_siting_vertical(), | |
| ColorSpace::ChromaSiting::kHalf); | |
| } | |
| TEST(Y4mHeaderParserTest, FractionalFramerate) { | |
| const std::string header_str = "YUV4MPEG2 W1920 H1080 F30000:1001 C420\n"; | |
| std::optional<Y4mHeader> header = ParseY4mHeader(header_str); | |
| ASSERT_TRUE(header.has_value()); | |
| // 30000 / 1001 = 29.97002997... Hz = 29970 mHz. | |
| EXPECT_THAT( | |
| *header, | |
| AllOf(Field(&Y4mHeader::resolution, | |
| Eq(Resolution{.width = 1920, .height = 1080})), | |
| Field(&Y4mHeader::framerate, Eq(Frequency::MilliHertz(29970))))); | |
| // Frequency::hertz() rounds to the nearest integer, so 29.97 Hz rounds to 30. | |
| EXPECT_EQ(header->framerate->hertz(), 30); | |
| } | |
| TEST(Y4mHeaderParserTest, ColorSpace420JpegRecognized) { | |
| std::optional<Y4mHeader> header = | |
| ParseY4mHeader("YUV4MPEG2 W640 H360 C420jpeg\n"); | |
| ASSERT_TRUE(header.has_value()); | |
| ASSERT_TRUE(header->color_space.has_value()); | |
| EXPECT_EQ(header->color_space->chroma_siting_horizontal(), | |
| ColorSpace::ChromaSiting::kHalf); | |
| EXPECT_EQ(header->color_space->chroma_siting_vertical(), | |
| ColorSpace::ChromaSiting::kHalf); | |
| } | |
| TEST(Y4mHeaderParserTest, ColorSpace420Mpeg2Recognized) { | |
| std::optional<Y4mHeader> header = | |
| ParseY4mHeader("YUV4MPEG2 W640 H360 C420mpeg2\n"); | |
| ASSERT_TRUE(header.has_value()); | |
| ASSERT_TRUE(header->color_space.has_value()); | |
| EXPECT_EQ(header->color_space->chroma_siting_horizontal(), | |
| ColorSpace::ChromaSiting::kCollocated); | |
| EXPECT_EQ(header->color_space->chroma_siting_vertical(), | |
| ColorSpace::ChromaSiting::kHalf); | |
| } | |
| TEST(Y4mHeaderParserTest, ColorSpace420PalDvRecognized) { | |
| std::optional<Y4mHeader> header = | |
| ParseY4mHeader("YUV4MPEG2 W640 H360 C420paldv\n"); | |
| ASSERT_TRUE(header.has_value()); | |
| ASSERT_TRUE(header->color_space.has_value()); | |
| EXPECT_EQ(header->color_space->chroma_siting_horizontal(), | |
| ColorSpace::ChromaSiting::kCollocated); | |
| EXPECT_EQ(header->color_space->chroma_siting_vertical(), | |
| ColorSpace::ChromaSiting::kCollocated); | |
| } | |
| TEST(Y4mHeaderParserTest, RejectsUnknownColorSpaceString) { | |
| EXPECT_FALSE(ParseY4mHeader("YUV4MPEG2 W640 H360 Cunknown\n").has_value()); | |
| } | |
| TEST(Y4mHeaderParserTest, RejectsUnsupportedColorSpaceString) { | |
| EXPECT_FALSE(ParseY4mHeader("YUV4MPEG2 W640 H360 Cbad_space\n").has_value()); | |
| } | |
| TEST(Y4mHeaderParserTest, HandlesWindowsCrlf) { | |
| const std::string header_str = "YUV4MPEG2 W320 H240 F15:1\r\n"; | |
| std::optional<Y4mHeader> header = ParseY4mHeader(header_str); | |
| ASSERT_TRUE(header.has_value()); | |
| EXPECT_THAT(*header, | |
| AllOf(Field(&Y4mHeader::resolution, | |
| Eq(Resolution{.width = 320, .height = 240})), | |
| Field(&Y4mHeader::header_size, | |
| Eq(DataSize::Bytes(header_str.size()))), | |
| Field(&Y4mHeader::framerate, Eq(Frequency::Hertz(15))))); | |
| } | |
| TEST(Y4mHeaderParserTest, HandlesExtraWhitespace) { | |
| const std::string header_str = "YUV4MPEG2 W800 H600 F60:1 \n"; | |
| std::optional<Y4mHeader> header = ParseY4mHeader(header_str); | |
| ASSERT_TRUE(header.has_value()); | |
| EXPECT_THAT(*header, | |
| AllOf(Field(&Y4mHeader::resolution, | |
| Eq(Resolution{.width = 800, .height = 600})), | |
| Field(&Y4mHeader::header_size, | |
| Eq(DataSize::Bytes(header_str.size()))), | |
| Field(&Y4mHeader::framerate, Eq(Frequency::Hertz(60))))); | |
| } | |
| TEST(Y4mHeaderParserTest, HandlesCommentsAndMetadata) { | |
| const std::string header_str = | |
| "YUV4MPEG2 W1280 H720 F25:1 Xsource=cam1 Xscene=room\n"; | |
| std::optional<Y4mHeader> header = ParseY4mHeader(header_str); | |
| ASSERT_TRUE(header.has_value()); | |
| EXPECT_THAT(*header, | |
| AllOf(Field(&Y4mHeader::resolution, | |
| Eq(Resolution{.width = 1280, .height = 720})), | |
| Field(&Y4mHeader::header_size, | |
| Eq(DataSize::Bytes(header_str.size()))), | |
| Field(&Y4mHeader::framerate, Eq(Frequency::Hertz(25))))); | |
| } | |
| TEST(Y4mHeaderParserTest, MaximumHeaderSize) { | |
| std::string header_str = "YUV4MPEG2 W1280 H720 F30:1 X"; | |
| // Pad with comment data up to exactly kMaxY4mHeaderSizeBytes. | |
| header_str.append(kMaxY4mHeaderSizeBytes - header_str.size() - 1, 'c'); | |
| header_str.push_back('\n'); | |
| ASSERT_EQ(header_str.size(), kMaxY4mHeaderSizeBytes); | |
| std::optional<Y4mHeader> header = ParseY4mHeader(header_str); | |
| ASSERT_TRUE(header.has_value()); | |
| EXPECT_THAT(*header, | |
| AllOf(Field(&Y4mHeader::resolution, | |
| Eq(Resolution{.width = 1280, .height = 720})), | |
| Field(&Y4mHeader::header_size, | |
| Eq(DataSize::Bytes(kMaxY4mHeaderSizeBytes))))); | |
| } | |
| TEST(Y4mHeaderParserTest, RejectsHeaderExceedingMaximumSize) { | |
| std::string header_str = "YUV4MPEG2 W1280 H720 F30:1 X"; | |
| header_str.append(kMaxY4mHeaderSizeBytes, 'c'); | |
| header_str.push_back('\n'); | |
| EXPECT_FALSE(ParseY4mHeader(header_str).has_value()); | |
| } | |
| TEST(Y4mHeaderParserTest, RejectsMissingNewline) { | |
| EXPECT_FALSE(ParseY4mHeader("YUV4MPEG2 W640 H480 F30:1").has_value()); | |
| } | |
| TEST(Y4mHeaderParserTest, RejectsInvalidMagic) { | |
| EXPECT_FALSE(ParseY4mHeader("NOTY4M W640 H480 F30:1\n").has_value()); | |
| } | |
| TEST(Y4mHeaderParserTest, RejectsMissingHeight) { | |
| EXPECT_FALSE(ParseY4mHeader("YUV4MPEG2 W640 F30:1\n").has_value()); | |
| } | |
| TEST(Y4mHeaderParserTest, RejectsMissingWidth) { | |
| EXPECT_FALSE(ParseY4mHeader("YUV4MPEG2 H480 F30:1\n").has_value()); | |
| } | |
| TEST(Y4mHeaderParserTest, RejectsMissingBothDimensions) { | |
| EXPECT_FALSE(ParseY4mHeader("YUV4MPEG2 F30:1\n").has_value()); | |
| } | |
| TEST(Y4mHeaderParserTest, RejectsZeroWidth) { | |
| EXPECT_FALSE(ParseY4mHeader("YUV4MPEG2 W0 H480 F30:1\n").has_value()); | |
| } | |
| TEST(Y4mHeaderParserTest, RejectsNegativeWidth) { | |
| EXPECT_FALSE(ParseY4mHeader("YUV4MPEG2 W-640 H480 F30:1\n").has_value()); | |
| } | |
| TEST(Y4mHeaderParserTest, RejectsZeroHeight) { | |
| EXPECT_FALSE(ParseY4mHeader("YUV4MPEG2 W640 H0 F30:1\n").has_value()); | |
| } | |
| TEST(Y4mHeaderParserTest, RejectsNegativeHeight) { | |
| EXPECT_FALSE(ParseY4mHeader("YUV4MPEG2 W640 H-480 F30:1\n").has_value()); | |
| } | |
| TEST(Y4mHeaderParserTest, RejectsNonNumericWidth) { | |
| EXPECT_FALSE(ParseY4mHeader("YUV4MPEG2 Wbad H480 F30:1\n").has_value()); | |
| } | |
| TEST(Y4mHeaderParserTest, RejectsNonNumericHeight) { | |
| EXPECT_FALSE(ParseY4mHeader("YUV4MPEG2 W640 Hbad F30:1\n").has_value()); | |
| } | |
| TEST(Y4mHeaderParserTest, RejectsFramerateMissingColon) { | |
| EXPECT_FALSE(ParseY4mHeader("YUV4MPEG2 W1920 H1080 F30 C420\n").has_value()); | |
| } | |
| TEST(Y4mHeaderParserTest, RejectsFramerateTrailingColon) { | |
| EXPECT_FALSE(ParseY4mHeader("YUV4MPEG2 W1920 H1080 F30: C420\n").has_value()); | |
| } | |
| TEST(Y4mHeaderParserTest, RejectsFramerateLeadingColon) { | |
| EXPECT_FALSE(ParseY4mHeader("YUV4MPEG2 W1920 H1080 F:1 C420\n").has_value()); | |
| } | |
| TEST(Y4mHeaderParserTest, RejectsFramerateNonParsableNumbers) { | |
| EXPECT_FALSE( | |
| ParseY4mHeader("YUV4MPEG2 W1920 H1080 Ffoo:bar C420\n").has_value()); | |
| } | |
| TEST(Y4mHeaderParserTest, RejectsFramerateNonParsableNumerator) { | |
| EXPECT_FALSE( | |
| ParseY4mHeader("YUV4MPEG2 W1920 H1080 Ffoo:1 C420\n").has_value()); | |
| } | |
| TEST(Y4mHeaderParserTest, RejectsFramerateNonParsableDenominator) { | |
| EXPECT_FALSE( | |
| ParseY4mHeader("YUV4MPEG2 W1920 H1080 F30:bar C420\n").has_value()); | |
| } | |
| TEST(Y4mHeaderParserTest, RejectsZeroFramerateNumerator) { | |
| EXPECT_FALSE(ParseY4mHeader("YUV4MPEG2 W640 H480 F0:1\n").has_value()); | |
| } | |
| TEST(Y4mHeaderParserTest, RejectsNegativeFramerateNumerator) { | |
| EXPECT_FALSE(ParseY4mHeader("YUV4MPEG2 W640 H480 F-30:1\n").has_value()); | |
| } | |
| TEST(Y4mHeaderParserTest, RejectsZeroFramerateDenominator) { | |
| EXPECT_FALSE(ParseY4mHeader("YUV4MPEG2 W640 H480 F30:0\n").has_value()); | |
| } | |
| TEST(Y4mHeaderParserTest, RejectsNegativeFramerateDenominator) { | |
| EXPECT_FALSE(ParseY4mHeader("YUV4MPEG2 W640 H480 F30:-1\n").has_value()); | |
| } | |
| TEST(Y4mHeaderParserTest, ParseFromFileMinimalFrame) { | |
| std::string filepath = TempFilename(OutputPath(), "minimal_test.y4m"); | |
| FILE* file = fopen(filepath.c_str(), "wb"); | |
| ASSERT_TRUE(file != nullptr); | |
| // Minimal 1x1 Y4M frame (1.5 bytes rounded up to 6 bytes in I420). | |
| const std::string content = | |
| "YUV4MPEG2 W1 H1 F1:1\n" | |
| "FRAME\n" | |
| "YUV123"; | |
| fwrite(content.data(), 1, content.size(), file); | |
| fclose(file); | |
| std::optional<Y4mHeader> header = ParseY4mHeaderFromFile(filepath); | |
| ASSERT_TRUE(header.has_value()); | |
| EXPECT_THAT(*header, | |
| AllOf(Field(&Y4mHeader::resolution, | |
| Eq(Resolution{.width = 1, .height = 1})), | |
| Field(&Y4mHeader::framerate, Eq(Frequency::Hertz(1))))); | |
| remove(filepath.c_str()); | |
| } | |
| TEST(Y4mHeaderParserTest, ParseFromFileMaxHeader) { | |
| std::string filepath = TempFilename(OutputPath(), "max_header_test.y4m"); | |
| FILE* file = fopen(filepath.c_str(), "wb"); | |
| ASSERT_TRUE(file != nullptr); | |
| std::string content = "YUV4MPEG2 W4 H4 F10:1 X"; | |
| content.append(kMaxY4mHeaderSizeBytes - content.size() - 1, 'x'); | |
| content.push_back('\n'); | |
| content.append("FRAME\n012345678901234567890123"); | |
| fwrite(content.data(), 1, content.size(), file); | |
| fclose(file); | |
| std::optional<Y4mHeader> header = ParseY4mHeaderFromFile(filepath); | |
| ASSERT_TRUE(header.has_value()); | |
| EXPECT_THAT(*header, | |
| AllOf(Field(&Y4mHeader::resolution, | |
| Eq(Resolution{.width = 4, .height = 4})), | |
| Field(&Y4mHeader::header_size, | |
| Eq(DataSize::Bytes(kMaxY4mHeaderSizeBytes))))); | |
| remove(filepath.c_str()); | |
| } | |
| TEST(Y4mHeaderParserTest, ParseFromNonExistentFileFails) { | |
| EXPECT_FALSE(ParseY4mHeaderFromFile("/non/existent/file.y4m").has_value()); | |
| } | |
| } // namespace test | |
| } // namespace webrtc |