Defer media playback in background tabs.
Videos which autoplay in the background will now have their load
deferred until the tab is visible for the first time -- this avoids
autoplay during session restore and premature playback.
Once a tab / RenderFrame has ever played media before, it's allowed
to continue to autoplay/autoload indefinitely; this is to support
playlist type applications.
BUG=509135
TEST=manual verification that background tabs don't autoplay;
playlist type media continues to work.
Review URL: https://codereview.chromium.org/1292433002
Cr-Commit-Position: refs/heads/master@{#344092}
diff --git a/chrome/browser/media/defer_background_media_browsertest.cc b/chrome/browser/media/defer_background_media_browsertest.cc
new file mode 100644
index 0000000..daa3e6b
--- /dev/null
+++ b/chrome/browser/media/defer_background_media_browsertest.cc
@@ -0,0 +1,60 @@
+// 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 "base/strings/utf_string_conversions.h"
+#include "build/build_config.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/tabs/tab_strip_model.h"
+#include "chrome/test/base/in_process_browser_test.h"
+#include "chrome/test/base/ui_test_utils.h"
+#include "content/public/test/browser_test_utils.h"
+#include "media/base/test_data_util.h"
+
+// TODO(dalecurtis): Android does not correctly defer media today, fix and
+// enable this test (needs disable of user gesture). http://crbug.com/522157
+#if !defined(OS_ANDROID)
+typedef InProcessBrowserTest MediaBrowserTest;
+
+IN_PROC_BROWSER_TEST_F(MediaBrowserTest, BackgroundMediaIsDeferred) {
+ // Navigate to a video file, which would autoplay in the foreground, but won't
+ // in the background due to deferred media loading for hidden tabs.
+ ui_test_utils::NavigateToURLWithDisposition(
+ browser(), content::GetFileUrlWithQuery(
+ media::GetTestDataFilePath("bear-640x360.webm"), ""),
+ NEW_BACKGROUND_TAB, ui_test_utils::BROWSER_TEST_WAIT_FOR_NAVIGATION);
+
+ ASSERT_EQ(2, browser()->tab_strip_model()->count());
+ content::WebContents* background_contents =
+ browser()->tab_strip_model()->GetWebContentsAt(1);
+ EXPECT_TRUE(
+ content::WaitForRenderFrameReady(background_contents->GetMainFrame()));
+ EXPECT_NE(background_contents,
+ browser()->tab_strip_model()->GetActiveWebContents());
+
+ // If autoplay for background tabs isn't deferred the play event listener will
+ // be attached too late to catch the event, and subsequently the test will hit
+ // the ended event before the play event.
+ EXPECT_TRUE(
+ content::ExecuteScript(background_contents,
+ "var video = document.querySelector('video');"
+ "video.addEventListener('ended', function(event) {"
+ " document.title = 'ended';"
+ "}, false);"
+ "video.addEventListener('play', function(event) {"
+ " document.title = 'playing';"
+ "}, false);"));
+
+ // Make the background tab w/ our video the active tab.
+ browser()->tab_strip_model()->SelectNextTab();
+ EXPECT_EQ(background_contents,
+ browser()->tab_strip_model()->GetActiveWebContents());
+
+ // If everything worked, we should see "playing" and not "ended".
+ const base::string16 playing_str = base::UTF8ToUTF16("playing");
+ const base::string16 ended_str = base::UTF8ToUTF16("ended");
+ content::TitleWatcher watcher(background_contents, playing_str);
+ watcher.AlsoWaitForTitle(ended_str);
+ EXPECT_EQ(playing_str, watcher.WaitAndGetTitle());
+}
+#endif // !defined(OS_ANDROID)
diff --git a/chrome/chrome_renderer.gypi b/chrome/chrome_renderer.gypi
index 281061d6..e835b61 100644
--- a/chrome/chrome_renderer.gypi
+++ b/chrome/chrome_renderer.gypi
@@ -156,10 +156,6 @@
'renderer/resources/extensions/tts_custom_bindings.js',
'renderer/resources/extensions/tts_engine_custom_bindings.js',
],
- 'chrome_renderer_non_android_sources': [
- 'renderer/prerender/prerender_media_load_deferrer.cc',
- 'renderer/prerender/prerender_media_load_deferrer.h',
- ],
'chrome_renderer_plugin_sources': [
'renderer/pepper/chrome_renderer_pepper_host_factory.cc',
'renderer/pepper/chrome_renderer_pepper_host_factory.h',
@@ -387,11 +383,6 @@
'<@(chrome_renderer_full_printing_sources)',
],
}],
- ['OS!="android"', {
- 'sources': [
- '<@(chrome_renderer_non_android_sources)',
- ],
- }],
['OS=="win"', {
'dependencies': [
'../components/components.gyp:dom_distiller_core', # Needed by chrome_content_renderer_client.cc.
diff --git a/chrome/chrome_tests.gypi b/chrome/chrome_tests.gypi
index a93d497..4f83083 100644
--- a/chrome/chrome_tests.gypi
+++ b/chrome/chrome_tests.gypi
@@ -320,6 +320,7 @@
'browser/media/chrome_webrtc_simulcast_browsertest.cc',
'browser/media/chrome_webrtc_video_quality_browsertest.cc',
'browser/media/chrome_webrtc_webcam_browsertest.cc',
+ 'browser/media/defer_background_media_browsertest.cc',
'browser/media/encrypted_media_browsertest.cc',
'browser/media/encrypted_media_istypesupported_browsertest.cc',
'browser/media/media_browsertest.cc',
diff --git a/chrome/renderer/BUILD.gn b/chrome/renderer/BUILD.gn
index ee9e894..d2cce4f 100644
--- a/chrome/renderer/BUILD.gn
+++ b/chrome/renderer/BUILD.gn
@@ -177,10 +177,6 @@
"spellchecker/hunspell_engine.h",
]
}
- if (!is_android) {
- sources +=
- rebase_path(gypi_values.chrome_renderer_non_android_sources, ".", "..")
- }
}
# In GYP this is part of test_support_common.
diff --git a/chrome/renderer/chrome_content_renderer_client.cc b/chrome/renderer/chrome_content_renderer_client.cc
index 34ee044..88a3fe0 100644
--- a/chrome/renderer/chrome_content_renderer_client.cc
+++ b/chrome/renderer/chrome_content_renderer_client.cc
@@ -46,7 +46,6 @@
#include "chrome/renderer/plugins/shadow_dom_plugin_placeholder.h"
#include "chrome/renderer/prerender/prerender_dispatcher.h"
#include "chrome/renderer/prerender/prerender_helper.h"
-#include "chrome/renderer/prerender/prerender_media_load_deferrer.h"
#include "chrome/renderer/prerender/prerenderer_client.h"
#include "chrome/renderer/safe_browsing/malware_dom_details.h"
#include "chrome/renderer/safe_browsing/phishing_classifier_delegate.h"
@@ -341,6 +340,29 @@
extensions::switches::kExtensionProcess);
}
#endif
+
+// Defers media player loading in background pages until they're visible.
+// TODO(dalecurtis): Include an idle listener too. http://crbug.com/509135
+class MediaLoadDeferrer : public content::RenderFrameObserver {
+ public:
+ MediaLoadDeferrer(content::RenderFrame* render_frame,
+ const base::Closure& continue_loading_cb)
+ : content::RenderFrameObserver(render_frame),
+ continue_loading_cb_(continue_loading_cb) {}
+ ~MediaLoadDeferrer() override {}
+
+ private:
+ // content::RenderFrameObserver implementation:
+ void WasShown() override {
+ continue_loading_cb_.Run();
+ delete this;
+ }
+
+ const base::Closure continue_loading_cb_;
+
+ DISALLOW_COPY_AND_ASSIGN(MediaLoadDeferrer);
+};
+
} // namespace
ChromeContentRendererClient::ChromeContentRendererClient() {
@@ -683,20 +705,22 @@
void ChromeContentRendererClient::DeferMediaLoad(
content::RenderFrame* render_frame,
+ bool has_played_media_before,
const base::Closure& closure) {
-#if defined(OS_ANDROID)
- // Chromium for Android doesn't support prerender yet.
- closure.Run();
- return;
-#else
- if (!prerender::PrerenderHelper::IsPrerendering(render_frame)) {
- closure.Run();
+ // Don't allow autoplay/autoload of media resources in a RenderFrame that is
+ // hidden and has never played any media before. We want to allow future
+ // loads even when hidden to allow playlist-like functionality.
+ //
+ // NOTE: This is also used to defer media loading for prerender.
+ //
+ // TODO(dalecurtis): Include an idle check too. http://crbug.com/509135
+ if (render_frame->IsHidden() && !has_played_media_before) {
+ // Lifetime is tied to |render_frame| via content::RenderFrameObserver.
+ new MediaLoadDeferrer(render_frame, closure);
return;
}
- // Lifetime is tied to |render_frame| via content::RenderFrameObserver.
- new prerender::PrerenderMediaLoadDeferrer(render_frame, closure);
-#endif
+ closure.Run();
}
#if defined(ENABLE_PLUGINS)
diff --git a/chrome/renderer/chrome_content_renderer_client.h b/chrome/renderer/chrome_content_renderer_client.h
index bafd24f..290217d 100644
--- a/chrome/renderer/chrome_content_renderer_client.h
+++ b/chrome/renderer/chrome_content_renderer_client.h
@@ -106,6 +106,7 @@
std::string* error_html,
base::string16* error_description) override;
void DeferMediaLoad(content::RenderFrame* render_frame,
+ bool has_played_media_before,
const base::Closure& closure) override;
bool RunIdleHandlerWhenWidgetsHidden() override;
bool AllowPopup() override;
diff --git a/chrome/renderer/prerender/prerender_media_load_deferrer.cc b/chrome/renderer/prerender/prerender_media_load_deferrer.cc
deleted file mode 100644
index 93bea766..0000000
--- a/chrome/renderer/prerender/prerender_media_load_deferrer.cc
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright 2013 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 "chrome/renderer/prerender/prerender_media_load_deferrer.h"
-
-#include "base/callback_helpers.h"
-#include "chrome/common/prerender_messages.h"
-
-namespace prerender {
-
-PrerenderMediaLoadDeferrer::PrerenderMediaLoadDeferrer(
- content::RenderFrame* render_frame,
- const base::Closure& closure)
- : RenderFrameObserver(render_frame),
- is_prerendering_(true),
- continue_loading_cb_(closure) {
- DCHECK(!continue_loading_cb_.is_null());
-}
-
-PrerenderMediaLoadDeferrer::~PrerenderMediaLoadDeferrer() {}
-
-bool PrerenderMediaLoadDeferrer::OnMessageReceived(
- const IPC::Message& message) {
- IPC_BEGIN_MESSAGE_MAP(PrerenderMediaLoadDeferrer, message)
- IPC_MESSAGE_HANDLER(PrerenderMsg_SetIsPrerendering, OnSetIsPrerendering)
- IPC_END_MESSAGE_MAP()
-
- return false;
-}
-
-void PrerenderMediaLoadDeferrer::OnSetIsPrerendering(bool is_prerendering) {
- // Prerendering can only be enabled prior to a RenderFrame's first
- // navigation, so no PrerenderMediaLoadDeferrer should see the notification
- // that enables prerendering.
- DCHECK(!is_prerendering);
- if (!is_prerendering_ || is_prerendering)
- return;
-
- is_prerendering_ = false;
- base::ResetAndReturn(&continue_loading_cb_).Run();
-}
-
-} // namespace prerender
diff --git a/chrome/renderer/prerender/prerender_media_load_deferrer.h b/chrome/renderer/prerender/prerender_media_load_deferrer.h
deleted file mode 100644
index 2c78389..0000000
--- a/chrome/renderer/prerender/prerender_media_load_deferrer.h
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright 2013 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.
-
-#ifndef CHROME_RENDERER_PRERENDER_PRERENDER_MEDIA_LOAD_DEFERRER_H_
-#define CHROME_RENDERER_PRERENDER_PRERENDER_MEDIA_LOAD_DEFERRER_H_
-
-#include "base/callback.h"
-#include "content/public/renderer/render_frame_observer.h"
-
-namespace prerender {
-
-// Defers media player loading in prerendered pages until the prerendered page
-// is swapped in.
-class PrerenderMediaLoadDeferrer : public content::RenderFrameObserver {
- public:
- // Will run |closure| to continue loading the media resource once the page is
- // swapped in.
- PrerenderMediaLoadDeferrer(content::RenderFrame* render_frame,
- const base::Closure& closure);
- ~PrerenderMediaLoadDeferrer() override;
-
- private:
- // RenderFrameObserver method:
- bool OnMessageReceived(const IPC::Message& message) override;
-
- void OnSetIsPrerendering(bool is_prerendering);
-
- bool is_prerendering_;
- base::Closure continue_loading_cb_;
-
- DISALLOW_COPY_AND_ASSIGN(PrerenderMediaLoadDeferrer);
-};
-
-} // namespace prerender
-
-#endif // CHROME_RENDERER_PRERENDER_PRERENDER_MEDIA_LOAD_DEFERRER_H_
diff --git a/chromecast/renderer/cast_content_renderer_client.cc b/chromecast/renderer/cast_content_renderer_client.cc
index 1f7c4a6..daebd98 100644
--- a/chromecast/renderer/cast_content_renderer_client.cc
+++ b/chromecast/renderer/cast_content_renderer_client.cc
@@ -210,6 +210,7 @@
void CastContentRendererClient::DeferMediaLoad(
content::RenderFrame* render_frame,
+ bool render_frame_has_played_media_before,
const base::Closure& closure) {
if (!render_frame->IsHidden()) {
closure.Run();
diff --git a/chromecast/renderer/cast_content_renderer_client.h b/chromecast/renderer/cast_content_renderer_client.h
index 0ee87ed..bdabd25 100644
--- a/chromecast/renderer/cast_content_renderer_client.h
+++ b/chromecast/renderer/cast_content_renderer_client.h
@@ -51,6 +51,7 @@
#endif
blink::WebPrescientNetworking* GetPrescientNetworking() override;
void DeferMediaLoad(content::RenderFrame* render_frame,
+ bool render_frame_has_played_media_before,
const base::Closure& closure) override;
protected:
diff --git a/content/public/renderer/content_renderer_client.cc b/content/public/renderer/content_renderer_client.cc
index 95af2f7..95133390 100644
--- a/content/public/renderer/content_renderer_client.cc
+++ b/content/public/renderer/content_renderer_client.cc
@@ -51,8 +51,10 @@
return false;
}
-void ContentRendererClient::DeferMediaLoad(RenderFrame* render_frame,
- const base::Closure& closure) {
+void ContentRendererClient::DeferMediaLoad(
+ RenderFrame* render_frame,
+ bool has_played_media_before,
+ const base::Closure& closure) {
closure.Run();
}
diff --git a/content/public/renderer/content_renderer_client.h b/content/public/renderer/content_renderer_client.h
index 837ba55..b1b0527 100644
--- a/content/public/renderer/content_renderer_client.h
+++ b/content/public/renderer/content_renderer_client.h
@@ -151,8 +151,10 @@
// Allows the embedder to control when media resources are loaded. Embedders
// can run |closure| immediately if they don't wish to defer media resource
- // loading.
+ // loading. If |has_played_media_before| is true, the render frame has
+ // previously started media playback (i.e. played audio and video).
virtual void DeferMediaLoad(RenderFrame* render_frame,
+ bool has_played_media_before,
const base::Closure& closure);
// Allows the embedder to override creating a WebMediaStreamCenter. If it
diff --git a/content/renderer/render_frame_impl.cc b/content/renderer/render_frame_impl.cc
index 05e6ad6..d29b700 100644
--- a/content/renderer/render_frame_impl.cc
+++ b/content/renderer/render_frame_impl.cc
@@ -676,6 +676,7 @@
#if defined(VIDEO_HOLE)
contains_media_player_(false),
#endif
+ has_played_media_(false),
devtools_agent_(nullptr),
geolocation_dispatcher_(NULL),
push_messaging_dispatcher_(NULL),
@@ -2041,7 +2042,7 @@
media::WebMediaPlayerParams params(
base::Bind(&ContentRendererClient::DeferMediaLoad,
base::Unretained(GetContentClient()->renderer()),
- static_cast<RenderFrame*>(this)),
+ static_cast<RenderFrame*>(this), has_played_media_),
render_thread->GetAudioRendererMixerManager()->CreateInput(routing_id_),
media_log, render_thread->GetMediaThreadTaskRunner(),
render_thread->compositor_task_runner(),
@@ -3827,6 +3828,7 @@
#endif
void RenderFrameImpl::DidPlay(WebMediaPlayer* player) {
+ has_played_media_ = true;
Send(new FrameHostMsg_MediaPlayingNotification(
routing_id_, reinterpret_cast<int64>(player), player->hasVideo(),
player->hasAudio(), player->isRemote()));
diff --git a/content/renderer/render_frame_impl.h b/content/renderer/render_frame_impl.h
index 321248fa..cdeba40c 100644
--- a/content/renderer/render_frame_impl.h
+++ b/content/renderer/render_frame_impl.h
@@ -966,6 +966,9 @@
bool contains_media_player_;
#endif
+ // True if this RenderFrame has ever played media.
+ bool has_played_media_;
+
// The devtools agent for this frame; only created for main frame and
// local roots.
DevToolsAgent* devtools_agent_;