chromium/content/browser/media/web_app_system_media_controls_manager.cc

// 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 "content/browser/media/web_app_system_media_controls_manager.h"

#include "base/functional/callback.h"
#include "base/metrics/histogram_functions.h"
#include "components/system_media_controls/system_media_controls.h"
#include "content/browser/browser_main_loop.h"
#include "content/browser/media/media_keys_listener_manager_impl.h"
#include "content/browser/media/system_media_controls_notifier.h"
#include "content/browser/media/web_app_system_media_controls.h"
#include "content/public/browser/media_session.h"
#include "content/public/browser/media_session_service.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_delegate.h"
#include "content/public/common/content_client.h"
#include "media/audio/audio_manager.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "services/media_session/public/mojom/audio_focus.mojom.h"
#include "services/media_session/public/mojom/media_controller.mojom.h"
#include "ui/gfx/native_widget_types.h"

#if BUILDFLAG(IS_MAC)
#include "components/remote_cocoa/browser/application_host.h"
#endif  // BUILDFLAG(IS_MAC)

#if BUILDFLAG(IS_WIN)
#include "ui/aura/window.h"
#include "ui/aura/window_tree_host.h"
#endif  // BUILDFLAG(IS_WIN)

namespace {

#if BUILDFLAG(IS_MAC)
remote_cocoa::ApplicationHost* GetApplicationHostFromWebContents(
    content::WebContents* web_contents) {
  // Get the ApplicationHost (ie. the browser-side component corresponding to
  // the NSApplication running in an app shim process) for the web contents.
  return remote_cocoa::ApplicationHost::GetForNativeView(
      web_contents ? web_contents->GetNativeView() : gfx::NativeView());
}
#endif  // BUILDFLAG(IS_MAC)

#if BUILDFLAG(IS_WIN)
intptr_t GetHWNDFromWebContents(content::WebContents* web_contents) {
  // Get the HWND for the window containing the web contents (Recreation
  // of HWNDForNativeView).
  gfx::NativeView native_view = web_contents->GetNativeView();
  if (native_view && native_view->GetRootWindow()) {
    return reinterpret_cast<intptr_t>(
        native_view->GetHost()->GetAcceleratedWidget());
  }
  return -1;
}
#endif  // BUILDFLAG(IS_WIN)

}  // namespace

namespace content {

WebAppSystemMediaControlsManager::WebAppSystemMediaControlsManager() = default;

WebAppSystemMediaControlsManager::~WebAppSystemMediaControlsManager() = default;

void WebAppSystemMediaControlsManager::Init() {
  CHECK(initialized_ == false);
  initialized_ = true;
  CHECK(!audio_focus_manager_.is_bound());
  CHECK(!audio_focus_observer_receiver_.is_bound());
  if (skip_mojo_connection_for_testing_) {
    return;
  }
  TryConnectToAudioFocusManager();
}

void WebAppSystemMediaControlsManager::TryConnectToAudioFocusManager() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  CHECK(!audio_focus_manager_.is_bound());

  // Bind our remote AudioFocusManager endpoint
  GetMediaSessionService().BindAudioFocusManager(
      audio_focus_manager_.BindNewPipeAndPassReceiver());
  // Set error handler
  audio_focus_manager_.set_disconnect_handler(base::BindOnce(
      &WebAppSystemMediaControlsManager::OnMojoError, base::Unretained(this)));

  CHECK(!audio_focus_observer_receiver_.is_bound());
  // Bind our receiver AudioFocusObserver endpoint and register it as an
  // observer of our remote AudioFocusManager endpoint bound above.
  audio_focus_manager_->AddObserver(
      audio_focus_observer_receiver_.BindNewPipeAndPassRemote());

  audio_focus_observer_receiver_.set_disconnect_handler(base::BindOnce(
      &WebAppSystemMediaControlsManager::OnMojoError, base::Unretained(this)));
}

void WebAppSystemMediaControlsManager::OnMojoError() {
  audio_focus_manager_.reset();
  audio_focus_observer_receiver_.reset();
}

// AudioFocusObserver overrides
void WebAppSystemMediaControlsManager::OnFocusGained(
    media_session::mojom::AudioFocusRequestStatePtr state) {
  CHECK(initialized_);
  const std::optional<base::UnguessableToken>& maybe_id = state->request_id;
  if (!maybe_id.has_value()) {
    return;
  }

  base::UnguessableToken request_id = maybe_id.value();
  DVLOG(1) << "WebAppSystemMediaControlsManager::OnFocusGained, "
              "request id = "
           << request_id;

  // Get the web contents associated with the request_id
  content::WebContents* web_contents =
      MediaSession::GetWebContentsFromRequestId(request_id);

  WebAppSystemMediaControls* existing_controls = nullptr;

  // It's possible no web contents is returned if the web contents
  // has been destroyed.
  if (!web_contents) {
    DVLOG(1) << "WebAppSystemMediaControlsManager::OnFocusGained received "
                "destroyed web contents";
    return;
  }

  // Check if the web contents found is in a dPWA. Occasionally, we've found it
  // is possible that the web contents does not have a delegate - we should just
  // abort in that scenario.
  WebContentsDelegate* web_contents_delegate = web_contents->GetDelegate();
  if (!web_contents_delegate) {
    return;
  }

  bool is_web_contents_for_web_app =
      web_contents_delegate->ShouldUseInstancedSystemMediaControls() ||
      always_assume_web_app_for_testing_;
  if (!is_web_contents_for_web_app) {
    // Non-webapp updates are handled by media_keys_listener_manager_impl and do
    // not need any intervention from us here.
    return;
  }

  // At this point, we know this web contents is for a dPWA.
  // See if controls already exists for this request id.
  existing_controls = GetControlsForRequestId(request_id);

  // It's also the right time to fire telemetry that a PWA session is playing
  // audio since we know it's not a browser.
  base::UmaHistogramEnumeration(
      "WebApp.Media.SystemMediaControls",
      WebAppSystemMediaControlsEvent::kPwaPlayingMedia);

  // if the controls don't exist, we need to make an SMC and the
  // controls object.
  if (!existing_controls) {
#if BUILDFLAG(IS_WIN)
    // `window` is -1 if no HWND found.
    intptr_t window = GetHWNDFromWebContents(web_contents);
    std::unique_ptr<system_media_controls::SystemMediaControls>
        system_media_controls =
            system_media_controls::SystemMediaControls::Create(
                media::AudioManager::GetGlobalAppName(), window);
#else
    remote_cocoa::ApplicationHost* application_host =
        GetApplicationHostFromWebContents(web_contents);

    std::unique_ptr<system_media_controls::SystemMediaControls>
        system_media_controls =
            system_media_controls::SystemMediaControls::Create(
                application_host);

    if (on_system_media_controls_bridge_created_callback_for_testing_) {
      system_media_controls->SetOnBridgeCreatedCallbackForTesting(
          on_system_media_controls_bridge_created_callback_for_testing_);
    }
#endif  // BUILDFLAG(IS_WIN)

    if (!system_media_controls) {
      DVLOG(1) << "WebAppSystemMediaControlsManager::OnFocusGained, "
               << "failed to create smc.";
      return;
    }

    // The global MKLM should be set as an observer for the newly created
    // SystemMediaControls object.
    system_media_controls->AddObserver(
        BrowserMainLoop::GetInstance()->media_keys_listener_manager());

    controls_map_.emplace(
        request_id,
        std::make_unique<WebAppSystemMediaControls>(
            request_id, std::move(system_media_controls),
            std::make_unique<SystemMediaControlsNotifier>(
                system_media_controls.get(), request_id),
            std::make_unique<ActiveMediaSessionController>(request_id)));

    if (test_observer_) {
      test_observer_->OnWebAppAdded(request_id);
    }
  } else {
    // If the requestID already exists in the map, we still need to rebind
    // the notifier and controller as they have been invalidated.
    // We observe this behavior for example, when triggering 'next track'
    // which causes onFocusGained to fire but the notifier and controller
    // cannot be reused afterwards.
    existing_controls->SetNotifier(
        std::make_unique<SystemMediaControlsNotifier>(
            existing_controls->GetSystemMediaControls(), request_id));
    existing_controls->SetController(
        std::make_unique<ActiveMediaSessionController>(request_id));
  }
}

void WebAppSystemMediaControlsManager::OnFocusLost(
    media_session::mojom::AudioFocusRequestStatePtr state) {
  CHECK(initialized_);

  if (!state->request_id) {
    return;
  }

  auto it = controls_map_.find(state->request_id.value());

  // There will be no entry if it was a browser session that lost focus.
  if (it == controls_map_.end()) {
    return;
  }

  // Tell the OS that audio stopped and to hide the UI.

  // For the browser, the SystemMediaControlsNotifier automatically follows the
  // "active" media session. However, because all PWA media controls are
  // associated with a specific media session, they don't receive the same
  // metadata updates. (crbug/326411160 for more information why).
  // Instead, `this` receives a FocusLost updates via AudioFocusObserver, and we
  // must then do the OS UI cleanup ourselves.

  // Because SystemMediaControlsNotifier keeps internal timers/logic to debounce
  // metadata updates, we leverage the existing logic there by directly calling
  // MediaSessionInfoChanged (a MediaControllerObserver function)
  // with empty information to force the SMCNotifier to take the normal cleanup
  // path to hide the OS UI and stop all running debounce timers. (Although
  // `state` has a session_info field, we can't use that because it will have
  // information. we need to pass an empty information so
  // MediaSessionInfoChanged will think a track ended and take the cleanup
  // route)
  content::SystemMediaControlsNotifier* notifier = it->second->GetNotifier();
  media_session::mojom::MediaSessionInfoPtr empty_info;
  notifier->MediaSessionInfoChanged(std::move(empty_info));
}

void WebAppSystemMediaControlsManager::OnRequestIdReleased(
    const base::UnguessableToken& request_id) {
  CHECK(initialized_);

  auto it = controls_map_.find(request_id);
  if (it == controls_map_.end()) {
    return;
  }

  // the controls_map_ holds unique_ptr so most of the destruction will happen
  // automatically.
  controls_map_.erase(request_id);
}

WebAppSystemMediaControls*
WebAppSystemMediaControlsManager::GetControlsForRequestId(
    base::UnguessableToken request_id) {
  CHECK(initialized_);
  auto it = controls_map_.find(request_id);
  if (it != controls_map_.end()) {
    return it->second.get();
  } else {
    return nullptr;
  }
}

WebAppSystemMediaControls* WebAppSystemMediaControlsManager::
    GetWebAppSystemMediaControlsForSystemMediaControls(
        system_media_controls::SystemMediaControls* system_media_controls) {
  CHECK(initialized_);
  for (auto& it : controls_map_) {
    WebAppSystemMediaControls* curr_controls = it.second.get();
    if (curr_controls->GetSystemMediaControls() == system_media_controls) {
      return curr_controls;
    }
  }
  return nullptr;
}

std::vector<WebAppSystemMediaControls*>
WebAppSystemMediaControlsManager::GetAllControls() {
  CHECK(initialized_);
  std::vector<WebAppSystemMediaControls*> vec;

  for (auto& it : controls_map_) {
    vec.push_back(it.second.get());
  }

  return vec;
}

void WebAppSystemMediaControlsManager::
    SetOnSystemMediaControlsBridgeCreatedCallbackForTesting(
        base::RepeatingCallback<void()> callback) {
  on_system_media_controls_bridge_created_callback_for_testing_ =
      std::move(callback);
}

void WebAppSystemMediaControlsManager::LogDataForDebugging() {
  DVLOG(1) << "WebAppSystemMediaControlsManager::LogDataForDebugging";
  int i = 0;
  for (auto& it : controls_map_) {
    DVLOG(1) << "Entry " << ++i << " "
             << "Request ID: " << it.first;

    if (it.second->GetSystemMediaControls()) {
      DVLOG(1) << "SystemMediaControls: "
               << it.second->GetSystemMediaControls();
    } else {
      DVLOG(1) << "SystemMediaControls: nullptr";
    }

    if (it.second->GetNotifier()) {
      DVLOG(1) << "Notifier: " << it.second->GetNotifier();
    } else {
      DVLOG(1) << "Notifier: nullptr";
    }

    if (it.second->GetController()) {
      DVLOG(1) << "Controller: " << it.second->GetController();
    } else {
      DVLOG(1) << "Controller: nullptr";
    }
  }
}

}  // namespace content