blob: 7de1350a8fbeb0d13688941e61ba05ce32b4e02e [file] [log] [blame]
// Copyright 2017 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 "content/renderer/media/media_stream_constraints_util_audio.h"
#include <algorithm>
#include <cmath>
#include <utility>
#include <vector>
#include "content/common/media/media_stream_options.h"
#include "content/renderer/media/media_stream_constraints_util_sets.h"
#include "content/renderer/media/media_stream_video_source.h"
#include "media/base/limits.h"
#include "third_party/WebKit/public/platform/WebMediaConstraints.h"
#include "third_party/WebKit/public/platform/WebString.h"
namespace content {
namespace {
// This class has the same data as ::mojom::AudioInputDeviceCapabilities, but
// adds extra operations to simplify access to device parameters.
class AudioDeviceInfo {
public:
// This constructor is intended for device capture.
explicit AudioDeviceInfo(
const ::mojom::AudioInputDeviceCapabilitiesPtr& device_info)
: device_id_(device_info->device_id),
parameters_(device_info->parameters) {
DCHECK(parameters_.IsValid());
}
// This constructor is intended for content capture, where constraints on
// audio parameters are not supported.
explicit AudioDeviceInfo(std::string device_id)
: device_id_(std::move(device_id)) {}
bool operator==(const AudioDeviceInfo& other) const {
return device_id_ == other.device_id_;
}
// Accessors
const std::string& device_id() const { return device_id_; }
const media::AudioParameters& parameters() const { return parameters_; }
// Convenience accessors
int SampleRate() const {
DCHECK(parameters_.IsValid());
return parameters_.sample_rate();
}
int SampleSize() const {
DCHECK(parameters_.IsValid());
return parameters_.bits_per_sample();
}
int ChannelCount() const {
DCHECK(parameters_.IsValid());
return parameters_.channels();
}
int Effects() const {
DCHECK(parameters_.IsValid());
return parameters_.effects();
}
private:
std::string device_id_;
media::AudioParameters parameters_;
};
using AudioDeviceSet = DiscreteSet<AudioDeviceInfo>;
AudioDeviceSet AudioDeviceSetForDeviceCapture(
const blink::WebMediaTrackConstraintSet& constraint_set,
const AudioDeviceCaptureCapabilities& capabilities,
const char** failed_constraint_name) {
std::vector<AudioDeviceInfo> result;
*failed_constraint_name = "";
for (auto& device_capabilities : capabilities) {
if (!constraint_set.device_id.Matches(
blink::WebString::FromASCII(device_capabilities->device_id))) {
if (failed_constraint_name)
*failed_constraint_name = constraint_set.device_id.GetName();
continue;
}
result.push_back(AudioDeviceInfo(device_capabilities));
}
if (!result.empty())
*failed_constraint_name = nullptr;
return AudioDeviceSet(std::move(result));
}
AudioDeviceSet AudioDeviceSetForContentCapture(
const blink::WebMediaTrackConstraintSet& constraint_set,
const char** failed_constraint_name = nullptr) {
if (!constraint_set.device_id.HasExact())
return AudioDeviceSet::UniversalSet();
std::vector<AudioDeviceInfo> result;
for (auto& device_id : constraint_set.device_id.Exact())
result.push_back(AudioDeviceInfo(device_id.Utf8()));
return AudioDeviceSet(std::move(result));
}
// This class represents a set of possible candidate settings.
// The SelectSettings algorithm starts with a set containing all possible
// candidates based on hardware capabilities and/or allowed values for supported
// properties. The is then reduced progressively as the basic and advanced
// constraint sets are applied.
// In the end, if the set of candidates is empty, SelectSettings fails.
// If not, the ideal values (if any) or tie breaker rules are used to select
// the final settings based on the candidates that survived the application
// of the constraint sets.
// This class is implemented as a collection of more specific sets for the
// various supported properties. If any of the specific sets is empty, the
// whole AudioCaptureCandidates set is considered empty as well.
class AudioCaptureCandidates {
public:
enum BoolConstraint {
// Constraints not related to audio processing.
HOTWORD_ENABLED,
DISABLE_LOCAL_ECHO,
RENDER_TO_ASSOCIATED_SINK,
// Constraints that enable/disable audio processing.
ECHO_CANCELLATION,
GOOG_ECHO_CANCELLATION,
// Constraints that control audio-processing behavior.
GOOG_AUDIO_MIRRORING,
GOOG_AUTO_GAIN_CONTROL,
GOOG_EXPERIMENTAL_ECHO_CANCELLATION,
GOOG_TYPING_NOISE_DETECTION,
GOOG_NOISE_SUPPRESSION,
GOOG_EXPERIMENTAL_NOISE_SUPPRESSION,
GOOG_BEAMFORMING,
GOOG_HIGHPASS_FILTER,
GOOG_EXPERIMENTAL_AUTO_GAIN_CONTROL,
NUM_BOOL_CONSTRAINTS
};
AudioCaptureCandidates();
AudioCaptureCandidates(
const blink::WebMediaTrackConstraintSet& constraint_set,
const AudioDeviceCaptureCapabilities& capabilities,
bool is_device_capture);
// Set operations.
bool IsEmpty() const { return failed_constraint_name_ != nullptr; }
AudioCaptureCandidates Intersection(const AudioCaptureCandidates& other);
// Accessors.
const char* failed_constraint_name() const { return failed_constraint_name_; }
const AudioDeviceSet& audio_device_set() const { return audio_device_set_; }
const DiscreteSet<std::string>& goog_array_geometry_set() const {
return goog_array_geometry_set_;
}
// Accessor for boolean sets.
const DiscreteSet<bool>& GetBoolSet(BoolConstraint property) const {
DCHECK_GE(property, 0);
DCHECK_LT(property, NUM_BOOL_CONSTRAINTS);
return bool_sets_[property];
}
// Convenience accessors.
const DiscreteSet<bool>& hotword_enabled_set() const {
return bool_sets_[HOTWORD_ENABLED];
}
const DiscreteSet<bool>& disable_local_echo_set() const {
return bool_sets_[DISABLE_LOCAL_ECHO];
}
const DiscreteSet<bool>& render_to_associated_sink_set() const {
return bool_sets_[RENDER_TO_ASSOCIATED_SINK];
}
const DiscreteSet<bool>& echo_cancellation_set() const {
return bool_sets_[ECHO_CANCELLATION];
}
const DiscreteSet<bool>& goog_echo_cancellation_set() const {
return bool_sets_[GOOG_ECHO_CANCELLATION];
}
const DiscreteSet<bool>& goog_audio_mirroring_set() const {
return bool_sets_[GOOG_AUDIO_MIRRORING];
}
private:
void MaybeUpdateFailedNonDeviceConstraintName();
void CheckContradictoryEchoCancellation();
// Maps BoolConstraint values to fields in blink::WebMediaTrackConstraintSet.
static const blink::BooleanConstraint blink::WebMediaTrackConstraintSet::*
kBlinkBoolConstraintFields[NUM_BOOL_CONSTRAINTS];
const char* failed_constraint_name_;
AudioDeviceSet audio_device_set_; // Device-related constraints.
std::array<DiscreteSet<bool>, NUM_BOOL_CONSTRAINTS> bool_sets_;
DiscreteSet<std::string> goog_array_geometry_set_;
};
const blink::BooleanConstraint blink::WebMediaTrackConstraintSet::*
AudioCaptureCandidates::kBlinkBoolConstraintFields[NUM_BOOL_CONSTRAINTS] = {
&blink::WebMediaTrackConstraintSet::hotword_enabled,
&blink::WebMediaTrackConstraintSet::disable_local_echo,
&blink::WebMediaTrackConstraintSet::render_to_associated_sink,
&blink::WebMediaTrackConstraintSet::echo_cancellation,
&blink::WebMediaTrackConstraintSet::goog_echo_cancellation,
&blink::WebMediaTrackConstraintSet::goog_audio_mirroring,
&blink::WebMediaTrackConstraintSet::goog_auto_gain_control,
&blink::WebMediaTrackConstraintSet::goog_experimental_echo_cancellation,
&blink::WebMediaTrackConstraintSet::goog_typing_noise_detection,
&blink::WebMediaTrackConstraintSet::goog_noise_suppression,
&blink::WebMediaTrackConstraintSet::goog_experimental_noise_suppression,
&blink::WebMediaTrackConstraintSet::goog_beamforming,
&blink::WebMediaTrackConstraintSet::goog_highpass_filter,
&blink::WebMediaTrackConstraintSet::
goog_experimental_auto_gain_control};
// directly mapped boolean sets to audio properties
struct BoolSetPropertyEntry {
DiscreteSet<bool> AudioCaptureCandidates::*bool_set;
bool AudioProcessingProperties::*bool_field;
bool default_value;
};
AudioCaptureCandidates::AudioCaptureCandidates()
: failed_constraint_name_(nullptr) {}
AudioCaptureCandidates::AudioCaptureCandidates(
const blink::WebMediaTrackConstraintSet& constraint_set,
const AudioDeviceCaptureCapabilities& capabilities,
bool is_device_capture)
: failed_constraint_name_(nullptr),
audio_device_set_(
is_device_capture
? AudioDeviceSetForDeviceCapture(constraint_set,
capabilities,
&failed_constraint_name_)
: AudioDeviceSetForContentCapture(constraint_set,
&failed_constraint_name_)),
goog_array_geometry_set_(
StringSetFromConstraint(constraint_set.goog_array_geometry)) {
for (size_t i = 0; i < NUM_BOOL_CONSTRAINTS; ++i) {
bool_sets_[i] =
BoolSetFromConstraint(constraint_set.*kBlinkBoolConstraintFields[i]);
}
MaybeUpdateFailedNonDeviceConstraintName();
}
AudioCaptureCandidates AudioCaptureCandidates::Intersection(
const AudioCaptureCandidates& other) {
AudioCaptureCandidates intersection;
intersection.audio_device_set_ =
audio_device_set_.Intersection(other.audio_device_set_);
if (intersection.audio_device_set_.IsEmpty()) {
// To mark the intersection as empty, it is necessary to assign a
// a non-null value to |failed_constraint_name_|. The specific value
// for an intersection does not actually matter, since the intersection
// is discarded if empty.
intersection.failed_constraint_name_ = "some device constraint";
return intersection;
}
for (size_t i = 0; i < NUM_BOOL_CONSTRAINTS; ++i) {
intersection.bool_sets_[i] =
bool_sets_[i].Intersection(other.bool_sets_[i]);
}
intersection.goog_array_geometry_set_ =
goog_array_geometry_set_.Intersection(other.goog_array_geometry_set_);
intersection.MaybeUpdateFailedNonDeviceConstraintName();
return intersection;
}
void AudioCaptureCandidates::MaybeUpdateFailedNonDeviceConstraintName() {
blink::WebMediaTrackConstraintSet constraint_set;
for (size_t i = 0; i < NUM_BOOL_CONSTRAINTS; ++i) {
if (bool_sets_[i].IsEmpty()) {
failed_constraint_name_ =
(constraint_set.*kBlinkBoolConstraintFields[i]).GetName();
}
}
if (goog_array_geometry_set_.IsEmpty())
failed_constraint_name_ = constraint_set.goog_array_geometry.GetName();
CheckContradictoryEchoCancellation();
}
void AudioCaptureCandidates::CheckContradictoryEchoCancellation() {
DiscreteSet<bool> echo_cancellation_intersection =
bool_sets_[ECHO_CANCELLATION].Intersection(
bool_sets_[GOOG_ECHO_CANCELLATION]);
// echoCancellation and googEchoCancellation constraints should not
// contradict each other. Mark the set as empty if they do.
if (echo_cancellation_intersection.IsEmpty()) {
failed_constraint_name_ =
blink::WebMediaTrackConstraintSet().echo_cancellation.GetName();
}
}
// Fitness function for constraints involved in device selection.
// Based on https://w3c.github.io/mediacapture-main/#dfn-fitness-distance
// TODO(guidou): Add support for sampleRate, sampleSize and channelCount
// constraints. http://crbug.com/
double DeviceInfoFitness(
bool is_device_capture,
const AudioDeviceInfo& device_info,
const blink::WebMediaTrackConstraintSet& basic_constraint_set) {
return StringConstraintFitnessDistance(
blink::WebString::FromASCII(device_info.device_id()),
basic_constraint_set.device_id);
}
AudioDeviceInfo SelectDevice(
const AudioDeviceSet& audio_device_set,
const blink::WebMediaTrackConstraintSet& basic_constraint_set,
const std::string& default_device_id,
bool is_device_capture) {
DCHECK(!audio_device_set.IsEmpty());
if (audio_device_set.is_universal()) {
DCHECK(!is_device_capture);
std::string device_id;
if (basic_constraint_set.device_id.HasIdeal()) {
device_id = basic_constraint_set.device_id.Ideal().begin()->Utf8();
}
return AudioDeviceInfo(std::move(device_id));
}
std::vector<double> best_fitness({HUGE_VAL, HUGE_VAL});
auto best_candidate = audio_device_set.elements().end();
for (auto it = audio_device_set.elements().begin();
it != audio_device_set.elements().end(); ++it) {
std::vector<double> fitness;
// First criterion is spec-based fitness function. Second criterion is
// being the default device.
fitness.push_back(
DeviceInfoFitness(is_device_capture, *it, basic_constraint_set));
fitness.push_back(it->device_id() == default_device_id ? 0.0 : HUGE_VAL);
if (fitness < best_fitness) {
best_fitness = std::move(fitness);
best_candidate = it;
}
}
DCHECK(best_candidate != audio_device_set.elements().end());
return *best_candidate;
}
bool SelectBool(const DiscreteSet<bool>& set,
const blink::BooleanConstraint& constraint,
bool default_value) {
DCHECK(!set.IsEmpty());
if (constraint.HasIdeal() && set.Contains(constraint.Ideal())) {
return constraint.Ideal();
}
// Return default value if unconstrained.
if (set.is_universal()) {
return default_value;
}
DCHECK_EQ(set.elements().size(), 1U);
return set.FirstElement();
}
base::Optional<bool> SelectOptionalBool(
const DiscreteSet<bool>& set,
const blink::BooleanConstraint& constraint) {
DCHECK(!set.IsEmpty());
if (constraint.HasIdeal() && set.Contains(constraint.Ideal())) {
return constraint.Ideal();
}
// Return no value if unconstrained.
if (set.is_universal()) {
return base::Optional<bool>();
}
DCHECK_EQ(set.elements().size(), 1U);
return set.FirstElement();
}
base::Optional<std::string> SelectOptionalString(
const DiscreteSet<std::string>& set,
const blink::StringConstraint& constraint) {
DCHECK(!set.IsEmpty());
if (constraint.HasIdeal()) {
for (const auto& ideal_candidate : constraint.Ideal()) {
std::string candidate = ideal_candidate.Utf8();
if (set.Contains(candidate)) {
return candidate;
}
}
}
// Return no value if unconstrained.
if (set.is_universal()) {
return base::Optional<std::string>();
}
return set.FirstElement();
}
bool SelectEnableSwEchoCancellation(
base::Optional<bool> echo_cancellation,
base::Optional<bool> goog_echo_cancellation,
const media::AudioParameters& audio_parameters,
bool default_audio_processing_value) {
// If there is hardware echo cancellation, return false.
if (audio_parameters.IsValid() &&
(audio_parameters.effects() & media::AudioParameters::ECHO_CANCELLER))
return false;
DCHECK(echo_cancellation && goog_echo_cancellation
? *echo_cancellation == *goog_echo_cancellation
: true);
if (echo_cancellation)
return *echo_cancellation;
if (goog_echo_cancellation)
return *goog_echo_cancellation;
return default_audio_processing_value;
}
struct AudioPropertyConstraintPair {
bool AudioProcessingProperties::*audio_property;
AudioCaptureCandidates::BoolConstraint bool_set_index;
blink::BooleanConstraint blink::WebMediaTrackConstraintSet::*constraint;
};
// Boolean audio properties that are mapped directly to a boolean constraint
// and which are subject to the same processing.
const AudioPropertyConstraintPair kAudioPropertyConstraintMap[] = {
{&AudioProcessingProperties::goog_auto_gain_control,
AudioCaptureCandidates::GOOG_AUTO_GAIN_CONTROL,
&blink::WebMediaTrackConstraintSet::goog_auto_gain_control},
{&AudioProcessingProperties::goog_experimental_echo_cancellation,
AudioCaptureCandidates::GOOG_EXPERIMENTAL_ECHO_CANCELLATION,
&blink::WebMediaTrackConstraintSet::goog_experimental_echo_cancellation},
{&AudioProcessingProperties::goog_typing_noise_detection,
AudioCaptureCandidates::GOOG_TYPING_NOISE_DETECTION,
&blink::WebMediaTrackConstraintSet::goog_typing_noise_detection},
{&AudioProcessingProperties::goog_noise_suppression,
AudioCaptureCandidates::GOOG_NOISE_SUPPRESSION,
&blink::WebMediaTrackConstraintSet::goog_noise_suppression},
{&AudioProcessingProperties::goog_experimental_noise_suppression,
AudioCaptureCandidates::GOOG_EXPERIMENTAL_NOISE_SUPPRESSION,
&blink::WebMediaTrackConstraintSet::goog_experimental_noise_suppression},
{&AudioProcessingProperties::goog_beamforming,
AudioCaptureCandidates::GOOG_BEAMFORMING,
&blink::WebMediaTrackConstraintSet::goog_beamforming},
{&AudioProcessingProperties::goog_highpass_filter,
AudioCaptureCandidates::GOOG_HIGHPASS_FILTER,
&blink::WebMediaTrackConstraintSet::goog_highpass_filter},
{&AudioProcessingProperties::goog_experimental_auto_gain_control,
AudioCaptureCandidates::GOOG_EXPERIMENTAL_AUTO_GAIN_CONTROL,
&blink::WebMediaTrackConstraintSet::goog_experimental_auto_gain_control}};
AudioProcessingProperties SelectAudioProcessingProperties(
const AudioCaptureCandidates& candidates,
const blink::WebMediaTrackConstraintSet& basic_constraint_set,
const media::AudioParameters& audio_parameters,
bool is_device_capture) {
DCHECK(!candidates.IsEmpty());
base::Optional<bool> echo_cancellation =
SelectOptionalBool(candidates.echo_cancellation_set(),
basic_constraint_set.echo_cancellation);
// Audio-processing properties are disabled by default for content capture, or
// if the |echo_cancellation| constraint is false.
bool default_audio_processing_value = true;
if (!is_device_capture || (echo_cancellation && !*echo_cancellation))
default_audio_processing_value = false;
base::Optional<bool> goog_echo_cancellation =
SelectOptionalBool(candidates.goog_echo_cancellation_set(),
basic_constraint_set.goog_echo_cancellation);
AudioProcessingProperties properties;
properties.enable_sw_echo_cancellation = SelectEnableSwEchoCancellation(
echo_cancellation, goog_echo_cancellation, audio_parameters,
default_audio_processing_value);
properties.disable_hw_echo_cancellation =
(echo_cancellation && !*echo_cancellation) ||
(goog_echo_cancellation && !*goog_echo_cancellation);
properties.goog_audio_mirroring =
SelectBool(candidates.goog_audio_mirroring_set(),
basic_constraint_set.goog_audio_mirroring,
properties.goog_audio_mirroring);
for (auto& entry : kAudioPropertyConstraintMap) {
properties.*entry.audio_property = SelectBool(
candidates.GetBoolSet(entry.bool_set_index),
basic_constraint_set.*entry.constraint,
default_audio_processing_value && properties.*entry.audio_property);
}
base::Optional<std::string> array_geometry =
SelectOptionalString(candidates.goog_array_geometry_set(),
basic_constraint_set.goog_array_geometry);
std::vector<media::Point> parsed_positions;
if (array_geometry)
parsed_positions = media::ParsePointsFromString(*array_geometry);
bool are_valid_parsed_positions =
!parsed_positions.empty() || (array_geometry && array_geometry->empty());
properties.goog_array_geometry = are_valid_parsed_positions
? std::move(parsed_positions)
: audio_parameters.mic_positions();
return properties;
}
AudioCaptureSettings SelectResult(
const AudioCaptureCandidates& candidates,
const blink::WebMediaTrackConstraintSet& basic_constraint_set,
const std::string& default_device_id,
const std::string& media_stream_source) {
bool is_device_capture = media_stream_source.empty();
AudioDeviceInfo device_info =
SelectDevice(candidates.audio_device_set(), basic_constraint_set,
default_device_id, is_device_capture);
bool hotword_enabled =
SelectBool(candidates.hotword_enabled_set(),
basic_constraint_set.hotword_enabled, false);
bool disable_local_echo_default =
media_stream_source != kMediaStreamSourceDesktop;
bool disable_local_echo = SelectBool(candidates.disable_local_echo_set(),
basic_constraint_set.disable_local_echo,
disable_local_echo_default);
bool render_to_associated_sink =
SelectBool(candidates.render_to_associated_sink_set(),
basic_constraint_set.render_to_associated_sink, false);
AudioProcessingProperties audio_processing_properties =
SelectAudioProcessingProperties(candidates, basic_constraint_set,
device_info.parameters(),
is_device_capture);
return AudioCaptureSettings(device_info.device_id(), device_info.parameters(),
hotword_enabled, disable_local_echo,
render_to_associated_sink,
audio_processing_properties);
}
} // namespace
AudioCaptureSettings SelectSettingsAudioCapture(
const AudioDeviceCaptureCapabilities& capabilities,
const blink::WebMediaConstraints& constraints) {
std::string media_stream_source = GetMediaStreamSource(constraints);
bool is_device_capture = media_stream_source.empty();
if (is_device_capture && capabilities.empty())
return AudioCaptureSettings();
AudioCaptureCandidates candidates(constraints.Basic(), capabilities,
is_device_capture);
if (candidates.IsEmpty()) {
return AudioCaptureSettings(candidates.failed_constraint_name());
}
for (const auto& advanced_set : constraints.Advanced()) {
AudioCaptureCandidates advanced_candidates(advanced_set, capabilities,
is_device_capture);
AudioCaptureCandidates intersection =
candidates.Intersection(advanced_candidates);
if (!intersection.IsEmpty())
candidates = std::move(intersection);
}
DCHECK(!candidates.IsEmpty());
std::string default_device_id;
if (!capabilities.empty())
default_device_id = (*capabilities.begin())->device_id;
return SelectResult(candidates, constraints.Basic(), default_device_id,
media_stream_source);
}
} // namespace content