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

#include "chrome/browser/media/audio_ducker.h"

#include "build/build_config.h"
#include "content/public/browser/media_session.h"
#include "content/public/browser/media_session_service.h"
#include "content/public/browser/web_contents.h"
#include "media/base/media_switches.h"

#if BUILDFLAG(IS_WIN)
#include "content/public/browser/audio_service_info.h"
#include "media/audio/win/audio_ducker_win.h"
#endif  // BUILDFLAG(IS_WIN)

AudioDucker::AudioDucker(content::Page& page)
    : content::PageUserData<AudioDucker>(page),
      content::WebContentsObserver(GetWebContents()) {
#if BUILDFLAG(IS_WIN)
  if (base::FeatureList::IsEnabled(media::kAudioDuckingWin)) {
    // `base::Unretained()` is safe here since we own `windows_ducker_`.
    windows_ducker_ =
        std::make_unique<media::AudioDuckerWin>(base::BindRepeating(
            &AudioDucker::ShouldDuckProcess, base::Unretained(this)));
  }
#endif  // BUILDFLAG(IS_WIN)
}

AudioDucker::~AudioDucker() {
  StopDuckingOtherAudio();
}

bool AudioDucker::StartDuckingOtherAudio() {
  if (ducking_state_ == AudioDuckingState::kDucking) {
    return true;
  }
  if (!BindToAudioFocusManagerIfNecessary()) {
    return false;
  }
  StartDuckingImpl();
  ducking_state_ = AudioDuckingState::kDucking;
  return true;
}

bool AudioDucker::StopDuckingOtherAudio() {
  if (ducking_state_ == AudioDuckingState::kNoDucking) {
    return true;
  }
  if (!BindToAudioFocusManagerIfNecessary()) {
    return false;
  }
  audio_focus_remote_->StopDuckingAllAudio();

#if BUILDFLAG(IS_WIN)
  if (windows_ducker_) {
    windows_ducker_->StopDuckingOtherWindowsApplications();
  }
#endif  // BUILDFLAG(IS_WIN)

  ducking_state_ = AudioDuckingState::kNoDucking;
  return true;
}

void AudioDucker::MediaSessionCreated(content::MediaSession* session) {
  // When a MediaSession is created and we're already ducking, we need to tell
  // the AudioFocusManager to start ducking again while exempting the new
  // request ID. This will supersede the previous request and replace it with a
  // request that has an exempted MediaSession.
  if (ducking_state_ == AudioDuckingState::kDucking &&
      BindToAudioFocusManagerIfNecessary()) {
    StartDuckingImpl();
  }
}

void AudioDucker::StartDuckingImpl() {
  // Null base::UnguessableTokens cannot be passed via mojo, so convert to an
  // empty optional if the request ID is null.
  const base::UnguessableToken& request_id =
      content::MediaSession::GetRequestIdFromWebContents(GetWebContents());
  std::optional<base::UnguessableToken> optional_request_id;
  if (!request_id.is_empty()) {
    optional_request_id = request_id;
  }
  audio_focus_remote_->StartDuckingAllAudio(optional_request_id);

#if BUILDFLAG(IS_WIN)
  if (windows_ducker_) {
    windows_ducker_->StartDuckingOtherWindowsApplications();
  }
#endif  // BUILDFLAG(IS_WIN)
}

content::WebContents* AudioDucker::GetWebContents() const {
  return content::WebContents::FromRenderFrameHost(&page().GetMainDocument());
}

bool AudioDucker::BindToAudioFocusManagerIfNecessary() {
  if (audio_focus_remote_.is_bound()) {
    return true;
  }
  content::GetMediaSessionService().BindAudioFocusManager(
      audio_focus_remote_.BindNewPipeAndPassReceiver());
  audio_focus_remote_.reset_on_disconnect();
  return audio_focus_remote_.is_bound();
}

#if BUILDFLAG(IS_WIN)
bool AudioDucker::ShouldDuckProcess(base::ProcessId process_id) const {
  // If this audio session is associated with Chrome (either the browser
  // process or the audio process), then we don't want to duck it.
  if (process_id == base::GetCurrentProcId()) {
    return false;
  }

  base::ProcessId audio_service_process_id =
      content::GetProcessIdForAudioService();
  if (audio_service_process_id != base::kNullProcessId &&
      process_id == audio_service_process_id) {
    return false;
  }

  return true;
}
#endif  // BUILDFLAG(IS_WIN)

PAGE_USER_DATA_KEY_IMPL(AudioDucker);
