blob: d29d23b5ea7c722a58d64d410c6ee7807c9c53c9 [file] [log] [blame]
// 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