blob: 9e089dbc9f6b31bb059823a082992c668124a68c [file] [log] [blame]
// Copyright 2019 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/active_media_session_controller.h"
#include <utility>
#include <vector>
#include "base/containers/contains.h"
#include "base/metrics/histogram_macros.h"
#include "base/unguessable_token.h"
#include "content/browser/browser_main_loop.h"
#include "content/browser/media/media_keys_listener_manager_impl.h"
#include "content/public/browser/media_keys_listener_manager.h"
#include "content/public/browser/media_session_service.h"
#include "services/media_session/public/mojom/media_session.mojom.h"
#include "ui/base/accelerators/accelerator.h"
#include "ui/base/accelerators/media_keys_util.h"
namespace content {
using media_session::mojom::MediaSessionAction;
ActiveMediaSessionController::ActiveMediaSessionController(
base::UnguessableToken request_id)
: request_id_(request_id) {
// Connect to the MediaControllerManager and create a MediaController that
// controls the session given by `request_id`.
GetMediaSessionService().BindMediaControllerManager(
controller_manager_remote_.BindNewPipeAndPassReceiver());
if (request_id == base::UnguessableToken::Null()) {
// Create a controller that automatically follows the active session.
controller_manager_remote_->CreateActiveMediaController(
media_controller_remote_.BindNewPipeAndPassReceiver());
} else {
// Create a controller for the media session with ID `request_id`.
controller_manager_remote_->CreateMediaControllerForSession(
media_controller_remote_.BindNewPipeAndPassReceiver(), request_id);
}
// Observe the media controller for changes to playback state and
// supported actions.
media_controller_remote_->AddObserver(
media_controller_observer_receiver_.BindNewPipeAndPassRemote());
}
ActiveMediaSessionController::~ActiveMediaSessionController() = default;
void ActiveMediaSessionController::MediaSessionInfoChanged(
media_session::mojom::MediaSessionInfoPtr session_info) {
MediaKeysListenerManagerImpl* media_keys_listener_manager_impl =
BrowserMainLoop::GetInstance()->media_keys_listener_manager();
DCHECK(media_keys_listener_manager_impl);
session_info_ = std::move(session_info);
media_keys_listener_manager_impl->SetIsMediaPlaying(
session_info_ && session_info_->playback_state ==
media_session::mojom::MediaPlaybackState::kPlaying);
}
void ActiveMediaSessionController::MediaSessionActionsChanged(
const std::vector<MediaSessionAction>& actions) {
MediaKeysListenerManager* media_keys_listener_manager =
MediaKeysListenerManager::GetInstance();
DCHECK(media_keys_listener_manager);
// Stop listening to any keys that are currently being watched, but aren't in
// |actions|.
// This loop is what tells the media keys listener manager to stop watching
// next/previous when a new tab is active because next/previous are in
// actions_ but NOT in |actions|.
for (const MediaSessionAction& action : actions_) {
std::optional<ui::KeyboardCode> action_key_code =
MediaSessionActionToKeyCode(action);
if (!action_key_code.has_value())
continue;
if (!base::Contains(actions, action))
media_keys_listener_manager->StopWatchingMediaKey(*action_key_code, this,
request_id_);
}
// Populate |actions_| with the new MediaSessionActions and start listening
// to necessary media keys.
actions_.clear();
for (const MediaSessionAction& action : actions) {
std::optional<ui::KeyboardCode> action_key_code =
MediaSessionActionToKeyCode(action);
if (action_key_code.has_value()) {
// It's okay to call this even on keys we're already listening to, since
// it's a no-op in that case.
if (media_keys_listener_manager->StartWatchingMediaKey(
*action_key_code, this, request_id_)) {
actions_.insert(action);
}
} else {
// If there is no media key associated with this action, then just add it
// to the list of actions we listen to (since we can receive certain
// non-key actions like SeekTo).
actions_.insert(action);
}
}
}
void ActiveMediaSessionController::MediaSessionPositionChanged(
const std::optional<media_session::MediaPosition>& position) {
position_ = position;
}
void ActiveMediaSessionController::FlushForTesting() {
media_controller_remote_.FlushForTesting(); // IN-TEST
}
void ActiveMediaSessionController::OnMediaKeysAccelerator(
const ui::Accelerator& accelerator) {
// Ignore key released events.
if (accelerator.key_state() == ui::Accelerator::KeyState::RELEASED)
return;
MaybePerformAction(KeyCodeToMediaSessionAction(accelerator.key_code()));
}
void ActiveMediaSessionController::OnNext() {
MaybePerformAction(MediaSessionAction::kNextTrack);
}
void ActiveMediaSessionController::OnPrevious() {
MaybePerformAction(MediaSessionAction::kPreviousTrack);
}
void ActiveMediaSessionController::OnPlay() {
MaybePerformAction(MediaSessionAction::kPlay);
}
void ActiveMediaSessionController::OnPause() {
MaybePerformAction(MediaSessionAction::kPause);
}
void ActiveMediaSessionController::OnPlayPause() {
if (session_info_ && session_info_->playback_state ==
media_session::mojom::MediaPlaybackState::kPlaying) {
MaybePerformAction(MediaSessionAction::kPause);
return;
}
MaybePerformAction(MediaSessionAction::kPlay);
}
void ActiveMediaSessionController::OnStop() {
MaybePerformAction(MediaSessionAction::kStop);
}
void ActiveMediaSessionController::OnSeek(const base::TimeDelta& time) {
media_controller_remote_->Seek(time);
}
void ActiveMediaSessionController::OnSeekTo(const base::TimeDelta& time) {
if (base::Contains(actions_,
media_session::mojom::MediaSessionAction::kSeekTo)) {
media_controller_remote_->SeekTo(time);
} else if (position_) {
auto time_diff =
time - position_->GetPositionAtTime(base::TimeTicks::Now());
media_controller_remote_->Seek(time_diff);
}
}
void ActiveMediaSessionController::MaybePerformAction(
MediaSessionAction action) {
// Ignore if we don't support the action.
if (!SupportsAction(action))
return;
PerformAction(action);
}
bool ActiveMediaSessionController::SupportsAction(
MediaSessionAction action) const {
return actions_.contains(action);
}
void ActiveMediaSessionController::PerformAction(MediaSessionAction action) {
DCHECK(SupportsAction(action));
switch (action) {
case MediaSessionAction::kPreviousTrack:
media_controller_remote_->PreviousTrack();
ui::RecordMediaHardwareKeyAction(
ui::MediaHardwareKeyAction::kPreviousTrack);
return;
case MediaSessionAction::kPlay:
media_controller_remote_->Resume();
ui::RecordMediaHardwareKeyAction(ui::MediaHardwareKeyAction::kPlay);
return;
case MediaSessionAction::kPause:
media_controller_remote_->Suspend();
ui::RecordMediaHardwareKeyAction(ui::MediaHardwareKeyAction::kPause);
return;
case MediaSessionAction::kNextTrack:
media_controller_remote_->NextTrack();
ui::RecordMediaHardwareKeyAction(ui::MediaHardwareKeyAction::kNextTrack);
return;
case MediaSessionAction::kStop:
media_controller_remote_->Stop();
ui::RecordMediaHardwareKeyAction(ui::MediaHardwareKeyAction::kStop);
return;
case MediaSessionAction::kSeekBackward:
case MediaSessionAction::kSeekForward:
case MediaSessionAction::kSkipAd:
case MediaSessionAction::kSeekTo:
case MediaSessionAction::kScrubTo:
case MediaSessionAction::kEnterPictureInPicture:
case MediaSessionAction::kExitPictureInPicture:
case MediaSessionAction::kSwitchAudioDevice:
case MediaSessionAction::kToggleMicrophone:
case MediaSessionAction::kToggleCamera:
case MediaSessionAction::kHangUp:
case MediaSessionAction::kRaise:
case MediaSessionAction::kSetMute:
case MediaSessionAction::kPreviousSlide:
case MediaSessionAction::kNextSlide:
case MediaSessionAction::kEnterAutoPictureInPicture:
NOTREACHED();
}
}
MediaSessionAction ActiveMediaSessionController::KeyCodeToMediaSessionAction(
ui::KeyboardCode key_code) const {
switch (key_code) {
case ui::KeyboardCode::VKEY_MEDIA_PLAY_PAUSE:
if (session_info_ &&
session_info_->playback_state ==
media_session::mojom::MediaPlaybackState::kPlaying) {
return MediaSessionAction::kPause;
}
return MediaSessionAction::kPlay;
case ui::KeyboardCode::VKEY_MEDIA_STOP:
return MediaSessionAction::kStop;
case ui::KeyboardCode::VKEY_MEDIA_NEXT_TRACK:
return MediaSessionAction::kNextTrack;
case ui::KeyboardCode::VKEY_MEDIA_PREV_TRACK:
return MediaSessionAction::kPreviousTrack;
default:
NOTREACHED();
}
}
std::optional<ui::KeyboardCode>
ActiveMediaSessionController::MediaSessionActionToKeyCode(
MediaSessionAction action) const {
switch (action) {
case MediaSessionAction::kPlay:
case MediaSessionAction::kPause:
return ui::KeyboardCode::VKEY_MEDIA_PLAY_PAUSE;
case MediaSessionAction::kStop:
return ui::KeyboardCode::VKEY_MEDIA_STOP;
case MediaSessionAction::kNextTrack:
return ui::KeyboardCode::VKEY_MEDIA_NEXT_TRACK;
case MediaSessionAction::kPreviousTrack:
return ui::KeyboardCode::VKEY_MEDIA_PREV_TRACK;
case MediaSessionAction::kSeekBackward:
case MediaSessionAction::kSeekForward:
case MediaSessionAction::kSkipAd:
case MediaSessionAction::kSeekTo:
case MediaSessionAction::kScrubTo:
case MediaSessionAction::kEnterPictureInPicture:
case MediaSessionAction::kExitPictureInPicture:
case MediaSessionAction::kSwitchAudioDevice:
case MediaSessionAction::kToggleMicrophone:
case MediaSessionAction::kToggleCamera:
case MediaSessionAction::kHangUp:
case MediaSessionAction::kRaise:
case MediaSessionAction::kSetMute:
case MediaSessionAction::kPreviousSlide:
case MediaSessionAction::kNextSlide:
case MediaSessionAction::kEnterAutoPictureInPicture:
return std::nullopt;
}
}
} // namespace content