| // 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/picture_in_picture/picture_in_picture_window_controller_impl.h" |
| #include "content/browser/web_contents/web_contents_impl.h" |
| #include "content/common/media/media_player_delegate_messages.h" |
| #include "content/public/browser/picture_in_picture_window_controller.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.render_frame_host); |
| if (players == player_map.end()) |
| return false; |
| |
| return players->second.find(player_id.delegate_id) != players->second.end(); |
| } |
| |
| #if defined(OS_ANDROID) |
| static void SuspendAllMediaPlayersInRenderFrame( |
| RenderFrameHost* render_frame_host) { |
| render_frame_host->Send(new MediaPlayerDelegateMsg_SuspendAllMediaPlayers( |
| render_frame_host->GetRoutingID())); |
| } |
| #endif // defined(OS_ANDROID) |
| |
| } // anonymous namespace |
| |
| MediaWebContentsObserver::MediaWebContentsObserver(WebContents* web_contents) |
| : WebContentsObserver(web_contents), |
| audible_metrics_(GetAudibleMetrics()), |
| session_controllers_manager_(this) {} |
| |
| MediaWebContentsObserver::~MediaWebContentsObserver() = default; |
| |
| void MediaWebContentsObserver::WebContentsDestroyed() { |
| AudioStreamMonitor* audio_stream_monitor = |
| web_contents_impl()->audio_stream_monitor(); |
| |
| audible_metrics_->WebContentsDestroyed( |
| web_contents(), audio_stream_monitor->WasRecentlyAudible() && |
| !web_contents()->IsAudioMuted()); |
| } |
| |
| void MediaWebContentsObserver::RenderFrameDeleted( |
| RenderFrameHost* render_frame_host) { |
| ClearWakeLocks(render_frame_host); |
| session_controllers_manager_.RenderFrameDeleted(render_frame_host); |
| |
| if (fullscreen_player_ && |
| fullscreen_player_->render_frame_host == 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(); |
| |
| audible_metrics_->UpdateAudibleWebContentsState( |
| web_contents(), audio_stream_monitor->IsCurrentlyAudible() && |
| !web_contents()->IsAudioMuted()); |
| } |
| |
| 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_; |
| } |
| |
| 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_UNHANDLED(handled = false) |
| IPC_END_MESSAGE_MAP() |
| return handled; |
| } |
| |
| void MediaWebContentsObserver::DidUpdateAudioMutingState(bool muted) { |
| session_controllers_manager_.WebContentsMutedStateChanged(muted); |
| } |
| |
| 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. |
| fullscreen_player_->render_frame_host->Send( |
| new MediaPlayerDelegateMsg_BecamePersistentVideo( |
| fullscreen_player_->render_frame_host->GetRoutingID(), |
| fullscreen_player_->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_); |
| |
| 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) { |
| // TODO(mlamouri): this used to be done to avoid video wake lock. However, it |
| // was doing much more. Removing will be done in a follow-up CL to avoid |
| // regressions to be pinpoint to the wake lock refactor. |
| 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_); |
| |
| if (!session_controllers_manager_.RequestPlay( |
| id, has_audio, is_remote, media_content_type)) { |
| return; |
| } |
| |
| // Notify observers of the new player. |
| 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::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())); |
| |
| // 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(); |
| } |
| |
| 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::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.render_frame_host].insert(id.delegate_id); |
| } |
| |
| bool MediaWebContentsObserver::RemoveMediaPlayerEntry( |
| const MediaPlayerId& id, |
| ActiveMediaPlayerMap* player_map) { |
| auto it = player_map->find(id.render_frame_host); |
| if (it == player_map->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 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()); |
| } |
| |
| #if defined(OS_ANDROID) |
| void MediaWebContentsObserver::SuspendAllMediaPlayers() { |
| web_contents()->ForEachFrame( |
| base::BindRepeating(&SuspendAllMediaPlayersInRenderFrame)); |
| } |
| #endif // defined(OS_ANDROID) |
| |
| } // namespace content |