summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authormlamouri <mlamouri@chromium.org>2015-05-29 05:34:20 -0700
committerCommit bot <commit-bot@chromium.org>2015-05-29 12:34:42 +0000
commit3d020b1e04e93bbd318105276a89dd3aa4af1a27 (patch)
tree7bca87ced56e080414f9cdf8b6192074340ffbf1
parentdb185223458b1b846d465749ab427627f2de915a (diff)
downloadchromium_src-3d020b1e04e93bbd318105276a89dd3aa4af1a27.zip
chromium_src-3d020b1e04e93bbd318105276a89dd3aa4af1a27.tar.gz
chromium_src-3d020b1e04e93bbd318105276a89dd3aa4af1a27.tar.bz2
Move audio focus control from media/ to content/ and make it per WebContents.
This CL is making each WebContents able to grab its own audio focus from the system instead of keeping the audio focus for Chromium regardless of the number of tab producing noise. This is improving Chromium integration in Android by having each tab behave as a user would expect an application to behave. For example, it is now possible to go to a music website, play something then go to Youtube and play a video. The video will automatically pause the music, the same way the native Youtube application would pause a music application. There is a slight "hack" where for sounds known to be short, the audio focus is requested as TRANSIENT_MAY_DUCK in order to not break running media for sound effects. BUG=486878 Review URL: https://codereview.chromium.org/1110833004 Cr-Commit-Position: refs/heads/master@{#331947}
-rw-r--r--android_webview/javatests/src/org/chromium/android_webview/test/AwContentsClientFullScreenTest.java20
-rw-r--r--android_webview/javatests/src/org/chromium/android_webview/test/MultipleVideosTest.java8
-rw-r--r--content/browser/android/browser_jni_registrar.cc2
-rw-r--r--content/browser/media/android/browser_media_player_manager.cc40
-rw-r--r--content/browser/media/android/browser_media_player_manager.h9
-rw-r--r--content/browser/media/android/media_session.cc183
-rw-r--r--content/browser/media/android/media_session.h134
-rw-r--r--content/browser/media/android/media_session_browsertest.cc544
-rw-r--r--content/browser/media/android/media_session_observer.h24
-rw-r--r--content/content_browser.gypi3
-rw-r--r--content/content_jni.gypi1
-rw-r--r--content/content_tests.gypi1
-rw-r--r--content/public/android/java/src/org/chromium/content/browser/MediaSession.java88
-rw-r--r--content/public/android/java/src/org/chromium/content/common/ContentSwitches.java4
-rw-r--r--content/public/android/javatests/src/org/chromium/content/browser/MediaSessionTest.java371
-rw-r--r--content/public/test/android/javatests/src/org/chromium/content/browser/test/util/DOMUtils.java57
-rw-r--r--content/test/data/android/media/audio-1second.oggbin0 -> 6087 bytes
-rw-r--r--content/test/data/android/media/audio-2seconds.oggbin0 -> 7542 bytes
-rw-r--r--content/test/data/android/media/audio-6seconds.oggbin0 -> 17400 bytes
-rw-r--r--content/test/data/android/media/media-session.html27
-rw-r--r--content/test/data/android/media/video-1second.mp4bin0 -> 39128 bytes
-rw-r--r--content/test/data/android/media/video-2seconds.mp4bin0 -> 68547 bytes
-rw-r--r--content/test/data/android/media/video-6seconds.mp4bin0 -> 192844 bytes
-rw-r--r--media/base/android/java/src/org/chromium/media/MediaPlayerListener.java31
-rw-r--r--media/base/android/media_player_bridge.cc5
-rw-r--r--media/base/android/media_player_listener.cc6
-rw-r--r--media/base/android/media_player_manager.h6
-rw-r--r--media/base/android/media_source_player.cc5
-rw-r--r--media/base/android/media_source_player_unittest.cc57
29 files changed, 1564 insertions, 62 deletions
diff --git a/android_webview/javatests/src/org/chromium/android_webview/test/AwContentsClientFullScreenTest.java b/android_webview/javatests/src/org/chromium/android_webview/test/AwContentsClientFullScreenTest.java
index 6168e45..7a5f7d4 100644
--- a/android_webview/javatests/src/org/chromium/android_webview/test/AwContentsClientFullScreenTest.java
+++ b/android_webview/javatests/src/org/chromium/android_webview/test/AwContentsClientFullScreenTest.java
@@ -190,10 +190,10 @@ public class AwContentsClientFullScreenTest extends AwTestBase {
public void doTestOnShowCustomViewAndPlayWithHtmlControl(String videoTestUrl) throws Throwable {
doOnShowCustomViewTest(videoTestUrl);
- assertTrue(DOMUtils.isVideoPaused(getWebContentsOnUiThread(), VIDEO_ID));
+ assertTrue(DOMUtils.isMediaPaused(getWebContentsOnUiThread(), VIDEO_ID));
tapPlayButton();
- assertTrue(DOMUtils.waitForVideoPlay(getWebContentsOnUiThread(), VIDEO_ID));
+ assertTrue(DOMUtils.waitForMediaPlay(getWebContentsOnUiThread(), VIDEO_ID));
}
@MediumTest
@@ -206,7 +206,7 @@ public class AwContentsClientFullScreenTest extends AwTestBase {
// Play and verify that a surface view for hole punching is not created.
// Note that VIDEO_TEST_URL contains clear video.
tapPlayButton();
- assertTrue(DOMUtils.waitForVideoPlay(getWebContentsOnUiThread(), VIDEO_ID));
+ assertTrue(DOMUtils.waitForMediaPlay(getWebContentsOnUiThread(), VIDEO_ID));
// Wait to ensure that the surface view is not added asynchronously.
VideoSurfaceViewUtils.waitAndAssertContainsZeroVideoHoleSurfaceViews(this,
mTestContainerView);
@@ -228,7 +228,7 @@ public class AwContentsClientFullScreenTest extends AwTestBase {
// Play and verify that there is a surface view for hole punching.
tapPlayButton();
- assertTrue(DOMUtils.waitForVideoPlay(getWebContentsOnUiThread(), VIDEO_ID));
+ assertTrue(DOMUtils.waitForMediaPlay(getWebContentsOnUiThread(), VIDEO_ID));
VideoSurfaceViewUtils.pollAndAssertContainsOneVideoHoleSurfaceView(this,
mTestContainerView);
@@ -262,7 +262,7 @@ public class AwContentsClientFullScreenTest extends AwTestBase {
// Play and verify that there is a surface view for hole punching.
tapPlayButton();
- assertTrue(DOMUtils.waitForVideoPlay(getWebContentsOnUiThread(), VIDEO_ID));
+ assertTrue(DOMUtils.waitForMediaPlay(getWebContentsOnUiThread(), VIDEO_ID));
VideoSurfaceViewUtils.pollAndAssertContainsOneVideoHoleSurfaceView(this,
mTestContainerView);
@@ -335,7 +335,7 @@ public class AwContentsClientFullScreenTest extends AwTestBase {
assertWaitForKeepScreenOnActive(customView, true);
// Stop the video and verify that the power save blocker is gone.
- DOMUtils.pauseVideo(getWebContentsOnUiThread(), VIDEO_ID);
+ DOMUtils.pauseMedia(getWebContentsOnUiThread(), VIDEO_ID);
assertWaitForKeepScreenOnActive(customView, false);
}
@@ -354,7 +354,7 @@ public class AwContentsClientFullScreenTest extends AwTestBase {
assertWaitForKeepScreenOnActive(mTestContainerView, true);
// Stop the video and verify that the power save blocker is gone.
- DOMUtils.pauseVideo(getWebContentsOnUiThread(), VIDEO_ID);
+ DOMUtils.pauseMedia(getWebContentsOnUiThread(), VIDEO_ID);
assertWaitForKeepScreenOnActive(mTestContainerView, false);
}
@@ -377,7 +377,7 @@ public class AwContentsClientFullScreenTest extends AwTestBase {
assertKeepScreenOnActive(customView, true);
// Pause the video and the power save blocker is gone.
- DOMUtils.pauseVideo(getWebContentsOnUiThread(), VIDEO_ID);
+ DOMUtils.pauseMedia(getWebContentsOnUiThread(), VIDEO_ID);
assertWaitForKeepScreenOnActive(customView, false);
// Exit fullscreen and the power save blocker is still gone.
@@ -435,7 +435,7 @@ public class AwContentsClientFullScreenTest extends AwTestBase {
public boolean isSatisfied() {
try {
return getKeepScreenOn(view) == expected
- && DOMUtils.isVideoPaused(getWebContentsOnUiThread(), VIDEO_ID)
+ && DOMUtils.isMediaPaused(getWebContentsOnUiThread(), VIDEO_ID)
!= expected;
} catch (InterruptedException | TimeoutException e) {
fail(e.getMessage());
@@ -448,7 +448,7 @@ public class AwContentsClientFullScreenTest extends AwTestBase {
private void assertKeepScreenOnActive(final View view, final boolean expected)
throws Exception {
assertTrue(getKeepScreenOn(view) == expected
- && DOMUtils.isVideoPaused(getWebContentsOnUiThread(), VIDEO_ID) != expected);
+ && DOMUtils.isMediaPaused(getWebContentsOnUiThread(), VIDEO_ID) != expected);
}
private boolean getKeepScreenOn(View view) {
diff --git a/android_webview/javatests/src/org/chromium/android_webview/test/MultipleVideosTest.java b/android_webview/javatests/src/org/chromium/android_webview/test/MultipleVideosTest.java
index 8e55cd2..490686c 100644
--- a/android_webview/javatests/src/org/chromium/android_webview/test/MultipleVideosTest.java
+++ b/android_webview/javatests/src/org/chromium/android_webview/test/MultipleVideosTest.java
@@ -57,7 +57,7 @@ public class MultipleVideosTest extends AwTestBase {
// Play the first video.
tapFirstPlayButton();
- assertTrue(DOMUtils.waitForVideoPlay(getWebContentsOnUiThread(), FIRST_VIDEO_ID));
+ assertTrue(DOMUtils.waitForMediaPlay(getWebContentsOnUiThread(), FIRST_VIDEO_ID));
// Verify that there is one video hole surface.
VideoSurfaceViewUtils.pollAndAssertContainsOneVideoHoleSurfaceView(this,
@@ -65,11 +65,11 @@ public class MultipleVideosTest extends AwTestBase {
// Start the second video.
tapSecondPlayButton();
- assertTrue(DOMUtils.waitForVideoPlay(getWebContentsOnUiThread(), SECOND_VIDEO_ID));
+ assertTrue(DOMUtils.waitForMediaPlay(getWebContentsOnUiThread(), SECOND_VIDEO_ID));
// Verify that the first video pauses once the second video starts.
- assertFalse(DOMUtils.isVideoPaused(getWebContentsOnUiThread(), SECOND_VIDEO_ID));
- assertTrue(DOMUtils.isVideoPaused(getWebContentsOnUiThread(), FIRST_VIDEO_ID));
+ assertFalse(DOMUtils.isMediaPaused(getWebContentsOnUiThread(), SECOND_VIDEO_ID));
+ assertTrue(DOMUtils.isMediaPaused(getWebContentsOnUiThread(), FIRST_VIDEO_ID));
// Verify that there is still only one video hole surface.
VideoSurfaceViewUtils.pollAndAssertContainsOneVideoHoleSurfaceView(this,
diff --git a/content/browser/android/browser_jni_registrar.cc b/content/browser/android/browser_jni_registrar.cc
index a5df91b..eaf9b46 100644
--- a/content/browser/android/browser_jni_registrar.cc
+++ b/content/browser/android/browser_jni_registrar.cc
@@ -30,6 +30,7 @@
#include "content/browser/geolocation/location_api_adapter_android.h"
#include "content/browser/media/android/media_drm_credential_manager.h"
#include "content/browser/media/android/media_resource_getter_impl.h"
+#include "content/browser/media/android/media_session.h"
#include "content/browser/mojo/service_registrar_android.h"
#include "content/browser/mojo/service_registry_android.h"
#include "content/browser/power_save_blocker_android.h"
@@ -75,6 +76,7 @@ base::android::RegistrationMethod kContentRegisteredMethods[] = {
content::MediaDrmCredentialManager::RegisterMediaDrmCredentialManager},
{"MediaResourceGetterImpl",
content::MediaResourceGetterImpl::RegisterMediaResourceGetter},
+ {"MediaSession", content::MediaSession::RegisterMediaSession},
{"MotionEventAndroid",
content::MotionEventAndroid::RegisterMotionEventAndroid},
{"NavigationControllerAndroid",
diff --git a/content/browser/media/android/browser_media_player_manager.cc b/content/browser/media/android/browser_media_player_manager.cc
index 68268f7..c77ead0 100644
--- a/content/browser/media/android/browser_media_player_manager.cc
+++ b/content/browser/media/android/browser_media_player_manager.cc
@@ -10,6 +10,7 @@
#include "content/browser/android/media_players_observer.h"
#include "content/browser/media/android/browser_demuxer_android.h"
#include "content/browser/media/android/media_resource_getter_impl.h"
+#include "content/browser/media/android/media_session.h"
#include "content/browser/renderer_host/render_view_host_impl.h"
#include "content/browser/web_contents/web_contents_view_android.h"
#include "content/common/media/media_player_messages_android.h"
@@ -44,6 +45,9 @@ namespace content {
const int kMediaPlayerThreshold = 1;
const int kInvalidMediaPlayerId = -1;
+// Minimal duration of a media player in order to be considered as Content type.
+const int kMinimumDurationForContentInSeconds = 5;
+
static BrowserMediaPlayerManager::Factory g_factory = NULL;
static media::MediaUrlInterceptor* media_url_interceptor_ = NULL;
@@ -160,6 +164,7 @@ BrowserMediaPlayerManager::~BrowserMediaPlayerManager() {
for (MediaPlayerAndroid* player : players_)
player->DeleteOnCorrectThread();
+ MediaSession::Get(web_contents())->RemovePlayers(this);
players_.weak_clear();
}
@@ -228,6 +233,8 @@ void BrowserMediaPlayerManager::OnMediaMetadataChanged(
void BrowserMediaPlayerManager::OnPlaybackComplete(int player_id) {
Send(new MediaPlayerMsg_MediaPlaybackCompleted(RoutingID(), player_id));
+ MediaSession::Get(web_contents())->RemovePlayer(this, player_id);
+
if (fullscreen_player_id_ == player_id)
video_view_->OnPlaybackComplete();
}
@@ -341,6 +348,21 @@ void BrowserMediaPlayerManager::RequestFullScreen(int player_id) {
Send(new MediaPlayerMsg_RequestFullscreen(RoutingID(), player_id));
}
+bool BrowserMediaPlayerManager::RequestPlay(int player_id) {
+ MediaPlayerAndroid* player = GetPlayer(player_id);
+ DCHECK(player);
+
+ MediaSession::Type media_session_type =
+ player->GetDuration().InSeconds() > kMinimumDurationForContentInSeconds
+ ? MediaSession::Type::Content : MediaSession::Type::Transient;
+
+ bool succeeded = MediaSession::Get(web_contents())->AddPlayer(
+ this, player_id, media_session_type);
+ if (!succeeded)
+ Send(new MediaPlayerMsg_DidMediaPlayerPause(RoutingID(), player_id));
+ return succeeded;
+}
+
#if defined(VIDEO_HOLE)
void BrowserMediaPlayerManager::AttachExternalVideoSurface(int player_id,
jobject surface) {
@@ -365,6 +387,22 @@ void BrowserMediaPlayerManager::OnFrameInfoUpdated() {
external_video_surface_container_->OnFrameInfoUpdated();
}
+void BrowserMediaPlayerManager::OnSuspend(int player_id) {
+ MediaPlayerAndroid* player = GetPlayer(player_id);
+ DCHECK(player);
+
+ player->Pause(true);
+ Send(new MediaPlayerMsg_DidMediaPlayerPause(RoutingID(), player_id));
+}
+
+void BrowserMediaPlayerManager::OnResume(int player_id) {
+ MediaPlayerAndroid* player = GetPlayer(player_id);
+ DCHECK(player);
+
+ player->Start();
+ Send(new MediaPlayerMsg_DidMediaPlayerPlay(RoutingID(), player_id));
+}
+
void BrowserMediaPlayerManager::OnNotifyExternalSurface(
int player_id, bool is_request, const gfx::RectF& rect) {
if (!web_contents_)
@@ -503,6 +541,8 @@ void BrowserMediaPlayerManager::OnPause(
MediaPlayerAndroid* player = GetPlayer(player_id);
if (player)
player->Pause(is_media_related_action);
+
+ MediaSession::Get(web_contents())->RemovePlayer(this, player_id);
}
void BrowserMediaPlayerManager::OnSetVolume(int player_id, double volume) {
diff --git a/content/browser/media/android/browser_media_player_manager.h b/content/browser/media/android/browser_media_player_manager.h
index a1ef3ff..8a8d5e6 100644
--- a/content/browser/media/android/browser_media_player_manager.h
+++ b/content/browser/media/android/browser_media_player_manager.h
@@ -11,6 +11,7 @@
#include "base/memory/scoped_vector.h"
#include "base/time/time.h"
#include "content/browser/android/content_video_view.h"
+#include "content/browser/media/android/media_session_observer.h"
#include "content/common/content_export.h"
#include "content/common/media/media_player_messages_enums_android.h"
#include "content/public/browser/android/content_view_core.h"
@@ -41,7 +42,8 @@ class WebContents;
// MediaPlayerAndroid objects are converted to IPCs and then sent to the render
// process.
class CONTENT_EXPORT BrowserMediaPlayerManager
- : public media::MediaPlayerManager {
+ : public media::MediaPlayerManager,
+ public MediaSessionObserver {
public:
// Permits embedders to provide an extended version of the class.
typedef BrowserMediaPlayerManager* (*Factory)(RenderFrameHost*,
@@ -97,12 +99,17 @@ class CONTENT_EXPORT BrowserMediaPlayerManager
media::MediaPlayerAndroid* GetFullscreenPlayer() override;
media::MediaPlayerAndroid* GetPlayer(int player_id) override;
void RequestFullScreen(int player_id) override;
+ bool RequestPlay(int player_id) override;
#if defined(VIDEO_HOLE)
void AttachExternalVideoSurface(int player_id, jobject surface);
void DetachExternalVideoSurface(int player_id);
void OnFrameInfoUpdated();
#endif // defined(VIDEO_HOLE)
+ // MediaSessionObserver overrides.
+ void OnSuspend(int player_id) override;
+ void OnResume(int player_id) override;
+
// Message handlers.
virtual void OnEnterFullscreen(int player_id);
virtual void OnExitFullscreen(int player_id);
diff --git a/content/browser/media/android/media_session.cc b/content/browser/media/android/media_session.cc
new file mode 100644
index 0000000..a19e29e
--- /dev/null
+++ b/content/browser/media/android/media_session.cc
@@ -0,0 +1,183 @@
+// 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/browser/media/android/media_session.h"
+
+#include "base/android/jni_android.h"
+#include "content/browser/media/android/media_session_observer.h"
+#include "jni/MediaSession_jni.h"
+
+namespace content {
+
+DEFINE_WEB_CONTENTS_USER_DATA_KEY(MediaSession);
+
+MediaSession::PlayerIdentifier::PlayerIdentifier(MediaSessionObserver* observer,
+ int player_id)
+ : observer(observer),
+ player_id(player_id) {
+}
+
+bool MediaSession::PlayerIdentifier::operator==(
+ const PlayerIdentifier& other) const {
+ return this->observer == other.observer && this->player_id == other.player_id;
+}
+
+size_t MediaSession::PlayerIdentifier::Hash::operator()(
+ const PlayerIdentifier& player_identifier) const {
+ size_t hash = BASE_HASH_NAMESPACE::hash<MediaSessionObserver*>()(
+ player_identifier.observer);
+ hash += BASE_HASH_NAMESPACE::hash<int>()(player_identifier.player_id);
+ return hash;
+}
+
+// static
+bool content::MediaSession::RegisterMediaSession(JNIEnv* env) {
+ return RegisterNativesImpl(env);
+}
+
+// static
+MediaSession* MediaSession::Get(WebContents* web_contents) {
+ MediaSession* session = FromWebContents(web_contents);
+ if (!session) {
+ CreateForWebContents(web_contents);
+ session = FromWebContents(web_contents);
+ session->Initialize();
+ }
+ return session;
+}
+
+MediaSession::~MediaSession() {
+ DCHECK(players_.empty());
+ DCHECK(audio_focus_state_ == State::Suspended);
+}
+
+bool MediaSession::AddPlayer(MediaSessionObserver* observer,
+ int player_id,
+ Type type) {
+ // If the audio focus is already granted and is of type Content, there is
+ // nothing to do. If it is granted of type Transient the requested type is
+ // also transient, there is also nothing to do. Otherwise, the session needs
+ // to request audio focus again.
+ if (audio_focus_state_ == State::Active &&
+ (audio_focus_type_ == Type::Content || audio_focus_type_ == type)) {
+ players_.insert(PlayerIdentifier(observer, player_id));
+ return true;
+ }
+
+ State old_audio_focus_state = audio_focus_state_;
+ audio_focus_state_ = RequestSystemAudioFocus(type) ? State::Active
+ : State::Suspended;
+ audio_focus_type_ = type;
+
+ if (audio_focus_state_ != State::Active)
+ return false;
+
+ // The session should be reset if a player is starting while all players are
+ // suspended.
+ if (old_audio_focus_state != State::Active)
+ players_.clear();
+
+ players_.insert(PlayerIdentifier(observer, player_id));
+
+ return true;
+}
+
+void MediaSession::RemovePlayer(MediaSessionObserver* observer,
+ int player_id) {
+ auto it = players_.find(PlayerIdentifier(observer, player_id));
+ if (it != players_.end())
+ players_.erase(it);
+
+ AbandonSystemAudioFocusIfNeeded();
+}
+
+void MediaSession::RemovePlayers(MediaSessionObserver* observer) {
+ for (auto it = players_.begin(); it != players_.end();) {
+ if (it->observer == observer)
+ players_.erase(it++);
+ else
+ ++it;
+ }
+
+ AbandonSystemAudioFocusIfNeeded();
+}
+
+void MediaSession::OnSuspend(JNIEnv* env, jobject obj, jboolean temporary) {
+ OnSuspend(temporary);
+}
+
+void MediaSession::OnResume(JNIEnv* env, jobject obj) {
+ OnResume();
+}
+
+void MediaSession::ResetJavaRefForTest() {
+ j_media_session_.Reset();
+}
+
+bool MediaSession::IsActiveForTest() const {
+ return audio_focus_state_ == State::Active;
+}
+
+MediaSession::Type MediaSession::audio_focus_type_for_test() const {
+ return audio_focus_type_;
+}
+
+void MediaSession::OnSuspend(bool temporary) {
+ if (temporary)
+ audio_focus_state_ = State::TemporarilySuspended;
+ else
+ audio_focus_state_ = State::Suspended;
+
+ for (const auto& it : players_)
+ it.observer->OnSuspend(it.player_id);
+}
+
+void MediaSession::OnResume() {
+ audio_focus_state_ = State::Active;
+
+ for (const auto& it : players_)
+ it.observer->OnResume(it.player_id);
+}
+
+MediaSession::MediaSession(WebContents* web_contents)
+ : WebContentsObserver(web_contents),
+ audio_focus_state_(State::Suspended),
+ audio_focus_type_(Type::Transient) {
+}
+
+void MediaSession::Initialize() {
+ JNIEnv* env = base::android::AttachCurrentThread();
+ DCHECK(env);
+ j_media_session_.Reset(Java_MediaSession_createMediaSession(
+ env,
+ base::android::GetApplicationContext(),
+ reinterpret_cast<intptr_t>(this)));
+}
+
+bool MediaSession::RequestSystemAudioFocus(Type type) {
+ // During tests, j_media_session_ might be null.
+ if (j_media_session_.is_null())
+ return true;
+
+ JNIEnv* env = base::android::AttachCurrentThread();
+ DCHECK(env);
+ return Java_MediaSession_requestAudioFocus(env, j_media_session_.obj(),
+ type == Type::Transient);
+}
+
+void MediaSession::AbandonSystemAudioFocusIfNeeded() {
+ if (audio_focus_state_ == State::Suspended || !players_.empty())
+ return;
+
+ // During tests, j_media_session_ might be null.
+ if (!j_media_session_.is_null()) {
+ JNIEnv* env = base::android::AttachCurrentThread();
+ DCHECK(env);
+ Java_MediaSession_abandonAudioFocus(env, j_media_session_.obj());
+ }
+
+ audio_focus_state_ = State::Suspended;
+}
+
+} // namespace content
diff --git a/content/browser/media/android/media_session.h b/content/browser/media/android/media_session.h
new file mode 100644
index 0000000..f36bb4e
--- /dev/null
+++ b/content/browser/media/android/media_session.h
@@ -0,0 +1,134 @@
+// 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.
+
+#ifndef CONTENT_BROWSER_MEDIA_ANDROID_MEDIA_SESSION_H_
+#define CONTENT_BROWSER_MEDIA_ANDROID_MEDIA_SESSION_H_
+
+#include <jni.h>
+
+#include "base/android/scoped_java_ref.h"
+#include "base/id_map.h"
+#include "content/common/content_export.h"
+#include "content/public/browser/web_contents_observer.h"
+#include "content/public/browser/web_contents_user_data.h"
+
+namespace content {
+
+class MediaSessionBrowserTest;
+class MediaSessionObserver;
+
+// MediaSession manages the Android AudioFocus for a given WebContents. It is
+// requesting the audio focus, pausing when requested by the system and dropping
+// it on demand.
+// The audio focus can be of two types: Transient or Content. A Transient audio
+// focus will allow other players to duck instead of pausing and will be
+// declared as temporary to the system. A Content audio focus will not be
+// declared as temporary and will not allow other players to duck. If a given
+// WebContents can only have one audio focus at a time, it will be Content in
+// case of Transient and Content audio focus are both requested.
+// Android system interaction occurs in the Java counterpart to this class.
+class CONTENT_EXPORT MediaSession
+ : public content::WebContentsObserver,
+ protected content::WebContentsUserData<MediaSession> {
+ public:
+ enum class Type {
+ Content,
+ Transient
+ };
+
+ static bool RegisterMediaSession(JNIEnv* env);
+
+ // Returns the MediaSession associated to this WebContents. Creates one if
+ // none is currently available.
+ static MediaSession* Get(WebContents* web_contents);
+
+ ~MediaSession() override;
+
+ // Adds the given player to the current media session. Returns whether the
+ // player was successfully added. If it returns false, AddPlayer() should be
+ // called again later.
+ bool AddPlayer(MediaSessionObserver* observer, int player_id, Type type);
+
+ // Removes the given player from the current media session. Abandons audio
+ // focus if that was the last player in the session.
+ void RemovePlayer(MediaSessionObserver* observer, int player_id);
+
+ // Removes all the players associated with |observer|. Abandons audio focus if
+ // these were the last players in the session.
+ void RemovePlayers(MediaSessionObserver* observer);
+
+ // Called when the Android system requests the MediaSession to be suspended.
+ // Called by Java through JNI.
+ void OnSuspend(JNIEnv* env, jobject obj, jboolean temporary);
+
+ // Called when the Android system requests the MediaSession to be resumed.
+ // Called by Java through JNI.
+ void OnResume(JNIEnv* env, jobject obj);
+
+ protected:
+ friend class content::MediaSessionBrowserTest;
+
+ // Resets the |j_media_session_| ref to prevent calling the Java backend
+ // during content_browsertests.
+ void ResetJavaRefForTest();
+
+ bool IsActiveForTest() const;
+ Type audio_focus_type_for_test() const;
+
+ void OnSuspend(bool temporary);
+ void OnResume();
+
+ private:
+ friend class content::WebContentsUserData<MediaSession>;
+
+ enum class State {
+ Active,
+ TemporarilySuspended,
+ Suspended,
+ };
+
+ // Representation of a player for the MediaSession.
+ struct PlayerIdentifier {
+ PlayerIdentifier(MediaSessionObserver* observer, int player_id);
+ PlayerIdentifier(const PlayerIdentifier&) = default;
+
+ void operator=(const PlayerIdentifier&) = delete;
+ bool operator==(const PlayerIdentifier& player_identifier) const;
+
+ // Hash operator for base::hash_map<>.
+ struct Hash {
+ size_t operator()(const PlayerIdentifier& player_identifier) const;
+ };
+
+ MediaSessionObserver* observer;
+ int player_id;
+ };
+ using PlayersMap = base::hash_set<PlayerIdentifier, PlayerIdentifier::Hash>;
+
+ explicit MediaSession(WebContents* web_contents);
+
+ // Setup the JNI.
+ void Initialize();
+
+ // Requests audio focus to Android using |j_media_session_|.
+ // Returns whether the request was granted. If |j_media_session_| is null, it
+ // will always return true.
+ bool RequestSystemAudioFocus(Type type);
+
+ // To be called after a call to AbandonAudioFocus() in order to call the Java
+ // MediaSession if the audio focus really need to be abandoned.
+ void AbandonSystemAudioFocusIfNeeded();
+
+ base::android::ScopedJavaGlobalRef<jobject> j_media_session_;
+ PlayersMap players_;
+
+ State audio_focus_state_;
+ Type audio_focus_type_;
+
+ DISALLOW_COPY_AND_ASSIGN(MediaSession);
+};
+
+} // namespace content
+
+#endif // CONTENT_BROWSER_MEDIA_ANDROID_MEDIA_SESSION_H_
diff --git a/content/browser/media/android/media_session_browsertest.cc b/content/browser/media/android/media_session_browsertest.cc
new file mode 100644
index 0000000..1fc8121
--- /dev/null
+++ b/content/browser/media/android/media_session_browsertest.cc
@@ -0,0 +1,544 @@
+// 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/browser/media/android/media_session.h"
+
+#include <list>
+#include <vector>
+
+#include "content/browser/media/android/media_session_observer.h"
+#include "content/public/test/content_browser_test.h"
+#include "content/shell/browser/shell.h"
+
+namespace content {
+
+class MockMediaSessionObserver : public MediaSessionObserver {
+ public:
+ MockMediaSessionObserver()
+ : received_resume_calls_(0),
+ received_suspend_calls_(0) {
+ }
+
+ ~MockMediaSessionObserver() override = default;
+
+ // Implements MediaSessionObserver.
+ void OnSuspend(int player_id) override {
+ DCHECK(player_id >= 0);
+ DCHECK(players_.size() > static_cast<size_t>(player_id));
+
+ ++received_suspend_calls_;
+ players_[player_id] = false;
+ }
+ void OnResume(int player_id) override {
+ DCHECK(player_id >= 0);
+ DCHECK(players_.size() > static_cast<size_t>(player_id));
+
+ ++received_resume_calls_;
+ players_[player_id] = true;
+ }
+
+ int StartNewPlayer() {
+ players_.push_back(true);
+ return players_.size() - 1;
+ }
+
+ bool IsPlaying(size_t player_id) {
+ DCHECK(players_.size() > player_id);
+ return players_[player_id];
+ }
+
+ void SetPlaying(size_t player_id, bool playing) {
+ DCHECK(players_.size() > player_id);
+ players_[player_id] = playing;
+ }
+
+ int received_suspend_calls() const {
+ return received_suspend_calls_;
+ }
+
+ int received_resume_calls() const {
+ return received_resume_calls_;
+ }
+
+ private:
+ // Basic representation of the players. The position in the vector is the
+ // player_id. The value of the vector is the playing status.
+ std::vector<bool> players_;
+
+ int received_resume_calls_;
+ int received_suspend_calls_;
+};
+
+class MediaSessionBrowserTest : public ContentBrowserTest {
+ protected:
+ MediaSessionBrowserTest() = default;
+
+ void DisableNativeBackend(MediaSession* media_session) {
+ media_session->ResetJavaRefForTest();
+ }
+
+ void StartNewPlayer(MediaSession* media_session,
+ MockMediaSessionObserver* media_session_observer,
+ MediaSession::Type type) {
+ bool result = media_session->AddPlayer(
+ media_session_observer,
+ media_session_observer->StartNewPlayer(),
+ type);
+ EXPECT_TRUE(result);
+ }
+
+ void SuspendSession(MediaSession* media_session) {
+ media_session->OnSuspend(true);
+ }
+
+ void ResumeSession(MediaSession* media_session) {
+ media_session->OnResume();
+ }
+
+ bool HasAudioFocus(MediaSession* media_session) {
+ return media_session->IsActiveForTest();
+ }
+
+ MediaSession::Type GetSessionType(MediaSession* media_session) {
+ return media_session->audio_focus_type_for_test();
+ }
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(MediaSessionBrowserTest);
+};
+
+IN_PROC_BROWSER_TEST_F(MediaSessionBrowserTest,
+ PlayersFromSameObserverDoNotStopEachOtherInSameSession) {
+ MediaSession* media_session = MediaSession::Get(shell()->web_contents());
+ ASSERT_TRUE(media_session);
+ DisableNativeBackend(media_session);
+
+ scoped_ptr<MockMediaSessionObserver> media_session_observer(
+ new MockMediaSessionObserver);
+
+ StartNewPlayer(media_session, media_session_observer.get(),
+ MediaSession::Type::Content);
+ StartNewPlayer(media_session, media_session_observer.get(),
+ MediaSession::Type::Content);
+ StartNewPlayer(media_session, media_session_observer.get(),
+ MediaSession::Type::Content);
+
+ EXPECT_TRUE(media_session_observer->IsPlaying(0));
+ EXPECT_TRUE(media_session_observer->IsPlaying(1));
+ EXPECT_TRUE(media_session_observer->IsPlaying(2));
+
+ media_session->RemovePlayers(media_session_observer.get());
+}
+
+IN_PROC_BROWSER_TEST_F(MediaSessionBrowserTest,
+ PlayersFromManyObserverDoNotStopEachOtherInSameSession) {
+ MediaSession* media_session = MediaSession::Get(shell()->web_contents());
+ ASSERT_TRUE(media_session);
+ DisableNativeBackend(media_session);
+
+ scoped_ptr<MockMediaSessionObserver> media_session_observer_1(
+ new MockMediaSessionObserver);
+ scoped_ptr<MockMediaSessionObserver> media_session_observer_2(
+ new MockMediaSessionObserver);
+ scoped_ptr<MockMediaSessionObserver> media_session_observer_3(
+ new MockMediaSessionObserver);
+
+ StartNewPlayer(media_session, media_session_observer_1.get(),
+ MediaSession::Type::Content);
+ StartNewPlayer(media_session, media_session_observer_2.get(),
+ MediaSession::Type::Content);
+ StartNewPlayer(media_session, media_session_observer_3.get(),
+ MediaSession::Type::Content);
+
+ EXPECT_TRUE(media_session_observer_1->IsPlaying(0));
+ EXPECT_TRUE(media_session_observer_2->IsPlaying(0));
+ EXPECT_TRUE(media_session_observer_3->IsPlaying(0));
+
+ media_session->RemovePlayers(media_session_observer_1.get());
+ media_session->RemovePlayers(media_session_observer_2.get());
+ media_session->RemovePlayers(media_session_observer_3.get());
+}
+
+IN_PROC_BROWSER_TEST_F(MediaSessionBrowserTest,
+ SuspendedMediaSessionStopsPlayers) {
+ MediaSession* media_session = MediaSession::Get(shell()->web_contents());
+ ASSERT_TRUE(media_session);
+ DisableNativeBackend(media_session);
+
+ scoped_ptr<MockMediaSessionObserver> media_session_observer(
+ new MockMediaSessionObserver);
+
+ StartNewPlayer(media_session, media_session_observer.get(),
+ MediaSession::Type::Content);
+ StartNewPlayer(media_session, media_session_observer.get(),
+ MediaSession::Type::Content);
+ StartNewPlayer(media_session, media_session_observer.get(),
+ MediaSession::Type::Content);
+
+ SuspendSession(media_session);
+
+ EXPECT_FALSE(media_session_observer->IsPlaying(0));
+ EXPECT_FALSE(media_session_observer->IsPlaying(1));
+ EXPECT_FALSE(media_session_observer->IsPlaying(2));
+
+ media_session->RemovePlayers(media_session_observer.get());
+}
+
+IN_PROC_BROWSER_TEST_F(MediaSessionBrowserTest,
+ ResumedMediaSessionRestartsPlayers) {
+ MediaSession* media_session = MediaSession::Get(shell()->web_contents());
+ ASSERT_TRUE(media_session);
+ DisableNativeBackend(media_session);
+
+ scoped_ptr<MockMediaSessionObserver> media_session_observer(
+ new MockMediaSessionObserver);
+
+ StartNewPlayer(media_session, media_session_observer.get(),
+ MediaSession::Type::Content);
+ StartNewPlayer(media_session, media_session_observer.get(),
+ MediaSession::Type::Content);
+ StartNewPlayer(media_session, media_session_observer.get(),
+ MediaSession::Type::Content);
+
+ SuspendSession(media_session);
+ ResumeSession(media_session);
+
+ EXPECT_TRUE(media_session_observer->IsPlaying(0));
+ EXPECT_TRUE(media_session_observer->IsPlaying(1));
+ EXPECT_TRUE(media_session_observer->IsPlaying(2));
+
+ media_session->RemovePlayers(media_session_observer.get());
+}
+
+IN_PROC_BROWSER_TEST_F(MediaSessionBrowserTest,
+ StartedPlayerOnSuspendedSessionPlaysAlone) {
+ MediaSession* media_session = MediaSession::Get(shell()->web_contents());
+ ASSERT_TRUE(media_session);
+ DisableNativeBackend(media_session);
+
+ scoped_ptr<MockMediaSessionObserver> media_session_observer(
+ new MockMediaSessionObserver);
+
+ StartNewPlayer(media_session, media_session_observer.get(),
+ MediaSession::Type::Content);
+
+ EXPECT_TRUE(media_session_observer->IsPlaying(0));
+
+ SuspendSession(media_session);
+
+ EXPECT_FALSE(media_session_observer->IsPlaying(0));
+
+ StartNewPlayer(media_session, media_session_observer.get(),
+ MediaSession::Type::Content);
+
+ EXPECT_FALSE(media_session_observer->IsPlaying(0));
+ EXPECT_TRUE(media_session_observer->IsPlaying(1));
+
+ StartNewPlayer(media_session, media_session_observer.get(),
+ MediaSession::Type::Content);
+
+ EXPECT_FALSE(media_session_observer->IsPlaying(0));
+ EXPECT_TRUE(media_session_observer->IsPlaying(1));
+ EXPECT_TRUE(media_session_observer->IsPlaying(2));
+
+ media_session->RemovePlayers(media_session_observer.get());
+}
+
+IN_PROC_BROWSER_TEST_F(MediaSessionBrowserTest, AudioFocusInitialState) {
+ MediaSession* media_session = MediaSession::Get(shell()->web_contents());
+ ASSERT_TRUE(media_session);
+ DisableNativeBackend(media_session);
+
+ EXPECT_FALSE(HasAudioFocus(media_session));
+}
+
+IN_PROC_BROWSER_TEST_F(MediaSessionBrowserTest, StartPlayerGivesFocus) {
+ MediaSession* media_session = MediaSession::Get(shell()->web_contents());
+ ASSERT_TRUE(media_session);
+ DisableNativeBackend(media_session);
+
+ scoped_ptr<MockMediaSessionObserver> media_session_observer(
+ new MockMediaSessionObserver);
+
+ StartNewPlayer(media_session, media_session_observer.get(),
+ MediaSession::Type::Content);
+
+ EXPECT_TRUE(HasAudioFocus(media_session));
+
+ media_session->RemovePlayers(media_session_observer.get());
+}
+
+IN_PROC_BROWSER_TEST_F(MediaSessionBrowserTest, SuspendGivesAwayAudioFocus) {
+ MediaSession* media_session = MediaSession::Get(shell()->web_contents());
+ ASSERT_TRUE(media_session);
+ DisableNativeBackend(media_session);
+
+ scoped_ptr<MockMediaSessionObserver> media_session_observer(
+ new MockMediaSessionObserver);
+
+ StartNewPlayer(media_session, media_session_observer.get(),
+ MediaSession::Type::Content);
+
+ SuspendSession(media_session);
+
+ EXPECT_FALSE(HasAudioFocus(media_session));
+
+ media_session->RemovePlayers(media_session_observer.get());
+}
+
+IN_PROC_BROWSER_TEST_F(MediaSessionBrowserTest, ResumeGivesBackAudioFocus) {
+ MediaSession* media_session = MediaSession::Get(shell()->web_contents());
+ ASSERT_TRUE(media_session);
+ DisableNativeBackend(media_session);
+
+ scoped_ptr<MockMediaSessionObserver> media_session_observer(
+ new MockMediaSessionObserver);
+
+ StartNewPlayer(media_session, media_session_observer.get(),
+ MediaSession::Type::Content);
+
+ SuspendSession(media_session);
+ ResumeSession(media_session);
+
+ EXPECT_TRUE(HasAudioFocus(media_session));
+
+ media_session->RemovePlayers(media_session_observer.get());
+}
+
+IN_PROC_BROWSER_TEST_F(MediaSessionBrowserTest,
+ RemovingLastPlayerDropsAudioFocus_1) {
+ MediaSession* media_session = MediaSession::Get(shell()->web_contents());
+ ASSERT_TRUE(media_session);
+ DisableNativeBackend(media_session);
+
+ scoped_ptr<MockMediaSessionObserver> media_session_observer(
+ new MockMediaSessionObserver);
+
+ StartNewPlayer(media_session, media_session_observer.get(),
+ MediaSession::Type::Content);
+ StartNewPlayer(media_session, media_session_observer.get(),
+ MediaSession::Type::Content);
+ StartNewPlayer(media_session, media_session_observer.get(),
+ MediaSession::Type::Content);
+
+ media_session->RemovePlayer(media_session_observer.get(), 0);
+ EXPECT_TRUE(HasAudioFocus(media_session));
+ media_session->RemovePlayer(media_session_observer.get(), 1);
+ EXPECT_TRUE(HasAudioFocus(media_session));
+ media_session->RemovePlayer(media_session_observer.get(), 2);
+ EXPECT_FALSE(HasAudioFocus(media_session));
+}
+
+IN_PROC_BROWSER_TEST_F(MediaSessionBrowserTest,
+ RemovingLastPlayerDropsAudioFocus_2) {
+ MediaSession* media_session = MediaSession::Get(shell()->web_contents());
+ ASSERT_TRUE(media_session);
+ DisableNativeBackend(media_session);
+
+ scoped_ptr<MockMediaSessionObserver> media_session_observer_1(
+ new MockMediaSessionObserver);
+ scoped_ptr<MockMediaSessionObserver> media_session_observer_2(
+ new MockMediaSessionObserver);
+ scoped_ptr<MockMediaSessionObserver> media_session_observer_3(
+ new MockMediaSessionObserver);
+
+ StartNewPlayer(media_session, media_session_observer_1.get(),
+ MediaSession::Type::Content);
+ StartNewPlayer(media_session, media_session_observer_2.get(),
+ MediaSession::Type::Content);
+ StartNewPlayer(media_session, media_session_observer_3.get(),
+ MediaSession::Type::Content);
+
+ media_session->RemovePlayer(media_session_observer_1.get(), 0);
+ EXPECT_TRUE(HasAudioFocus(media_session));
+ media_session->RemovePlayer(media_session_observer_2.get(), 0);
+ EXPECT_TRUE(HasAudioFocus(media_session));
+ media_session->RemovePlayer(media_session_observer_3.get(), 0);
+ EXPECT_FALSE(HasAudioFocus(media_session));
+}
+
+IN_PROC_BROWSER_TEST_F(MediaSessionBrowserTest,
+ RemovingLastPlayerDropsAudioFocus_3) {
+ MediaSession* media_session = MediaSession::Get(shell()->web_contents());
+ ASSERT_TRUE(media_session);
+ DisableNativeBackend(media_session);
+
+ scoped_ptr<MockMediaSessionObserver> media_session_observer_1(
+ new MockMediaSessionObserver);
+ scoped_ptr<MockMediaSessionObserver> media_session_observer_2(
+ new MockMediaSessionObserver);
+
+ StartNewPlayer(media_session, media_session_observer_1.get(),
+ MediaSession::Type::Content);
+ StartNewPlayer(media_session, media_session_observer_1.get(),
+ MediaSession::Type::Content);
+ StartNewPlayer(media_session, media_session_observer_2.get(),
+ MediaSession::Type::Content);
+ StartNewPlayer(media_session, media_session_observer_2.get(),
+ MediaSession::Type::Content);
+
+ media_session->RemovePlayers(media_session_observer_1.get());
+ EXPECT_TRUE(HasAudioFocus(media_session));
+ media_session->RemovePlayers(media_session_observer_2.get());
+ EXPECT_FALSE(HasAudioFocus(media_session));
+}
+
+IN_PROC_BROWSER_TEST_F(MediaSessionBrowserTest, ResumePlayGivesAudioFocus) {
+ MediaSession* media_session = MediaSession::Get(shell()->web_contents());
+ ASSERT_TRUE(media_session);
+ DisableNativeBackend(media_session);
+
+ scoped_ptr<MockMediaSessionObserver> media_session_observer(
+ new MockMediaSessionObserver);
+
+ StartNewPlayer(media_session, media_session_observer.get(),
+ MediaSession::Type::Content);
+
+ media_session->RemovePlayer(media_session_observer.get(), 0);
+ EXPECT_FALSE(HasAudioFocus(media_session));
+
+ EXPECT_TRUE(media_session->AddPlayer(media_session_observer.get(), 0,
+ MediaSession::Type::Content));
+ EXPECT_TRUE(HasAudioFocus(media_session));
+
+ media_session->RemovePlayers(media_session_observer.get());
+}
+
+IN_PROC_BROWSER_TEST_F(MediaSessionBrowserTest,
+ ResumeSuspendAreSentOnlyOncePerPlayers_1) {
+ MediaSession* media_session = MediaSession::Get(shell()->web_contents());
+ ASSERT_TRUE(media_session);
+ DisableNativeBackend(media_session);
+
+ scoped_ptr<MockMediaSessionObserver> media_session_observer(
+ new MockMediaSessionObserver);
+
+ StartNewPlayer(media_session, media_session_observer.get(),
+ MediaSession::Type::Content);
+ StartNewPlayer(media_session, media_session_observer.get(),
+ MediaSession::Type::Content);
+ StartNewPlayer(media_session, media_session_observer.get(),
+ MediaSession::Type::Content);
+
+ EXPECT_EQ(0, media_session_observer->received_suspend_calls());
+ EXPECT_EQ(0, media_session_observer->received_resume_calls());
+
+ SuspendSession(media_session);
+ EXPECT_EQ(3, media_session_observer->received_suspend_calls());
+
+ ResumeSession(media_session);
+ EXPECT_EQ(3, media_session_observer->received_resume_calls());
+
+ media_session->RemovePlayers(media_session_observer.get());
+}
+
+IN_PROC_BROWSER_TEST_F(MediaSessionBrowserTest,
+ ResumeSuspendAreSentOnlyOncePerPlayers_2) {
+ MediaSession* media_session = MediaSession::Get(shell()->web_contents());
+ ASSERT_TRUE(media_session);
+ DisableNativeBackend(media_session);
+
+ scoped_ptr<MockMediaSessionObserver> media_session_observer(
+ new MockMediaSessionObserver);
+
+ StartNewPlayer(media_session, media_session_observer.get(),
+ MediaSession::Type::Content);
+ StartNewPlayer(media_session, media_session_observer.get(),
+ MediaSession::Type::Content);
+ StartNewPlayer(media_session, media_session_observer.get(),
+ MediaSession::Type::Content);
+
+ // Adding the three players above again.
+ EXPECT_TRUE(media_session->AddPlayer(media_session_observer.get(), 0,
+ MediaSession::Type::Content));
+ EXPECT_TRUE(media_session->AddPlayer(media_session_observer.get(), 1,
+ MediaSession::Type::Content));
+ EXPECT_TRUE(media_session->AddPlayer(media_session_observer.get(), 2,
+ MediaSession::Type::Content));
+
+ EXPECT_EQ(0, media_session_observer->received_suspend_calls());
+ EXPECT_EQ(0, media_session_observer->received_resume_calls());
+
+ SuspendSession(media_session);
+ EXPECT_EQ(3, media_session_observer->received_suspend_calls());
+
+ ResumeSession(media_session);
+ EXPECT_EQ(3, media_session_observer->received_resume_calls());
+
+ media_session->RemovePlayers(media_session_observer.get());
+}
+
+IN_PROC_BROWSER_TEST_F(MediaSessionBrowserTest,
+ RemovingTheSamePlayerTwiceIsANoop) {
+ MediaSession* media_session = MediaSession::Get(shell()->web_contents());
+ ASSERT_TRUE(media_session);
+ DisableNativeBackend(media_session);
+
+ scoped_ptr<MockMediaSessionObserver> media_session_observer(
+ new MockMediaSessionObserver);
+
+ StartNewPlayer(media_session, media_session_observer.get(),
+ MediaSession::Type::Content);
+
+ media_session->RemovePlayer(media_session_observer.get(), 0);
+ media_session->RemovePlayer(media_session_observer.get(), 0);
+
+ media_session->RemovePlayers(media_session_observer.get());
+}
+
+IN_PROC_BROWSER_TEST_F(MediaSessionBrowserTest, MediaSessionType) {
+ MediaSession* media_session = MediaSession::Get(shell()->web_contents());
+ ASSERT_TRUE(media_session);
+ DisableNativeBackend(media_session);
+
+ scoped_ptr<MockMediaSessionObserver> media_session_observer(
+ new MockMediaSessionObserver);
+
+ // Starting a player with a given type should set the session to that type.
+ StartNewPlayer(media_session, media_session_observer.get(),
+ MediaSession::Type::Transient);
+ EXPECT_EQ(MediaSession::Type::Transient, GetSessionType(media_session));
+
+ // Adding a player of the same type should have no effect on the type.
+ StartNewPlayer(media_session, media_session_observer.get(),
+ MediaSession::Type::Transient);
+ EXPECT_EQ(MediaSession::Type::Transient, GetSessionType(media_session));
+
+ // Adding a player of Content type should override the current type.
+ StartNewPlayer(media_session, media_session_observer.get(),
+ MediaSession::Type::Content);
+ EXPECT_EQ(MediaSession::Type::Content, GetSessionType(media_session));
+
+ // Adding a player of the Transient type should have no effect on the type.
+ StartNewPlayer(media_session, media_session_observer.get(),
+ MediaSession::Type::Transient);
+ EXPECT_EQ(MediaSession::Type::Content, GetSessionType(media_session));
+
+ EXPECT_TRUE(media_session_observer->IsPlaying(0));
+ EXPECT_TRUE(media_session_observer->IsPlaying(1));
+ EXPECT_TRUE(media_session_observer->IsPlaying(2));
+ EXPECT_TRUE(media_session_observer->IsPlaying(3));
+
+ SuspendSession(media_session);
+
+ EXPECT_FALSE(media_session_observer->IsPlaying(0));
+ EXPECT_FALSE(media_session_observer->IsPlaying(1));
+ EXPECT_FALSE(media_session_observer->IsPlaying(2));
+ EXPECT_FALSE(media_session_observer->IsPlaying(3));
+
+ EXPECT_EQ(MediaSession::Type::Content, GetSessionType(media_session));
+
+ ResumeSession(media_session);
+
+ EXPECT_TRUE(media_session_observer->IsPlaying(0));
+ EXPECT_TRUE(media_session_observer->IsPlaying(1));
+ EXPECT_TRUE(media_session_observer->IsPlaying(2));
+ EXPECT_TRUE(media_session_observer->IsPlaying(3));
+
+ EXPECT_EQ(MediaSession::Type::Content, GetSessionType(media_session));
+
+ media_session->RemovePlayers(media_session_observer.get());
+}
+
+} // namespace content
diff --git a/content/browser/media/android/media_session_observer.h b/content/browser/media/android/media_session_observer.h
new file mode 100644
index 0000000..6316ab6
--- /dev/null
+++ b/content/browser/media/android/media_session_observer.h
@@ -0,0 +1,24 @@
+// 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.
+
+#ifndef CONTENT_BROWSER_MEDIA_ANDROID_MEDIA_SESSION_OBSERVER_H_
+#define CONTENT_BROWSER_MEDIA_ANDROID_MEDIA_SESSION_OBSERVER_H_
+
+namespace content {
+
+class MediaSessionObserver {
+ public:
+ MediaSessionObserver() = default;
+ virtual ~MediaSessionObserver() = default;
+
+ // The given |player_id| has been suspended by the MediaSession.
+ virtual void OnSuspend(int player_id) = 0;
+
+ // The given |player_id| has been resumed by the MediaSession.
+ virtual void OnResume(int player_id) = 0;
+};
+
+} // namespace content
+
+#endif // CONTENT_BROWSER_MEDIA_ANDROID_MEDIA_SESSION_OBSERVER_H_
diff --git a/content/content_browser.gypi b/content/content_browser.gypi
index b8a142a..a48854e 100644
--- a/content/content_browser.gypi
+++ b/content/content_browser.gypi
@@ -976,6 +976,9 @@
'browser/media/android/media_drm_credential_manager.h',
'browser/media/android/media_resource_getter_impl.cc',
'browser/media/android/media_resource_getter_impl.h',
+ 'browser/media/android/media_session.cc',
+ 'browser/media/android/media_session.h',
+ 'browser/media/android/media_session_observer.h',
'browser/media/audio_state_provider.cc',
'browser/media/audio_state_provider.h',
'browser/media/audio_stream_monitor.cc',
diff --git a/content/content_jni.gypi b/content/content_jni.gypi
index 85f417c..8ea648f 100644
--- a/content/content_jni.gypi
+++ b/content/content_jni.gypi
@@ -29,6 +29,7 @@
'public/android/java/src/org/chromium/content/browser/InterstitialPageDelegateAndroid.java',
'public/android/java/src/org/chromium/content/browser/LocationProviderAdapter.java',
'public/android/java/src/org/chromium/content/browser/MediaDrmCredentialManager.java',
+ 'public/android/java/src/org/chromium/content/browser/MediaSession.java',
'public/android/java/src/org/chromium/content/browser/MediaResourceGetter.java',
'public/android/java/src/org/chromium/content/browser/PowerSaveBlocker.java',
'public/android/java/src/org/chromium/content/browser/ServiceRegistrar.java',
diff --git a/content/content_tests.gypi b/content/content_tests.gypi
index 09fee67..4c225d6 100644
--- a/content/content_tests.gypi
+++ b/content/content_tests.gypi
@@ -215,6 +215,7 @@
'browser/indexed_db/mock_browsertest_indexed_db_class_factory.h',
'browser/loader/resource_dispatcher_host_browsertest.cc',
'browser/manifest/manifest_browsertest.cc',
+ 'browser/media/android/media_session_browsertest.cc',
'browser/media/encrypted_media_browsertest.cc',
'browser/media/media_browsertest.cc',
'browser/media/media_browsertest.h',
diff --git a/content/public/android/java/src/org/chromium/content/browser/MediaSession.java b/content/public/android/java/src/org/chromium/content/browser/MediaSession.java
new file mode 100644
index 0000000..7395f9b
--- /dev/null
+++ b/content/public/android/java/src/org/chromium/content/browser/MediaSession.java
@@ -0,0 +1,88 @@
+// 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.
+
+package org.chromium.content.browser;
+
+import android.content.Context;
+import android.media.AudioManager;
+
+import org.chromium.base.CalledByNative;
+import org.chromium.base.JNINamespace;
+import org.chromium.base.Log;
+
+/**
+ * MediaSession is the Java counterpart of content::MediaSession.
+ * It is being used to communicate from content::MediaSession (C++) to the
+ * Android system. A MediaSession is implementing OnAudioFocusChangeListener,
+ * making it an audio focus holder for Android. Thus two instances of
+ * MediaSession can't have audio focus at the same time.
+ * A MediaSession will use the type requested from its C++ counterpart and will
+ * resume its play using the same type if it were to happen, for example, when
+ * it got temporarily suspended by a transient sound like a notification.
+ */
+@JNINamespace("content")
+public class MediaSession implements AudioManager.OnAudioFocusChangeListener {
+ private static final String TAG = "MediaSession";
+
+ private Context mContext;
+ private int mFocusType;
+
+ // Native pointer to C++ content::MediaSession.
+ private final long mNativeMediaSession;
+
+ private MediaSession(final Context context, long nativeMediaSession) {
+ mContext = context;
+ mNativeMediaSession = nativeMediaSession;
+ }
+
+ @CalledByNative
+ private static MediaSession createMediaSession(Context context, long nativeMediaSession) {
+ return new MediaSession(context, nativeMediaSession);
+ }
+
+ @CalledByNative
+ private boolean requestAudioFocus(boolean transientFocus) {
+ mFocusType = transientFocus ? AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
+ : AudioManager.AUDIOFOCUS_GAIN;
+ return requestAudioFocusInternal();
+ }
+
+ @CalledByNative
+ private void abandonAudioFocus() {
+ AudioManager am = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+ am.abandonAudioFocus(this);
+ }
+
+ private boolean requestAudioFocusInternal() {
+ AudioManager am = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+
+ int result = am.requestAudioFocus(this, AudioManager.STREAM_MUSIC, mFocusType);
+ return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
+ }
+
+ @Override
+ public void onAudioFocusChange(int focusChange) {
+ switch (focusChange) {
+ case AudioManager.AUDIOFOCUS_GAIN:
+ if (requestAudioFocusInternal()) {
+ nativeOnResume(mNativeMediaSession);
+ }
+ break;
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
+ nativeOnSuspend(mNativeMediaSession, true);
+ break;
+ case AudioManager.AUDIOFOCUS_LOSS:
+ abandonAudioFocus();
+ nativeOnSuspend(mNativeMediaSession, false);
+ break;
+ default:
+ Log.w(TAG, "onAudioFocusChange called with unexpected value " + focusChange);
+ break;
+ }
+ }
+
+ private native void nativeOnSuspend(long nativeMediaSession, boolean temporary);
+ private native void nativeOnResume(long nativeMediaSession);
+}
diff --git a/content/public/android/java/src/org/chromium/content/common/ContentSwitches.java b/content/public/android/java/src/org/chromium/content/common/ContentSwitches.java
index d40be24..0c29fc7 100644
--- a/content/public/android/java/src/org/chromium/content/common/ContentSwitches.java
+++ b/content/public/android/java/src/org/chromium/content/common/ContentSwitches.java
@@ -69,6 +69,10 @@ public abstract class ContentSwitches {
// Native switch kEnableCredentialManagerAPI
public static final String ENABLE_CREDENTIAL_MANAGER_API = "enable-credential-manager-api";
+ // Native switch kDisableGestureRequirementForMediaPlayback
+ public static final String DISABLE_GESTURE_REQUIREMENT_FOR_MEDIA_PLAYBACK =
+ "disable-gesture-requirement-for-media-playback";
+
// Prevent instantiation.
private ContentSwitches() {}
}
diff --git a/content/public/android/javatests/src/org/chromium/content/browser/MediaSessionTest.java b/content/public/android/javatests/src/org/chromium/content/browser/MediaSessionTest.java
new file mode 100644
index 0000000..9a9576d
--- /dev/null
+++ b/content/public/android/javatests/src/org/chromium/content/browser/MediaSessionTest.java
@@ -0,0 +1,371 @@
+// 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.
+
+package org.chromium.content.browser;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import org.chromium.base.test.util.CommandLineFlags;
+import org.chromium.base.test.util.Feature;
+import org.chromium.content.browser.test.util.Criteria;
+import org.chromium.content.browser.test.util.CriteriaHelper;
+import org.chromium.content.browser.test.util.DOMUtils;
+import org.chromium.content.common.ContentSwitches;
+import org.chromium.content_shell_apk.ContentShellTestBase;
+
+/**
+ * Tests for MediaSession.
+ */
+@CommandLineFlags.Add(ContentSwitches.DISABLE_GESTURE_REQUIREMENT_FOR_MEDIA_PLAYBACK)
+public class MediaSessionTest extends ContentShellTestBase {
+ private static final String MEDIA_SESSION_TEST_URL =
+ "content/test/data/android/media/media-session.html";
+ private static final String VERY_SHORT_AUDIO = "very-short-audio";
+ private static final String SHORT_AUDIO = "short-audio";
+ private static final String LONG_AUDIO = "long-audio";
+ private static final String VERY_SHORT_VIDEO = "very-short-video";
+ private static final String SHORT_VIDEO = "short-video";
+ private static final String LONG_VIDEO = "long-video";
+
+ private AudioManager getAudioManager() {
+ return (AudioManager) getActivity().getApplicationContext().getSystemService(
+ Context.AUDIO_SERVICE);
+ }
+
+ private class MockAudioFocusChangeListener implements AudioManager.OnAudioFocusChangeListener {
+ private int mAudioFocusState = AudioManager.AUDIOFOCUS_LOSS;
+
+ @Override
+ public void onAudioFocusChange(int focusChange) {
+ mAudioFocusState = focusChange;
+ }
+
+ public int getAudioFocusState() {
+ return mAudioFocusState;
+ }
+
+ public void requestAudioFocus(int focusType) throws Exception {
+ int result = getAudioManager().requestAudioFocus(
+ this, AudioManager.STREAM_MUSIC, focusType);
+ if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+ fail("Did not get audio focus");
+ } else {
+ mAudioFocusState = focusType;
+ }
+ }
+
+ public void abandonAudioFocus() {
+ getAudioManager().abandonAudioFocus(this);
+ mAudioFocusState = AudioManager.AUDIOFOCUS_LOSS;
+ }
+
+ public boolean waitForFocusStateChange(final int focusType) throws InterruptedException {
+ return CriteriaHelper.pollForCriteria(new Criteria() {
+ @Override
+ public boolean isSatisfied() {
+ return getAudioFocusState() == focusType;
+ }
+ });
+ }
+ }
+
+ private MockAudioFocusChangeListener mAudioFocusChangeListener;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ try {
+ startActivityWithTestUrl(MEDIA_SESSION_TEST_URL);
+ } catch (Throwable t) {
+ fail("Couldn't load test page");
+ }
+
+ mAudioFocusChangeListener = new MockAudioFocusChangeListener();
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mAudioFocusChangeListener.abandonAudioFocus();
+
+ super.tearDown();
+ }
+
+ @SmallTest
+ @Feature({"MediaSession"})
+ public void testDontStopEachOther() throws Exception {
+ assertTrue(DOMUtils.isMediaPaused(getWebContents(), LONG_AUDIO));
+ DOMUtils.playMedia(getWebContents(), LONG_AUDIO);
+ assertTrue(DOMUtils.waitForMediaPlay(getWebContents(), LONG_AUDIO));
+
+ assertTrue(DOMUtils.isMediaPaused(getWebContents(), LONG_VIDEO));
+ DOMUtils.playMedia(getWebContents(), LONG_VIDEO);
+ assertTrue(DOMUtils.waitForMediaPlay(getWebContents(), LONG_VIDEO));
+
+ assertTrue(DOMUtils.isMediaPaused(getWebContents(), SHORT_VIDEO));
+ DOMUtils.playMedia(getWebContents(), SHORT_VIDEO);
+ assertTrue(DOMUtils.waitForMediaPlay(getWebContents(), SHORT_VIDEO));
+
+ assertTrue(DOMUtils.isMediaPaused(getWebContents(), SHORT_AUDIO));
+ DOMUtils.playMedia(getWebContents(), SHORT_AUDIO);
+ assertTrue(DOMUtils.waitForMediaPlay(getWebContents(), SHORT_AUDIO));
+
+ assertFalse(DOMUtils.isMediaPaused(getWebContents(), SHORT_AUDIO));
+ assertFalse(DOMUtils.isMediaPaused(getWebContents(), LONG_AUDIO));
+ assertFalse(DOMUtils.isMediaPaused(getWebContents(), SHORT_VIDEO));
+ assertFalse(DOMUtils.isMediaPaused(getWebContents(), LONG_VIDEO));
+ }
+
+ @MediumTest
+ @Feature({"MediaSession"})
+ public void testShortAudioIsTransient() throws Exception {
+ assertEquals(AudioManager.AUDIOFOCUS_LOSS, mAudioFocusChangeListener.getAudioFocusState());
+ mAudioFocusChangeListener.requestAudioFocus(AudioManager.AUDIOFOCUS_GAIN);
+ assertEquals(AudioManager.AUDIOFOCUS_GAIN, mAudioFocusChangeListener.getAudioFocusState());
+
+ DOMUtils.playMedia(getWebContents(), VERY_SHORT_AUDIO);
+ assertTrue(DOMUtils.waitForMediaPlay(getWebContents(), VERY_SHORT_AUDIO));
+
+ assertTrue(mAudioFocusChangeListener.waitForFocusStateChange(
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK));
+ assertTrue(mAudioFocusChangeListener.waitForFocusStateChange(AudioManager.AUDIOFOCUS_GAIN));
+ }
+
+ @MediumTest
+ @Feature({"MediaSession"})
+ public void testShortVideoIsTransient() throws Exception {
+ assertEquals(AudioManager.AUDIOFOCUS_LOSS, mAudioFocusChangeListener.getAudioFocusState());
+ mAudioFocusChangeListener.requestAudioFocus(AudioManager.AUDIOFOCUS_GAIN);
+ assertEquals(AudioManager.AUDIOFOCUS_GAIN, mAudioFocusChangeListener.getAudioFocusState());
+
+ DOMUtils.playMedia(getWebContents(), VERY_SHORT_VIDEO);
+ assertTrue(DOMUtils.waitForMediaPlay(getWebContents(), VERY_SHORT_VIDEO));
+
+ assertTrue(mAudioFocusChangeListener.waitForFocusStateChange(
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK));
+ assertTrue(mAudioFocusChangeListener.waitForFocusStateChange(AudioManager.AUDIOFOCUS_GAIN));
+ }
+
+ @SmallTest
+ @Feature({"MediaSession"})
+ public void testAudioGainFocus() throws Exception {
+ assertEquals(AudioManager.AUDIOFOCUS_LOSS, mAudioFocusChangeListener.getAudioFocusState());
+ mAudioFocusChangeListener.requestAudioFocus(AudioManager.AUDIOFOCUS_GAIN);
+ assertEquals(AudioManager.AUDIOFOCUS_GAIN, mAudioFocusChangeListener.getAudioFocusState());
+
+ DOMUtils.playMedia(getWebContents(), LONG_AUDIO);
+ assertTrue(DOMUtils.waitForMediaPlay(getWebContents(), LONG_AUDIO));
+
+ assertTrue(mAudioFocusChangeListener.waitForFocusStateChange(AudioManager.AUDIOFOCUS_LOSS));
+ }
+
+ @SmallTest
+ @Feature({"MediaSession"})
+ public void testVideoGainFocus() throws Exception {
+ assertEquals(AudioManager.AUDIOFOCUS_LOSS, mAudioFocusChangeListener.getAudioFocusState());
+ mAudioFocusChangeListener.requestAudioFocus(AudioManager.AUDIOFOCUS_GAIN);
+ assertEquals(AudioManager.AUDIOFOCUS_GAIN, mAudioFocusChangeListener.getAudioFocusState());
+
+ DOMUtils.playMedia(getWebContents(), LONG_VIDEO);
+ assertTrue(DOMUtils.waitForMediaPlay(getWebContents(), LONG_VIDEO));
+
+ assertTrue(mAudioFocusChangeListener.waitForFocusStateChange(AudioManager.AUDIOFOCUS_LOSS));
+ }
+
+ @SmallTest
+ @Feature({"MediaSession"})
+ public void testLongAudioAfterShortGainsFocus() throws Exception {
+ assertEquals(AudioManager.AUDIOFOCUS_LOSS, mAudioFocusChangeListener.getAudioFocusState());
+ mAudioFocusChangeListener.requestAudioFocus(AudioManager.AUDIOFOCUS_GAIN);
+ assertEquals(AudioManager.AUDIOFOCUS_GAIN, mAudioFocusChangeListener.getAudioFocusState());
+
+ DOMUtils.playMedia(getWebContents(), SHORT_AUDIO);
+ assertTrue(DOMUtils.waitForMediaPlay(getWebContents(), SHORT_AUDIO));
+ assertTrue(mAudioFocusChangeListener.waitForFocusStateChange(
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK));
+
+ DOMUtils.playMedia(getWebContents(), LONG_AUDIO);
+ assertTrue(DOMUtils.waitForMediaPlay(getWebContents(), LONG_AUDIO));
+ assertTrue(mAudioFocusChangeListener.waitForFocusStateChange(AudioManager.AUDIOFOCUS_LOSS));
+ }
+
+ @SmallTest
+ @Feature({"MediaSession"})
+ public void testLongVideoAfterShortGainsFocus() throws Exception {
+ assertEquals(AudioManager.AUDIOFOCUS_LOSS, mAudioFocusChangeListener.getAudioFocusState());
+ mAudioFocusChangeListener.requestAudioFocus(AudioManager.AUDIOFOCUS_GAIN);
+ assertEquals(AudioManager.AUDIOFOCUS_GAIN, mAudioFocusChangeListener.getAudioFocusState());
+
+ DOMUtils.playMedia(getWebContents(), SHORT_VIDEO);
+ assertTrue(DOMUtils.waitForMediaPlay(getWebContents(), SHORT_VIDEO));
+ assertTrue(mAudioFocusChangeListener.waitForFocusStateChange(
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK));
+
+ DOMUtils.playMedia(getWebContents(), LONG_VIDEO);
+ assertTrue(DOMUtils.waitForMediaPlay(getWebContents(), LONG_VIDEO));
+ assertTrue(mAudioFocusChangeListener.waitForFocusStateChange(AudioManager.AUDIOFOCUS_LOSS));
+ }
+
+ @SmallTest
+ @Feature({"MediaSession"})
+ public void testShortAudioStopsIfLostFocus() throws Exception {
+ assertEquals(AudioManager.AUDIOFOCUS_LOSS, mAudioFocusChangeListener.getAudioFocusState());
+ mAudioFocusChangeListener.requestAudioFocus(AudioManager.AUDIOFOCUS_GAIN);
+ assertEquals(AudioManager.AUDIOFOCUS_GAIN, mAudioFocusChangeListener.getAudioFocusState());
+
+ DOMUtils.playMedia(getWebContents(), SHORT_AUDIO);
+ assertTrue(DOMUtils.waitForMediaPlay(getWebContents(), SHORT_AUDIO));
+
+ // Wait for the media to be really playing.
+ assertTrue(mAudioFocusChangeListener.waitForFocusStateChange(
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK));
+
+ mAudioFocusChangeListener.requestAudioFocus(AudioManager.AUDIOFOCUS_GAIN);
+ assertEquals(AudioManager.AUDIOFOCUS_GAIN, mAudioFocusChangeListener.getAudioFocusState());
+
+ assertTrue(DOMUtils.waitForMediaPause(getWebContents(), SHORT_AUDIO));
+ }
+
+ @SmallTest
+ @Feature({"MediaSession"})
+ public void testShortVideoStopsIfLostFocus() throws Exception {
+ assertEquals(AudioManager.AUDIOFOCUS_LOSS, mAudioFocusChangeListener.getAudioFocusState());
+ mAudioFocusChangeListener.requestAudioFocus(AudioManager.AUDIOFOCUS_GAIN);
+ assertEquals(AudioManager.AUDIOFOCUS_GAIN, mAudioFocusChangeListener.getAudioFocusState());
+
+ DOMUtils.playMedia(getWebContents(), SHORT_VIDEO);
+ assertTrue(DOMUtils.waitForMediaPlay(getWebContents(), SHORT_VIDEO));
+
+ // Wait for the media to be really playing.
+ assertTrue(mAudioFocusChangeListener.waitForFocusStateChange(
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK));
+
+ mAudioFocusChangeListener.requestAudioFocus(AudioManager.AUDIOFOCUS_GAIN);
+ assertEquals(AudioManager.AUDIOFOCUS_GAIN, mAudioFocusChangeListener.getAudioFocusState());
+
+ assertTrue(DOMUtils.waitForMediaPause(getWebContents(), SHORT_VIDEO));
+ }
+
+ @MediumTest
+ @Feature({"MediaSession"})
+ public void testAudioStopsIfLostFocus() throws Exception {
+ assertEquals(AudioManager.AUDIOFOCUS_LOSS, mAudioFocusChangeListener.getAudioFocusState());
+ mAudioFocusChangeListener.requestAudioFocus(AudioManager.AUDIOFOCUS_GAIN);
+ assertEquals(AudioManager.AUDIOFOCUS_GAIN, mAudioFocusChangeListener.getAudioFocusState());
+
+ DOMUtils.playMedia(getWebContents(), LONG_AUDIO);
+ assertTrue(DOMUtils.waitForMediaPlay(getWebContents(), LONG_AUDIO));
+
+ // Wait for the media to be really playing.
+ assertTrue(mAudioFocusChangeListener.waitForFocusStateChange(AudioManager.AUDIOFOCUS_LOSS));
+
+ mAudioFocusChangeListener.requestAudioFocus(AudioManager.AUDIOFOCUS_GAIN);
+ assertEquals(AudioManager.AUDIOFOCUS_GAIN, mAudioFocusChangeListener.getAudioFocusState());
+
+ assertTrue(DOMUtils.waitForMediaPause(getWebContents(), LONG_AUDIO));
+ }
+
+ @SmallTest
+ @Feature({"MediaSession"})
+ public void testVideoStopsIfLostFocus() throws Exception {
+ assertEquals(AudioManager.AUDIOFOCUS_LOSS, mAudioFocusChangeListener.getAudioFocusState());
+ mAudioFocusChangeListener.requestAudioFocus(AudioManager.AUDIOFOCUS_GAIN);
+ assertEquals(AudioManager.AUDIOFOCUS_GAIN, mAudioFocusChangeListener.getAudioFocusState());
+
+ DOMUtils.playMedia(getWebContents(), LONG_VIDEO);
+ assertTrue(DOMUtils.waitForMediaPlay(getWebContents(), LONG_VIDEO));
+
+ // Wait for the media to be really playing.
+ assertTrue(mAudioFocusChangeListener.waitForFocusStateChange(AudioManager.AUDIOFOCUS_LOSS));
+
+ mAudioFocusChangeListener.requestAudioFocus(AudioManager.AUDIOFOCUS_GAIN);
+ assertEquals(AudioManager.AUDIOFOCUS_GAIN, mAudioFocusChangeListener.getAudioFocusState());
+
+ assertTrue(DOMUtils.waitForMediaPause(getWebContents(), LONG_VIDEO));
+ }
+
+ @SmallTest
+ @Feature({"MediaSession"})
+ public void testMediaDontDuck() throws Exception {
+ assertEquals(AudioManager.AUDIOFOCUS_LOSS, mAudioFocusChangeListener.getAudioFocusState());
+ mAudioFocusChangeListener.requestAudioFocus(AudioManager.AUDIOFOCUS_GAIN);
+ assertEquals(AudioManager.AUDIOFOCUS_GAIN, mAudioFocusChangeListener.getAudioFocusState());
+
+ DOMUtils.playMedia(getWebContents(), LONG_AUDIO);
+ assertTrue(DOMUtils.waitForMediaPlay(getWebContents(), LONG_AUDIO));
+ DOMUtils.playMedia(getWebContents(), LONG_VIDEO);
+ assertTrue(DOMUtils.waitForMediaPlay(getWebContents(), LONG_VIDEO));
+
+ // Wait for the media to be really playing.
+ assertTrue(mAudioFocusChangeListener.waitForFocusStateChange(AudioManager.AUDIOFOCUS_LOSS));
+
+ mAudioFocusChangeListener.requestAudioFocus(
+ AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
+ assertEquals(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK,
+ mAudioFocusChangeListener.getAudioFocusState());
+
+ assertTrue(DOMUtils.waitForMediaPause(getWebContents(), LONG_AUDIO));
+ assertTrue(DOMUtils.waitForMediaPause(getWebContents(), LONG_VIDEO));
+ }
+
+ @MediumTest
+ @Feature({"MediaSession"})
+ public void testMediaResumeAfterTransientMayDuckFocusLoss() throws Exception {
+ assertEquals(AudioManager.AUDIOFOCUS_LOSS, mAudioFocusChangeListener.getAudioFocusState());
+ mAudioFocusChangeListener.requestAudioFocus(AudioManager.AUDIOFOCUS_GAIN);
+ assertEquals(AudioManager.AUDIOFOCUS_GAIN, mAudioFocusChangeListener.getAudioFocusState());
+
+ DOMUtils.playMedia(getWebContents(), LONG_AUDIO);
+ assertTrue(DOMUtils.waitForMediaPlay(getWebContents(), LONG_AUDIO));
+ DOMUtils.playMedia(getWebContents(), LONG_VIDEO);
+ assertTrue(DOMUtils.waitForMediaPlay(getWebContents(), LONG_VIDEO));
+
+ // Wait for the media to be really playing.
+ assertTrue(mAudioFocusChangeListener.waitForFocusStateChange(AudioManager.AUDIOFOCUS_LOSS));
+
+ mAudioFocusChangeListener.requestAudioFocus(
+ AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
+ assertEquals(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK,
+ mAudioFocusChangeListener.getAudioFocusState());
+
+ assertTrue(DOMUtils.waitForMediaPause(getWebContents(), LONG_AUDIO));
+ assertTrue(DOMUtils.waitForMediaPause(getWebContents(), LONG_VIDEO));
+
+ mAudioFocusChangeListener.abandonAudioFocus();
+
+ assertTrue(DOMUtils.waitForMediaPlay(getWebContents(), LONG_AUDIO));
+ assertTrue(DOMUtils.waitForMediaPlay(getWebContents(), LONG_VIDEO));
+ }
+
+ @MediumTest
+ @Feature({"MediaSession"})
+ public void testMediaResumeAfterTransientFocusLoss() throws Exception {
+ assertEquals(AudioManager.AUDIOFOCUS_LOSS, mAudioFocusChangeListener.getAudioFocusState());
+ mAudioFocusChangeListener.requestAudioFocus(AudioManager.AUDIOFOCUS_GAIN);
+ assertEquals(AudioManager.AUDIOFOCUS_GAIN, mAudioFocusChangeListener.getAudioFocusState());
+
+ DOMUtils.playMedia(getWebContents(), LONG_AUDIO);
+ assertTrue(DOMUtils.waitForMediaPlay(getWebContents(), LONG_AUDIO));
+ DOMUtils.playMedia(getWebContents(), LONG_VIDEO);
+ assertTrue(DOMUtils.waitForMediaPlay(getWebContents(), LONG_VIDEO));
+
+ // Wait for the media to be really playing.
+ assertTrue(mAudioFocusChangeListener.waitForFocusStateChange(AudioManager.AUDIOFOCUS_LOSS));
+
+ mAudioFocusChangeListener.requestAudioFocus(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+ assertEquals(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT,
+ mAudioFocusChangeListener.getAudioFocusState());
+
+ assertTrue(DOMUtils.waitForMediaPause(getWebContents(), LONG_AUDIO));
+ assertTrue(DOMUtils.waitForMediaPause(getWebContents(), LONG_VIDEO));
+
+ mAudioFocusChangeListener.abandonAudioFocus();
+
+ assertTrue(DOMUtils.waitForMediaPlay(getWebContents(), LONG_AUDIO));
+ assertTrue(DOMUtils.waitForMediaPlay(getWebContents(), LONG_VIDEO));
+ }
+}
diff --git a/content/public/test/android/javatests/src/org/chromium/content/browser/test/util/DOMUtils.java b/content/public/test/android/javatests/src/org/chromium/content/browser/test/util/DOMUtils.java
index 418cb95..380de11 100644
--- a/content/public/test/android/javatests/src/org/chromium/content/browser/test/util/DOMUtils.java
+++ b/content/public/test/android/javatests/src/org/chromium/content/browser/test/util/DOMUtils.java
@@ -22,39 +22,76 @@ import java.util.concurrent.TimeoutException;
*/
public class DOMUtils {
/**
- * Pauses the video with given {@code nodeId}.
+ * Plays the media with given {@code id}.
*/
- public static void pauseVideo(final WebContents webContents, final String nodeId)
+ public static void playMedia(final WebContents webContents, final String id)
throws InterruptedException, TimeoutException {
StringBuilder sb = new StringBuilder();
sb.append("(function() {");
- sb.append(" var video = document.getElementById('" + nodeId + "');");
- sb.append(" if (video) video.pause();");
+ sb.append(" var media = document.getElementById('" + id + "');");
+ sb.append(" if (media) media.play();");
sb.append("})();");
JavaScriptUtils.executeJavaScriptAndWaitForResult(
webContents, sb.toString());
}
/**
- * Returns whether the video with given {@code nodeId} is paused.
+ * Pauses the media with given {@code id}.
*/
- public static boolean isVideoPaused(final WebContents webContents, final String nodeId)
+ public static void pauseMedia(final WebContents webContents, final String id)
throws InterruptedException, TimeoutException {
- return getNodeField("paused", webContents, nodeId, Boolean.class);
+ StringBuilder sb = new StringBuilder();
+ sb.append("(function() {");
+ sb.append(" var media = document.getElementById('" + id + "');");
+ sb.append(" if (media) media.pause();");
+ sb.append("})();");
+ JavaScriptUtils.executeJavaScriptAndWaitForResult(
+ webContents, sb.toString());
+ }
+
+ /**
+ * Returns whether the media with given {@code id} is paused.
+ */
+ public static boolean isMediaPaused(final WebContents webContents, final String id)
+ throws InterruptedException, TimeoutException {
+ return getNodeField("paused", webContents, id, Boolean.class);
+ }
+
+ /**
+ * Waits until the playback of the media with given {@code id} has started.
+ *
+ * @return Whether the playback has started.
+ */
+ public static boolean waitForMediaPlay(final WebContents webContents, final String id)
+ throws InterruptedException {
+ return CriteriaHelper.pollForCriteria(new Criteria() {
+ @Override
+ public boolean isSatisfied() {
+ try {
+ return !DOMUtils.isMediaPaused(webContents, id);
+ } catch (InterruptedException e) {
+ // Intentionally do nothing
+ return false;
+ } catch (TimeoutException e) {
+ // Intentionally do nothing
+ return false;
+ }
+ }
+ });
}
/**
- * Waits until the playback of the video with given {@code nodeId} has started.
+ * Waits until the playback of the media with given {@code id} has stopped.
*
* @return Whether the playback has started.
*/
- public static boolean waitForVideoPlay(final WebContents webContents, final String nodeId)
+ public static boolean waitForMediaPause(final WebContents webContents, final String id)
throws InterruptedException {
return CriteriaHelper.pollForCriteria(new Criteria() {
@Override
public boolean isSatisfied() {
try {
- return !DOMUtils.isVideoPaused(webContents, nodeId);
+ return DOMUtils.isMediaPaused(webContents, id);
} catch (InterruptedException e) {
// Intentionally do nothing
return false;
diff --git a/content/test/data/android/media/audio-1second.ogg b/content/test/data/android/media/audio-1second.ogg
new file mode 100644
index 0000000..e537715
--- /dev/null
+++ b/content/test/data/android/media/audio-1second.ogg
Binary files differ
diff --git a/content/test/data/android/media/audio-2seconds.ogg b/content/test/data/android/media/audio-2seconds.ogg
new file mode 100644
index 0000000..fa09b9f
--- /dev/null
+++ b/content/test/data/android/media/audio-2seconds.ogg
Binary files differ
diff --git a/content/test/data/android/media/audio-6seconds.ogg b/content/test/data/android/media/audio-6seconds.ogg
new file mode 100644
index 0000000..33c795e
--- /dev/null
+++ b/content/test/data/android/media/audio-6seconds.ogg
Binary files differ
diff --git a/content/test/data/android/media/media-session.html b/content/test/data/android/media/media-session.html
new file mode 100644
index 0000000..9798e60
--- /dev/null
+++ b/content/test/data/android/media/media-session.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test page for MediaSession.java</title>
+ <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
+</head>
+<body>
+ <audio id='very-short-audio' controls>
+ <source src='audio-1second.ogg'>
+ </audio>
+ <audio id='short-audio' controls>
+ <source src='audio-2seconds.ogg'>
+ </audio>
+ <audio id='long-audio' controls>
+ <source src='audio-6seconds.ogg'>
+ </audio>
+ <video id='very-short-video' controls>
+ <source src='video-1second.mp4'>
+ </video>
+ <video id='short-video' controls>
+ <source src='video-2seconds.mp4'>
+ </video>
+ <video id='long-video' controls>
+ <source src='video-6seconds.mp4'>
+ </video>
+</body>
+</html>
diff --git a/content/test/data/android/media/video-1second.mp4 b/content/test/data/android/media/video-1second.mp4
new file mode 100644
index 0000000..d8f8958
--- /dev/null
+++ b/content/test/data/android/media/video-1second.mp4
Binary files differ
diff --git a/content/test/data/android/media/video-2seconds.mp4 b/content/test/data/android/media/video-2seconds.mp4
new file mode 100644
index 0000000..ec0c507
--- /dev/null
+++ b/content/test/data/android/media/video-2seconds.mp4
Binary files differ
diff --git a/content/test/data/android/media/video-6seconds.mp4 b/content/test/data/android/media/video-6seconds.mp4
new file mode 100644
index 0000000..d278c8a
--- /dev/null
+++ b/content/test/data/android/media/video-6seconds.mp4
Binary files differ
diff --git a/media/base/android/java/src/org/chromium/media/MediaPlayerListener.java b/media/base/android/java/src/org/chromium/media/MediaPlayerListener.java
index 47bd01a..78723b7 100644
--- a/media/base/android/java/src/org/chromium/media/MediaPlayerListener.java
+++ b/media/base/android/java/src/org/chromium/media/MediaPlayerListener.java
@@ -5,7 +5,6 @@
package org.chromium.media;
import android.content.Context;
-import android.media.AudioManager;
import android.media.MediaPlayer;
import org.chromium.base.CalledByNative;
@@ -19,8 +18,7 @@ class MediaPlayerListener implements MediaPlayer.OnPreparedListener,
MediaPlayer.OnBufferingUpdateListener,
MediaPlayer.OnSeekCompleteListener,
MediaPlayer.OnVideoSizeChangedListener,
- MediaPlayer.OnErrorListener,
- AudioManager.OnAudioFocusChangeListener {
+ MediaPlayer.OnErrorListener {
// These values are mirrored as enums in media/base/android/media_player_bridge.h.
// Please ensure they stay in sync.
private static final int MEDIA_ERROR_FORMAT = 0;
@@ -101,25 +99,6 @@ class MediaPlayerListener implements MediaPlayer.OnPreparedListener,
nativeOnMediaPrepared(mNativeMediaPlayerListener);
}
- @Override
- public void onAudioFocusChange(int focusChange) {
- if (focusChange == AudioManager.AUDIOFOCUS_LOSS
- || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
- nativeOnMediaInterrupted(mNativeMediaPlayerListener);
- }
- }
-
- @CalledByNative
- public void releaseResources() {
- if (mContext != null) {
- // Unregister the wish for audio focus.
- AudioManager am = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
- if (am != null) {
- am.abandonAudioFocus(this);
- }
- }
- }
-
@CalledByNative
private static MediaPlayerListener create(long nativeMediaPlayerListener,
Context context, MediaPlayerBridge mediaPlayerBridge) {
@@ -133,14 +112,6 @@ class MediaPlayerListener implements MediaPlayer.OnPreparedListener,
mediaPlayerBridge.setOnSeekCompleteListener(listener);
mediaPlayerBridge.setOnVideoSizeChangedListener(listener);
}
-
- AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
- am.requestAudioFocus(
- listener,
- AudioManager.STREAM_MUSIC,
-
- // Request permanent focus.
- AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
return listener;
}
diff --git a/media/base/android/media_player_bridge.cc b/media/base/android/media_player_bridge.cc
index ed8854f..29049cb 100644
--- a/media/base/android/media_player_bridge.cc
+++ b/media/base/android/media_player_bridge.cc
@@ -456,6 +456,11 @@ void MediaPlayerBridge::UpdateAllowedOperations() {
}
void MediaPlayerBridge::StartInternal() {
+ if (!manager()->RequestPlay(player_id())) {
+ Pause(true);
+ return;
+ }
+
JNIEnv* env = base::android::AttachCurrentThread();
Java_MediaPlayerBridge_start(env, j_media_player_bridge_.obj());
if (!time_update_timer_.IsRunning()) {
diff --git a/media/base/android/media_player_listener.cc b/media/base/android/media_player_listener.cc
index 861a34f..2561deb 100644
--- a/media/base/android/media_player_listener.cc
+++ b/media/base/android/media_player_listener.cc
@@ -41,12 +41,6 @@ void MediaPlayerListener::CreateMediaPlayerListener(
void MediaPlayerListener::ReleaseMediaPlayerListenerResources() {
- JNIEnv* env = AttachCurrentThread();
- CHECK(env);
- if (!j_media_player_listener_.is_null()) {
- Java_MediaPlayerListener_releaseResources(
- env, j_media_player_listener_.obj());
- }
j_media_player_listener_.Reset();
}
diff --git a/media/base/android/media_player_manager.h b/media/base/android/media_player_manager.h
index feac84e..d2d35f4 100644
--- a/media/base/android/media_player_manager.h
+++ b/media/base/android/media_player_manager.h
@@ -78,6 +78,12 @@ class MEDIA_EXPORT MediaPlayerManager {
// Called by the player to get a hardware protected surface.
virtual void RequestFullScreen(int player_id) = 0;
+
+ // Called by the player to request to play. The manager should use this
+ // opportunity to check if the current context is appropriate for a media to
+ // play.
+ // Returns whether the request was granted.
+ virtual bool RequestPlay(int player_id) = 0;
};
} // namespace media
diff --git a/media/base/android/media_source_player.cc b/media/base/android/media_source_player.cc
index 7e8f49a..460fbec 100644
--- a/media/base/android/media_source_player.cc
+++ b/media/base/android/media_source_player.cc
@@ -217,6 +217,11 @@ void MediaSourcePlayer::StartInternal() {
if (pending_event_ != NO_EVENT_PENDING)
return;
+ if (!manager()->RequestPlay(player_id())) {
+ Pause(true);
+ return;
+ }
+
// When we start, we could have new demuxed data coming in. This new data
// could be clear (not encrypted) or encrypted with different keys. So key
// related info should all be cleared.
diff --git a/media/base/android/media_source_player_unittest.cc b/media/base/android/media_source_player_unittest.cc
index f37cf20..ca13a06 100644
--- a/media/base/android/media_source_player_unittest.cc
+++ b/media/base/android/media_source_player_unittest.cc
@@ -48,7 +48,8 @@ class MockMediaPlayerManager : public MediaPlayerManager {
num_metadata_changes_(0),
timestamp_updated_(false),
is_audible_(false),
- is_delay_expired_(false) {}
+ is_delay_expired_(false),
+ allow_play_(true) {}
~MockMediaPlayerManager() override {}
// MediaPlayerManager implementation.
@@ -82,6 +83,10 @@ class MockMediaPlayerManager : public MediaPlayerManager {
MediaPlayerAndroid* GetPlayer(int player_id) override { return NULL; }
void RequestFullScreen(int player_id) override {}
+ bool RequestPlay(int player_id) override {
+ return allow_play_;
+ }
+
void OnAudibleStateChanged(int player_id, bool is_audible_now) override {
is_audible_ = is_audible_now;
}
@@ -122,6 +127,10 @@ class MockMediaPlayerManager : public MediaPlayerManager {
is_delay_expired_ = value;
}
+ void set_allow_play(bool value) {
+ allow_play_ = value;
+ }
+
private:
base::MessageLoop* message_loop_;
bool playback_completed_;
@@ -135,6 +144,8 @@ class MockMediaPlayerManager : public MediaPlayerManager {
bool is_audible_;
// Helper flag to ensure delay for WaitForDelay().
bool is_delay_expired_;
+ // Whether the manager will allow players that request playing.
+ bool allow_play_;
DISALLOW_COPY_AND_ASSIGN(MockMediaPlayerManager);
};
@@ -2513,4 +2524,48 @@ TEST_F(MediaSourcePlayerTest, VideoMetadataChangeAfterConfigChange) {
WaitForVideoDecodeDone();
}
+TEST_F(MediaSourcePlayerTest, RequestPlayDeniedDontPlay_Audio) {
+ SKIP_TEST_IF_MEDIA_CODEC_BRIDGE_IS_NOT_AVAILABLE();
+
+ EXPECT_EQ(demuxer_->num_data_requests(), 0);
+ player_.OnDemuxerConfigsAvailable(CreateDemuxerConfigs(true, false));
+
+ manager_.set_allow_play(false);
+ player_.Start();
+ EXPECT_FALSE(player_.IsPlaying());
+}
+
+TEST_F(MediaSourcePlayerTest, RequestPlayDeniedDontPlay_Video) {
+ SKIP_TEST_IF_MEDIA_CODEC_BRIDGE_IS_NOT_AVAILABLE();
+
+ EXPECT_EQ(demuxer_->num_data_requests(), 0);
+ player_.OnDemuxerConfigsAvailable(CreateDemuxerConfigs(false, true));
+
+ manager_.set_allow_play(false);
+ player_.Start();
+ EXPECT_FALSE(player_.IsPlaying());
+}
+
+TEST_F(MediaSourcePlayerTest, RequestPlayDeniedDontPlay_AV) {
+ SKIP_TEST_IF_MEDIA_CODEC_BRIDGE_IS_NOT_AVAILABLE();
+
+ EXPECT_EQ(demuxer_->num_data_requests(), 0);
+ player_.OnDemuxerConfigsAvailable(CreateDemuxerConfigs(true, true));
+
+ manager_.set_allow_play(false);
+ player_.Start();
+ EXPECT_FALSE(player_.IsPlaying());
+}
+
+TEST_F(MediaSourcePlayerTest, RequestPlayGrantedPlays) {
+ SKIP_TEST_IF_MEDIA_CODEC_BRIDGE_IS_NOT_AVAILABLE();
+
+ EXPECT_EQ(demuxer_->num_data_requests(), 0);
+ player_.OnDemuxerConfigsAvailable(CreateDemuxerConfigs(true, true));
+
+ manager_.set_allow_play(true);
+ player_.Start();
+ EXPECT_TRUE(player_.IsPlaying());
+}
+
} // namespace media