| // Copyright 2015 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/renderer/media/renderer_webmediaplayer_delegate.h" |
| |
| #include <stdint.h> |
| |
| #include "base/auto_reset.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/metrics/user_metrics_action.h" |
| #include "base/sys_info.h" |
| #include "content/common/media/media_player_delegate_messages.h" |
| #include "content/public/common/content_client.h" |
| #include "content/public/renderer/content_renderer_client.h" |
| #include "content/public/renderer/render_frame.h" |
| #include "content/public/renderer/render_thread.h" |
| #include "third_party/blink/public/platform/web_fullscreen_video_status.h" |
| #include "third_party/blink/public/platform/web_size.h" |
| #include "third_party/blink/public/web/web_scoped_user_gesture.h" |
| #include "ui/gfx/geometry/size.h" |
| |
| #if defined(OS_ANDROID) |
| #include "base/android/build_info.h" |
| #endif |
| |
| namespace { |
| |
| void RecordAction(const base::UserMetricsAction& action) { |
| content::RenderThread::Get()->RecordAction(action); |
| } |
| |
| } // namespace |
| |
| namespace media { |
| |
| RendererWebMediaPlayerDelegate::RendererWebMediaPlayerDelegate( |
| content::RenderFrame* render_frame) |
| : RenderFrameObserver(render_frame), |
| allow_idle_cleanup_( |
| content::GetContentClient()->renderer()->AllowIdleMediaSuspend()), |
| tick_clock_(base::DefaultTickClock::GetInstance()) { |
| idle_cleanup_interval_ = base::TimeDelta::FromSeconds(5); |
| idle_timeout_ = base::TimeDelta::FromSeconds(15); |
| |
| is_jelly_bean_ = false; |
| |
| #if defined(OS_ANDROID) |
| // On Android, due to the instability of the OS level media components, we |
| // consider all pre-KitKat devices to be potentially buggy. |
| is_jelly_bean_ |= base::android::BuildInfo::GetInstance()->sdk_int() <= |
| base::android::SDK_VERSION_JELLY_BEAN_MR2; |
| #endif |
| |
| idle_cleanup_timer_.SetTaskRunner( |
| render_frame->GetTaskRunner(blink::TaskType::kInternalMedia)); |
| } |
| |
| RendererWebMediaPlayerDelegate::~RendererWebMediaPlayerDelegate() {} |
| |
| bool RendererWebMediaPlayerDelegate::IsFrameHidden() { |
| if (is_frame_hidden_for_testing_) |
| return true; |
| |
| return (render_frame() && render_frame()->IsHidden()) || is_frame_closed_; |
| } |
| |
| bool RendererWebMediaPlayerDelegate::IsFrameClosed() { |
| return is_frame_closed_; |
| } |
| |
| int RendererWebMediaPlayerDelegate::AddObserver(Observer* observer) { |
| return id_map_.Add(observer); |
| } |
| |
| void RendererWebMediaPlayerDelegate::RemoveObserver(int player_id) { |
| DCHECK(id_map_.Lookup(player_id)); |
| id_map_.Remove(player_id); |
| idle_player_map_.erase(player_id); |
| stale_players_.erase(player_id); |
| playing_videos_.erase(player_id); |
| |
| Send( |
| new MediaPlayerDelegateHostMsg_OnMediaDestroyed(routing_id(), player_id)); |
| |
| ScheduleUpdateTask(); |
| } |
| |
| void RendererWebMediaPlayerDelegate::DidPlay( |
| int player_id, |
| bool has_video, |
| bool has_audio, |
| MediaContentType media_content_type) { |
| DVLOG(2) << __func__ << "(" << player_id << ", " << has_video << ", " |
| << has_audio << ", " << static_cast<int>(media_content_type) << ")"; |
| DCHECK(id_map_.Lookup(player_id)); |
| |
| has_played_media_ = true; |
| if (has_video) { |
| if (!playing_videos_.count(player_id)) { |
| playing_videos_.insert(player_id); |
| has_played_video_ = true; |
| } |
| } else { |
| playing_videos_.erase(player_id); |
| } |
| |
| Send(new MediaPlayerDelegateHostMsg_OnMediaPlaying( |
| routing_id(), player_id, has_video, has_audio, false, |
| media_content_type)); |
| |
| ScheduleUpdateTask(); |
| } |
| |
| void RendererWebMediaPlayerDelegate::DidPlayerMutedStatusChange(int delegate_id, |
| bool muted) { |
| Send(new MediaPlayerDelegateHostMsg_OnMutedStatusChanged(routing_id(), |
| delegate_id, muted)); |
| } |
| |
| void RendererWebMediaPlayerDelegate::DidPictureInPictureModeStart( |
| int delegate_id, |
| const viz::SurfaceId& surface_id, |
| const gfx::Size& natural_size, |
| blink::WebMediaPlayer::PipWindowSizeCallback callback) { |
| int request_id = next_picture_in_picture_callback_id_++; |
| enter_picture_in_picture_callback_map_.insert( |
| std::make_pair(request_id, std::move(callback))); |
| Send(new MediaPlayerDelegateHostMsg_OnPictureInPictureModeStarted( |
| routing_id(), delegate_id, surface_id, natural_size, request_id)); |
| } |
| |
| void RendererWebMediaPlayerDelegate::DidPictureInPictureModeEnd( |
| int delegate_id, |
| base::OnceClosure callback) { |
| int request_id = next_picture_in_picture_callback_id_++; |
| exit_picture_in_picture_callback_map_.insert( |
| std::make_pair(request_id, std::move(callback))); |
| Send(new MediaPlayerDelegateHostMsg_OnPictureInPictureModeEnded( |
| routing_id(), delegate_id, request_id)); |
| } |
| |
| void RendererWebMediaPlayerDelegate::DidPause(int player_id) { |
| DVLOG(2) << __func__ << "(" << player_id << ")"; |
| DCHECK(id_map_.Lookup(player_id)); |
| playing_videos_.erase(player_id); |
| Send(new MediaPlayerDelegateHostMsg_OnMediaPaused(routing_id(), player_id, |
| false)); |
| |
| // Required to keep background playback statistics up to date. |
| ScheduleUpdateTask(); |
| } |
| |
| void RendererWebMediaPlayerDelegate::PlayerGone(int player_id) { |
| DVLOG(2) << __func__ << "(" << player_id << ")"; |
| DCHECK(id_map_.Lookup(player_id)); |
| playing_videos_.erase(player_id); |
| Send( |
| new MediaPlayerDelegateHostMsg_OnMediaDestroyed(routing_id(), player_id)); |
| |
| // Required to keep background playback statistics up to date. |
| ScheduleUpdateTask(); |
| } |
| |
| void RendererWebMediaPlayerDelegate::SetIdle(int player_id, bool is_idle) { |
| DVLOG(2) << __func__ << "(" << player_id << ", " << is_idle << ")"; |
| |
| if (is_idle == IsIdle(player_id)) |
| return; |
| |
| if (is_idle) { |
| idle_player_map_[player_id] = tick_clock_->NowTicks(); |
| } else { |
| idle_player_map_.erase(player_id); |
| stale_players_.erase(player_id); |
| } |
| |
| ScheduleUpdateTask(); |
| } |
| |
| bool RendererWebMediaPlayerDelegate::IsIdle(int player_id) { |
| return idle_player_map_.count(player_id) || stale_players_.count(player_id); |
| } |
| |
| void RendererWebMediaPlayerDelegate::ClearStaleFlag(int player_id) { |
| DVLOG(2) << __func__ << "(" << player_id << ")"; |
| |
| if (!stale_players_.erase(player_id)) |
| return; |
| |
| // Set the idle time such that the player will be considered stale the next |
| // time idle cleanup runs. |
| idle_player_map_[player_id] = tick_clock_->NowTicks() - idle_timeout_; |
| |
| // No need to call Update immediately, just make sure the idle |
| // timer is running. Calling ScheduleUpdateTask() here will cause |
| // immediate cleanup, and if that fails, this function gets called |
| // again which uses 100% cpu until resolved. |
| if (!idle_cleanup_timer_.IsRunning() && !pending_update_task_) { |
| idle_cleanup_timer_.Start( |
| FROM_HERE, idle_cleanup_interval_, |
| base::Bind(&RendererWebMediaPlayerDelegate::UpdateTask, |
| base::Unretained(this))); |
| } |
| } |
| |
| bool RendererWebMediaPlayerDelegate::IsStale(int player_id) { |
| return stale_players_.count(player_id); |
| } |
| |
| void RendererWebMediaPlayerDelegate::SetIsEffectivelyFullscreen( |
| int player_id, |
| blink::WebFullscreenVideoStatus fullscreen_video_status) { |
| Send(new MediaPlayerDelegateHostMsg_OnMediaEffectivelyFullscreenChanged( |
| routing_id(), player_id, fullscreen_video_status)); |
| } |
| |
| void RendererWebMediaPlayerDelegate::DidPlayerSizeChange( |
| int delegate_id, |
| const gfx::Size& size) { |
| Send(new MediaPlayerDelegateHostMsg_OnMediaSizeChanged(routing_id(), |
| delegate_id, size)); |
| } |
| |
| void RendererWebMediaPlayerDelegate::WasHidden() { |
| RecordAction(base::UserMetricsAction("Media.Hidden")); |
| |
| for (base::IDMap<Observer*>::iterator it(&id_map_); !it.IsAtEnd(); |
| it.Advance()) |
| it.GetCurrentValue()->OnFrameHidden(); |
| |
| ScheduleUpdateTask(); |
| } |
| |
| void RendererWebMediaPlayerDelegate::WasShown() { |
| RecordAction(base::UserMetricsAction("Media.Shown")); |
| is_frame_closed_ = false; |
| |
| for (base::IDMap<Observer*>::iterator it(&id_map_); !it.IsAtEnd(); |
| it.Advance()) |
| it.GetCurrentValue()->OnFrameShown(); |
| |
| ScheduleUpdateTask(); |
| } |
| |
| bool RendererWebMediaPlayerDelegate::OnMessageReceived( |
| const IPC::Message& msg) { |
| IPC_BEGIN_MESSAGE_MAP(RendererWebMediaPlayerDelegate, msg) |
| IPC_MESSAGE_HANDLER(MediaPlayerDelegateMsg_Pause, OnMediaDelegatePause) |
| IPC_MESSAGE_HANDLER(MediaPlayerDelegateMsg_Play, OnMediaDelegatePlay) |
| IPC_MESSAGE_HANDLER(MediaPlayerDelegateMsg_SeekForward, |
| OnMediaDelegateSeekForward) |
| IPC_MESSAGE_HANDLER(MediaPlayerDelegateMsg_SeekBackward, |
| OnMediaDelegateSeekBackward) |
| IPC_MESSAGE_HANDLER(MediaPlayerDelegateMsg_SuspendAllMediaPlayers, |
| OnMediaDelegateSuspendAllMediaPlayers) |
| IPC_MESSAGE_HANDLER(MediaPlayerDelegateMsg_UpdateVolumeMultiplier, |
| OnMediaDelegateVolumeMultiplierUpdate) |
| IPC_MESSAGE_HANDLER(MediaPlayerDelegateMsg_BecamePersistentVideo, |
| OnMediaDelegateBecamePersistentVideo) |
| IPC_MESSAGE_HANDLER(MediaPlayerDelegateMsg_EndPictureInPictureMode, |
| OnPictureInPictureModeEnded) |
| IPC_MESSAGE_HANDLER(MediaPlayerDelegateMsg_OnPictureInPictureModeEnded_ACK, |
| OnPictureInPictureModeEndedAck) |
| IPC_MESSAGE_HANDLER( |
| MediaPlayerDelegateMsg_OnPictureInPictureModeStarted_ACK, |
| OnPictureInPictureModeStartedAck) |
| IPC_MESSAGE_UNHANDLED(return false) |
| IPC_END_MESSAGE_MAP() |
| return true; |
| } |
| |
| void RendererWebMediaPlayerDelegate::SetIdleCleanupParamsForTesting( |
| base::TimeDelta idle_timeout, |
| base::TimeDelta idle_cleanup_interval, |
| const base::TickClock* tick_clock, |
| bool is_jelly_bean) { |
| idle_cleanup_interval_ = idle_cleanup_interval; |
| idle_timeout_ = idle_timeout; |
| tick_clock_ = tick_clock; |
| is_jelly_bean_ = is_jelly_bean; |
| } |
| |
| bool RendererWebMediaPlayerDelegate::IsIdleCleanupTimerRunningForTesting() |
| const { |
| return idle_cleanup_timer_.IsRunning(); |
| } |
| |
| void RendererWebMediaPlayerDelegate::SetFrameHiddenForTesting(bool is_hidden) { |
| if (is_hidden == is_frame_hidden_for_testing_) |
| return; |
| |
| is_frame_hidden_for_testing_ = is_hidden; |
| |
| ScheduleUpdateTask(); |
| } |
| |
| void RendererWebMediaPlayerDelegate::OnMediaDelegatePause(int player_id) { |
| RecordAction(base::UserMetricsAction("Media.Controls.RemotePause")); |
| |
| Observer* observer = id_map_.Lookup(player_id); |
| if (observer) { |
| // TODO(avayvod): remove when default play/pause is handled via |
| // the MediaSession code path. |
| std::unique_ptr<blink::WebScopedUserGesture> gesture( |
| render_frame() |
| ? new blink::WebScopedUserGesture(render_frame()->GetWebFrame()) |
| : nullptr); |
| observer->OnPause(); |
| } |
| } |
| |
| void RendererWebMediaPlayerDelegate::OnMediaDelegatePlay(int player_id) { |
| RecordAction(base::UserMetricsAction("Media.Controls.RemotePlay")); |
| |
| Observer* observer = id_map_.Lookup(player_id); |
| if (observer) { |
| // TODO(avayvod): remove when default play/pause is handled via |
| // the MediaSession code path. |
| std::unique_ptr<blink::WebScopedUserGesture> gesture( |
| render_frame() |
| ? new blink::WebScopedUserGesture(render_frame()->GetWebFrame()) |
| : nullptr); |
| observer->OnPlay(); |
| } |
| } |
| |
| void RendererWebMediaPlayerDelegate::OnMediaDelegateSeekForward( |
| int player_id, |
| base::TimeDelta seek_time) { |
| RecordAction(base::UserMetricsAction("Media.Controls.RemoteSeekForward")); |
| |
| Observer* observer = id_map_.Lookup(player_id); |
| if (observer) |
| observer->OnSeekForward(seek_time.InSecondsF()); |
| } |
| |
| void RendererWebMediaPlayerDelegate::OnMediaDelegateSeekBackward( |
| int player_id, |
| base::TimeDelta seek_time) { |
| RecordAction(base::UserMetricsAction("Media.Controls.RemoteSeekBackward")); |
| |
| Observer* observer = id_map_.Lookup(player_id); |
| if (observer) |
| observer->OnSeekBackward(seek_time.InSecondsF()); |
| } |
| |
| void RendererWebMediaPlayerDelegate::OnMediaDelegateSuspendAllMediaPlayers() { |
| is_frame_closed_ = true; |
| |
| for (base::IDMap<Observer*>::iterator it(&id_map_); !it.IsAtEnd(); |
| it.Advance()) |
| it.GetCurrentValue()->OnFrameClosed(); |
| } |
| |
| void RendererWebMediaPlayerDelegate::OnMediaDelegateVolumeMultiplierUpdate( |
| int player_id, |
| double multiplier) { |
| Observer* observer = id_map_.Lookup(player_id); |
| if (observer) |
| observer->OnVolumeMultiplierUpdate(multiplier); |
| } |
| |
| void RendererWebMediaPlayerDelegate::OnMediaDelegateBecamePersistentVideo( |
| int player_id, |
| bool value) { |
| Observer* observer = id_map_.Lookup(player_id); |
| if (observer) |
| observer->OnBecamePersistentVideo(value); |
| } |
| |
| void RendererWebMediaPlayerDelegate::OnPictureInPictureModeEnded( |
| int player_id) { |
| Observer* observer = id_map_.Lookup(player_id); |
| if (observer) |
| observer->OnPictureInPictureModeEnded(); |
| } |
| |
| void RendererWebMediaPlayerDelegate::OnPictureInPictureModeEndedAck( |
| int player_id, |
| int request_id) { |
| auto iter = exit_picture_in_picture_callback_map_.find(request_id); |
| DCHECK(iter != exit_picture_in_picture_callback_map_.end()); |
| |
| std::move(iter->second).Run(); |
| exit_picture_in_picture_callback_map_.erase(iter); |
| } |
| |
| void RendererWebMediaPlayerDelegate::OnPictureInPictureModeStartedAck( |
| int player_id, |
| int request_id, |
| const gfx::Size& window_size) { |
| auto iter = enter_picture_in_picture_callback_map_.find(request_id); |
| DCHECK(iter != enter_picture_in_picture_callback_map_.end()); |
| |
| std::move(iter->second).Run(blink::WebSize(window_size)); |
| enter_picture_in_picture_callback_map_.erase(iter); |
| } |
| |
| void RendererWebMediaPlayerDelegate::ScheduleUpdateTask() { |
| if (!pending_update_task_) { |
| base::ThreadTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, base::BindOnce(&RendererWebMediaPlayerDelegate::UpdateTask, |
| AsWeakPtr())); |
| pending_update_task_ = true; |
| } |
| } |
| |
| void RendererWebMediaPlayerDelegate::UpdateTask() { |
| DVLOG(3) << __func__; |
| pending_update_task_ = false; |
| |
| // Check whether a player was played since the last UpdateTask(). We basically |
| // treat this as a parameter to UpdateTask(), except that it can be changed |
| // between posting the task and UpdateTask() executing. |
| bool has_played_video_since_last_update_task = has_played_video_; |
| has_played_video_ = false; |
| |
| // Record UMAs for background video playback. |
| RecordBackgroundVideoPlayback(); |
| |
| if (!allow_idle_cleanup_) |
| return; |
| |
| // Clean up idle players. |
| bool aggressive_cleanup = false; |
| |
| // When we reach the maximum number of idle players, clean them up |
| // aggressively. Values chosen after testing on a Galaxy Nexus device for |
| // http://crbug.com/612909. |
| if (idle_player_map_.size() > (is_jelly_bean_ ? 2u : 8u)) |
| aggressive_cleanup = true; |
| |
| // When a player plays on a buggy old device, clean up idle players |
| // aggressively. |
| if (has_played_video_since_last_update_task && is_jelly_bean_) |
| aggressive_cleanup = true; |
| |
| CleanUpIdlePlayers(aggressive_cleanup ? base::TimeDelta() : idle_timeout_); |
| |
| // If there are still idle players, schedule an attempt to clean them up. |
| // This construct ensures that the next callback is always |
| // |idle_cleanup_interval_| from now. |
| idle_cleanup_timer_.Stop(); |
| if (!idle_player_map_.empty()) { |
| idle_cleanup_timer_.Start( |
| FROM_HERE, idle_cleanup_interval_, |
| base::Bind(&RendererWebMediaPlayerDelegate::UpdateTask, |
| base::Unretained(this))); |
| } |
| } |
| |
| void RendererWebMediaPlayerDelegate::RecordBackgroundVideoPlayback() { |
| #if defined(OS_ANDROID) |
| // TODO(avayvod): This would be useful to collect on desktop too and express |
| // in actual media watch time vs. just elapsed time. |
| // See https://crbug.com/638726. |
| bool has_playing_background_video = |
| IsFrameHidden() && !IsFrameClosed() && !playing_videos_.empty(); |
| |
| if (has_playing_background_video != was_playing_background_video_) { |
| was_playing_background_video_ = has_playing_background_video; |
| |
| if (has_playing_background_video) { |
| RecordAction(base::UserMetricsAction("Media.Session.BackgroundResume")); |
| background_video_start_time_ = base::TimeTicks::Now(); |
| } else { |
| RecordAction(base::UserMetricsAction("Media.Session.BackgroundSuspend")); |
| UMA_HISTOGRAM_CUSTOM_TIMES( |
| "Media.Android.BackgroundVideoTime", |
| base::TimeTicks::Now() - background_video_start_time_, |
| base::TimeDelta::FromSeconds(7), base::TimeDelta::FromHours(10), 50); |
| } |
| } |
| #endif // OS_ANDROID |
| } |
| |
| void RendererWebMediaPlayerDelegate::CleanUpIdlePlayers( |
| base::TimeDelta timeout) { |
| const base::TimeTicks now = tick_clock_->NowTicks(); |
| |
| // Create a list of stale players before making any possibly reentrant calls |
| // to OnIdleTimeout(). |
| std::vector<int> stale_players; |
| for (const auto& it : idle_player_map_) { |
| if (now - it.second >= timeout) |
| stale_players.push_back(it.first); |
| } |
| |
| // Notify stale players. |
| for (int player_id : stale_players) { |
| Observer* player = id_map_.Lookup(player_id); |
| if (player && idle_player_map_.erase(player_id)) { |
| stale_players_.insert(player_id); |
| player->OnIdleTimeout(); |
| } |
| } |
| } |
| |
| void RendererWebMediaPlayerDelegate::OnDestruct() { |
| delete this; |
| } |
| |
| } // namespace media |