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