| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/picture_in_picture/picture_in_picture_window_controller_impl.h" |
| |
| #include <set> |
| |
| #include "components/viz/common/surfaces/surface_id.h" |
| #include "content/browser/media/media_web_contents_observer.h" |
| #include "content/browser/media/session/media_session_impl.h" |
| #include "content/browser/picture_in_picture/overlay_surface_embedder.h" |
| #include "content/browser/picture_in_picture/picture_in_picture_service_impl.h" |
| #include "content/browser/web_contents/web_contents_impl.h" |
| #include "content/common/media/media_player_delegate_messages.h" |
| #include "content/public/browser/content_browser_client.h" |
| #include "content/public/browser/overlay_window.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/browser/web_contents_observer.h" |
| #include "content/public/common/content_client.h" |
| #include "ui/compositor/layer.h" |
| |
| namespace content { |
| |
| // static |
| PictureInPictureWindowController* |
| PictureInPictureWindowController::GetOrCreateForWebContents( |
| WebContents* web_contents) { |
| return PictureInPictureWindowControllerImpl::GetOrCreateForWebContents( |
| web_contents); |
| } |
| |
| // static |
| PictureInPictureWindowControllerImpl* |
| PictureInPictureWindowControllerImpl::GetOrCreateForWebContents( |
| WebContents* web_contents) { |
| DCHECK(web_contents); |
| |
| // This is a no-op if the controller already exists. |
| CreateForWebContents(web_contents); |
| return FromWebContents(web_contents); |
| } |
| |
| PictureInPictureWindowControllerImpl::~PictureInPictureWindowControllerImpl() { |
| if (window_) |
| window_->Close(); |
| |
| // If the initiator WebContents is being destroyed, there is no need to put |
| // the video's media player in a post-Picture-in-Picture mode. In fact, some |
| // things, such as the MediaWebContentsObserver, may already been torn down. |
| if (initiator_->IsBeingDestroyed()) |
| return; |
| |
| initiator_->SetHasPictureInPictureVideo(false); |
| OnLeavingPictureInPicture(true /* should_pause_video */, |
| true /* should_reset_pip_player */); |
| } |
| |
| PictureInPictureWindowControllerImpl::PictureInPictureWindowControllerImpl( |
| WebContents* initiator) |
| : WebContentsObserver(initiator), |
| initiator_(static_cast<WebContentsImpl* const>(initiator)) { |
| DCHECK(initiator_); |
| |
| media_web_contents_observer_ = initiator_->media_web_contents_observer(); |
| |
| EnsureWindow(); |
| DCHECK(window_) << "Picture in Picture requires a valid window."; |
| } |
| |
| gfx::Size PictureInPictureWindowControllerImpl::Show() { |
| DCHECK(window_); |
| DCHECK(surface_id_.is_valid()); |
| |
| MediaSessionImpl* media_session = MediaSessionImpl::Get(initiator_); |
| media_session_action_play_handled_ = media_session->ShouldRouteAction( |
| media_session::mojom::MediaSessionAction::kPlay); |
| media_session_action_pause_handled_ = media_session->ShouldRouteAction( |
| media_session::mojom::MediaSessionAction::kPause); |
| media_session_action_skip_ad_handled_ = media_session->ShouldRouteAction( |
| media_session::mojom::MediaSessionAction::kSkipAd); |
| media_session_action_next_track_handled_ = media_session->ShouldRouteAction( |
| media_session::mojom::MediaSessionAction::kNextTrack); |
| media_session_action_previous_track_handled_ = |
| media_session->ShouldRouteAction( |
| media_session::mojom::MediaSessionAction::kPreviousTrack); |
| |
| UpdatePlayPauseButtonVisibility(); |
| window_->SetSkipAdButtonVisibility(media_session_action_skip_ad_handled_); |
| window_->SetNextTrackButtonVisibility( |
| media_session_action_next_track_handled_); |
| window_->SetPreviousTrackButtonVisibility( |
| media_session_action_previous_track_handled_); |
| window_->ShowInactive(); |
| initiator_->SetHasPictureInPictureVideo(true); |
| |
| return window_->GetBounds().size(); |
| } |
| |
| void PictureInPictureWindowControllerImpl::Close(bool should_pause_video, |
| bool should_reset_pip_player) { |
| if (!window_ || !window_->IsVisible()) |
| return; |
| |
| window_->Hide(); |
| CloseInternal(should_pause_video, should_reset_pip_player); |
| } |
| |
| void PictureInPictureWindowControllerImpl::CloseAndFocusInitiator() { |
| Close(false /* should_pause_video */, true /* should_reset_pip_player */); |
| initiator_->Activate(); |
| } |
| |
| void PictureInPictureWindowControllerImpl::OnWindowDestroyed() { |
| window_ = nullptr; |
| embedder_ = nullptr; |
| CloseInternal(true /* should_pause_video */, |
| true /* should_reset_pip_player */); |
| } |
| |
| void PictureInPictureWindowControllerImpl::EmbedSurface( |
| const viz::SurfaceId& surface_id, |
| const gfx::Size& natural_size) { |
| EnsureWindow(); |
| DCHECK(window_); |
| |
| DCHECK(surface_id.is_valid()); |
| |
| // TODO(https://crbug.com/925346): We also want to unregister the page that |
| // used to embed the video as its parent. |
| ui::Compositor* compositor = window_->GetLayer()->GetCompositor(); |
| if (!surface_id_.is_valid()) { |
| compositor->AddChildFrameSink(surface_id.frame_sink_id()); |
| } else if (surface_id_.frame_sink_id() != surface_id.frame_sink_id()) { |
| compositor->RemoveChildFrameSink(surface_id_.frame_sink_id()); |
| compositor->AddChildFrameSink(surface_id.frame_sink_id()); |
| } |
| |
| surface_id_ = surface_id; |
| |
| // Update the media player id in step with the video surface id. If the |
| // surface id was updated for the same video, this is a no-op. This could |
| // be updated for a different video if another media player on the same |
| // |initiator_| enters Picture-in-Picture mode. |
| UpdateMediaPlayerId(); |
| |
| window_->UpdateVideoSize(natural_size); |
| |
| if (!embedder_) |
| embedder_.reset(new OverlaySurfaceEmbedder(window_.get())); |
| embedder_->SetSurfaceId(surface_id_); |
| } |
| |
| OverlayWindow* PictureInPictureWindowControllerImpl::GetWindowForTesting() { |
| return window_.get(); |
| } |
| |
| void PictureInPictureWindowControllerImpl::UpdateLayerBounds() { |
| if (media_player_id_.has_value() && service_ && window_ && |
| window_->IsVisible()) { |
| service_->NotifyWindowResized(window_->GetBounds().size()); |
| } |
| |
| if (embedder_) |
| embedder_->UpdateLayerBounds(); |
| } |
| |
| bool PictureInPictureWindowControllerImpl::IsPlayerActive() { |
| if (!media_player_id_.has_value()) |
| media_player_id_ = service_ ? service_->player_id() : base::nullopt; |
| |
| // At creation time, the player id may not be set. |
| if (!media_player_id_.has_value()) |
| return false; |
| |
| return media_web_contents_observer_->IsPlayerActive(*media_player_id_); |
| } |
| |
| WebContents* PictureInPictureWindowControllerImpl::GetInitiatorWebContents() { |
| return initiator_; |
| } |
| |
| void PictureInPictureWindowControllerImpl::UpdatePlaybackState( |
| bool is_playing, |
| bool reached_end_of_stream) { |
| if (!window_) |
| return; |
| |
| if (reached_end_of_stream) { |
| window_->SetPlaybackState(OverlayWindow::PlaybackState::kEndOfVideo); |
| return; |
| } |
| |
| DCHECK(media_player_id_.has_value()); |
| |
| window_->SetPlaybackState(is_playing ? OverlayWindow::PlaybackState::kPlaying |
| : OverlayWindow::PlaybackState::kPaused); |
| } |
| |
| bool PictureInPictureWindowControllerImpl::TogglePlayPause() { |
| DCHECK(window_); |
| |
| if (IsPlayerActive()) { |
| if (media_session_action_pause_handled_) { |
| MediaSessionImpl::Get(initiator_) |
| ->Suspend(MediaSession::SuspendType::kUI); |
| return true /* still playing */; |
| } |
| |
| media_player_id_->render_frame_host->Send(new MediaPlayerDelegateMsg_Pause( |
| media_player_id_->render_frame_host->GetRoutingID(), |
| media_player_id_->delegate_id)); |
| return false /* paused */; |
| } |
| |
| if (media_session_action_play_handled_) { |
| MediaSessionImpl::Get(initiator_)->Resume(MediaSession::SuspendType::kUI); |
| return false /* still paused */; |
| } |
| |
| media_player_id_->render_frame_host->Send(new MediaPlayerDelegateMsg_Play( |
| media_player_id_->render_frame_host->GetRoutingID(), |
| media_player_id_->delegate_id)); |
| return true /* playing */; |
| } |
| |
| void PictureInPictureWindowControllerImpl::UpdateMutedState() { |
| if (!window_) |
| return; |
| |
| if (always_hide_mute_button_) { |
| window_->SetMutedState(OverlayWindow::MutedState::kNoAudio); |
| return; |
| } |
| |
| window_->SetMutedState(IsPlayerMuted() ? OverlayWindow::MutedState::kMuted |
| : OverlayWindow::MutedState::kUnmuted); |
| } |
| |
| bool PictureInPictureWindowControllerImpl::ToggleMute() { |
| DCHECK(window_); |
| |
| bool new_muted_status = !IsPlayerMuted(); |
| media_player_id_->render_frame_host->Send(new MediaPlayerDelegateMsg_Muted( |
| media_player_id_->render_frame_host->GetRoutingID(), |
| media_player_id_->delegate_id, new_muted_status)); |
| return new_muted_status; |
| } |
| |
| void PictureInPictureWindowControllerImpl::UpdateMediaPlayerId() { |
| media_player_id_ = service_ ? service_->player_id() : base::nullopt; |
| UpdatePlaybackState(IsPlayerActive(), !media_player_id_.has_value()); |
| UpdateMutedState(); |
| } |
| |
| void PictureInPictureWindowControllerImpl::SetAlwaysHidePlayPauseButton( |
| bool is_visible) { |
| always_hide_play_pause_button_ = is_visible; |
| UpdatePlayPauseButtonVisibility(); |
| } |
| |
| void PictureInPictureWindowControllerImpl::SetAlwaysHideMuteButton( |
| bool is_visible) { |
| always_hide_mute_button_ = !is_visible; |
| UpdateMutedState(); |
| } |
| |
| void PictureInPictureWindowControllerImpl::SkipAd() { |
| if (media_session_action_skip_ad_handled_) |
| MediaSession::Get(initiator_)->SkipAd(); |
| } |
| |
| void PictureInPictureWindowControllerImpl::NextTrack() { |
| if (media_session_action_next_track_handled_) |
| MediaSession::Get(initiator_)->NextTrack(); |
| } |
| |
| void PictureInPictureWindowControllerImpl::PreviousTrack() { |
| if (media_session_action_previous_track_handled_) |
| MediaSession::Get(initiator_)->PreviousTrack(); |
| } |
| |
| void PictureInPictureWindowControllerImpl::MediaSessionActionsChanged( |
| const std::set<media_session::mojom::MediaSessionAction>& actions) { |
| // TODO(crbug.com/919842): Currently, the first Media Session to be created |
| // (independently of the frame) will be used. This means, we could show a |
| // Skip Ad button for a PiP video from another frame. Ideally, we should have |
| // a Media Session per frame, not per tab. This is not implemented yet. |
| |
| media_session_action_pause_handled_ = |
| actions.find(media_session::mojom::MediaSessionAction::kPause) != |
| actions.end(); |
| media_session_action_play_handled_ = |
| actions.find(media_session::mojom::MediaSessionAction::kPlay) != |
| actions.end(); |
| media_session_action_skip_ad_handled_ = |
| actions.find(media_session::mojom::MediaSessionAction::kSkipAd) != |
| actions.end(); |
| media_session_action_next_track_handled_ = |
| actions.find(media_session::mojom::MediaSessionAction::kNextTrack) != |
| actions.end(); |
| media_session_action_previous_track_handled_ = |
| actions.find(media_session::mojom::MediaSessionAction::kPreviousTrack) != |
| actions.end(); |
| |
| if (!window_) |
| return; |
| |
| UpdatePlayPauseButtonVisibility(); |
| window_->SetSkipAdButtonVisibility(media_session_action_skip_ad_handled_); |
| window_->SetNextTrackButtonVisibility( |
| media_session_action_next_track_handled_); |
| window_->SetPreviousTrackButtonVisibility( |
| media_session_action_previous_track_handled_); |
| } |
| |
| void PictureInPictureWindowControllerImpl::MediaStartedPlaying( |
| const MediaPlayerInfo&, |
| const MediaPlayerId& media_player_id) { |
| if (initiator_->IsBeingDestroyed()) |
| return; |
| |
| if (media_player_id_ != media_player_id) |
| return; |
| |
| UpdatePlaybackState(true /* is_playing */, false /* reached_end_of_stream */); |
| UpdateMutedState(); |
| } |
| |
| void PictureInPictureWindowControllerImpl::MediaStoppedPlaying( |
| const MediaPlayerInfo&, |
| const MediaPlayerId& media_player_id, |
| WebContentsObserver::MediaStoppedReason reason) { |
| if (initiator_->IsBeingDestroyed()) |
| return; |
| |
| if (media_player_id_ != media_player_id) |
| return; |
| |
| UpdatePlaybackState( |
| false /* is_playing */, |
| reason == WebContentsObserver::MediaStoppedReason::kReachedEndOfStream); |
| } |
| |
| void PictureInPictureWindowControllerImpl::MediaMutedStatusChanged( |
| const MediaPlayerId& media_player_id, |
| bool muted) { |
| if (initiator_->IsBeingDestroyed()) |
| return; |
| |
| if (muted) |
| AddMutedPlayerEntry(media_player_id); |
| else |
| RemoveMutedPlayerEntry(media_player_id); |
| |
| if (media_player_id_ == media_player_id) |
| UpdateMutedState(); |
| } |
| |
| void PictureInPictureWindowControllerImpl::AddMutedPlayerEntry( |
| const MediaPlayerId& id) { |
| muted_players_[id.render_frame_host].insert(id.delegate_id); |
| } |
| |
| bool PictureInPictureWindowControllerImpl::RemoveMutedPlayerEntry( |
| const MediaPlayerId& id) { |
| auto it = muted_players_.find(id.render_frame_host); |
| if (it == muted_players_.end()) |
| return false; |
| |
| // Remove the player. |
| bool did_remove = it->second.erase(id.delegate_id) == 1; |
| if (!did_remove) |
| return false; |
| |
| // If there are no players left, remove the entry. |
| if (it->second.empty()) |
| muted_players_.erase(it); |
| |
| return true; |
| } |
| |
| bool PictureInPictureWindowControllerImpl::IsPlayerMuted() { |
| // At creation time, the player id may not be set. |
| if (!media_player_id_.has_value()) |
| return false; |
| |
| const auto& players = |
| muted_players_.find(media_player_id_->render_frame_host); |
| if (players == muted_players_.end()) |
| return false; |
| |
| return players->second.find(media_player_id_->delegate_id) != |
| players->second.end(); |
| } |
| |
| void PictureInPictureWindowControllerImpl::OnLeavingPictureInPicture( |
| bool should_pause_video, |
| bool should_reset_pip_player) { |
| if (window_ && surface_id_.is_valid()) { |
| window_->GetLayer()->GetCompositor()->RemoveChildFrameSink( |
| surface_id_.frame_sink_id()); |
| } |
| |
| if (IsPlayerActive() && should_pause_video) { |
| // Pause the current video so there is only one video playing at a time. |
| media_player_id_->render_frame_host->Send(new MediaPlayerDelegateMsg_Pause( |
| media_player_id_->render_frame_host->GetRoutingID(), |
| media_player_id_->delegate_id)); |
| } |
| |
| if (media_player_id_.has_value()) { |
| media_player_id_->render_frame_host->Send( |
| new MediaPlayerDelegateMsg_EndPictureInPictureMode( |
| media_player_id_->render_frame_host->GetRoutingID(), |
| media_player_id_->delegate_id)); |
| |
| if (should_reset_pip_player) { |
| DCHECK(service_); |
| service_->ResetPlayerId(); |
| media_player_id_.reset(); |
| } |
| } |
| } |
| |
| void PictureInPictureWindowControllerImpl::CloseInternal( |
| bool should_pause_video, |
| bool should_reset_pip_player) { |
| if (initiator_->IsBeingDestroyed()) |
| return; |
| |
| initiator_->SetHasPictureInPictureVideo(false); |
| OnLeavingPictureInPicture(should_pause_video, should_reset_pip_player); |
| surface_id_ = viz::SurfaceId(); |
| } |
| |
| void PictureInPictureWindowControllerImpl::EnsureWindow() { |
| if (window_) |
| return; |
| |
| window_ = |
| GetContentClient()->browser()->CreateWindowForPictureInPicture(this); |
| } |
| |
| void PictureInPictureWindowControllerImpl::UpdatePlayPauseButtonVisibility() { |
| if (!window_) |
| return; |
| |
| window_->SetAlwaysHidePlayPauseButton((media_session_action_pause_handled_ && |
| media_session_action_play_handled_) || |
| always_hide_play_pause_button_); |
| } |
| |
| WEB_CONTENTS_USER_DATA_KEY_IMPL(PictureInPictureWindowControllerImpl) |
| |
| } // namespace content |