| // 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/system_media_controls_notifier.h" |
| |
| #include <memory> |
| #include <utility> |
| |
| #include "base/functional/bind.h" |
| #include "base/time/time.h" |
| #include "build/build_config.h" |
| #include "components/system_media_controls/system_media_controls.h" |
| #include "content/public/browser/content_browser_client.h" |
| #include "content/public/browser/media_session_service.h" |
| #include "content/public/common/content_client.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| #include "services/media_session/public/mojom/media_session.mojom.h" |
| #include "ui/gfx/image/image_skia.h" |
| |
| #if BUILDFLAG(IS_WIN) |
| #include "ui/base/idle/idle.h" |
| #endif // BUILDFLAG(IS_WIN) |
| |
| namespace content { |
| |
| using PlaybackStatus = |
| system_media_controls::SystemMediaControls::PlaybackStatus; |
| |
| const int kMinImageSize = 71; |
| const int kDesiredImageSize = 150; |
| |
| #if BUILDFLAG(IS_WIN) |
| constexpr base::TimeDelta kScreenLockPollInterval = base::Seconds(1); |
| constexpr int kHideSmtcDelaySeconds = 5; |
| constexpr base::TimeDelta kHideSmtcDelay = base::Seconds(kHideSmtcDelaySeconds); |
| #endif // BUILDFLAG(IS_WIN) |
| |
| constexpr base::TimeDelta kDebounceDelay = base::Milliseconds(10); |
| |
| SystemMediaControlsNotifier::SystemMediaControlsNotifier( |
| system_media_controls::SystemMediaControls* system_media_controls) |
| : system_media_controls_(system_media_controls) { |
| DCHECK(system_media_controls_); |
| |
| #if BUILDFLAG(IS_WIN) |
| lock_polling_timer_.Start( |
| FROM_HERE, kScreenLockPollInterval, |
| base::BindRepeating(&SystemMediaControlsNotifier::CheckLockState, |
| base::Unretained(this))); |
| #endif // BUILDFLAG(IS_WIN) |
| |
| // Connect to the MediaControllerManager and create a MediaController that |
| // controls the active session so we can observe it. |
| mojo::Remote<media_session::mojom::MediaControllerManager> controller_manager; |
| GetMediaSessionService().BindMediaControllerManager( |
| controller_manager.BindNewPipeAndPassReceiver()); |
| controller_manager->CreateActiveMediaController( |
| media_controller_.BindNewPipeAndPassReceiver()); |
| |
| // Observe the active media controller for changes to playback state and |
| // supported actions. |
| media_controller_->AddObserver( |
| media_controller_observer_receiver_.BindNewPipeAndPassRemote()); |
| |
| // Observe the active media controller for changes to provided artwork. |
| media_controller_->ObserveImages( |
| media_session::mojom::MediaSessionImageType::kArtwork, kMinImageSize, |
| kDesiredImageSize, |
| media_controller_image_observer_receiver_.BindNewPipeAndPassRemote()); |
| } |
| |
| SystemMediaControlsNotifier::~SystemMediaControlsNotifier() = default; |
| |
| void SystemMediaControlsNotifier::MediaSessionInfoChanged( |
| media_session::mojom::MediaSessionInfoPtr session_info_ptr) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| bool is_playing = false; |
| |
| session_info_ptr_ = std::move(session_info_ptr); |
| if (session_info_ptr_) { |
| is_playing = session_info_ptr_->playback_state == |
| media_session::mojom::MediaPlaybackState::kPlaying; |
| |
| DebouncePlaybackStatusUpdate(is_playing ? PlaybackStatus::kPlaying |
| : PlaybackStatus::kPaused); |
| } else { |
| system_media_controls_->SetPlaybackStatus(PlaybackStatus::kStopped); |
| |
| // These steps reference the Media Session Standard |
| // https://wicg.github.io/mediasession/#metadata |
| // 5.3.1 If the active media session is null, unset the media metadata |
| // presented to the platform, and terminate these steps. |
| ClearAllMetadata(); |
| } |
| |
| #if BUILDFLAG(IS_WIN) |
| if (screen_locked_) { |
| if (is_playing) { |
| StopHideSmtcTimer(); |
| } else if (!hide_smtc_timer_.IsRunning()) { |
| StartHideSmtcTimer(); |
| } |
| } |
| #endif // BUILDFLAG(IS_WIN) |
| } |
| |
| void SystemMediaControlsNotifier::MediaSessionMetadataChanged( |
| const absl::optional<media_session::MediaMetadata>& metadata) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (metadata.has_value()) { |
| // 5.3.3 Update the media metadata presented to the platform to match the |
| // metadata for the active media session. |
| DebounceMetadataUpdate(*metadata); |
| } else { |
| // 5.3.2 If the metadata of the active media session is an empty metadata, |
| // unset the media metadata presented to the platform. |
| ClearAllMetadata(); |
| } |
| } |
| |
| void SystemMediaControlsNotifier::MediaSessionActionsChanged( |
| const std::vector<media_session::mojom::MediaSessionAction>& actions) { |
| // SeekTo is not often supported so we will emulate "seekto" using |
| // "seekforward" and "seekbackward" if they exist. |
| bool seek_available = false; |
| for (const media_session::mojom::MediaSessionAction& action : actions) { |
| if (action == media_session::mojom::MediaSessionAction::kSeekTo || |
| action == media_session::mojom::MediaSessionAction::kSeekBackward || |
| action == media_session::mojom::MediaSessionAction::kSeekForward) { |
| seek_available = true; |
| break; |
| } |
| } |
| DebounceSetIsSeekToEnabled(seek_available); |
| } |
| |
| void SystemMediaControlsNotifier::MediaSessionChanged( |
| const absl::optional<base::UnguessableToken>& request_id) { |
| if (!request_id.has_value()) { |
| system_media_controls_->SetID(nullptr); |
| return; |
| } |
| auto string_id = request_id->ToString(); |
| system_media_controls_->SetID(&string_id); |
| } |
| |
| void SystemMediaControlsNotifier::MediaControllerImageChanged( |
| media_session::mojom::MediaSessionImageType type, |
| const SkBitmap& bitmap) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| DebounceIconUpdate(bitmap); |
| } |
| |
| void SystemMediaControlsNotifier::MediaSessionPositionChanged( |
| const absl::optional<media_session::MediaPosition>& position) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (position) { |
| DebouncePositionUpdate(*position); |
| } else { |
| ClearAllMetadata(); |
| } |
| } |
| |
| void SystemMediaControlsNotifier::DebouncePositionUpdate( |
| media_session::MediaPosition position) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| delayed_position_update_ = position; |
| |
| MaybeScheduleMetadataUpdate(); |
| } |
| |
| void SystemMediaControlsNotifier::DebounceMetadataUpdate( |
| media_session::MediaMetadata metadata) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| delayed_metadata_update_ = metadata; |
| |
| MaybeScheduleMetadataUpdate(); |
| } |
| |
| void SystemMediaControlsNotifier::DebouncePlaybackStatusUpdate( |
| system_media_controls::SystemMediaControls::PlaybackStatus |
| playback_status) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| delayed_playback_status_ = playback_status; |
| |
| MaybeScheduleMetadataUpdate(); |
| } |
| |
| void SystemMediaControlsNotifier::DebounceIconUpdate(const SkBitmap& bitmap) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| delayed_icon_update_ = bitmap; |
| |
| // Only update `delayed_icon_update_` once every kDebounceDelay. |
| if (!icon_update_timer_.IsRunning()) { |
| icon_update_timer_.Start( |
| FROM_HERE, kDebounceDelay, |
| base::BindOnce(&SystemMediaControlsNotifier::UpdateIcon, |
| base::Unretained(this))); |
| } |
| } |
| |
| void SystemMediaControlsNotifier::DebounceSetIsSeekToEnabled( |
| bool is_seek_to_enabled) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| delayed_is_seek_to_enabled_ = is_seek_to_enabled; |
| |
| // Only update `delayed_is_seek_to_enabled_` once every kDebounceDelay. |
| if (!actions_update_timer_.IsRunning()) { |
| auto update_seek_to_is_enabled = [](SystemMediaControlsNotifier* self) { |
| CHECK(self->delayed_is_seek_to_enabled_); |
| |
| self->system_media_controls_->SetIsSeekToEnabled( |
| *self->delayed_is_seek_to_enabled_); |
| |
| self->delayed_is_seek_to_enabled_ = absl::nullopt; |
| }; |
| |
| actions_update_timer_.Start( |
| FROM_HERE, kDebounceDelay, |
| base::BindOnce(std::move(update_seek_to_is_enabled), |
| base::Unretained(this))); |
| } |
| } |
| |
| void SystemMediaControlsNotifier::MaybeScheduleMetadataUpdate() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (metadata_update_timer_.IsRunning()) { |
| return; |
| } |
| |
| metadata_update_timer_.Start( |
| FROM_HERE, kDebounceDelay, |
| base::BindOnce(&SystemMediaControlsNotifier::UpdateMetadata, |
| base::Unretained(this))); |
| } |
| |
| void SystemMediaControlsNotifier::UpdateMetadata() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (delayed_position_update_) { |
| system_media_controls_->SetPosition(*delayed_position_update_); |
| delayed_position_update_ = absl::nullopt; |
| } |
| |
| if (delayed_metadata_update_) { |
| // If no title was provided, the title of the tab will be in the title |
| // property. |
| system_media_controls_->SetTitle(delayed_metadata_update_->title); |
| |
| // If no artist was provided, then the source URL will be in the artist |
| // property. |
| system_media_controls_->SetArtist(delayed_metadata_update_->artist); |
| |
| system_media_controls_->SetAlbum(delayed_metadata_update_->album); |
| |
| system_media_controls_->UpdateDisplay(); |
| delayed_metadata_update_ = absl::nullopt; |
| } |
| |
| if (delayed_playback_status_) { |
| system_media_controls_->SetPlaybackStatus(*delayed_playback_status_); |
| delayed_playback_status_ = absl::nullopt; |
| } |
| } |
| |
| void SystemMediaControlsNotifier::UpdateIcon() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| CHECK(delayed_icon_update_); |
| |
| if (!delayed_icon_update_->empty()) { |
| // 5.3.4.4.3 If the image format is supported, use the image as the artwork |
| // for display in the platform UI. Otherwise the fetch image algorithm fails |
| // and terminates. |
| system_media_controls_->SetThumbnail(*delayed_icon_update_); |
| } else { |
| // 5.3.4.2 If metadata's artwork is empty, terminate these steps. |
| // If no images are fetched in the fetch image algorithm, the user agent may |
| // have fallback behavior such as displaying a default image as artwork. |
| // We display the application icon if no artwork is provided. |
| absl::optional<gfx::ImageSkia> icon = |
| GetContentClient()->browser()->GetProductLogo(); |
| if (icon.has_value()) { |
| system_media_controls_->SetThumbnail(*icon->bitmap()); |
| } else { |
| system_media_controls_->ClearThumbnail(); |
| } |
| } |
| |
| delayed_icon_update_ = absl::nullopt; |
| } |
| |
| void SystemMediaControlsNotifier::ClearAllMetadata() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| metadata_update_timer_.Stop(); |
| |
| delayed_position_update_ = absl::nullopt; |
| delayed_metadata_update_ = absl::nullopt; |
| delayed_playback_status_ = absl::nullopt; |
| |
| system_media_controls_->ClearMetadata(); |
| } |
| |
| #if BUILDFLAG(IS_WIN) |
| void SystemMediaControlsNotifier::CheckLockState() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| bool new_state = ui::CheckIdleStateIsLocked(); |
| if (screen_locked_ == new_state) |
| return; |
| |
| screen_locked_ = new_state; |
| if (screen_locked_) |
| OnScreenLocked(); |
| else |
| OnScreenUnlocked(); |
| } |
| |
| void SystemMediaControlsNotifier::OnScreenLocked() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // If media is currently playing, don't hide the SMTC. |
| if (session_info_ptr_ && |
| session_info_ptr_->playback_state == |
| media_session::mojom::MediaPlaybackState::kPlaying) { |
| return; |
| } |
| |
| // Otherwise, hide them. |
| system_media_controls_->SetEnabled(false); |
| } |
| |
| void SystemMediaControlsNotifier::OnScreenUnlocked() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| StopHideSmtcTimer(); |
| system_media_controls_->SetEnabled(true); |
| } |
| |
| void SystemMediaControlsNotifier::StartHideSmtcTimer() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| hide_smtc_timer_.Start( |
| FROM_HERE, kHideSmtcDelay, |
| base::BindOnce(&SystemMediaControlsNotifier::HideSmtcTimerFired, |
| base::Unretained(this))); |
| } |
| |
| void SystemMediaControlsNotifier::StopHideSmtcTimer() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| hide_smtc_timer_.Stop(); |
| } |
| |
| void SystemMediaControlsNotifier::HideSmtcTimerFired() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| system_media_controls_->SetEnabled(false); |
| } |
| #endif // BUILDFLAG(IS_WIN) |
| |
| } // namespace content |