Media Engagement: use playback, tab visibility and tab muted as signals.
The implementation does not yet use the following signals:
- audio track;
- muted state;
- video size.
Furthermore, the information are not yet recorded.
Bug: 715051
Change-Id: I94176ab98b05d5251f0f580c39d064b670e0496d
Reviewed-on: https://chromium-review.googlesource.com/505618
Reviewed-by: Jennifer Apacible <[email protected]>
Commit-Queue: Mounir Lamouri <[email protected]>
Cr-Commit-Position: refs/heads/master@{#474228}
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index 0a4d786f..9185306 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -573,6 +573,8 @@
"media/media_access_handler.h",
"media/media_device_id_salt.cc",
"media/media_device_id_salt.h",
+ "media/media_engagement_contents_observer.cc",
+ "media/media_engagement_contents_observer.h",
"media/media_engagement_service.cc",
"media/media_engagement_service.h",
"media/media_engagement_service_factory.cc",
diff --git a/chrome/browser/media/media_engagement_contents_observer.cc b/chrome/browser/media/media_engagement_contents_observer.cc
new file mode 100644
index 0000000..a595b5b
--- /dev/null
+++ b/chrome/browser/media/media_engagement_contents_observer.cc
@@ -0,0 +1,129 @@
+// Copyright 2017 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/browser/media/media_engagement_contents_observer.h"
+
+#include "chrome/browser/media/media_engagement_service.h"
+#include "content/public/browser/navigation_handle.h"
+#include "content/public/browser/web_contents.h"
+
+constexpr base::TimeDelta
+ MediaEngagementContentsObserver::kSignificantMediaPlaybackTime;
+
+MediaEngagementContentsObserver::MediaEngagementContentsObserver(
+ content::WebContents* web_contents,
+ MediaEngagementService* service)
+ : WebContentsObserver(web_contents),
+ service_(service),
+ playback_timer_(new base::Timer(true, false)) {}
+
+MediaEngagementContentsObserver::~MediaEngagementContentsObserver() = default;
+
+void MediaEngagementContentsObserver::WebContentsDestroyed() {
+ playback_timer_->Stop();
+ service_->contents_observers_.erase(this);
+ delete this;
+}
+
+void MediaEngagementContentsObserver::DidFinishNavigation(
+ content::NavigationHandle* navigation_handle) {
+ if (!navigation_handle->IsInMainFrame() ||
+ !navigation_handle->HasCommitted() ||
+ navigation_handle->IsSameDocument() || navigation_handle->IsErrorPage()) {
+ return;
+ }
+
+ DCHECK(!playback_timer_->IsRunning());
+ DCHECK(significant_players_.empty());
+
+ url::Origin new_origin(navigation_handle->GetURL());
+ if (committed_origin_.IsSameOriginWith(new_origin))
+ return;
+
+ committed_origin_ = new_origin;
+ significant_playback_recorded_ = false;
+
+ if (committed_origin_.unique())
+ return;
+
+ // TODO(mlamouri): record the visit into content settings.
+}
+
+void MediaEngagementContentsObserver::WasShown() {
+ is_visible_ = true;
+ UpdateTimer();
+}
+
+void MediaEngagementContentsObserver::WasHidden() {
+ is_visible_ = false;
+ UpdateTimer();
+}
+
+void MediaEngagementContentsObserver::MediaStartedPlaying(
+ const MediaPlayerInfo& media_player_info,
+ const MediaPlayerId& media_player_id) {
+ // TODO(mlamouri): check if:
+ // - the playback has the minimum size requirements;
+ // - the playback has an audio track;
+ // - the playback isn't muted.
+ DCHECK(significant_players_.find(media_player_id) ==
+ significant_players_.end());
+ significant_players_.insert(media_player_id);
+ UpdateTimer();
+}
+
+void MediaEngagementContentsObserver::MediaStoppedPlaying(
+ const MediaPlayerInfo& media_player_info,
+ const MediaPlayerId& media_player_id) {
+ DCHECK(significant_players_.find(media_player_id) !=
+ significant_players_.end());
+ significant_players_.erase(media_player_id);
+ UpdateTimer();
+}
+
+void MediaEngagementContentsObserver::DidUpdateAudioMutingState(bool muted) {
+ UpdateTimer();
+}
+
+void MediaEngagementContentsObserver::OnSignificantMediaPlaybackTime() {
+ DCHECK(!significant_playback_recorded_);
+
+ significant_playback_recorded_ = true;
+
+ if (committed_origin_.unique())
+ return;
+
+ // TODO(mlamouri): record the playback into content settings.
+}
+
+bool MediaEngagementContentsObserver::AreConditionsMet() const {
+ if (significant_players_.empty() || !is_visible_)
+ return false;
+
+ return !web_contents()->IsAudioMuted();
+}
+
+void MediaEngagementContentsObserver::UpdateTimer() {
+ if (significant_playback_recorded_)
+ return;
+
+ if (AreConditionsMet()) {
+ if (playback_timer_->IsRunning())
+ return;
+ playback_timer_->Start(
+ FROM_HERE, kSignificantMediaPlaybackTime,
+ base::Bind(
+ &MediaEngagementContentsObserver::OnSignificantMediaPlaybackTime,
+ base::Unretained(this)));
+ } else {
+ if (!playback_timer_->IsRunning())
+ return;
+ playback_timer_->Stop();
+ }
+}
+
+void MediaEngagementContentsObserver::SetTimerForTest(
+ std::unique_ptr<base::Timer> timer) {
+ playback_timer_ = std::move(timer);
+}
diff --git a/chrome/browser/media/media_engagement_contents_observer.h b/chrome/browser/media/media_engagement_contents_observer.h
new file mode 100644
index 0000000..94a57f7
--- /dev/null
+++ b/chrome/browser/media/media_engagement_contents_observer.h
@@ -0,0 +1,66 @@
+// Copyright 2017 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_BROWSER_MEDIA_MEDIA_ENGAGEMENT_CONTENTS_OBSERVER_H_
+#define CHROME_BROWSER_MEDIA_MEDIA_ENGAGEMENT_CONTENTS_OBSERVER_H_
+
+#include "content/public/browser/web_contents_observer.h"
+
+class MediaEngagementContentsObserverTest;
+class MediaEngagementService;
+
+class MediaEngagementContentsObserver : public content::WebContentsObserver {
+ public:
+ ~MediaEngagementContentsObserver() override;
+
+ // WebContentsObserver implementation.
+ void WebContentsDestroyed() override;
+ void DidFinishNavigation(
+ content::NavigationHandle* navigation_handle) override;
+ void WasShown() override;
+ void WasHidden() override;
+ void MediaStartedPlaying(const MediaPlayerInfo& media_player_info,
+ const MediaPlayerId& media_player_id) override;
+ void MediaStoppedPlaying(const MediaPlayerInfo& media_player_info,
+ const MediaPlayerId& media_player_id) override;
+ void DidUpdateAudioMutingState(bool muted) override;
+
+ private:
+ // Only MediaEngagementService can create a MediaEngagementContentsObserver.
+ friend MediaEngagementService;
+ friend MediaEngagementContentsObserverTest;
+
+ static constexpr base::TimeDelta kSignificantMediaPlaybackTime =
+ base::TimeDelta::FromSeconds(7);
+
+ MediaEngagementContentsObserver(content::WebContents* web_contents,
+ MediaEngagementService* service);
+
+ void OnSignificantMediaPlaybackTime();
+ bool AreConditionsMet() const;
+ void UpdateTimer();
+
+ void SetTimerForTest(std::unique_ptr<base::Timer> timer);
+
+ // |this| is owned by |service_|.
+ MediaEngagementService* service_;
+
+ // Timer that will fire when the playback time reaches the minimum for
+ // significant media playback.
+ std::unique_ptr<base::Timer> playback_timer_;
+
+ // Set of active players that can produce a significant playback. In other
+ // words, whether this set is empty can be used to know if there is a
+ // significant playback.
+ std::set<MediaPlayerId> significant_players_;
+
+ bool is_visible_ = false;
+ bool significant_playback_recorded_ = false;
+
+ url::Origin committed_origin_;
+
+ DISALLOW_COPY_AND_ASSIGN(MediaEngagementContentsObserver);
+};
+
+#endif // CHROME_BROWSER_MEDIA_MEDIA_ENGAGEMENT_CONTENTS_OBSERVER_H_
diff --git a/chrome/browser/media/media_engagement_contents_observer_unittest.cc b/chrome/browser/media/media_engagement_contents_observer_unittest.cc
new file mode 100644
index 0000000..2eaf676
--- /dev/null
+++ b/chrome/browser/media/media_engagement_contents_observer_unittest.cc
@@ -0,0 +1,171 @@
+// Copyright 2017 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/browser/media/media_engagement_contents_observer.h"
+
+#include "base/test/scoped_feature_list.h"
+#include "base/timer/mock_timer.h"
+#include "chrome/browser/media/media_engagement_service.h"
+#include "chrome/browser/media/media_engagement_service_factory.h"
+#include "chrome/test/base/chrome_render_view_host_test_harness.h"
+#include "chrome/test/base/testing_profile.h"
+#include "content/public/browser/web_contents.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+class MediaEngagementContentsObserverTest
+ : public ChromeRenderViewHostTestHarness {
+ public:
+ void SetUp() override {
+ scoped_feature_list_.InitFromCommandLine("media-engagement", std::string());
+
+ ChromeRenderViewHostTestHarness::SetUp();
+
+ MediaEngagementService* service = MediaEngagementService::Get(profile());
+ ASSERT_TRUE(service);
+ contents_observer_ =
+ new MediaEngagementContentsObserver(web_contents(), service);
+
+ playback_timer_ = new base::MockTimer(true, false);
+ contents_observer_->SetTimerForTest(base::WrapUnique(playback_timer_));
+ }
+
+ bool IsTimerRunning() const { return playback_timer_->IsRunning(); }
+
+ bool WasSignificantPlaybackRecorded() const {
+ return contents_observer_->significant_playback_recorded_;
+ }
+
+ size_t GetSignificantActivePlayersCount() const {
+ return contents_observer_->significant_players_.size();
+ }
+
+ void SimulatePlaybackStarted(int id) {
+ content::WebContentsObserver::MediaPlayerInfo player_info(true);
+ content::WebContentsObserver::MediaPlayerId player_id =
+ std::make_pair(nullptr /* RenderFrameHost */, id);
+ contents_observer_->MediaStartedPlaying(player_info, player_id);
+ }
+
+ void SimulatePlaybackStopped(int id) {
+ content::WebContentsObserver::MediaPlayerInfo player_info(true);
+ content::WebContentsObserver::MediaPlayerId player_id =
+ std::make_pair(nullptr /* RenderFrameHost */, id);
+ contents_observer_->MediaStoppedPlaying(player_info, player_id);
+ }
+
+ void SimulateIsVisible() { contents_observer_->WasShown(); }
+
+ void SimulateIsHidden() { contents_observer_->WasHidden(); }
+
+ bool AreConditionsMet() const {
+ return contents_observer_->AreConditionsMet();
+ }
+
+ void SimulateSignificantPlaybackRecorded() {
+ contents_observer_->significant_playback_recorded_ = true;
+ }
+
+ void SimulatePlaybackTimerFired() { playback_timer_->Fire(); }
+
+ private:
+ // contents_observer_ auto-destroys when WebContents is destroyed.
+ MediaEngagementContentsObserver* contents_observer_;
+
+ base::test::ScopedFeatureList scoped_feature_list_;
+
+ base::MockTimer* playback_timer_;
+};
+
+// TODO(mlamouri): test that visits are not recorded multiple times when a
+// same-origin navigation happens.
+
+TEST_F(MediaEngagementContentsObserverTest, SignificantActivePlayerCount) {
+ EXPECT_EQ(0u, GetSignificantActivePlayersCount());
+
+ SimulatePlaybackStarted(0);
+ EXPECT_EQ(1u, GetSignificantActivePlayersCount());
+
+ SimulatePlaybackStarted(1);
+ EXPECT_EQ(2u, GetSignificantActivePlayersCount());
+
+ SimulatePlaybackStarted(2);
+ EXPECT_EQ(3u, GetSignificantActivePlayersCount());
+
+ SimulatePlaybackStopped(1);
+ EXPECT_EQ(2u, GetSignificantActivePlayersCount());
+
+ SimulatePlaybackStopped(0);
+ EXPECT_EQ(1u, GetSignificantActivePlayersCount());
+
+ SimulatePlaybackStopped(2);
+ EXPECT_EQ(0u, GetSignificantActivePlayersCount());
+}
+
+TEST_F(MediaEngagementContentsObserverTest, AreConditionsMet) {
+ EXPECT_FALSE(AreConditionsMet());
+
+ SimulatePlaybackStarted(0);
+ SimulateIsVisible();
+ web_contents()->SetAudioMuted(false);
+ EXPECT_TRUE(AreConditionsMet());
+
+ web_contents()->SetAudioMuted(true);
+ EXPECT_FALSE(AreConditionsMet());
+
+ web_contents()->SetAudioMuted(false);
+ SimulateIsHidden();
+ EXPECT_FALSE(AreConditionsMet());
+
+ SimulateIsVisible();
+ SimulatePlaybackStopped(0);
+ EXPECT_FALSE(AreConditionsMet());
+
+ SimulatePlaybackStarted(0);
+ EXPECT_TRUE(AreConditionsMet());
+}
+
+TEST_F(MediaEngagementContentsObserverTest, TimerRunsDependingOnConditions) {
+ EXPECT_FALSE(IsTimerRunning());
+
+ SimulatePlaybackStarted(0);
+ SimulateIsVisible();
+ web_contents()->SetAudioMuted(false);
+ EXPECT_TRUE(IsTimerRunning());
+
+ web_contents()->SetAudioMuted(true);
+ EXPECT_FALSE(IsTimerRunning());
+
+ web_contents()->SetAudioMuted(false);
+ SimulateIsHidden();
+ EXPECT_FALSE(IsTimerRunning());
+
+ SimulateIsVisible();
+ SimulatePlaybackStopped(0);
+ EXPECT_FALSE(IsTimerRunning());
+
+ SimulatePlaybackStarted(0);
+ EXPECT_TRUE(IsTimerRunning());
+}
+
+TEST_F(MediaEngagementContentsObserverTest, TimerDoesNotRunIfEntryRecorded) {
+ SimulateSignificantPlaybackRecorded();
+
+ SimulatePlaybackStarted(0);
+ SimulateIsVisible();
+ web_contents()->SetAudioMuted(false);
+
+ EXPECT_FALSE(IsTimerRunning());
+}
+
+TEST_F(MediaEngagementContentsObserverTest,
+ SignificantPlaybackRecordedWhenTimerFires) {
+ SimulatePlaybackStarted(0);
+ SimulateIsVisible();
+ web_contents()->SetAudioMuted(false);
+ EXPECT_TRUE(IsTimerRunning());
+ EXPECT_FALSE(WasSignificantPlaybackRecorded());
+
+ SimulatePlaybackTimerFired();
+ EXPECT_TRUE(WasSignificantPlaybackRecorded());
+}
diff --git a/chrome/browser/media/media_engagement_service.cc b/chrome/browser/media/media_engagement_service.cc
index 2eb7ffb..1c4de26e 100644
--- a/chrome/browser/media/media_engagement_service.cc
+++ b/chrome/browser/media/media_engagement_service.cc
@@ -4,34 +4,12 @@
#include "chrome/browser/media/media_engagement_service.h"
+#include "chrome/browser/media/media_engagement_contents_observer.h"
#include "chrome/browser/media/media_engagement_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "content/public/browser/web_contents.h"
-#include "content/public/browser/web_contents_observer.h"
#include "media/base/media_switches.h"
-class MediaEngagementService::ContentsObserver
- : public content::WebContentsObserver {
- public:
- ContentsObserver(content::WebContents* web_contents,
- MediaEngagementService* service)
- : WebContentsObserver(web_contents), service_(service) {}
-
- ~ContentsObserver() override = default;
-
- // WebContentsObserver implementation.
- void WebContentsDestroyed() override {
- service_->contents_observers_.erase(this);
- delete this;
- }
-
- private:
- // |this| is owned by |service_|.
- MediaEngagementService* service_;
-
- DISALLOW_COPY_AND_ASSIGN(ContentsObserver);
-};
-
// static
bool MediaEngagementService::IsEnabled() {
return base::FeatureList::IsEnabled(media::kMediaEngagement);
@@ -52,7 +30,7 @@
if (!service)
return;
service->contents_observers_.insert(
- new ContentsObserver(web_contents, service));
+ new MediaEngagementContentsObserver(web_contents, service));
}
MediaEngagementService::MediaEngagementService(Profile* profile) {
diff --git a/chrome/browser/media/media_engagement_service.h b/chrome/browser/media/media_engagement_service.h
index 3701c02..7f4d228a 100644
--- a/chrome/browser/media/media_engagement_service.h
+++ b/chrome/browser/media/media_engagement_service.h
@@ -10,6 +10,7 @@
#include "base/macros.h"
#include "components/keyed_service/core/keyed_service.h"
+class MediaEngagementContentsObserver;
class Profile;
namespace content {
@@ -32,9 +33,9 @@
~MediaEngagementService() override;
private:
- class ContentsObserver;
+ friend MediaEngagementContentsObserver;
- std::set<ContentsObserver*> contents_observers_;
+ std::set<MediaEngagementContentsObserver*> contents_observers_;
DISALLOW_COPY_AND_ASSIGN(MediaEngagementService);
};
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index 90fe4f7..8ec65cc 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -3092,6 +3092,7 @@
"../browser/manifest/manifest_icon_selector_unittest.cc",
"../browser/media/android/router/media_router_android_unittest.cc",
"../browser/media/cast_remoting_connector_unittest.cc",
+ "../browser/media/media_engagement_contents_observer_unittest.cc",
"../browser/media/midi_permission_context_unittest.cc",
"../browser/media/router/browser_presentation_connection_proxy_unittest.cc",
"../browser/media/router/create_presentation_connection_request_unittest.cc",