Commit 0b4af239 authored by Guido Urdaneta's avatar Guido Urdaneta Committed by Commit Bot

Update MediaStreamTrack constraints processing algorithm for audio

This CL introduces two main updates to constraints processing for audio:
1. The SelectSettings() algorithm now considers audio-processing
   properties as belonging to each device instead of being considered
   independent of the device. The reason is that, while the
   audio-processing module can be applied to any source, once a source
   has processing applied, all tracks for that source must use the same
   audio-processing settings.
2. The getUserMedia() implementation leverages (1) and makes sure that
   candidate settings for audio sources already in use are limited to
   their current configuration.

Bug: 802198
Change-Id: I3554206e852ff41116a1fe02be45a7a8dff4760d
Reviewed-on: https://chromium-review.googlesource.com/881183Reviewed-by: default avatarHenrik Boström <hbos@chromium.org>
Commit-Queue: Guido Urdaneta <guidou@chromium.org>
Cr-Commit-Position: refs/heads/master@{#533288}
parent a97831b2
...@@ -811,4 +811,12 @@ IN_PROC_BROWSER_TEST_F(WebRtcGetUserMediaBrowserTest, ...@@ -811,4 +811,12 @@ IN_PROC_BROWSER_TEST_F(WebRtcGetUserMediaBrowserTest,
ExecuteJavascriptAndWaitForOk("getUserMediaAfterStopCanvasCapture()"); ExecuteJavascriptAndWaitForOk("getUserMediaAfterStopCanvasCapture()");
} }
IN_PROC_BROWSER_TEST_F(WebRtcGetUserMediaBrowserTest,
GetUserMediaEchoCancellationOnAndOff) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url(embedded_test_server()->GetURL("/media/getusermedia.html"));
NavigateToURL(shell(), url);
ExecuteJavascriptAndWaitForOk("getUserMediaEchoCancellationOnAndOff()");
}
} // namespace content } // namespace content
...@@ -23,122 +23,53 @@ namespace content { ...@@ -23,122 +23,53 @@ namespace content {
namespace { namespace {
// This class has the same data as blink::mojom::AudioInputDeviceCapabilities, enum BoolConstraint {
// but adds extra operations to simplify access to device parameters. // Constraints not related to audio processing.
class AudioDeviceInfo { HOTWORD_ENABLED,
public: DISABLE_LOCAL_ECHO,
// This constructor is intended for device capture. RENDER_TO_ASSOCIATED_SINK,
explicit AudioDeviceInfo(
const blink::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)) {}
// This constructor is intended to aid in the implementation of
// applyConstraints(), where the source/device is already set and cannot be
// changed.
AudioDeviceInfo(std::string device_id,
const media::AudioParameters& parameters)
: device_id_(std::move(device_id)), parameters_(parameters) {}
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 // Constraints that enable/disable audio processing.
int SampleRate() const { ECHO_CANCELLATION,
DCHECK(parameters_.IsValid()); GOOG_ECHO_CANCELLATION,
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: // Constraints that control audio-processing behavior.
std::string device_id_; GOOG_AUDIO_MIRRORING,
media::AudioParameters parameters_; 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
}; };
using AudioDeviceSet = DiscreteSet<AudioDeviceInfo>; // This struct groups related fields or entries from AudioProcessingProperties,
// SingleDeviceCandidateSet::bool_sets_ and blink::WebMediaTrackConstraintSet.
AudioDeviceSet AudioDeviceSetForDeviceCapture( struct AudioPropertyConstraintPair {
const blink::WebMediaTrackConstraintSet& constraint_set, bool AudioProcessingProperties::* audio_property;
const AudioDeviceCaptureCapabilities& capabilities, BoolConstraint bool_set_index;
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 function returns a set that contains the intersection of the device
// associated to |source| and the set of devices allowed by the device_id
// property of |constraint_set|. Therefore, the return value of this function
// is either an empty set or a set containing only the |source|'s device.
AudioDeviceSet AudioDeviceSetFromSource(
const blink::WebMediaTrackConstraintSet& constraint_set,
MediaStreamAudioSource* source,
const char** failed_constraint_name) {
std::vector<AudioDeviceInfo> result;
*failed_constraint_name = "";
if (constraint_set.device_id.Matches(
blink::WebString::FromASCII(source->device().id))) {
result.push_back(
AudioDeviceInfo(source->device().id, source->device().input));
} else {
if (failed_constraint_name)
*failed_constraint_name = constraint_set.device_id.GetName();
}
if (!result.empty())
*failed_constraint_name = nullptr;
return AudioDeviceSet(std::move(result)); // 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,
GOOG_AUTO_GAIN_CONTROL},
{&AudioProcessingProperties::goog_experimental_echo_cancellation,
GOOG_EXPERIMENTAL_ECHO_CANCELLATION},
{&AudioProcessingProperties::goog_typing_noise_detection,
GOOG_TYPING_NOISE_DETECTION},
{&AudioProcessingProperties::goog_noise_suppression,
GOOG_NOISE_SUPPRESSION},
{&AudioProcessingProperties::goog_experimental_noise_suppression,
GOOG_EXPERIMENTAL_NOISE_SUPPRESSION},
{&AudioProcessingProperties::goog_beamforming, GOOG_BEAMFORMING},
{&AudioProcessingProperties::goog_highpass_filter, GOOG_HIGHPASS_FILTER},
{&AudioProcessingProperties::goog_experimental_auto_gain_control,
GOOG_EXPERIMENTAL_AUTO_GAIN_CONTROL}};
// TODO(guidou): Remove this function. http://crbug.com/796955 // TODO(guidou): Remove this function. http://crbug.com/796955
std::string MediaPointToString(const media::Point& point) { std::string MediaPointToString(const media::Point& point) {
...@@ -166,221 +97,125 @@ std::string MediaPointsToString(const std::vector<media::Point>& points) { ...@@ -166,221 +97,125 @@ std::string MediaPointsToString(const std::vector<media::Point>& points) {
return points_string; return points_string;
} }
// This class represents a set of possible candidate settings. // Selects the best value from the nonempty |set|, subject to |constraint|. The
// The SelectSettings algorithm starts with a set containing all possible // first selection criteria is equality to |constraint.Ideal()|, followed by
// candidates based on hardware capabilities and/or allowed values for supported // equality to |default_value|. There is always a single best value.
// properties. The is then reduced progressively as the basic and advanced bool SelectBool(const DiscreteSet<bool>& set,
// constraint sets are applied. const blink::BooleanConstraint& constraint,
// In the end, if the set of candidates is empty, SelectSettings fails. bool default_value) {
// If not, the ideal values (if any) or tie breaker rules are used to select DCHECK(!set.IsEmpty());
// the final settings based on the candidates that survived the application if (constraint.HasIdeal() && set.Contains(constraint.Ideal()))
// of the constraint sets. return constraint.Ideal();
// 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( if (set.is_universal())
const blink::WebMediaTrackConstraintSet& constraint_set, return default_value;
const AudioDeviceCaptureCapabilities& capabilities,
bool is_device_capture);
// Returns a set of candidates compatible with |source|'s settings. DCHECK_EQ(set.elements().size(), 1U);
AudioCaptureCandidates( return set.FirstElement();
const blink::WebMediaTrackConstraintSet& constraint_set, }
MediaStreamAudioSource* source);
// Set operations. // Selects the best value from the nonempty |set|, subject to |constraint|. The
bool IsEmpty() const { return failed_constraint_name_ != nullptr; } // only decision criteria is equality to |constraint.Ideal()|. If there is no
AudioCaptureCandidates Intersection(const AudioCaptureCandidates& other); // single best value in |set|, returns nullopt.
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();
// Accessors. if (set.is_universal())
const char* failed_constraint_name() const { return failed_constraint_name_; } return base::Optional<bool>();
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. DCHECK_EQ(set.elements().size(), 1U);
const DiscreteSet<bool>& GetBoolSet(BoolConstraint property) const { return set.FirstElement();
DCHECK_GE(property, 0); }
DCHECK_LT(property, NUM_BOOL_CONSTRAINTS);
return bool_sets_[property];
}
// Convenience accessors. // Selects the best value from the nonempty |set|, subject to |constraint|. The
const DiscreteSet<bool>& hotword_enabled_set() const { // first selection criteria is inclusion in |constraint.Ideal()|, followed by
return bool_sets_[HOTWORD_ENABLED]; // equality to |default_value|. There is always a single best value.
} std::string SelectString(const DiscreteSet<std::string>& set,
const DiscreteSet<bool>& disable_local_echo_set() const { const blink::StringConstraint& constraint,
return bool_sets_[DISABLE_LOCAL_ECHO]; const std::string& default_value) {
} DCHECK(!set.IsEmpty());
const DiscreteSet<bool>& render_to_associated_sink_set() const { if (constraint.HasIdeal()) {
return bool_sets_[RENDER_TO_ASSOCIATED_SINK]; for (const blink::WebString& ideal_candidate : constraint.Ideal()) {
} std::string candidate = ideal_candidate.Utf8();
const DiscreteSet<bool>& echo_cancellation_set() const { if (set.Contains(candidate))
return bool_sets_[ECHO_CANCELLATION]; return candidate;
}
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: if (set.Contains(default_value))
void MaybeUpdateFailedNonDeviceConstraintName(); return default_value;
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. return set.FirstElement();
std::array<DiscreteSet<bool>, NUM_BOOL_CONSTRAINTS> bool_sets_; }
DiscreteSet<std::string> goog_array_geometry_set_;
};
const blink::BooleanConstraint blink::WebMediaTrackConstraintSet::* // Selects the best value from the nonempty |set|, subject to |constraint|. The
AudioCaptureCandidates::kBlinkBoolConstraintFields[NUM_BOOL_CONSTRAINTS] = { // only decision criteria is inclusion in |constraint.Ideal()|. If there is no
&blink::WebMediaTrackConstraintSet::hotword_enabled, // single best value in |set|, returns nullopt.
&blink::WebMediaTrackConstraintSet::disable_local_echo, base::Optional<std::string> SelectOptionalString(
&blink::WebMediaTrackConstraintSet::render_to_associated_sink, const DiscreteSet<std::string>& set,
&blink::WebMediaTrackConstraintSet::echo_cancellation, const blink::StringConstraint& constraint) {
&blink::WebMediaTrackConstraintSet::goog_echo_cancellation, DCHECK(!set.IsEmpty());
&blink::WebMediaTrackConstraintSet::goog_audio_mirroring, if (constraint.HasIdeal()) {
&blink::WebMediaTrackConstraintSet::goog_auto_gain_control, for (const auto& ideal_candidate : constraint.Ideal()) {
&blink::WebMediaTrackConstraintSet::goog_experimental_echo_cancellation, std::string candidate = ideal_candidate.Utf8();
&blink::WebMediaTrackConstraintSet::goog_typing_noise_detection, if (set.Contains(candidate))
&blink::WebMediaTrackConstraintSet::goog_noise_suppression, return candidate;
&blink::WebMediaTrackConstraintSet::goog_experimental_noise_suppression, }
&blink::WebMediaTrackConstraintSet::goog_beamforming, }
&blink::WebMediaTrackConstraintSet::goog_highpass_filter,
&blink::WebMediaTrackConstraintSet::
goog_experimental_auto_gain_control};
// This struct groups related fields or entries from AudioProcessingProperties, if (set.is_universal())
// AudioCaptureCandidates::bool_sets_ and blink::WebMediaTrackConstraintSet. return base::Optional<std::string>();
struct AudioPropertyConstraintTuple {
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 return set.FirstElement();
// and which are subject to the same processing. }
const AudioPropertyConstraintTuple 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}};
// directly mapped boolean sets to audio properties
struct BoolSetPropertyEntry {
DiscreteSet<bool> AudioCaptureCandidates::*bool_set;
bool AudioProcessingProperties::*bool_field;
bool default_value;
};
AudioCaptureCandidates::AudioCaptureCandidates() bool SelectEnableSwEchoCancellation(
: failed_constraint_name_(nullptr) {} 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;
AudioCaptureCandidates::AudioCaptureCandidates( return default_audio_processing_value;
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( // This class represents all the candidates settings for a single audio device.
const blink::WebMediaTrackConstraintSet& constraint_set, class SingleDeviceCandidateSet {
MediaStreamAudioSource* source) public:
: failed_constraint_name_(nullptr), explicit SingleDeviceCandidateSet(
audio_device_set_(AudioDeviceSetFromSource(constraint_set, const AudioDeviceCaptureCapability& capability)
source, : parameters_(capability.Parameters()) {
&failed_constraint_name_)), // If empty, all values for the deviceId constraint are allowed and
goog_array_geometry_set_( // |device_id_set_| is the universal set. Otherwise, limit |device_id_set_|
StringSetFromConstraint(constraint_set.goog_array_geometry)) { // to the known device ID.
for (size_t i = 0; i < NUM_BOOL_CONSTRAINTS; ++i) { if (!capability.DeviceID().empty())
bool_sets_[i] = device_id_set_ = DiscreteSet<std::string>({capability.DeviceID()});
BoolSetFromConstraint(constraint_set.*kBlinkBoolConstraintFields[i]);
} MediaStreamAudioSource* source = capability.source();
if (!source)
// |audio_device_set_| contains, at most, the device ID of |source|, but return;
// |bool_sets_| and |goog_array_geometry_set_| are constrained by
// |constraint_set| but not by |source|'s settings. To ensure that only
// values compatible with |source| are allowed, each of these sets has to be
// intersected with the values allowed by |source| for each property.
// Properties not related to audio processing. // Properties not related to audio processing.
bool_sets_[HOTWORD_ENABLED] = bool_sets_[HOTWORD_ENABLED].Intersection( bool_sets_[HOTWORD_ENABLED] =
DiscreteSet<bool>({source->hotword_enabled()})); DiscreteSet<bool>({source->hotword_enabled()});
bool_sets_[DISABLE_LOCAL_ECHO] = bool_sets_[DISABLE_LOCAL_ECHO].Intersection( bool_sets_[DISABLE_LOCAL_ECHO] =
DiscreteSet<bool>({source->disable_local_echo()})); DiscreteSet<bool>({source->disable_local_echo()});
bool_sets_[RENDER_TO_ASSOCIATED_SINK] = bool_sets_[RENDER_TO_ASSOCIATED_SINK] =
bool_sets_[RENDER_TO_ASSOCIATED_SINK].Intersection( DiscreteSet<bool>({source->RenderToAssociatedSinkEnabled()});
DiscreteSet<bool>({source->RenderToAssociatedSinkEnabled()}));
// Properties related with audio processing. // Properties related with audio processing.
AudioProcessingProperties properties; AudioProcessingProperties properties;
...@@ -403,20 +238,25 @@ AudioCaptureCandidates::AudioCaptureCandidates( ...@@ -403,20 +238,25 @@ AudioCaptureCandidates::AudioCaptureCandidates(
echo_cancellation_enabled = source->device().input.effects() & echo_cancellation_enabled = source->device().input.effects() &
media::AudioParameters::ECHO_CANCELLER; media::AudioParameters::ECHO_CANCELLER;
} }
bool_sets_[ECHO_CANCELLATION] = bool_sets_[ECHO_CANCELLATION].Intersection( bool_sets_[ECHO_CANCELLATION] =
DiscreteSet<bool>({echo_cancellation_enabled})); DiscreteSet<bool>({echo_cancellation_enabled});
bool_sets_[GOOG_ECHO_CANCELLATION] = bool_sets_[ECHO_CANCELLATION]; bool_sets_[GOOG_ECHO_CANCELLATION] = bool_sets_[ECHO_CANCELLATION];
bool_sets_[GOOG_AUDIO_MIRRORING] = bool_sets_[GOOG_AUDIO_MIRRORING] =
bool_sets_[GOOG_AUDIO_MIRRORING].Intersection( DiscreteSet<bool>({properties.goog_audio_mirroring});
DiscreteSet<bool>({properties.goog_audio_mirroring}));
for (auto& entry : kAudioPropertyConstraintMap) { for (auto& entry : kAudioPropertyConstraintMap) {
bool_sets_[entry.bool_set_index] = bool_sets_[entry.bool_set_index] =
bool_sets_[entry.bool_set_index].Intersection( DiscreteSet<bool>({properties.*entry.audio_property});
DiscreteSet<bool>({properties.*entry.audio_property}));
} }
#if DCHECK_IS_ON()
for (const auto& bool_set : bool_sets_) {
DCHECK(!bool_set.is_universal());
DCHECK(!bool_set.IsEmpty());
}
#endif
// This fails with input strings that are equivalent to // This fails with input strings that are equivalent to
// |properties.goog_array_geometry|, but not exactly equal to the string // |properties.goog_array_geometry|, but not exactly equal to the string
// returned by MediaPointsToString(). // returned by MediaPointsToString().
...@@ -424,204 +264,148 @@ AudioCaptureCandidates::AudioCaptureCandidates( ...@@ -424,204 +264,148 @@ AudioCaptureCandidates::AudioCaptureCandidates(
// points instead of a set of strings. http://crbug.com/796955 // points instead of a set of strings. http://crbug.com/796955
std::string mic_positions = std::string mic_positions =
MediaPointsToString(properties.goog_array_geometry); MediaPointsToString(properties.goog_array_geometry);
goog_array_geometry_set_ = goog_array_geometry_set_.Intersection( goog_array_geometry_set_ = DiscreteSet<std::string>({mic_positions});
DiscreteSet<std::string>({mic_positions})); }
MaybeUpdateFailedNonDeviceConstraintName();
}
AudioCaptureCandidates AudioCaptureCandidates::Intersection( // Accessors
const AudioCaptureCandidates& other) { const char* failed_constraint_name() const { return failed_constraint_name_; }
AudioCaptureCandidates intersection; const DiscreteSet<std::string>& device_id_set() const {
intersection.audio_device_set_ = return device_id_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 IsEmpty() const { return failed_constraint_name_ != nullptr; }
bool_sets_[i].Intersection(other.bool_sets_[i]);
void ApplyConstraintSet(
const blink::WebMediaTrackConstraintSet& constraint_set) {
device_id_set_ = device_id_set_.Intersection(
StringSetFromConstraint(constraint_set.device_id));
if (device_id_set_.IsEmpty()) {
failed_constraint_name_ = constraint_set.device_id.GetName();
return;
} }
intersection.goog_array_geometry_set_ =
goog_array_geometry_set_.Intersection(other.goog_array_geometry_set_);
intersection.MaybeUpdateFailedNonDeviceConstraintName();
return intersection; goog_array_geometry_set_ = goog_array_geometry_set_.Intersection(
} StringSetFromConstraint(constraint_set.goog_array_geometry));
if (goog_array_geometry_set_.IsEmpty()) {
failed_constraint_name_ = constraint_set.goog_array_geometry.GetName();
return;
}
void AudioCaptureCandidates::MaybeUpdateFailedNonDeviceConstraintName() {
blink::WebMediaTrackConstraintSet constraint_set;
for (size_t i = 0; i < NUM_BOOL_CONSTRAINTS; ++i) { for (size_t i = 0; i < NUM_BOOL_CONSTRAINTS; ++i) {
bool_sets_[i] = bool_sets_[i].Intersection(
BoolSetFromConstraint(constraint_set.*kBlinkBoolConstraintFields[i]));
if (bool_sets_[i].IsEmpty()) { if (bool_sets_[i].IsEmpty()) {
failed_constraint_name_ = failed_constraint_name_ =
(constraint_set.*kBlinkBoolConstraintFields[i]).GetName(); (constraint_set.*kBlinkBoolConstraintFields[i]).GetName();
return;
} }
} }
if (goog_array_geometry_set_.IsEmpty())
failed_constraint_name_ = constraint_set.goog_array_geometry.GetName();
CheckContradictoryEchoCancellation(); // echoCancellation and googEchoCancellation constraints should not
} // contradict each other. Mark the set as empty if they do.
void AudioCaptureCandidates::CheckContradictoryEchoCancellation() {
DiscreteSet<bool> echo_cancellation_intersection = DiscreteSet<bool> echo_cancellation_intersection =
bool_sets_[ECHO_CANCELLATION].Intersection( bool_sets_[ECHO_CANCELLATION].Intersection(
bool_sets_[GOOG_ECHO_CANCELLATION]); 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()) { if (echo_cancellation_intersection.IsEmpty()) {
failed_constraint_name_ = failed_constraint_name_ =
blink::WebMediaTrackConstraintSet().echo_cancellation.GetName(); blink::WebMediaTrackConstraintSet().echo_cancellation.GetName();
return;
} }
}
// 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/731170
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}); // Fitness function to support device selection. Based on
auto best_candidate = audio_device_set.elements().end(); // https://w3c.github.io/mediacapture-main/#dfn-fitness-distance
for (auto it = audio_device_set.elements().begin(); double Fitness(
it != audio_device_set.elements().end(); ++it) { const blink::WebMediaTrackConstraintSet& constraint_set) const {
std::vector<double> fitness; double fitness = 0.0;
// 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, if (constraint_set.device_id.HasIdeal()) {
const blink::BooleanConstraint& constraint, for (const blink::WebString& ideal_value :
bool default_value) { constraint_set.device_id.Ideal()) {
DCHECK(!set.IsEmpty()); if (device_id_set_.Contains(ideal_value.Utf8())) {
if (constraint.HasIdeal() && set.Contains(constraint.Ideal())) { fitness += 1.0;
return constraint.Ideal(); break;
} }
// 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. for (size_t i = 0; i < NUM_BOOL_CONSTRAINTS; ++i) {
if (set.is_universal()) { if ((constraint_set.*kBlinkBoolConstraintFields[i]).HasIdeal() &&
return base::Optional<bool>(); bool_sets_[i].Contains(
(constraint_set.*kBlinkBoolConstraintFields[i]).Ideal())) {
fitness += 1.0;
}
} }
DCHECK_EQ(set.elements().size(), 1U);
return set.FirstElement();
}
base::Optional<std::string> SelectOptionalString( if (constraint_set.goog_array_geometry.HasIdeal()) {
const DiscreteSet<std::string>& set, for (const blink::WebString& ideal_value :
const blink::StringConstraint& constraint) { constraint_set.goog_array_geometry.Ideal()) {
DCHECK(!set.IsEmpty()); if (goog_array_geometry_set_.Contains(ideal_value.Utf8())) {
if (constraint.HasIdeal()) { fitness += 1.0;
for (const auto& ideal_candidate : constraint.Ideal()) { break;
std::string candidate = ideal_candidate.Utf8();
if (set.Contains(candidate)) {
return candidate;
} }
} }
} }
// Return no value if unconstrained. return fitness;
if (set.is_universal()) {
return base::Optional<std::string>();
} }
return set.FirstElement();
}
bool SelectEnableSwEchoCancellation( // Returns the settings supported by this SingleDeviceCandidateSet that best
base::Optional<bool> echo_cancellation, // satisfy the ideal values in |basic_constraint_set|.
base::Optional<bool> goog_echo_cancellation, AudioCaptureSettings SelectBestSettings(
const media::AudioParameters& audio_parameters, const blink::WebMediaTrackConstraintSet& basic_constraint_set,
bool default_audio_processing_value) { const std::string& default_device_id,
// If there is hardware echo cancellation, return false. const std::string& media_stream_source,
if (audio_parameters.IsValid() && bool should_disable_hardware_noise_suppression) const {
(audio_parameters.effects() & media::AudioParameters::ECHO_CANCELLER)) std::string device_id = SelectString(
return false; device_id_set_, basic_constraint_set.device_id, default_device_id);
DCHECK(echo_cancellation && goog_echo_cancellation bool hotword_enabled =
? *echo_cancellation == *goog_echo_cancellation SelectBool(bool_sets_[HOTWORD_ENABLED],
: true); basic_constraint_set.hotword_enabled, false);
if (echo_cancellation) bool disable_local_echo_default =
return *echo_cancellation; media_stream_source != kMediaStreamSourceDesktop;
if (goog_echo_cancellation) bool disable_local_echo = SelectBool(
return *goog_echo_cancellation; bool_sets_[DISABLE_LOCAL_ECHO], basic_constraint_set.disable_local_echo,
disable_local_echo_default);
bool render_to_associated_sink =
SelectBool(bool_sets_[RENDER_TO_ASSOCIATED_SINK],
basic_constraint_set.render_to_associated_sink, false);
return default_audio_processing_value; bool is_device_capture = media_stream_source.empty();
} AudioProcessingProperties audio_processing_properties =
SelectAudioProcessingProperties(
basic_constraint_set, is_device_capture,
should_disable_hardware_noise_suppression);
AudioProcessingProperties SelectAudioProcessingProperties( return AudioCaptureSettings(
const AudioCaptureCandidates& candidates, std::move(device_id), parameters_, hotword_enabled, disable_local_echo,
render_to_associated_sink, audio_processing_properties);
}
private:
// Returns the audio-processing properties supported by this
// SingleDeviceCandidateSet that best satisfy the ideal values in
// |basic_constraint_set|.
AudioProcessingProperties SelectAudioProcessingProperties(
const blink::WebMediaTrackConstraintSet& basic_constraint_set, const blink::WebMediaTrackConstraintSet& basic_constraint_set,
const media::AudioParameters& audio_parameters,
bool is_device_capture, bool is_device_capture,
bool should_disable_hardware_noise_suppression) { bool should_disable_hardware_noise_suppression) const {
DCHECK(!candidates.IsEmpty()); DCHECK(!IsEmpty());
base::Optional<bool> echo_cancellation = base::Optional<bool> echo_cancellation = SelectOptionalBool(
SelectOptionalBool(candidates.echo_cancellation_set(), bool_sets_[ECHO_CANCELLATION], basic_constraint_set.echo_cancellation);
basic_constraint_set.echo_cancellation); // Audio-processing properties are disabled by default for content capture,
// Audio-processing properties are disabled by default for content capture, or // or if the |echo_cancellation| constraint is false.
// if the |echo_cancellation| constraint is false.
bool default_audio_processing_value = true; bool default_audio_processing_value = true;
if (!is_device_capture || (echo_cancellation && !*echo_cancellation)) if (!is_device_capture || (echo_cancellation && !*echo_cancellation))
default_audio_processing_value = false; default_audio_processing_value = false;
base::Optional<bool> goog_echo_cancellation = base::Optional<bool> goog_echo_cancellation =
SelectOptionalBool(candidates.goog_echo_cancellation_set(), SelectOptionalBool(bool_sets_[GOOG_ECHO_CANCELLATION],
basic_constraint_set.goog_echo_cancellation); basic_constraint_set.goog_echo_cancellation);
AudioProcessingProperties properties; AudioProcessingProperties properties;
properties.enable_sw_echo_cancellation = SelectEnableSwEchoCancellation( properties.enable_sw_echo_cancellation = SelectEnableSwEchoCancellation(
echo_cancellation, goog_echo_cancellation, audio_parameters, echo_cancellation, goog_echo_cancellation, parameters_,
default_audio_processing_value); default_audio_processing_value);
properties.disable_hw_echo_cancellation = properties.disable_hw_echo_cancellation =
(echo_cancellation && !*echo_cancellation) || (echo_cancellation && !*echo_cancellation) ||
...@@ -631,98 +415,208 @@ AudioProcessingProperties SelectAudioProcessingProperties( ...@@ -631,98 +415,208 @@ AudioProcessingProperties SelectAudioProcessingProperties(
!properties.disable_hw_echo_cancellation; !properties.disable_hw_echo_cancellation;
properties.goog_audio_mirroring = properties.goog_audio_mirroring =
SelectBool(candidates.goog_audio_mirroring_set(), SelectBool(bool_sets_[GOOG_AUDIO_MIRRORING],
basic_constraint_set.goog_audio_mirroring, basic_constraint_set.goog_audio_mirroring,
properties.goog_audio_mirroring); properties.goog_audio_mirroring);
for (auto& entry : kAudioPropertyConstraintMap) { for (auto& entry : kAudioPropertyConstraintMap) {
properties.*entry.audio_property = SelectBool( properties.*entry.audio_property = SelectBool(
candidates.GetBoolSet(entry.bool_set_index), bool_sets_[entry.bool_set_index],
basic_constraint_set.*entry.constraint, basic_constraint_set.*
kBlinkBoolConstraintFields[entry.bool_set_index],
default_audio_processing_value && properties.*entry.audio_property); default_audio_processing_value && properties.*entry.audio_property);
} }
base::Optional<std::string> array_geometry = base::Optional<std::string> array_geometry = SelectOptionalString(
SelectOptionalString(candidates.goog_array_geometry_set(), goog_array_geometry_set_, basic_constraint_set.goog_array_geometry);
basic_constraint_set.goog_array_geometry);
std::vector<media::Point> parsed_positions; std::vector<media::Point> parsed_positions;
if (array_geometry) if (array_geometry)
parsed_positions = media::ParsePointsFromString(*array_geometry); parsed_positions = media::ParsePointsFromString(*array_geometry);
bool are_valid_parsed_positions = bool are_valid_parsed_positions =
!parsed_positions.empty() || (array_geometry && array_geometry->empty()); !parsed_positions.empty() ||
(array_geometry && array_geometry->empty());
properties.goog_array_geometry = are_valid_parsed_positions properties.goog_array_geometry = are_valid_parsed_positions
? std::move(parsed_positions) ? std::move(parsed_positions)
: audio_parameters.mic_positions(); : parameters_.mic_positions();
return properties; return properties;
} }
static constexpr blink::BooleanConstraint
blink::WebMediaTrackConstraintSet::* const
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};
const char* failed_constraint_name_ = nullptr;
DiscreteSet<std::string> device_id_set_;
std::array<DiscreteSet<bool>, NUM_BOOL_CONSTRAINTS> bool_sets_;
DiscreteSet<std::string> goog_array_geometry_set_;
media::AudioParameters parameters_;
};
constexpr blink::BooleanConstraint blink::WebMediaTrackConstraintSet::* const
SingleDeviceCandidateSet::kBlinkBoolConstraintFields[NUM_BOOL_CONSTRAINTS];
// 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 set 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:
AudioCaptureCandidates(
const blink::WebMediaTrackConstraintSet& constraint_set,
const AudioDeviceCaptureCapabilities& capabilities) {
for (const auto& capability : capabilities)
candidate_sets_.emplace_back(capability);
ApplyConstraintSet(constraint_set);
}
const char* failed_constraint_name() const { return failed_constraint_name_; }
bool IsEmpty() const { return failed_constraint_name_ != nullptr; }
AudioCaptureSettings SelectResult( void ApplyConstraintSet(
const AudioCaptureCandidates& candidates, const blink::WebMediaTrackConstraintSet& constraint_set) {
for (auto& candidate_set : candidate_sets_)
candidate_set.ApplyConstraintSet(constraint_set);
const char* failed_constraint_name = nullptr;
for (auto it = candidate_sets_.begin(); it != candidate_sets_.end();) {
if (it->IsEmpty()) {
DCHECK(it->failed_constraint_name());
failed_constraint_name = it->failed_constraint_name();
it = candidate_sets_.erase(it);
} else {
++it;
}
}
if (candidate_sets_.empty())
failed_constraint_name_ = failed_constraint_name;
}
// Returns the settings that best satisfy the ideal values in
// |basic_constraint_set| subject to the limitations of this
// AudioCaptureCandidates object.
AudioCaptureSettings SelectBestSettings(
const blink::WebMediaTrackConstraintSet& basic_constraint_set, const blink::WebMediaTrackConstraintSet& basic_constraint_set,
const std::string& default_device_id, const std::string& default_device_id,
const std::string& media_stream_source, const std::string& media_stream_source,
bool should_disable_hardware_noise_suppression) { bool should_disable_hardware_noise_suppression) const {
bool is_device_capture = media_stream_source.empty(); const SingleDeviceCandidateSet* device_candidate_set =
AudioDeviceInfo device_info = SelectBestDevice(basic_constraint_set, default_device_id);
SelectDevice(candidates.audio_device_set(), basic_constraint_set, DCHECK(!device_candidate_set->IsEmpty());
default_device_id, is_device_capture); return device_candidate_set->SelectBestSettings(
bool hotword_enabled = basic_constraint_set, default_device_id, media_stream_source,
SelectBool(candidates.hotword_enabled_set(), should_disable_hardware_noise_suppression);
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 = private:
SelectAudioProcessingProperties( // Selects the best device based on the fitness function.
candidates, basic_constraint_set, device_info.parameters(), // The returned pointer is valid as long as |candidate_sets_| is not mutated.
is_device_capture, should_disable_hardware_noise_suppression); const SingleDeviceCandidateSet* SelectBestDevice(
const blink::WebMediaTrackConstraintSet& constraint_set,
const std::string& default_device_id) const {
DCHECK(!candidate_sets_.empty());
return AudioCaptureSettings(device_info.device_id(), device_info.parameters(), auto best_candidate = candidate_sets_.end();
hotword_enabled, disable_local_echo, std::vector<double> best_fitness({-1, -1});
render_to_associated_sink,
audio_processing_properties); for (auto it = candidate_sets_.begin(); it != candidate_sets_.end(); ++it) {
} DCHECK(!it->IsEmpty());
std::vector<double> fitness;
fitness.push_back(it->Fitness(constraint_set));
// Second selection criterion is being the default device.
fitness.push_back(it->device_id_set().Contains(default_device_id) ? 1.0
: 0.0);
if (fitness > best_fitness) {
best_fitness = fitness;
best_candidate = it;
}
}
return &(*best_candidate);
}
const char* failed_constraint_name_ = nullptr;
std::vector<SingleDeviceCandidateSet> candidate_sets_;
};
} // namespace } // namespace
AudioDeviceCaptureCapability::AudioDeviceCaptureCapability()
: parameters_(media::AudioParameters::UnavailableDeviceParams()) {}
AudioDeviceCaptureCapability::AudioDeviceCaptureCapability(
MediaStreamAudioSource* source)
: source_(source) {}
AudioDeviceCaptureCapability::AudioDeviceCaptureCapability(
std::string device_id,
const media::AudioParameters& parameters)
: device_id_(std::move(device_id)), parameters_(parameters) {
DCHECK(!device_id_.empty());
}
const std::string& AudioDeviceCaptureCapability::DeviceID() const {
return source_ ? source_->device().id : device_id_;
}
const media::AudioParameters& AudioDeviceCaptureCapability::Parameters() const {
return source_ ? source_->device().input : parameters_;
}
AudioCaptureSettings SelectSettingsAudioCapture( AudioCaptureSettings SelectSettingsAudioCapture(
const AudioDeviceCaptureCapabilities& capabilities, const AudioDeviceCaptureCapabilities& capabilities,
const blink::WebMediaConstraints& constraints, const blink::WebMediaConstraints& constraints,
bool should_disable_hardware_noise_suppression) { bool should_disable_hardware_noise_suppression) {
std::string media_stream_source = GetMediaStreamSource(constraints); std::string media_stream_source = GetMediaStreamSource(constraints);
bool is_device_capture = media_stream_source.empty(); bool is_device_capture = media_stream_source.empty();
if (is_device_capture && capabilities.empty()) if (capabilities.empty())
return AudioCaptureSettings(); return AudioCaptureSettings();
AudioCaptureCandidates candidates(constraints.Basic(), capabilities, AudioCaptureCandidates candidates(constraints.Basic(), capabilities);
is_device_capture); if (candidates.IsEmpty())
if (candidates.IsEmpty()) {
return AudioCaptureSettings(candidates.failed_constraint_name()); return AudioCaptureSettings(candidates.failed_constraint_name());
}
for (const auto& advanced_set : constraints.Advanced()) { for (const auto& advanced_set : constraints.Advanced()) {
AudioCaptureCandidates advanced_candidates(advanced_set, capabilities, AudioCaptureCandidates copy = candidates;
is_device_capture); candidates.ApplyConstraintSet(advanced_set);
AudioCaptureCandidates intersection = if (candidates.IsEmpty())
candidates.Intersection(advanced_candidates); candidates = std::move(copy);
if (!intersection.IsEmpty())
candidates = std::move(intersection);
} }
DCHECK(!candidates.IsEmpty()); DCHECK(!candidates.IsEmpty());
std::string default_device_id; std::string default_device_id;
if (!capabilities.empty()) if (is_device_capture)
default_device_id = (*capabilities.begin())->device_id; default_device_id = capabilities.begin()->DeviceID();
return SelectResult(candidates, constraints.Basic(), default_device_id, return candidates.SelectBestSettings(
media_stream_source, constraints.Basic(), default_device_id, media_stream_source,
should_disable_hardware_noise_suppression); should_disable_hardware_noise_suppression);
} }
...@@ -757,25 +651,13 @@ SelectSettingsAudioCapture(MediaStreamAudioSource* source, ...@@ -757,25 +651,13 @@ SelectSettingsAudioCapture(MediaStreamAudioSource* source,
constraints.Basic().media_stream_source.GetName()); constraints.Basic().media_stream_source.GetName());
} }
AudioCaptureCandidates candidates(constraints.Basic(), source); AudioDeviceCaptureCapabilities capabilities = {
if (candidates.IsEmpty()) { AudioDeviceCaptureCapability(source)};
return AudioCaptureSettings(candidates.failed_constraint_name());
}
for (const auto& advanced_set : constraints.Advanced()) {
AudioCaptureCandidates advanced_candidates(advanced_set, source);
AudioCaptureCandidates intersection =
candidates.Intersection(advanced_candidates);
if (!intersection.IsEmpty())
candidates = std::move(intersection);
}
DCHECK(!candidates.IsEmpty());
bool should_disable_hardware_noise_suppression = bool should_disable_hardware_noise_suppression =
!(source->device().input.effects() & !(source->device().input.effects() &
media::AudioParameters::NOISE_SUPPRESSION); media::AudioParameters::NOISE_SUPPRESSION);
return SelectResult(candidates, constraints.Basic(), source->device().id,
media_stream_source, return SelectSettingsAudioCapture(capabilities, constraints,
should_disable_hardware_noise_suppression); should_disable_hardware_noise_suppression);
} }
......
...@@ -20,8 +20,63 @@ namespace content { ...@@ -20,8 +20,63 @@ namespace content {
class MediaStreamAudioSource; class MediaStreamAudioSource;
// This class represents the capability of an audio-capture device.
// It may represent three different things:
// 1. An audio-capture device that is currently in use.
// 2. An audio-capture device that is currently not in use, but whose ID and
// parameters are known (suitable for device capture, where device IDs are
// always known).
// 3. A "device" whose ID is not known (suitable for content capture, where
// it is not possible to have a list of known valid device IDs).
// In cases (1) and (2), the known device ID introduces a restriction on the
// acceptable values for the deviceId constraint, while in case (3) no such
// restriction is imposed and any requested deviceID value will be acceptable
// while processing constraints.
class CONTENT_EXPORT AudioDeviceCaptureCapability {
public:
// This creates an AudioDeviceCaptureCapability that admits all possible
// device names and settings. This is intended to be used as the single
// capability for getUserMedia() with content capture, where the set of valid
// device IDs is infinite.
AudioDeviceCaptureCapability();
// This creates an AudioDeviceCaptureCapability where the device ID is limited
// to |device_id| and other settings are limited by the given |parameters|.
// |device_id| must not be empty. Intended to be used by getUserMedia() with
// device capture for devices that are not currently in use.
AudioDeviceCaptureCapability(std::string device_id,
const media::AudioParameters& parameters);
// This creates an AudioDeviceCaptureCapability where the device ID and other
// settings are restricted to the current settings of |source|. Intended to be
// used by applyConstraints() for both device and content capture, and by
// getUserMedia() with device capture for devices that are currently in use.
explicit AudioDeviceCaptureCapability(MediaStreamAudioSource* source);
// If this capability represents a device currently in use, this method
// returns a pointer to the MediaStreamAudioSource object associated with the
// device. Otherwise, it returns null.
MediaStreamAudioSource* source() const { return source_; }
// Returns the ID of the device associated with this capability. If empty,
// it means that this capability is not associated with a known device and
// no restrictions are imposed on the deviceId or other constraints while
// processing constraints.
const std::string& DeviceID() const;
// Returns the audio parameters for the device associated with this
// capability. If DeviceID() returns an empty string, these parameters contain
// default values that work well for content capture.
const media::AudioParameters& Parameters() const;
private:
MediaStreamAudioSource* source_ = nullptr;
std::string device_id_;
media::AudioParameters parameters_;
};
using AudioDeviceCaptureCapabilities = using AudioDeviceCaptureCapabilities =
std::vector<blink::mojom::AudioInputDeviceCapabilitiesPtr>; std::vector<AudioDeviceCaptureCapability>;
// This function implements the SelectSettings algorithm for audio tracks as // This function implements the SelectSettings algorithm for audio tracks as
// described in https://w3c.github.io/mediacapture-main/#dfn-selectsettings // described in https://w3c.github.io/mediacapture-main/#dfn-selectsettings
......
...@@ -63,39 +63,38 @@ class MediaStreamConstraintsUtilAudioTest ...@@ -63,39 +63,38 @@ class MediaStreamConstraintsUtilAudioTest
public: public:
void SetUp() override { void SetUp() override {
ResetFactory(); ResetFactory();
if (!IsDeviceCapture()) if (IsDeviceCapture()) {
return; capabilities_.emplace_back(
"default_device",
blink::mojom::AudioInputDeviceCapabilitiesPtr device = media::AudioParameters(media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
blink::mojom::AudioInputDeviceCapabilities::New();
device->device_id = "default_device";
device->parameters = media::AudioParameters(
media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
media::CHANNEL_LAYOUT_STEREO, media::CHANNEL_LAYOUT_STEREO,
media::AudioParameters::kAudioCDSampleRate, 16, 1000); media::AudioParameters::kAudioCDSampleRate, 16,
capabilities_.push_back(std::move(device)); 1000));
device = blink::mojom::AudioInputDeviceCapabilities::New(); media::AudioParameters hw_echo_canceller_parameters(
device->device_id = "hw_echo_canceller_device";
device->parameters = media::AudioParameters(
media::AudioParameters::AUDIO_PCM_LOW_LATENCY, media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
media::CHANNEL_LAYOUT_STEREO, media::CHANNEL_LAYOUT_STEREO,
media::AudioParameters::kAudioCDSampleRate, 24, 1000); media::AudioParameters::kAudioCDSampleRate, 24, 1000);
device->parameters.set_effects(media::AudioParameters::ECHO_CANCELLER); hw_echo_canceller_parameters.set_effects(
capabilities_.push_back(std::move(device)); media::AudioParameters::ECHO_CANCELLER);
capabilities_.emplace_back("hw_echo_canceller_device",
hw_echo_canceller_parameters);
device = blink::mojom::AudioInputDeviceCapabilities::New(); media::AudioParameters geometry_parameters(
device->device_id = "geometry device";
device->parameters = media::AudioParameters(
media::AudioParameters::AUDIO_PCM_LOW_LATENCY, media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
media::CHANNEL_LAYOUT_STEREO, media::CHANNEL_LAYOUT_STEREO,
media::AudioParameters::kAudioCDSampleRate, 16, 1000); media::AudioParameters::kAudioCDSampleRate, 16, 1000);
device->parameters.set_mic_positions(kMicPositions); geometry_parameters.set_mic_positions(kMicPositions);
capabilities_.push_back(std::move(device)); capabilities_.emplace_back("geometry device", geometry_parameters);
default_device_ = capabilities_[0].get(); default_device_ = &capabilities_[0];
hw_echo_canceller_device_ = capabilities_[1].get(); hw_echo_canceller_device_ = &capabilities_[1];
geometry_device_ = capabilities_[2].get(); geometry_device_ = &capabilities_[2];
} else {
// For content capture, use a single capability that admits all possible
// settings.
capabilities_.emplace_back();
}
} }
protected: protected:
...@@ -125,6 +124,7 @@ class MediaStreamConstraintsUtilAudioTest ...@@ -125,6 +124,7 @@ class MediaStreamConstraintsUtilAudioTest
bool disable_local_echo, bool disable_local_echo,
bool render_to_associated_sink) { bool render_to_associated_sink) {
MediaStreamDevice device; MediaStreamDevice device;
device.id = "processed_source";
device.type = GetMediaStreamType(); device.type = GetMediaStreamType();
if (render_to_associated_sink) if (render_to_associated_sink)
device.matched_output_device_id = std::string("some_device_id"); device.matched_output_device_id = std::string("some_device_id");
...@@ -318,17 +318,16 @@ class MediaStreamConstraintsUtilAudioTest ...@@ -318,17 +318,16 @@ class MediaStreamConstraintsUtilAudioTest
} }
} }
void CheckDevice( void CheckDevice(const AudioDeviceCaptureCapability& expected_device,
const blink::mojom::AudioInputDeviceCapabilities& expected_device,
const AudioCaptureSettings& result) { const AudioCaptureSettings& result) {
EXPECT_EQ(expected_device.device_id, result.device_id()); EXPECT_EQ(expected_device.DeviceID(), result.device_id());
EXPECT_EQ(expected_device.parameters.sample_rate(), EXPECT_EQ(expected_device.Parameters().sample_rate(),
result.device_parameters().sample_rate()); result.device_parameters().sample_rate());
EXPECT_EQ(expected_device.parameters.bits_per_sample(), EXPECT_EQ(expected_device.Parameters().bits_per_sample(),
result.device_parameters().bits_per_sample()); result.device_parameters().bits_per_sample());
EXPECT_EQ(expected_device.parameters.channels(), EXPECT_EQ(expected_device.Parameters().channels(),
result.device_parameters().channels()); result.device_parameters().channels());
EXPECT_EQ(expected_device.parameters.effects(), EXPECT_EQ(expected_device.Parameters().effects(),
result.device_parameters().effects()); result.device_parameters().effects());
} }
...@@ -355,10 +354,9 @@ class MediaStreamConstraintsUtilAudioTest ...@@ -355,10 +354,9 @@ class MediaStreamConstraintsUtilAudioTest
MockConstraintFactory constraint_factory_; MockConstraintFactory constraint_factory_;
AudioDeviceCaptureCapabilities capabilities_; AudioDeviceCaptureCapabilities capabilities_;
const blink::mojom::AudioInputDeviceCapabilities* default_device_ = nullptr; const AudioDeviceCaptureCapability* default_device_ = nullptr;
const blink::mojom::AudioInputDeviceCapabilities* hw_echo_canceller_device_ = const AudioDeviceCaptureCapability* hw_echo_canceller_device_ = nullptr;
nullptr; const AudioDeviceCaptureCapability* geometry_device_ = nullptr;
const blink::mojom::AudioInputDeviceCapabilities* geometry_device_ = nullptr;
const std::vector<media::Point> kMicPositions = {{8, 8, 8}, {4, 4, 4}}; const std::vector<media::Point> kMicPositions = {{8, 8, 8}, {4, 4, 4}};
private: private:
...@@ -520,18 +518,18 @@ TEST_P(MediaStreamConstraintsUtilAudioTest, IdealArbitraryDeviceID) { ...@@ -520,18 +518,18 @@ TEST_P(MediaStreamConstraintsUtilAudioTest, IdealArbitraryDeviceID) {
TEST_P(MediaStreamConstraintsUtilAudioTest, ExactValidDeviceID) { TEST_P(MediaStreamConstraintsUtilAudioTest, ExactValidDeviceID) {
for (const auto& device : capabilities_) { for (const auto& device : capabilities_) {
constraint_factory_.basic().device_id.SetExact( constraint_factory_.basic().device_id.SetExact(
blink::WebString::FromASCII(device->device_id)); blink::WebString::FromASCII(device.DeviceID()));
auto result = SelectSettings(); auto result = SelectSettings();
EXPECT_TRUE(result.HasValue()); EXPECT_TRUE(result.HasValue());
CheckDevice(*device, result); CheckDevice(device, result);
CheckBoolDefaults(AudioSettingsBoolMembers(), CheckBoolDefaults(AudioSettingsBoolMembers(),
{&AudioProcessingProperties::enable_sw_echo_cancellation}, {&AudioProcessingProperties::enable_sw_echo_cancellation},
result); result);
bool has_hw_echo_cancellation = bool has_hw_echo_cancellation =
device->parameters.effects() & media::AudioParameters::ECHO_CANCELLER; device.Parameters().effects() & media::AudioParameters::ECHO_CANCELLER;
EXPECT_EQ(!has_hw_echo_cancellation, EXPECT_EQ(IsDeviceCapture() && !has_hw_echo_cancellation,
result.audio_processing_properties().enable_sw_echo_cancellation); result.audio_processing_properties().enable_sw_echo_cancellation);
if (&*device == geometry_device_) { if (&device == geometry_device_) {
EXPECT_EQ(kMicPositions, EXPECT_EQ(kMicPositions,
result.audio_processing_properties().goog_array_geometry); result.audio_processing_properties().goog_array_geometry);
} else { } else {
...@@ -619,7 +617,7 @@ TEST_P(MediaStreamConstraintsUtilAudioTest, EchoCancellationWithHw) { ...@@ -619,7 +617,7 @@ TEST_P(MediaStreamConstraintsUtilAudioTest, EchoCancellationWithHw) {
for (bool value : kBoolValues) { for (bool value : kBoolValues) {
ResetFactory(); ResetFactory();
constraint_factory_.basic().device_id.SetExact( constraint_factory_.basic().device_id.SetExact(
blink::WebString::FromASCII(hw_echo_canceller_device_->device_id)); blink::WebString::FromASCII(hw_echo_canceller_device_->DeviceID()));
((constraint_factory_.*accessor)().echo_cancellation.* ((constraint_factory_.*accessor)().echo_cancellation.*
set_function)(value); set_function)(value);
auto result = SelectSettings(); auto result = SelectSettings();
...@@ -715,7 +713,7 @@ TEST_P(MediaStreamConstraintsUtilAudioTest, GoogEchoCancellationWithHw) { ...@@ -715,7 +713,7 @@ TEST_P(MediaStreamConstraintsUtilAudioTest, GoogEchoCancellationWithHw) {
for (bool value : kBoolValues) { for (bool value : kBoolValues) {
ResetFactory(); ResetFactory();
constraint_factory_.basic().device_id.SetExact( constraint_factory_.basic().device_id.SetExact(
blink::WebString::FromASCII(hw_echo_canceller_device_->device_id)); blink::WebString::FromASCII(hw_echo_canceller_device_->DeviceID()));
((constraint_factory_.*accessor)().goog_echo_cancellation.* ((constraint_factory_.*accessor)().goog_echo_cancellation.*
set_function)(value); set_function)(value);
auto result = SelectSettings(); auto result = SelectSettings();
...@@ -956,7 +954,7 @@ TEST_P(MediaStreamConstraintsUtilAudioTest, DeviceGeometry) { ...@@ -956,7 +954,7 @@ TEST_P(MediaStreamConstraintsUtilAudioTest, DeviceGeometry) {
return; return;
constraint_factory_.basic().device_id.SetExact( constraint_factory_.basic().device_id.SetExact(
blink::WebString::FromASCII(geometry_device_->device_id)); blink::WebString::FromASCII(geometry_device_->DeviceID()));
{ {
const blink::WebString kValidGeometry = const blink::WebString kValidGeometry =
...@@ -1236,6 +1234,52 @@ TEST_P(MediaStreamConstraintsUtilAudioTest, SourceWithAudioProcessing) { ...@@ -1236,6 +1234,52 @@ TEST_P(MediaStreamConstraintsUtilAudioTest, SourceWithAudioProcessing) {
} }
} }
TEST_P(MediaStreamConstraintsUtilAudioTest, UsedAndUnusedSources) {
// The distinction of used and unused sources is relevant only for device
// capture.
if (!IsDeviceCapture())
return;
AudioProcessingProperties properties;
properties.enable_sw_echo_cancellation = true;
std::unique_ptr<ProcessedLocalAudioSource> processed_source =
GetProcessedLocalAudioSource(properties, false /* hotword_enabled */,
false /* disable_local_echo */,
false /* render_to_associated_sink */);
const std::string kUnusedDeviceID = "unused_device";
AudioDeviceCaptureCapabilities capabilities;
capabilities.emplace_back(processed_source.get());
capabilities.emplace_back(kUnusedDeviceID,
media::AudioParameters::UnavailableDeviceParams());
{
constraint_factory_.Reset();
constraint_factory_.basic().echo_cancellation.SetExact(false);
auto result = SelectSettingsAudioCapture(
capabilities, constraint_factory_.CreateWebMediaConstraints(),
false /* should_disable_hardware_noise_suppression */);
EXPECT_TRUE(result.HasValue());
EXPECT_EQ(result.device_id(), kUnusedDeviceID);
EXPECT_FALSE(
result.audio_processing_properties().enable_sw_echo_cancellation);
}
{
constraint_factory_.Reset();
constraint_factory_.basic().echo_cancellation.SetExact(true);
auto result = SelectSettingsAudioCapture(
capabilities, constraint_factory_.CreateWebMediaConstraints(),
false /* should_disable_hardware_noise_suppression */);
EXPECT_TRUE(result.HasValue());
EXPECT_EQ(result.device_id(), processed_source->device().id);
EXPECT_TRUE(
result.audio_processing_properties().enable_sw_echo_cancellation);
}
}
INSTANTIATE_TEST_CASE_P(, INSTANTIATE_TEST_CASE_P(,
MediaStreamConstraintsUtilAudioTest, MediaStreamConstraintsUtilAudioTest,
testing::Values("", testing::Values("",
......
...@@ -372,8 +372,8 @@ void UserMediaProcessor::SetupAudioInput() { ...@@ -372,8 +372,8 @@ void UserMediaProcessor::SetupAudioInput() {
current_request_info_->web_request().AudioConstraints(), &audio_controls); current_request_info_->web_request().AudioConstraints(), &audio_controls);
if (IsDeviceSource(audio_controls.stream_source)) { if (IsDeviceSource(audio_controls.stream_source)) {
GetMediaDevicesDispatcher()->GetAudioInputCapabilities(base::BindOnce( GetMediaDevicesDispatcher()->GetAudioInputCapabilities(base::BindOnce(
&UserMediaProcessor::SelectAudioSettings, weak_factory_.GetWeakPtr(), &UserMediaProcessor::SelectAudioDeviceSettings,
current_request_info_->web_request())); weak_factory_.GetWeakPtr(), current_request_info_->web_request()));
} else { } else {
if (!IsValidAudioContentSource(audio_controls.stream_source)) { if (!IsValidAudioContentSource(audio_controls.stream_source)) {
blink::WebString failed_constraint_name = blink::WebString failed_constraint_name =
...@@ -386,14 +386,41 @@ void UserMediaProcessor::SetupAudioInput() { ...@@ -386,14 +386,41 @@ void UserMediaProcessor::SetupAudioInput() {
return; return;
} }
SelectAudioSettings(current_request_info_->web_request(), SelectAudioSettings(current_request_info_->web_request(),
AudioDeviceCaptureCapabilities()); {AudioDeviceCaptureCapability()});
} }
} }
void UserMediaProcessor::SelectAudioSettings( void UserMediaProcessor::SelectAudioDeviceSettings(
const blink::WebUserMediaRequest& web_request, const blink::WebUserMediaRequest& web_request,
std::vector<blink::mojom::AudioInputDeviceCapabilitiesPtr> std::vector<blink::mojom::AudioInputDeviceCapabilitiesPtr>
audio_input_capabilities) { audio_input_capabilities) {
AudioDeviceCaptureCapabilities capabilities;
for (const auto& device : audio_input_capabilities) {
MediaStreamAudioSource* audio_source = nullptr;
auto it =
std::find_if(local_sources_.begin(), local_sources_.end(),
[&device](const blink::WebMediaStreamSource& web_source) {
DCHECK(!web_source.IsNull());
return web_source.Id().Utf8() == device->device_id;
});
if (it != local_sources_.end()) {
MediaStreamSource* const source =
static_cast<MediaStreamSource*>(it->GetExtraData());
if (source->device().type == MEDIA_DEVICE_AUDIO_CAPTURE)
audio_source = static_cast<MediaStreamAudioSource*>(source);
}
if (audio_source)
capabilities.emplace_back(audio_source);
else
capabilities.emplace_back(device->device_id, device->parameters);
}
SelectAudioSettings(web_request, capabilities);
}
void UserMediaProcessor::SelectAudioSettings(
const blink::WebUserMediaRequest& web_request,
const std::vector<AudioDeviceCaptureCapability>& capabilities) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// The frame might reload or |web_request| might be cancelled while // The frame might reload or |web_request| might be cancelled while
// capabilities are queried. Do nothing if a different request is being // capabilities are queried. Do nothing if a different request is being
...@@ -403,7 +430,7 @@ void UserMediaProcessor::SelectAudioSettings( ...@@ -403,7 +430,7 @@ void UserMediaProcessor::SelectAudioSettings(
DCHECK(current_request_info_->stream_controls()->audio.requested); DCHECK(current_request_info_->stream_controls()->audio.requested);
auto settings = SelectSettingsAudioCapture( auto settings = SelectSettingsAudioCapture(
std::move(audio_input_capabilities), web_request.AudioConstraints(), capabilities, web_request.AudioConstraints(),
web_request.ShouldDisableHardwareNoiseSuppression()); web_request.ShouldDisableHardwareNoiseSuppression());
if (!settings.HasValue()) { if (!settings.HasValue()) {
blink::WebString failed_constraint_name = blink::WebString failed_constraint_name =
......
...@@ -32,6 +32,7 @@ class WebString; ...@@ -32,6 +32,7 @@ class WebString;
namespace content { namespace content {
class AudioCaptureSettings; class AudioCaptureSettings;
class AudioDeviceCaptureCapability;
class MediaStreamAudioSource; class MediaStreamAudioSource;
class MediaStreamDeviceObserver; class MediaStreamDeviceObserver;
class MediaStreamVideoSource; class MediaStreamVideoSource;
...@@ -239,10 +240,13 @@ class CONTENT_EXPORT UserMediaProcessor ...@@ -239,10 +240,13 @@ class CONTENT_EXPORT UserMediaProcessor
GetMediaDevicesDispatcher(); GetMediaDevicesDispatcher();
void SetupAudioInput(); void SetupAudioInput();
void SelectAudioSettings( void SelectAudioDeviceSettings(
const blink::WebUserMediaRequest& web_request, const blink::WebUserMediaRequest& web_request,
std::vector<blink::mojom::AudioInputDeviceCapabilitiesPtr> std::vector<blink::mojom::AudioInputDeviceCapabilitiesPtr>
audio_input_capabilities); audio_input_capabilities);
void SelectAudioSettings(
const blink::WebUserMediaRequest& web_request,
const std::vector<AudioDeviceCaptureCapability>& capabilities);
void SetupVideoInput(); void SetupVideoInput();
void SelectVideoDeviceSettings( void SelectVideoDeviceSettings(
......
...@@ -829,6 +829,34 @@ ...@@ -829,6 +829,34 @@
reportTestSuccess(); reportTestSuccess();
}).catch(failTest); }).catch(failTest);
} }
function getUserMediaEchoCancellationOnAndOff() {
var stream;
navigator.mediaDevices.getUserMedia({
audio: {
deviceId: {exact: "default"},
echoCancellation: {exact: true}
}
}).then(s => {
stream = s;
assertTrue(stream.getAudioTracks()[0].getSettings().echoCancellation);
// Chromium's current implementation does not support tracks from the
// same source to have different echo cancellation settings. This
// getUserMedia() call is expected to fail.
navigator.mediaDevices.getUserMedia({
audio: {
deviceId: {exact: "default"},
echoCancellation: {exact: false}
}
}).then(
s => failTest("Unexpected accepted promise from getUserMedia()"),
e => {
assertEquals(e.name, "OverconstrainedError");
stream.getAudioTracks()[0].stop();
reportTestSuccess();
});
}).catch(failTest);
}
</script> </script>
</head> </head>
<body> <body>
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment