| // Copyright 2014 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/media/media_web_contents_observer.h" |
| |
| #include <memory> |
| |
| #include "build/build_config.h" |
| #include "content/browser/media/audible_metrics.h" |
| #include "content/browser/media/audio_stream_monitor.h" |
| #include "content/browser/web_contents/web_contents_impl.h" |
| #include "content/common/media/media_player_delegate_messages.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "ipc/ipc_message_macros.h" |
| #include "mojo/public/cpp/bindings/interface_request.h" |
| #include "services/device/public/mojom/wake_lock_context.mojom.h" |
| #include "third_party/blink/public/platform/web_fullscreen_video_status.h" |
| #include "ui/gfx/geometry/size.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| AudibleMetrics* GetAudibleMetrics() { |
| static AudibleMetrics* metrics = new AudibleMetrics(); |
| return metrics; |
| } |
| |
| void CheckFullscreenDetectionEnabled(WebContents* web_contents) { |
| #if defined(OS_ANDROID) |
| DCHECK(web_contents->GetRenderViewHost() |
| ->GetWebkitPreferences() |
| .video_fullscreen_detection_enabled) |
| << "Attempt to use method relying on fullscreen detection while " |
| << "fullscreen detection is disabled."; |
| #else // defined(OS_ANDROID) |
| NOTREACHED() << "Attempt to use method relying on fullscreen detection, " |
| << "which is only enabled on Android."; |
| #endif // defined(OS_ANDROID) |
| } |
| |
| // Returns true if |player_id| exists in |player_map|. |
| bool MediaPlayerEntryExists( |
| const WebContentsObserver::MediaPlayerId& player_id, |
| const MediaWebContentsObserver::ActiveMediaPlayerMap& player_map) { |
| const auto& players = player_map.find(player_id.first); |
| if (players == player_map.end()) |
| return false; |
| |
| return players->second.find(player_id.second) != players->second.end(); |
| } |
| |
| } // anonymous namespace |
| |
| MediaWebContentsObserver::MediaWebContentsObserver(WebContents* web_contents) |
| : WebContentsObserver(web_contents), |
| session_controllers_manager_(this) {} |
| |
| MediaWebContentsObserver::~MediaWebContentsObserver() = default; |
| |
| void MediaWebContentsObserver::WebContentsDestroyed() { |
| GetAudibleMetrics()->UpdateAudibleWebContentsState(web_contents(), false); |
| } |
| |
| void MediaWebContentsObserver::RenderFrameDeleted( |
| RenderFrameHost* render_frame_host) { |
| ClearWakeLocks(render_frame_host); |
| session_controllers_manager_.RenderFrameDeleted(render_frame_host); |
| |
| if (fullscreen_player_ && fullscreen_player_->first == render_frame_host) { |
| picture_in_picture_allowed_in_fullscreen_.reset(); |
| fullscreen_player_.reset(); |
| } |
| } |
| |
| void MediaWebContentsObserver::MaybeUpdateAudibleState() { |
| AudioStreamMonitor* audio_stream_monitor = |
| web_contents_impl()->audio_stream_monitor(); |
| |
| if (audio_stream_monitor->WasRecentlyAudible()) |
| LockAudio(); |
| else |
| CancelAudioLock(); |
| |
| GetAudibleMetrics()->UpdateAudibleWebContentsState( |
| web_contents(), audio_stream_monitor->IsCurrentlyAudible()); |
| } |
| |
| bool MediaWebContentsObserver::HasActiveEffectivelyFullscreenVideo() const { |
| CheckFullscreenDetectionEnabled(web_contents_impl()); |
| if (!web_contents()->IsFullscreen() || !fullscreen_player_) |
| return false; |
| |
| // Check that the player is active. |
| return MediaPlayerEntryExists(*fullscreen_player_, active_video_players_); |
| } |
| |
| bool MediaWebContentsObserver::IsPictureInPictureAllowedForFullscreenVideo() |
| const { |
| DCHECK(picture_in_picture_allowed_in_fullscreen_.has_value()); |
| |
| return *picture_in_picture_allowed_in_fullscreen_; |
| } |
| |
| const base::Optional<WebContentsObserver::MediaPlayerId>& |
| MediaWebContentsObserver::GetFullscreenVideoMediaPlayerId() const { |
| CheckFullscreenDetectionEnabled(web_contents_impl()); |
| return fullscreen_player_; |
| } |
| |
| const base::Optional<WebContentsObserver::MediaPlayerId>& |
| MediaWebContentsObserver::GetPictureInPictureVideoMediaPlayerId() const { |
| return pip_player_; |
| } |
| |
| bool MediaWebContentsObserver::OnMessageReceived( |
| const IPC::Message& msg, |
| RenderFrameHost* render_frame_host) { |
| bool handled = true; |
| IPC_BEGIN_MESSAGE_MAP_WITH_PARAM(MediaWebContentsObserver, msg, |
| render_frame_host) |
| IPC_MESSAGE_HANDLER(MediaPlayerDelegateHostMsg_OnMediaDestroyed, |
| OnMediaDestroyed) |
| IPC_MESSAGE_HANDLER(MediaPlayerDelegateHostMsg_OnMediaPaused, OnMediaPaused) |
| IPC_MESSAGE_HANDLER(MediaPlayerDelegateHostMsg_OnMediaPlaying, |
| OnMediaPlaying) |
| IPC_MESSAGE_HANDLER(MediaPlayerDelegateHostMsg_OnMutedStatusChanged, |
| OnMediaMutedStatusChanged) |
| IPC_MESSAGE_HANDLER( |
| MediaPlayerDelegateHostMsg_OnMediaEffectivelyFullscreenChanged, |
| OnMediaEffectivelyFullscreenChanged) |
| IPC_MESSAGE_HANDLER(MediaPlayerDelegateHostMsg_OnMediaSizeChanged, |
| OnMediaSizeChanged) |
| IPC_MESSAGE_HANDLER( |
| MediaPlayerDelegateHostMsg_OnPictureInPictureModeStarted, |
| OnPictureInPictureModeStarted) |
| IPC_MESSAGE_HANDLER(MediaPlayerDelegateHostMsg_OnPictureInPictureModeEnded, |
| OnPictureInPictureModeEnded) |
| IPC_MESSAGE_UNHANDLED(handled = false) |
| IPC_END_MESSAGE_MAP() |
| return handled; |
| } |
| |
| void MediaWebContentsObserver::OnVisibilityChanged( |
| content::Visibility visibility) { |
| UpdateVideoLock(); |
| } |
| |
| void MediaWebContentsObserver::RequestPersistentVideo(bool value) { |
| if (!fullscreen_player_) |
| return; |
| |
| // The message is sent to the renderer even though the video is already the |
| // fullscreen element itself. It will eventually be handled by Blink. |
| RenderFrameHost* target_frame = fullscreen_player_->first; |
| int delegate_id = fullscreen_player_->second; |
| target_frame->Send(new MediaPlayerDelegateMsg_BecamePersistentVideo( |
| target_frame->GetRoutingID(), delegate_id, value)); |
| } |
| |
| bool MediaWebContentsObserver::IsPlayerActive( |
| const MediaPlayerId& player_id) const { |
| if (MediaPlayerEntryExists(player_id, active_video_players_)) |
| return true; |
| |
| return MediaPlayerEntryExists(player_id, active_audio_players_); |
| } |
| |
| void MediaWebContentsObserver::OnMediaDestroyed( |
| RenderFrameHost* render_frame_host, |
| int delegate_id) { |
| OnMediaPaused(render_frame_host, delegate_id, true); |
| } |
| |
| void MediaWebContentsObserver::OnMediaPaused(RenderFrameHost* render_frame_host, |
| int delegate_id, |
| bool reached_end_of_stream) { |
| const MediaPlayerId player_id(render_frame_host, delegate_id); |
| const bool removed_audio = |
| RemoveMediaPlayerEntry(player_id, &active_audio_players_); |
| const bool removed_video = |
| RemoveMediaPlayerEntry(player_id, &active_video_players_); |
| |
| UpdateVideoLock(); |
| |
| if (removed_audio || removed_video) { |
| // Notify observers the player has been "paused". |
| web_contents_impl()->MediaStoppedPlaying( |
| WebContentsObserver::MediaPlayerInfo(removed_video, removed_audio), |
| player_id, |
| reached_end_of_stream |
| ? WebContentsObserver::MediaStoppedReason::kReachedEndOfStream |
| : WebContentsObserver::MediaStoppedReason::kUnspecified); |
| } |
| |
| if (reached_end_of_stream) |
| session_controllers_manager_.OnEnd(player_id); |
| else |
| session_controllers_manager_.OnPause(player_id); |
| } |
| |
| void MediaWebContentsObserver::OnMediaPlaying( |
| RenderFrameHost* render_frame_host, |
| int delegate_id, |
| bool has_video, |
| bool has_audio, |
| bool is_remote, |
| media::MediaContentType media_content_type) { |
| // Ignore the videos playing remotely and don't hold the wake lock for the |
| // screen. TODO(dalecurtis): Is this correct? It means observers will not |
| // receive play and pause messages. |
| if (is_remote) |
| return; |
| |
| const MediaPlayerId id(render_frame_host, delegate_id); |
| if (has_audio) |
| AddMediaPlayerEntry(id, &active_audio_players_); |
| |
| if (has_video) { |
| AddMediaPlayerEntry(id, &active_video_players_); |
| |
| UpdateVideoLock(); |
| } |
| |
| if (!session_controllers_manager_.RequestPlay( |
| id, has_audio, is_remote, media_content_type)) { |
| return; |
| } |
| |
| // Notify observers of the new player. |
| DCHECK(has_audio || has_video); |
| web_contents_impl()->MediaStartedPlaying( |
| WebContentsObserver::MediaPlayerInfo(has_video, has_audio), id); |
| } |
| |
| void MediaWebContentsObserver::OnMediaEffectivelyFullscreenChanged( |
| RenderFrameHost* render_frame_host, |
| int delegate_id, |
| blink::WebFullscreenVideoStatus fullscreen_status) { |
| const MediaPlayerId id(render_frame_host, delegate_id); |
| |
| switch (fullscreen_status) { |
| case blink::WebFullscreenVideoStatus::kFullscreenAndPictureInPictureEnabled: |
| fullscreen_player_ = id; |
| picture_in_picture_allowed_in_fullscreen_ = true; |
| break; |
| case blink::WebFullscreenVideoStatus:: |
| kFullscreenAndPictureInPictureDisabled: |
| fullscreen_player_ = id; |
| picture_in_picture_allowed_in_fullscreen_ = false; |
| break; |
| case blink::WebFullscreenVideoStatus::kNotEffectivelyFullscreen: |
| if (!fullscreen_player_ || *fullscreen_player_ != id) |
| return; |
| |
| picture_in_picture_allowed_in_fullscreen_.reset(); |
| fullscreen_player_.reset(); |
| break; |
| } |
| |
| bool is_fullscreen = |
| (fullscreen_status != |
| blink::WebFullscreenVideoStatus::kNotEffectivelyFullscreen); |
| web_contents_impl()->MediaEffectivelyFullscreenChanged(is_fullscreen); |
| } |
| |
| void MediaWebContentsObserver::OnMediaSizeChanged( |
| RenderFrameHost* render_frame_host, |
| int delegate_id, |
| const gfx::Size& size) { |
| const MediaPlayerId id(render_frame_host, delegate_id); |
| web_contents_impl()->MediaResized(size, id); |
| } |
| |
| void MediaWebContentsObserver::OnPictureInPictureModeStarted( |
| RenderFrameHost* render_frame_host, |
| int delegate_id, |
| const viz::SurfaceId& surface_id, |
| const gfx::Size& natural_size, |
| int request_id) { |
| DCHECK(surface_id.is_valid()); |
| pip_player_ = MediaPlayerId(render_frame_host, delegate_id); |
| |
| UpdateVideoLock(); |
| |
| gfx::Size window_size = |
| web_contents_impl()->EnterPictureInPicture(surface_id, natural_size); |
| |
| render_frame_host->Send( |
| new MediaPlayerDelegateMsg_OnPictureInPictureModeStarted_ACK( |
| render_frame_host->GetRoutingID(), delegate_id, request_id, |
| window_size)); |
| } |
| |
| void MediaWebContentsObserver::OnPictureInPictureModeEnded( |
| RenderFrameHost* render_frame_host, |
| int delegate_id, |
| int request_id) { |
| // TODO(mlamouri): must be a DCHECK but can't at the moment because we do not |
| // correctly notify players when switching PIP video in the same tab. |
| if (pip_player_) { |
| web_contents_impl()->ExitPictureInPicture(); |
| |
| // Reset must happen after notifying the WebContents because it may interact |
| // with it. |
| pip_player_.reset(); |
| |
| UpdateVideoLock(); |
| } |
| |
| render_frame_host->Send( |
| new MediaPlayerDelegateMsg_OnPictureInPictureModeEnded_ACK( |
| render_frame_host->GetRoutingID(), delegate_id, request_id)); |
| } |
| |
| void MediaWebContentsObserver::ClearWakeLocks( |
| RenderFrameHost* render_frame_host) { |
| std::set<MediaPlayerId> video_players; |
| RemoveAllMediaPlayerEntries(render_frame_host, &active_video_players_, |
| &video_players); |
| std::set<MediaPlayerId> audio_players; |
| RemoveAllMediaPlayerEntries(render_frame_host, &active_audio_players_, |
| &audio_players); |
| |
| std::set<MediaPlayerId> removed_players; |
| std::set_union(video_players.begin(), video_players.end(), |
| audio_players.begin(), audio_players.end(), |
| std::inserter(removed_players, removed_players.end())); |
| |
| UpdateVideoLock(); |
| |
| // Notify all observers the player has been "paused". |
| for (const auto& id : removed_players) { |
| auto it = video_players.find(id); |
| bool was_video = (it != video_players.end()); |
| bool was_audio = (audio_players.find(id) != audio_players.end()); |
| web_contents_impl()->MediaStoppedPlaying( |
| WebContentsObserver::MediaPlayerInfo(was_video, was_audio), id, |
| WebContentsObserver::MediaStoppedReason::kUnspecified); |
| } |
| } |
| |
| device::mojom::WakeLock* MediaWebContentsObserver::GetAudioWakeLock() { |
| // Here is a lazy binding, and will not reconnect after connection error. |
| if (!audio_wake_lock_) { |
| device::mojom::WakeLockRequest request = |
| mojo::MakeRequest(&audio_wake_lock_); |
| device::mojom::WakeLockContext* wake_lock_context = |
| web_contents()->GetWakeLockContext(); |
| if (wake_lock_context) { |
| wake_lock_context->GetWakeLock( |
| device::mojom::WakeLockType::kPreventAppSuspension, |
| device::mojom::WakeLockReason::kAudioPlayback, "Playing audio", |
| std::move(request)); |
| } |
| } |
| return audio_wake_lock_.get(); |
| } |
| |
| device::mojom::WakeLock* MediaWebContentsObserver::GetVideoWakeLock() { |
| // Here is a lazy binding, and will not reconnect after connection error. |
| if (!video_wake_lock_) { |
| device::mojom::WakeLockRequest request = |
| mojo::MakeRequest(&video_wake_lock_); |
| device::mojom::WakeLockContext* wake_lock_context = |
| web_contents()->GetWakeLockContext(); |
| if (wake_lock_context) { |
| wake_lock_context->GetWakeLock( |
| device::mojom::WakeLockType::kPreventDisplaySleep, |
| device::mojom::WakeLockReason::kVideoPlayback, "Playing video", |
| std::move(request)); |
| } |
| } |
| return video_wake_lock_.get(); |
| } |
| |
| void MediaWebContentsObserver::LockAudio() { |
| GetAudioWakeLock()->RequestWakeLock(); |
| has_audio_wake_lock_for_testing_ = true; |
| } |
| |
| void MediaWebContentsObserver::CancelAudioLock() { |
| GetAudioWakeLock()->CancelWakeLock(); |
| has_audio_wake_lock_for_testing_ = false; |
| } |
| |
| void MediaWebContentsObserver::UpdateVideoLock() { |
| if (active_video_players_.empty() || |
| (web_contents()->GetVisibility() == Visibility::HIDDEN && |
| !web_contents()->IsBeingCaptured() && !pip_player_.has_value())) { |
| // Need to release a wake lock if one is held. |
| if (!has_video_wake_lock_) |
| return; |
| |
| GetVideoWakeLock()->CancelWakeLock(); |
| has_video_wake_lock_ = false; |
| return; |
| } |
| |
| // Need to take a wake lock if not already done. |
| if (has_video_wake_lock_) |
| return; |
| |
| GetVideoWakeLock()->RequestWakeLock(); |
| has_video_wake_lock_ = true; |
| } |
| |
| void MediaWebContentsObserver::OnMediaMutedStatusChanged( |
| RenderFrameHost* render_frame_host, |
| int delegate_id, |
| bool muted) { |
| const MediaPlayerId id(render_frame_host, delegate_id); |
| web_contents_impl()->MediaMutedStatusChanged(id, muted); |
| } |
| |
| void MediaWebContentsObserver::AddMediaPlayerEntry( |
| const MediaPlayerId& id, |
| ActiveMediaPlayerMap* player_map) { |
| (*player_map)[id.first].insert(id.second); |
| } |
| |
| bool MediaWebContentsObserver::RemoveMediaPlayerEntry( |
| const MediaPlayerId& id, |
| ActiveMediaPlayerMap* player_map) { |
| auto it = player_map->find(id.first); |
| if (it == player_map->end()) |
| return false; |
| |
| // Remove the player. |
| bool did_remove = it->second.erase(id.second) == 1; |
| if (!did_remove) |
| return false; |
| |
| // If there are no players left, remove the map entry. |
| if (it->second.empty()) |
| player_map->erase(it); |
| |
| return true; |
| } |
| |
| void MediaWebContentsObserver::RemoveAllMediaPlayerEntries( |
| RenderFrameHost* render_frame_host, |
| ActiveMediaPlayerMap* player_map, |
| std::set<MediaPlayerId>* removed_players) { |
| auto it = player_map->find(render_frame_host); |
| if (it == player_map->end()) |
| return; |
| |
| for (int delegate_id : it->second) |
| removed_players->insert(MediaPlayerId(render_frame_host, delegate_id)); |
| |
| player_map->erase(it); |
| } |
| |
| WebContentsImpl* MediaWebContentsObserver::web_contents_impl() const { |
| return static_cast<WebContentsImpl*>(web_contents()); |
| } |
| |
| } // namespace content |