/* * 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 #include #include #include #include #include #include "absl/strings/string_view.h" #include "api/units/data_size.h" #include "api/units/frequency.h" #include "api/video/color_space.h" #include "api/video/resolution.h" #include "rtc_base/logging.h" #include "rtc_base/string_to_number.h" namespace webrtc { namespace test { namespace { std::optional ParseColorSpace(absl::string_view color_space_str) { // Common Y4M chroma types: // "420" / "420jpeg": 4:20 subsampling, centered chroma (JPEG / WebRTC // default). "420paldv": 4:20 subsampling, collocated horizontally and // vertically (PAL DV). "420mpeg2": 4:20 subsampling, collocated horizontally, // half vertical (MPEG-2 / H.264 / AV1 default). "422": 4:22 subsampling. // "444": 4:44 subsampling. // "mono": Monochrome. if (color_space_str == "420" || color_space_str == "420jpeg") { return ColorSpace( ColorSpace::PrimaryID::kBT709, ColorSpace::TransferID::kBT709, ColorSpace::MatrixID::kBT709, ColorSpace::RangeID::kLimited, ColorSpace::ChromaSiting::kHalf, ColorSpace::ChromaSiting::kHalf, /*hdr_metadata=*/nullptr); } if (color_space_str == "420mpeg2") { return ColorSpace( ColorSpace::PrimaryID::kBT709, ColorSpace::TransferID::kBT709, ColorSpace::MatrixID::kBT709, ColorSpace::RangeID::kLimited, ColorSpace::ChromaSiting::kCollocated, ColorSpace::ChromaSiting::kHalf, /*hdr_metadata=*/nullptr); } if (color_space_str == "420paldv") { return ColorSpace( ColorSpace::PrimaryID::kBT709, ColorSpace::TransferID::kBT709, ColorSpace::MatrixID::kBT709, ColorSpace::RangeID::kLimited, ColorSpace::ChromaSiting::kCollocated, ColorSpace::ChromaSiting::kCollocated, /*hdr_metadata=*/nullptr); } if (color_space_str == "422") { return ColorSpace( ColorSpace::PrimaryID::kBT709, ColorSpace::TransferID::kBT709, ColorSpace::MatrixID::kBT709, ColorSpace::RangeID::kLimited, ColorSpace::ChromaSiting::kCollocated, ColorSpace::ChromaSiting::kUnspecified, /*hdr_metadata=*/nullptr); } if (color_space_str == "444") { return ColorSpace( ColorSpace::PrimaryID::kBT709, ColorSpace::TransferID::kBT709, ColorSpace::MatrixID::kBT709, ColorSpace::RangeID::kLimited, ColorSpace::ChromaSiting::kUnspecified, ColorSpace::ChromaSiting::kUnspecified, /*hdr_metadata=*/nullptr); } if (color_space_str == "mono") { return ColorSpace( ColorSpace::PrimaryID::kBT709, ColorSpace::TransferID::kBT709, ColorSpace::MatrixID::kBT709, ColorSpace::RangeID::kLimited, ColorSpace::ChromaSiting::kUnspecified, ColorSpace::ChromaSiting::kUnspecified, /*hdr_metadata=*/nullptr); } RTC_LOG(LS_WARNING) << "Unknown or unsupported Y4M color space: " << color_space_str; return std::nullopt; } bool ApplyHeaderToken(const Y4mHeaderToken& token, Y4mHeader& header) { switch (token.tag) { case 'W': { if (header.resolution.width > 0) { RTC_LOG(LS_WARNING) << "Duplicate Y4M width tag"; return false; } std::optional width = webrtc::StringToNumber(token.value); if (!width.has_value() || *width <= 0) { RTC_LOG(LS_WARNING) << "Invalid Y4M width: " << token.value; return false; } header.resolution.width = *width; break; } case 'H': { if (header.resolution.height > 0) { RTC_LOG(LS_WARNING) << "Duplicate Y4M height tag"; return false; } std::optional height = webrtc::StringToNumber(token.value); if (!height.has_value() || *height <= 0) { RTC_LOG(LS_WARNING) << "Invalid Y4M height: " << token.value; return false; } header.resolution.height = *height; break; } case 'F': { if (header.framerate.has_value()) { RTC_LOG(LS_WARNING) << "Duplicate Y4M framerate tag"; return false; } size_t colon_pos = token.value.find(':'); if (colon_pos == absl::string_view::npos) { RTC_LOG(LS_WARNING) << "Invalid Y4M framerate format: " << token.value; return false; } std::optional num = webrtc::StringToNumber(token.value.substr(0, colon_pos)); std::optional den = webrtc::StringToNumber(token.value.substr(colon_pos + 1)); if (!num.has_value() || !den.has_value() || *num <= 0 || *den <= 0) { RTC_LOG(LS_WARNING) << "Invalid Y4M framerate values: " << token.value; return false; } // Round to nearest millihertz for high-precision frequency // representation. int64_t millihertz = std::llround((1000.0 * *num) / *den); header.framerate = Frequency::MilliHertz(millihertz); break; } case 'C': { if (header.color_space.has_value()) { RTC_LOG(LS_WARNING) << "Duplicate Y4M color space tag"; return false; } std::optional color_space = ParseColorSpace(token.value); if (!color_space.has_value()) { return false; } header.color_space = color_space; break; } case 'I': case 'A': case 'X': default: // Other valid Y4M tags (interlacing, aspect ratio, metadata comments) // or ignored parameters. break; } return true; } } // namespace std::optional> TokenizeY4mHeader( absl::string_view header_content) { size_t newline_pos = header_content.find('\n'); if (newline_pos == absl::string_view::npos) { RTC_LOG(LS_WARNING) << "Y4M header missing newline terminator"; return std::nullopt; } if (newline_pos >= kMaxY4mHeaderSizeBytes) { RTC_LOG(LS_WARNING) << "Y4M header exceeds maximum supported size"; return std::nullopt; } absl::string_view line = header_content.substr(0, newline_pos); if (!line.empty() && line.back() == '\r') { line.remove_suffix(1); } size_t pos = 0; while (pos < line.size() && line[pos] == ' ') { ++pos; } if (pos >= line.size()) { return std::nullopt; } size_t magic_end = line.find(' ', pos); if (magic_end == absl::string_view::npos) { magic_end = line.size(); } absl::string_view magic = line.substr(pos, magic_end - pos); if (magic != "YUV4MPEG2") { RTC_LOG(LS_WARNING) << "Invalid Y4M magic: " << magic; return std::nullopt; } pos = magic_end; std::vector tokens; while (pos < line.size()) { while (pos < line.size() && line[pos] == ' ') { ++pos; } if (pos >= line.size()) { break; } size_t token_end = line.find(' ', pos); if (token_end == absl::string_view::npos) { token_end = line.size(); } absl::string_view token_str = line.substr(pos, token_end - pos); pos = token_end; if (token_str.empty()) { continue; } tokens.push_back( Y4mHeaderToken{.tag = token_str.front(), .value = token_str.substr(1)}); } return tokens; } std::optional ParseY4mHeader(absl::string_view header_content) { size_t newline_pos = header_content.find('\n'); if (newline_pos == absl::string_view::npos || newline_pos >= kMaxY4mHeaderSizeBytes) { // Detailed warnings logged in TokenizeY4mHeader. return std::nullopt; } std::optional> tokens = TokenizeY4mHeader(header_content); if (!tokens.has_value()) { return std::nullopt; } Y4mHeader header; header.header_size = DataSize::Bytes(newline_pos + 1); for (const Y4mHeaderToken& token : *tokens) { if (!ApplyHeaderToken(token, header)) { return std::nullopt; } } if (header.resolution.width <= 0 || header.resolution.height <= 0) { RTC_LOG(LS_WARNING) << "Y4M header missing valid width or height"; return std::nullopt; } return header; } std::optional ParseY4mHeader(FILE* file) { if (file == nullptr) { return std::nullopt; } std::string buffer; buffer.reserve(128); int c = 0; while (buffer.size() < kMaxY4mHeaderSizeBytes && (c = fgetc(file)) != EOF) { buffer.push_back(static_cast(c)); if (c == '\n') { break; } } if (buffer.empty() || buffer.back() != '\n') { return std::nullopt; } return ParseY4mHeader(buffer); } std::optional ParseY4mHeaderFromFile(absl::string_view filepath) { std::string path_str(filepath); FILE* file = fopen(path_str.c_str(), "rb"); if (file == nullptr) { RTC_LOG(LS_WARNING) << "Cannot open file: " << path_str; return std::nullopt; } std::optional header = ParseY4mHeader(file); fclose(file); return header; } } // namespace test } // namespace webrtc