910e62b5创建于 1月15日历史提交
// 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 "media/audio/mac/catap_audio_input_stream.h"

#include <CoreAudio/AudioHardware.h>
#include <CoreAudio/AudioHardwareTapping.h>
#include <CoreAudio/CATapDescription.h>
#include <CoreAudio/CoreAudio.h>
#import <Foundation/Foundation.h>
#include <MacTypes.h>
#include <unistd.h>

#include <string_view>

#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#include "base/timer/elapsed_timer.h"
#include "base/trace_event/trace_event.h"
#include "media/audio/application_loopback_device_helper.h"
#include "media/audio/audio_features.h"
#include "media/audio/mac/audio_loopback_input_mac.h"
#include "media/audio/mac/catap_api.h"
#include "media/audio/mac/core_audio_util_mac.h"
#include "media/base/audio_bus.h"
#include "media/base/audio_sample_types.h"
#include "media/base/audio_timestamp_helper.h"

namespace media {
namespace {
const char kCatapAudioInputStreamUmaBaseName[] =
    "Media.Audio.Mac.CatapAudioInputStream";

const AudioObjectPropertyAddress kDeviceIsAliveAddress = {
    kAudioDevicePropertyDeviceIsAlive, kAudioObjectPropertyScopeGlobal,
    kAudioObjectPropertyElementMain};

const AudioObjectPropertyAddress kDefaultOutputDevicePropertyAddress = {
    kAudioHardwarePropertyDefaultOutputDevice, kAudioObjectPropertyScopeGlobal,
    kAudioObjectPropertyElementMain};

const AudioObjectPropertyAddress kVirtualFormatAddress = {
    kAudioStreamPropertyVirtualFormat, kAudioObjectPropertyScopeGlobal,
    kAudioObjectPropertyElementMain};

const AudioObjectPropertyAddress kSampleRateAddress = {
    kAudioDevicePropertyNominalSampleRate, kAudioObjectPropertyScopeGlobal,
    kAudioObjectPropertyElementMain};

const char kHistogramPartsSeparator[] = ".";
const char kHistogramStatusPrefix[] = "Status";
const char kHistogramOperationDurationPrefix[] = "OperationDuration";
const char kHistogramOpenSuffix[] = "Open";
const char kHistogramStartSuffix[] = "Start";
const char kHistogramStopSuffix[] = "Stop";
const char kHistogramCloseSuffix[] = "Close";
const char kHistogramGetProcessAudioDeviceIdsSuffix[] =
    "GetProcessAudioDeviceIds";
const char kHistogramSuccessSuffix[] = "Success";
const char kHistogramFailureSuffix[] = "Failure";
const char kHostTimeStatusName[] = "HostTimeStatus";
const char kHistogramDeviceIsAliveName[] = "IsAlive";
const char kHistogramChannelCountMismatchName[] = "ChannelCountMismatch";
const char kHistogramFramesMismatchName[] = "FramesMismatch";

// If this feature is enabled, the CoreAudio tap is probed after creation to
// verify that we have the proper permissions. If this fails the creation is
// reported as failed.
BASE_FEATURE(kMacCatapProbeTapOnCreation, base::FEATURE_ENABLED_BY_DEFAULT);

// When `kMacCatapCaptureAllDevices` is disabled:
//
// CatapAudioInputStreamSource captures audio from the default output device.
// However, if the device ID is explicitly set to `kLoopbackAllDevicesId`, it
// will capture all system audio regardless of the specific output device used
// for playback.
//
// When `kMacCatapCaptureAllDevices` is enabled:
//
// CatapAudioInputStreamSource captures all system audio, irrespective of the
// specific output device it's played on or the device ID set.
BASE_FEATURE(kMacCatapCaptureAllDevices, base::FEATURE_DISABLED_BY_DEFAULT);

// If this feature is enabled, mono capture is forced for mono devices. This
// will be upmixed to stereo in CatapAudioInputStreamSource if the output is
// configured to be sterero.
BASE_FEATURE(kMacCatapForceMonoCaptureOfMonoDevices,
             base::FEATURE_ENABLED_BY_DEFAULT);

API_AVAILABLE(macos(14.2))
OSStatus DeviceIoProc(AudioDeviceID,
                      const AudioTimeStamp*,
                      const AudioBufferList* input_data,
                      const AudioTimeStamp* input_time,
                      AudioBufferList* output_data,
                      const AudioTimeStamp* output_time,
                      void* client_data) {
  CatapAudioInputStreamSource* catap_input_stream =
      reinterpret_cast<CatapAudioInputStreamSource*>(client_data);
  CHECK(catap_input_stream != nullptr);

  // Multiple buffers correspond to multiple streams. This is not expected
  // during system audio capture, and the OnCatapSample() function is designed
  // to only process the first buffer. A DCHECK is used here to notify us in
  // debug builds if the OS provides more than one buffer. This would indicate
  // an unexpected change in behavior that requires investigation.
  DCHECK_EQ(input_data->mNumberBuffers, 1u);

  if (input_data->mNumberBuffers > 0 && input_data->mBuffers->mData != NULL) {
    catap_input_stream->OnCatapSample(input_data->mBuffers, input_time);
  }
  return noErr;
}

// Helper functions to generate histogram names.
std::string GetHistogramName(std::string_view status_prefix,
                             std::string_view operation_suffix,
                             std::string_view extra_suffix) {
  return base::JoinString({kCatapAudioInputStreamUmaBaseName, status_prefix,
                           operation_suffix, extra_suffix},
                          kHistogramPartsSeparator);
}

std::string GetHistogramName(std::string_view status_prefix,
                             std::string_view operation_suffix) {
  return base::JoinString(
      {kCatapAudioInputStreamUmaBaseName, status_prefix, operation_suffix},
      kHistogramPartsSeparator);
}

API_AVAILABLE(macos(14.2))
void ReportOpenStatus(CatapAudioInputStreamSource::OpenStatus status,
                      base::TimeDelta duration) {
  base::UmaHistogramEnumeration(
      GetHistogramName(kHistogramStatusPrefix, kHistogramOpenSuffix), status);
  base::UmaHistogramTimes(
      GetHistogramName(kHistogramOperationDurationPrefix, kHistogramOpenSuffix,
                       status == CatapAudioInputStreamSource::OpenStatus::kOk
                           ? kHistogramSuccessSuffix
                           : kHistogramFailureSuffix),
      duration);
}

void ReportStartStatus(bool success, base::TimeDelta duration) {
  base::UmaHistogramBoolean(
      GetHistogramName(kHistogramStatusPrefix, kHistogramStartSuffix), success);
  base::UmaHistogramTimes(
      GetHistogramName(
          kHistogramOperationDurationPrefix, kHistogramStartSuffix,
          success ? kHistogramSuccessSuffix : kHistogramFailureSuffix),
      duration);
}

void ReportStopStatus(bool success, base::TimeDelta duration) {
  base::UmaHistogramBoolean(
      GetHistogramName(kHistogramStatusPrefix, kHistogramStopSuffix), success);
  base::UmaHistogramTimes(
      GetHistogramName(
          kHistogramOperationDurationPrefix, kHistogramStopSuffix,
          success ? kHistogramSuccessSuffix : kHistogramFailureSuffix),
      duration);
}

API_AVAILABLE(macos(14.2))
void ReportCloseStatus(CatapAudioInputStreamSource::CloseStatus status,
                       base::TimeDelta duration) {
  base::UmaHistogramEnumeration(
      GetHistogramName(kHistogramStatusPrefix, kHistogramCloseSuffix), status);
  base::UmaHistogramTimes(
      GetHistogramName(kHistogramOperationDurationPrefix, kHistogramCloseSuffix,
                       status == CatapAudioInputStreamSource::CloseStatus::kOk
                           ? kHistogramSuccessSuffix
                           : kHistogramFailureSuffix),
      duration);
}

void ReportGetProcessAudioDeviceIdsDuration(bool success,
                                            base::TimeDelta duration) {
  base::UmaHistogramTimes(
      GetHistogramName(
          kHistogramOperationDurationPrefix,
          kHistogramGetProcessAudioDeviceIdsSuffix,
          success ? kHistogramSuccessSuffix : kHistogramFailureSuffix),
      duration);
}

// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class HostTimeStatus {
  kNoMissingHostTime = 0,
  kSometimesMissingHostTimeNoRecover = 1,
  kSometimesMissingHostTimeRecovered = 2,
  kAlwaysMissingHostTime = 3,
  kMaxValue = kAlwaysMissingHostTime
};

HostTimeStatus GetHostTimeStatus(int total_callbacks,
                                 int callbacks_with_missing_host_time,
                                 bool has_recovered) {
  if (callbacks_with_missing_host_time == 0) {
    return HostTimeStatus::kNoMissingHostTime;
  }
  if (callbacks_with_missing_host_time == total_callbacks) {
    return HostTimeStatus::kAlwaysMissingHostTime;
  }

  return has_recovered ? HostTimeStatus::kSometimesMissingHostTimeRecovered
                       : HostTimeStatus::kSometimesMissingHostTimeNoRecover;
}

void ReportHostTimeStatus(int total_callbacks,
                          int callbacks_with_missing_host_time,
                          bool has_recovered) {
  base::UmaHistogramEnumeration(
      base::JoinString({kCatapAudioInputStreamUmaBaseName, kHostTimeStatusName},
                       kHistogramPartsSeparator),
      GetHostTimeStatus(total_callbacks, callbacks_with_missing_host_time,
                        has_recovered));
}

void ReportMismatchStatus(int total_callbacks_with_channel_count_mismatch,
                          int total_callbacks_with_frames_mismatch) {
  base::UmaHistogramCounts1000(
      base::JoinString({kCatapAudioInputStreamUmaBaseName,
                        kHistogramChannelCountMismatchName},
                       kHistogramPartsSeparator),
      total_callbacks_with_channel_count_mismatch);
  base::UmaHistogramCounts1000(
      base::JoinString(
          {kCatapAudioInputStreamUmaBaseName, kHistogramFramesMismatchName},
          kHistogramPartsSeparator),
      total_callbacks_with_frames_mismatch);
}

bool IsLoopbackDevice(const std::string& device_id) {
  return device_id == AudioDeviceDescription::kLoopbackInputDeviceId ||
         device_id == AudioDeviceDescription::kLoopbackWithMuteDeviceId ||
         device_id == AudioDeviceDescription::kLoopbackWithMuteDeviceIdCast ||
         device_id == AudioDeviceDescription::kLoopbackWithoutChromeId ||
         device_id == AudioDeviceDescription::kLoopbackAllDevicesId ||
         AudioDeviceDescription::IsApplicationLoopbackDevice(device_id);
}

// True if the capturer should be configured to only capture the default
// device.
bool IsDefaultOutputDeviceLoopback(const std::string& device_id) {
  return device_id != AudioDeviceDescription::kLoopbackAllDevicesId &&
         !AudioDeviceDescription::IsApplicationLoopbackDevice(device_id) &&
         !base::FeatureList::IsEnabled(kMacCatapCaptureAllDevices);
}

bool ExcludeChromeLoopback(const std::string& device_id) {
  return device_id == AudioDeviceDescription::kLoopbackWithoutChromeId;
}

bool MuteLocalPlaybackLoopback(const std::string& device_id) {
  return device_id == AudioDeviceDescription::kLoopbackWithMuteDeviceId ||
         device_id == AudioDeviceDescription::kLoopbackWithMuteDeviceIdCast;
}

// Returns AudioDeviceID and Unique ID (UID) for default output device, or
// `nullopt` if there were any errors.
API_AVAILABLE(macos(14.2))
CatapAudioInputStream::AudioDeviceIds GetDefaultOutputDeviceIds() {
  CatapAudioInputStream::AudioDeviceIds device_ids;
  device_ids.id = core_audio_mac::GetDefaultDevice(/*input=*/false);
  if (device_ids.id) {
    device_ids.uid = core_audio_mac::GetDeviceUniqueID(*device_ids.id);
  }
  return device_ids;
}

bool operator==(const AudioObjectPropertyAddress& x,
                const AudioObjectPropertyAddress& y) {
  return x.mSelector == y.mSelector && x.mScope == y.mScope &&
         x.mElement == y.mElement;
}

}  // namespace

// Helper class to manage CoreAudio property listeners.
//
// This class abstracts the process of adding and removing property listeners
// for CoreAudio objects. It listens for changes to the
// kAudioDevicePropertyDeviceIsAlive property of the aggregate device and,
// optionally, the kAudioHardwarePropertyDefaultOutputDevice property of the
// system object.
//
// The property listener block uses `dispatch_get_main_queue()` to ensure that
// property change notifications are delivered on the main thread. Using a weak
// pointer for the callback acts as a final safeguard to prevent a crash if a
// notification fires during the object's destruction.
class PropertyListenerHelper {
 public:
  using ProcessPropertyChangeCallback = base::RepeatingCallback<void(
      base::span<const AudioObjectPropertyAddress>)>;
  PropertyListenerHelper(
      bool capture_default_device,
      AudioObjectID aggregate_device_id,
      ProcessPropertyChangeCallback process_property_change_callback,
      const raw_ptr<CatapApi> catap_api)
      : capture_default_device_(capture_default_device),
        aggregate_device_id_(aggregate_device_id),
        catap_api_(catap_api) {
    AddPropertyListener(process_property_change_callback);
  }

  ~PropertyListenerHelper() { RemovePropertyListener(); }

 private:
  void AddPropertyListener(
      const ProcessPropertyChangeCallback process_property_change_callback) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    TRACE_EVENT0("audio", "PropertyListenerHelper::AddPropertyListener");
    property_listener_block_ = ^(UInt32 number_of_addresses,
                                 const AudioObjectPropertyAddress* addresses) {
      // SAFETY: The type of addresses cannot be changed since it's received
      // from the OS. Wrap it immediately using its specified size.
      base::span UNSAFE_BUFFERS(
          property_addresses(addresses, number_of_addresses));
      process_property_change_callback.Run(property_addresses);
    };

    catap_api_->AudioObjectAddPropertyListenerBlock(
        aggregate_device_id_, &kDeviceIsAliveAddress, dispatch_get_main_queue(),
        property_listener_block_);

    if (capture_default_device_) {
      catap_api_->AudioObjectAddPropertyListenerBlock(
          kAudioObjectSystemObject, &kDefaultOutputDevicePropertyAddress,
          dispatch_get_main_queue(), property_listener_block_);
    }

    catap_api_->AudioObjectAddPropertyListenerBlock(
        aggregate_device_id_, &kSampleRateAddress, dispatch_get_main_queue(),
        property_listener_block_);
  }

  void RemovePropertyListener() {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    TRACE_EVENT0("audio", "PropertyListenerHelper::RemovePropertyListener");

    // Use the stored block reference to remove the listener.
    catap_api_->AudioObjectRemovePropertyListenerBlock(
        aggregate_device_id_, &kSampleRateAddress, dispatch_get_main_queue(),
        property_listener_block_);

    if (capture_default_device_) {
      catap_api_->AudioObjectRemovePropertyListenerBlock(
          kAudioObjectSystemObject, &kDefaultOutputDevicePropertyAddress,
          dispatch_get_main_queue(), property_listener_block_);
    }

    catap_api_->AudioObjectRemovePropertyListenerBlock(
        aggregate_device_id_, &kDeviceIsAliveAddress, dispatch_get_main_queue(),
        property_listener_block_);

    property_listener_block_ = nil;
  }

  const bool capture_default_device_;

  const AudioObjectID aggregate_device_id_;

  // Interface used to access the CoreAudio framework.
  const raw_ptr<CatapApi> catap_api_;

  // A reference to the listener block is needed to remove the listener when the
  // capture stream is stopped.
  AudioObjectPropertyListenerBlock property_listener_block_
      GUARDED_BY_CONTEXT(sequence_checker_);

  SEQUENCE_CHECKER(sequence_checker_);
};

// 0.0 is used to indicate that this device doesn't support setting the volume.
// TODO(crbug.com/415953612): Is this okay, or do we need to support this?
constexpr float kMaxVolume = 0.0;

CatapAudioInputStreamSource::Config::Config(const AudioParameters& params,
                                            const std::string& device_id,
                                            bool force_mono_capture)
    : catap_channels(force_mono_capture ? 1 : params.channels()),
      output_channels(params.channels()),
      sample_rate(params.sample_rate()),
      frames_per_buffer(params.frames_per_buffer()),
      capture_default_device(IsDefaultOutputDeviceLoopback(device_id)),
      mute_local_device(MuteLocalPlaybackLoopback(device_id)),
      exclude_chrome(ExcludeChromeLoopback(device_id)),
      capture_application_process_id(
          AudioDeviceDescription::IsApplicationLoopbackDevice(device_id)
              ? std::make_optional(
                    GetApplicationIdFromApplicationLoopbackDeviceId(device_id))
              : std::nullopt) {}

std::string CatapAudioInputStreamSource::Config::AsHumanReadableString() const {
  std::ostringstream s;
  s << "output channels: " << output_channels
    << ", sample_rate: " << sample_rate
    << ", frames_per_buffer: " << frames_per_buffer
    << ", capture_default_device: " << capture_default_device
    << ", mute_local_device: " << mute_local_device
    << ", exclude_chrome: " << exclude_chrome
    << ", catap_channels: " << catap_channels;
  if (capture_application_process_id) {
    s << ", capture_application_process_id: "
      << *capture_application_process_id;
  }
  return s.str();
}

CatapAudioInputStreamSource::CatapAudioInputStreamSource(
    const raw_ptr<CatapApi> catap_api,
    const Config& config,
    const AudioManager::LogCallback log_callback,
    const raw_ptr<AudioPropertyChangeCallback> audio_property_change_callback)
    : catap_api_(catap_api),
      config_(config),
      buffer_frames_duration_(
          AudioTimestampHelper::FramesToTime(config_.frames_per_buffer,
                                             config_.sample_rate)),
      glitch_helper_(config_.sample_rate,
                     AudioGlitchInfo::Direction::kLoopback),
      audio_bus_(config_.catap_channels == 1
                     ? AudioBus::CreateWrapper(config_.output_channels)
                     : AudioBus::Create(config_.output_channels,
                                        config_.frames_per_buffer)),
      sink_(nullptr),
      log_callback_(std::move(log_callback)),
      audio_property_change_callback_(audio_property_change_callback) {
  CHECK(!log_callback_.is_null());
  CHECK(catap_api_);

  // Only mono and stereo audio is supported.
  CHECK(config_.output_channels == 1 || config_.output_channels == 2);
  CHECK(config_.catap_channels == 1 ||
        config_.catap_channels == config_.output_channels);

  SendLogMessage("%s({config=[%s]})", __func__,
                 config_.AsHumanReadableString().c_str());
}

CatapAudioInputStreamSource::~CatapAudioInputStreamSource() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  Close();
  ReportAndResetStats();
}

AudioInputStream::OpenOutcome CatapAudioInputStreamSource::Open(
    std::optional<std::string> default_output_device_uid) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  TRACE_EVENT0("audio", "CatapAudioInputStreamSource::Open");
  base::ElapsedTimer timer;

  SendLogMessage("%s", __func__);

  if (is_device_open_) {
    ReportOpenStatus(OpenStatus::kErrorDeviceAlreadyOpen, timer.Elapsed());
    SendLogMessage("%s => Device is already open.", __func__);
    return AudioInputStream::OpenOutcome::kAlreadyOpen;
  }

  if (config_.capture_application_process_id) {
    // Get a list of all CoreAudio process device IDs that belong to the
    // specified application process.
    pid_t application_pid = *config_.capture_application_process_id;
    NSArray<NSNumber*>* process_audio_device_ids_to_include =
        GetProcessAudioDeviceIds(application_pid);
    if (![process_audio_device_ids_to_include count]) {
      ReportOpenStatus(OpenStatus::kGetProcessAudioDeviceIdsReturnedEmpty,
                       timer.Elapsed());
      SendLogMessage("%s => Could not determine audio objects that belong to "
                     "the application process.",
                     __func__);
      return AudioInputStream::OpenOutcome::kFailed;
    }
    // Mix the given process to a stereo stream. We will not select default
    // device below when we capture application audio.
    tap_description_ = [[CATapDescription alloc]
        initStereoMixdownOfProcesses:process_audio_device_ids_to_include];
  } else {
    NSArray<NSNumber*>* process_audio_device_ids_to_exclude = @[];
    if (config_.exclude_chrome) {
      // Get a list of all CoreAudio process device IDs that belong to the
      // Chrome audio service.
      pid_t chrome_audio_service_pid = getpid();
      process_audio_device_ids_to_exclude =
          GetProcessAudioDeviceIds(chrome_audio_service_pid);
      if (![process_audio_device_ids_to_exclude count]) {
        ReportOpenStatus(OpenStatus::kGetProcessAudioDeviceIdsReturnedEmpty,
                         timer.Elapsed());
        SendLogMessage("%s => Could not determine audio objects that belong to "
                       "the audio service.",
                       __func__);
      }
    }

    // Default initialization: Mix all processes to a stereo stream except the
    // given processes. The default output device is selected below unless the
    // device ID specifies that all devices should be captured.
    tap_description_ =
        [[CATapDescription alloc] initStereoGlobalTapButExcludeProcesses:
                                      process_audio_device_ids_to_exclude];
  }

  if (tap_description_ == nil) {
    ReportOpenStatus(OpenStatus::kErrorCreatingTapDescription, timer.Elapsed());
    SendLogMessage("%s => CATapDescription initialization failed.", __func__);
    return AudioInputStream::OpenOutcome::kFailed;
  }

  if (config_.capture_default_device) {
    if (!default_output_device_uid) {
      ReportOpenStatus(OpenStatus::kGetDefaultDeviceUidEmpty, timer.Elapsed());
      SendLogMessage("%s => Error getting UID for default output device",
                     __func__);
      return AudioInputStream::OpenOutcome::kFailed;
    }
    // Select the default output device.
    tap_description_.deviceUID = @(default_output_device_uid->c_str());
    tap_description_.stream = @(0);
  }

  if (config_.catap_channels == 1) {
    [tap_description_ setMono:YES];
  }
  if (config_.mute_local_device) {
    // device_id_ == AudioDeviceDescription::kLoopbackWithMuteDeviceId ||
    //   device_id_ == AudioDeviceDescription::kLoopbackWithMuteDeviceIdCast) {
    //  No audio is sent to the hardware (e.g, speakers) while the audio is
    //  captured.
    [tap_description_ setMuteBehavior:CATapMuted];
  }
  [tap_description_ setName:@"ChromeAudioService"];
  [tap_description_ setPrivate:YES];

  // Initialization: Step 1.
  OSStatus status =
      catap_api_->AudioHardwareCreateProcessTap(tap_description_, &tap_);
  if (status != noErr || tap_ == kAudioObjectUnknown) {
    // `kAudioObjectUnknown` is returned if the specified output device doesn't
    // exist.
    ReportOpenStatus(OpenStatus::kErrorCreatingProcessTap, timer.Elapsed());
    SendLogMessage("%s => Error creating process tap. Status: %d", __func__,
                   status);
    return AudioInputStream::OpenOutcome::kFailed;
  }

  NSString* tap_uid = [[tap_description_ UUID] UUIDString];
  NSArray<NSDictionary*>* taps = @[
    @{
      @kAudioSubTapUIDKey : (NSString*)tap_uid,
      @kAudioSubTapDriftCompensationKey : @YES,
    },
  ];

  // Get a unique ID.
  NSUUID* uuid = [NSUUID UUID];
  NSString* unique_uid = [uuid UUIDString];

  NSDictionary* aggregate_device_properties_ = @{
    @kAudioAggregateDeviceNameKey : @"ChromeAudioAggregateDevice",
    @kAudioAggregateDeviceUIDKey : unique_uid,
    @kAudioAggregateDeviceTapListKey : taps,
    @kAudioAggregateDeviceTapAutoStartKey : @NO,
    @kAudioAggregateDeviceIsPrivateKey : @YES,
  };

  // Initialization: Step 2.
  // Create the aggregate device.
  status = catap_api_->AudioHardwareCreateAggregateDevice(
      (__bridge CFDictionaryRef)aggregate_device_properties_,
      &aggregate_device_id_);
  if (status != noErr) {
    ReportOpenStatus(OpenStatus::kErrorCreatingAggregateDevice,
                     timer.Elapsed());
    SendLogMessage("%s => Error creating aggregate device. Status: %d",
                   __func__, status);
    return AudioInputStream::OpenOutcome::kFailed;
  }

  // Configure the aggregate device.
  if (!ConfigureSampleRateOfAggregateDevice()) {
    ReportOpenStatus(OpenStatus::kErrorConfiguringSampleRate, timer.Elapsed());
    SendLogMessage(
        "%s => Could not configure the aggregate device with sample rate.",
        __func__);
    return AudioInputStream::OpenOutcome::kFailed;
  }
  if (!ConfigureFramesPerBufferOfAggregateDevice()) {
    ReportOpenStatus(OpenStatus::kErrorConfiguringFramesPerBuffer,
                     timer.Elapsed());
    SendLogMessage("%s => Could not configure the aggregate device with frame "
                   "buffer size.",
                   __func__);
    return AudioInputStream::OpenOutcome::kFailed;
  }

  // Initialization: Step 3.
  // Attach callback to the aggregate device.
  status = catap_api_->AudioDeviceCreateIOProcID(
      aggregate_device_id_, DeviceIoProc, this, &tap_io_proc_id_);
  if (status != noErr) {
    ReportOpenStatus(OpenStatus::kErrorCreatingIOProcID, timer.Elapsed());
    SendLogMessage("%s => Error calling AudioDeviceCreateIOProcID. Status: %d",
                   __func__, status);
    return AudioInputStream::OpenOutcome::kFailed;
  }

  // Try to explicitly set a property, if this fails this is a sign that we
  // don't have audio capture permission.
  if (base::FeatureList::IsEnabled(kMacCatapProbeTapOnCreation) &&
      !ProbeAudioTapPermissions()) {
    ReportOpenStatus(OpenStatus::kErrorMissingAudioTapPermission,
                     timer.Elapsed());
    SendLogMessage("%s => Error when probing audio tap permissions.", __func__);
    return AudioInputStream::OpenOutcome::kFailedSystemPermissions;
  }

  property_listener_ = std::make_unique<PropertyListenerHelper>(
      config_.capture_default_device, aggregate_device_id_,
      base::BindRepeating(&CatapAudioInputStreamSource::ProcessPropertyChange,
                          weak_ptr_factory_.GetWeakPtr()),
      catap_api_);

  is_device_open_ = true;
  ReportOpenStatus(OpenStatus::kOk, timer.Elapsed());
  return AudioInputStream::OpenOutcome::kSuccess;
}

void CatapAudioInputStreamSource::Start(
    AudioInputStream::AudioInputCallback* callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  TRACE_EVENT0("audio", "CatapAudioInputStreamSource::Start");
  SendLogMessage("%s", __func__);
  base::ElapsedTimer timer;
  CHECK(callback);
  CHECK(is_device_open_);

  sink_ = callback;
  // Initialization: Step 4.
  // Start the aggregate device.
  OSStatus status =
      catap_api_->AudioDeviceStart(aggregate_device_id_, tap_io_proc_id_);
  if (status != noErr) {
    ReportStartStatus(false, timer.Elapsed());
    SendLogMessage("%s => Error starting the device. Status: %d", __func__,
                   status);
    sink_->OnError();
  }
  ReportStartStatus(true, timer.Elapsed());
}

void CatapAudioInputStreamSource::Stop() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  TRACE_EVENT0("audio", "CatapAudioInputStreamSource::Stop");
  SendLogMessage("%s", __func__);
  base::ElapsedTimer timer;

  property_listener_.reset();

  if (!sink_) {
    return;
  }

  CHECK_NE(aggregate_device_id_, kAudioObjectUnknown);
  CHECK_NE(tap_io_proc_id_, nullptr);

  // Reversing Step 4.
  // The call to AudioDeviceStop is synchronous. It will not return until any
  // current callbacks have finished executing. The call to AudioDeviceStop()
  // succeeds even though AudioDeviceStart() has not been called.
  OSStatus status =
      catap_api_->AudioDeviceStop(aggregate_device_id_, tap_io_proc_id_);
  if (status != noErr) {
    ReportStopStatus(false, timer.Elapsed());
    SendLogMessage("%s => Error stopping the device. Status: %d", __func__,
                   status);
  }

  ReportHostTimeStatus(total_callbacks_, callbacks_with_missing_host_time_,
                       recovered_from_missing_host_time_);
  ReportMismatchStatus(total_callbacks_with_channel_count_mismatch_,
                       total_callbacks_with_frames_mismatch_);
  if (total_callbacks_with_channel_count_mismatch_ > 0) {
    SendLogMessage("%s => total_callbacks_with_channel_count_mismatch_: %d",
                   __func__, total_callbacks_with_channel_count_mismatch_);
  }
  if (total_callbacks_with_frames_mismatch_ > 0) {
    SendLogMessage("%s => total_callbacks_with_frames_mismatch_: %d", __func__,
                   total_callbacks_with_frames_mismatch_);
  }

  sink_ = nullptr;
  ReportStopStatus(true, timer.Elapsed());
  ReportAndResetStats();
}

void CatapAudioInputStreamSource::Close() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  TRACE_EVENT0("audio", "CatapAudioInputStreamSource::Close");
  SendLogMessage("%s", __func__);
  // Check if stopped.
  CHECK(!sink_);
  base::ElapsedTimer timer;

  is_device_open_ = false;

  if (aggregate_device_id_ != kAudioObjectUnknown &&
      tap_io_proc_id_ != nullptr) {
    // Reversing Step 3.
    OSStatus status = catap_api_->AudioDeviceDestroyIOProcID(
        aggregate_device_id_, tap_io_proc_id_);
    if (status != noErr) {
      ReportCloseStatus(CloseStatus::kErrorDestroyingIOProcID, timer.Elapsed());
      SendLogMessage("%s => Error destroying device IO process ID. Status: %d",
                     __func__, status);
    }
    tap_io_proc_id_ = nullptr;
  }

  if (aggregate_device_id_ != kAudioObjectUnknown) {
    // Reversing Step 2.
    OSStatus status =
        catap_api_->AudioHardwareDestroyAggregateDevice(aggregate_device_id_);
    if (status != noErr) {
      ReportCloseStatus(CloseStatus::kErrorDestroyingAggregateDevice,
                        timer.Elapsed());
      SendLogMessage("%s => Error destroying aggregate device. Status: %d",
                     __func__, status);
    }
    aggregate_device_id_ = kAudioObjectUnknown;
  }

  if (tap_ != kAudioObjectUnknown) {
    // Reversing Step 1.
    OSStatus status = catap_api_->AudioHardwareDestroyProcessTap(tap_);
    if (status != noErr) {
      ReportCloseStatus(CloseStatus::kErrorDestroyingProcessTap,
                        timer.Elapsed());
      SendLogMessage("%s => Error destroying process tap. Status: %d", __func__,
                     status);
    }
    tap_ = kAudioObjectUnknown;
  }

  if (tap_description_ != nil) {
    tap_description_ = nil;
  }

  ReportCloseStatus(CloseStatus::kOk, timer.Elapsed());
}

void CatapAudioInputStreamSource::OnCatapSample(
    const AudioBuffer* input_buffer,
    const AudioTimeStamp* input_time) {
  CHECK(input_buffer);
  CHECK(input_time);
  base::TimeTicks capture_time;
  if (!(input_time->mFlags & kAudioTimeStampHostTimeValid)) {
    // Fallback if there's no host time stamp. There's no evidence that this
    // ever happens, so this is just in case.
    capture_time = next_expected_capture_time_ ? *next_expected_capture_time_
                                               : base::TimeTicks::Now();
    ++callbacks_with_missing_host_time_;
  } else {
    capture_time = base::TimeTicks::FromMachAbsoluteTime(input_time->mHostTime);
    recovered_from_missing_host_time_ = callbacks_with_missing_host_time_ > 0;
  }
  ++total_callbacks_;
  TRACE_EVENT1("audio", "CatapAudioInputStreamSource::OnCatapSample",
               "capture_time", capture_time);

  float* data = (float*)input_buffer->mData;
  int frames = input_buffer->mDataByteSize /
               (input_buffer->mNumberChannels * sizeof(Float32));

  // The number of channels may change when a bluetooth device is captured and
  // the bluetooth profile is switched between A2DP and HFP. The sample rate
  // changes at the same time, this means that the property listener will detect
  // the change and call OnError(). We have not seen such case, but it could
  // happen that one buffer is received with the wrong number of channels.
  constexpr int kMaxNumberOfWarningReports = 10;
  if (static_cast<unsigned int>(config_.catap_channels) !=
      input_buffer->mNumberChannels) {
    ++total_callbacks_with_channel_count_mismatch_;
    if (total_callbacks_with_channel_count_mismatch_ <
        kMaxNumberOfWarningReports) {
      DLOG(WARNING)
          << "CatapAudioInputStream::OnCatapSample: Channel count mismatch, "
             "input_buffer->mNumberChannels: "
          << input_buffer->mNumberChannels
          << " config_.catap_channels: " << config_.catap_channels;
    }
    return;
  }
  if (frames != config_.frames_per_buffer) {
    ++total_callbacks_with_frames_mismatch_;
    if (total_callbacks_with_frames_mismatch_ < kMaxNumberOfWarningReports) {
      DLOG(WARNING) << "CatapAudioInputStream::OnCatapSample: "
                       "frames: "
                    << frames << " does not match config_.frames_per_buffer: "
                    << config_.frames_per_buffer;
    }
    return;
  }

  glitch_helper_.OnFramesReceived(*input_time, config_.frames_per_buffer);

  if (config_.catap_channels == 1) {
    // If the captured signal is mono, we may need to upmix it. This loop copies
    // the single mono channel to all output channels. For example, if
    // outputting to stereo, both left and right channels will get the same mono
    // data.

    // SAFETY: This comes from a struct provided by the OS and the number of
    // frames is calculated based on the information provided in the struct.
    base::span UNSAFE_BUFFERS(mono_data(data, (size_t)frames));
    audio_bus_->set_frames(frames);
    for (int i = 0; i < config_.output_channels; ++i) {
      audio_bus_->SetChannelData(i, mono_data);
    }
  } else {
    // The captured signal is already stereo, so we can de-interleave it
    // directly into the audio bus.
    audio_bus_->FromInterleaved<Float32SampleTypeTraits>(data, frames);
  }
  sink_->OnData(audio_bus_.get(), capture_time, kMaxVolume,
                glitch_helper_.ConsumeGlitchInfo());

  // Stores the time of the next expected audio callback. This is used as a
  // fallback if the host doesn't provide a timestamp.
  next_expected_capture_time_ = capture_time + buffer_frames_duration_;
}

NSArray<NSNumber*>* CatapAudioInputStreamSource::GetProcessAudioDeviceIds(
    pid_t chrome_process_id) {
  // Returns all CoreAudio process audio device IDs that belong to the specified
  // process ID.
  base::ElapsedTimer timer;

  AudioObjectPropertyAddress property_address = {
      kAudioHardwarePropertyProcessObjectList, kAudioObjectPropertyScopeGlobal,
      kAudioObjectPropertyElementMain};
  UInt32 property_size;

  // Get all CoreAudio process audio device IDs (which are UInt32).
  OSStatus result = catap_api_->AudioObjectGetPropertyDataSize(
      kAudioObjectSystemObject, &property_address, /*in_qualifier_data_size=*/0,
      /*in_qualifier_data=*/nullptr, &property_size);
  if (result != noErr) {
    ReportGetProcessAudioDeviceIdsDuration(false, timer.Elapsed());
    SendLogMessage(
        "%s => Could not get number of process audio device IDs. Status: %d",
        __func__, result);
    return @[];
  }

  UInt32 num_devices = property_size / sizeof(AudioDeviceID);
  auto device_ids = std::vector<AudioDeviceID>(num_devices);
  result = catap_api_->AudioObjectGetPropertyData(
      kAudioObjectSystemObject, &property_address, /*in_qualifier_data_size=*/0,
      /*in_qualifier_data=*/nullptr, &property_size, device_ids.data());
  if (result != noErr) {
    ReportGetProcessAudioDeviceIdsDuration(false, timer.Elapsed());
    SendLogMessage("%s => Could not get process audio device IDs. Status: %d",
                   __func__, result);
    return @[];
  }

  NSMutableArray<NSNumber*>* process_audio_device_ids_array =
      [NSMutableArray arrayWithCapacity:num_devices];

  for (AudioDeviceID device_id : device_ids) {
    // Get the process ID and add the device to the list if there's a match.
    property_address.mSelector = kAudioProcessPropertyPID;
    int32_t process_id;
    property_size = sizeof(int32_t);
    result = catap_api_->AudioObjectGetPropertyData(
        device_id, &property_address, /*in_qualifier_data_size=*/0,
        /*in_qualifier_data=*/nullptr, &property_size, &process_id);
    if (result != noErr) {
      SendLogMessage("%s => Could not determine process ID of process audio "
                     "device ID. Status: %d",
                     __func__, result);
      continue;  // Skip this device and continue to the next.
    }

    if (process_id == chrome_process_id) {
      [process_audio_device_ids_array addObject:@(device_id)];
    }
  }

  ReportGetProcessAudioDeviceIdsDuration(true, timer.Elapsed());
  return process_audio_device_ids_array;
}

bool CatapAudioInputStreamSource::ConfigureSampleRateOfAggregateDevice() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // Set sample rate.
  UInt32 property_size = sizeof(Float64);
  Float64 sample_rate = config_.sample_rate;
  OSStatus result = catap_api_->AudioObjectSetPropertyData(
      aggregate_device_id_, &kSampleRateAddress, /*in_qualifier_data_size=*/0,
      /*in_qualifier_data=*/nullptr, property_size, &sample_rate);
  if (result != noErr) {
    SendLogMessage(
        "%s => Could not set sample rate of the aggregate device. Status: %d",
        __func__, result);
    return false;
  }
  return true;
}

std::optional<double>
CatapAudioInputStreamSource::GetSampleRateOfAggregateDevice() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // Get sample rate.
  UInt32 property_size = sizeof(Float64);
  Float64 sample_rate = 0.0;
  OSStatus result = catap_api_->AudioObjectGetPropertyData(
      aggregate_device_id_, &kSampleRateAddress, /*in_qualifier_data_size=*/0,
      /*in_qualifier_data=*/nullptr, &property_size, &sample_rate);
  if (result != noErr) {
    SendLogMessage(
        "%s => Could not get sample rate of the aggregate device. Status: %d",
        __func__, result);
    return std::nullopt;
  }
  return sample_rate;
}

bool CatapAudioInputStreamSource::ConfigureFramesPerBufferOfAggregateDevice() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // Set frames per buffer.
  // Set sample rate.
  AudioObjectPropertyAddress property_address = {
      kAudioDevicePropertyBufferFrameSize, kAudioObjectPropertyScopeGlobal,
      kAudioObjectPropertyElementMain};
  UInt32 property_size = sizeof(UInt32);
  UInt32 frames_per_buffer = config_.frames_per_buffer;
  OSStatus result = catap_api_->AudioObjectSetPropertyData(
      aggregate_device_id_, &property_address, /*in_qualifier_data_size=*/0,
      /*in_qualifier_data=*/nullptr, property_size, &frames_per_buffer);
  if (result != noErr) {
    SendLogMessage("%s => Could not set frames per buffer of the aggregate "
                   "device. Status: %d",
                   __func__, result);
    return false;
  }
  return true;
}

bool CatapAudioInputStreamSource::ProbeAudioTapPermissions() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  UInt32 propertySize = sizeof(CATapDescription*);
  AudioObjectPropertyAddress propertyAddress = {
      kAudioTapPropertyDescription, kAudioObjectPropertyScopeGlobal,
      kAudioObjectPropertyElementMain};

  void* get_description_ptr = nullptr;
  OSStatus status = catap_api_->AudioObjectGetPropertyData(
      tap_, &propertyAddress, /*in_qualifier_data_size=*/0,
      /*in_qualifier_data=*/nullptr, &propertySize, &get_description_ptr);
  if (status != noErr) {
    return false;
  }

  // We receive ownership of the Core Foundation object returned by
  // `AudioObjectGetPropertyData()`. `CFBridgingRelease` transfers this
  // ownership to ARC. The `description` object will now be released
  // automatically when it goes out of scope.
  CATapDescription* description = CFBridgingRelease(get_description_ptr);

  // `AudioObjectSetPropertyData()` does not take ownership of the object. We
  // use a non-owning `__bridge` cast to pass the pointer. ARC retains
  // ownership, and `description` will be released when it goes out of scope.
  void* set_description_ptr = (__bridge void*)description;
  status = catap_api_->AudioObjectSetPropertyData(
      tap_, &propertyAddress, /*in_qualifier_data_size=*/0,
      /*in_qualifier_data=*/nullptr, propertySize, &set_description_ptr);

  if (status != noErr) {
    return false;
  }
  return true;
}

void CatapAudioInputStreamSource::ProcessPropertyChange(
    base::span<const AudioObjectPropertyAddress> property_addresses) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  for (const AudioObjectPropertyAddress& property_address :
       property_addresses) {
    if (property_address == kDeviceIsAliveAddress) {
      TRACE_EVENT1("audio",
                   "CatapAudioInputStreamSource::ProcessPropertyChange",
                   "property", "DeviceIsAlive");
      // Read IsAlive property.
      UInt32 property_size = sizeof(UInt32);
      UInt32 is_alive = false;
      OSStatus status = catap_api_->AudioObjectGetPropertyData(
          aggregate_device_id_, &kDeviceIsAliveAddress,
          /*in_qualifier_data_size=*/0,
          /*in_qualifier_data=*/nullptr, &property_size, &is_alive);
      if (status != noErr) {
        continue;
      }
      base::UmaHistogramBoolean(
          base::JoinString(
              {kCatapAudioInputStreamUmaBaseName, kHistogramDeviceIsAliveName},
              kHistogramPartsSeparator),
          is_alive);
      SendLogMessage("%s => Device is alive property changed: %d", __func__,
                     is_alive);
      if (!is_alive) {
        OnError();
      }
    } else if (property_address == kDefaultOutputDevicePropertyAddress) {
      TRACE_EVENT1("audio",
                   "CatapAudioInputStreamSource::ProcessPropertyChange",
                   "property", "DefaultOutputDevice");
      SendLogMessage("%s => Default output device changed.", __func__);
      // Nothing should be done after the callback is called, because 'this'
      // might be deleted within the callback implementation.
      audio_property_change_callback_->OnDefaultDeviceChange();
    } else if (property_address == kSampleRateAddress) {
      TRACE_EVENT1("audio",
                   "CatapAudioInputStreamSource::ProcessPropertyChange",
                   "property", "SampleRate");
      std::optional<double> sample_rate = GetSampleRateOfAggregateDevice();
      if (!sample_rate.has_value() ||
          sample_rate.value() != config_.sample_rate) {
        SendLogMessage("%s => Sample rate changed. New sample rate: %f",
                       __func__, sample_rate.value_or(-1.0));
        // Nothing should be done after the callback is called, because 'this'
        // might be deleted within the callback implementation.
        audio_property_change_callback_->OnSampleRateChange();
      }
    }
  }
}

void CatapAudioInputStreamSource::OnError() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  SendLogMessage("%s", __func__);
  if (sink_) {
    sink_->OnError();
  }
}

void CatapAudioInputStreamSource::SendLogMessage(const char* format, ...) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  va_list args;
  va_start(args, format);
  log_callback_.Run("CatapAudioInputStreamSource::" +
                    base::StringPrintV(format, args));
  va_end(args);
}

void CatapAudioInputStreamSource::ReportAndResetStats() {
  std::optional<std::string> log_message =
      glitch_helper_.LogAndReset("CATap in");
  if (log_message) {
    SendLogMessage(log_message->c_str());
  }
}

CatapAudioInputStream::AudioDeviceIds::AudioDeviceIds() = default;
CatapAudioInputStream::AudioDeviceIds::~AudioDeviceIds() = default;
CatapAudioInputStream::AudioDeviceIds::AudioDeviceIds(
    const AudioDeviceIds& other) = default;
CatapAudioInputStream::AudioDeviceIds::AudioDeviceIds(AudioDeviceID device_id,
                                                      std::string uid)
    : id(device_id), uid(std::move(uid)) {}

CatapAudioInputStream::CatapAudioInputStream(
    std::unique_ptr<CatapApi> catap_api,
    GetDefaultDeviceIdsCallback get_default_device_ids_callback,
    const AudioParameters& params,
    const std::string& device_id,
    AudioManager::LogCallback log_callback,
    NotifyOnCloseCallback close_callback)
    : catap_api_(std::move(catap_api)),
      params_(params),
      device_id_(device_id),
      restart_on_device_change_(IsDefaultOutputDeviceLoopback(device_id_) &&
                                base::FeatureList::IsEnabled(
                                    features::kMacCatapRestartOnDeviceChange)),
      close_callback_(std::move(close_callback)),
      log_callback_(log_callback),

      get_default_device_ids_callback_(
          std::move(get_default_device_ids_callback)) {
  CHECK(IsLoopbackDevice(device_id_));
}

AudioInputStream::OpenOutcome CatapAudioInputStream::Open() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  TRACE_EVENT0("audio", "CatapAudioInputStream::Open");

  if (source_) {
    return AudioInputStream::OpenOutcome::kAlreadyOpen;
  }

  AudioDeviceIds default_device_ids = get_default_device_ids_callback_.Run();

  // The microphone input from Bluetooth headsets using the headset profile
  // is mono. Typically the OS handles this and provides a stereo stream,
  // but this mechanism can fail. Forcing a direct mono capture for these
  // mono sources serves as a workaround.
  bool force_mono_capture =
      IsDefaultOutputDeviceLoopback(device_id_) &&
      base::FeatureList::IsEnabled(kMacCatapForceMonoCaptureOfMonoDevices) &&
      GetVirtualFormatChannels(
          default_device_ids.id.value_or(kAudioObjectUnknown)) == 1;

  CatapAudioInputStreamSource::Config config(params_, device_id_,
                                             force_mono_capture);

  if (AudioDeviceDescription::IsApplicationLoopbackDevice(device_id_) &&
      !config.capture_application_process_id) {
    SendLogMessage("%s => No valid Application PID to capture.", __func__);
    return AudioInputStream::OpenOutcome::kFailed;
  }

  source_ = std::make_unique<CatapAudioInputStreamSource>(
      catap_api_.get(), config, log_callback_, this);

  AudioInputStream::OpenOutcome outcome = source_->Open(default_device_ids.uid);

  if (outcome != OpenOutcome::kSuccess) {
    SendLogMessage("%s => Failed to open(), outcome: %d", __func__, outcome);
    source_.reset();
  }

  return outcome;
}

void CatapAudioInputStream::Start(AudioInputCallback* callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  TRACE_EVENT0("audio", "CatapAudioInputStream::Start");
  CHECK(callback);
  if (!source_) {
    SendLogMessage("%s => stream is nullptr", __func__);
    callback->OnError();
    return;
  }
  audio_input_callback_ = callback;
  source_->Start(audio_input_callback_);
}

void CatapAudioInputStream::Stop() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  TRACE_EVENT0("audio", "CatapAudioInputStream::Stop");
  if (!audio_input_callback_) {
    return;
  }
  CHECK(source_);
  source_->Stop();
  audio_input_callback_ = nullptr;
}

void CatapAudioInputStream::Close() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  TRACE_EVENT0("audio", "CatapAudioInputStream::Close");
  Stop();
  source_.reset();

  // Notify the owner that the stream can be deleted.
  std::move(close_callback_).Run(this);
}

double CatapAudioInputStream::GetMaxVolume() {
  return kMaxVolume;
}

void CatapAudioInputStream::SetVolume(double volume) {
  // SetVolume() is not supported, ignore call.
}

double CatapAudioInputStream::GetVolume() {
  return kMaxVolume;
}

bool CatapAudioInputStream::IsMuted() {
  return false;
}

void CatapAudioInputStream::SetOutputDeviceForAec(
    const std::string& output_device_id) {
  return;
}

void CatapAudioInputStream::OnSampleRateChange() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (restart_on_device_change_) {
    RestartStream();
  } else {
    OnError();
  }
}

void CatapAudioInputStream::OnDefaultDeviceChange() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (restart_on_device_change_) {
    RestartStream();
  }
}

CatapAudioInputStream::~CatapAudioInputStream() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  CHECK(!source_);
}

void CatapAudioInputStream::OnError() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  SendLogMessage("%s", __func__);
  if (audio_input_callback_) {
    audio_input_callback_->OnError();
  }
}

int CatapAudioInputStream::GetVirtualFormatChannels(AudioDeviceID device_id) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  AudioStreamBasicDescription stream_format;
  UInt32 property_size = sizeof(AudioStreamBasicDescription);
  // Get the Virtual Format data.
  OSStatus status = catap_api_->AudioObjectGetPropertyData(
      device_id, &kVirtualFormatAddress, 0, NULL, &property_size,
      &stream_format);

  if (status != noErr) {
    return 0;
  }

  return stream_format.mChannelsPerFrame;
}

void CatapAudioInputStream::RestartStream() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  TRACE_EVENT0("audio", "CatapAudioInputStream::RestartStream");
  // There can't be any audio property callbacks (OnSampleRateChange() and
  // OnDefaultDeviceChange()) from the `source_` if it doesn't exist. And
  // RestartStream() is only called from the property callbacks. Therefore
  // `source_` will always exist.
  CHECK(source_);
  source_->Stop();
  source_.reset();
  if (Open() != OpenOutcome::kSuccess) {
    CHECK(!source_);
    OnError();
    audio_input_callback_ = nullptr;
    return;
  }
  if (audio_input_callback_) {
    // The existence of an audio callback implies the previous `source_` was
    // active. Start the new `source_` immediately to maintain the stream's
    // started state.
    source_->Start(audio_input_callback_);
  }
}

void CatapAudioInputStream::SendLogMessage(const char* format, ...) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  va_list args;
  va_start(args, format);
  log_callback_.Run("CatapAudioInputStream::" +
                    base::StringPrintV(format, args));
  va_end(args);
}

AudioInputStream* CreateCatapAudioInputStream(
    const AudioParameters& params,
    const std::string& device_id,
    AudioManager::LogCallback log_callback,
    base::OnceCallback<void(AudioInputStream*)> close_callback) {
  if (@available(macOS 14.2, *)) {
    return new CatapAudioInputStream(
        std::make_unique<CatapApiImpl>(),
        base::BindRepeating(GetDefaultOutputDeviceIds), params, device_id,
        log_callback, std::move(close_callback));
  }
  log_callback.Run("CatapAudioInputStream::CreateCatapAudioInputStream() Catap "
                   "not supported");
  return nullptr;
}

API_AVAILABLE(macos(14.2))
AudioInputStream* CreateCatapAudioInputStreamForTesting(
    const AudioParameters& params,
    const std::string& device_id,
    AudioManager::LogCallback log_callback,
    base::OnceCallback<void(AudioInputStream*)> close_callback,
    std::unique_ptr<CatapApi> catap_api,
    base::RepeatingCallback<CatapAudioInputStream::AudioDeviceIds()>
        get_default_device_ids_callback) {
  return new CatapAudioInputStream(
      std::move(catap_api), std::move(get_default_device_ids_callback), params,
      device_id, log_callback, std::move(close_callback));
}

}  // namespace media