blob: 2bb02b50f81c8cac9e898571b68aed931dfd5365 [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chromecast/starboard/media/renderer/chromium_starboard_conversions.h"
#include "base/logging.h"
#include "base/no_destructor.h"
#include "base/synchronization/lock.h"
#include "base/thread_annotations.h"
#include "chromecast/starboard/media/media/mime_utils.h"
#include "chromecast/starboard/media/media/starboard_api_wrapper.h"
#include "media/base/audio_codecs.h"
#include "media/base/channel_layout.h"
#include "media/base/video_codecs.h"
#include "media/base/video_color_space.h"
#include "third_party/abseil-cpp/absl/container/node_hash_set.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/hdr_metadata.h"
namespace chromecast {
namespace media {
namespace {
using ::media::VideoColorSpace;
// Registers the given MIME type, if it has not already been registered. Returns
// a c string of the MIME type, which is guaranteed to point to valid data for
// the duration of the program.
const char* RegisterMimeType(std::string mime) {
if (mime.empty()) {
return "";
}
static base::NoDestructor<base::Lock> lock;
// A node_hash_set is used here because we require key ptr stability (so that
// c strings remain valid).
static base::NoDestructor<absl::node_hash_set<std::string>> registry
GUARDED_BY(*lock);
base::AutoLock autlock(*lock);
auto it_and_inserted = registry->insert(std::move(mime));
return it_and_inserted.first->c_str();
}
// Converts a chromium codec to a starboard codec, returning nullopt if the
// codec does not exist in starboard.
//
// `profile` is necessary for differentiating Dolby Vision codecs.
std::optional<StarboardVideoCodec> ToSbVideoCodec(
::media::VideoCodec codec,
::media::VideoCodecProfile profile) {
switch (codec) {
case ::media::VideoCodec::kH264:
return StarboardVideoCodec::kStarboardVideoCodecH264;
case ::media::VideoCodec::kMPEG2:
return StarboardVideoCodec::kStarboardVideoCodecMpeg2;
case ::media::VideoCodec::kTheora:
return StarboardVideoCodec::kStarboardVideoCodecTheora;
case ::media::VideoCodec::kVP8:
return StarboardVideoCodec::kStarboardVideoCodecVp8;
case ::media::VideoCodec::kVP9:
return StarboardVideoCodec::kStarboardVideoCodecVp9;
case ::media::VideoCodec::kHEVC:
return StarboardVideoCodec::kStarboardVideoCodecH265;
case ::media::VideoCodec::kAV1:
return StarboardVideoCodec::kStarboardVideoCodecAv1;
case ::media::VideoCodec::kVC1:
return StarboardVideoCodec::kStarboardVideoCodecVc1;
case ::media::VideoCodec::kDolbyVision:
switch (profile) {
// This logic was copied from
// https://source.chromium.org/chromium/chromium/src/+/main:chromecast/media/base/media_codec_support.cc;l=63;drc=586d9e059d27bfbe85c8df737882821e7b68929d
case ::media::VideoCodecProfile::DOLBYVISION_PROFILE5:
case ::media::VideoCodecProfile::DOLBYVISION_PROFILE7:
case ::media::VideoCodecProfile::DOLBYVISION_PROFILE8:
return StarboardVideoCodec::kStarboardVideoCodecH265;
default:
// We only support Dolby Vision for HEVC currently. The H264 profiles
// (DOLBYVISION_PROFILE0 and DOLBYVISION_PROFILE9) are considered
// unsupported.
LOG(INFO) << "Unsupported Dolby Vision profile=" << profile;
return std::nullopt;
}
default:
LOG(ERROR) << "Unsupported video codec: " << codec;
return std::nullopt;
}
}
// Populates HDR fields of `out_color_metadata` based on `hdr_metadata`.
void PopulateHdrMetadata(const gfx::HDRMetadata& hdr_metadata,
StarboardColorMetadata& out_color_metadata) {
if (hdr_metadata.cta_861_3) {
out_color_metadata.max_cll =
hdr_metadata.cta_861_3->max_content_light_level;
out_color_metadata.max_fall =
hdr_metadata.cta_861_3->max_frame_average_light_level;
} else {
LOG(INFO) << "HDR metadata is missing cta_861_3 info.";
}
if (hdr_metadata.smpte_st_2086) {
const auto& color_volume_metadata = hdr_metadata.smpte_st_2086->primaries;
StarboardMediaMasteringMetadata& mastering_metadata =
out_color_metadata.mastering_metadata;
mastering_metadata.primary_r_chromaticity_x = color_volume_metadata.fRX;
mastering_metadata.primary_r_chromaticity_y = color_volume_metadata.fRY;
mastering_metadata.primary_g_chromaticity_x = color_volume_metadata.fGX;
mastering_metadata.primary_g_chromaticity_y = color_volume_metadata.fGY;
mastering_metadata.primary_b_chromaticity_x = color_volume_metadata.fBX;
mastering_metadata.primary_b_chromaticity_y = color_volume_metadata.fBY;
mastering_metadata.white_point_chromaticity_x = color_volume_metadata.fWX;
mastering_metadata.white_point_chromaticity_y = color_volume_metadata.fWY;
mastering_metadata.luminance_max =
hdr_metadata.smpte_st_2086->luminance_max;
mastering_metadata.luminance_min =
hdr_metadata.smpte_st_2086->luminance_min;
} else {
LOG(INFO) << "HDR metadata is missing smpte_st_2086 info.";
}
}
// Converts chromium color metadata to starboard color metadata, returning
// nullopt if the chromium color metadata cannot be converted.
std::optional<StarboardColorMetadata> ToSbColorMetadata(
const std::optional<gfx::HDRMetadata>& hdr_metadata,
const ::media::VideoColorSpace& color_space) {
StarboardColorMetadata color_metadata = {};
// bits_per_channel and the chroma_*/cb_* fields below need to be derived from
// the MIME string. See crbug.com/230915942 for more info.
// Unfortunately, it doesn't look like MIME type is exposed to cast. Note that
// in Cobalt, these fields are all currently hard-coded to zero (in
// third_party/chromium/media/base/starboard_utils.cc). I don't think they're
// necessary for cast either, since cast doesn't seem to populate this info
// anywhere.
// 0 translates to "unknown".
color_metadata.bits_per_channel = 0;
color_metadata.chroma_subsampling_horizontal = 0;
color_metadata.chroma_subsampling_vertical = 0;
color_metadata.cb_subsampling_horizontal = 0;
color_metadata.cb_subsampling_vertical = 0;
color_metadata.chroma_siting_horizontal = 0;
color_metadata.chroma_siting_vertical = 0;
if (hdr_metadata) {
LOG(INFO) << "Video config has HDR metadata.";
PopulateHdrMetadata(*hdr_metadata, color_metadata);
} else {
LOG(INFO) << "Video config has no HDR metadata.";
color_metadata.max_cll = 0;
color_metadata.max_fall = 0;
}
switch (color_space.primaries) {
case VideoColorSpace::PrimaryID::INVALID:
case VideoColorSpace::PrimaryID::BT709:
case VideoColorSpace::PrimaryID::UNSPECIFIED:
case VideoColorSpace::PrimaryID::BT470M:
case VideoColorSpace::PrimaryID::BT470BG:
case VideoColorSpace::PrimaryID::SMPTE170M:
case VideoColorSpace::PrimaryID::SMPTE240M:
case VideoColorSpace::PrimaryID::FILM:
case VideoColorSpace::PrimaryID::BT2020:
case VideoColorSpace::PrimaryID::SMPTEST428_1:
case VideoColorSpace::PrimaryID::SMPTEST431_2:
case VideoColorSpace::PrimaryID::SMPTEST432_1:
color_metadata.primaries = static_cast<int>(color_space.primaries);
break;
default:
LOG(ERROR) << "Unsupported color space primaries: "
<< static_cast<int>(color_space.primaries);
return std::nullopt;
}
switch (color_space.transfer) {
case VideoColorSpace::TransferID::INVALID:
case VideoColorSpace::TransferID::BT709:
case VideoColorSpace::TransferID::UNSPECIFIED:
case VideoColorSpace::TransferID::GAMMA22:
case VideoColorSpace::TransferID::GAMMA28:
case VideoColorSpace::TransferID::SMPTE170M:
case VideoColorSpace::TransferID::SMPTE240M:
case VideoColorSpace::TransferID::LINEAR:
case VideoColorSpace::TransferID::LOG:
case VideoColorSpace::TransferID::LOG_SQRT:
case VideoColorSpace::TransferID::IEC61966_2_4:
case VideoColorSpace::TransferID::BT1361_ECG:
case VideoColorSpace::TransferID::IEC61966_2_1:
case VideoColorSpace::TransferID::BT2020_10:
case VideoColorSpace::TransferID::BT2020_12:
case VideoColorSpace::TransferID::SMPTEST2084:
case VideoColorSpace::TransferID::SMPTEST428_1:
color_metadata.transfer = static_cast<int>(color_space.transfer);
break;
default:
LOG(ERROR) << "Unsupported color space transfer: "
<< static_cast<int>(color_space.transfer);
return std::nullopt;
}
switch (color_space.matrix) {
case VideoColorSpace::MatrixID::RGB:
case VideoColorSpace::MatrixID::BT709:
case VideoColorSpace::MatrixID::UNSPECIFIED:
case VideoColorSpace::MatrixID::FCC:
case VideoColorSpace::MatrixID::BT470BG:
case VideoColorSpace::MatrixID::SMPTE170M:
case VideoColorSpace::MatrixID::SMPTE240M:
case VideoColorSpace::MatrixID::YCOCG:
case VideoColorSpace::MatrixID::BT2020_NCL:
case VideoColorSpace::MatrixID::BT2020_CL:
case VideoColorSpace::MatrixID::YDZDX:
case VideoColorSpace::MatrixID::INVALID:
color_metadata.matrix = static_cast<int>(color_space.matrix);
break;
default:
LOG(ERROR) << "Unsupported color space matrix: "
<< static_cast<int>(color_space.matrix);
return std::nullopt;
}
switch (color_space.range) {
case gfx::ColorSpace::RangeID::INVALID:
case gfx::ColorSpace::RangeID::LIMITED:
case gfx::ColorSpace::RangeID::FULL:
case gfx::ColorSpace::RangeID::DERIVED:
color_metadata.range = static_cast<int>(color_space.range);
break;
default:
LOG(ERROR) << "Unsupported color space range: "
<< static_cast<int>(color_space.range);
return std::nullopt;
}
// color_space.primaries (::media::VideoColorSpace::PrimaryID)
// does not support any value equivalent to Starboard's
// kSbMediaPrimaryIdCustom. Thus, we don't need to populate
// custom_primary_matrix. Just zero it, in case something reads from it.
return color_metadata;
}
} // namespace
std::optional<StarboardAudioSampleInfo> ToStarboardAudioSampleInfo(
const ::media::AudioDecoderConfig& in_config) {
StarboardAudioSampleInfo out_config = {};
switch (in_config.codec()) {
case ::media::AudioCodec::kAAC:
out_config.codec = StarboardAudioCodec::kStarboardAudioCodecAac;
break;
case ::media::AudioCodec::kMP3:
out_config.codec = StarboardAudioCodec::kStarboardAudioCodecMp3;
break;
case ::media::AudioCodec::kVorbis:
out_config.codec = StarboardAudioCodec::kStarboardAudioCodecVorbis;
break;
case ::media::AudioCodec::kFLAC:
out_config.codec = StarboardAudioCodec::kStarboardAudioCodecFlac;
break;
case ::media::AudioCodec::kPCM:
case ::media::AudioCodec::kPCM_S16BE:
case ::media::AudioCodec::kPCM_S24BE:
out_config.codec = StarboardAudioCodec::kStarboardAudioCodecPcm;
break;
case ::media::AudioCodec::kOpus:
out_config.codec = StarboardAudioCodec::kStarboardAudioCodecOpus;
break;
case ::media::AudioCodec::kEAC3:
out_config.codec = StarboardAudioCodec::kStarboardAudioCodecEac3;
break;
case ::media::AudioCodec::kAC3:
out_config.codec = StarboardAudioCodec::kStarboardAudioCodecAc3;
break;
default:
LOG(ERROR) << "Unsupported audio codec: " << in_config.codec();
return std::nullopt;
}
out_config.mime = RegisterMimeType(GetMimeType(in_config.codec()));
out_config.format_tag = 0;
out_config.number_of_channels = in_config.channels();
out_config.samples_per_second = in_config.samples_per_second();
// Based on starboard_utils.cc (MediaAudioConfigToSbMediaAudioSampleInfo) in
// the cobalt codebase, bits_per_sample does not take into account the number
// of channels.
if (out_config.codec == StarboardAudioCodec::kStarboardAudioCodecPcm) {
// TODO(antoniori): handle resampling to other formats. Currently we only
// support S16.
out_config.bits_per_sample = 16;
} else {
// For starboard, "bits per sample" does not factor in channel count.
out_config.bits_per_sample = in_config.bytes_per_channel() * 8;
}
out_config.average_bytes_per_second = out_config.number_of_channels *
out_config.samples_per_second *
out_config.bits_per_sample / 8;
out_config.block_alignment = 4;
out_config.audio_specific_config_size = in_config.extra_data().size();
out_config.audio_specific_config = in_config.extra_data().data();
return out_config;
}
std::optional<StarboardVideoSampleInfo> ToStarboardVideoSampleInfo(
const ::media::VideoDecoderConfig& in_config) {
StarboardVideoSampleInfo out_config = {};
std::optional<StarboardVideoCodec> sb_codec =
ToSbVideoCodec(in_config.codec(), in_config.profile());
if (!sb_codec.has_value()) {
return std::nullopt;
}
out_config.codec = *sb_codec;
out_config.mime = RegisterMimeType(
GetMimeType(in_config.codec(), in_config.profile(), in_config.level()));
// Specify that the max capabilities are not known.
out_config.max_video_capabilities = "";
// This needs to be set on a per-sample basis later.
out_config.is_key_frame = false;
const gfx::Size aspect_ratio = in_config.coded_size();
out_config.frame_width = aspect_ratio.width();
out_config.frame_height = aspect_ratio.height();
std::optional<StarboardColorMetadata> sb_color_metadata =
ToSbColorMetadata(in_config.hdr_metadata(), in_config.color_space_info());
if (!sb_color_metadata.has_value()) {
return std::nullopt;
}
out_config.color_metadata = *std::move(sb_color_metadata);
return out_config;
}
} // namespace media
} // namespace chromecast