1 // Copyright 2018 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "content/browser/picture_in_picture/picture_in_picture_window_controller_impl.h"
6 
7 #include <set>
8 
9 #include "components/viz/common/surfaces/surface_id.h"
10 #include "content/browser/media/media_web_contents_observer.h"
11 #include "content/browser/media/session/media_session_impl.h"
12 #include "content/browser/picture_in_picture/picture_in_picture_session.h"
13 #include "content/browser/web_contents/web_contents_impl.h"
14 #include "content/common/media/media_player_delegate_messages.h"
15 #include "content/public/browser/content_browser_client.h"
16 #include "content/public/browser/overlay_window.h"
17 #include "content/public/browser/web_contents.h"
18 #include "content/public/browser/web_contents_observer.h"
19 #include "content/public/common/content_client.h"
20 
21 namespace content {
22 
23 // static
24 PictureInPictureWindowController*
GetOrCreateForWebContents(WebContents * web_contents)25 PictureInPictureWindowController::GetOrCreateForWebContents(
26     WebContents* web_contents) {
27   return PictureInPictureWindowControllerImpl::GetOrCreateForWebContents(
28       web_contents);
29 }
30 
31 // static
32 PictureInPictureWindowControllerImpl*
GetOrCreateForWebContents(WebContents * web_contents)33 PictureInPictureWindowControllerImpl::GetOrCreateForWebContents(
34     WebContents* web_contents) {
35   DCHECK(web_contents);
36 
37   // This is a no-op if the controller already exists.
38   CreateForWebContents(web_contents);
39   return FromWebContents(web_contents);
40 }
41 
~PictureInPictureWindowControllerImpl()42 PictureInPictureWindowControllerImpl::~PictureInPictureWindowControllerImpl() {
43   if (window_)
44     window_->Close();
45 
46   // If the initiator WebContents is being destroyed, there is no need to put
47   // the video's media player in a post-Picture-in-Picture mode. In fact, some
48   // things, such as the MediaWebContentsObserver, may already been torn down.
49   if (initiator_->IsBeingDestroyed())
50     return;
51 
52   initiator_->SetHasPictureInPictureVideo(false);
53   OnLeavingPictureInPicture(true /* should_pause_video */);
54 }
55 
PictureInPictureWindowControllerImpl(WebContents * initiator)56 PictureInPictureWindowControllerImpl::PictureInPictureWindowControllerImpl(
57     WebContents* initiator)
58     : WebContentsObserver(initiator),
59       initiator_(static_cast<WebContentsImpl* const>(initiator)) {
60   DCHECK(initiator_);
61 
62   media_web_contents_observer_ = initiator_->media_web_contents_observer();
63 
64   EnsureWindow();
65   DCHECK(window_) << "Picture in Picture requires a valid window.";
66 }
67 
Show()68 void PictureInPictureWindowControllerImpl::Show() {
69   DCHECK(window_);
70   DCHECK(surface_id_.is_valid());
71 
72   MediaSessionImpl* media_session = MediaSessionImpl::Get(initiator_);
73   media_session_action_play_handled_ = media_session->ShouldRouteAction(
74       media_session::mojom::MediaSessionAction::kPlay);
75   media_session_action_pause_handled_ = media_session->ShouldRouteAction(
76       media_session::mojom::MediaSessionAction::kPause);
77   media_session_action_skip_ad_handled_ = media_session->ShouldRouteAction(
78       media_session::mojom::MediaSessionAction::kSkipAd);
79   media_session_action_next_track_handled_ = media_session->ShouldRouteAction(
80       media_session::mojom::MediaSessionAction::kNextTrack);
81   media_session_action_previous_track_handled_ =
82       media_session->ShouldRouteAction(
83           media_session::mojom::MediaSessionAction::kPreviousTrack);
84 
85   UpdatePlayPauseButtonVisibility();
86   window_->SetSkipAdButtonVisibility(media_session_action_skip_ad_handled_);
87   window_->SetNextTrackButtonVisibility(
88       media_session_action_next_track_handled_);
89   window_->SetPreviousTrackButtonVisibility(
90       media_session_action_previous_track_handled_);
91   window_->ShowInactive();
92   initiator_->SetHasPictureInPictureVideo(true);
93 }
94 
Close(bool should_pause_video)95 void PictureInPictureWindowControllerImpl::Close(bool should_pause_video) {
96   if (!window_ || !window_->IsVisible())
97     return;
98 
99   window_->Hide();
100   CloseInternal(should_pause_video);
101 }
102 
CloseAndFocusInitiator()103 void PictureInPictureWindowControllerImpl::CloseAndFocusInitiator() {
104   Close(false /* should_pause_video */);
105   initiator_->Activate();
106 }
107 
OnWindowDestroyed()108 void PictureInPictureWindowControllerImpl::OnWindowDestroyed() {
109   window_ = nullptr;
110   CloseInternal(true /* should_pause_video */);
111 }
112 
EmbedSurface(const viz::SurfaceId & surface_id,const gfx::Size & natural_size)113 void PictureInPictureWindowControllerImpl::EmbedSurface(
114     const viz::SurfaceId& surface_id,
115     const gfx::Size& natural_size) {
116   EnsureWindow();
117   DCHECK(window_);
118 
119   DCHECK(surface_id.is_valid());
120 
121   surface_id_ = surface_id;
122 
123   // Update the media player id in step with the video surface id. If the
124   // surface id was updated for the same video, this is a no-op. This could
125   // be updated for a different video if another media player on the same
126   // |initiator_| enters Picture-in-Picture mode.
127   UpdateMediaPlayerId();
128 
129   window_->UpdateVideoSize(natural_size);
130   window_->SetSurfaceId(surface_id_);
131 }
132 
GetWindowForTesting()133 OverlayWindow* PictureInPictureWindowControllerImpl::GetWindowForTesting() {
134   return window_.get();
135 }
136 
UpdateLayerBounds()137 void PictureInPictureWindowControllerImpl::UpdateLayerBounds() {
138   if (media_player_id_.has_value() && active_session_ && window_ &&
139       window_->IsVisible()) {
140     active_session_->NotifyWindowResized(window_->GetBounds().size());
141   }
142 }
143 
IsPlayerActive()144 bool PictureInPictureWindowControllerImpl::IsPlayerActive() {
145   if (!media_player_id_.has_value())
146     media_player_id_ =
147         active_session_ ? active_session_->player_id() : base::nullopt;
148 
149   // At creation time, the player id may not be set.
150   if (!media_player_id_.has_value())
151     return false;
152 
153   return media_web_contents_observer_->IsPlayerActive(*media_player_id_);
154 }
155 
GetInitiatorWebContents()156 WebContents* PictureInPictureWindowControllerImpl::GetInitiatorWebContents() {
157   return initiator_;
158 }
159 
UpdatePlaybackState(bool is_playing,bool reached_end_of_stream)160 void PictureInPictureWindowControllerImpl::UpdatePlaybackState(
161     bool is_playing,
162     bool reached_end_of_stream) {
163   if (!window_)
164     return;
165 
166   if (reached_end_of_stream) {
167     window_->SetPlaybackState(OverlayWindow::PlaybackState::kEndOfVideo);
168     return;
169   }
170 
171   DCHECK(media_player_id_.has_value());
172 
173   window_->SetPlaybackState(is_playing ? OverlayWindow::PlaybackState::kPlaying
174                                        : OverlayWindow::PlaybackState::kPaused);
175 }
176 
TogglePlayPause()177 bool PictureInPictureWindowControllerImpl::TogglePlayPause() {
178   DCHECK(window_);
179 
180   if (IsPlayerActive()) {
181     if (media_session_action_pause_handled_) {
182       MediaSessionImpl::Get(initiator_)
183           ->Suspend(MediaSession::SuspendType::kUI);
184       return true /* still playing */;
185     }
186 
187     media_player_id_->render_frame_host->Send(new MediaPlayerDelegateMsg_Pause(
188         media_player_id_->render_frame_host->GetRoutingID(),
189         media_player_id_->delegate_id, false /* triggered_by_user */));
190     return false /* paused */;
191   }
192 
193   if (media_session_action_play_handled_) {
194     MediaSessionImpl::Get(initiator_)->Resume(MediaSession::SuspendType::kUI);
195     return false /* still paused */;
196   }
197 
198   media_player_id_->render_frame_host->Send(new MediaPlayerDelegateMsg_Play(
199       media_player_id_->render_frame_host->GetRoutingID(),
200       media_player_id_->delegate_id));
201   return true /* playing */;
202 }
203 
UpdateMediaPlayerId()204 void PictureInPictureWindowControllerImpl::UpdateMediaPlayerId() {
205   media_player_id_ =
206       active_session_ ? active_session_->player_id() : base::nullopt;
207   UpdatePlaybackState(IsPlayerActive(), !media_player_id_.has_value());
208 }
209 
SetActiveSession(PictureInPictureSession * session)210 void PictureInPictureWindowControllerImpl::SetActiveSession(
211     PictureInPictureSession* session) {
212   if (active_session_ == session)
213     return;
214 
215   if (active_session_)
216     active_session_->Shutdown();
217 
218   active_session_ = session;
219 }
220 
SetAlwaysHidePlayPauseButton(bool is_visible)221 void PictureInPictureWindowControllerImpl::SetAlwaysHidePlayPauseButton(
222     bool is_visible) {
223   always_hide_play_pause_button_ = is_visible;
224   UpdatePlayPauseButtonVisibility();
225 }
226 
SkipAd()227 void PictureInPictureWindowControllerImpl::SkipAd() {
228   if (media_session_action_skip_ad_handled_)
229     MediaSession::Get(initiator_)->SkipAd();
230 }
231 
NextTrack()232 void PictureInPictureWindowControllerImpl::NextTrack() {
233   if (media_session_action_next_track_handled_)
234     MediaSession::Get(initiator_)->NextTrack();
235 }
236 
PreviousTrack()237 void PictureInPictureWindowControllerImpl::PreviousTrack() {
238   if (media_session_action_previous_track_handled_)
239     MediaSession::Get(initiator_)->PreviousTrack();
240 }
241 
MediaSessionActionsChanged(const std::set<media_session::mojom::MediaSessionAction> & actions)242 void PictureInPictureWindowControllerImpl::MediaSessionActionsChanged(
243     const std::set<media_session::mojom::MediaSessionAction>& actions) {
244   // TODO(crbug.com/919842): Currently, the first Media Session to be created
245   // (independently of the frame) will be used. This means, we could show a
246   // Skip Ad button for a PiP video from another frame. Ideally, we should have
247   // a Media Session per frame, not per tab. This is not implemented yet.
248 
249   media_session_action_pause_handled_ =
250       actions.find(media_session::mojom::MediaSessionAction::kPause) !=
251       actions.end();
252   media_session_action_play_handled_ =
253       actions.find(media_session::mojom::MediaSessionAction::kPlay) !=
254       actions.end();
255   media_session_action_skip_ad_handled_ =
256       actions.find(media_session::mojom::MediaSessionAction::kSkipAd) !=
257       actions.end();
258   media_session_action_next_track_handled_ =
259       actions.find(media_session::mojom::MediaSessionAction::kNextTrack) !=
260       actions.end();
261   media_session_action_previous_track_handled_ =
262       actions.find(media_session::mojom::MediaSessionAction::kPreviousTrack) !=
263       actions.end();
264 
265   if (!window_)
266     return;
267 
268   UpdatePlayPauseButtonVisibility();
269   window_->SetSkipAdButtonVisibility(media_session_action_skip_ad_handled_);
270   window_->SetNextTrackButtonVisibility(
271       media_session_action_next_track_handled_);
272   window_->SetPreviousTrackButtonVisibility(
273       media_session_action_previous_track_handled_);
274 }
275 
GetSize()276 gfx::Size PictureInPictureWindowControllerImpl::GetSize() {
277   return window_->GetBounds().size();
278 }
279 
MediaStartedPlaying(const MediaPlayerInfo &,const MediaPlayerId & media_player_id)280 void PictureInPictureWindowControllerImpl::MediaStartedPlaying(
281     const MediaPlayerInfo&,
282     const MediaPlayerId& media_player_id) {
283   if (initiator_->IsBeingDestroyed())
284     return;
285 
286   if (media_player_id_ != media_player_id)
287     return;
288 
289   UpdatePlaybackState(true /* is_playing */, false /* reached_end_of_stream */);
290 }
291 
MediaStoppedPlaying(const MediaPlayerInfo &,const MediaPlayerId & media_player_id,WebContentsObserver::MediaStoppedReason reason)292 void PictureInPictureWindowControllerImpl::MediaStoppedPlaying(
293     const MediaPlayerInfo&,
294     const MediaPlayerId& media_player_id,
295     WebContentsObserver::MediaStoppedReason reason) {
296   if (initiator_->IsBeingDestroyed())
297     return;
298 
299   if (media_player_id_ != media_player_id)
300     return;
301 
302   UpdatePlaybackState(
303       false /* is_playing */,
304       reason == WebContentsObserver::MediaStoppedReason::kReachedEndOfStream);
305 }
306 
OnLeavingPictureInPicture(bool should_pause_video)307 void PictureInPictureWindowControllerImpl::OnLeavingPictureInPicture(
308     bool should_pause_video) {
309   if (IsPlayerActive() && should_pause_video) {
310     // Pause the current video so there is only one video playing at a time.
311     media_player_id_->render_frame_host->Send(new MediaPlayerDelegateMsg_Pause(
312         media_player_id_->render_frame_host->GetRoutingID(),
313         media_player_id_->delegate_id, false /* triggered_by_user */));
314   }
315 
316   if (media_player_id_.has_value()) {
317     if (active_session_)
318       active_session_->Shutdown();
319 
320     active_session_ = nullptr;
321     media_player_id_.reset();
322   }
323 }
324 
CloseInternal(bool should_pause_video)325 void PictureInPictureWindowControllerImpl::CloseInternal(
326     bool should_pause_video) {
327   if (initiator_->IsBeingDestroyed())
328     return;
329 
330   initiator_->SetHasPictureInPictureVideo(false);
331   OnLeavingPictureInPicture(should_pause_video);
332   surface_id_ = viz::SurfaceId();
333 }
334 
EnsureWindow()335 void PictureInPictureWindowControllerImpl::EnsureWindow() {
336   if (window_)
337     return;
338 
339   window_ =
340       GetContentClient()->browser()->CreateWindowForPictureInPicture(this);
341 }
342 
UpdatePlayPauseButtonVisibility()343 void PictureInPictureWindowControllerImpl::UpdatePlayPauseButtonVisibility() {
344   if (!window_)
345     return;
346 
347   window_->SetAlwaysHidePlayPauseButton((media_session_action_pause_handled_ &&
348                                          media_session_action_play_handled_) ||
349                                         always_hide_play_pause_button_);
350 }
351 
352 WEB_CONTENTS_USER_DATA_KEY_IMPL(PictureInPictureWindowControllerImpl)
353 
354 }  // namespace content
355