// Copyright 2020 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "cast/streaming/capture_recommendations.h" #include #include #include "cast/streaming/answer_messages.h" #include "util/osp_logging.h" namespace openscreen { namespace cast { namespace capture_recommendations { namespace { bool DoubleEquals(double a, double b) { // Choice of epsilon for double comparison allows for proper comparison // for both aspect ratios and frame rates. For frame rates, it is based on the // broadcast rate of 29.97fps, which is actually 29.976. For aspect ratios, it // allows for a one-pixel difference at a 4K resolution, we want it to be // relatively high to avoid false negative comparison results. const double kEpsilon = .0001; return std::abs(a - b) < kEpsilon; } void ApplyDisplay(const DisplayDescription& description, Recommendations* recommendations) { recommendations->video.supports_scaling = (description.aspect_ratio_constraint && (description.aspect_ratio_constraint.value() == AspectRatioConstraint::kVariable)); // We should never exceed the display's resolution, since it will always // force scaling. if (description.dimensions) { const double frame_rate = static_cast(description.dimensions->frame_rate); recommendations->video.maximum = Resolution{description.dimensions->width, description.dimensions->height, frame_rate}; recommendations->video.bit_rate_limits.maximum = recommendations->video.maximum.effective_bit_rate(); recommendations->video.minimum.set_minimum(recommendations->video.maximum); } // If the receiver gives us an aspect ratio that doesn't match the display // resolution they give us, the behavior is undefined from the spec. // Here we prioritize the aspect ratio, and the receiver can scale the frame // as they wish. double aspect_ratio = 0.0; if (description.aspect_ratio) { aspect_ratio = static_cast(description.aspect_ratio->width) / description.aspect_ratio->height; #if OSP_DCHECK_IS_ON() if (description.dimensions) { const double from_dims = static_cast(description.dimensions->width) / description.dimensions->height; if (!DoubleEquals(from_dims, aspect_ratio)) { OSP_DLOG_WARN << "Received mismatched aspect ratio from the receiver."; } } #endif recommendations->video.maximum.width = recommendations->video.maximum.height * aspect_ratio; } else if (description.dimensions) { aspect_ratio = static_cast(description.dimensions->width) / description.dimensions->height; } else { return; } recommendations->video.minimum.width = recommendations->video.minimum.height * aspect_ratio; } Resolution ToResolution(const Dimensions& dims) { return {dims.width, dims.height, static_cast(dims.frame_rate)}; } void ApplyConstraints(const Constraints& constraints, Recommendations* recommendations) { // Audio has no fields in the display description, so we can safely // ignore the current recommendations when setting values here. if (constraints.audio.max_delay.has_value()) { recommendations->audio.max_delay = constraints.audio.max_delay.value(); } recommendations->audio.max_channels = constraints.audio.max_channels; recommendations->audio.max_sample_rate = constraints.audio.max_sample_rate; recommendations->audio.bit_rate_limits = BitRateLimits{ std::max(constraints.audio.min_bit_rate, kDefaultAudioMinBitRate), std::max(constraints.audio.max_bit_rate, kDefaultAudioMinBitRate)}; // With video, we take the intersection of values of the constraints and // the display description. if (constraints.video.max_delay.has_value()) { recommendations->video.max_delay = constraints.video.max_delay.value(); } if (constraints.video.max_pixels_per_second.has_value()) { recommendations->video.max_pixels_per_second = constraints.video.max_pixels_per_second.value(); } recommendations->video.bit_rate_limits = BitRateLimits{std::max(constraints.video.min_bit_rate, recommendations->video.bit_rate_limits.minimum), std::min(constraints.video.max_bit_rate, recommendations->video.bit_rate_limits.maximum)}; Resolution max = ToResolution(constraints.video.max_dimensions); if (max <= kDefaultMinResolution) { recommendations->video.maximum = kDefaultMinResolution; } else if (max < recommendations->video.maximum) { recommendations->video.maximum = std::move(max); } // Implicit else: maximum = kDefaultMaxResolution. if (constraints.video.min_dimensions) { Resolution min = ToResolution(constraints.video.min_dimensions.value()); if (kDefaultMinResolution < min) { recommendations->video.minimum = std::move(min); } } } } // namespace bool BitRateLimits::operator==(const BitRateLimits& other) const { return std::tie(minimum, maximum) == std::tie(other.minimum, other.maximum); } bool Audio::operator==(const Audio& other) const { return std::tie(bit_rate_limits, max_delay, max_channels, max_sample_rate) == std::tie(other.bit_rate_limits, other.max_delay, other.max_channels, other.max_sample_rate); } bool Resolution::operator==(const Resolution& other) const { return (std::tie(width, height) == std::tie(other.width, other.height)) && DoubleEquals(frame_rate, other.frame_rate); } bool Resolution::operator<(const Resolution& other) const { return effective_bit_rate() < other.effective_bit_rate(); } bool Resolution::operator<=(const Resolution& other) const { return (*this == other) || (*this < other); } void Resolution::set_minimum(const Resolution& other) { if (other < *this) { *this = other; } } bool Video::operator==(const Video& other) const { return std::tie(bit_rate_limits, minimum, maximum, supports_scaling, max_delay, max_pixels_per_second) == std::tie(other.bit_rate_limits, other.minimum, other.maximum, other.supports_scaling, other.max_delay, other.max_pixels_per_second); } bool Recommendations::operator==(const Recommendations& other) const { return std::tie(audio, video) == std::tie(other.audio, other.video); } Recommendations GetRecommendations(const Answer& answer) { Recommendations recommendations; if (answer.display.has_value() && answer.display->IsValid()) { ApplyDisplay(answer.display.value(), &recommendations); } if (answer.constraints.has_value() && answer.constraints->IsValid()) { ApplyConstraints(answer.constraints.value(), &recommendations); } return recommendations; } } // namespace capture_recommendations } // namespace cast } // namespace openscreen