blob: cd2ead7f9f2d7fe0e66aa09c21ebcccf1a2bac16 [file]
/*
* 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 <cmath>
#include <cstdint>
#include <cstdio>
#include <optional>
#include <string>
#include <vector>
#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<ColorSpace> 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<int> width = webrtc::StringToNumber<int>(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<int> height = webrtc::StringToNumber<int>(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<int64_t> num =
webrtc::StringToNumber<int64_t>(token.value.substr(0, colon_pos));
std::optional<int64_t> den =
webrtc::StringToNumber<int64_t>(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<ColorSpace> 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<std::vector<Y4mHeaderToken>> 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<Y4mHeaderToken> 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<Y4mHeader> 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<std::vector<Y4mHeaderToken>> 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<Y4mHeader> 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<char>(c));
if (c == '\n') {
break;
}
}
if (buffer.empty() || buffer.back() != '\n') {
return std::nullopt;
}
return ParseY4mHeader(buffer);
}
std::optional<Y4mHeader> 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<Y4mHeader> header = ParseY4mHeader(file);
fclose(file);
return header;
}
} // namespace test
} // namespace webrtc