1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
   12
   13
   14
   15
   16
   17
   18
   19
   20
   21
   22
   23
   24
   25
   26
   27
   28
   29
   30
   31
   32
   33
   34
   35
   36
   37
   38
   39
   40
   41
   42
   43
   44
   45
   46
   47
   48
   49
   50
   51
   52
   53
   54
   55
   56
   57
   58
   59
   60
   61
   62
   63
   64
   65
   66
   67
   68
   69
   70
   71
   72
   73
   74
   75
   76
   77
   78
   79
   80
   81
   82
   83
   84
   85
   86
   87
   88
   89
   90
   91
   92
   93
   94
   95
   96
   97
   98
   99
  100
  101
  102
  103
  104
  105
  106
  107
  108
  109
  110
  111
  112
  113
  114
  115
  116
  117
  118
  119
  120
  121
  122
  123
  124
  125
  126
  127
  128
  129
  130
  131
  132
  133
  134
  135
  136
  137
  138
  139
  140
  141
  142
  143
  144
  145
  146
  147
  148
  149
  150
  151
  152
  153
  154
  155
  156
  157
  158
  159
  160
  161
  162
  163
  164
  165
  166
  167
  168
  169
  170
  171
  172
  173
  174
  175
  176
  177
  178
  179
  180
  181
  182
  183
  184
  185
  186

media / audio / pulse / pulse_loopback_manager.cc [blame]

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "media/audio/pulse/pulse_loopback_manager.h"

#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "media/audio/pulse/audio_manager_pulse.h"
#include "media/audio/pulse/pulse_input.h"
#include "media/audio/pulse/pulse_util.h"

namespace media {

// Special PulseAudio identifier.
constexpr char kDefaultSinkName[] = "@DEFAULT_SINK@";

// static
std::unique_ptr<PulseLoopbackManager> PulseLoopbackManager::Create(
    ReleaseStreamCallback release_stream_callback,
    pa_context* context,
    pa_threaded_mainloop* mainloop) {
  // We want to open the monitor device of the default sink, which is
  // specified using a special identifier.
  const std::string default_monitor_name =
      pulse::GetMonitorSourceNameForSink(mainloop, context, kDefaultSinkName);
  if (default_monitor_name.empty()) {
    LOG(ERROR) << "Failed to retrieve default monitor name.";
    return nullptr;
  }

  auto manager = base::WrapUnique(new PulseLoopbackManager(
      default_monitor_name, release_stream_callback, context, mainloop));

  // Register a callback to be notified about default sink changes. When such
  // an event occurs, we want to switch the source to the monitor of the new
  // default sink.
  pulse::AutoPulseLock auto_lock(manager->mainloop_);
  pa_context_set_subscribe_callback(manager->context_, &EventCallback,
                                    manager.get());

  // TODO(crbug.com/40281249): Check if subscription was reported as successful
  // in pulse::ContextSuccessCallback.
  pa_operation* operation =
      pa_context_subscribe(manager->context_, PA_SUBSCRIPTION_MASK_SERVER,
                           &pulse::ContextSuccessCallback, manager->mainloop_);
  if (!pulse::WaitForOperationCompletion(manager->mainloop_, operation)) {
    LOG(ERROR) << "Failed to subscribe to PulseAudio events.";
    return nullptr;
  }

  return manager;
}

PulseLoopbackManager::~PulseLoopbackManager() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  weak_ptr_factory_.InvalidateWeakPtrs();
  pulse::AutoPulseLock auto_lock(mainloop_);
  pa_operation* operation =
      pa_context_subscribe(context_, PA_SUBSCRIPTION_MASK_NULL,
                           &pulse::ContextSuccessCallback, mainloop_);
  if (!pulse::WaitForOperationCompletion(mainloop_, operation)) {
    LOG(ERROR) << "Failed to unsubscribe from PulseAudio events.";
  }
  pa_context_set_subscribe_callback(context_, nullptr, nullptr);
}

PulseLoopbackAudioStream* PulseLoopbackManager::MakeLoopbackStream(
    const AudioParameters& params,
    AudioManager::LogCallback log_callback,
    bool should_mute_system_audio) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  // Check if we're trying to create a loopback-with-muting when other loopbacks
  // are in progress
  // TODO(crbug.com/359102845): Support simultaneous loopback-with-muting
  // streams
  if (should_mute_system_audio && !streams_.empty()) {
    LOG(ERROR) << "Cannot create loopback-with-muting: other loopbacks are in "
                  "progress.";
    return nullptr;
  }

  // Check if we're trying to create a loopback-without-muting when a
  // loopback-with-muting exists
  if (!should_mute_system_audio && has_muting_loopback_) {
    LOG(ERROR) << "Cannot create loopback-without-muting: a "
                  "loopback-with-muting is in progress.";
    return nullptr;
  }

  auto* stream = new PulseLoopbackAudioStream(
      release_stream_callback_, GetLoopbackSourceName(), params, mainloop_,
      context_, std::move(log_callback), should_mute_system_audio);

  streams_.emplace_back(stream);

  if (should_mute_system_audio) {
    has_muting_loopback_ = true;
    pulse::MuteAllSinksExcept(mainloop_, context_, GetLoopbackSourceName());
  }

  return stream;
}

void PulseLoopbackManager::RemoveStream(AudioInputStream* stream) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  std::erase(streams_, reinterpret_cast<PulseLoopbackAudioStream*>(stream));
}

const std::string& PulseLoopbackManager::GetLoopbackSourceName() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  return default_monitor_name_;
}

// static
void PulseLoopbackManager::EventCallback(pa_context* context,
                                         pa_subscription_event_type_t type,
                                         uint32_t index,
                                         void* user_data) {
  // It is safe to access `manager`, as the registration of the callback is
  // managed by the manager in the constructor and destructor.
  auto* manager = reinterpret_cast<PulseLoopbackManager*>(user_data);
  if ((type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) ==
          PA_SUBSCRIPTION_EVENT_SERVER &&
      (type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) ==
          PA_SUBSCRIPTION_EVENT_CHANGE) {
    // This event can occur when the default sink or source is changed.
    // Post to the thread that created the PulseAudioInputStreams.
    manager->task_runner_->PostTask(
        FROM_HERE, base::BindOnce(&PulseLoopbackManager::OnServerChangeEvent,
                                  manager->weak_this_));
  }

  pa_threaded_mainloop_signal(manager->mainloop_, 0);
}

PulseLoopbackManager::PulseLoopbackManager(
    const std::string& default_monitor_name,
    ReleaseStreamCallback release_stream_callback,
    pa_context* context,
    pa_threaded_mainloop* mainloop)
    : default_monitor_name_(default_monitor_name),
      release_stream_callback_(release_stream_callback),
      context_(context),
      mainloop_(mainloop),
      task_runner_(base::SingleThreadTaskRunner::GetCurrentDefault()) {
  weak_this_ = weak_ptr_factory_.GetWeakPtr();
}

void PulseLoopbackManager::OnServerChangeEvent() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  const std::string default_monitor_name =
      pulse::GetMonitorSourceNameForSink(mainloop_, context_, kDefaultSinkName);
  if (default_monitor_name.empty()) {
    LOG(ERROR) << "Default sink does not have a monitor.";
    return;
  }

  // Verify that the event relates to a default sink change.
  if (default_monitor_name == default_monitor_name_) {
    return;
  }
  if (has_muting_loopback_) {
    pulse::UnmuteAllSinks(mainloop_, context_);
  }

  // There is no data race here, as OnServerChangeEvent() and
  // GetLoopbackSourceName() must run on the same `task_runner_`.
  default_monitor_name_ = default_monitor_name;

  for (PulseLoopbackAudioStream* stream : streams_) {
    stream->ChangeStreamSource(default_monitor_name_);
    // TODO(crbug.com/40281249): Support
    // AudioDeviceDescription::kLoopbackWithMuteDeviceId. Store the original
    // device name, i.e., AudioDeviceDescription::kLoopback*, and check it here
    // to determine if muting was requested for any of the streams, and mute the
    // default sink accordingly.
  }
  // Reapply muting with the new default monitor name
  if (has_muting_loopback_) {
    pulse::MuteAllSinksExcept(mainloop_, context_, default_monitor_name);
  }
}

}  // namespace media