blob: cef8a6c2a9fc1a900a79bd8df5c4e4f91ddec6bf [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/public/browser/audio_service.h"
#include <stddef.h>
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/strings/stringprintf.h"
#include "base/test/gmock_expected_support.h"
#include "base/test/scoped_feature_list.h"
#include "base/threading/thread_restrictions.h"
#include "base/values.h"
#include "build/build_config.h"
#include "content/browser/browser_main_loop.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "media/audio/audio_device_name.h"
#include "media/audio/audio_manager.h"
#include "media/audio/fake_audio_input_stream.h"
#include "media/audio/fake_audio_log_factory.h"
#include "media/audio/fake_audio_manager.h"
#include "media/audio/test_audio_thread.h"
#include "media/base/media_switches.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "services/audio/public/mojom/testing_api.mojom.h"
#include "services/audio/service_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using ::testing::_;
using ::testing::Eq;
using ::testing::NiceMock;
using ::testing::Return;
using ::testing::StrictMock;
namespace {
const media::AudioParameters kInputParamsDefault =
media::AudioParameters(media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
media::ChannelLayoutConfig::Mono(),
48000,
/*change depending on audio processor?*/ 480);
media::AudioParameters AddSystemAec(media::AudioParameters params) {
params.set_effects(media::AudioParameters::ECHO_CANCELLER);
return params;
}
const media::AudioParameters kInputParamsSystemAec =
AddSystemAec(kInputParamsDefault);
enum SystemAecAvailability { kSystemAecNotAvailable, kSystemAecAvailable };
} // namespace
namespace content {
class MockAudioInputStream : public media::AudioInputStream {
public:
explicit MockAudioInputStream(media::AudioManagerBase* audio_manager_base)
: audio_manager_base_(audio_manager_base) {}
MOCK_METHOD0(Open, OpenOutcome());
MOCK_METHOD1(Start, void(AudioInputCallback* callback));
MOCK_METHOD0(Stop, void());
MOCK_METHOD0(GetMaxVolume, double());
MOCK_METHOD1(SetVolume, void(double volume));
MOCK_METHOD0(GetVolume, double());
MOCK_METHOD1(SetAutomaticGainControl, bool(bool enabled));
MOCK_METHOD0(GetAutomaticGainControl, bool());
MOCK_METHOD0(IsMuted, bool());
MOCK_METHOD1(SetOutputDeviceForAec,
void(const std::string& output_device_id));
void Close() override { audio_manager_base_->ReleaseInputStream(this); }
raw_ptr<media::AudioManagerBase> audio_manager_base_;
};
class MockAudioLog : public media::AudioLog {
public:
MockAudioLog() = default;
MOCK_METHOD2(OnCreated,
void(const media::AudioParameters& params,
const std::string& device_id));
MOCK_METHOD0(OnStarted, void());
MOCK_METHOD0(OnStopped, void());
MOCK_METHOD0(OnClosed, void());
MOCK_METHOD0(OnError, void());
MOCK_METHOD1(OnSetVolume, void(double));
MOCK_METHOD1(OnProcessingStateChanged, void(const std::string&));
MOCK_METHOD1(OnLogMessage, void(const std::string& message));
};
class LocalMockAudioManager : public media::FakeAudioManager {
public:
LocalMockAudioManager()
: media::FakeAudioManager(std::make_unique<media::TestAudioThread>(true),
&log_factory_) {}
~LocalMockAudioManager() override = default;
media::AudioParameters GetInputStreamParameters(
const std::string& device_id) override {
return device_id == "default" ? default_device_params : kInputParamsDefault;
}
bool HasAudioInputDevices() override { return true; }
std::unique_ptr<media::AudioLog> CreateAudioLog(
media::AudioLogFactory::AudioComponent component,
int component_id) override {
return std::make_unique<NiceMock<MockAudioLog>>();
}
void GetAudioInputDeviceNames(
media::AudioDeviceNames* device_names) override {
device_names->push_back(media::AudioDeviceName::CreateDefault());
}
media::AudioInputStream* MakeLowLatencyInputStream(
const media::AudioParameters& params,
const std::string& device_id,
const LogCallback& log_callback) override {
OnMakeLowLatencyInputStream(params, device_id, log_callback);
return new NiceMock<MockAudioInputStream>(this);
}
MOCK_METHOD(void,
OnMakeLowLatencyInputStream,
(const media::AudioParameters& params,
const std::string& device_id,
const LogCallback& log_callback));
media::AudioParameters default_device_params = kInputParamsDefault;
private:
media::FakeAudioLogFactory log_factory_;
};
class AudioServiceBrowserTest
: public ContentBrowserTest,
public ::testing::WithParamInterface<SystemAecAvailability> {
public:
AudioServiceBrowserTest() {
// Automatically grant device permission.
base::CommandLine::ForCurrentProcess()->AppendSwitch(
switches::kUseFakeUIForMediaStream);
scoped_feature_list_.InitWithFeatures(
{features::kUserMediaCaptureOnFocus},
// Because we will override the AudioService with one that lives in the
// current process, we disable out-of-process AudioService.
{features::kAudioServiceOutOfProcess});
}
~AudioServiceBrowserTest() override = default;
void SetUp() override {
// Since we are going to override AudioManager, do not use fake streams.
SetUseFakeMediaStreamDevices(false);
ContentBrowserTest::SetUp();
}
enum ExpectedBehaviour {
kOverconstrained,
kUnprocessed,
kProcessed,
kLoopbackAec,
kSystemAec
};
protected:
void TestGetUserMedia(ExpectedBehaviour expected_behaviour,
std::string gum_config);
bool SystemAecAvailable() { return GetParam() == kSystemAecAvailable; }
bool AudioProcessorAecAvailable() {
// AEC never runs in Chrome on Android.
return !BUILDFLAG(IS_ANDROID);
}
bool LoopbackAecAvailable() {
return media::IsSystemLoopbackAsAecReferenceEnabled();
}
base::test::ScopedFeatureList scoped_feature_list_;
};
// Overrides the AudioService with a new one which has a mocked AudioManager
// within its scope.
class AudioServiceOverride {
public:
AudioServiceOverride()
: audio_service_(CreateAudioService()),
audio_service_auto_reset_(
OverrideAudioServiceForTesting(audio_service_remote_.get())) {}
std::unique_ptr<audio::Service> CreateAudioService() {
std::unique_ptr<audio::Service> audio_service;
mojo::PendingReceiver<audio::mojom::AudioService> pending_receiver =
audio_service_remote_.BindNewPipeAndPassReceiver();
base::WaitableEvent created_audio_service_event;
audio_manager_.GetTaskRunner()->PostTask(
FROM_HERE,
base::BindOnce(
[](base::WaitableEvent* created_audio_service_event,
media::AudioManager* audio_manager,
mojo::PendingReceiver<audio::mojom::AudioService>
pending_receiver,
std::unique_ptr<audio::Service>* returned_audio_service) {
(*returned_audio_service) = audio::CreateEmbeddedService(
audio_manager, std::move(pending_receiver));
created_audio_service_event->Signal();
},
&created_audio_service_event, &audio_manager_,
std::move(pending_receiver), &audio_service));
EXPECT_TRUE(created_audio_service_event.TimedWait(base::Seconds(1)));
return audio_service;
}
~AudioServiceOverride() {
base::WaitableEvent destroyed_audio_service_event;
audio_manager_.GetTaskRunner()->PostTask(
FROM_HERE,
base::BindOnce(
[](base::WaitableEvent* destroyed_audio_service_event,
std::unique_ptr<audio::Service> audio_service) {
audio_service.reset();
destroyed_audio_service_event->Signal();
},
&destroyed_audio_service_event, std::move(audio_service_)));
EXPECT_TRUE(destroyed_audio_service_event.TimedWait(base::Seconds(1)));
audio_manager_.Shutdown();
}
StrictMock<LocalMockAudioManager> audio_manager_;
mojo::Remote<audio::mojom::AudioService> audio_service_remote_;
std::unique_ptr<audio::Service> audio_service_;
base::AutoReset<audio::mojom::AudioService*> audio_service_auto_reset_;
};
MATCHER_P(AudioParamsHasSystemAec, system_aec, "") {
return !!(arg.effects() & media::AudioParameters::ECHO_CANCELLER) ==
system_aec;
}
// Tests that calling getUserMedia() with `gum_config` yields behaviour
// specified by `expected_behaviour`.
void AudioServiceBrowserTest::TestGetUserMedia(
ExpectedBehaviour expected_behaviour,
std::string gum_config) {
AudioServiceOverride audio_service_override;
audio_service_override.audio_manager_.default_device_params =
SystemAecAvailable() ? kInputParamsSystemAec : kInputParamsDefault;
switch (expected_behaviour) {
case kOverconstrained:
// The stream will not be created.
break;
case kUnprocessed:
case kProcessed:
// TODO(fhernqvist): Differentiate between processed and unprocessed.
// Alternatives include:
// * Looking for AudioProcessor::Create in the MediaInternals logs
// * Expecting a buffer size specified by APM
EXPECT_CALL(audio_service_override.audio_manager_,
OnMakeLowLatencyInputStream(AudioParamsHasSystemAec(false),
"default", _));
break;
case kSystemAec:
EXPECT_CALL(audio_service_override.audio_manager_,
OnMakeLowLatencyInputStream(AudioParamsHasSystemAec(true),
"default", _));
break;
case kLoopbackAec:
// TODO(http://crbug.com/445323379): HasSystemAec should always be false
EXPECT_CALL(
audio_service_override.audio_manager_,
OnMakeLowLatencyInputStream(
AudioParamsHasSystemAec(SystemAecAvailable()), "default", _));
EXPECT_CALL(audio_service_override.audio_manager_,
OnMakeLowLatencyInputStream(AudioParamsHasSystemAec(false),
"loopbackAllDevices", _));
break;
}
ASSERT_TRUE(embedded_test_server()->Start());
GURL url(embedded_test_server()->GetURL("/simple_page.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
bool success_expected =
expected_behaviour != ExpectedBehaviour::kOverconstrained;
EXPECT_EQ(success_expected,
ExecJs(shell(), base::StringPrintf(
"navigator.mediaDevices.getUserMedia(%s)"
" .then(stream => {"
" const [track] = stream.getAudioTracks();"
" track.stop();"
" return true;"
" })",
gum_config)));
}
IN_PROC_BROWSER_TEST_P(AudioServiceBrowserTest, GetUserMediaEcFalse) {
TestGetUserMedia(kUnprocessed, "{audio: {echoCancellation: {exact: false}}}");
}
IN_PROC_BROWSER_TEST_P(AudioServiceBrowserTest, GetUserMediaEcTrue) {
TestGetUserMedia(SystemAecAvailable() ? kSystemAec : kProcessed,
"{audio: {echoCancellation: {exact: true}}}");
}
IN_PROC_BROWSER_TEST_P(AudioServiceBrowserTest, GetUserMediaEcRemoteOnly) {
TestGetUserMedia(AudioProcessorAecAvailable() ? kProcessed : kOverconstrained,
"{audio: {echoCancellation: {exact: \"remote-only\"}}}");
}
IN_PROC_BROWSER_TEST_P(AudioServiceBrowserTest, GetUserMediaEcAll) {
ExpectedBehaviour expected_behaviour = kOverconstrained;
if (LoopbackAecAvailable()) {
expected_behaviour = kLoopbackAec;
} else if (SystemAecAvailable()) {
expected_behaviour = kSystemAec;
}
TestGetUserMedia(expected_behaviour,
"{audio: {echoCancellation: {exact: \"all\"}}}");
}
INSTANTIATE_TEST_SUITE_P(
All,
AudioServiceBrowserTest,
::testing::Values(kSystemAecAvailable, kSystemAecNotAvailable),
[](const testing::TestParamInfo<SystemAecAvailability>& info) {
switch (info.param) {
case kSystemAecNotAvailable:
return "SystemAecNotAvailable";
case kSystemAecAvailable:
return "SystemAecAvailable";
}
});
} // namespace content