| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "fuchsia_web/webengine/browser/media_player_impl.h" |
| |
| #include <lib/async/default.h> |
| |
| #include "base/fuchsia/fuchsia_logging.h" |
| #include "base/logging.h" |
| #include "base/strings/string_piece.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/time/time.h" |
| #include "content/public/browser/media_session.h" |
| #include "services/media_session/public/mojom/constants.mojom.h" |
| |
| namespace { |
| |
| fuchsia_media_sessions2::PlayerCapabilityFlags ActionToCapabilityFlag( |
| media_session::mojom::MediaSessionAction action) { |
| using MediaSessionAction = media_session::mojom::MediaSessionAction; |
| using PlayerCapabilityFlags = fuchsia_media_sessions2::PlayerCapabilityFlags; |
| switch (action) { |
| case MediaSessionAction::kEnterPictureInPicture: |
| case MediaSessionAction::kExitPictureInPicture: |
| return {}; // PlayerControl does not support picture-in-picture. |
| case MediaSessionAction::kPlay: |
| return PlayerCapabilityFlags::kPlay; |
| case MediaSessionAction::kPause: |
| return PlayerCapabilityFlags::kPause; |
| case MediaSessionAction::kPreviousTrack: |
| return PlayerCapabilityFlags::kChangeToPrevItem; |
| case MediaSessionAction::kNextTrack: |
| return PlayerCapabilityFlags::kChangeToNextItem; |
| case MediaSessionAction::kSeekBackward: |
| return PlayerCapabilityFlags::kSkipReverse; |
| case MediaSessionAction::kSeekForward: |
| return PlayerCapabilityFlags::kSkipForward; |
| case MediaSessionAction::kSeekTo: |
| return PlayerCapabilityFlags::kSeek; |
| case MediaSessionAction::kScrubTo: |
| return {}; // PlayerControl does not support scrub-to. |
| case MediaSessionAction::kSkipAd: |
| return {}; // PlayerControl does not support skipping ads. |
| case MediaSessionAction::kStop: |
| return {}; // PlayerControl assumes that stop is always supported. |
| case MediaSessionAction::kSwitchAudioDevice: |
| return {}; // PlayerControl does not support switching audio device. |
| case MediaSessionAction::kToggleMicrophone: |
| return {}; // PlayerControl does not support toggling microphone. |
| case MediaSessionAction::kToggleCamera: |
| return {}; // PlayerControl does not support toggling camera. |
| case MediaSessionAction::kHangUp: |
| return {}; // PlayerControl does not support hanging up. |
| case MediaSessionAction::kRaise: |
| return {}; // PlayerControl does not support raising. |
| case MediaSessionAction::kSetMute: |
| return {}; // TODO(crbug.com/1240811): implement set mute. |
| case MediaSessionAction::kPreviousSlide: |
| return {}; // PlayerControl does not support going back to previous |
| // slide. |
| case MediaSessionAction::kNextSlide: |
| return {}; // PlayerControl does not support going to next slide. |
| case MediaSessionAction::kEnterAutoPictureInPicture: |
| return {}; // PlayerControl does not support picture-in-picture. |
| } |
| } |
| |
| void AddMetadata(base::StringPiece label, |
| base::StringPiece16 value, |
| fuchsia_media::Metadata* metadata) { |
| fuchsia_media::Property property{ |
| {.label{label}, .value{base::UTF16ToUTF8(value)}}}; |
| metadata->properties().emplace_back(std::move(property)); |
| } |
| |
| fuchsia_media_sessions2::PlayerState SessionStateToPlayerState( |
| media_session::mojom::MediaSessionInfo::SessionState state) { |
| switch (state) { |
| case media_session::mojom::MediaSessionInfo::SessionState::kActive: |
| case media_session::mojom::MediaSessionInfo::SessionState::kDucking: |
| return fuchsia_media_sessions2::PlayerState::kPlaying; |
| case media_session::mojom::MediaSessionInfo::SessionState::kInactive: |
| return fuchsia_media_sessions2::PlayerState::kIdle; |
| case media_session::mojom::MediaSessionInfo::SessionState::kSuspended: |
| return fuchsia_media_sessions2::PlayerState::kPaused; |
| }; |
| } |
| |
| } // namespace |
| |
| MediaPlayerImpl::MediaPlayerImpl( |
| content::MediaSession* media_session, |
| fidl::ServerEnd<fuchsia_media_sessions2::Player> server_end, |
| base::OnceClosure on_disconnect) |
| : media_session_(media_session), |
| on_disconnect_(std::move(on_disconnect)), |
| binding_(async_get_default_dispatcher(), |
| std::move(server_end), |
| this, |
| fit::bind_member(this, &MediaPlayerImpl::OnBindingClosure)), |
| observer_receiver_(this) { |
| // Set default values for some fields in |pending_info_delta_|, which some |
| // clients may otherwise use incorrect defaults for. |
| pending_info_delta_.local(true); |
| } |
| |
| MediaPlayerImpl::~MediaPlayerImpl() { |
| if (pending_info_change_callback_) { |
| pending_info_change_callback_->Close(ZX_ERR_PEER_CLOSED); |
| } |
| } |
| |
| void MediaPlayerImpl::WatchInfoChange( |
| MediaPlayerImpl::WatchInfoChangeCompleter::Sync& completer) { |
| if (pending_info_change_callback_) { |
| ReportErrorAndDisconnect(ZX_ERR_BAD_STATE); |
| return; |
| } |
| |
| pending_info_change_callback_ = completer.ToAsync(); |
| |
| if (!observer_receiver_.is_bound()) { |
| // |media_session| will notify us via our MediaSessionObserver interface |
| // of the current state of the session (metadata, actions, etc) in response |
| // to AddObserver(). |
| media_session_->AddObserver(observer_receiver_.BindNewPipeAndPassRemote()); |
| } |
| |
| MaybeSendPlayerInfoDelta(); |
| } |
| |
| void MediaPlayerImpl::Play( |
| MediaPlayerImpl::PlayCompleter::Sync& ignored_completer) { |
| media_session_->Resume(content::MediaSession::SuspendType::kUI); |
| } |
| |
| void MediaPlayerImpl::Pause( |
| MediaPlayerImpl::PauseCompleter::Sync& ignored_completer) { |
| media_session_->Suspend(content::MediaSession::SuspendType::kUI); |
| } |
| |
| void MediaPlayerImpl::Stop( |
| MediaPlayerImpl::StopCompleter::Sync& ignored_completer) { |
| media_session_->Suspend(content::MediaSession::SuspendType::kUI); |
| } |
| |
| void MediaPlayerImpl::Seek( |
| MediaPlayerImpl::SeekRequest& request, |
| MediaPlayerImpl::SeekCompleter::Sync& ignored_completer) { |
| media_session_->SeekTo(base::TimeDelta::FromZxDuration(request.position())); |
| } |
| |
| void MediaPlayerImpl::SkipForward( |
| MediaPlayerImpl::SkipForwardCompleter::Sync& ignored_completer) { |
| media_session_->Seek( |
| base::Seconds(media_session::mojom::kDefaultSeekTimeSeconds)); |
| } |
| |
| void MediaPlayerImpl::SkipReverse( |
| MediaPlayerImpl::SkipReverseCompleter::Sync& ignored_completer) { |
| media_session_->Seek( |
| -base::Seconds(media_session::mojom::kDefaultSeekTimeSeconds)); |
| } |
| |
| void MediaPlayerImpl::NextItem( |
| MediaPlayerImpl::NextItemCompleter::Sync& ignored_completer) { |
| media_session_->NextTrack(); |
| } |
| |
| void MediaPlayerImpl::PrevItem( |
| MediaPlayerImpl::PrevItemCompleter::Sync& ignored_completer) { |
| media_session_->PreviousTrack(); |
| } |
| |
| void MediaPlayerImpl::SetPlaybackRate( |
| MediaPlayerImpl::SetPlaybackRateRequest& request, |
| MediaPlayerImpl::SetPlaybackRateCompleter::Sync& ignored_completer) { |
| // content::MediaSession does not support changes to playback rate. |
| NOTIMPLEMENTED_LOG_ONCE(); |
| } |
| |
| void MediaPlayerImpl::SetRepeatMode( |
| MediaPlayerImpl::SetRepeatModeRequest& request, |
| MediaPlayerImpl::SetRepeatModeCompleter::Sync& ignored_completer) { |
| // content::MediaSession does not provide control over repeat playback. |
| NOTIMPLEMENTED_LOG_ONCE(); |
| } |
| |
| void MediaPlayerImpl::SetShuffleMode( |
| MediaPlayerImpl::SetShuffleModeRequest& request, |
| MediaPlayerImpl::SetShuffleModeCompleter::Sync& ignored_completer) { |
| // content::MediaSession does not provide control over item playback order. |
| NOTIMPLEMENTED_LOG_ONCE(); |
| } |
| |
| void MediaPlayerImpl::BindVolumeControl( |
| MediaPlayerImpl::BindVolumeControlRequest& request, |
| MediaPlayerImpl::BindVolumeControlCompleter::Sync& ignored_completer) { |
| // content::MediaSession does not provide control over audio gain. |
| request.volume_control_request().Close(ZX_ERR_NOT_SUPPORTED); |
| } |
| |
| void MediaPlayerImpl::MediaSessionInfoChanged( |
| media_session::mojom::MediaSessionInfoPtr info) { |
| fuchsia_media_sessions2::PlayerStatus status{{ |
| .player_state = SessionStateToPlayerState(info->state), |
| }}; |
| pending_info_delta_.player_status(std::move(status)); |
| MaybeSendPlayerInfoDelta(); |
| } |
| |
| void MediaPlayerImpl::MediaSessionMetadataChanged( |
| const absl::optional<media_session::MediaMetadata>& metadata_mojo) { |
| fuchsia_media::Metadata metadata; |
| if (metadata_mojo) { |
| AddMetadata(fuchsia_media::kMetadataLabelTitle, metadata_mojo->title, |
| &metadata); |
| AddMetadata(fuchsia_media::kMetadataLabelArtist, metadata_mojo->artist, |
| &metadata); |
| AddMetadata(fuchsia_media::kMetadataLabelAlbum, metadata_mojo->album, |
| &metadata); |
| AddMetadata(fuchsia_media::kMetadataSourceTitle, |
| metadata_mojo->source_title, &metadata); |
| } |
| pending_info_delta_.metadata(std::move(metadata)); |
| MaybeSendPlayerInfoDelta(); |
| } |
| |
| void MediaPlayerImpl::MediaSessionActionsChanged( |
| const std::vector<media_session::mojom::MediaSessionAction>& actions) { |
| // TODO(https://crbug.com/879317): Implement PROVIDE_BITMAPS. |
| fuchsia_media_sessions2::PlayerCapabilityFlags capability_flags{}; |
| for (auto action : actions) |
| capability_flags |= ActionToCapabilityFlag(action); |
| pending_info_delta_.player_capabilities( |
| fuchsia_media_sessions2::PlayerCapabilities{ |
| {.flags = std::move(capability_flags)}}); |
| MaybeSendPlayerInfoDelta(); |
| } |
| |
| void MediaPlayerImpl::MediaSessionImagesChanged( |
| const base::flat_map<media_session::mojom::MediaSessionImageType, |
| std::vector<media_session::MediaImage>>& images) { |
| // TODO(https://crbug.com/879317): Implement image-changed. |
| NOTIMPLEMENTED_LOG_ONCE(); |
| } |
| |
| void MediaPlayerImpl::MediaSessionPositionChanged( |
| const absl::optional<media_session::MediaPosition>& position) { |
| // TODO(https://crbug.com/879317): Implement media position changes. |
| NOTIMPLEMENTED_LOG_ONCE(); |
| } |
| |
| void MediaPlayerImpl::MaybeSendPlayerInfoDelta() { |
| if (!pending_info_change_callback_) |
| return; |
| if (pending_info_delta_.IsEmpty()) |
| return; |
| // std::exchange(foo, {}) returns the contents of |foo|, while ensuring that |
| // |foo| is reset to the initial/empty state. |
| std::exchange(pending_info_change_callback_, absl::nullopt) |
| ->Reply(std::exchange(pending_info_delta_, {})); |
| } |
| |
| void MediaPlayerImpl::OnBindingClosure(fidl::UnbindInfo info) { |
| ZX_LOG_IF(ERROR, info.status() != ZX_ERR_PEER_CLOSED, info.status()) |
| << "Player disconnected."; |
| if (on_disconnect_) { |
| std::move(on_disconnect_).Run(); |
| } |
| } |
| |
| void MediaPlayerImpl::ReportErrorAndDisconnect(zx_status_t status) { |
| binding_.Close(status); |
| } |