| // 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 "services/audio/loopback_reference_manager.h" |
| |
| #include <memory> |
| |
| #include "base/containers/flat_set.h" |
| #include "base/functional/callback.h" |
| #include "base/logging.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/synchronization/lock.h" |
| #include "base/task/bind_post_task.h" |
| #include "base/time/time.h" |
| #include "base/trace_event/trace_event.h" |
| #include "media/audio/audio_device_description.h" |
| #include "media/audio/audio_io.h" |
| #include "media/audio/system_glitch_reporter.h" |
| #include "media/base/audio_bus.h" |
| #include "services/audio/audio_manager_power_user.h" |
| #include "services/audio/reference_signal_provider.h" |
| |
| // The design of LoopbackReferenceManager is divided into three classes: |
| // |
| // * LoopbackReferenceManager is a singleton in the AudioService, and is |
| // responsible for creating LoopbackReferenceProviders for the caller. It owns |
| // up to one lazily created LoopbackReferenceManagerCore, which the created |
| // LoopbackReferenceProviders get a weak reference to. |
| // |
| // * LoopbackReferenceManagerCore contains the logic for starting and stopping |
| // the loopback stream, as well as delivering data to the listeners. If the |
| // loopback stream experiences an error while it's running, the core will be |
| // destroyed. |
| // |
| // * LoopbackReferenceProvider implements ReferenceSignalProvider. Each |
| // LoopbackReferenceProvider contains a weak pointer to a |
| // LoopbackReferenceManagerCore, which it forwards StartListening() and |
| // StopListening() to. If the core has been destroyed due to an error, these |
| // calls are safe no-ops. |
| |
| namespace audio { |
| namespace { |
| using ReferenceOpenOutcome = ReferenceSignalProvider::ReferenceOpenOutcome; |
| using OpenOutcome = media::AudioInputStream::OpenOutcome; |
| |
| ReferenceOpenOutcome MapStreamOpenOutcomeToReferenceOpenOutcome( |
| OpenOutcome outcome) { |
| switch (outcome) { |
| case OpenOutcome::kSuccess: |
| return ReferenceOpenOutcome::SUCCESS; |
| case OpenOutcome::kFailedSystemPermissions: |
| return ReferenceOpenOutcome::STREAM_OPEN_SYSTEM_PERMISSIONS_ERROR; |
| case OpenOutcome::kFailedInUse: |
| return ReferenceOpenOutcome::STREAM_OPEN_DEVICE_IN_USE_ERROR; |
| default: |
| return ReferenceOpenOutcome::STREAM_OPEN_ERROR; |
| } |
| } |
| |
| ReferenceOpenOutcome ReportOpenResult(ReferenceOpenOutcome outcome) { |
| base::UmaHistogramEnumeration("Media.Audio.LoopbackReference.OpenResult", |
| outcome); |
| return outcome; |
| } |
| |
| } // namespace |
| |
| class LoopbackReferenceStreamIdProvider { |
| public: |
| LoopbackReferenceStreamIdProvider() = default; |
| ~LoopbackReferenceStreamIdProvider() = default; |
| |
| LoopbackReferenceStreamIdProvider(const LoopbackReferenceStreamIdProvider&) = |
| delete; |
| LoopbackReferenceStreamIdProvider& operator=( |
| const LoopbackReferenceStreamIdProvider&) = delete; |
| |
| int GetId() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_); |
| return next_stream_id_; |
| } |
| |
| int GetNextId() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_); |
| return next_stream_id_++; |
| } |
| |
| private: |
| SEQUENCE_CHECKER(owning_sequence_); |
| // To differentiate the streams that LoopbackReferenceManagerCore creates from |
| // the InputControllers, we start their ids at 1000000. |
| // TODO(crbug.com/412581642): Remove this hack once the reference streams get |
| // their own category. |
| int next_stream_id_ = 1000000; |
| }; |
| |
| // Tracks ReferenceOutput::Listeners. When there are at least one listener, it |
| // creates a system loopback stream and forwards the audio to the listeners. |
| class LoopbackReferenceManagerCore |
| : public media::AudioInputStream::AudioInputCallback { |
| public: |
| using ErrorCallback = base::OnceCallback<void()>; |
| |
| explicit LoopbackReferenceManagerCore( |
| media::AudioManager* audio_manager, |
| LoopbackReferenceStreamIdProvider* stream_id_provider, |
| ErrorCallback on_error_callback) |
| : id_(base::UnguessableToken::Create()), |
| audio_manager_(audio_manager), |
| glitch_reporter_( |
| media::SystemGlitchReporter::StreamType::kLoopbackReference), |
| task_runner_(base::SequencedTaskRunner::GetCurrentDefault()), |
| stream_id_provider_(stream_id_provider), |
| on_error_callback_(std::move(on_error_callback)) {} |
| |
| LoopbackReferenceManagerCore(const LoopbackReferenceManagerCore&) = delete; |
| LoopbackReferenceManagerCore& operator=(const LoopbackReferenceManagerCore&) = |
| delete; |
| |
| ~LoopbackReferenceManagerCore() override { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| EnsureLoopbackStreamClosed(); |
| // If the error callback has been used, there has been an error. |
| bool had_runtime_error = !on_error_callback_; |
| base::UmaHistogramBoolean("Media.Audio.LoopbackReference.HadRuntimeError", |
| had_runtime_error); |
| if (had_runtime_error) { |
| for (ReferenceOutput::Listener* listener : listeners_) { |
| listener->OnReferenceStreamError(); |
| } |
| } |
| } |
| |
| void SendLogMessage(const std::string& message) { |
| if (!audio_log_) { |
| return; |
| } |
| |
| audio_log_->OnLogMessage(base::StringPrintf( |
| "LRMC::%s [stream_id=%u] [id=%s]", message.c_str(), |
| stream_id_provider_->GetId(), id_.ToString().c_str())); |
| } |
| |
| void ReportAndResetGlitchStats() { |
| media::SystemGlitchReporter::Stats stats = |
| glitch_reporter_.GetLongTermStatsAndReset(); |
| std::string formatted_message = base::StringPrintf( |
| "%s => (num_glitches_detected=[%d], cumulative_audio_lost=[%llu ms], " |
| "largest_glitch=[%llu ms])", |
| __func__, stats.glitches_detected, |
| stats.total_glitch_duration.InMilliseconds(), |
| stats.largest_glitch_duration.InMilliseconds()); |
| SendLogMessage(formatted_message); |
| } |
| |
| ReferenceOpenOutcome StartListening(ReferenceOutput::Listener* listener, |
| const std::string& device_id) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| if (loopback_stream_) { |
| // The core was already successfully started. |
| base::AutoLock scoped_lock(lock_); |
| listeners_.insert(listener); |
| return ReferenceOpenOutcome::SUCCESS; |
| } |
| |
| // Capture audio from all audio devices, or equivalently, audio from all |
| // PIDs playing out audio. |
| const std::string loopback_device_id = |
| media::AudioDeviceDescription::kLoopbackAllDevicesId; |
| // TODO(crbug.com/412581642): Determine optimal parameters. |
| const media::AudioParameters params = |
| AudioManagerPowerUser(audio_manager_) |
| .GetInputStreamParameters(loopback_device_id); |
| // Does not require the lock because the audio stream is not started. |
| sample_rate_ = params.sample_rate(); |
| |
| // Create an AudioLog for the new stream. |
| // TODO(crbug.com/412581642): Add a different AudioComponent for the |
| // reference loopback streams and show them in chrome://media-internals |
| audio_log_ = audio_manager_->CreateAudioLog( |
| media::AudioLogFactory::AudioComponent::kAudioInputController, |
| stream_id_provider_->GetNextId()); |
| SendLogMessage( |
| base::StrCat({"StartListening(device_id=", loopback_device_id, ")"})); |
| |
| // Create the stream, and return an error if we fail. |
| loopback_stream_ = audio_manager_->MakeAudioInputStream( |
| params, loopback_device_id, |
| base::BindRepeating(&media::AudioLog::OnLogMessage, |
| base::Unretained(audio_log_.get()))); |
| if (!loopback_stream_) { |
| return ReferenceOpenOutcome::STREAM_CREATE_ERROR; |
| } |
| |
| // Open the stream, and return an error if we fail. |
| media::AudioInputStream::OpenOutcome stream_open_outcome = |
| loopback_stream_->Open(); |
| if (stream_open_outcome != OpenOutcome::kSuccess) { |
| loopback_stream_.ExtractAsDangling()->Close(); |
| return MapStreamOpenOutcomeToReferenceOpenOutcome(stream_open_outcome); |
| } |
| audio_log_->OnCreated(params, loopback_device_id); |
| |
| // Add the listener and start the stream. |
| { |
| base::AutoLock scoped_lock(lock_); |
| listeners_.insert(listener); |
| } |
| loopback_stream_->Start(this); |
| audio_log_->OnStarted(); |
| |
| return ReferenceOpenOutcome::SUCCESS; |
| } |
| |
| void StopListening(ReferenceOutput::Listener* listener) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| SendLogMessage("StopListening()"); |
| bool is_empty; |
| { |
| base::AutoLock scoped_lock(lock_); |
| listeners_.erase(listener); |
| is_empty = listeners_.empty(); |
| } |
| if (is_empty) { |
| // TODO(crbug.com/412581642): Close after a delay instead, in case we are |
| // calling StartListening() again soon. |
| EnsureLoopbackStreamClosed(); |
| } |
| } |
| |
| base::WeakPtr<LoopbackReferenceManagerCore> GetWeakPtr() { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| return weak_ptr_factory_.GetWeakPtr(); |
| } |
| |
| // AudioInputCallback implementation |
| void OnData(const media::AudioBus* source, |
| base::TimeTicks capture_time, |
| double volume, |
| const media::AudioGlitchInfo& audio_glitch_info) override { |
| base::AutoLock scoped_lock(lock_); |
| for (ReferenceOutput::Listener* listener : listeners_) { |
| // Since we are using a loopback signal, the audio has likely already been |
| // played out at this point, so the delay would be negative. To avoid |
| // confusion in the AudioProcessor, we set it to 0 for now. |
| // TODO(crbug.com/412581642): Figure out the correct value for this delay. |
| listener->OnPlayoutData(*source, sample_rate_, |
| /*audio_delay=*/base::TimeDelta()); |
| } |
| glitch_reporter_.UpdateStats(audio_glitch_info.duration); |
| } |
| |
| void OnError() override { |
| // We post a new task even when we run on the same sequence, to avoid odd |
| // call stacks where the input stream is closed while it's being started. |
| task_runner_->PostTask( |
| FROM_HERE, |
| base::BindOnce(&LoopbackReferenceManagerCore::OnErrorMainSequence, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void OnErrorMainSequence() { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| SendLogMessage("OnError()"); |
| if (on_error_callback_) { |
| std::move(on_error_callback_).Run(); |
| } |
| } |
| |
| private: |
| void EnsureLoopbackStreamClosed() { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| if (!loopback_stream_) { |
| return; |
| } |
| loopback_stream_->Stop(); |
| ReportAndResetGlitchStats(); |
| audio_log_->OnStopped(); |
| // The the stream will destroy itself upon Close(), so we use |
| // ExtractAsDangling() to clear the raw_ptr first. |
| loopback_stream_.ExtractAsDangling()->Close(); |
| audio_log_->OnClosed(); |
| audio_log_.reset(); |
| } |
| |
| const base::UnguessableToken id_; |
| const raw_ptr<media::AudioManager> audio_manager_; |
| media::SystemGlitchReporter glitch_reporter_; |
| const scoped_refptr<base::SequencedTaskRunner> task_runner_; |
| const raw_ptr<LoopbackReferenceStreamIdProvider> stream_id_provider_; |
| ErrorCallback on_error_callback_; |
| raw_ptr<media::AudioInputStream> loopback_stream_ = nullptr; |
| std::unique_ptr<media::AudioLog> audio_log_; |
| int sample_rate_; |
| |
| base::Lock lock_; |
| base::flat_set<ReferenceOutput::Listener*> listeners_ GUARDED_BY(lock_); |
| |
| base::WeakPtrFactory<LoopbackReferenceManagerCore> weak_ptr_factory_{this}; |
| }; |
| |
| // Contains a weak pointer to a LoopbackReferenceManagerCore, and forwards its |
| // calls to it. If the core has been destroyed due to an error, all its |
| // operations become safe no-ops. (not implemented yet). |
| class LoopbackReferenceProvider : public ReferenceSignalProvider { |
| public: |
| LoopbackReferenceProvider(base::WeakPtr<LoopbackReferenceManagerCore> core) |
| : core_(core) {} |
| |
| ReferenceSignalProvider::Type GetType() const final { |
| return ReferenceSignalProvider::Type::kLoopbackReference; |
| } |
| |
| ReferenceOpenOutcome StartListening(ReferenceOutput::Listener* listener, |
| const std::string& device_id) final { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_); |
| if (core_) { |
| return ReportOpenResult(core_->StartListening(listener, device_id)); |
| } |
| // If the core no longer exists, it must have been destroyed due to an |
| // error. |
| return ReportOpenResult(ReferenceOpenOutcome::STREAM_PREVIOUS_ERROR); |
| } |
| |
| void StopListening(ReferenceOutput::Listener* listener) final { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_); |
| if (core_) { |
| core_->StopListening(listener); |
| } |
| } |
| |
| private: |
| SEQUENCE_CHECKER(owning_sequence_); |
| base::WeakPtr<LoopbackReferenceManagerCore> core_; |
| }; |
| |
| LoopbackReferenceManager::LoopbackReferenceManager( |
| media::AudioManager* audio_manager) |
| : audio_manager_(audio_manager), |
| stream_id_provider_( |
| std::make_unique<LoopbackReferenceStreamIdProvider>()) {} |
| |
| std::unique_ptr<ReferenceSignalProvider> |
| LoopbackReferenceManager::GetReferenceSignalProvider() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_); |
| if (!core_) { |
| core_ = std::make_unique<LoopbackReferenceManagerCore>( |
| audio_manager_, stream_id_provider_.get(), |
| base::BindOnce(&LoopbackReferenceManager::OnCoreError, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| return std::make_unique<LoopbackReferenceProvider>(core_->GetWeakPtr()); |
| } |
| |
| void LoopbackReferenceManager::OnCoreError() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_); |
| CHECK(core_); |
| core_.reset(); |
| } |
| |
| LoopbackReferenceManager::~LoopbackReferenceManager() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_); |
| } |
| |
| } // namespace audio |