Commit d0ab2edb authored by dalecurtis's avatar dalecurtis Committed by Commit bot

Reduce number of active codecs on low end devices.

This fixes OOM crashes in the mediaserver process on older devices
by suspending players more frequently and preventing unused players
and SurfaceTextures from ever being created. Both fixes are required
to enable playback on JellyBean devices; independently neither is
sufficient for playback to succeed.

- On low end devices, suspends all idle players immediately upon
  playback of another player.
- On low end devices, if a codec already exists, codec creation is
  deferred until the first Decode() call. Resolves issues with sites
  like vimeo.com that are appending initialization segments to MSE
  but never any further data.
- Once more than 2 players exist on a low end device, every extra
  player will cause immediate idle player collection. Normal devices
  will now do this after 8 players.

A followup CL will set preload=none for all JellyBean devices.

BUG=612909
TEST=manual, new tests.

Review-Url: https://codereview.chromium.org/2333983002
Cr-Commit-Position: refs/heads/master@{#419375}
parent 69142169
......@@ -9,11 +9,16 @@
#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/renderer/render_frame.h"
#include "content/public/renderer/render_thread.h"
#include "third_party/WebKit/public/platform/WebMediaPlayer.h"
#if defined(OS_ANDROID)
#include "base/android/build_info.h"
#endif
namespace {
void RecordAction(const base::UserMetricsAction& action) {
......@@ -27,10 +32,21 @@ namespace media {
RendererWebMediaPlayerDelegate::RendererWebMediaPlayerDelegate(
content::RenderFrame* render_frame)
: RenderFrameObserver(render_frame),
idle_cleanup_timer_(true, true),
default_tick_clock_(new base::DefaultTickClock()),
tick_clock_(default_tick_clock_.get()) {
idle_cleanup_interval_ = base::TimeDelta::FromSeconds(5);
idle_timeout_ = base::TimeDelta::FromSeconds(15);
// To conserve resources, cleanup idle players more often on low end devices.
is_low_end_device_ = base::SysInfo::IsLowEndDevice();
#if defined(OS_ANDROID)
// On Android, due to the instability of the OS level media components, we
// consider all pre-KitKat devices to be low end.
is_low_end_device_ |=
base::android::BuildInfo::GetInstance()->sdk_int() <= 18;
#endif
}
RendererWebMediaPlayerDelegate::~RendererWebMediaPlayerDelegate() {}
......@@ -63,6 +79,11 @@ void RendererWebMediaPlayerDelegate::DidPlay(
else
playing_videos_.erase(delegate_id);
RemoveIdleDelegate(delegate_id);
// Upon receipt of a playback request, suspend everything that's not used.
if (is_low_end_device_)
CleanupIdleDelegates(base::TimeDelta());
Send(new MediaPlayerDelegateHostMsg_OnMediaPlaying(
routing_id(), delegate_id, has_video, has_audio, is_remote,
media_content_type));
......@@ -80,7 +101,6 @@ void RendererWebMediaPlayerDelegate::DidPause(int delegate_id,
void RendererWebMediaPlayerDelegate::PlayerGone(int delegate_id) {
DCHECK(id_map_.Lookup(delegate_id));
RemoveIdleDelegate(delegate_id);
playing_videos_.erase(delegate_id);
Send(new MediaPlayerDelegateHostMsg_OnMediaDestroyed(routing_id(),
delegate_id));
......@@ -126,10 +146,12 @@ bool RendererWebMediaPlayerDelegate::OnMessageReceived(
void RendererWebMediaPlayerDelegate::SetIdleCleanupParamsForTesting(
base::TimeDelta idle_timeout,
base::TickClock* tick_clock) {
base::TickClock* tick_clock,
bool is_low_end_device) {
idle_cleanup_interval_ = base::TimeDelta();
idle_timeout_ = idle_timeout;
tick_clock_ = tick_clock;
is_low_end_device_ = is_low_end_device;
}
void RendererWebMediaPlayerDelegate::OnMediaDelegatePause(int delegate_id) {
......@@ -171,9 +193,16 @@ void RendererWebMediaPlayerDelegate::AddIdleDelegate(int delegate_id) {
idle_delegate_map_[delegate_id] = tick_clock_->NowTicks();
if (!idle_cleanup_timer_.IsRunning()) {
idle_cleanup_timer_.Start(
FROM_HERE, idle_cleanup_interval_, this,
&RendererWebMediaPlayerDelegate::CleanupIdleDelegates);
FROM_HERE, idle_cleanup_interval_,
base::Bind(&RendererWebMediaPlayerDelegate::CleanupIdleDelegates,
base::Unretained(this), idle_timeout_));
}
// When we reach the maximum number of idle players, aggressively suspend idle
// delegates to try and remain under the limit. Values chosen after testing on
// a Galaxy Nexus device for http://crbug.com/612909.
if (idle_delegate_map_.size() > (is_low_end_device_ ? 2u : 8u))
CleanupIdleDelegates(base::TimeDelta());
}
void RendererWebMediaPlayerDelegate::RemoveIdleDelegate(int delegate_id) {
......@@ -189,14 +218,15 @@ void RendererWebMediaPlayerDelegate::RemoveIdleDelegate(int delegate_id) {
idle_cleanup_timer_.Stop();
}
void RendererWebMediaPlayerDelegate::CleanupIdleDelegates() {
void RendererWebMediaPlayerDelegate::CleanupIdleDelegates(
base::TimeDelta timeout) {
// Iterate over the delegates and suspend the idle ones. Note: The call to
// OnHidden() can trigger calls into RemoveIdleDelegate(), so for iterator
// validity we set |idle_cleanup_running_| to true and defer deletions.
base::AutoReset<bool> scoper(&idle_cleanup_running_, true);
const base::TimeTicks now = tick_clock_->NowTicks();
for (auto& idle_delegate_entry : idle_delegate_map_) {
if (now - idle_delegate_entry.second > idle_timeout_) {
if (now - idle_delegate_entry.second > timeout) {
id_map_.Lookup(idle_delegate_entry.first)->OnSuspendRequested(false);
// Whether or not the player accepted the suspension, mark it for removal
......
......@@ -63,11 +63,12 @@ class CONTENT_EXPORT RendererWebMediaPlayerDelegate
bool OnMessageReceived(const IPC::Message& msg) override;
void OnDestruct() override;
// Zeros out |idle_cleanup_interval_|, and sets |idle_timeout_| to
// |idle_timeout|. A zero cleanup interval will cause the idle timer to run
// with each run of the message loop.
// Zeros out |idle_cleanup_interval_|, sets |idle_timeout_| to |idle_timeout|,
// and |is_low_end_device_| to |is_low_end_device|. A zero cleanup interval
// will cause the idle timer to run with each run of the message loop.
void SetIdleCleanupParamsForTesting(base::TimeDelta idle_timeout,
base::TickClock* tick_clock);
base::TickClock* tick_clock,
bool is_low_end_device);
bool IsIdleCleanupTimerRunningForTesting() const {
return idle_cleanup_timer_.IsRunning();
}
......@@ -86,8 +87,9 @@ class CONTENT_EXPORT RendererWebMediaPlayerDelegate
void AddIdleDelegate(int delegate_id);
void RemoveIdleDelegate(int delegate_id);
// Runs periodically to suspend idle delegates in |idle_delegate_map_|.
void CleanupIdleDelegates();
// Runs periodically to suspend idle delegates in |idle_delegate_map_| which
// have been idle for longer than |timeout|.
void CleanupIdleDelegates(base::TimeDelta timeout);
// Setter for |is_playing_background_video_| that updates the metrics.
void SetIsPlayingBackgroundVideo(bool is_playing);
......@@ -99,7 +101,7 @@ class CONTENT_EXPORT RendererWebMediaPlayerDelegate
// inactivity these players will be suspended to release unused resources.
bool idle_cleanup_running_ = false;
std::map<int, base::TimeTicks> idle_delegate_map_;
base::RepeatingTimer idle_cleanup_timer_;
base::Timer idle_cleanup_timer_;
// Amount of time allowed to elapse after a delegate enters the paused before
// the delegate is suspended.
......@@ -129,6 +131,10 @@ class CONTENT_EXPORT RendererWebMediaPlayerDelegate
// not.
std::set<int> playing_videos_;
// Determined at construction time based on system information; determines
// when the idle cleanup timer should be fired more aggressively.
bool is_low_end_device_;
DISALLOW_COPY_AND_ASSIGN(RendererWebMediaPlayerDelegate);
};
......
......@@ -173,13 +173,89 @@ TEST_F(RendererWebMediaPlayerDelegateTest, DeliversObserverNotifications) {
delegate_manager_->RemoveObserver(delegate_id);
}
TEST_F(RendererWebMediaPlayerDelegateTest, PlaySuspendsLowEndIdleDelegates) {
// Start the tick clock off at a non-null value.
base::SimpleTestTickClock tick_clock;
tick_clock.Advance(base::TimeDelta::FromSeconds(1234));
const base::TimeDelta kIdleTimeout = base::TimeDelta::FromSeconds(10);
delegate_manager_->SetIdleCleanupParamsForTesting(kIdleTimeout, &tick_clock,
true);
EXPECT_FALSE(delegate_manager_->IsIdleCleanupTimerRunningForTesting());
// Add two observers, both of which should keep the idle timer running.
testing::StrictMock<MockWebMediaPlayerDelegateObserver> observer_1;
const int delegate_id_1 = delegate_manager_->AddObserver(&observer_1);
EXPECT_TRUE(delegate_manager_->IsIdleCleanupTimerRunningForTesting());
testing::StrictMock<MockWebMediaPlayerDelegateObserver> observer_2;
const int delegate_id_2 = delegate_manager_->AddObserver(&observer_2);
EXPECT_TRUE(delegate_manager_->IsIdleCleanupTimerRunningForTesting());
// Calling play on the first player should suspend the other idle player.
EXPECT_CALL(observer_2, OnSuspendRequested(false))
.WillOnce(RunClosure(base::Bind(
&RendererWebMediaPlayerDelegate::PlayerGone,
base::Unretained(delegate_manager_.get()), delegate_id_2)));
base::RunLoop run_loop;
base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE,
run_loop.QuitClosure());
tick_clock.Advance(base::TimeDelta::FromMicroseconds(1));
delegate_manager_->DidPlay(delegate_id_1, true, true, false,
media::MediaContentType::Persistent);
run_loop.Run();
}
TEST_F(RendererWebMediaPlayerDelegateTest, MaxLowEndIdleDelegates) {
// Start the tick clock off at a non-null value.
base::SimpleTestTickClock tick_clock;
tick_clock.Advance(base::TimeDelta::FromSeconds(1234));
const base::TimeDelta kIdleTimeout = base::TimeDelta::FromSeconds(10);
delegate_manager_->SetIdleCleanupParamsForTesting(kIdleTimeout, &tick_clock,
true);
EXPECT_FALSE(delegate_manager_->IsIdleCleanupTimerRunningForTesting());
// Add two observers, both of which should keep the idle timer running.
testing::StrictMock<MockWebMediaPlayerDelegateObserver> observer_1;
const int delegate_id_1 = delegate_manager_->AddObserver(&observer_1);
EXPECT_TRUE(delegate_manager_->IsIdleCleanupTimerRunningForTesting());
testing::StrictMock<MockWebMediaPlayerDelegateObserver> observer_2;
const int delegate_id_2 = delegate_manager_->AddObserver(&observer_2);
EXPECT_TRUE(delegate_manager_->IsIdleCleanupTimerRunningForTesting());
tick_clock.Advance(base::TimeDelta::FromMicroseconds(1));
// Just adding a third idle observer should suspend the others.
EXPECT_CALL(observer_1, OnSuspendRequested(false))
.WillOnce(RunClosure(base::Bind(
&RendererWebMediaPlayerDelegate::PlayerGone,
base::Unretained(delegate_manager_.get()), delegate_id_1)));
EXPECT_CALL(observer_2, OnSuspendRequested(false))
.WillOnce(RunClosure(base::Bind(
&RendererWebMediaPlayerDelegate::PlayerGone,
base::Unretained(delegate_manager_.get()), delegate_id_2)));
base::RunLoop run_loop;
base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE,
run_loop.QuitClosure());
testing::StrictMock<MockWebMediaPlayerDelegateObserver> observer_3;
delegate_manager_->AddObserver(&observer_3);
EXPECT_TRUE(delegate_manager_->IsIdleCleanupTimerRunningForTesting());
run_loop.Run();
}
TEST_F(RendererWebMediaPlayerDelegateTest, IdleDelegatesAreSuspended) {
// Start the tick clock off at a non-null value.
base::SimpleTestTickClock tick_clock;
tick_clock.Advance(base::TimeDelta::FromSeconds(1234));
const base::TimeDelta kIdleTimeout = base::TimeDelta::FromSeconds(2);
delegate_manager_->SetIdleCleanupParamsForTesting(kIdleTimeout, &tick_clock);
delegate_manager_->SetIdleCleanupParamsForTesting(kIdleTimeout, &tick_clock,
false);
EXPECT_FALSE(delegate_manager_->IsIdleCleanupTimerRunningForTesting());
// Just adding an observer should start the idle timer.
......@@ -264,7 +340,8 @@ TEST_F(RendererWebMediaPlayerDelegateTest, IdleDelegatesIgnoresSuspendRequest) {
tick_clock.Advance(base::TimeDelta::FromSeconds(1234));
const base::TimeDelta kIdleTimeout = base::TimeDelta::FromSeconds(2);
delegate_manager_->SetIdleCleanupParamsForTesting(kIdleTimeout, &tick_clock);
delegate_manager_->SetIdleCleanupParamsForTesting(kIdleTimeout, &tick_clock,
false);
EXPECT_FALSE(delegate_manager_->IsIdleCleanupTimerRunningForTesting());
testing::StrictMock<MockWebMediaPlayerDelegateObserver> observer_1;
......
......@@ -1579,13 +1579,7 @@ void WebMediaPlayerImpl::UpdatePlayState() {
}
void WebMediaPlayerImpl::SetDelegateState(DelegateState new_state) {
if (!delegate_)
return;
// Dedupe state changes in the general case, but make an exception for gone
// since the delegate will use that information to decide when the idle timer
// should be fired.
if (delegate_state_ == new_state && new_state != DelegateState::GONE)
if (!delegate_ || delegate_state_ == new_state)
return;
delegate_state_ = new_state;
......
......@@ -276,12 +276,10 @@ TEST_F(WebMediaPlayerImplTest, DidLoadingProgressClearsIdle) {
EXPECT_CALL(strict_delegate, IsPlayingBackgroundVideo()).Times(AnyNumber());
InitializeWebMediaPlayerImpl(strict_delegate.AsWeakPtr());
EXPECT_FALSE(IsSuspended());
EXPECT_CALL(strict_delegate, PlayerGone(0));
wmpi_->OnSuspendRequested(false);
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(IsSuspended());
AddBufferedRanges();
EXPECT_CALL(strict_delegate, PlayerGone(0));
wmpi_->didLoadingProgress();
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(IsSuspended());
......
......@@ -18,6 +18,7 @@
#include "base/logging.h"
#include "base/message_loop/message_loop.h"
#include "base/metrics/histogram.h"
#include "base/sys_info.h"
#include "base/task_runner_util.h"
#include "base/threading/thread_checker.h"
#include "base/threading/thread_task_runner_handle.h"
......@@ -324,6 +325,16 @@ class AVDAManager {
waiter->OnSurfaceAvailable(true);
}
// On low end devices (< KitKat is always low-end due to buggy MediaCodec),
// defer the surface creation until the codec is actually used if we know no
// software fallback exists.
bool ShouldDeferSurfaceCreation(int surface_id, VideoCodec codec) {
return surface_id == AndroidVideoDecodeAccelerator::Config::kNoSurfaceID &&
codec == kCodecH264 && !thread_avda_instances_.empty() &&
(base::android::BuildInfo::GetInstance()->sdk_int() <= 18 ||
base::SysInfo::IsLowEndDevice());
}
private:
friend struct base::DefaultLazyInstanceTraits<AVDAManager>;
......@@ -425,6 +436,7 @@ AndroidVideoDecodeAccelerator::AndroidVideoDecodeAccelerator(
defer_errors_(false),
deferred_initialization_pending_(false),
codec_needs_reset_(false),
defer_surface_creation_(false),
weak_this_factory_(this) {}
AndroidVideoDecodeAccelerator::~AndroidVideoDecodeAccelerator() {
......@@ -471,15 +483,6 @@ bool AndroidVideoDecodeAccelerator::Initialize(const Config& config,
codec_config_->initial_expected_coded_size_ =
config.initial_expected_coded_size;
// We signalled that we support deferred initialization, so see if the client
// does also.
deferred_initialization_pending_ = config.is_deferred_initialization_allowed;
if (config_.is_encrypted && !deferred_initialization_pending_) {
DLOG(ERROR) << "Deferred initialization must be used for encrypted streams";
return false;
}
if (codec_config_->codec_ != kCodecVP8 &&
codec_config_->codec_ != kCodecVP9 &&
#if BUILDFLAG(ENABLE_HEVC_DEMUXING)
......@@ -507,13 +510,31 @@ bool AndroidVideoDecodeAccelerator::Initialize(const Config& config,
return false;
}
if (!make_context_current_cb_.Run()) {
LOG(ERROR) << "Failed to make this decoder's GL context current.";
// If we're low on resources, we may decide to defer creation of the surface
// until the codec is actually used.
if (g_avda_manager.Get().ShouldDeferSurfaceCreation(config_.surface_id,
codec_config_->codec_)) {
DCHECK(!deferred_initialization_pending_);
// We should never be here if a SurfaceView is required.
DCHECK_EQ(config_.surface_id, Config::kNoSurfaceID);
DCHECK(g_avda_manager.Get().AllocateSurface(config_.surface_id, this));
defer_surface_creation_ = true;
NotifyInitializationComplete(true);
return true;
}
// We signaled that we support deferred initialization, so see if the client
// does also.
deferred_initialization_pending_ = config.is_deferred_initialization_allowed;
if (config_.is_encrypted && !deferred_initialization_pending_) {
DLOG(ERROR) << "Deferred initialization must be used for encrypted streams";
return false;
}
if (g_avda_manager.Get().AllocateSurface(config_.surface_id, this)) {
// We have succesfully owned the surface, so finish initialization now.
// We have successfully owned the surface, so finish initialization now.
return InitializePictureBufferManager();
}
......@@ -524,6 +545,7 @@ bool AndroidVideoDecodeAccelerator::Initialize(const Config& config,
void AndroidVideoDecodeAccelerator::OnSurfaceAvailable(bool success) {
DCHECK(deferred_initialization_pending_);
DCHECK(!defer_surface_creation_);
if (!success || !InitializePictureBufferManager()) {
NotifyInitializationComplete(false);
......@@ -532,6 +554,11 @@ void AndroidVideoDecodeAccelerator::OnSurfaceAvailable(bool success) {
}
bool AndroidVideoDecodeAccelerator::InitializePictureBufferManager() {
if (!make_context_current_cb_.Run()) {
LOG(ERROR) << "Failed to make this decoder's GL context current.";
return false;
}
codec_config_->surface_ =
picture_buffer_manager_.Initialize(this, config_.surface_id);
if (codec_config_->surface_.IsEmpty())
......@@ -552,7 +579,8 @@ bool AndroidVideoDecodeAccelerator::InitializePictureBufferManager() {
return true;
}
if (deferred_initialization_pending_) {
if (deferred_initialization_pending_ || defer_surface_creation_) {
defer_surface_creation_ = false;
ConfigureMediaCodecAsynchronously();
return true;
}
......@@ -790,7 +818,7 @@ bool AndroidVideoDecodeAccelerator::DequeueOutput() {
// decoded images. Breaking their connection to the decoded image will
// cause rendering of black frames. Instead, we let the existing
// PictureBuffers live on and we simply update their size the next time
// they're attachted to an image of the new resolution. See the
// they're attached to an image of the new resolution. See the
// size update in |SendDecodedFrameToClient| and https://crbug/587994.
if (output_picture_buffers_.empty() && !picturebuffers_requested_) {
picturebuffers_requested_ = true;
......@@ -928,6 +956,12 @@ void AndroidVideoDecodeAccelerator::Decode(
const BitstreamBuffer& bitstream_buffer) {
DCHECK(thread_checker_.CalledOnValidThread());
if (defer_surface_creation_ && !InitializePictureBufferManager()) {
POST_ERROR(PLATFORM_FAILURE,
"Failed deferred surface and MediaCodec initialization.");
return;
}
// If we previously deferred a codec restart, take care of it now. This can
// happen on older devices where configuration changes require a codec reset.
if (codec_needs_reset_) {
......@@ -1027,7 +1061,7 @@ void AndroidVideoDecodeAccelerator::Flush() {
DVLOG(1) << __FUNCTION__;
DCHECK(thread_checker_.CalledOnValidThread());
if (state_ == SURFACE_DESTROYED)
if (state_ == SURFACE_DESTROYED || defer_surface_creation_)
NotifyFlushDone();
else
StartCodecDrain(DRAIN_FOR_FLUSH);
......@@ -1258,7 +1292,7 @@ void AndroidVideoDecodeAccelerator::ResetCodecState() {
// Flush the codec if possible, or create a new one if not.
if (!did_codec_error_happen &&
!media::MediaCodecUtil::CodecNeedsFlushWorkaround(media_codec_.get())) {
!MediaCodecUtil::CodecNeedsFlushWorkaround(media_codec_.get())) {
DVLOG(3) << __FUNCTION__ << " Flushing MediaCodec.";
media_codec_->Flush();
// Since we just flushed all the output buffers, make sure that nothing is
......@@ -1277,6 +1311,16 @@ void AndroidVideoDecodeAccelerator::Reset() {
DCHECK(thread_checker_.CalledOnValidThread());
TRACE_EVENT0("media", "AVDA::Reset");
if (defer_surface_creation_) {
DCHECK(!media_codec_);
DCHECK(pending_bitstream_records_.empty());
DCHECK_EQ(state_, NO_ERROR);
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::Bind(&AndroidVideoDecodeAccelerator::NotifyResetDone,
weak_this_factory_.GetWeakPtr()));
return;
}
while (!pending_bitstream_records_.empty()) {
int32_t bitstream_buffer_id =
pending_bitstream_records_.front().buffer.id();
......@@ -1635,10 +1679,10 @@ AndroidVideoDecodeAccelerator::GetCapabilities(
// is disabled (http://crbug.com/582170).
if (gpu_preferences.enable_threaded_texture_mailboxes) {
capabilities.flags |=
media::VideoDecodeAccelerator::Capabilities::REQUIRES_TEXTURE_COPY;
} else if (media::MediaCodecUtil::IsSurfaceViewOutputSupported()) {
capabilities.flags |= media::VideoDecodeAccelerator::Capabilities::
SUPPORTS_EXTERNAL_OUTPUT_SURFACE;
VideoDecodeAccelerator::Capabilities::REQUIRES_TEXTURE_COPY;
} else if (MediaCodecUtil::IsSurfaceViewOutputSupported()) {
capabilities.flags |=
VideoDecodeAccelerator::Capabilities::SUPPORTS_EXTERNAL_OUTPUT_SURFACE;
}
#if BUILDFLAG(ENABLE_HEVC_DEMUXING)
......
......@@ -374,6 +374,10 @@ class MEDIA_GPU_EXPORT AndroidVideoDecodeAccelerator
// when the EOS buffer is received.
bool codec_needs_reset_;
// True if surface creation and |picture_buffer_manager_| initialization has
// been defered until the first Decode() call.
bool defer_surface_creation_;
// Copy of the VDA::Config we were given.
Config config_;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment