summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoraberent <aberent@chromium.org>2015-03-27 06:37:00 -0700
committerCommit bot <commit-bot@chromium.org>2015-03-27 13:37:47 +0000
commit76097722a9e8577767a1d21a4a6b98ca322bfa7b (patch)
treef80d8ac4bd601609e1521f63aed51a7aba07043a
parent96a3b4c52765cdbe28329c972efb07bcc75c372a (diff)
downloadchromium_src-76097722a9e8577767a1d21a4a6b98ca322bfa7b.zip
chromium_src-76097722a9e8577767a1d21a4a6b98ca322bfa7b.tar.gz
chromium_src-76097722a9e8577767a1d21a4a6b98ca322bfa7b.tar.bz2
Upstream Chrome for Android Cast.
This upstreams all of Chrome for Android's cast code with the exception of: 1. The YouTube support code. 2. The id of the Chrome cast receiver. The YouTube support code and the id of Chrome cast receiver have to remain proprietory due to licencing restrictions. The tests will be upstreamed seperately, but need further work to make them independent of other downstream code. BUG=315088 Review URL: https://codereview.chromium.org/928643003 Cr-Commit-Position: refs/heads/master@{#322569}
-rw-r--r--chrome/android/BUILD.gn4
-rw-r--r--chrome/android/DEPS1
-rw-r--r--chrome/android/java/res/drawable-hdpi-v21/ic_vidcontrol_pause.pngbin0 -> 94 bytes
-rw-r--r--chrome/android/java/res/drawable-hdpi-v21/ic_vidcontrol_play.pngbin0 -> 192 bytes
-rw-r--r--chrome/android/java/res/drawable-hdpi-v21/ic_vidcontrol_stop.pngbin0 -> 190 bytes
-rw-r--r--chrome/android/java/res/drawable-hdpi/ic_cast_dark_off.pngbin0 -> 376 bytes
-rw-r--r--chrome/android/java/res/drawable-hdpi/ic_cast_dark_on.pngbin0 -> 541 bytes
-rw-r--r--chrome/android/java/res/drawable-hdpi/ic_notification_media_route.pngbin0 -> 259 bytes
-rw-r--r--chrome/android/java/res/drawable-hdpi/ic_vidcontrol_pause.pngbin0 -> 93 bytes
-rw-r--r--chrome/android/java/res/drawable-hdpi/ic_vidcontrol_play.pngbin0 -> 186 bytes
-rw-r--r--chrome/android/java/res/drawable-hdpi/ic_vidcontrol_stop.pngbin0 -> 180 bytes
-rw-r--r--chrome/android/java/res/drawable-mdpi-v21/ic_vidcontrol_pause.pngbin0 -> 93 bytes
-rw-r--r--chrome/android/java/res/drawable-mdpi-v21/ic_vidcontrol_play.pngbin0 -> 148 bytes
-rw-r--r--chrome/android/java/res/drawable-mdpi-v21/ic_vidcontrol_stop.pngbin0 -> 145 bytes
-rw-r--r--chrome/android/java/res/drawable-mdpi/ic_cast_dark_off.pngbin0 -> 253 bytes
-rw-r--r--chrome/android/java/res/drawable-mdpi/ic_cast_dark_on.pngbin0 -> 387 bytes
-rw-r--r--chrome/android/java/res/drawable-mdpi/ic_notification_media_route.pngbin0 -> 190 bytes
-rw-r--r--chrome/android/java/res/drawable-mdpi/ic_vidcontrol_pause.pngbin0 -> 90 bytes
-rw-r--r--chrome/android/java/res/drawable-mdpi/ic_vidcontrol_play.pngbin0 -> 146 bytes
-rw-r--r--chrome/android/java/res/drawable-mdpi/ic_vidcontrol_stop.pngbin0 -> 140 bytes
-rw-r--r--chrome/android/java/res/drawable-v19/notification_icon_bg.xml15
-rw-r--r--chrome/android/java/res/drawable-v21/notification_icon_bg.xml15
-rw-r--r--chrome/android/java/res/drawable-xhdpi-v21/ic_vidcontrol_pause.pngbin0 -> 103 bytes
-rw-r--r--chrome/android/java/res/drawable-xhdpi-v21/ic_vidcontrol_stop.pngbin0 -> 211 bytes
-rw-r--r--chrome/android/java/res/drawable-xhdpi/ic_cast_dark_off.pngbin0 -> 465 bytes
-rw-r--r--chrome/android/java/res/drawable-xhdpi/ic_cast_dark_on.pngbin0 -> 657 bytes
-rw-r--r--chrome/android/java/res/drawable-xhdpi/ic_notification_media_route.pngbin0 -> 342 bytes
-rw-r--r--chrome/android/java/res/drawable-xhdpi/ic_vidcontrol_pause.pngbin0 -> 90 bytes
-rw-r--r--chrome/android/java/res/drawable-xhdpi/ic_vidcontrol_play.pngbin0 -> 208 bytes
-rw-r--r--chrome/android/java/res/drawable-xhdpi/ic_vidcontrol_stop.pngbin0 -> 201 bytes
-rw-r--r--chrome/android/java/res/drawable-xxhdpi-v21/ic_vidcontrol_pause.pngbin0 -> 109 bytes
-rw-r--r--chrome/android/java/res/drawable-xxhdpi-v21/ic_vidcontrol_play.pngbin0 -> 303 bytes
-rw-r--r--chrome/android/java/res/drawable-xxhdpi-v21/ic_vidcontrol_stop.pngbin0 -> 270 bytes
-rw-r--r--chrome/android/java/res/drawable-xxhdpi/ic_cast_dark_off.pngbin0 -> 721 bytes
-rw-r--r--chrome/android/java/res/drawable-xxhdpi/ic_cast_dark_on.pngbin0 -> 924 bytes
-rw-r--r--chrome/android/java/res/drawable-xxhdpi/ic_notification_media_route.pngbin0 -> 472 bytes
-rw-r--r--chrome/android/java/res/drawable-xxhdpi/ic_vidcontrol_pause.pngbin0 -> 92 bytes
-rw-r--r--chrome/android/java/res/drawable-xxhdpi/ic_vidcontrol_play.pngbin0 -> 278 bytes
-rw-r--r--chrome/android/java/res/drawable-xxhdpi/ic_vidcontrol_stop.pngbin0 -> 260 bytes
-rw-r--r--chrome/android/java/res/drawable-xxxhdpi-v21/ic_vidcontrol_pause.pngbin0 -> 114 bytes
-rw-r--r--chrome/android/java/res/drawable-xxxhdpi-v21/ic_vidcontrol_play.pngbin0 -> 370 bytes
-rw-r--r--chrome/android/java/res/drawable-xxxhdpi-v21/ic_vidcontrol_stop.pngbin0 -> 331 bytes
-rw-r--r--chrome/android/java/res/drawable-xxxhdpi/ic_notification_media_route.pngbin0 -> 599 bytes
-rw-r--r--chrome/android/java/res/drawable-xxxhdpi/ic_vidcontrol_pause.pngbin0 -> 97 bytes
-rw-r--r--chrome/android/java/res/drawable-xxxhdpi/ic_vidcontrol_play.pngbin0 -> 345 bytes
-rw-r--r--chrome/android/java/res/drawable-xxxhdpi/ic_vidcontrol_stop.pngbin0 -> 318 bytes
-rw-r--r--chrome/android/java/res/drawable/ic_cast_dark_chrome.xml15
-rw-r--r--chrome/android/java/res/layout/cast_controller_media_route_button.xml13
-rw-r--r--chrome/android/java/res/layout/expanded_cast_controller.xml41
-rw-r--r--chrome/android/java/res/layout/remote_notification_bar.xml101
-rw-r--r--chrome/android/java/res/values-v17/styles.xml20
-rw-r--r--chrome/android/java/res/values-v21/styles.xml12
-rw-r--r--chrome/android/java/res/values/dimens.xml4
-rw-r--r--chrome/android/java/res/values/values.xml3
-rw-r--r--chrome/android/java/src/org/chromium/chrome/browser/media/remote/AbstractMediaRouteController.java582
-rw-r--r--chrome/android/java/src/org/chromium/chrome/browser/media/remote/ChromeMediaRouteDialogFactory.java96
-rw-r--r--chrome/android/java/src/org/chromium/chrome/browser/media/remote/DefaultMediaRouteController.java1101
-rw-r--r--chrome/android/java/src/org/chromium/chrome/browser/media/remote/ExpandedControllerActivity.java353
-rw-r--r--chrome/android/java/src/org/chromium/chrome/browser/media/remote/FullscreenMediaRouteButton.java77
-rw-r--r--chrome/android/java/src/org/chromium/chrome/browser/media/remote/LockScreenTransportControl.java182
-rw-r--r--chrome/android/java/src/org/chromium/chrome/browser/media/remote/LockScreenTransportControlV14.java234
-rw-r--r--chrome/android/java/src/org/chromium/chrome/browser/media/remote/LockScreenTransportControlV16.java37
-rw-r--r--chrome/android/java/src/org/chromium/chrome/browser/media/remote/LockScreenTransportControlV18.java81
-rw-r--r--chrome/android/java/src/org/chromium/chrome/browser/media/remote/MediaRouteController.java308
-rw-r--r--chrome/android/java/src/org/chromium/chrome/browser/media/remote/MediaUrlResolver.java168
-rw-r--r--chrome/android/java/src/org/chromium/chrome/browser/media/remote/NotificationTransportControl.java629
-rw-r--r--chrome/android/java/src/org/chromium/chrome/browser/media/remote/RecordCastAction.java90
-rw-r--r--chrome/android/java/src/org/chromium/chrome/browser/media/remote/RemoteMediaPlayerBridge.java344
-rw-r--r--chrome/android/java/src/org/chromium/chrome/browser/media/remote/RemoteMediaPlayerController.java450
-rw-r--r--chrome/android/java/src/org/chromium/chrome/browser/media/remote/RemotePlaybackSettings.java109
-rw-r--r--chrome/android/java/src/org/chromium/chrome/browser/media/remote/RemoteVideoInfo.java115
-rw-r--r--chrome/android/java/src/org/chromium/chrome/browser/media/remote/TransportControl.java185
-rw-r--r--chrome/android/java/strings/android_chrome_strings.grd48
-rw-r--r--chrome/android/shell/java/AndroidManifest.xml.jinja225
-rw-r--r--chrome/app/android/DEPS6
-rw-r--r--chrome/app/android/chrome_main_delegate_android.cc19
-rw-r--r--chrome/browser/android/chrome_jni_registrar.cc5
-rw-r--r--chrome/browser/media/android/remote/DEPS16
-rw-r--r--chrome/browser/media/android/remote/OWNERS3
-rw-r--r--chrome/browser/media/android/remote/record_cast_action.cc108
-rw-r--r--chrome/browser/media/android/remote/record_cast_action.h17
-rw-r--r--chrome/browser/media/android/remote/remote_media_player_bridge.cc512
-rw-r--r--chrome/browser/media/android/remote/remote_media_player_bridge.h154
-rw-r--r--chrome/browser/media/android/remote/remote_media_player_manager.cc310
-rw-r--r--chrome/browser/media/android/remote/remote_media_player_manager.h115
-rw-r--r--chrome/chrome.gyp3
-rw-r--r--chrome/chrome_browser.gypi8
-rw-r--r--content/browser/media/android/browser_media_player_manager.cc7
88 files changed, 6737 insertions, 4 deletions
diff --git a/chrome/android/BUILD.gn b/chrome/android/BUILD.gn
index 9aed3b7..981ff3f 100644
--- a/chrome/android/BUILD.gn
+++ b/chrome/android/BUILD.gn
@@ -18,6 +18,7 @@ android_resources("chrome_java_resources") {
"//content/public/android:content_java_resources",
"//chrome/app:java_strings_grd",
"//third_party/android_data_chart:android_data_chart_java_resources",
+ "//third_party/android_media:android_media_resources",
"//third_party/android_tools:android_support_v7_appcompat_resources",
]
custom_package = "org.chromium.chrome"
@@ -92,12 +93,15 @@ android_library("chrome_java") {
"//printing:printing_java",
"//sync/android:sync_java",
"//third_party/android_data_chart:android_data_chart_java",
+ "//third_party/android_media:android_media_java",
"//third_party/android_protobuf:protobuf_nano_javalib",
"//third_party/android_tools:android_support_v13_java",
"//third_party/android_tools:android_support_v7_appcompat_java",
+ "//third_party/android_tools:android_support_v7_mediarouter_java",
"//third_party/cacheinvalidation:cacheinvalidation_javalib",
"//third_party/cacheinvalidation:cacheinvalidation_proto_java",
"//third_party/jsr-305:jsr_305_javalib",
+ "//media/base/android:media_java",
"//ui/android:ui_java",
"//ui/android:ui_java_resources",
google_play_services_library,
diff --git a/chrome/android/DEPS b/chrome/android/DEPS
index 32b2fa6..f83eebe 100644
--- a/chrome/android/DEPS
+++ b/chrome/android/DEPS
@@ -1,4 +1,5 @@
include_rules = [
"+components/invalidation",
"+jni",
+ "+media/base/android/java",
]
diff --git a/chrome/android/java/res/drawable-hdpi-v21/ic_vidcontrol_pause.png b/chrome/android/java/res/drawable-hdpi-v21/ic_vidcontrol_pause.png
new file mode 100644
index 0000000..6f3d24d
--- /dev/null
+++ b/chrome/android/java/res/drawable-hdpi-v21/ic_vidcontrol_pause.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-hdpi-v21/ic_vidcontrol_play.png b/chrome/android/java/res/drawable-hdpi-v21/ic_vidcontrol_play.png
new file mode 100644
index 0000000..3cc7488
--- /dev/null
+++ b/chrome/android/java/res/drawable-hdpi-v21/ic_vidcontrol_play.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-hdpi-v21/ic_vidcontrol_stop.png b/chrome/android/java/res/drawable-hdpi-v21/ic_vidcontrol_stop.png
new file mode 100644
index 0000000..cc8c01e
--- /dev/null
+++ b/chrome/android/java/res/drawable-hdpi-v21/ic_vidcontrol_stop.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-hdpi/ic_cast_dark_off.png b/chrome/android/java/res/drawable-hdpi/ic_cast_dark_off.png
new file mode 100644
index 0000000..e1d0f88
--- /dev/null
+++ b/chrome/android/java/res/drawable-hdpi/ic_cast_dark_off.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-hdpi/ic_cast_dark_on.png b/chrome/android/java/res/drawable-hdpi/ic_cast_dark_on.png
new file mode 100644
index 0000000..549aed3
--- /dev/null
+++ b/chrome/android/java/res/drawable-hdpi/ic_cast_dark_on.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-hdpi/ic_notification_media_route.png b/chrome/android/java/res/drawable-hdpi/ic_notification_media_route.png
new file mode 100644
index 0000000..6f69b68
--- /dev/null
+++ b/chrome/android/java/res/drawable-hdpi/ic_notification_media_route.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-hdpi/ic_vidcontrol_pause.png b/chrome/android/java/res/drawable-hdpi/ic_vidcontrol_pause.png
new file mode 100644
index 0000000..7be7c85
--- /dev/null
+++ b/chrome/android/java/res/drawable-hdpi/ic_vidcontrol_pause.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-hdpi/ic_vidcontrol_play.png b/chrome/android/java/res/drawable-hdpi/ic_vidcontrol_play.png
new file mode 100644
index 0000000..7e7513b
--- /dev/null
+++ b/chrome/android/java/res/drawable-hdpi/ic_vidcontrol_play.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-hdpi/ic_vidcontrol_stop.png b/chrome/android/java/res/drawable-hdpi/ic_vidcontrol_stop.png
new file mode 100644
index 0000000..868a1ae
--- /dev/null
+++ b/chrome/android/java/res/drawable-hdpi/ic_vidcontrol_stop.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-mdpi-v21/ic_vidcontrol_pause.png b/chrome/android/java/res/drawable-mdpi-v21/ic_vidcontrol_pause.png
new file mode 100644
index 0000000..bcc194a
--- /dev/null
+++ b/chrome/android/java/res/drawable-mdpi-v21/ic_vidcontrol_pause.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-mdpi-v21/ic_vidcontrol_play.png b/chrome/android/java/res/drawable-mdpi-v21/ic_vidcontrol_play.png
new file mode 100644
index 0000000..e617efa
--- /dev/null
+++ b/chrome/android/java/res/drawable-mdpi-v21/ic_vidcontrol_play.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-mdpi-v21/ic_vidcontrol_stop.png b/chrome/android/java/res/drawable-mdpi-v21/ic_vidcontrol_stop.png
new file mode 100644
index 0000000..1026bec
--- /dev/null
+++ b/chrome/android/java/res/drawable-mdpi-v21/ic_vidcontrol_stop.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-mdpi/ic_cast_dark_off.png b/chrome/android/java/res/drawable-mdpi/ic_cast_dark_off.png
new file mode 100644
index 0000000..4f17ebc
--- /dev/null
+++ b/chrome/android/java/res/drawable-mdpi/ic_cast_dark_off.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-mdpi/ic_cast_dark_on.png b/chrome/android/java/res/drawable-mdpi/ic_cast_dark_on.png
new file mode 100644
index 0000000..304b484
--- /dev/null
+++ b/chrome/android/java/res/drawable-mdpi/ic_cast_dark_on.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-mdpi/ic_notification_media_route.png b/chrome/android/java/res/drawable-mdpi/ic_notification_media_route.png
new file mode 100644
index 0000000..a2ab731
--- /dev/null
+++ b/chrome/android/java/res/drawable-mdpi/ic_notification_media_route.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-mdpi/ic_vidcontrol_pause.png b/chrome/android/java/res/drawable-mdpi/ic_vidcontrol_pause.png
new file mode 100644
index 0000000..9d73b64
--- /dev/null
+++ b/chrome/android/java/res/drawable-mdpi/ic_vidcontrol_pause.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-mdpi/ic_vidcontrol_play.png b/chrome/android/java/res/drawable-mdpi/ic_vidcontrol_play.png
new file mode 100644
index 0000000..4ad5315
--- /dev/null
+++ b/chrome/android/java/res/drawable-mdpi/ic_vidcontrol_play.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-mdpi/ic_vidcontrol_stop.png b/chrome/android/java/res/drawable-mdpi/ic_vidcontrol_stop.png
new file mode 100644
index 0000000..f1343ea
--- /dev/null
+++ b/chrome/android/java/res/drawable-mdpi/ic_vidcontrol_stop.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-v19/notification_icon_bg.xml b/chrome/android/java/res/drawable-v19/notification_icon_bg.xml
new file mode 100644
index 0000000..11ef229
--- /dev/null
+++ b/chrome/android/java/res/drawable-v19/notification_icon_bg.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2014 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.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval" >
+ <solid android:color="@color/light_active_color" />
+ <size
+ android:width="40dp"
+ android:height="40dp"/>
+</shape> \ No newline at end of file
diff --git a/chrome/android/java/res/drawable-v21/notification_icon_bg.xml b/chrome/android/java/res/drawable-v21/notification_icon_bg.xml
new file mode 100644
index 0000000..11ef229
--- /dev/null
+++ b/chrome/android/java/res/drawable-v21/notification_icon_bg.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2014 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.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval" >
+ <solid android:color="@color/light_active_color" />
+ <size
+ android:width="40dp"
+ android:height="40dp"/>
+</shape> \ No newline at end of file
diff --git a/chrome/android/java/res/drawable-xhdpi-v21/ic_vidcontrol_pause.png b/chrome/android/java/res/drawable-xhdpi-v21/ic_vidcontrol_pause.png
new file mode 100644
index 0000000..82c2552
--- /dev/null
+++ b/chrome/android/java/res/drawable-xhdpi-v21/ic_vidcontrol_pause.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-xhdpi-v21/ic_vidcontrol_stop.png b/chrome/android/java/res/drawable-xhdpi-v21/ic_vidcontrol_stop.png
new file mode 100644
index 0000000..725633c
--- /dev/null
+++ b/chrome/android/java/res/drawable-xhdpi-v21/ic_vidcontrol_stop.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-xhdpi/ic_cast_dark_off.png b/chrome/android/java/res/drawable-xhdpi/ic_cast_dark_off.png
new file mode 100644
index 0000000..343419a
--- /dev/null
+++ b/chrome/android/java/res/drawable-xhdpi/ic_cast_dark_off.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-xhdpi/ic_cast_dark_on.png b/chrome/android/java/res/drawable-xhdpi/ic_cast_dark_on.png
new file mode 100644
index 0000000..82648a1
--- /dev/null
+++ b/chrome/android/java/res/drawable-xhdpi/ic_cast_dark_on.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-xhdpi/ic_notification_media_route.png b/chrome/android/java/res/drawable-xhdpi/ic_notification_media_route.png
new file mode 100644
index 0000000..158151f
--- /dev/null
+++ b/chrome/android/java/res/drawable-xhdpi/ic_notification_media_route.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-xhdpi/ic_vidcontrol_pause.png b/chrome/android/java/res/drawable-xhdpi/ic_vidcontrol_pause.png
new file mode 100644
index 0000000..eb9a5a4
--- /dev/null
+++ b/chrome/android/java/res/drawable-xhdpi/ic_vidcontrol_pause.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-xhdpi/ic_vidcontrol_play.png b/chrome/android/java/res/drawable-xhdpi/ic_vidcontrol_play.png
new file mode 100644
index 0000000..e9aacb2
--- /dev/null
+++ b/chrome/android/java/res/drawable-xhdpi/ic_vidcontrol_play.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-xhdpi/ic_vidcontrol_stop.png b/chrome/android/java/res/drawable-xhdpi/ic_vidcontrol_stop.png
new file mode 100644
index 0000000..be08ad5
--- /dev/null
+++ b/chrome/android/java/res/drawable-xhdpi/ic_vidcontrol_stop.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-xxhdpi-v21/ic_vidcontrol_pause.png b/chrome/android/java/res/drawable-xxhdpi-v21/ic_vidcontrol_pause.png
new file mode 100644
index 0000000..46a97c3
--- /dev/null
+++ b/chrome/android/java/res/drawable-xxhdpi-v21/ic_vidcontrol_pause.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-xxhdpi-v21/ic_vidcontrol_play.png b/chrome/android/java/res/drawable-xxhdpi-v21/ic_vidcontrol_play.png
new file mode 100644
index 0000000..842438f
--- /dev/null
+++ b/chrome/android/java/res/drawable-xxhdpi-v21/ic_vidcontrol_play.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-xxhdpi-v21/ic_vidcontrol_stop.png b/chrome/android/java/res/drawable-xxhdpi-v21/ic_vidcontrol_stop.png
new file mode 100644
index 0000000..ec50bbe
--- /dev/null
+++ b/chrome/android/java/res/drawable-xxhdpi-v21/ic_vidcontrol_stop.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-xxhdpi/ic_cast_dark_off.png b/chrome/android/java/res/drawable-xxhdpi/ic_cast_dark_off.png
new file mode 100644
index 0000000..e11f480
--- /dev/null
+++ b/chrome/android/java/res/drawable-xxhdpi/ic_cast_dark_off.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-xxhdpi/ic_cast_dark_on.png b/chrome/android/java/res/drawable-xxhdpi/ic_cast_dark_on.png
new file mode 100644
index 0000000..ace0ca4
--- /dev/null
+++ b/chrome/android/java/res/drawable-xxhdpi/ic_cast_dark_on.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-xxhdpi/ic_notification_media_route.png b/chrome/android/java/res/drawable-xxhdpi/ic_notification_media_route.png
new file mode 100644
index 0000000..bfff22f
--- /dev/null
+++ b/chrome/android/java/res/drawable-xxhdpi/ic_notification_media_route.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-xxhdpi/ic_vidcontrol_pause.png b/chrome/android/java/res/drawable-xxhdpi/ic_vidcontrol_pause.png
new file mode 100644
index 0000000..2ac0603
--- /dev/null
+++ b/chrome/android/java/res/drawable-xxhdpi/ic_vidcontrol_pause.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-xxhdpi/ic_vidcontrol_play.png b/chrome/android/java/res/drawable-xxhdpi/ic_vidcontrol_play.png
new file mode 100644
index 0000000..b3129f8
--- /dev/null
+++ b/chrome/android/java/res/drawable-xxhdpi/ic_vidcontrol_play.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-xxhdpi/ic_vidcontrol_stop.png b/chrome/android/java/res/drawable-xxhdpi/ic_vidcontrol_stop.png
new file mode 100644
index 0000000..1ebc210
--- /dev/null
+++ b/chrome/android/java/res/drawable-xxhdpi/ic_vidcontrol_stop.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-xxxhdpi-v21/ic_vidcontrol_pause.png b/chrome/android/java/res/drawable-xxxhdpi-v21/ic_vidcontrol_pause.png
new file mode 100644
index 0000000..071f8c2
--- /dev/null
+++ b/chrome/android/java/res/drawable-xxxhdpi-v21/ic_vidcontrol_pause.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-xxxhdpi-v21/ic_vidcontrol_play.png b/chrome/android/java/res/drawable-xxxhdpi-v21/ic_vidcontrol_play.png
new file mode 100644
index 0000000..b5e8921
--- /dev/null
+++ b/chrome/android/java/res/drawable-xxxhdpi-v21/ic_vidcontrol_play.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-xxxhdpi-v21/ic_vidcontrol_stop.png b/chrome/android/java/res/drawable-xxxhdpi-v21/ic_vidcontrol_stop.png
new file mode 100644
index 0000000..ebff9ca
--- /dev/null
+++ b/chrome/android/java/res/drawable-xxxhdpi-v21/ic_vidcontrol_stop.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-xxxhdpi/ic_notification_media_route.png b/chrome/android/java/res/drawable-xxxhdpi/ic_notification_media_route.png
new file mode 100644
index 0000000..f83ba97
--- /dev/null
+++ b/chrome/android/java/res/drawable-xxxhdpi/ic_notification_media_route.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-xxxhdpi/ic_vidcontrol_pause.png b/chrome/android/java/res/drawable-xxxhdpi/ic_vidcontrol_pause.png
new file mode 100644
index 0000000..0bb162d
--- /dev/null
+++ b/chrome/android/java/res/drawable-xxxhdpi/ic_vidcontrol_pause.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-xxxhdpi/ic_vidcontrol_play.png b/chrome/android/java/res/drawable-xxxhdpi/ic_vidcontrol_play.png
new file mode 100644
index 0000000..71185b0
--- /dev/null
+++ b/chrome/android/java/res/drawable-xxxhdpi/ic_vidcontrol_play.png
Binary files differ
diff --git a/chrome/android/java/res/drawable-xxxhdpi/ic_vidcontrol_stop.png b/chrome/android/java/res/drawable-xxxhdpi/ic_vidcontrol_stop.png
new file mode 100644
index 0000000..01e4bc7
--- /dev/null
+++ b/chrome/android/java/res/drawable-xxxhdpi/ic_vidcontrol_stop.png
Binary files differ
diff --git a/chrome/android/java/res/drawable/ic_cast_dark_chrome.xml b/chrome/android/java/res/drawable/ic_cast_dark_chrome.xml
new file mode 100644
index 0000000..070f460
--- /dev/null
+++ b/chrome/android/java/res/drawable/ic_cast_dark_chrome.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2013 The Chromium Authors. All rights reserved.
+
+ Use of this source code is governed by a BSD-style license that can be
+ found in the LICENSE file.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_checked="true" android:state_enabled="true"
+ android:drawable="@drawable/ic_cast_dark_on" />
+ <item android:state_activated="true" android:state_enabled="true"
+ android:drawable="@drawable/ic_cast_dark_on" />
+ <item android:state_enabled="true"
+ android:drawable="@drawable/ic_cast_dark_off" />
+</selector> \ No newline at end of file
diff --git a/chrome/android/java/res/layout/cast_controller_media_route_button.xml b/chrome/android/java/res/layout/cast_controller_media_route_button.xml
new file mode 100644
index 0000000..547361b
--- /dev/null
+++ b/chrome/android/java/res/layout/cast_controller_media_route_button.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2013 The Chromium Authors. All rights reserved.
+ Use of this source code is governed by a BSD-style license that can be
+ found in the LICENSE file.
+-->
+<org.chromium.chrome.browser.media.remote.FullscreenMediaRouteButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/cast_controller_media_route_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="visible"
+ android:layout_gravity = "start|top"
+ style="@style/CastMediaRouteButton" />
diff --git a/chrome/android/java/res/layout/expanded_cast_controller.xml b/chrome/android/java/res/layout/expanded_cast_controller.xml
new file mode 100644
index 0000000..340cef1
--- /dev/null
+++ b/chrome/android/java/res/layout/expanded_cast_controller.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2013 The Chromium Authors. All rights reserved.
+
+ Use of this source code is governed by a BSD-style license that can be
+ found in the LICENSE file.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/cast_frame_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:minHeight="200dp">
+ <ImageView
+ android:id="@+id/cast_background_image"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:paddingStart="12dp"
+ android:paddingEnd="12dp"
+ android:alpha="0.7"
+ android:scaleType="fitCenter"
+ android:adjustViewBounds="true"
+ android:contentDescription="@null"/>
+ <TextView
+ android:id="@+id/cast_screen_title"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:textSize="14sp"
+ android:textStyle="bold"
+ android:textColor="@color/cast_media_controller_text" />
+ <org.chromium.third_party.android.media.MediaController
+ android:id="@+id/cast_media_controller"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom">
+ </org.chromium.third_party.android.media.MediaController>
+</FrameLayout>
diff --git a/chrome/android/java/res/layout/remote_notification_bar.xml b/chrome/android/java/res/layout/remote_notification_bar.xml
new file mode 100644
index 0000000..5cce18d
--- /dev/null
+++ b/chrome/android/java/res/layout/remote_notification_bar.xml
@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2014 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. -->
+
+<!--
+ Notification layout for remote controls.
+ ___________________________________________________________
+ | | | |
+ | | [Living Room TV] | _ |
+ | ICON | =====0============================= | || |_| |
+ | | Playing "[Web Page Title]" | |
+ |________|______________________________________|_________|
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical">
+
+ <FrameLayout
+ android:layout_width="@android:dimen/notification_large_icon_width"
+ android:layout_height="@android:dimen/notification_large_icon_height" >
+
+ <ImageView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:contentDescription="@null"
+ android:scaleType="centerInside"
+ android:src="@drawable/notification_icon_bg" />
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:contentDescription="@null"
+ android:scaleType="center"
+ android:src="@drawable/ic_notification_media_route" />
+ </FrameLayout>
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="7dp"
+ android:layout_weight="1"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start"
+ android:ellipsize="end"
+ android:singleLine="true"
+ style="@style/RemoteNotificationTitle"/>
+
+ <!-- android:visibility is set to 'gone' by default since we don't want to show it as long
+ as the duration of the video is unknown. The duration can be unknown in the case of
+ live streaming videos or YouTube. -->
+ <ProgressBar
+ android:id="@+id/progress"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ style="@style/RemoteNotificationProgressBar"/>
+
+ <TextView
+ android:id="@+id/status"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start"
+ android:ellipsize="end"
+ android:singleLine="true"
+ style="@style/RemoteNotificationText"/>
+
+ </LinearLayout>
+
+ <ImageButton
+ android:id="@+id/playpause"
+ android:src="@drawable/ic_vidcontrol_play"
+ android:layout_width="40dp"
+ android:layout_height="48dp"
+ android:layout_marginStart="8dp"
+ android:gravity="center"
+ android:padding="8dp"
+ android:scaleType="center"
+ android:background="?android:attr/selectableItemBackground"
+ android:contentDescription="@null"/>
+
+ <ImageButton
+ android:id="@+id/stop"
+ android:src="@drawable/ic_vidcontrol_stop"
+ android:layout_width="40dp"
+ android:layout_height="48dp"
+ android:layout_marginEnd="8dp"
+ android:gravity="center"
+ android:padding="8dp"
+ android:scaleType="center"
+ android:background="?android:attr/selectableItemBackground"
+ android:contentDescription="@string/accessibility_stop"/>
+
+</LinearLayout> \ No newline at end of file
diff --git a/chrome/android/java/res/values-v17/styles.xml b/chrome/android/java/res/values-v17/styles.xml
index 3957100..ef22bc5 100644
--- a/chrome/android/java/res/values-v17/styles.xml
+++ b/chrome/android/java/res/values-v17/styles.xml
@@ -224,5 +224,23 @@
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowNoTitle">true</item>
</style>
-
+ <!-- Cast -->
+ <style
+ name="RemoteNotificationTitle"
+ parent="@android:style/TextAppearance.StatusBar.EventContent.Title"/>
+ <style
+ name="RemoteNotificationText"
+ parent="@android:style/TextAppearance.StatusBar.EventContent"/>
+ <style
+ name="RemoteNotificationProgressBar"
+ parent="@android:style/Widget.Holo.ProgressBar.Horizontal"/>
+ <style name="CastMediaRouteButton" parent="@style/Widget.MediaRouter.MediaRouteButton">
+ <item name="android:background">@null</item>
+ <item name="android:paddingTop">0dp</item>
+ <item name="android:paddingBottom">0dp</item>
+ <item name="android:paddingStart">0dp</item>
+ <item name="android:paddingEnd">0dp</item>
+ <item name="externalRouteEnabledDrawable">@drawable/ic_cast_dark_chrome</item>
+ </style>
+
</resources>
diff --git a/chrome/android/java/res/values-v21/styles.xml b/chrome/android/java/res/values-v21/styles.xml
index b156f18..75f28bf 100644
--- a/chrome/android/java/res/values-v21/styles.xml
+++ b/chrome/android/java/res/values-v21/styles.xml
@@ -58,5 +58,15 @@
<style name="ButtonCompatBorderless" parent="ButtonCompat">
<item name="android:background">@drawable/button_borderless_compat</item>
</style>
-
+ <!-- Cast notification -->
+ <style name="RemoteNotificationTitle"
+ parent="@android:style/TextAppearance.Material.Notification.Title">
+ <item name="android:layout_marginBottom">4dp</item>
+ </style>
+ <style name="RemoteNotificationText"
+ parent="@android:style/TextAppearance.Material.Notification.Line2"/>
+ <style name="RemoteNotificationProgressBar" parent="@android:style/Widget.ProgressBar.Horizontal">
+ <item name="android:minHeight">5dp</item>
+ </style>
+
</resources>
diff --git a/chrome/android/java/res/values/dimens.xml b/chrome/android/java/res/values/dimens.xml
index ef408db..baa7ce4 100644
--- a/chrome/android/java/res/values/dimens.xml
+++ b/chrome/android/java/res/values/dimens.xml
@@ -126,4 +126,8 @@
<!-- Autofill card unmasking prompt dimensions -->
<dimen name="autofill_card_unmask_tooltip_horizontal_padding">16dp</dimen>
<dimen name="autofill_card_unmask_tooltip_vertical_padding">4dp</dimen>
+
+ <!-- Cast related constants -->
+ <dimen name="remote_notification_logo_max_width">@android:dimen/notification_large_icon_width</dimen>
+ <dimen name="remote_notification_logo_max_height">64dp</dimen>
</resources>
diff --git a/chrome/android/java/res/values/values.xml b/chrome/android/java/res/values/values.xml
index bb04f2f..6e3b434 100644
--- a/chrome/android/java/res/values/values.xml
+++ b/chrome/android/java/res/values/values.xml
@@ -39,4 +39,7 @@
<item type="id" name="menu_id_help_privacy" />
<item type="id" name="menu_id_contextual_search_learn" />
+ <!-- Cast notification -->
+ <item type="id" name="remote_notification" />
+
</resources>
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/remote/AbstractMediaRouteController.java b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/AbstractMediaRouteController.java
new file mode 100644
index 0000000..24352d4
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/AbstractMediaRouteController.java
@@ -0,0 +1,582 @@
+// 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.chrome.browser.media.remote;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.os.Handler;
+import android.support.v7.media.MediaControlIntent;
+import android.support.v7.media.MediaItemStatus;
+import android.support.v7.media.MediaRouteSelector;
+import android.support.v7.media.MediaRouter;
+import android.support.v7.media.MediaRouter.RouteInfo;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.google.android.gms.cast.CastMediaControlIntent;
+
+import org.chromium.base.ApplicationStatus;
+import org.chromium.base.CommandLine;
+import org.chromium.chrome.ChromeSwitches;
+import org.chromium.chrome.R;
+import org.chromium.chrome.browser.media.remote.RemoteVideoInfo.PlayerState;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+import javax.annotation.Nullable;
+
+/**
+ * Class containing the common, connection type independent, code for all MediaRouteControllers.
+ */
+public abstract class AbstractMediaRouteController implements MediaRouteController {
+
+ /**
+ * Callback class for monitoring whether any routes exist, and hence deciding whether to show
+ * the cast UI to users.
+ */
+ private class DeviceDiscoveryCallback extends MediaRouter.Callback {
+ @Override
+ public void onProviderAdded(MediaRouter router, MediaRouter.ProviderInfo provider) {
+ updateRouteAvailability();
+ }
+
+ @Override
+ public void onProviderChanged(
+ MediaRouter router, MediaRouter.ProviderInfo provider) {
+ updateRouteAvailability();
+ }
+
+ @Override
+ public void onProviderRemoved(
+ MediaRouter router, MediaRouter.ProviderInfo provider) {
+ updateRouteAvailability();
+ }
+
+ @Override
+ public void onRouteAdded(MediaRouter router, RouteInfo route) {
+ if (mDebug) Log.d(TAG, "Added route " + route.getName() + " " + route.getId());
+ updateRouteAvailability();
+ }
+
+ @Override
+ public void onRouteRemoved(MediaRouter router, RouteInfo route) {
+ if (mDebug) {
+ Log.d(TAG, "Removed route " + route.getName() + " " + route.getId());
+ }
+ updateRouteAvailability();
+ }
+
+ private void updateRouteAvailability() {
+ if (mediaRouterInitializationFailed()) return;
+
+ boolean routesAvailable = getMediaRouter().isRouteAvailable(mMediaRouteSelector,
+ MediaRouter.AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE);
+ if (routesAvailable != mRoutesAvailable) {
+ mRoutesAvailable = routesAvailable;
+ if (mDebug) {
+ Log.d(TAG, "Remote media route availability changed, updating listeners");
+ }
+ for (MediaStateListener listener : mAvailableRouteListeners) {
+ listener.onRouteAvailabilityChanged(routesAvailable);
+ }
+ }
+ }
+ }
+
+ /**
+ * Callback class for monitoring whether a route has been selected, and the state of the
+ * selected route.
+ */
+ private class DeviceSelectionCallback extends MediaRouter.Callback {
+ private Runnable mConnectionFailureNotifier = new Runnable() {
+ @Override
+ public void run() {
+ release();
+ mConnectionFailureNotifierQueued = false;
+ }
+ };
+
+ /** True if we are waiting for the MediaRouter route to connect or reconnect */
+ private boolean mConnectionFailureNotifierQueued = false;
+
+ private void clearConnectionFailureCallback() {
+ getHandler().removeCallbacks(mConnectionFailureNotifier);
+ mConnectionFailureNotifierQueued = false;
+ }
+
+ @Override
+ public void onRouteAdded(MediaRouter router, RouteInfo route) {
+ onRouteAddedEvent(router, route);
+ }
+
+ @Override
+ public void onRouteChanged(MediaRouter router, RouteInfo route) {
+ // We only care about changes to the current route.
+ if (!route.equals(getCurrentRoute())) return;
+ // When there is no wifi connection, this condition becomes true.
+ if (route.isConnecting()) {
+ // We don't want to post the same Runnable twice.
+ if (!mConnectionFailureNotifierQueued) {
+ mConnectionFailureNotifierQueued = true;
+ getHandler().postDelayed(mConnectionFailureNotifier,
+ CONNECTION_FAILURE_NOTIFICATION_DELAY_MS);
+ }
+ } else {
+ // Only cancel the disconnect if we already posted the message. We can get into this
+ // situation if we swap the current route provider (for example, switching to a YT
+ // video while casting a non-YT video).
+ if (mConnectionFailureNotifierQueued) {
+ // We have reconnected, cancel the delayed disconnect.
+ getHandler().removeCallbacks(mConnectionFailureNotifier);
+ mConnectionFailureNotifierQueued = false;
+ }
+ }
+ }
+
+ @Override
+ public void onRouteSelected(MediaRouter router, RouteInfo route) {
+ onRouteSelectedEvent(router, route);
+ }
+
+ @Override
+ public void onRouteUnselected(MediaRouter router, RouteInfo route) {
+ onRouteUnselectedEvent(router, route);
+ if (getCurrentRoute() != null && !getCurrentRoute().isDefault()
+ && route.getId().equals(getCurrentRoute().getId())) {
+ RecordCastAction.castEndedTimeRemaining(getDuration(),
+ getDuration() - getPosition());
+ release();
+ }
+ }
+ }
+
+ /** Number of ms to wait for reconnection, after which we call the failure callbacks. */
+ protected static final int CONNECTION_FAILURE_NOTIFICATION_DELAY_MS = 10000;
+ private static final int END_OF_VIDEO_THRESHOLD_MS = 500;
+ private static final String TAG = "AbstractMediaRouteController";
+ private final Set<MediaStateListener> mAvailableRouteListeners;
+ private final Context mContext;
+ private RouteInfo mCurrentRoute;
+ private boolean mDebug;
+ private final DeviceDiscoveryCallback mDeviceDiscoveryCallback;;
+ private String mDeviceId;
+ private final DeviceSelectionCallback mDeviceSelectionCallback;
+
+ private final Handler mHandler;
+ private boolean mIsPrepared = false;
+
+ private final MediaRouter mMediaRouter;
+
+ private final MediaRouteSelector mMediaRouteSelector;
+ private MediaStateListener mMediaStateListener;
+ private PlayerState mPlaybackState = PlayerState.FINISHED;
+ private boolean mRoutesAvailable = false;
+ private final Set<UiListener> mUiListeners;
+ private boolean mWatchingRouteSelection = false;
+
+ /**
+ * Sole constructor
+ */
+ protected AbstractMediaRouteController() {
+
+ mDebug = CommandLine.getInstance().hasSwitch(ChromeSwitches.ENABLE_CAST_DEBUG_LOGS);
+
+ mContext = ApplicationStatus.getApplicationContext();
+ assert (getContext() != null);
+
+ mHandler = new Handler();
+
+ mMediaRouteSelector = buildMediaRouteSelector();
+
+ MediaRouter mediaRouter;
+
+ try {
+ // Pre-MR1 versions of JB do not have the complete MediaRouter APIs,
+ // so getting the MediaRouter instance will throw an exception.
+ mediaRouter = MediaRouter.getInstance(getContext());
+ } catch (NoSuchMethodError e) {
+ Log.e(TAG, "Can't get an instance of MediaRouter, casting is not supported."
+ + " Are you still on JB (JVP15S)?");
+ mediaRouter = null;
+ }
+ mMediaRouter = mediaRouter;
+
+ mAvailableRouteListeners = new HashSet<MediaStateListener>();
+ // TODO(aberent): I am unclear why this is accessed from multiple threads, but
+ // if I make it a HashSet then it gets ConcurrentModificationExceptions on some
+ // types of disconnect. Investigate and fix.
+ mUiListeners = new CopyOnWriteArraySet<UiListener>();
+
+ mDeviceDiscoveryCallback = new DeviceDiscoveryCallback();
+ mDeviceSelectionCallback = new DeviceSelectionCallback();
+ }
+
+ @Override
+ public void addMediaStateListener(MediaStateListener listener) {
+ if (mediaRouterInitializationFailed()) return;
+
+ if (mAvailableRouteListeners.isEmpty()) {
+ getMediaRouter().addCallback(mMediaRouteSelector, mDeviceDiscoveryCallback,
+ MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
+ if (mDebug) Log.d(TAG, "Started device discovery");
+
+ // Get the initial state
+ mRoutesAvailable = getMediaRouter().isRouteAvailable(
+ mMediaRouteSelector, MediaRouter.AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE);
+ }
+ mAvailableRouteListeners.add(listener);
+ // Send the current state to the listener.
+ listener.onRouteAvailabilityChanged(mRoutesAvailable);
+ }
+
+ @Override
+ public void addUiListener(UiListener listener) {
+ mUiListeners.add(listener);
+ }
+
+ protected void clearConnectionFailureCallback() {
+ mDeviceSelectionCallback.clearConnectionFailureCallback();
+ }
+
+ /**
+ * Clear the current playing item (if any) but not the associated session.
+ */
+ protected void clearItemState() {
+ mPlaybackState = PlayerState.FINISHED;
+ updateTitle(null);
+ }
+
+ /**
+ * Reset the media route to the default
+ */
+ protected void clearMediaRoute() {
+ if (getMediaRouter() != null) {
+ getMediaRouter().getDefaultRoute().select();
+ registerRoute(getMediaRouter().getDefaultRoute());
+ RemotePlaybackSettings.setDeviceId(getContext(), null);
+ }
+ }
+
+ @Override
+ public boolean currentRouteSupportsRemotePlayback() {
+ return mCurrentRoute != null && mCurrentRoute.supportsControlCategory(
+ MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+ }
+
+ protected final Context getContext() {
+ return mContext;
+ }
+
+ protected final RouteInfo getCurrentRoute() {
+ return mCurrentRoute;
+ }
+
+ protected final String getDeviceId() {
+ return mDeviceId;
+ }
+
+ protected final Handler getHandler() {
+ return mHandler;
+ }
+
+ /**
+ * @return the mMediaRouter
+ */
+ protected final MediaRouter getMediaRouter() {
+ return mMediaRouter;
+ }
+
+ @Override
+ public final MediaStateListener getMediaStateListener() {
+ return mMediaStateListener;
+ }
+
+ @Override
+ public final PlayerState getPlayerState() {
+ return mPlaybackState;
+ }
+
+ @Override
+ public final String getRouteName() {
+ return mCurrentRoute == null ? null : mCurrentRoute.getName();
+ }
+
+ /**
+ * @return The list of MediaRouteController.Listener objects that will receive messages from
+ * this class.
+ */
+ protected final Set<UiListener> getUiListeners() {
+ return mUiListeners;
+ }
+
+ private final boolean isAtEndOfVideo(int positionMs, int videoLengthMs) {
+ return videoLengthMs - positionMs < END_OF_VIDEO_THRESHOLD_MS && videoLengthMs > 0;
+ }
+
+ @Override
+ public final boolean isBeingCast() {
+ return (mPlaybackState != PlayerState.INVALIDATED && mPlaybackState != PlayerState.ERROR
+ && mPlaybackState != PlayerState.FINISHED);
+ }
+
+ @Override
+ public final boolean isPlaying() {
+ return mPlaybackState == PlayerState.PLAYING || mPlaybackState == PlayerState.LOADING;
+ }
+
+ @Override
+ public final boolean isRemotePlaybackAvailable() {
+ if (mediaRouterInitializationFailed()) return false;
+
+ return getMediaRouter().getSelectedRoute().getPlaybackType()
+ == MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE || getMediaRouter().isRouteAvailable(
+ mMediaRouteSelector, MediaRouter.AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE);
+ }
+
+ protected final boolean mediaRouterInitializationFailed() {
+ return getMediaRouter() == null;
+ }
+
+ protected final void notifyRouteSelected(RouteInfo route) {
+ for (UiListener listener : mUiListeners) {
+ listener.onRouteSelected(route.getName(), this);
+ }
+ if (mMediaStateListener != null) mMediaStateListener.onRouteSelected(route.getName());
+ }
+
+ @Override
+ public void onPause() {
+ pause();
+ }
+
+ @Override
+ public void onPlay() {
+ resume();
+ }
+
+ protected abstract void onRouteAddedEvent(MediaRouter router, RouteInfo route);
+
+ protected abstract void onRouteSelectedEvent(MediaRouter router, RouteInfo route);
+
+ protected abstract void onRouteUnselectedEvent(MediaRouter router, RouteInfo route);
+
+ @Override
+ public void onSeek(int position) {
+ seekTo(position);
+ }
+
+ @Override
+ public void onStop() {
+ release();
+ }
+
+ @Override
+ public void prepareMediaRoute() {
+ startWatchingRouteSelection();
+ }
+
+ protected final void registerRoute(RouteInfo route) {
+ mCurrentRoute = route;
+
+ if (route != null) {
+ setDeviceId(route.getId());
+ if (mDebug) Log.d(TAG, "Selected route " + getDeviceId());
+ if (!route.isDefault()) {
+ RemotePlaybackSettings.setDeviceId(getContext(), getDeviceId());
+ }
+ } else {
+ RemotePlaybackSettings.setDeviceId(getContext(), null);
+ }
+ }
+
+ protected void removeAllListeners() {
+ mUiListeners.clear();
+ }
+
+ @Override
+ public void removeMediaStateListener(MediaStateListener listener) {
+ if (mediaRouterInitializationFailed()) return;
+
+ mAvailableRouteListeners.remove(listener);
+ if (mAvailableRouteListeners.isEmpty()) {
+ getMediaRouter().removeCallback(mDeviceDiscoveryCallback);
+ if (mDebug) Log.d(TAG, "Stopped device discovery");
+ }
+ }
+
+ @Override
+ public void removeUiListener(UiListener listener) {
+ mUiListeners.remove(listener);
+ }
+
+ @Override
+ public boolean routeIsDefaultRoute() {
+ return mCurrentRoute != null && mCurrentRoute.isDefault();
+ }
+
+ protected void sendErrorToListeners(int error) {
+ String errorMessage =
+ getContext().getString(R.string.cast_error_playing_video, mCurrentRoute.getName());
+
+ for (UiListener listener : mUiListeners) {
+ listener.onError(error, errorMessage);
+ }
+
+ if (mMediaStateListener != null) mMediaStateListener.onError();
+ }
+
+ protected void setDeviceId(String mDeviceId) {
+ this.mDeviceId = mDeviceId;
+ }
+
+ @Override
+ public void setMediaStateListener(MediaStateListener mediaStateListener) {
+ mMediaStateListener = mediaStateListener;
+ }
+
+ private void setPrepared() {
+ if (!mIsPrepared) {
+ for (UiListener listener : mUiListeners) {
+ listener.onPrepared(this);
+ }
+ if (mMediaStateListener != null) mMediaStateListener.onPrepared();
+ RecordCastAction.castDefaultPlayerResult(true);
+ mIsPrepared = true;
+ }
+ }
+
+ protected void setUnprepared() {
+ mIsPrepared = false;
+ }
+
+ @Override
+ public boolean shouldResetState(MediaStateListener newPlayer) {
+ return !isBeingCast() || newPlayer != getMediaStateListener();
+ }
+
+ protected void showCastError(String routeName) {
+ Toast toast = Toast.makeText(
+ getContext(),
+ getContext().getString(R.string.cast_error_playing_video, routeName),
+ Toast.LENGTH_SHORT);
+ toast.show();
+ }
+
+ private void startWatchingRouteSelection() {
+ if (mWatchingRouteSelection || mediaRouterInitializationFailed()) return;
+
+ mWatchingRouteSelection = true;
+ // Start listening
+ getMediaRouter().addCallback(mMediaRouteSelector, mDeviceSelectionCallback,
+ MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
+ if (mDebug) Log.d(TAG, "Started route selection discovery");
+ }
+
+ protected void stopWatchingRouteSelection() {
+ mWatchingRouteSelection = false;
+ if (getMediaRouter() != null) {
+ getMediaRouter().removeCallback(mDeviceSelectionCallback);
+ if (mDebug) Log.d(TAG, "Stopped route selection discovery");
+ }
+ }
+
+ protected void updateState(int state) {
+ if (mDebug) {
+ Log.d(TAG, "updateState oldState: " + this.mPlaybackState + " newState: " + state);
+ }
+
+ PlayerState oldState = this.mPlaybackState;
+
+ PlayerState playerState = PlayerState.STOPPED;
+ switch (state) {
+ case MediaItemStatus.PLAYBACK_STATE_BUFFERING:
+ playerState = PlayerState.LOADING;
+ break;
+ case MediaItemStatus.PLAYBACK_STATE_CANCELED:
+ playerState = PlayerState.FINISHED;
+ break;
+ case MediaItemStatus.PLAYBACK_STATE_ERROR:
+ playerState = PlayerState.ERROR;
+ break;
+ case MediaItemStatus.PLAYBACK_STATE_FINISHED:
+ playerState = PlayerState.FINISHED;
+ break;
+ case MediaItemStatus.PLAYBACK_STATE_INVALIDATED:
+ playerState = PlayerState.INVALIDATED;
+ break;
+ case MediaItemStatus.PLAYBACK_STATE_PAUSED:
+ if (isAtEndOfVideo(getPosition(), getDuration())) {
+ playerState = PlayerState.FINISHED;
+ } else {
+ playerState = PlayerState.PAUSED;
+ }
+ break;
+ case MediaItemStatus.PLAYBACK_STATE_PENDING:
+ playerState = PlayerState.PAUSED;
+ break;
+ case MediaItemStatus.PLAYBACK_STATE_PLAYING:
+ playerState = PlayerState.PLAYING;
+ break;
+ default:
+ break;
+ }
+
+ this.mPlaybackState = playerState;
+
+ for (UiListener listener : mUiListeners) {
+ listener.onPlaybackStateChanged(oldState, playerState);
+ }
+
+ if (mMediaStateListener != null) mMediaStateListener.onPlaybackStateChanged(playerState);
+
+ if (oldState != mPlaybackState) {
+ // We need to persist our state in case we get killed.
+ RemotePlaybackSettings.setLastVideoState(getContext(), mPlaybackState.name());
+
+ switch (mPlaybackState) {
+ case PLAYING:
+ RemotePlaybackSettings.setRemainingTime(getContext(),
+ getDuration() - getPosition());
+ RemotePlaybackSettings.setLastPlayedTime(getContext(),
+ System.currentTimeMillis());
+ RemotePlaybackSettings.setShouldReconnectToRemote(getContext(),
+ !mCurrentRoute.isDefault());
+ setPrepared();
+ break;
+ case PAUSED:
+ RemotePlaybackSettings.setShouldReconnectToRemote(getContext(),
+ !mCurrentRoute.isDefault());
+ setPrepared();
+ break;
+ case FINISHED:
+ release();
+ break;
+ case INVALIDATED:
+ clearItemState();
+ break;
+ case ERROR:
+ sendErrorToListeners(CastMediaControlIntent.ERROR_CODE_REQUEST_FAILED);
+ release();
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ protected void updateTitle(@Nullable String newTitle) {
+ for (UiListener listener : mUiListeners) {
+ listener.onTitleChanged(newTitle);
+ }
+ }
+
+ @Override
+ public Bitmap getPoster() {
+ if (mMediaStateListener == null) return null;
+ return mMediaStateListener.getPosterBitmap();
+ }
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/remote/ChromeMediaRouteDialogFactory.java b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/ChromeMediaRouteDialogFactory.java
new file mode 100644
index 0000000..8ed805d
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/ChromeMediaRouteDialogFactory.java
@@ -0,0 +1,96 @@
+// Copyright 2014 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.chrome.browser.media.remote;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.v7.app.MediaRouteChooserDialog;
+import android.support.v7.app.MediaRouteChooserDialogFragment;
+import android.support.v7.app.MediaRouteControllerDialog;
+import android.support.v7.app.MediaRouteControllerDialogFragment;
+import android.support.v7.app.MediaRouteDialogFactory;
+import android.view.View;
+import android.widget.FrameLayout;
+
+/**
+ * The Chrome implementation of the dialog factory so custom behavior can
+ * be injected for the disconnect button.
+ */
+public class ChromeMediaRouteDialogFactory extends MediaRouteDialogFactory {
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ private static class SystemVisibilitySaver {
+ private int mSystemVisibility;
+ private boolean mRestoreSystemVisibility;
+
+ void saveSystemVisibility(Activity activity) {
+ // The Android APIs don't exist on old versions.
+ // TODO(aberent) this can go once the minSdkVersion has been updated in the manifests.
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) return;
+ // If we are in fullscreen we may have also have hidden the system UI. This
+ // is overridden when we display the dialog. Save the system UI visibility
+ // state so we can restore it.
+ FrameLayout decor = (FrameLayout) activity.getWindow().getDecorView();
+ mSystemVisibility = decor.getSystemUiVisibility();
+ mRestoreSystemVisibility = (
+ (mSystemVisibility & View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) != 0);
+ }
+
+ void restoreSystemVisibility(Activity activity) {
+ if (mRestoreSystemVisibility) {
+ FrameLayout decor = (FrameLayout) activity.getWindow().getDecorView();
+ // In some cases we come out of fullscreen before closing this dialog. In these
+ // cases we don't want to restore the system UI visibility state.
+ int systemVisibility = decor.getSystemUiVisibility();
+ if ((systemVisibility & View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) != 0) {
+ decor.setSystemUiVisibility(mSystemVisibility);
+ }
+ }
+ }
+ }
+
+ @Override
+ public MediaRouteControllerDialogFragment onCreateControllerDialogFragment() {
+ return new MediaRouteControllerDialogFragment() {
+ final SystemVisibilitySaver mVisibilitySaver = new SystemVisibilitySaver();
+
+ @Override
+ public Dialog onCreateDialog(Bundle saved) {
+ mVisibilitySaver.saveSystemVisibility(getActivity());
+ return new MediaRouteControllerDialog(getActivity());
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ mVisibilitySaver.restoreSystemVisibility(getActivity());
+ }
+ };
+ }
+
+ @Override
+ public MediaRouteChooserDialogFragment onCreateChooserDialogFragment() {
+ return new MediaRouteChooserDialogFragment() {
+ final SystemVisibilitySaver mVisibilitySaver = new SystemVisibilitySaver();
+
+ @Override
+ public MediaRouteChooserDialog onCreateChooserDialog(
+ Context context, Bundle savedInstanceState) {
+ mVisibilitySaver.saveSystemVisibility(getActivity());
+ return new MediaRouteChooserDialog(context);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ mVisibilitySaver.restoreSystemVisibility(getActivity());
+ }
+ };
+ }
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/remote/DefaultMediaRouteController.java b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/DefaultMediaRouteController.java
new file mode 100644
index 0000000..027b5ef
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/DefaultMediaRouteController.java
@@ -0,0 +1,1101 @@
+// Copyright 2012 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.chrome.browser.media.remote;
+
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.v7.media.MediaControlIntent;
+import android.support.v7.media.MediaItemMetadata;
+import android.support.v7.media.MediaItemStatus;
+import android.support.v7.media.MediaRouteSelector;
+import android.support.v7.media.MediaRouter;
+import android.support.v7.media.MediaRouter.RouteInfo;
+import android.support.v7.media.MediaSessionStatus;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.google.android.gms.cast.CastMediaControlIntent;
+
+import org.apache.http.Header;
+import org.chromium.base.ApplicationState;
+import org.chromium.base.ApplicationStatus;
+import org.chromium.base.CommandLine;
+import org.chromium.chrome.ChromeSwitches;
+import org.chromium.chrome.R;
+import org.chromium.chrome.browser.media.remote.RemoteVideoInfo.PlayerState;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import javax.annotation.Nullable;
+
+/**
+ * Class that abstracts all communication to and from the Android MediaRoutes. This class is
+ * responsible for connecting to the MRs as well as sending commands and receiving status updates
+ * from the remote player.
+ *
+ * We have three main scenarios for Cast:
+ *
+ * - the first cast: user plays the first video on the Chromecast so we start a new session with
+ * the player and fling the video
+ *
+ * - the consequent cast: users plays another video while the previous one is still playing
+ * remotely meaning that we don't have to start the session but to replace the current video with
+ * the new one
+ *
+ * - the reconnect: if Clank crashes, we need to try to reconnect to the existing session and
+ * continue controlling the currently playing video.
+ *
+ * Casting the first video takes three intents sent to the selected media route:
+ * ACTION_START_SESSION, ACTION_SYNC_STATUS and ACTION_PLAY. The first one is sent before anything
+ * else. We get the session id from the result bundle of the intent but need to wait until the
+ * session becomes active before continuing to the next step. Then we send the ACTION_SYNC_STATUS
+ * intent to update the media item status and pass the PendingIntent for the media item status
+ * events to the Cast MRP. Finally we send the video URL via the ACTION_PLAY intent.
+ *
+ * Casting the second video should only take one ACTION_PLAY intent if the session is still active.
+ * Otherwise, the scenario is the same as for the first video. However, due to the crbug.com/336188
+ * we need to restart the session for each ACTION_PLAY so we go through the same process as above.
+ *
+ * In order to reconnect, we need to programmatically select the previously selected media route.
+ * To do this we send an ACTION_START_SESSION with the old session ID. This is not clearly
+ * documented in the Android documentation, but seems to only succeed if the session still exists.
+ * Otherwise we need to start a new session.
+ *
+ * Note that, if the Chrome cast notification restarts following a crash, instances of this class
+ * may exist before the C++ library has been loaded. As such this class should avoid using anything
+ * that might use the C++ library (almost anything else in Chrome) or check that the library is
+ * loaded before using them (as it does for recording UMA statistics).
+ */
+public class DefaultMediaRouteController extends AbstractMediaRouteController {
+
+ /**
+ * Interface for MediaRouter intents result handlers.
+ */
+ protected interface ResultBundleHandler {
+ void onResult(Bundle data);
+
+ void onError(String message, Bundle data);
+ }
+
+ private static final String TAG = "DefaultMediaRouteController";
+
+ private static final String ACTION_RECEIVE_SESSION_STATUS_UPDATE =
+ "com.google.android.apps.chrome.videofling.RECEIVE_SESSION_STATUS_UPDATE";
+ private static final String ACTION_RECEIVE_MEDIA_STATUS_UPDATE =
+ "com.google.android.apps.chrome.videofling.RECEIVE_MEDIA_STATUS_UPDATE";
+ private static final String MIME_TYPE = "video/mp4";
+
+ private boolean mDebug;
+ private String mCurrentSessionId;
+ private String mCurrentItemId;
+ private int mStreamPositionTimestamp;
+ private int mLastKnownStreamPosition;
+ private int mStreamDuration;
+ private boolean mSeeking;
+ private final String mIntentCategory;
+ private PendingIntent mSessionStatusUpdateIntent;
+ private BroadcastReceiver mSessionStatusBroadcastReceiver;
+ private PendingIntent mMediaStatusUpdateIntent;
+ private BroadcastReceiver mMediaStatusBroadcastReceiver;
+ private boolean mReconnecting = false;
+
+ private Uri mVideoUriToStart;
+ private String mPreferredTitle;
+ private long mStartPositionMillis;
+
+ private Uri mLocalVideoUri;
+
+ private String mLocalVideoCookies;
+
+ private MediaUrlResolver mMediaUrlResolver;
+
+ private int mSessionState = MediaSessionStatus.SESSION_STATE_INVALIDATED;
+
+ // Media types supported for cast, see
+ // media/base/container_names.h for the actual enum where these are defined
+ private static final int UNKNOWN_MEDIA = 0;
+ private static final int SMOOTHSTREAM_MEDIA = 39;
+ private static final int DASH_MEDIA = 38;
+ private static final int HLS_MEDIA = 22;
+ private static final int MPEG4_MEDIA = 29;
+
+ private final ApplicationStatus.ApplicationStateListener
+ mApplicationStateListener = new ApplicationStatus.ApplicationStateListener() {
+ @Override
+ public void onApplicationStateChange(int newState) {
+ switch (newState) {
+ // HAS_DESTROYED_ACTIVITIES means all Chrome activities have been destroyed.
+ case ApplicationState.HAS_DESTROYED_ACTIVITIES:
+ onActivitiesDestroyed();
+ break;
+ default:
+ break;
+ }
+ }
+ };
+
+ private final MediaUrlResolver.Delegate
+ mMediaUrlResolverDelegate = new MediaUrlResolver.Delegate() {
+ @Override
+ public Uri getUri() {
+ return mLocalVideoUri;
+ }
+
+ @Override
+ public String getCookies() {
+ return mLocalVideoCookies;
+ }
+
+ @Override
+ public void setUri(Uri uri, Header[] headers) {
+ if (canPlayMedia(uri, headers)) {
+ mLocalVideoUri = uri;
+ playMedia();
+ return;
+ }
+ mLocalVideoUri = null;
+ showMessageToast(
+ getContext().getString(R.string.cast_permission_error_playing_video));
+ release();
+ }
+ };
+
+ /**
+ * Default and only constructor.
+ */
+ public DefaultMediaRouteController() {
+ mDebug = CommandLine.getInstance().hasSwitch(ChromeSwitches.ENABLE_CAST_DEBUG_LOGS);
+ mIntentCategory = getContext().getPackageName();
+ }
+
+ @Override
+ public boolean initialize() {
+ if (mediaRouterInitializationFailed()) return false;
+
+ ApplicationStatus.registerApplicationStateListener(mApplicationStateListener);
+
+ if (mSessionStatusUpdateIntent == null) {
+ Intent sessionStatusUpdateIntent = new Intent(ACTION_RECEIVE_SESSION_STATUS_UPDATE);
+ sessionStatusUpdateIntent.addCategory(mIntentCategory);
+ mSessionStatusUpdateIntent = PendingIntent.getBroadcast(getContext(), 0,
+ sessionStatusUpdateIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ }
+
+ if (mMediaStatusUpdateIntent == null) {
+ Intent mediaStatusUpdateIntent = new Intent(ACTION_RECEIVE_MEDIA_STATUS_UPDATE);
+ mediaStatusUpdateIntent.addCategory(mIntentCategory);
+ mMediaStatusUpdateIntent = PendingIntent.getBroadcast(getContext(), 0,
+ mediaStatusUpdateIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean canPlayMedia(String sourceUrl, String frameUrl) {
+
+ if (mediaRouterInitializationFailed()) return false;
+
+ if (sourceUrl == null) return false;
+
+ try {
+ String scheme = new URI(sourceUrl).getScheme();
+ if (scheme == null) return false;
+ return scheme.equals("http") || scheme.equals("https");
+ } catch (URISyntaxException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public void setRemoteVolume(int delta) {
+ boolean canChangeRemoteVolume = (getCurrentRoute().getVolumeHandling()
+ == MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE);
+ if (currentRouteSupportsRemotePlayback() && canChangeRemoteVolume) {
+ getCurrentRoute().requestUpdateVolume(delta);
+ }
+ }
+
+ @Override
+ public MediaRouteSelector buildMediaRouteSelector() {
+ return new MediaRouteSelector.Builder().addControlCategory(
+ CastMediaControlIntent.categoryForRemotePlayback(getCastReceiverId())).build();
+ }
+
+ protected String getCastReceiverId() {
+ return CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID;
+ }
+
+ @Override
+ public boolean reconnectAnyExistingRoute() {
+ String deviceId = RemotePlaybackSettings.getDeviceId(getContext());
+ RouteInfo defaultRoute = getMediaRouter().getDefaultRoute();
+ if (deviceId == null || deviceId.equals(defaultRoute.getId()) || !shouldReconnect()) {
+ RemotePlaybackSettings.setShouldReconnectToRemote(getContext(), false);
+ return false;
+ }
+ mReconnecting = true;
+ selectDevice(deviceId);
+ getHandler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (mReconnecting) {
+ Log.d(TAG, "Reconnection timed out");
+ // We have been trying to reconnect for too long. Give up and save battery.
+ mReconnecting = false;
+ release();
+ }
+ }
+ }, CONNECTION_FAILURE_NOTIFICATION_DELAY_MS);
+ return true;
+ }
+
+ private boolean shouldReconnect() {
+ if (CommandLine.getInstance().hasSwitch(ChromeSwitches.DISABLE_CAST_RECONNECTION)) {
+ if (mDebug) Log.d(TAG, "Cast reconnection disabled");
+ return false;
+ }
+ boolean reconnect = false;
+ if (RemotePlaybackSettings.getShouldReconnectToRemote(getContext())) {
+ String lastState = RemotePlaybackSettings.getLastVideoState(getContext());
+ if (lastState != null) {
+ PlayerState state = PlayerState.valueOf(lastState);
+ if (state == PlayerState.PLAYING || state == PlayerState.LOADING) {
+ // If we were playing when we got killed, check the time to
+ // see if it's still
+ // plausible that the remote video is playing currently
+ long remainingPlaytime = RemotePlaybackSettings.getRemainingTime(getContext());
+ long lastPlayedTime = RemotePlaybackSettings.getLastPlayedTime(getContext());
+ long currentTime = System.currentTimeMillis();
+ if (currentTime < lastPlayedTime + remainingPlaytime) {
+ reconnect = true;
+ }
+ } else if (state == PlayerState.PAUSED) {
+ reconnect = true;
+ }
+ }
+ }
+ if (mDebug) Log.d(TAG, "shouldReconnect returning: " + reconnect);
+ return reconnect;
+ }
+
+ /**
+ * Tries to select a device with the given device ID. The device ID is cached so that if the
+ * route does not exist yet, we will connect to it as soon as it comes back up again
+ *
+ * @param deviceId the ID of the device to connect to
+ */
+ private void selectDevice(String deviceId) {
+ if (deviceId == null) {
+ release();
+ return;
+ }
+
+ setDeviceId(deviceId);
+
+ if (mDebug) Log.d(TAG, "Trying to select " + getDeviceId());
+
+ // See if we can select the device at this point.
+ if (getMediaRouter() != null) {
+ for (MediaRouter.RouteInfo route : getMediaRouter().getRoutes()) {
+ if (deviceId.equals(route.getId())) {
+ route.select();
+ break;
+ }
+ }
+ }
+ }
+
+ @Override
+ public void resume() {
+ if (mCurrentItemId == null) return;
+
+ Intent intent = new Intent(MediaControlIntent.ACTION_RESUME);
+ intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+ intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, mCurrentSessionId);
+ sendIntentToRoute(intent, new ResultBundleHandler() {
+ @Override
+ public void onResult(Bundle data) {
+ processMediaStatusBundle(data);
+ }
+
+ @Override
+ public void onError(String message, Bundle data) {
+ release();
+ }
+ });
+
+ updateState(MediaItemStatus.PLAYBACK_STATE_BUFFERING);
+ }
+
+ @Override
+ public void pause() {
+ if (mCurrentItemId == null) return;
+
+ Intent intent = new Intent(MediaControlIntent.ACTION_PAUSE);
+ intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+ intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, mCurrentSessionId);
+ sendIntentToRoute(intent, new ResultBundleHandler() {
+ @Override
+ public void onResult(Bundle data) {
+ processMediaStatusBundle(data);
+ }
+
+ @Override
+ public void onError(String message, Bundle data) {
+ // Do not release the player just because of a failed pause
+ // request. This can happen when pausing more than once for
+ // example.
+ }
+ });
+
+ // Update the last known position to the current one so that we don't
+ // jump back in time discarding whatever we extrapolated from the last
+ // time the position was updated.
+ mLastKnownStreamPosition = getPosition();
+ updateState(MediaItemStatus.PLAYBACK_STATE_PAUSED);
+ }
+
+ /**
+ * Plays the given Uri on the currently selected player. This will replace any currently playing
+ * video
+ *
+ * @param videoUri Uri of the video to play
+ * @param preferredTitle the preferred title of the current playback session to display
+ * @param startPositionMillis from which to start playing.
+ */
+ private void playUri(final Uri videoUri,
+ @Nullable final String preferredTitle, final long startPositionMillis) {
+
+ RecordCastAction.castMediaType(getMediaType(videoUri));
+ installBroadcastReceivers();
+
+ // Check if we are reconnecting or have reconnected and are playing the same video
+ if ((mReconnecting || mCurrentSessionId != null)
+ && videoUri.toString().equals(RemotePlaybackSettings.getUriPlaying(getContext()))) {
+ return;
+ }
+
+ // If the session is already started (meaning we are casting a video already), we simply
+ // load the new URL with one ACTION_PLAY intent.
+ if (mCurrentSessionId != null) {
+ if (mDebug) Log.d(TAG, "Playing a new url: " + videoUri);
+
+ RemotePlaybackSettings.setUriPlaying(getContext(), videoUri.toString());
+
+ // We keep the same session so only clear the playing item status.
+ clearItemState();
+ startPlayback(videoUri, preferredTitle, startPositionMillis);
+ return;
+ }
+
+ RemotePlaybackSettings.setPlayerInUse(getContext(), getCastReceiverId());
+ if (mDebug) {
+ Log.d(TAG, "Sending stream to app: " + getCastReceiverId());
+ Log.d(TAG, "Url: " + videoUri);
+ }
+
+ startSession(true, null, new ResultBundleHandler() {
+ @Override
+ public void onResult(Bundle data) {
+ configureNewSession(data);
+
+ mVideoUriToStart = videoUri;
+ RemotePlaybackSettings.setUriPlaying(getContext(), videoUri.toString());
+ mPreferredTitle = preferredTitle;
+ mStartPositionMillis = startPositionMillis;
+ // Make sure we get a session status. If the session becomes active
+ // immediately then the broadcast session status can arrive before we have
+ // the session id, so this ensures we get it whatever happens.
+ getSessionStatus(mCurrentSessionId);
+ }
+
+ @Override
+ public void onError(String message, Bundle data) {
+ release();
+ RecordCastAction.castDefaultPlayerResult(false);
+ }
+ });
+ }
+
+ /**
+ * Send a start session intent.
+ *
+ * @param relaunch Whether we should relaunch the cast application.
+ * @param resultBundleHandler BundleHandler to handle reply.
+ */
+ private void startSession(boolean relaunch, String sessionId,
+ ResultBundleHandler resultBundleHandler) {
+ Intent intent = new Intent(MediaControlIntent.ACTION_START_SESSION);
+ intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+
+ intent.putExtra(CastMediaControlIntent.EXTRA_CAST_STOP_APPLICATION_WHEN_SESSION_ENDS, true);
+ intent.putExtra(MediaControlIntent.EXTRA_SESSION_STATUS_UPDATE_RECEIVER,
+ mSessionStatusUpdateIntent);
+ intent.putExtra(CastMediaControlIntent.EXTRA_CAST_APPLICATION_ID, getCastReceiverId());
+ intent.putExtra(CastMediaControlIntent.EXTRA_CAST_RELAUNCH_APPLICATION, relaunch);
+ if (sessionId != null) intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, sessionId);
+
+ if (mDebug) intent.putExtra(CastMediaControlIntent.EXTRA_DEBUG_LOGGING_ENABLED, true);
+
+ sendIntentToRoute(intent, resultBundleHandler);
+ }
+
+ private void getSessionStatus(String sessionId) {
+ Intent intent = new Intent(MediaControlIntent.ACTION_GET_SESSION_STATUS);
+ intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+
+ intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, sessionId);
+
+ sendIntentToRoute(intent, new ResultBundleHandler() {
+ @Override
+ public void onResult(Bundle data) {
+ if (mDebug) Log.d(TAG, "getSessionStatus result : " + bundleToString(data));
+
+ processSessionStatusBundle(data);
+ }
+
+ @Override
+ public void onError(String message, Bundle data) {
+ release();
+ }
+ });
+ }
+
+ private void startPlayback(final Uri videoUri, @Nullable final String preferredTitle,
+ final long startPositionMillis) {
+ setUnprepared();
+ Intent intent = new Intent(MediaControlIntent.ACTION_PLAY);
+ intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+ intent.setDataAndType(videoUri, MIME_TYPE);
+ intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, mCurrentSessionId);
+ intent.putExtra(MediaControlIntent.EXTRA_ITEM_STATUS_UPDATE_RECEIVER,
+ mMediaStatusUpdateIntent);
+ intent.putExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, startPositionMillis);
+
+ if (preferredTitle != null) {
+ Bundle metadata = new Bundle();
+ metadata.putString(MediaItemMetadata.KEY_TITLE, preferredTitle);
+ intent.putExtra(MediaControlIntent.EXTRA_ITEM_METADATA, metadata);
+ }
+
+ sendIntentToRoute(intent, new ResultBundleHandler() {
+ @Override
+ public void onResult(Bundle data) {
+ mCurrentItemId = data.getString(MediaControlIntent.EXTRA_ITEM_ID);
+ processMediaStatusBundle(data);
+ RecordCastAction.castDefaultPlayerResult(true);
+ }
+
+ @Override
+ public void onError(String message, Bundle data) {
+ release();
+ RecordCastAction.castDefaultPlayerResult(false);
+ }
+ });
+ }
+
+ @Override
+ public int getPosition() {
+ boolean paused = (getPlayerState() != PlayerState.PLAYING);
+ if ((mStreamPositionTimestamp != 0) && !mSeeking && !paused
+ && (mLastKnownStreamPosition < mStreamDuration)) {
+
+ long extrapolatedStreamPosition = mLastKnownStreamPosition
+ + (SystemClock.uptimeMillis() - mStreamPositionTimestamp);
+ if (extrapolatedStreamPosition > mStreamDuration) {
+ extrapolatedStreamPosition = mStreamDuration;
+ }
+ return (int) extrapolatedStreamPosition;
+ }
+ return mLastKnownStreamPosition;
+ }
+
+ @Override
+ public int getDuration() {
+ return mStreamDuration;
+ }
+
+ @Override
+ public void seekTo(int msec) {
+ if (msec == getPosition()) return;
+ // Update the position now since the MRP will update it only once the video is playing
+ // remotely. In particular, if the video is paused, the MRP doesn't send the command until
+ // the video is resumed.
+ mLastKnownStreamPosition = msec;
+ mSeeking = true;
+ Intent intent = new Intent(MediaControlIntent.ACTION_SEEK);
+ intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+ intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, mCurrentSessionId);
+ intent.putExtra(MediaControlIntent.EXTRA_ITEM_ID, mCurrentItemId);
+ intent.putExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, (long) msec);
+ sendIntentToRoute(intent, new ResultBundleHandler() {
+ @Override
+ public void onResult(Bundle data) {
+ if (getMediaStateListener() != null) getMediaStateListener().onSeekCompleted();
+ processMediaStatusBundle(data);
+ }
+
+ @Override
+ public void onError(String message, Bundle data) {
+ release();
+ }
+ });
+ }
+
+ @Override
+ public void release() {
+ for (UiListener listener : getUiListeners()) {
+ listener.onRouteUnselected(this);
+ }
+ if (getMediaStateListener() != null) getMediaStateListener().onRouteUnselected();
+ setMediaStateListener(null);
+
+ stopAndDisconnect();
+ }
+
+ /**
+ * Stop the current remote playback and release all associated resources. Resources will be
+ * released even if the stop operation fails.
+ */
+ private void stopAndDisconnect() {
+ if (mediaRouterInitializationFailed()) return;
+ if (mCurrentSessionId == null) {
+ // This can happen if we disconnect after a failure (because the
+ // media could not be casted).
+ disconnect(true);
+ return;
+ }
+
+ Intent stopIntent = new Intent(MediaControlIntent.ACTION_STOP);
+ stopIntent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+ stopIntent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, mCurrentSessionId);
+
+ sendIntentToRoute(stopIntent, new ResultBundleHandler() {
+ @Override
+ public void onResult(Bundle data) {
+ processMediaStatusBundle(data);
+ }
+
+ @Override
+ public void onError(String message, Bundle data) {}
+ });
+
+ Intent endSessionIntent = new Intent(MediaControlIntent.ACTION_END_SESSION);
+ endSessionIntent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+ endSessionIntent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, mCurrentSessionId);
+
+ sendIntentToRoute(endSessionIntent, new ResultBundleHandler() {
+ @Override
+ public void onResult(Bundle data) {
+ if (mDebug) {
+ MediaSessionStatus status = MediaSessionStatus.fromBundle(
+ data.getBundle(MediaControlIntent.EXTRA_SESSION_STATUS));
+ int sessionState = status.getSessionState();
+ Log.d(TAG, "Session state after ending session: " + sessionState);
+ }
+
+ for (UiListener listener : getUiListeners()) {
+ listener.onPlaybackStateChanged(getPlayerState(), PlayerState.FINISHED);
+ }
+
+ if (getMediaStateListener() != null) {
+ getMediaStateListener().onPlaybackStateChanged(PlayerState.FINISHED);
+ }
+ RecordCastAction.castEndedTimeRemaining(mStreamDuration,
+ mStreamDuration - getPosition());
+ disconnect(true);
+ }
+
+ @Override
+ public void onError(String message, Bundle data) {
+ disconnect(true);
+ }
+ });
+ }
+
+ /**
+ * Disconnect from the remote screen without stopping the media playing. use release() for
+ * disconnect + stop.
+ *
+ * @param finishedWithRoute true if finished with route and remote device, false if just
+ * shutting down Chrome.
+ */
+ private void disconnect(boolean finishedWithRoute) {
+ if (finishedWithRoute) {
+ clearStreamState();
+ clearMediaRoute();
+ }
+
+ if (mSessionStatusBroadcastReceiver != null) {
+ getContext().unregisterReceiver(mSessionStatusBroadcastReceiver);
+ mSessionStatusBroadcastReceiver = null;
+ }
+ if (mMediaStatusBroadcastReceiver != null) {
+ getContext().unregisterReceiver(mMediaStatusBroadcastReceiver);
+ mMediaStatusBroadcastReceiver = null;
+ }
+ clearConnectionFailureCallback();
+
+ stopWatchingRouteSelection();
+ removeAllListeners();
+ }
+
+ @Override
+ protected void onRouteAddedEvent(MediaRouter router, RouteInfo route) {
+ if (mDebug) Log.d(TAG, "Added route " + route);
+ if (getDeviceId() != null && getDeviceId().equals(route.getId())) {
+ // This is the route we are waiting to connect to, select it.
+ if (mDebug) Log.d(TAG, "Selecting Added Device " + route.getName());
+ route.select();
+ }
+ }
+
+ @Override
+ protected void onRouteSelectedEvent(MediaRouter router, RouteInfo route) {
+ if (mDebug) Log.d(TAG, "Selected route " + route);
+ if (!route.isSelected()) return;
+
+ RecordCastAction.remotePlaybackDeviceSelected(
+ RecordCastAction.DEVICE_TYPE_CAST_GENERIC);
+ installBroadcastReceivers();
+
+ if (getMediaStateListener() == null) {
+ showCastError(route.getName());
+ release();
+ return;
+ }
+
+ registerRoute(route);
+ if (shouldReconnect()) {
+ startSession(false, RemotePlaybackSettings.getSessionId(getContext()),
+ new ResultBundleHandler() {
+ @Override
+ public void onResult(Bundle data) {
+ configureNewSession(data);
+ setUnprepared();
+ mReconnecting = false;
+ // Make sure we get a session status. If the session becomes active
+ // immediately then the broadcast session status can arrive before we
+ // have the session id, so this ensures we get it whatever happens.
+ getSessionStatus(mCurrentSessionId);
+ }
+
+ @Override
+ public void onError(String message, Bundle data) {
+ // Ignore errors, the connection sometimes is bouncy on reconnection,
+ // and the reconnection timer is still running so will tidy up if
+ // we never manage to connect.
+ }
+ });
+ } else {
+ clearStreamState();
+ mReconnecting = false;
+ }
+
+ notifyRouteSelected(route);
+ }
+
+ /*
+ * Although our custom implementation of the disconnect button doesn't need this, it is
+ * needed when the route is released due to, for example, another application stealing the
+ * route, or when we switch to a YouTube video on the same device.
+ */
+ @Override
+ protected void onRouteUnselectedEvent(MediaRouter router, RouteInfo route) {
+ if (mDebug) Log.d(TAG, "Unselected route " + route);
+ // Preserve our best guess as to the final position; this is needed to reset the
+ // local position while switching back to local playback.
+ mLastKnownStreamPosition = getPosition();
+ if (getCurrentRoute() != null && route.getId().equals(getCurrentRoute().getId())) {
+ clearStreamState();
+ }
+ }
+
+ private void installBroadcastReceivers() {
+ if (mSessionStatusBroadcastReceiver == null) {
+ mSessionStatusBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (mDebug) {
+ dumpIntentToLog("Got a session broadcast intent from the MRP: ", intent);
+ }
+ Bundle statusBundle = intent.getExtras();
+
+ // Ignore null status bundles.
+ if (statusBundle == null) return;
+
+ // Ignore the status of old sessions.
+ String sessionId = statusBundle.getString(MediaControlIntent.EXTRA_SESSION_ID);
+ if (mCurrentSessionId == null || !mCurrentSessionId.equals(sessionId)) return;
+
+ processSessionStatusBundle(statusBundle);
+ }
+ };
+ IntentFilter sessionBroadcastIntentFilter =
+ new IntentFilter(ACTION_RECEIVE_SESSION_STATUS_UPDATE);
+ sessionBroadcastIntentFilter.addCategory(mIntentCategory);
+ getContext().registerReceiver(mSessionStatusBroadcastReceiver,
+ sessionBroadcastIntentFilter);
+ }
+
+ if (mMediaStatusBroadcastReceiver == null) {
+ mMediaStatusBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (mDebug) dumpIntentToLog("Got a broadcast intent from the MRP: ", intent);
+
+ processMediaStatusBundle(intent.getExtras());
+ }
+ };
+ IntentFilter mediaBroadcastIntentFilter =
+ new IntentFilter(ACTION_RECEIVE_MEDIA_STATUS_UPDATE);
+ mediaBroadcastIntentFilter.addCategory(mIntentCategory);
+ getContext().registerReceiver(mMediaStatusBroadcastReceiver,
+ mediaBroadcastIntentFilter);
+ }
+ }
+
+ /**
+ * Called when the main activity receives an onDestroy() call.
+ */
+ protected void onActivitiesDestroyed() {
+ ApplicationStatus.unregisterApplicationStateListener(mApplicationStateListener);
+ // It is important to not clear the stream state here to let Chrome
+ // reconnect to a session upon startup.
+ disconnect(false);
+ }
+
+ /**
+ * Clear the session and the currently playing item (if any).
+ */
+ protected void clearStreamState() {
+ mVideoUriToStart = null;
+ mLocalVideoUri = null;
+ mCurrentSessionId = null;
+ clearItemState();
+
+ if (getContext() != null) {
+ RemotePlaybackSettings.setShouldReconnectToRemote(getContext(), false);
+ RemotePlaybackSettings.setUriPlaying(getContext(), null);
+ }
+ }
+
+ @Override
+ protected void clearItemState() {
+ // Note: do not clear the stream position, since this is still needed so
+ // that we can reset the local stream position to match.
+ super.clearItemState();
+ mCurrentItemId = null;
+ mStreamPositionTimestamp = 0;
+ mStreamDuration = 0;
+ mSeeking = false;
+ }
+
+ private void syncStatus(String sessionId, ResultBundleHandler bundleHandler) {
+ if (sessionId == null) return;
+ Intent intent = new Intent(CastMediaControlIntent.ACTION_SYNC_STATUS);
+ intent.addCategory(CastMediaControlIntent.categoryForRemotePlayback());
+ intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, sessionId);
+ intent.putExtra(MediaControlIntent.EXTRA_ITEM_STATUS_UPDATE_RECEIVER,
+ mMediaStatusUpdateIntent);
+ sendIntentToRoute(intent, bundleHandler);
+ }
+
+ private void processSessionStatusBundle(Bundle statusBundle) {
+ MediaSessionStatus status = MediaSessionStatus.fromBundle(
+ statusBundle.getBundle(MediaControlIntent.EXTRA_SESSION_STATUS));
+ int sessionState = status.getSessionState();
+
+ // If no change do nothing
+ if (sessionState == mSessionState) return;
+ mSessionState = sessionState;
+
+ switch (sessionState) {
+ case MediaSessionStatus.SESSION_STATE_ACTIVE:
+ // TODO(aberent): This should not be needed. Remove this once b/12921924 is fixed.
+ syncStatus(mCurrentSessionId, new ResultBundleHandler() {
+ @Override
+ public void onResult(Bundle data) {
+ processMediaStatusBundle(data);
+ if (mVideoUriToStart != null) {
+ startPlayback(mVideoUriToStart, mPreferredTitle, mStartPositionMillis);
+ mVideoUriToStart = null;
+ }
+ }
+
+ @Override
+ public void onError(String message, Bundle data) {
+ release();
+ }
+ });
+ break;
+
+ case MediaSessionStatus.SESSION_STATE_ENDED:
+ case MediaSessionStatus.SESSION_STATE_INVALIDATED:
+ for (UiListener listener : getUiListeners()) {
+ listener.onPlaybackStateChanged(getPlayerState(), PlayerState.INVALIDATED);
+ }
+ if (getMediaStateListener() != null) {
+ getMediaStateListener().onPlaybackStateChanged(PlayerState.INVALIDATED);
+ }
+ // Set the current session id to null so we don't send the stop intent.
+ mCurrentSessionId = null;
+ release();
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ private void processMediaStatusBundle(Bundle statusBundle) {
+ if (statusBundle == null) return;
+
+ if (mDebug) Log.d(TAG, "processMediaStatusBundle: " + bundleToString(statusBundle));
+
+ String itemId = statusBundle.getString(MediaControlIntent.EXTRA_ITEM_ID);
+ if (itemId == null || !itemId.equals(mCurrentItemId)) return;
+
+ // Extract item metadata, if available.
+ if (statusBundle.containsKey(MediaControlIntent.EXTRA_ITEM_METADATA)) {
+ Bundle metadataBundle =
+ (Bundle) statusBundle.getParcelable(MediaControlIntent.EXTRA_ITEM_METADATA);
+ updateTitle(metadataBundle.getString(MediaItemMetadata.KEY_TITLE));
+ }
+
+ // Extract the item status, if available.
+ if (statusBundle.containsKey(MediaControlIntent.EXTRA_ITEM_STATUS)) {
+ Bundle itemStatusBundle =
+ (Bundle) statusBundle.getParcelable(MediaControlIntent.EXTRA_ITEM_STATUS);
+ MediaItemStatus itemStatus = MediaItemStatus.fromBundle(itemStatusBundle);
+
+ if (mDebug) Log.d(TAG, "Received item status: " + bundleToString(itemStatusBundle));
+
+ updateState(itemStatus.getPlaybackState());
+
+ if ((getPlayerState() == PlayerState.PAUSED)
+ || (getPlayerState() == PlayerState.PLAYING)
+ || (getPlayerState() == PlayerState.LOADING)) {
+
+ this.mCurrentItemId = itemId;
+
+ int duration = (int) itemStatus.getContentDuration();
+ // duration can possibly be -1 if it's unknown, so cap to 0
+ updateDuration(Math.max(duration, 0));
+
+ // update the position using the remote player's position
+ mLastKnownStreamPosition = (int) itemStatus.getContentPosition();
+ mStreamPositionTimestamp = (int) itemStatus.getTimestamp();
+ updatePosition();
+
+ if (mSeeking) {
+ mSeeking = false;
+ if (getMediaStateListener() != null) getMediaStateListener().onSeekCompleted();
+ }
+ }
+
+ Bundle extras = itemStatus.getExtras();
+ if (mDebug && extras != null) {
+ if (extras.containsKey(MediaItemStatus.EXTRA_HTTP_STATUS_CODE)) {
+ int httpStatus = extras.getInt(MediaItemStatus.EXTRA_HTTP_STATUS_CODE);
+ Log.d(TAG, "HTTP status: " + httpStatus);
+ }
+ if (extras.containsKey(MediaItemStatus.EXTRA_HTTP_RESPONSE_HEADERS)) {
+ Bundle headers = extras.getBundle(MediaItemStatus.EXTRA_HTTP_RESPONSE_HEADERS);
+ Log.d(TAG, "HTTP headers: " + headers);
+ }
+ }
+ }
+ }
+
+ /**
+ * Send the given intent to the current route. The result will be returned in the given
+ * ResultBundleHandler. This function will also check to see if the current route can handle the
+ * intent before sending it.
+ *
+ * @param intent the intent to send to the current route.
+ * @param bundleHandler contains the result of sending the intent
+ */
+ private void sendIntentToRoute(final Intent intent, final ResultBundleHandler bundleHandler) {
+ if (getCurrentRoute() == null) {
+ if (mDebug) {
+ dumpIntentToLog("sendIntentToRoute ", intent);
+ Log.d(TAG, "The current route is null.");
+ }
+ if (bundleHandler != null) bundleHandler.onError(null, null);
+ return;
+ }
+
+ if (!getCurrentRoute().supportsControlRequest(intent)) {
+ if (mDebug) {
+ dumpIntentToLog("sendIntentToRoute ", intent);
+ Log.d(TAG, "The intent is not supported by the route: " + getCurrentRoute());
+ }
+ if (bundleHandler != null) bundleHandler.onError(null, null);
+ return;
+ }
+
+ sendControlIntent(intent, bundleHandler);
+ }
+
+ private void sendControlIntent(final Intent intent, final ResultBundleHandler bundleHandler) {
+
+ if (mDebug) {
+ Log.d(TAG,
+ "Sending intent to " + getCurrentRoute().getName() + " "
+ + getCurrentRoute().getId());
+ dumpIntentToLog("sendControlIntent ", intent);
+ }
+ if (getCurrentRoute().isDefault()) {
+ if (mDebug) Log.d(TAG, "Route is default, not sending");
+ return;
+ }
+
+ getCurrentRoute().sendControlRequest(intent, new MediaRouter.ControlRequestCallback() {
+ @Override
+ public void onResult(Bundle data) {
+ if (data != null && bundleHandler != null) bundleHandler.onResult(data);
+ }
+
+ @Override
+ public void onError(String message, Bundle data) {
+ if (mDebug) {
+ // The intent may contain some PII so we don't want to log it in the released
+ // version by default.
+ Log.e(TAG, String.format(
+ "Error sending control request %s %s. Data: %s Error: %s", intent,
+ bundleToString(intent.getExtras()), bundleToString(data), message));
+ }
+
+ int errorCode = 0;
+ if (data != null) {
+ errorCode = data.getInt(CastMediaControlIntent.EXTRA_ERROR_CODE);
+ }
+
+ sendErrorToListeners(errorCode);
+
+ if (bundleHandler != null) bundleHandler.onError(message, data);
+ }
+ });
+ }
+
+ private void updateDuration(int durationMillis) {
+ mStreamDuration = durationMillis;
+
+ for (UiListener listener : getUiListeners()) {
+ listener.onDurationUpdated(durationMillis);
+ }
+ }
+
+ private void updatePosition() {
+ for (UiListener listener : getUiListeners()) {
+ listener.onPositionChanged(getPosition());
+ }
+ }
+
+ private void dumpIntentToLog(String prefix, Intent intent) {
+ Log.d(TAG, prefix + intent + " extras: " + bundleToString(intent.getExtras()));
+ }
+
+ private String bundleToString(Bundle bundle) {
+ if (bundle == null) return "";
+
+ StringBuilder extras = new StringBuilder();
+ extras.append("[");
+ for (String key : bundle.keySet()) {
+ Object value = bundle.get(key);
+ String valueText = value == null ? "null" : value.toString();
+ if (value instanceof Bundle) valueText = bundleToString((Bundle) value);
+ extras.append(key).append("=").append(valueText).append(",");
+ }
+ extras.append("]");
+ return extras.toString();
+ }
+
+ @Override
+ public void setDataSource(Uri uri, String cookies) {
+ if (mDebug) Log.d(TAG, "setDataSource called, uri = " + uri);
+ mLocalVideoUri = uri;
+ mLocalVideoCookies = cookies;
+ }
+
+ @Override
+ public void prepareAsync(String frameUrl, long startPositionMillis) {
+ if (mDebug) Log.d(TAG, "prepareAsync called, mLocalVideoUri = " + mLocalVideoUri);
+ if (mLocalVideoUri == null) return;
+
+ RecordCastAction.castPlayRequested();
+
+ // Cancel the previous task for URL resolving so that we don't get an old URI set.
+ if (mMediaUrlResolver != null) mMediaUrlResolver.cancel(true);
+
+ // Create a new MediaUrlResolver since the previous one may still be running despite the
+ // cancel() call.
+ mMediaUrlResolver = new MediaUrlResolver(getContext(), mMediaUrlResolverDelegate);
+
+ mStartPositionMillis = startPositionMillis;
+ mMediaUrlResolver.execute();
+ }
+
+ private boolean canPlayMedia(Uri uri, Header[] headers) {
+ if (uri == Uri.EMPTY) return false;
+
+ // HLS media requires Cors headers. Since these are the only ones
+ // sent now we can just check that headers is not empty but
+ // if more headers are added we should be more strict in the check.
+ if ((headers == null || headers.length == 0) && isEnhancedMedia(uri)) {
+ if (mDebug) Log.d(TAG, "HLS stream without CORs header: " + uri);
+ return false;
+ }
+ return true;
+ }
+
+ private void playMedia() {
+ String title = getMediaStateListener().getTitle();
+ playUri(mLocalVideoUri, title, mStartPositionMillis);
+ }
+
+ private void showMessageToast(String message) {
+ Toast toast = Toast.makeText(getContext(), message, Toast.LENGTH_SHORT);
+ toast.show();
+ }
+
+ private void configureNewSession(Bundle data) {
+ mCurrentSessionId = data.getString(MediaControlIntent.EXTRA_SESSION_ID);
+ mSessionState = MediaSessionStatus.SESSION_STATE_INVALIDATED;
+ RemotePlaybackSettings.setSessionId(getContext(), mCurrentSessionId);
+ if (mDebug) Log.d(TAG, "Got a session id: " + mCurrentSessionId);
+ }
+
+ private int getMediaType(Uri videoUri) {
+ String uriString = videoUri.toString();
+ if (uriString.contains(".m3u8")) {
+ return HLS_MEDIA;
+ }
+ if (uriString.contains(".mp4")) {
+ return MPEG4_MEDIA;
+ }
+ if (uriString.contains(".mpd")) {
+ return DASH_MEDIA;
+ }
+ if (uriString.contains(".ism")) {
+ return SMOOTHSTREAM_MEDIA;
+ }
+ return UNKNOWN_MEDIA;
+ }
+
+ private boolean isEnhancedMedia(Uri videoUri) {
+ int mediaType = getMediaType(videoUri);
+ return mediaType == HLS_MEDIA || mediaType == DASH_MEDIA || mediaType == SMOOTHSTREAM_MEDIA;
+ }
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/remote/ExpandedControllerActivity.java b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/ExpandedControllerActivity.java
new file mode 100644
index 0000000..f452fc1
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/ExpandedControllerActivity.java
@@ -0,0 +1,353 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.media.remote;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.media.TransportMediator;
+import android.support.v4.media.TransportPerformer;
+import android.text.TextUtils;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.google.android.gms.cast.CastMediaControlIntent;
+
+import org.chromium.base.ApiCompatibilityUtils;
+import org.chromium.chrome.R;
+import org.chromium.chrome.browser.media.remote.RemoteVideoInfo.PlayerState;
+import org.chromium.third_party.android.media.MediaController;
+
+/**
+ * The activity that's opened by clicking the video flinging (casting) notification.
+ *
+ * TODO(cimamoglu): Refactor to merge some common logic with {@link TransportControl}.
+ */
+public class ExpandedControllerActivity
+ extends FragmentActivity implements MediaRouteController.UiListener {
+ private static final int PROGRESS_UPDATE_PERIOD_IN_MS = 1000;
+ // The alpha value for the poster/placeholder image, an integer between 0 and 256 (opaque).
+ private static final int POSTER_IMAGE_ALPHA = 200;
+
+ private Handler mHandler;
+ // We don't use the standard android.media.MediaController, but a custom one.
+ // See the class itself for details.
+ private MediaController mMediaController;
+ private FullscreenMediaRouteButton mMediaRouteButton;
+ private MediaRouteController mMediaRouteController;
+ private RemoteVideoInfo mVideoInfo;
+ private String mScreenName;
+ private TransportMediator mTransportMediator;
+
+ /**
+ * Handle actions from on-screen media controls.
+ */
+ private TransportPerformer mTransportPerformer = new TransportPerformer() {
+ @Override
+ public void onStart() {
+ mMediaRouteController.resume();
+ }
+
+ @Override
+ public void onStop() {
+ onPause();
+ mMediaRouteController.release();
+ }
+
+ @Override
+ public void onPause() {
+ mMediaRouteController.pause();
+ }
+
+ @Override
+ public long onGetDuration() {
+ return mMediaRouteController.getDuration();
+ }
+
+ @Override
+ public long onGetCurrentPosition() {
+ return mMediaRouteController.getPosition();
+ }
+
+ @Override
+ public void onSeekTo(long pos) {
+ mMediaRouteController.seekTo((int) pos);
+ }
+
+ @Override
+ public boolean onIsPlaying() {
+ return mMediaRouteController.isPlaying();
+ }
+
+ @Override
+ public int onGetTransportControlFlags() {
+ int flags = TransportMediator.FLAG_KEY_MEDIA_REWIND
+ | TransportMediator.FLAG_KEY_MEDIA_FAST_FORWARD;
+ if (mMediaRouteController.isPlaying()) {
+ flags |= TransportMediator.FLAG_KEY_MEDIA_PAUSE;
+ } else {
+ flags |= TransportMediator.FLAG_KEY_MEDIA_PLAY;
+ }
+ return flags;
+ }
+ };
+
+ private Runnable mProgressUpdater = new Runnable() {
+ @Override
+ public void run() {
+ if (mMediaRouteController.isPlaying()) {
+ mMediaController.updateProgress();
+ mHandler.postDelayed(this, PROGRESS_UPDATE_PERIOD_IN_MS);
+ } else {
+ mHandler.removeCallbacks(this);
+ }
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mMediaRouteController =
+ RemoteMediaPlayerController.instance().getCurrentlyPlayingMediaRouteController();
+
+ if (mMediaRouteController == null || mMediaRouteController.routeIsDefaultRoute()) {
+ // We don't want to do anything for the default (local) route
+ finish();
+ return;
+ }
+
+ // Make the activity full screen.
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
+ WindowManager.LayoutParams.FLAG_FULLSCREEN);
+
+ // requestWindowFeature must be called before adding content.
+ setContentView(R.layout.expanded_cast_controller);
+ mHandler = new Handler();
+
+ ViewGroup rootView = (ViewGroup) findViewById(android.R.id.content);
+ rootView.setBackgroundColor(Color.BLACK);
+
+ mMediaRouteController.addUiListener(this);
+
+ // Create transport controller to control video, giving the callback
+ // interface to receive actions from.
+ mTransportMediator = new TransportMediator(this, mTransportPerformer);
+
+ // Create and initialize the media control UI.
+ mMediaController = (MediaController) findViewById(R.id.cast_media_controller);
+ mMediaController.setMediaPlayer(mTransportMediator);
+
+ View button = getLayoutInflater().inflate(R.layout.cast_controller_media_route_button,
+ rootView, false);
+
+ if (button instanceof FullscreenMediaRouteButton) {
+ mMediaRouteButton = (FullscreenMediaRouteButton) button;
+ rootView.addView(mMediaRouteButton);
+ mMediaRouteButton.bringToFront();
+ mMediaRouteButton.initialize(mMediaRouteController);
+ } else {
+ mMediaRouteButton = null;
+ }
+
+ // Initialize the video info.
+ setVideoInfo(new RemoteVideoInfo(null, 0, RemoteVideoInfo.PlayerState.STOPPED, 0, null));
+
+ mMediaController.refresh();
+
+ scheduleProgressUpdate();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (mVideoInfo.state == PlayerState.FINISHED) finish();
+ if (mMediaRouteController == null) return;
+ mMediaRouteController.prepareMediaRoute();
+
+ ImageView iv = (ImageView) findViewById(R.id.cast_background_image);
+ if (iv == null) return;
+ Bitmap posterBitmap = mMediaRouteController.getPoster();
+ if (posterBitmap != null) iv.setImageBitmap(posterBitmap);
+ ApiCompatibilityUtils.setImageAlpha(iv, POSTER_IMAGE_ALPHA);
+ }
+
+ @Override
+ protected void onDestroy() {
+ cleanup();
+ super.onDestroy();
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ int keyCode = event.getKeyCode();
+ if ((keyCode != KeyEvent.KEYCODE_VOLUME_DOWN && keyCode != KeyEvent.KEYCODE_VOLUME_UP)
+ || mVideoInfo.state == PlayerState.FINISHED) {
+ return super.dispatchKeyEvent(event);
+ }
+
+ return handleVolumeKeyEvent(mMediaRouteController, event);
+ }
+
+ private void cleanup() {
+ if (mHandler != null) mHandler.removeCallbacks(mProgressUpdater);
+ if (mMediaRouteController != null) mMediaRouteController.removeUiListener(this);
+ mMediaRouteController = null;
+ mProgressUpdater = null;
+ }
+
+ /**
+ * Sets the remote's video information to display.
+ */
+ private final void setVideoInfo(RemoteVideoInfo videoInfo) {
+ if ((mVideoInfo == null) ? (videoInfo == null) : mVideoInfo.equals(videoInfo)) return;
+
+ mVideoInfo = videoInfo;
+ onVideoInfoChanged();
+ }
+
+ private void scheduleProgressUpdate() {
+ mHandler.removeCallbacks(mProgressUpdater);
+ if (mMediaRouteController.isPlaying()) {
+ mHandler.post(mProgressUpdater);
+ }
+ }
+
+ /**
+ * Sets the name to display for the device.
+ */
+ private void setScreenName(String screenName) {
+ if (TextUtils.equals(mScreenName, screenName)) return;
+
+ mScreenName = screenName;
+ onScreenNameChanged();
+ }
+
+ private void onVideoInfoChanged() {
+ updateUi();
+ }
+
+ private void onScreenNameChanged() {
+ updateUi();
+ }
+
+ private void updateUi() {
+ if (mMediaController == null || mMediaRouteController == null) return;
+
+ String deviceName = mMediaRouteController.getRouteName();
+ String castText = "";
+ if (deviceName != null) {
+ castText = getResources().getString(R.string.cast_casting_video, deviceName);
+ }
+ TextView castTextView = (TextView) findViewById(R.id.cast_screen_title);
+ castTextView.setText(castText);
+
+ mMediaController.refresh();
+ }
+
+ @Override
+ public void onRouteSelected(String name, MediaRouteController mediaRouteController) {
+ setScreenName(name);
+ }
+
+ @Override
+ public void onRouteUnselected(MediaRouteController mediaRouteController) {
+ finish();
+ }
+
+ @Override
+ public void onPrepared(MediaRouteController mediaRouteController) {
+ // No implementation.
+ }
+
+ @Override
+ public void onError(int error, String message) {
+ if (error == CastMediaControlIntent.ERROR_CODE_SESSION_START_FAILED) finish();
+ }
+
+ @Override
+ public void onPlaybackStateChanged(PlayerState oldState, PlayerState newState) {
+ RemoteVideoInfo videoInfo = new RemoteVideoInfo(mVideoInfo);
+ videoInfo.state = newState;
+ setVideoInfo(videoInfo);
+
+ scheduleProgressUpdate();
+
+ if (newState == PlayerState.FINISHED || newState == PlayerState.INVALIDATED) {
+ // If we are switching to a finished state, stop the notifications.
+ finish();
+ }
+ }
+
+ @Override
+ public void onDurationUpdated(int durationMillis) {
+ RemoteVideoInfo videoInfo = new RemoteVideoInfo(mVideoInfo);
+ videoInfo.durationMillis = durationMillis;
+ setVideoInfo(videoInfo);
+ }
+
+ @Override
+ public void onPositionChanged(int positionMillis) {
+ RemoteVideoInfo videoInfo = new RemoteVideoInfo(mVideoInfo);
+ videoInfo.currentTimeMillis = positionMillis;
+ setVideoInfo(videoInfo);
+ }
+
+ @Override
+ public void onTitleChanged(String title) {
+ RemoteVideoInfo videoInfo = new RemoteVideoInfo(mVideoInfo);
+ videoInfo.title = title;
+ setVideoInfo(videoInfo);
+ }
+
+ /**
+ * Modify remote volume by handling volume keys.
+ *
+ * @param controller The remote controller through which the volume will be modified.
+ * @param event The key event. Its keycode needs to be either {@code KEYCODE_VOLUME_DOWN} or
+ * {@code KEYCODE_VOLUME_UP} otherwise this method will return false.
+ * @return True if the event is handled.
+ */
+ private boolean handleVolumeKeyEvent(MediaRouteController controller, KeyEvent event) {
+ if (!controller.isBeingCast()) return false;
+
+ int action = event.getAction();
+ int keyCode = event.getKeyCode();
+ // Intercept the volume keys to affect only remote volume.
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_VOLUME_DOWN:
+ if (action == KeyEvent.ACTION_DOWN) controller.setRemoteVolume(-1);
+ return true;
+ case KeyEvent.KEYCODE_VOLUME_UP:
+ if (action == KeyEvent.ACTION_DOWN) controller.setRemoteVolume(1);
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Launches the ExpandedControllerActivity as a new task.
+ *
+ * @param context the Context to start this activity within.
+ */
+ public static void startActivity(Context context) {
+ if (context == null) return;
+
+ Intent intent = new Intent(context, ExpandedControllerActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(intent);
+ }
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/remote/FullscreenMediaRouteButton.java b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/FullscreenMediaRouteButton.java
new file mode 100644
index 0000000..f9005a4
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/FullscreenMediaRouteButton.java
@@ -0,0 +1,77 @@
+// Copyright 2012 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.chrome.browser.media.remote;
+
+import android.content.Context;
+import android.support.v7.app.MediaRouteButton;
+import android.util.AttributeSet;
+import android.view.View;
+
+/**
+ * Cast button that wraps around a MediaRouteButton. We show the button only if there are available
+ * cast devices.
+ */
+public class FullscreenMediaRouteButton extends MediaRouteButton {
+
+ // Are we in the time window when the button should become visible if there're routes?
+ private boolean mVisibilityRequested;
+
+ /**
+ * The constructor invoked when inflating the button.
+ */
+ public FullscreenMediaRouteButton(Context context, AttributeSet attributeSet) {
+ super(context, attributeSet);
+ mVisibilityRequested = false;
+ }
+
+ /**
+ * Set the necessary state for the button to work.
+ */
+ /**
+ * Set the necessary state for the button to work
+ * @param controller the MediaRouteController controlling the route
+ */
+ public void initialize(MediaRouteController controller) {
+ setRouteSelector(controller.buildMediaRouteSelector());
+ setDialogFactory(new ChromeMediaRouteDialogFactory());
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ if (!RemoteMediaPlayerController.isRemotePlaybackEnabled()) return;
+
+ // TODO(aberent) not sure if this is still used, and in particular if mVisibilityRequest
+ // is still used.
+
+ // We need to check if the button was in the same state before to avoid doing anything,
+ // but we also need to update the current state for {@link #setButtonVisibility} to work.
+ boolean wasEnabled = isEnabled();
+ super.setEnabled(enabled);
+
+ if (wasEnabled == enabled) return;
+
+ if (enabled && mVisibilityRequested) {
+ setButtonVisibility(View.VISIBLE);
+ } else {
+ setVisibility(View.GONE);
+ }
+ }
+
+ private void setButtonVisibility(int visibility) {
+ // If the button is being set to visible, first make sure that it can even cast
+ // to anything before making it actually visible.
+ if (visibility == View.VISIBLE) {
+ if (isEnabled()) {
+ setVisibility(View.VISIBLE);
+ } else {
+ setVisibility(View.GONE);
+ }
+ } else {
+ setVisibility(visibility);
+ }
+ }
+
+}
+
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/remote/LockScreenTransportControl.java b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/LockScreenTransportControl.java
new file mode 100644
index 0000000..5232309
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/LockScreenTransportControl.java
@@ -0,0 +1,182 @@
+// Copyright 2014 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.chrome.browser.media.remote;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+import android.view.KeyEvent;
+
+import org.chromium.base.CommandLine;
+import org.chromium.base.VisibleForTesting;
+import org.chromium.chrome.ChromeSwitches;
+import org.chromium.chrome.browser.media.remote.RemoteVideoInfo.PlayerState;
+
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * An abstract base class and factory for {@link TransportControl}s that are displayed on the lock
+ * screen.
+ */
+public abstract class LockScreenTransportControl
+ extends TransportControl implements MediaRouteController.UiListener {
+ private static final String TAG = "LockScreenTransportControl";
+
+ private static LockScreenTransportControl sInstance;
+
+ private MediaRouteController mMediaRouteController = null;
+
+ private static final Object LOCK = new Object();
+
+ private static boolean sDebug;
+
+ // Needed to get around findbugs complaints.
+ private static void setSDebug() {
+ sDebug = CommandLine.getInstance().hasSwitch(ChromeSwitches.ENABLE_CAST_DEBUG_LOGS);
+ }
+
+ protected LockScreenTransportControl() {
+ setSDebug();
+ }
+
+ /**
+ * {@link BroadcastReceiver} that receives the media button events from the lock screen and
+ * forwards the messages on to the {@link TransportControl}'s listeners.
+ *
+ * Ideally this class should be private, but public is required to create as a
+ * BroadcastReceiver.
+ */
+ public static class MediaButtonIntentReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (sDebug) Log.d(TAG, "Received intent: " + intent);
+ if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) {
+ KeyEvent event = (KeyEvent) intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
+ LockScreenTransportControl control = LockScreenTransportControl.getIfExists();
+ if (control == null) {
+ Log.w(TAG, "Event received when no LockScreenTransportControl exists");
+ return;
+ }
+ Set<Listener> listeners = control.getListeners();
+
+ // Ignore ACTION_DOWN. We'll get an ACTION_UP soon enough!
+ if (event.getAction() == KeyEvent.ACTION_DOWN) return;
+
+ switch (event.getKeyCode()) {
+ case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+ for (Listener listener : listeners) {
+ if (control.isPlaying()) {
+ listener.onPause();
+ } else {
+ listener.onPlay();
+ }
+ }
+ break;
+ case KeyEvent.KEYCODE_MEDIA_STOP:
+ for (Listener listener : listeners)
+ listener.onStop();
+ break;
+ default:
+ Log.w(TAG, "Unrecognized event: " + event);
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Get the unique LockScreenTransportControl, creating it if necessary.
+ * @param context The context of the activity
+ * @param mediaRouteController The current mediaRouteController, if any.
+ * @return a {@code LockScreenTransportControl} based on the platform's SDK API or null if the
+ * current platform's SDK API is not supported.
+ */
+ public static LockScreenTransportControl getOrCreate(Context context,
+ @Nullable MediaRouteController mediaRouteController) {
+ Log.d(TAG, "getOrCreate called");
+ synchronized (LOCK) {
+ if (sInstance == null) {
+
+ // TODO(aberent) Investigate disabling the lock screen for Android L. It is
+ // supposedly deprecated, but the still seems to be the only way of controlling the
+ // wallpaper (which we set to the poster of the current video) when the phone is
+ // locked. Also, once the minSdkVersion is updated in the manifest, get rid of the
+ // code for older SDK versions.
+ if (!enabled()) {
+ return null;
+ } else if (android.os.Build.VERSION.SDK_INT < 16) {
+ sInstance = new LockScreenTransportControlV14(context);
+ } else if (android.os.Build.VERSION.SDK_INT < 18) {
+ sInstance = new LockScreenTransportControlV16(context);
+ } else {
+ sInstance = new LockScreenTransportControlV18(context);
+ }
+ }
+ sInstance.setVideoInfo(
+ new RemoteVideoInfo(null, 0, RemoteVideoInfo.PlayerState.STOPPED, 0, null));
+
+ sInstance.mMediaRouteController = mediaRouteController;
+ return sInstance;
+ }
+ }
+
+ protected MediaRouteController getMediaRouteController() {
+ return mMediaRouteController;
+ }
+
+ /**
+ * TODO(aberent) From PlayMovies code. Either remove this and get V14/15 working or combine V14
+ * and V16 versions only and change getOrCreate to only support V16 or later.
+ *
+ * @return true if lock screen transport controls should be used on this device.
+ */
+ private static boolean enabled() {
+ // Lock screen controls don't work well prior to JB, see b/9101584
+ return android.os.Build.VERSION.SDK_INT >= 16;
+ }
+
+ /**
+ * Internal function for callbacks that need to get the current lock screen statically, but
+ * don't want to create a new one.
+ *
+ * @return the current lock screen, if any.
+ */
+ @VisibleForTesting
+ static LockScreenTransportControl getIfExists() {
+ return sInstance;
+ }
+
+ @Override
+ public void hide() {
+ onLockScreenPlaybackStateChanged(null, PlayerState.STOPPED);
+ mMediaRouteController.removeUiListener(this);
+ }
+
+ @Override
+ public void show(PlayerState initialState) {
+ mMediaRouteController.addUiListener(this);
+ onLockScreenPlaybackStateChanged(null, initialState);
+ }
+
+ @Override
+ public void setRouteController(MediaRouteController controller) {
+ synchronized (LOCK) {
+ if (sInstance != null) sInstance.mMediaRouteController = controller;
+ }
+ }
+
+ @Override
+ public void onPlaybackStateChanged(PlayerState oldState, PlayerState newState) {
+ onLockScreenPlaybackStateChanged(oldState, newState);
+ }
+
+ protected abstract void onLockScreenPlaybackStateChanged(PlayerState oldState,
+ PlayerState newState);
+
+ protected abstract boolean isPlaying();
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/remote/LockScreenTransportControlV14.java b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/LockScreenTransportControlV14.java
new file mode 100644
index 0000000..f4dc73e
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/LockScreenTransportControlV14.java
@@ -0,0 +1,234 @@
+// Copyright 2014 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.chrome.browser.media.remote;
+
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.media.AudioManager;
+import android.media.AudioManager.OnAudioFocusChangeListener;
+import android.media.MediaMetadataRetriever;
+import android.os.Build;
+import android.util.Log;
+
+import org.chromium.base.CommandLine;
+import org.chromium.chrome.ChromeSwitches;
+import org.chromium.chrome.browser.media.remote.RemoteVideoInfo.PlayerState;
+
+/**
+ * An implementation of {@link LockScreenTransportControl} targeting platforms with an API of 14 or
+ * above.
+ */
+@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
+// TODO(aberent) This whole class is based upon RemoteControlClient, which is deprecated in
+// the Android L SDK. It, however, still seems to be the only way of controlling the
+// lock screen wallpaper. We need to investigate whether there is an alternative. See
+// LockScreenTransportControl.java.
+@SuppressWarnings("deprecation")
+public class LockScreenTransportControlV14 extends LockScreenTransportControl {
+
+ private static final String TAG = "LockScreenTransportControlV14";
+
+ private static boolean sDebug;
+ private final AudioManager mAudioManager;
+ private final PendingIntent mMediaPendingIntent;
+ private final ComponentName mMediaEventReceiver;
+ private final AudioFocusListener mAudioFocusListener;
+
+ private android.media.RemoteControlClient mRemoteControlClient;
+ private boolean mIsPlaying;
+
+ protected LockScreenTransportControlV14(Context context) {
+ sDebug = CommandLine.getInstance().hasSwitch(ChromeSwitches.ENABLE_CAST_DEBUG_LOGS);
+
+ mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ mMediaEventReceiver = new ComponentName(context.getPackageName(),
+ MediaButtonIntentReceiver.class.getName());
+ Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
+ mediaButtonIntent.setComponent(mMediaEventReceiver);
+ mMediaPendingIntent = PendingIntent.getBroadcast(context.getApplicationContext(), 0,
+ mediaButtonIntent, 0);
+ mAudioFocusListener = new AudioFocusListener();
+ }
+
+ @Override
+ public void onErrorChanged() {
+ if (hasError()) updatePlaybackState(android.media.RemoteControlClient.PLAYSTATE_ERROR);
+ }
+
+ @Override
+ public void onLockScreenPlaybackStateChanged(PlayerState oldState, PlayerState newState) {
+ if (sDebug) Log.d(TAG, "onLockScreenPlaybackStateChanged - new state: " + newState);
+ int playbackState = android.media.RemoteControlClient.PLAYSTATE_STOPPED;
+ boolean shouldBeRegistered = false;
+ if (newState != null) {
+ mIsPlaying = false;
+ shouldBeRegistered = true;
+ switch (newState) {
+ case PAUSED:
+ playbackState = android.media.RemoteControlClient.PLAYSTATE_PAUSED;
+ break;
+ case ERROR:
+ playbackState = android.media.RemoteControlClient.PLAYSTATE_ERROR;
+ break;
+ case PLAYING:
+ playbackState = android.media.RemoteControlClient.PLAYSTATE_PLAYING;
+ mIsPlaying = true;
+ break;
+ case LOADING:
+ playbackState = android.media.RemoteControlClient.PLAYSTATE_BUFFERING;
+ break;
+ default:
+ shouldBeRegistered = false;
+ break;
+ }
+ }
+
+ boolean registered = (mRemoteControlClient != null);
+ if (registered != shouldBeRegistered) {
+ if (shouldBeRegistered) {
+ register();
+ onVideoInfoChanged();
+ onPosterBitmapChanged();
+ } else {
+ unregister();
+ }
+ }
+
+ updatePlaybackState(playbackState);
+ }
+
+ @Override
+ public void onVideoInfoChanged() {
+ if (mRemoteControlClient == null) return;
+
+ RemoteVideoInfo videoInfo = getVideoInfo();
+
+ String title = null;
+ long duration = 0;
+ if (videoInfo != null) {
+ title = videoInfo.title;
+ duration = videoInfo.durationMillis;
+ }
+
+ android.media.RemoteControlClient.MetadataEditor editor = mRemoteControlClient.editMetadata(
+ true);
+ editor.putString(MediaMetadataRetriever.METADATA_KEY_TITLE, title);
+ editor.putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, duration);
+ updateBitmap(editor);
+ editor.apply();
+ }
+
+ @Override
+ public void onPosterBitmapChanged() {
+ if (mRemoteControlClient == null) return;
+
+ android.media.RemoteControlClient.MetadataEditor editor = mRemoteControlClient.editMetadata(
+ false);
+ updateBitmap(editor);
+ editor.apply();
+ }
+
+ private void updateBitmap(android.media.RemoteControlClient.MetadataEditor editor) {
+ // RemoteControlClient likes to recycle bitmaps that have been passed to it through
+ // BITMAP_KEY_ARTWORK. We can't go recycling bitmaps like this since they are also used by
+ // {@link ExpandedControllerActivity} and their life cycle is controller by
+ // {@link RemoteMediaPlayerController}. See crbug.com/356612
+ Bitmap src = getPosterBitmap();
+ Bitmap copy = src != null ? src.copy(src.getConfig(), true) : null;
+ editor.putBitmap(android.media.RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, copy);
+ }
+
+ protected final android.media.RemoteControlClient getRemoteControlClient() {
+ return mRemoteControlClient;
+ }
+
+ protected void register() {
+ if (sDebug) Log.d(TAG, "register called");
+ mRemoteControlClient = new android.media.RemoteControlClient(mMediaPendingIntent);
+ mAudioManager.requestAudioFocus(mAudioFocusListener, AudioManager.USE_DEFAULT_STREAM_TYPE,
+ AudioManager.AUDIOFOCUS_GAIN);
+ mAudioManager.registerMediaButtonEventReceiver(mMediaEventReceiver);
+ mAudioManager.registerRemoteControlClient(mRemoteControlClient);
+ mRemoteControlClient.setTransportControlFlags(getTransportControlFlags());
+ }
+
+ protected void unregister() {
+ if (sDebug) Log.d(TAG, "unregister called");
+ mRemoteControlClient.editMetadata(true).apply();
+ mRemoteControlClient.setTransportControlFlags(0);
+ mAudioManager.abandonAudioFocus(mAudioFocusListener);
+ mAudioManager.unregisterMediaButtonEventReceiver(mMediaEventReceiver);
+ mAudioManager.unregisterRemoteControlClient(mRemoteControlClient);
+ mRemoteControlClient = null;
+ }
+
+ protected void updatePlaybackState(int state) {
+ if (mRemoteControlClient != null) mRemoteControlClient.setPlaybackState(state);
+ }
+
+ protected int getTransportControlFlags() {
+ return android.media.RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE
+ | android.media.RemoteControlClient.FLAG_KEY_MEDIA_STOP;
+ }
+
+ private static class AudioFocusListener implements OnAudioFocusChangeListener {
+ @Override
+ public void onAudioFocusChange(int focusChange) {
+ // Do nothing, the listener is only used to later abandon audio focus.
+ }
+ }
+
+ // TODO(aberent): Investigate moving some or all of the MediaRouterController.Listener
+ // implementations to TransportControl. See http://crbug/354490.
+ @Override
+ public void onRouteSelected(String name, MediaRouteController mediaRouteController) {
+ setScreenName(name);
+ }
+
+ @Override
+ public void onRouteUnselected(MediaRouteController mediaRouteController) {
+ hide();
+ }
+
+ @Override
+ public void onPrepared(MediaRouteController mediaRouteController) {
+ }
+
+ @Override
+ public void onError(int error, String errorMessage) {
+ // Stop the session for all errors
+ hide();
+ }
+
+ @Override
+ public void onDurationUpdated(int durationMillis) {
+ RemoteVideoInfo videoInfo = new RemoteVideoInfo(getVideoInfo());
+ videoInfo.durationMillis = durationMillis;
+ setVideoInfo(videoInfo);
+ }
+
+ @Override
+ public void onPositionChanged(int positionMillis) {
+ RemoteVideoInfo videoInfo = new RemoteVideoInfo(getVideoInfo());
+ videoInfo.currentTimeMillis = positionMillis;
+ setVideoInfo(videoInfo);
+ }
+
+ @Override
+ public void onTitleChanged(String title) {
+ RemoteVideoInfo videoInfo = new RemoteVideoInfo(getVideoInfo());
+ videoInfo.title = title;
+ setVideoInfo(videoInfo);
+ }
+
+ @Override
+ protected boolean isPlaying() {
+ return mIsPlaying;
+ }
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/remote/LockScreenTransportControlV16.java b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/LockScreenTransportControlV16.java
new file mode 100644
index 0000000..f0eff3c
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/LockScreenTransportControlV16.java
@@ -0,0 +1,37 @@
+// Copyright 2014 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.chrome.browser.media.remote;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.support.v7.media.MediaRouter;
+
+/**
+ * An implementation of {@link LockScreenTransportControl} targeting platforms with an API greater
+ * than 15. Extends {@link LockScreenTransportControlV14}, adding support for remote volume control.
+ */
+@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+class LockScreenTransportControlV16 extends LockScreenTransportControlV14 {
+
+ private final MediaRouter mMediaRouter;
+
+ LockScreenTransportControlV16(Context context) {
+ super(context);
+ mMediaRouter = MediaRouter.getInstance(context);
+ }
+
+ @Override
+ protected void register() {
+ super.register();
+ mMediaRouter.addRemoteControlClient(getRemoteControlClient());
+ }
+
+ @Override
+ protected void unregister() {
+ mMediaRouter.removeRemoteControlClient(getRemoteControlClient());
+ super.unregister();
+ }
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/remote/LockScreenTransportControlV18.java b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/LockScreenTransportControlV18.java
new file mode 100644
index 0000000..c435092
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/LockScreenTransportControlV18.java
@@ -0,0 +1,81 @@
+// Copyright 2014 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.chrome.browser.media.remote;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+
+/**
+ * An implementation of {@link LockScreenTransportControl} targeting platforms with an API greater
+ * than 17. Extends {@link LockScreenTransportControlV16}, adding support for seeking.
+ */
+@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
+// This whole class is based upon RemoteControlClient, which is deprecated (and non-functional) in
+// the Android L SDK. Once the L SDK is released we will add code in LockSceenTransportControl to
+// prevent the use of this class on Android L. See {@link LockScreenTransportControl}.
+@SuppressWarnings("deprecation")
+class LockScreenTransportControlV18 extends LockScreenTransportControlV16 {
+
+ private final PlaybackPositionUpdateListener mPlaybackPositionUpdateListener;
+ private final GetPlaybackPositionUpdateListener mGetPlaybackPositionUpdateListener;
+
+ LockScreenTransportControlV18(Context context) {
+ super(context);
+ mPlaybackPositionUpdateListener = new PlaybackPositionUpdateListener();
+ mGetPlaybackPositionUpdateListener = new GetPlaybackPositionUpdateListener();
+ }
+
+ @Override
+ protected void register() {
+ super.register();
+ getRemoteControlClient().setPlaybackPositionUpdateListener(mPlaybackPositionUpdateListener);
+ getRemoteControlClient().setOnGetPlaybackPositionListener(
+ mGetPlaybackPositionUpdateListener);
+ }
+
+ @Override
+ protected void unregister() {
+ getRemoteControlClient().setOnGetPlaybackPositionListener(null);
+ getRemoteControlClient().setPlaybackPositionUpdateListener(null);
+ super.unregister();
+ }
+
+ @Override
+ protected void updatePlaybackState(int state) {
+ RemoteVideoInfo videoInfo = getVideoInfo();
+ if (videoInfo != null && getRemoteControlClient() != null) {
+ getRemoteControlClient().setPlaybackState(state, videoInfo.currentTimeMillis, 1.0f);
+ } else {
+ super.updatePlaybackState(state);
+ }
+ }
+
+ @Override
+ protected int getTransportControlFlags() {
+ return super.getTransportControlFlags()
+ | android.media.RemoteControlClient.FLAG_KEY_MEDIA_POSITION_UPDATE;
+ }
+
+ private class GetPlaybackPositionUpdateListener implements
+ android.media.RemoteControlClient.OnGetPlaybackPositionListener {
+
+ @Override
+ public long onGetPlaybackPosition() {
+ RemoteVideoInfo videoInfo = getVideoInfo();
+ return videoInfo == null ? 0 : videoInfo.currentTimeMillis;
+ }
+ }
+
+ private class PlaybackPositionUpdateListener implements
+ android.media.RemoteControlClient.OnPlaybackPositionUpdateListener {
+
+ @Override
+ public void onPlaybackPositionUpdate(long position) {
+ for (Listener listener : getListeners())
+ listener.onSeek((int) position);
+ }
+ }
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/remote/MediaRouteController.java b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/MediaRouteController.java
new file mode 100644
index 0000000..647b988
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/MediaRouteController.java
@@ -0,0 +1,308 @@
+// Copyright 2014 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.chrome.browser.media.remote;
+
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.support.v7.media.MediaRouteSelector;
+
+import org.chromium.base.VisibleForTesting;
+import org.chromium.chrome.browser.media.remote.RemoteVideoInfo.PlayerState;
+
+/**
+ * Each MediaRouteController controls the routes to devices which support remote playback of
+ * particular categories of Media elements (e.g. all YouTube media elements, all media elements
+ * with simple http source URLs). The MediaRouteController is responsible for configuring
+ * and controlling remote playback of the media elements it supports.
+ */
+public interface MediaRouteController extends TransportControl.Listener {
+ /**
+ * Listener for events that are relevant to the state of the media and the media controls
+ */
+ public interface MediaStateListener {
+ /**
+ * Called when the first route becomes available, or the last route
+ * is removed.
+ * @param available whether routes are available.
+ */
+ public void onRouteAvailabilityChanged(boolean available);
+
+ /**
+ * Called when an error is detected by the media route controller
+ */
+ public void onError();
+
+ /**
+ * Called when a seek completes on the current route
+ */
+ public void onSeekCompleted();
+
+ /**
+ * Called when the current route is ready to be used
+ */
+ public void onPrepared();
+
+ /**
+ * Called when a new route has been selected for Cast
+ * @param name the name of the route
+ */
+ public void onRouteSelected(String name);
+
+ /**
+ * Called when the current route is unselected
+ */
+ public void onRouteUnselected();
+
+ /**
+ * Called when the playback state changes (e.g. from Playing to Paused)
+ * @param newState the new playback state
+ */
+ public void onPlaybackStateChanged(PlayerState newState);
+
+ /**
+ * @return the title of the video
+ */
+ public String getTitle();
+
+ /**
+ * @return the poster bitmap
+ */
+ public Bitmap getPosterBitmap();
+ }
+
+ /**
+ * Listener for events that are relevant to the Browser UI.
+ */
+ public interface UiListener {
+
+ /**
+ * Called when a new route is selected
+ * @param name the name of the new route
+ * @param mediaRouteController the controller that selected the route
+ */
+ void onRouteSelected(String name, MediaRouteController mediaRouteController);
+
+ /**
+ * Called when the current route is unselected
+ * @param mediaRouteController the controller that had the route.
+ */
+ void onRouteUnselected(MediaRouteController mediaRouteController);
+
+ /**
+ * Called when the current route is ready to be used
+ * @param mediaRouteController the controller that has the route.
+ */
+ void onPrepared(MediaRouteController mediaRouteController);
+
+ /**
+ * Called when an error is detected by the controller
+ * @param errorType One of the error types from CastMediaControlIntent
+ * @param message The message for the error
+ */
+ void onError(int errorType, String message);
+
+ /**
+ * Called when the Playback state has changed (e.g. from playing to paused)
+ * @param oldState the old state
+ * @param newState the new state
+ */
+ void onPlaybackStateChanged(PlayerState oldState, PlayerState newState);
+
+ /**
+ * Called when the duration of the currently playing video changes.
+ * @param durationMillis the new duration in ms.
+ */
+ void onDurationUpdated(int durationMillis);
+
+ /**
+ * Called when the media route controller receives new information about the
+ * current position in the video.
+ * @param positionMillis the current position in the video in ms.
+ */
+ void onPositionChanged(int positionMillis);
+
+ /**
+ * Called if the title of the video changes
+ * @param title the new title
+ */
+ void onTitleChanged(String title);
+ }
+
+ /**
+ * Scan routes, and set up the MediaRouter object. This is called at every time we need to reset
+ * the state. Because of that, this function is idempotent. If that changes in the future, where
+ * this function gets called needs to be re-evaluated.
+ *
+ * @return false if device doesn't support cast, true otherwise.
+ */
+ public boolean initialize();
+
+ /**
+ * Can this mediaRouteController handle a media element?
+ * @param sourceUrl the source
+ * @param frameUrl
+ * @return true if it can, false if it can't.
+ */
+ public boolean canPlayMedia(String sourceUrl, String frameUrl);
+
+ /**
+ * @return A new MediaRouteSelector filtering the remote playback devices from all the routes.
+ */
+ public MediaRouteSelector buildMediaRouteSelector();
+
+ /**
+ * @return Whether there're remote playback devices available.
+ */
+ public boolean isRemotePlaybackAvailable();
+
+ /**
+ * @return Whether the currently selected device supports remote playback
+ */
+ public boolean currentRouteSupportsRemotePlayback();
+
+ /**
+ * Checks if we want to reconnect, and if so starts trying to do so. Otherwise clears out the
+ * persistent request to reconnect.
+ */
+ public boolean reconnectAnyExistingRoute();
+
+ /**
+ * Sets the video URL when it becomes known.
+ *
+ * This is the original video URL but if there's URL redirection, it will change as resolved by
+ * {@link MediaUrlResolver}.
+ *
+ * @param uri The video URL.
+ */
+ public void setDataSource(Uri uri, String cookies);
+
+ /**
+ * Setup this object to discover new routes and register the necessary players.
+ */
+ public void prepareMediaRoute();
+
+ /**
+ * Add a Listener that will listen to events from this object
+ *
+ * @param listener the Listener that will receive the events
+ */
+ public void addUiListener(UiListener listener);
+
+ /**
+ * Removes a Listener from this object
+ *
+ * @param listener the Listener to remove
+ */
+ public void removeUiListener(UiListener listener);
+
+ /**
+ * @return The currently selected route's friendly name, or null if there is none selected
+ */
+ public String getRouteName();
+
+ /**
+ * @return true if this is currently using the default route, false if not.
+ */
+ public boolean routeIsDefaultRoute();
+
+ /**
+ * Called to prepare the remote playback asyncronously. onPrepared() of the current remote media
+ * player object is called when the player is ready.
+ *
+ * @param startPositionMillis indicates where in the stream to start playing
+ */
+ public void prepareAsync(String frameUrl, long startPositionMillis);
+
+ /**
+ * Sets the remote volume of the current route.
+ *
+ * @param delta The delta value in arbitrary "Android Volume Units".
+ */
+ public void setRemoteVolume(int delta);
+
+ /**
+ * Resume paused playback of the current video.
+ */
+ public void resume();
+
+ /**
+ * Pauses the currently playing video if any.
+ */
+ public void pause();
+
+ /**
+ * Returns the current remote playback position. Estimates the current position by using the
+ * last known position and the current time.
+ *
+ * TODO(avayvod): Send periodic status update requests to update the position once in several
+ * seconds or so.
+ *
+ * @return The current position of the remote playback in milliseconds.
+ */
+ public int getPosition();
+
+ /**
+ * @return The stream duration in milliseconds.
+ */
+ public int getDuration();
+
+ /**
+ * @return Whether the video is currently being played.
+ */
+ public boolean isPlaying();
+
+ /**
+ * @return Whether the video is being cast (any of playing/paused/loading/stopped).
+ */
+ public boolean isBeingCast();
+
+ /**
+ * Initiates a seek request for the remote playback device to the specified position.
+ *
+ * @param msec The position to seek to, in milliseconds.
+ */
+ public void seekTo(int msec);
+
+ /**
+ * Stop the current remote playback completely and release all resources.
+ */
+ public void release();
+
+ /**
+ * @param player - the current player using this media route controller.
+ */
+ public void setMediaStateListener(MediaStateListener listener);
+
+ /**
+ * @return the current VideoStateListener
+ */
+ public MediaStateListener getMediaStateListener();
+
+ /**
+ * @return true if the video is new
+ */
+ public boolean shouldResetState(MediaStateListener newListener);
+
+ @VisibleForTesting
+ public PlayerState getPlayerState();
+
+ /**
+ * Add a media state listener
+ * @param listener
+ */
+ public void removeMediaStateListener(MediaStateListener listener);
+
+ /**
+ * Remove an existing media state listener
+ * @param listener
+ */
+ public void addMediaStateListener(MediaStateListener listener);
+
+ /**
+ * Get the poster for the video, if any
+ * @return the poster bitmap, or Null.
+ */
+ public Bitmap getPoster();
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/remote/MediaUrlResolver.java b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/MediaUrlResolver.java
new file mode 100644
index 0000000..133b506
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/MediaUrlResolver.java
@@ -0,0 +1,168 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.media.remote;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.text.TextUtils;
+import android.util.Log;
+
+import org.apache.http.Header;
+import org.apache.http.message.BasicHeader;
+import org.chromium.chrome.browser.ChromiumApplication;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.Arrays;
+
+/**
+ * Resolves the final URL if it's a redirect. Works asynchronously, uses HTTP
+ * HEAD request to determine if the URL is redirected.
+ */
+public class MediaUrlResolver extends AsyncTask<Void, Void, MediaUrlResolver.Result> {
+
+ /**
+ * The interface to get the initial URI with cookies from and pass the final
+ * URI to.
+ */
+ public interface Delegate {
+ /**
+ * @return the original URL to resolve.
+ */
+ Uri getUri();
+
+ /**
+ * @return the cookies to fetch the URL with.
+ */
+ String getCookies();
+
+ /**
+ * Passes the resolved URL to the delegate.
+ *
+ * @param uri the resolved URL.
+ */
+ void setUri(Uri uri, Header[] headers);
+ }
+
+
+ protected static final class Result {
+ private final String mUri;
+ private final Header[] mRelevantHeaders;
+
+ public Result(String uri, Header[] relevantHeaders) {
+ mUri = uri;
+ mRelevantHeaders =
+ relevantHeaders != null
+ ? Arrays.copyOf(relevantHeaders, relevantHeaders.length)
+ : null;
+ }
+
+ public String getUri() {
+ return mUri;
+ }
+
+ public Header[] getRelevantHeaders() {
+ return mRelevantHeaders != null
+ ? Arrays.copyOf(mRelevantHeaders, mRelevantHeaders.length)
+ : null;
+ }
+ }
+
+ private static final String TAG = "MediaUrlResolver";
+
+ private static final String CORS_HEADER_NAME = "Access-Control-Allow-Origin";
+ private static final String COOKIES_HEADER_NAME = "Cookies";
+ private static final String USER_AGENT_HEADER_NAME = "User-Agent";
+ private static final String RANGE_HEADER_NAME = "Range";
+
+ // We don't want to necessarily fetch the whole video but we don't want to miss the CORS header.
+ // Assume that 64k should be more than enough to keep all the headers.
+ private static final String RANGE_HEADER_VALUE = "bytes: 0-65536";
+
+ private final Context mContext;
+ private final Delegate mDelegate;
+
+ /**
+ * The constructor
+ * @param context the context to use to resolve the URL
+ * @param delegate The customer for this URL resolver.
+ */
+ public MediaUrlResolver(Context context, Delegate delegate) {
+ mContext = context;
+ mDelegate = delegate;
+ }
+
+ @Override
+ protected MediaUrlResolver.Result doInBackground(Void... params) {
+ Uri uri = mDelegate.getUri();
+ String url = uri.toString();
+ Header[] relevantHeaders = null;
+ String cookies = mDelegate.getCookies();
+ String userAgent = ChromiumApplication.getBrowserUserAgent();
+ // URL may already be partially percent encoded; double percent encoding will break
+ // things, so decode it before sanitizing it.
+ String sanitizedUrl = sanitizeUrl(Uri.decode(url));
+
+ // If we failed to sanitize the URL (e.g. because the host name contains underscores) then
+ // HttpURLConnection won't work,so we can't follow redirections. Just try to use it as is.
+ // TODO (aberent): Find out if there is a way of following redirections that is not so
+ // strict on the URL format.
+ if (!sanitizedUrl.equals("")) {
+ HttpURLConnection urlConnection = null;
+ try {
+ URL requestUrl = new URL(sanitizedUrl);
+ urlConnection = (HttpURLConnection) requestUrl.openConnection();
+ if (!TextUtils.isEmpty(cookies)) {
+ urlConnection.setRequestProperty(COOKIES_HEADER_NAME, cookies);
+ }
+ urlConnection.setRequestProperty(USER_AGENT_HEADER_NAME, userAgent);
+ urlConnection.setRequestProperty(RANGE_HEADER_NAME, RANGE_HEADER_VALUE);
+
+ // This triggers resolving the URL and receiving the headers.
+ urlConnection.getHeaderFields();
+
+ url = urlConnection.getURL().toString();
+ String corsHeader = urlConnection.getHeaderField(CORS_HEADER_NAME);
+ if (corsHeader != null) {
+ relevantHeaders = new Header[1];
+ relevantHeaders[0] = new BasicHeader(CORS_HEADER_NAME, corsHeader);
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to fetch the final URI", e);
+ url = "";
+ }
+ if (urlConnection != null) urlConnection.disconnect();
+ }
+ return new MediaUrlResolver.Result(url, relevantHeaders);
+ }
+
+ @Override
+ protected void onPostExecute(MediaUrlResolver.Result result) {
+ String url = result.getUri();
+ Uri uri = "".equals(url) ? Uri.EMPTY : Uri.parse(url);
+ mDelegate.setUri(uri, result.getRelevantHeaders());
+ }
+
+ private String sanitizeUrl(String unsafeUrl) {
+ URL url;
+ URI uri;
+ try {
+ url = new URL(unsafeUrl);
+ uri = new URI(url.getProtocol(), url.getUserInfo(), url.getHost(), url.getPort(),
+ url.getPath(), url.getQuery(), url.getRef());
+ return uri.toURL().toString();
+ } catch (URISyntaxException syntaxException) {
+ Log.w(TAG, "URISyntaxException " + syntaxException);
+ } catch (MalformedURLException malformedUrlException) {
+ Log.w(TAG, "MalformedURLException " + malformedUrlException);
+ }
+ return "";
+ }
+} \ No newline at end of file
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/remote/NotificationTransportControl.java b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/NotificationTransportControl.java
new file mode 100644
index 0000000..3ce2f52
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/NotificationTransportControl.java
@@ -0,0 +1,629 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.media.remote;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.os.Handler;
+import android.os.IBinder;
+import android.support.v4.app.NotificationCompat;
+import android.util.DisplayMetrics;
+import android.view.View;
+import android.widget.RemoteViews;
+
+import org.chromium.base.ApiCompatibilityUtils;
+import org.chromium.base.VisibleForTesting;
+import org.chromium.chrome.R;
+import org.chromium.chrome.browser.media.remote.RemoteVideoInfo.PlayerState;
+
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * A class for notifications that provide information and optional transport controls for a given
+ * remote control. Internally implements a Service for transforming notification Intents into
+ * {@link TransportControl.Listener} calls for all registered listeners.
+ *
+ */
+public class NotificationTransportControl
+ extends TransportControl implements MediaRouteController.UiListener {
+ /**
+ * Service used to transform intent requests triggered from the notification into
+ * {@code Listener} callbacks. Ideally this class should be protected, but public is required to
+ * create as a service.
+ */
+ public static class ListenerService extends Service {
+ private static final String ACTION_PREFIX = ListenerService.class.getName() + ".";
+
+ // Constants used by intent actions
+ public static final int ACTION_ID_PLAY = 0;
+ public static final int ACTION_ID_PAUSE = 1;
+ public static final int ACTION_ID_SEEK = 2;
+ public static final int ACTION_ID_STOP = 3;
+ public static final int ACTION_ID_SELECT = 4;
+
+ // Intent parameters
+ public static final String SEEK_POSITION = "SEEK_POSITION";
+
+ // Must be kept in sync with the ACTION_ID_XXX constants above
+ private static final String[] ACTION_VERBS = {"PLAY", "PAUSE", "SEEK", "STOP", "SELECT" };
+
+ private PendingIntent[] mPendingIntents;
+
+ @VisibleForTesting
+ PendingIntent getPendingIntent(int id) {
+ return mPendingIntents[id];
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ // Create all the PendingIntents
+ int actionCount = ACTION_VERBS.length;
+ mPendingIntents = new PendingIntent[actionCount];
+ for (int i = 0; i < actionCount; ++i) {
+ Intent intent = new Intent(this, ListenerService.class);
+ intent.setAction(ACTION_PREFIX + ACTION_VERBS[i]);
+ mPendingIntents[i] = PendingIntent.getService(this, 0, intent,
+ PendingIntent.FLAG_CANCEL_CURRENT);
+ }
+ if (sInstance == null) {
+ // This can only happen if we have been recreated by the OS after Chrome has died.
+ // In this case we need to create a MediaRouteController so that we can reconnect
+ // to the Chromecast.
+ RemoteMediaPlayerController playerController =
+ RemoteMediaPlayerController.instance();
+ playerController.createMediaRouteControllers(this);
+ for (MediaRouteController routeController :
+ playerController.getMediaRouteControllers()) {
+ routeController.initialize();
+ if (routeController.reconnectAnyExistingRoute()) {
+ playerController.setCurrentMediaRouteController(routeController);
+ routeController.prepareMediaRoute();
+ NotificationTransportControl.getOrCreate(this, routeController);
+ sInstance.addListener(routeController);
+ break;
+ }
+ }
+ if (sInstance == null) {
+ // No controller wants to reconnect, so we haven't created a notification.
+ return;
+ }
+ }
+ onServiceStarted(this);
+ }
+
+ @Override
+ public void onDestroy() {
+ onServiceDestroyed();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (sInstance == null) {
+ // This can only happen after a restart where none of the controllers
+ // wanted to reconnect.
+ stopSelf();
+ return START_NOT_STICKY;
+ }
+ if (intent != null) {
+ String action = intent.getAction();
+ if (action != null && action.startsWith(ACTION_PREFIX)) {
+ Set<Listener> listeners = sInstance.getListeners();
+
+ // Strip the prefix for matching the verb
+ action = action.substring(ACTION_PREFIX.length());
+ if (ACTION_VERBS[ACTION_ID_PLAY].equals(action)) {
+ for (Listener listener : listeners) {
+ listener.onPlay();
+ }
+ } else if (ACTION_VERBS[ACTION_ID_PAUSE].equals(action)) {
+ for (Listener listener : listeners) {
+ listener.onPause();
+ }
+ } else if (ACTION_VERBS[ACTION_ID_SEEK].equals(action)) {
+ int seekPosition = intent.getIntExtra(SEEK_POSITION, 0);
+ for (Listener listener : listeners) {
+ listener.onSeek(seekPosition);
+ }
+ } else if (ACTION_VERBS[ACTION_ID_STOP].equals(action)) {
+ for (Listener listener : listeners) {
+ listener.onStop();
+ stopSelf();
+ }
+ } else if (ACTION_VERBS[ACTION_ID_SELECT].equals(action)) {
+ for (Listener listener : listeners) {
+ ExpandedControllerActivity.startActivity(this);
+ }
+ }
+ }
+ }
+
+ return START_STICKY;
+ }
+ }
+
+ private static NotificationTransportControl sInstance = null;
+ private static final Object LOCK = new Object();
+ private static final int MSG_UPDATE_NOTIFICATION = 100;
+
+ private static final int MINIMUM_PROGRESS_UPDATE_INTERVAL_MS = 1000;
+
+ /**
+ * Returns the singleton NotificationTransportControl object.
+ *
+ * @param context The Context that the notification service needs to be created in.
+ * @param mrc The MediaRouteController object to use.
+ * @return A {@code NotificationTransportControl} object that uses the given
+ * MediaRouteController object.
+ */
+ public static NotificationTransportControl getOrCreate(Context context,
+ @Nullable MediaRouteController mrc) {
+ synchronized (LOCK) {
+ if (sInstance == null) {
+ sInstance = new NotificationTransportControl(context);
+ sInstance.setVideoInfo(
+ new RemoteVideoInfo(null, 0, RemoteVideoInfo.PlayerState.STOPPED, 0, null));
+ }
+
+ sInstance.setMediaRouteController(mrc);
+ return sInstance;
+ }
+ }
+
+ @VisibleForTesting
+ static NotificationTransportControl getIfExists() {
+ return sInstance;
+ }
+
+ /**
+ * Ensures the truth of an expression involving the state of the calling instance, but not
+ * involving any parameters to the calling method.
+ *
+ * @param expression a boolean expression
+ * @throws IllegalStateException if {@code expression} is false
+ */
+ private static void checkState(boolean expression) {
+ if (!expression) {
+ throw new IllegalStateException();
+ }
+ }
+
+ private static void onServiceDestroyed() {
+ if (sInstance == null) return;
+ checkState(sInstance.mService != null);
+ sInstance.destroyNotification();
+ sInstance.mService = null;
+ }
+
+ private static void onServiceStarted(ListenerService service) {
+ checkState(sInstance != null);
+ checkState(sInstance.mService == null);
+ sInstance.mService = service;
+ sInstance.createNotification();
+ }
+
+ /**
+ * Scale the specified bitmap to the desired with and height while preserving aspect ratio.
+ */
+ private static Bitmap scaleBitmap(Bitmap bitmap, int maxWidth, int maxHeight) {
+ if (bitmap == null) {
+ return null;
+ }
+
+ float scaleX = 1.0f;
+ float scaleY = 1.0f;
+ if (bitmap.getWidth() > maxWidth) {
+ scaleX = maxWidth / (float) bitmap.getWidth();
+ }
+ if (bitmap.getHeight() > maxHeight) {
+ scaleY = maxHeight / (float) bitmap.getHeight();
+ }
+ float scale = Math.min(scaleX, scaleY);
+ int width = (int) (bitmap.getWidth() * scale);
+ int height = (int) (bitmap.getHeight() * scale);
+ return Bitmap.createScaledBitmap(bitmap, width, height, false);
+ }
+
+ private final Context mContext;
+ private MediaRouteController mMediaRouteController;
+
+ // ListenerService running for the notification. Only non-null when showing.
+ private ListenerService mService;
+
+ private final String mPlayDescription;
+
+ private final String mPauseDescription;
+
+ private Notification mNotification;
+
+ private Bitmap mIcon;
+
+ private Handler mHandler;
+
+ private int mProgressUpdateInterval = MINIMUM_PROGRESS_UPDATE_INTERVAL_MS;
+
+ private NotificationTransportControl(Context context) {
+ this.mContext = context;
+ mHandler = new Handler(context.getMainLooper()) {
+ @Override
+ public void handleMessage(android.os.Message msg) {
+ if (msg.what == MSG_UPDATE_NOTIFICATION) {
+ mHandler.removeMessages(MSG_UPDATE_NOTIFICATION); // Only one update is needed.
+ updateNotificationInternal();
+ }
+ }
+ };
+
+ mPlayDescription = context.getResources().getString(R.string.accessibility_play);
+ mPauseDescription = context.getResources().getString(R.string.accessibility_pause);
+ }
+
+ @Override
+ public void hide() {
+ mContext.stopService(new Intent(mContext, ListenerService.class));
+ }
+
+ /**
+ * @return true if the notification is currently visible to the user.
+ */
+ public boolean isShowing() {
+ return mService != null;
+ }
+
+ @Override
+ public void onDurationUpdated(int durationMillis) {
+ RemoteVideoInfo videoInfo = new RemoteVideoInfo(getVideoInfo());
+ videoInfo.durationMillis = durationMillis;
+ setVideoInfo(videoInfo);
+
+ // Set the progress update interval based on the screen height/width, since there's no point
+ // in updating the progress bar more frequently than what the user can see.
+ // getDisplayMetrics() is dependent on the current orientation, so we need to get the max
+ // of both height and width so that both portrait and landscape notifications are covered.
+ DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();
+ float density = metrics.density;
+ float dpHeight = metrics.heightPixels / density;
+ float dpWidth = metrics.widthPixels / density;
+
+ float maxDimen = Math.max(dpHeight, dpWidth);
+
+ mProgressUpdateInterval = Math.max(MINIMUM_PROGRESS_UPDATE_INTERVAL_MS,
+ Math.round(durationMillis / maxDimen));
+ }
+
+ @Override
+ public void onError(int error, String errorMessage) {
+ // Stop the session for all errors
+ hide();
+ }
+
+ @Override
+ public void onPlaybackStateChanged(PlayerState oldState, PlayerState newState) {
+ RemoteVideoInfo videoInfo = new RemoteVideoInfo(getVideoInfo());
+ videoInfo.state = newState;
+ setVideoInfo(videoInfo);
+
+ if (newState == oldState) return;
+
+ if (newState == PlayerState.PLAYING || newState == PlayerState.LOADING
+ || newState == PlayerState.PAUSED) {
+ show(newState);
+ if (newState == PlayerState.PLAYING) {
+ // If we transitioned from not playing to playing, start monitoring the playback.
+ monitorProgress();
+ }
+ } else if (isShowing()
+ && (newState == PlayerState.FINISHED || newState == PlayerState.INVALIDATED)) {
+ // If we are switching to a finished state, stop the notifications.
+ hide();
+ }
+ }
+
+ @Override
+ public void onPositionChanged(int positionMillis) {
+ RemoteVideoInfo videoInfo = new RemoteVideoInfo(getVideoInfo());
+ videoInfo.currentTimeMillis = positionMillis;
+ setVideoInfo(videoInfo);
+ }
+
+ @Override
+ public void onPrepared(MediaRouteController mediaRouteController) {
+ show(PlayerState.PLAYING);
+ }
+
+ @Override
+ public void onRouteSelected(String name, MediaRouteController mediaRouteController) {
+ setScreenName(name);
+ }
+
+ @Override
+ public void onRouteUnselected(MediaRouteController mediaRouteController) {
+ hide();
+ }
+
+ @Override
+ public void onTitleChanged(String title) {
+ RemoteVideoInfo videoInfo = new RemoteVideoInfo(getVideoInfo());
+ videoInfo.title = title;
+ setVideoInfo(videoInfo);
+ }
+
+ @Override
+ public void setRouteController(MediaRouteController controller) {
+ setMediaRouteController(controller);
+ }
+
+ @Override
+ public void show(PlayerState initialState) {
+ mMediaRouteController.addUiListener(this);
+ RemoteVideoInfo newVideoInfo = new RemoteVideoInfo(mVideoInfo);
+ newVideoInfo.state = initialState;
+ setVideoInfo(newVideoInfo);
+ mContext.startService(new Intent(mContext, ListenerService.class));
+
+ if (initialState == PlayerState.PLAYING) {
+ monitorProgress();
+ }
+ }
+
+ private void updateNotification() {
+ checkState(mNotification != null);
+
+ // Defer the call to updateNotificationInternal() so it can be cancelled in
+ // destroyNotification(). This is done to avoid the OS bug b/8798662.
+ mHandler.sendEmptyMessage(MSG_UPDATE_NOTIFICATION);
+ }
+
+ @VisibleForTesting
+ Notification getNotification() {
+ return mNotification;
+ }
+
+ @VisibleForTesting
+ final ListenerService getService() {
+ return mService;
+ }
+
+ @Override
+ protected void onErrorChanged() {
+ if (isShowing()) {
+ updateNotification();
+ }
+ }
+
+ @Override
+ protected void onPosterBitmapChanged() {
+ Bitmap posterBitmap = getPosterBitmap();
+ mIcon = scaleBitmapForIcon(posterBitmap);
+ super.onPosterBitmapChanged();
+ }
+
+ @Override
+ protected void onScreenNameChanged() {
+ if (isShowing()) {
+ updateNotification();
+ }
+ }
+
+ @Override
+ protected void onVideoInfoChanged() {
+ if (isShowing()) {
+ updateNotification();
+ }
+ }
+
+ private RemoteViews createContentView() {
+ RemoteViews contentView =
+ new RemoteViews(getContext().getPackageName(), R.layout.remote_notification_bar);
+ contentView.setOnClickPendingIntent(R.id.stop,
+ getService().getPendingIntent(ListenerService.ACTION_ID_STOP));
+
+ return contentView;
+ }
+
+ private void createNotification() {
+ checkState(mNotification == null);
+
+ NotificationCompat.Builder notificationBuilder =
+ new NotificationCompat.Builder(getContext())
+ .setDefaults(0)
+ .setSmallIcon(R.drawable.ic_notification_media_route)
+ .setLocalOnly(true)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .setOnlyAlertOnce(true)
+ .setOngoing(true)
+ .setContent(createContentView())
+ .setContentIntent(getService().getPendingIntent(ListenerService.ACTION_ID_SELECT))
+ .setDeleteIntent(getService().getPendingIntent(ListenerService.ACTION_ID_STOP));
+ mNotification = notificationBuilder.build();
+ updateNotification();
+ }
+
+ private void destroyNotification() {
+ checkState(mNotification != null);
+
+ // Cancel any pending updates - we're about to tear down the notification.
+ mHandler.removeMessages(MSG_UPDATE_NOTIFICATION);
+
+ NotificationManager manager =
+ (NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE);
+ manager.cancel(R.id.remote_notification);
+ mNotification = null;
+ }
+
+ private final Context getContext() {
+ return mContext;
+ }
+
+ private String getStatus() {
+ Context context = getContext();
+ RemoteVideoInfo videoInfo = getVideoInfo();
+ String videoTitle = videoInfo != null ? videoInfo.title : null;
+ if (hasError()) {
+ return getError();
+ } else if (videoInfo != null) {
+ switch (videoInfo.state) {
+ case PLAYING:
+ return videoTitle != null ? context.getString(
+ R.string.cast_notification_playing_for_video, videoTitle)
+ : context.getString(R.string.cast_notification_playing);
+ case LOADING:
+ return videoTitle != null ? context.getString(
+ R.string.cast_notification_loading_for_video, videoTitle)
+ : context.getString(R.string.cast_notification_loading);
+ case PAUSED:
+ return videoTitle != null ? context.getString(
+ R.string.cast_notification_paused_for_video, videoTitle)
+ : context.getString(R.string.cast_notification_paused);
+ case STOPPED:
+ return context.getString(R.string.cast_notification_stopped);
+ case FINISHED:
+ case INVALIDATED:
+ return videoTitle != null ? context.getString(
+ R.string.cast_notification_finished_for_video, videoTitle)
+ : context.getString(R.string.cast_notification_finished);
+ case ERROR:
+ default:
+ return videoInfo.errorMessage;
+ }
+ } else {
+ return ""; // TODO(bclayton): Is there something better to display here?
+ }
+ }
+
+ private String getTitle() {
+ return getScreenName();
+ }
+
+ private void monitorProgress() {
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ onPositionChanged(mMediaRouteController.getPosition());
+ if (mMediaRouteController.isPlaying()) {
+ mHandler.postDelayed(this, mProgressUpdateInterval);
+ }
+ }
+ }, mProgressUpdateInterval);
+
+ }
+
+ /**
+ * Scale the specified bitmap to properly fit as a notification icon. If the argument is null
+ * the function returns null.
+ */
+ private Bitmap scaleBitmapForIcon(Bitmap bitmap) {
+ Resources res = getContext().getResources();
+ float maxWidth = res.getDimension(R.dimen.remote_notification_logo_max_width);
+ float maxHeight = res.getDimension(R.dimen.remote_notification_logo_max_height);
+ return scaleBitmap(bitmap, (int) maxWidth, (int) maxHeight);
+ }
+
+ /**
+ * Sets the MediaRouteController the notification should be using to get the data from.
+ *
+ * @param mrc the MediaRouteController object to use. If null, the previous MediaRouteController
+ * object will not be overwritten.
+ */
+ private void setMediaRouteController(@Nullable MediaRouteController mrc) {
+ if (mrc == null || mMediaRouteController == mrc) return;
+
+ if (mMediaRouteController != null) {
+ mMediaRouteController.removeUiListener(this);
+ }
+
+ mMediaRouteController = mrc;
+ mMediaRouteController.addUiListener(this);
+ }
+
+ private void updateNotificationInternal() {
+ checkState(mNotification != null);
+
+ RemoteViews contentView = createContentView();
+
+ contentView.setTextViewText(R.id.title, getTitle());
+ contentView.setTextViewText(R.id.status, getStatus());
+ if (mIcon != null) {
+ contentView.setImageViewBitmap(R.id.icon, mIcon);
+ } else {
+ contentView.setImageViewResource(R.id.icon, R.drawable.ic_notification_media_route);
+ }
+
+ RemoteVideoInfo videoInfo = getVideoInfo();
+ if (videoInfo != null) {
+ boolean showPlayPause = false;
+ boolean showProgress = false;
+ switch (videoInfo.state) {
+ case PLAYING:
+ showProgress = true;
+ showPlayPause = true;
+ contentView.setProgressBar(R.id.progress, videoInfo.durationMillis,
+ videoInfo.currentTimeMillis, false);
+ contentView.setImageViewResource(R.id.playpause,
+ R.drawable.ic_vidcontrol_pause);
+ ApiCompatibilityUtils.setContentDescriptionForRemoteView(contentView,
+ R.id.playpause, mPauseDescription);
+ contentView.setOnClickPendingIntent(R.id.playpause,
+ getService().getPendingIntent(ListenerService.ACTION_ID_PAUSE));
+ break;
+ case PAUSED:
+ showProgress = true;
+ showPlayPause = true;
+ contentView.setProgressBar(R.id.progress, videoInfo.durationMillis,
+ videoInfo.currentTimeMillis, false);
+ contentView.setImageViewResource(R.id.playpause, R.drawable.ic_vidcontrol_play);
+ ApiCompatibilityUtils.setContentDescriptionForRemoteView(contentView,
+ R.id.playpause, mPlayDescription);
+ contentView.setOnClickPendingIntent(R.id.playpause,
+ getService().getPendingIntent(ListenerService.ACTION_ID_PLAY));
+ break;
+ case LOADING:
+ showProgress = true;
+ contentView.setProgressBar(R.id.progress, 0, 0, true);
+ break;
+ case ERROR:
+ showProgress = true;
+ break;
+ default:
+ break;
+ }
+
+ contentView.setViewVisibility(R.id.playpause,
+ showPlayPause ? View.VISIBLE : View.INVISIBLE);
+ // We use GONE instead of INVISIBLE for this because the notification looks funny with
+ // a large gap in the middle if we have no duration. Setting it to GONE forces the
+ // layout to squeeze tighter to the middle.
+ contentView.setViewVisibility(R.id.progress,
+ (showProgress && videoInfo.durationMillis > 0) ? View.VISIBLE : View.GONE);
+ contentView.setViewVisibility(R.id.stop, showPlayPause ? View.VISIBLE : View.INVISIBLE);
+
+ mNotification.contentView = contentView;
+
+ NotificationManager manager = (NotificationManager) getContext().getSystemService(
+ Context.NOTIFICATION_SERVICE);
+ manager.notify(R.id.remote_notification, mNotification);
+
+ if (videoInfo.state == PlayerState.STOPPED || videoInfo.state == PlayerState.FINISHED) {
+ getService().stopSelf();
+ } else {
+ getService().startForeground(R.id.remote_notification, mNotification);
+ }
+ }
+ }
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/remote/RecordCastAction.java b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/RecordCastAction.java
new file mode 100644
index 0000000..7ee6c05
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/RecordCastAction.java
@@ -0,0 +1,90 @@
+// Copyright 2014 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.chrome.browser.media.remote;
+
+import org.chromium.base.JNINamespace;
+import org.chromium.base.library_loader.LibraryLoader;
+
+/**
+ * Record statistics on interesting cast events and actions.
+ */
+@JNINamespace("remote_media")
+public class RecordCastAction {
+
+ // UMA histogram values for the device types the user could select.
+ // Keep in sync with the enum in uma_record_action.cc
+ public static final int DEVICE_TYPE_CAST_GENERIC = 0;
+ public static final int DEVICE_TYPE_CAST_YOUTUBE = 1;
+ public static final int DEVICE_TYPE_NON_CAST_YOUTUBE = 2;
+ public static final int DEVICE_TYPE_COUNT = 3;
+
+ /**
+ * Record the type of cast receiver we to which we are casting.
+ * @param playerType the type of cast receiver.
+ */
+ public static void remotePlaybackDeviceSelected(int playerType) {
+ assert playerType >= 0
+ && playerType < RecordCastAction.DEVICE_TYPE_COUNT;
+ if (LibraryLoader.isInitialized()) nativeRecordRemotePlaybackDeviceSelected(playerType);
+ }
+
+ /**
+ * Record that a remote playback was requested. This is intended to record all playback
+ * requests, whether they were user initiated or was an auto-playback resulting from the user
+ * selecting the device initially.
+ */
+ public static void castPlayRequested() {
+ if (LibraryLoader.isInitialized()) nativeRecordCastPlayRequested();
+ }
+
+ /**
+ * Record the result of the cast playback request.
+ *
+ * @param castSucceeded true if the playback succeeded, false if there was an error
+ */
+ public static void castDefaultPlayerResult(boolean castSucceeded) {
+ if (LibraryLoader.isInitialized()) nativeRecordCastDefaultPlayerResult(castSucceeded);
+ }
+
+ /**
+ * Record the result of casting a YouTube video.
+ *
+ * @param castSucceeded true if the playback succeeded, false if there was an error
+ */
+ public static void castYouTubePlayerResult(boolean castSucceeded) {
+ if (LibraryLoader.isInitialized()) nativeRecordCastYouTubePlayerResult(castSucceeded);
+ }
+
+ /**
+ * Record the amount of time remaining on the video when the remote playback stops.
+ *
+ * @param videoLengthMs the total length of the video in milliseconds
+ * @param timeRemainingMs the remaining time in the video in milliseconds
+ */
+ public static void castEndedTimeRemaining(int videoLengthMs, int timeRemainingMs) {
+ if (LibraryLoader.isInitialized()) {
+ nativeRecordCastEndedTimeRemaining(videoLengthMs, timeRemainingMs);
+ }
+ }
+
+ /**
+ * Record the type of the media being cast.
+ *
+ * @param mediaType the type of the media being casted, see media/base/container_names.h for
+ * possible media types.
+ */
+ public static void castMediaType(int mediaType) {
+ if (LibraryLoader.isInitialized()) nativeRecordCastMediaType(mediaType);
+ }
+
+ // Cast sending
+ private static native void nativeRecordRemotePlaybackDeviceSelected(int deviceType);
+ private static native void nativeRecordCastPlayRequested();
+ private static native void nativeRecordCastDefaultPlayerResult(boolean castSucceeded);
+ private static native void nativeRecordCastYouTubePlayerResult(boolean castSucceeded);
+ private static native void nativeRecordCastEndedTimeRemaining(
+ int videoLengthMs, int timeRemainingMs);
+ private static native void nativeRecordCastMediaType(int mediaType);
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/remote/RemoteMediaPlayerBridge.java b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/RemoteMediaPlayerBridge.java
new file mode 100644
index 0000000..8367df7
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/RemoteMediaPlayerBridge.java
@@ -0,0 +1,344 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.media.remote;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.Build;
+import android.util.Log;
+
+import org.chromium.base.CalledByNative;
+import org.chromium.base.CommandLine;
+import org.chromium.base.JNINamespace;
+import org.chromium.chrome.ChromeSwitches;
+import org.chromium.chrome.browser.media.remote.RemoteVideoInfo.PlayerState;
+import org.chromium.media.MediaPlayerBridge;
+
+/**
+ * Acts as a proxy between the remotely playing video and the HTMLMediaElement.
+ *
+ * Note that the only reason this derives from MediaPlayerBridge is that the
+ * MediaPlayerListener takes a MediaPlayerBridge in its constructor.
+ * TODO(aberent) fix this by creating a MediaPlayerBridgeInterface (or similar).
+ */
+@JNINamespace("remote_media")
+public class RemoteMediaPlayerBridge extends MediaPlayerBridge {
+ private final long mStartPositionMillis;
+ private long mNativeRemoteMediaPlayerBridge;
+
+ private MediaPlayer.OnCompletionListener mOnCompletionListener;
+ private MediaPlayer.OnSeekCompleteListener mOnSeekCompleteListener;
+ private MediaPlayer.OnErrorListener mOnErrorListener;
+ private MediaPlayer.OnPreparedListener mOnPreparedListener;
+
+ private final MediaRouteController mRouteController;
+ private final String mSourceUrl;
+ private final String mFrameUrl;
+ private final boolean mDebug;
+ private Bitmap mPosterBitmap;
+
+ // mActive is true when the Chrome is playing, or preparing to play, this player's video
+ // remotely.
+ private boolean mActive = false;
+
+ private static final String TAG = "RemoteMediaPlayerBridge";
+
+ private MediaRouteController.MediaStateListener mMediaStateListener =
+ new MediaRouteController.MediaStateListener() {
+ @Override
+ public void onRouteAvailabilityChanged(boolean available) {
+ if (mNativeRemoteMediaPlayerBridge == 0) return;
+ nativeOnRouteAvailabilityChanged(mNativeRemoteMediaPlayerBridge, available);
+ }
+
+ @Override
+ public void onError() {
+ if (mActive && mOnErrorListener != null) {
+ @SuppressLint("InlinedApi")
+ int errorExtra = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1
+ ? MediaPlayer.MEDIA_ERROR_TIMED_OUT
+ : 0;
+ mOnErrorListener.onError(null, MediaPlayer.MEDIA_ERROR_UNKNOWN, errorExtra);
+ }
+ }
+
+ @Override
+ public void onSeekCompleted() {
+ if (mActive && mOnSeekCompleteListener != null) {
+ mOnSeekCompleteListener.onSeekComplete(null);
+ }
+ }
+
+ @Override
+ public void onPrepared() {
+ if (mActive && mOnPreparedListener != null) {
+ mOnPreparedListener.onPrepared(null);
+ }
+ }
+
+ @Override
+ public void onRouteSelected(String name) {
+ if (mNativeRemoteMediaPlayerBridge == 0) return;
+ nativeOnRouteSelected(mNativeRemoteMediaPlayerBridge,
+ RemoteMediaPlayerController.instance().getCastingMessage(name));
+ }
+
+ @Override
+ public void onRouteUnselected() {
+ if (mNativeRemoteMediaPlayerBridge == 0) return;
+ nativeOnRouteUnselected(mNativeRemoteMediaPlayerBridge);
+ }
+
+ @Override
+ public void onPlaybackStateChanged(PlayerState newState) {
+ if (mNativeRemoteMediaPlayerBridge == 0) return;
+ if (newState == PlayerState.FINISHED || newState == PlayerState.INVALIDATED) {
+ onCompleted();
+ nativeOnPlaybackFinished(mNativeRemoteMediaPlayerBridge);
+ } else if (newState == PlayerState.PLAYING) {
+ nativeOnPlaying(mNativeRemoteMediaPlayerBridge);
+ } else if (newState == PlayerState.PAUSED) {
+ nativeOnPaused(mNativeRemoteMediaPlayerBridge);
+ }
+ }
+
+ @Override
+ public String getTitle() {
+ if (mNativeRemoteMediaPlayerBridge == 0) return null;
+ return nativeGetTitle(mNativeRemoteMediaPlayerBridge);
+ }
+
+ @Override
+ public Bitmap getPosterBitmap() {
+ return mPosterBitmap;
+ }
+ };
+
+ private RemoteMediaPlayerBridge(long nativeRemoteMediaPlayerBridge, long startPositionMillis,
+ String sourceUrl, String frameUrl) {
+
+ mDebug = CommandLine.getInstance().hasSwitch(ChromeSwitches.ENABLE_CAST_DEBUG_LOGS);
+
+ if (mDebug) Log.i(TAG, "Creating RemoteMediaPlayerBridge");
+ mNativeRemoteMediaPlayerBridge = nativeRemoteMediaPlayerBridge;
+ mStartPositionMillis = startPositionMillis;
+ mSourceUrl = sourceUrl;
+ mFrameUrl = frameUrl;
+ // This will get null if there isn't a mediaRouteController that can play this media.
+ mRouteController = RemoteMediaPlayerController.instance()
+ .getMediaRouteController(sourceUrl, frameUrl);
+ }
+
+ @CalledByNative
+ private static RemoteMediaPlayerBridge create(long nativeRemoteMediaPlayerBridge,
+ long startPositionMillis, String sourceUrl, String frameUrl) {
+ return new RemoteMediaPlayerBridge(nativeRemoteMediaPlayerBridge, startPositionMillis,
+ sourceUrl, frameUrl);
+ }
+
+ /**
+ * Called when a lower layer requests that a video be cast. This will typically be a request
+ * from Blink when the cast button is pressed on the default video controls.
+ */
+ @CalledByNative
+ private void requestRemotePlayback() {
+ if (mDebug) Log.i(TAG, "requestRemotePlayback");
+ RemoteMediaPlayerController.instance().requestRemotePlayback(
+ mMediaStateListener, mRouteController);
+ }
+
+ /**
+ * Called when a lower layer requests control of a video that is being cast.
+ */
+ @CalledByNative
+ private void requestRemotePlaybackControl() {
+ if (mDebug) Log.i(TAG, "requestRemotePlaybackControl");
+ RemoteMediaPlayerController.instance().requestRemotePlaybackControl(mMediaStateListener);
+ }
+
+ @CalledByNative
+ private void setNativePlayer() {
+ if (mDebug) Log.i(TAG, "setNativePlayer");
+ mRouteController.setMediaStateListener(mMediaStateListener);
+ mActive = true;
+ }
+
+ @CalledByNative
+ private void onPlayerCreated() {
+ if (mDebug) Log.i(TAG, "onPlayerCreated");
+ if (mRouteController != null) {
+ mRouteController.addMediaStateListener(mMediaStateListener);
+ }
+ }
+
+ @CalledByNative
+ private void onPlayerDestroyed() {
+ if (mDebug) Log.i(TAG, "onPlayerDestroyed");
+ if (mRouteController != null) {
+ mRouteController.removeMediaStateListener(mMediaStateListener);
+ }
+ }
+
+ /**
+ * @return Whether there're remote playback devices available.
+ */
+ @CalledByNative
+ private boolean isRemotePlaybackAvailable() {
+ return mRouteController.isRemotePlaybackAvailable();
+ }
+
+ /**
+ * @param bitmap The bitmap of the poster for the video, null if no poster image exists.
+ *
+ * TODO(cimamoglu): Notify the clients (probably through MediaRouteController.Listener)
+ * of the poster image change. This is necessary for when a web page changes the poster
+ * while the client (i.e. only ExpandedControllerActivity for now) is active.
+ */
+ @CalledByNative
+ private void setPosterBitmap(Bitmap bitmap) {
+ mPosterBitmap = bitmap;
+ }
+
+ /**
+ * @return Whether the video should be played remotely if possible
+ */
+ @CalledByNative
+ private boolean isRemotePlaybackPreferredForFrame() {
+ return !mRouteController.routeIsDefaultRoute()
+ && mRouteController.currentRouteSupportsRemotePlayback();
+ }
+
+ @CalledByNative
+ private boolean isMediaPlayableRemotely() {
+ return mRouteController != null;
+ }
+
+ @Override
+ @CalledByNative
+ protected boolean prepareAsync() {
+ mRouteController.prepareAsync(mFrameUrl, mStartPositionMillis);
+ return true;
+ }
+
+ @Override
+ @CalledByNative
+ protected boolean isPlaying() {
+ return mRouteController.isPlaying();
+ }
+
+ @Override
+ @CalledByNative
+ protected int getCurrentPosition() {
+ return mRouteController.getPosition();
+ }
+
+ @Override
+ @CalledByNative
+ protected int getDuration() {
+ return mRouteController.getDuration();
+ }
+
+ @Override
+ @CalledByNative
+ protected void release() {
+ // Remove the state change listeners. Release does mean that Chrome is no longer interested
+ // in events from the media player.
+ mRouteController.setMediaStateListener(null);
+ mActive = false;
+ }
+
+ @Override
+ @CalledByNative
+ protected void setVolume(double volume) {
+ }
+
+ @Override
+ @CalledByNative
+ protected void start() throws IllegalStateException {
+ mRouteController.resume();
+ }
+
+ @Override
+ @CalledByNative
+ protected void pause() throws IllegalStateException {
+ mRouteController.pause();
+ }
+
+ @Override
+ @CalledByNative
+ protected void seekTo(int msec) throws IllegalStateException {
+ mRouteController.seekTo(msec);
+ }
+
+ @Override
+ @CalledByNative
+ protected boolean setDataSource(
+ Context context, String url, String cookies, String userAgent, boolean hideUrlLog) {
+ mRouteController.setDataSource(Uri.parse(url), cookies);
+ return true;
+ }
+
+ @Override
+ protected void setOnBufferingUpdateListener(MediaPlayer.OnBufferingUpdateListener listener) {
+ }
+
+ @Override
+ protected void setOnCompletionListener(MediaPlayer.OnCompletionListener listener) {
+ mOnCompletionListener = listener;
+ }
+
+ @Override
+ protected void setOnSeekCompleteListener(MediaPlayer.OnSeekCompleteListener listener) {
+ mOnSeekCompleteListener = listener;
+ }
+
+ @Override
+ protected void setOnErrorListener(MediaPlayer.OnErrorListener listener) {
+ mOnErrorListener = listener;
+ }
+
+ @Override
+ protected void setOnPreparedListener(MediaPlayer.OnPreparedListener listener) {
+ mOnPreparedListener = listener;
+ }
+
+ @Override
+ protected void setOnVideoSizeChangedListener(MediaPlayer.OnVideoSizeChangedListener listener) {
+ }
+
+ /**
+ * Called when the video finishes
+ */
+ public void onCompleted() {
+ if (mActive && mOnCompletionListener != null) {
+ mOnCompletionListener.onCompletion(null);
+ }
+ }
+
+ @Override
+ @CalledByNative
+ protected void destroy() {
+ if (mDebug) Log.i(TAG, "destroy");
+ if (mRouteController != null) {
+ mRouteController.removeMediaStateListener(mMediaStateListener);
+ }
+ mNativeRemoteMediaPlayerBridge = 0;
+ }
+
+ private native String nativeGetFrameUrl(long nativeRemoteMediaPlayerBridge);
+ private native void nativeOnPlaying(long nativeRemoteMediaPlayerBridge);
+ private native void nativeOnPaused(long nativeRemoteMediaPlayerBridge);
+ private native void nativeOnRouteSelected(long nativeRemoteMediaPlayerBridge,
+ String playerName);
+ private native void nativeOnRouteUnselected(long nativeRemoteMediaPlayerBridge);
+ private native void nativeOnPlaybackFinished(long nativeRemoteMediaPlayerBridge);
+ private native void nativeOnRouteAvailabilityChanged(long nativeRemoteMediaPlayerBridge,
+ boolean available);
+ private native String nativeGetTitle(long nativeRemoteMediaPlayerBridge);
+
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/remote/RemoteMediaPlayerController.java b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/RemoteMediaPlayerController.java
new file mode 100644
index 0000000..6c6284d
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/RemoteMediaPlayerController.java
@@ -0,0 +1,450 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.media.remote;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentManager;
+import android.support.v7.app.MediaRouteChooserDialogFragment;
+import android.support.v7.app.MediaRouteControllerDialogFragment;
+import android.support.v7.app.MediaRouteDialogFactory;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.google.android.gms.cast.CastMediaControlIntent;
+
+import org.chromium.base.ApplicationStatus;
+import org.chromium.base.CommandLine;
+import org.chromium.base.ThreadUtils;
+import org.chromium.base.VisibleForTesting;
+import org.chromium.chrome.ChromeSwitches;
+import org.chromium.chrome.R;
+import org.chromium.chrome.browser.media.remote.RemoteVideoInfo.PlayerState;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The singleton responsible managing the global resources for remote media playback (cast)
+ */
+public class RemoteMediaPlayerController implements MediaRouteController.UiListener {
+ // Singleton instance of the class. May only be accessed from UI thread.
+ private static RemoteMediaPlayerController sInstance;
+
+ private static final String TAG = "VideoFling";
+
+ private static final String DEFAULT_CASTING_MESSAGE = "Casting to Chromecast";
+
+ private TransportControl mNotificationControl;
+ private TransportControl mLockScreenControl;
+
+ private Context mCastContextApplicationContext;
+ // The Activity that was in the foreground when the video was cast.
+ private WeakReference<Activity> mChromeVideoActivity;
+
+ private List<MediaRouteController> mMediaRouteControllers;
+
+ // points to mDefaultRouteSelector, mYouTubeRouteSelector or null
+ private MediaRouteController mCurrentRouteController;
+
+ private boolean mFirstConnection = true;
+
+ // This is a key for meta-data in the package manifest.
+ private static final String REMOTE_MEDIA_PLAYERS_KEY =
+ "org.chromium.content.browser.REMOTE_MEDIA_PLAYERS";
+
+ /**
+ * The private constructor to make sure the object is only created by the instance() method.
+ */
+ private RemoteMediaPlayerController() {
+ mChromeVideoActivity = new WeakReference<Activity>(null);
+ mMediaRouteControllers = new ArrayList<MediaRouteController>();
+ }
+
+ /**
+ * @return Whether the remote playback is enabled.
+ */
+ public static boolean isRemotePlaybackEnabled() {
+ return !CommandLine.getInstance().hasSwitch(ChromeSwitches.DISABLE_CAST);
+ }
+
+ /**
+ * @return The poster image for the currently playing remote video, null if there's none.
+ */
+ public Bitmap getPoster() {
+ if (mCurrentRouteController == null) return null;
+ return mCurrentRouteController.getPoster();
+ }
+
+ /**
+ * The singleton instance access method for native objects. Must be called on the UI thread
+ * only.
+ */
+ public static RemoteMediaPlayerController instance() {
+ ThreadUtils.assertOnUiThread();
+
+ if (sInstance == null) sInstance = new RemoteMediaPlayerController();
+ if (sInstance.mChromeVideoActivity.get() == null) sInstance.linkToBrowserActivity();
+
+ return sInstance;
+ }
+
+ /**
+ * Gets the MediaRouteController for a video, creating it if necessary.
+ * @param frameUrl The Url of the frame containing the video
+ * @return the MediaRouteController, or null if none.
+ */
+ public MediaRouteController getMediaRouteController(String sourceUrl, String frameUrl) {
+ for (MediaRouteController controller: mMediaRouteControllers) {
+ if (controller.canPlayMedia(sourceUrl, frameUrl)) {
+ return controller;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Gets the default MediaRouteController, creating it if necessary.
+ * @return the default MediaRouteController.
+ */
+ public List<MediaRouteController> getMediaRouteControllers() {
+ return mMediaRouteControllers;
+ }
+
+ /**
+ * Links this object to the Activity that owns the video, if it exists.
+ *
+ */
+ private void linkToBrowserActivity() {
+
+ Activity currentActivity = ApplicationStatus.getLastTrackedFocusedActivity();
+ if (currentActivity != null) {
+ mChromeVideoActivity = new WeakReference<Activity>(currentActivity);
+
+ mCastContextApplicationContext = currentActivity.getApplicationContext();
+ createMediaRouteControllers(currentActivity);
+ }
+ }
+
+ /**
+ * Create the mediaRouteControllers
+ * @param context - the current Android Context
+ */
+ public void createMediaRouteControllers(Context context) {
+ // We only need to do this once
+ if (!mMediaRouteControllers.isEmpty()) return;
+ try {
+ ApplicationInfo ai = context.getPackageManager().getApplicationInfo(
+ context.getPackageName(), PackageManager.GET_META_DATA);
+ Bundle bundle = ai.metaData;
+ String classNameString = bundle.getString(REMOTE_MEDIA_PLAYERS_KEY);
+
+ if (classNameString != null) {
+ String[] classNames = classNameString.split(",");
+ for (String className : classNames) {
+ Log.i(TAG, "Adding remote media route controller " + className.trim());
+ Class<?> mediaRouteControllerClass = Class.forName(className.trim());
+ Object mediaRouteController = mediaRouteControllerClass.newInstance();
+ assert mediaRouteController instanceof MediaRouteController;
+ mMediaRouteControllers.add((MediaRouteController) mediaRouteController);
+ }
+ }
+ } catch (NameNotFoundException | ClassNotFoundException | SecurityException
+ | InstantiationException | IllegalAccessException | IllegalArgumentException e) {
+ // Should never happen, implies corrupt AndroidManifest
+ Log.e(TAG, "Couldn't instatiate MediaRouteControllers", e);
+ assert false;
+ }
+ }
+
+ private void onStateReset(MediaRouteController controller) {
+
+ if (!controller.initialize()) return;
+
+ if (mFirstConnection) {
+ controller.reconnectAnyExistingRoute();
+ mFirstConnection = false;
+ }
+
+ if (mNotificationControl != null) {
+ mNotificationControl.setRouteController(controller);
+ }
+ if (mLockScreenControl != null) {
+ mLockScreenControl.setRouteController(controller);
+ }
+ controller.prepareMediaRoute();
+
+ controller.addUiListener(this);
+ }
+
+ /**
+ * Called when a lower layer requests that a video be cast. This will typically be a request
+ * from Blink when the cast button is pressed on the default video controls.
+ * @param player the player for which cast is being requested
+ * @param frameUrl the URL of the frame containing the video, needed for YouTube videos
+ */
+ public void requestRemotePlayback(
+ MediaRouteController.MediaStateListener player, MediaRouteController controller) {
+ Activity currentActivity = ApplicationStatus.getLastTrackedFocusedActivity();
+ mChromeVideoActivity = new WeakReference<Activity>(currentActivity);
+
+ if (mCurrentRouteController != null && controller != mCurrentRouteController) {
+ mCurrentRouteController.release();
+ }
+
+ onStateReset(controller);
+ if (controller.shouldResetState(player)) {
+ controller.setMediaStateListener(player);
+ showMediaRouteDialog(controller, currentActivity);
+ }
+
+ }
+
+ /**
+ * Called when a lower layer requests control of a video that is being cast.
+ * @param player The player for which remote playback control is being requested.
+ */
+ public void requestRemotePlaybackControl(MediaRouteController.MediaStateListener player) {
+ // Player should match currently remotely played item, but there
+ // can be a race between various
+ // ways that the a video can stop playing remotely. Check that the
+ // player is current, and ignore if not.
+
+ if (mCurrentRouteController == null) return;
+ if (mCurrentRouteController.getMediaStateListener() != player) return;
+
+ showMediaRouteControlDialog(mCurrentRouteController,
+ ApplicationStatus.getLastTrackedFocusedActivity());
+ }
+
+ private void showMediaRouteDialog(MediaRouteController controller, Activity activity) {
+
+ FragmentManager fm = ((FragmentActivity) activity).getSupportFragmentManager();
+ if (fm == null) {
+ throw new IllegalStateException("The activity must be a subclass of FragmentActivity");
+ }
+
+ MediaRouteDialogFactory factory = new ChromeMediaRouteDialogFactory();
+
+ if (fm.findFragmentByTag(
+ "android.support.v7.mediarouter:MediaRouteChooserDialogFragment") != null) {
+ Log.w(TAG, "showDialog(): Route chooser dialog already showing!");
+ return;
+ }
+ MediaRouteChooserDialogFragment f = factory.onCreateChooserDialogFragment();
+
+ f.setRouteSelector(controller.buildMediaRouteSelector());
+ f.show(fm, "android.support.v7.mediarouter:MediaRouteChooserDialogFragment");
+ }
+
+ private void showMediaRouteControlDialog(MediaRouteController controller, Activity activity) {
+
+ FragmentManager fm = ((FragmentActivity) activity).getSupportFragmentManager();
+ if (fm == null) {
+ throw new IllegalStateException("The activity must be a subclass of FragmentActivity");
+ }
+ MediaRouteDialogFactory factory = new ChromeMediaRouteDialogFactory();
+
+ if (fm.findFragmentByTag(
+ "android.support.v7.mediarouter:MediaRouteControllerDialogFragment") != null) {
+ Log.w(TAG, "showDialog(): Route controller dialog already showing!");
+ return;
+ }
+ MediaRouteControllerDialogFragment f = factory.onCreateControllerDialogFragment();
+
+ f.show(fm, "android.support.v7.mediarouter:MediaRouteControllerDialogFragment");
+ }
+
+ /**
+ * Starts up the notification and lock screen with the given playback state.
+ *
+ * @param initialState the initial state of the notification
+ * @param mediaRouteController the mediaRouteController for which these are needed
+ */
+ public void startNotificationAndLockScreen(PlayerState initialState,
+ MediaRouteController mediaRouteController) {
+ mCurrentRouteController = mediaRouteController;
+ createNotificationControl();
+ getNotification().show(initialState);
+ createLockScreen();
+ TransportControl lockScreen = getLockScreen();
+ if (lockScreen != null) lockScreen.show(initialState);
+ }
+
+ /**
+ * Returns the current remote playback position.
+ *
+ * @return The current position of the remote playback in milliseconds.
+ */
+ public int getPosition() {
+ if (mCurrentRouteController == null) return -1;
+ return mCurrentRouteController.getPosition();
+ }
+
+ /**
+ * @return The stream duration in milliseconds.
+ */
+ public int getDuration() {
+ if (mCurrentRouteController == null) return 0;
+ return mCurrentRouteController.getDuration();
+ }
+
+ /**
+ * @return Whether the video is currently being played.
+ */
+ public boolean isPlaying() {
+ return mCurrentRouteController != null && mCurrentRouteController.isPlaying();
+ }
+
+ /**
+ * Initiates a seek request for the remote playback device to the specified position.
+ *
+ * @param msec The position to seek to, in milliseconds.
+ */
+ public void seekTo(int msec) {
+ if (mCurrentRouteController == null) return;
+ mCurrentRouteController.seekTo(msec);
+ }
+
+ /**
+ * @return the currently playing MediaRouteController
+ */
+ public MediaRouteController getCurrentlyPlayingMediaRouteController() {
+ return mCurrentRouteController;
+ }
+
+ /**
+ * Set the current MediaRouteController
+ * @param controller the controller
+ */
+ public void setCurrentMediaRouteController(MediaRouteController controller) {
+ mCurrentRouteController = controller;
+ }
+
+ private TransportControl getNotification() {
+ return mNotificationControl;
+ }
+
+ /**
+ *
+ */
+ private void createNotificationControl() {
+ mNotificationControl = NotificationTransportControl.getOrCreate(
+ mChromeVideoActivity.get(), mCurrentRouteController);
+ mNotificationControl.setError(null);
+ mNotificationControl.setScreenName(mCurrentRouteController.getRouteName());
+ mNotificationControl.addListener(mCurrentRouteController);
+ }
+
+ private TransportControl getLockScreen() {
+ return mLockScreenControl;
+ }
+
+ /**
+ *
+ */
+ private void createLockScreen() {
+ mLockScreenControl = LockScreenTransportControl.getOrCreate(
+ mChromeVideoActivity.get(), mCurrentRouteController);
+ if (mLockScreenControl != null) {
+ mLockScreenControl.setError(null);
+ mLockScreenControl.setScreenName(mCurrentRouteController.getRouteName());
+ mLockScreenControl.addListener(mCurrentRouteController);
+ }
+ if (mLockScreenControl != null) mLockScreenControl.setPosterBitmap(getPoster());
+ }
+
+ @Override
+ public void onPrepared(MediaRouteController mediaRouteController) {
+
+ startNotificationAndLockScreen(PlayerState.PLAYING, mediaRouteController);
+ }
+
+ @Override
+ public void onPlaybackStateChanged(PlayerState oldState, PlayerState newState) {
+ if (newState == PlayerState.PLAYING || newState == PlayerState.LOADING
+ || newState == PlayerState.PAUSED) {
+ TransportControl notificationControl = getNotification();
+ if (notificationControl != null) notificationControl.show(newState);
+ TransportControl lockScreen = getLockScreen();
+ if (lockScreen != null) lockScreen.show(newState);
+ }
+ }
+
+ @Override
+ public void onError(int error, String errorMessage) {
+ if (error == CastMediaControlIntent.ERROR_CODE_SESSION_START_FAILED) {
+ showMessageToast(errorMessage);
+ }
+ }
+
+ @Override
+ public void onDurationUpdated(int durationMillis) {}
+
+ @Override
+ public void onPositionChanged(int positionMillis) {}
+
+ @Override
+ public void onTitleChanged(String title) {}
+
+ @Override
+ public void onRouteSelected(String routeName, MediaRouteController mediaRouteController) {
+ if (mCurrentRouteController != mediaRouteController) {
+ mCurrentRouteController = mediaRouteController;
+ resetPlayingVideo();
+ }
+ }
+
+ /**
+ * Gets some text to tell the user that the video is being cast.
+ * @param routeName The name of the route on which the video is being cast.
+ * @return A String to be shown to the user.
+ */
+ public String getCastingMessage(String routeName) {
+ String castingMessage = DEFAULT_CASTING_MESSAGE;
+ if (mCastContextApplicationContext != null) {
+ castingMessage = mCastContextApplicationContext.getString(
+ R.string.cast_casting_video, routeName);
+ }
+ return castingMessage;
+ }
+
+ // Note that, after switching MediaRouteControllers onRouteUnselected may be called for
+ // the old media route controller, so this should not do anything to
+ // mCurrentRouteController
+ @Override
+ public void onRouteUnselected(MediaRouteController mediaRouteController) {
+ if (mediaRouteController == mCurrentRouteController) {
+ mCurrentRouteController = null;
+ }
+ }
+
+ private void showMessageToast(String message) {
+ Toast toast = Toast.makeText(mCastContextApplicationContext, message, Toast.LENGTH_SHORT);
+ toast.show();
+ }
+
+ private void resetPlayingVideo() {
+ if (mNotificationControl != null) {
+ mNotificationControl.setRouteController(mCurrentRouteController);
+ }
+ if (mLockScreenControl != null) {
+ mLockScreenControl.setRouteController(mCurrentRouteController);
+ }
+ }
+
+ @VisibleForTesting
+ static RemoteMediaPlayerController getIfExists() {
+ return sInstance;
+ }
+
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/remote/RemotePlaybackSettings.java b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/RemotePlaybackSettings.java
new file mode 100644
index 0000000..aeb90be
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/RemotePlaybackSettings.java
@@ -0,0 +1,109 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.media.remote;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+/**
+ * Maintains the persistent settings that the app needs for casting.
+ */
+public class RemotePlaybackSettings {
+ private static final String DEVICE_ID_KEY = "remote_device_id";
+ private static final String RECONNECT_KEY = "reconnect_remote_device";
+ private static final String LAST_VIDEO_TIME_REMAINING_KEY = "last_video_time_remaining";
+ private static final String LAST_VIDEO_TIME_PLAYED_KEY = "last_video_time_played";
+ private static final String LAST_VIDEO_STATE_KEY = "last_video_state";
+ private static final String PLAYER_IN_USE_KEY = "player_in_use";
+ private static final String URI_PLAYING = "uri playing";
+ private static final String SESSION_ID = "session_id";
+
+ public static String getDeviceId(Context context) {
+ return getPreferences(context).getString(DEVICE_ID_KEY, null);
+ }
+
+ public static void setDeviceId(Context context, String deviceId) {
+ putValue(getPreferences(context), DEVICE_ID_KEY, deviceId);
+ }
+
+ public static String getSessionId(Context context) {
+ return getPreferences(context).getString(SESSION_ID, null);
+ }
+
+ public static void setSessionId(Context context, String sessionId) {
+ putValue(getPreferences(context), SESSION_ID, sessionId);
+ }
+
+ public static boolean getShouldReconnectToRemote(Context context) {
+ return getPreferences(context).getBoolean(RECONNECT_KEY, false);
+ }
+
+ public static void setShouldReconnectToRemote(Context context, boolean reconnect) {
+ putValue(getPreferences(context), RECONNECT_KEY, reconnect);
+ }
+
+ public static long getRemainingTime(Context context) {
+ return getPreferences(context).getLong(LAST_VIDEO_TIME_REMAINING_KEY, 0);
+ }
+
+ public static void setRemainingTime(Context context, long time) {
+ putValue(getPreferences(context), LAST_VIDEO_TIME_REMAINING_KEY, time);
+ }
+
+ public static long getLastPlayedTime(Context context) {
+ return getPreferences(context).getLong(LAST_VIDEO_TIME_PLAYED_KEY, 0);
+ }
+
+ public static void setLastPlayedTime(Context context, long time) {
+ putValue(getPreferences(context), LAST_VIDEO_TIME_PLAYED_KEY, time);
+ }
+
+ public static String getLastVideoState(Context context) {
+ return getPreferences(context).getString(LAST_VIDEO_STATE_KEY, null);
+ }
+
+ public static void setLastVideoState(Context context, String title) {
+ putValue(getPreferences(context), LAST_VIDEO_STATE_KEY, title);
+ }
+
+ public static String getPlayerInUse(Context context) {
+ return getPreferences(context).getString(PLAYER_IN_USE_KEY, null);
+ }
+
+ public static void setPlayerInUse(Context context, String player) {
+ putValue(getPreferences(context), PLAYER_IN_USE_KEY, player);
+ }
+
+ public static String getUriPlaying(Context context) {
+ return getPreferences(context).getString(URI_PLAYING, null);
+ }
+
+ public static void setUriPlaying(Context context, String url) {
+ putValue(getPreferences(context), URI_PLAYING, url);
+ }
+
+ private static void putValue(SharedPreferences preferences, String key, String value) {
+ SharedPreferences.Editor editor = preferences.edit();
+ editor.putString(key, value);
+ editor.apply();
+ }
+
+ private static void putValue(SharedPreferences preferences, String key, boolean value) {
+ SharedPreferences.Editor editor = preferences.edit();
+ editor.putBoolean(key, value);
+ editor.apply();
+ }
+
+ private static void putValue(SharedPreferences preferences, String key, long value) {
+ SharedPreferences.Editor editor = preferences.edit();
+ editor.putLong(key, value);
+ editor.apply();
+ }
+
+ private static SharedPreferences getPreferences(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context);
+ }
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/remote/RemoteVideoInfo.java b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/RemoteVideoInfo.java
new file mode 100644
index 0000000..3982cf6
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/RemoteVideoInfo.java
@@ -0,0 +1,115 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.media.remote;
+
+import android.text.TextUtils;
+
+/**
+ * Exposes information about the current video to the external clients.
+ */
+public class RemoteVideoInfo {
+
+ /**
+ * This lists all the states that the remote video can be in.
+ */
+ public enum PlayerState {
+ /** The remote player is currently stopped. */
+ STOPPED,
+
+ /** The remote player is buffering this video. */
+ LOADING,
+
+ /** The remote player is playing this video. */
+ PLAYING,
+
+ /** The remote player is paused. */
+ PAUSED,
+
+ /** The remote player is in an error state. */
+ ERROR,
+
+ /** The remote player has been replaced by another player (so the current session has
+ * finished) */
+ INVALIDATED,
+
+ /** The remote video has completed playing. */
+ FINISHED
+ }
+
+ /**
+ * The title of the video
+ */
+ public String title;
+ /**
+ * The duration of the video
+ */
+ public int durationMillis;
+ /**
+ * The current state of the video
+ */
+ public PlayerState state;
+ /**
+ * The last known position in the video
+ */
+ public int currentTimeMillis;
+ /**
+ * The current error message, if any
+ */
+ // TODO(aberent) At present nothing sets this to anything other than Null.
+ public String errorMessage;
+
+ /**
+ * Create a new RemoteVideoInfo
+ * @param title
+ * @param durationMillis
+ * @param state
+ * @param currentTimeMillis
+ * @param errorMessage
+ */
+ public RemoteVideoInfo(String title, int durationMillis, PlayerState state,
+ int currentTimeMillis, String errorMessage) {
+ this.title = title;
+ this.durationMillis = durationMillis;
+ this.state = state;
+ this.currentTimeMillis = currentTimeMillis;
+ this.errorMessage = errorMessage;
+ }
+
+ /**
+ * Copy a remote video info
+ * @param other the source.
+ */
+ public RemoteVideoInfo(RemoteVideoInfo other) {
+ this(other.title, other.durationMillis, other.state, other.currentTimeMillis,
+ other.errorMessage);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (!(obj instanceof RemoteVideoInfo)) {
+ return false;
+ }
+
+ RemoteVideoInfo other = (RemoteVideoInfo) obj;
+ return durationMillis == other.durationMillis
+ && currentTimeMillis == other.currentTimeMillis
+ && state == other.state
+ && TextUtils.equals(title, other.title)
+ && TextUtils.equals(errorMessage, other.errorMessage);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = durationMillis;
+ result = 31 * result + currentTimeMillis;
+ result = 31 * result + (title == null ? 0 : title.hashCode());
+ result = 31 * result + (state == null ? 0 : state.hashCode());
+ result = 31 * result + (errorMessage == null ? 0 : errorMessage.hashCode());
+ return result;
+ }
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/remote/TransportControl.java b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/TransportControl.java
new file mode 100644
index 0000000..7cdf637
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/TransportControl.java
@@ -0,0 +1,185 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.media.remote;
+
+import android.graphics.Bitmap;
+import android.text.TextUtils;
+
+import org.chromium.chrome.browser.media.remote.RemoteVideoInfo.PlayerState;
+
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+/**
+ * TransportControl is an abstract base class for various UI views that are intended to control
+ * video playback. TransportControl contains a number of setters that will update the
+ * TransportControl's UI.
+ * Call {@code TransportControl#addListener} with an implementation of
+ * {@code TransportControl.Listener} to receive messages when the user interacts with the
+ * TransportControl.
+ */
+public abstract class TransportControl {
+
+ /**
+ * Base interface for classes that need to listen to transport control events.
+ */
+ public static interface Listener {
+ void onPlay();
+ void onPause();
+ void onSeek(int position);
+ void onStop();
+ }
+
+ private final Set<Listener> mListeners = new CopyOnWriteArraySet<Listener>();
+ private String mScreenName;
+ private String mError;
+ protected RemoteVideoInfo mVideoInfo;
+ private Bitmap mPosterBitmap;
+
+ /**
+ * @return the screen name previously assigned with {@link #setScreenName(String)}.
+ */
+ public final String getScreenName() {
+ return mScreenName;
+ }
+
+ /**
+ * Sets the name to display for the device on the TransportControl.
+ */
+ public final void setScreenName(String screenName) {
+ if (TextUtils.equals(this.mScreenName, screenName)) {
+ return;
+ }
+
+ this.mScreenName = screenName;
+ onScreenNameChanged();
+ }
+
+ /**
+ * @return the error message previously assigned with {@link #setError(String)}, or
+ * {@code null} if {@link #hasError()} returns {@code false}.
+ */
+ public final String getError() {
+ return mError;
+ }
+
+ /**
+ * Sets the error message to display on the TransportControl.
+ * Calling this method with {@code null} or an empty string is equivalent to calling
+ * {@link #clearError()}
+ */
+ public final void setError(String message) {
+ if (TextUtils.equals(mError, message)) {
+ return;
+ }
+
+ mError = TextUtils.isEmpty(message) ? null : message;
+ onErrorChanged();
+ }
+
+ /**
+ * @return true if an error message is assigned to this TransportControl, otherwise false.
+ */
+ public final boolean hasError() {
+ return mError != null;
+ }
+
+ /**
+ * Clears the error message previously assigned by {@code #setError(String)}.
+ */
+ public final void clearError() {
+ setError(null);
+ }
+
+ /**
+ * @return the remote's video information previously assigned with
+ * {@link #setVideoInfo(RemoteVideoInfo)}, or {@code null} if the {@link RemoteVideoInfo}
+ * has not yet been assigned.
+ */
+ public final RemoteVideoInfo getVideoInfo() {
+ return mVideoInfo;
+ }
+
+ /**
+ * Sets the remote's video information to display on the TransportControl.
+ * @param videoInfo the video information to use.
+ */
+ public final void setVideoInfo(RemoteVideoInfo videoInfo) {
+ if (equal(this.mVideoInfo, videoInfo)) {
+ return;
+ }
+
+ this.mVideoInfo = videoInfo;
+ onVideoInfoChanged();
+ }
+
+ /**
+ * @return the poster bitmap previously assigned with {@link #setPosterBitmap(Bitmap)}, or
+ * {@code null} if the poster bitmap has not yet been assigned.
+ */
+ public final Bitmap getPosterBitmap() {
+ return mPosterBitmap;
+ }
+
+ /**
+ * Sets the poster bitmap to display on the TransportControl.
+ */
+ public final void setPosterBitmap(Bitmap posterBitmap) {
+ if (equal(this.mPosterBitmap, posterBitmap)) {
+ return;
+ }
+
+ this.mPosterBitmap = posterBitmap;
+ onPosterBitmapChanged();
+ }
+
+ /**
+ * Registers a {@link Listener} with the TransportControl.
+ * @param listener the Listener to be registered.
+ */
+ public void addListener(Listener listener) {
+ mListeners.add(listener);
+ }
+
+ /**
+ * Unregisters a {@link Listener} previously registered with {@link #addListener(Listener)}.
+ * @param listener the Listener to be removed.
+ */
+ public void removeListener(Listener listener) {
+ mListeners.remove(listener);
+ }
+
+ /**
+ * Displays the TransportControl to the user.
+ * @param initialState the state of the player when this is called
+ */
+ public abstract void show(PlayerState initialState);
+
+ /**
+ * Hides the TransportControl.
+ */
+ public abstract void hide();
+
+ /**
+ * Updates the corresponding route controller.
+ */
+ public abstract void setRouteController(MediaRouteController controller);
+
+ /**
+ * @return the current list of listeners.
+ */
+ protected final Set<Listener> getListeners() {
+ return mListeners;
+ }
+
+ protected void onScreenNameChanged() {}
+ protected void onErrorChanged() {}
+ protected void onVideoInfoChanged() {}
+ protected void onPosterBitmapChanged() {}
+
+ private static boolean equal(Object a, Object b) {
+ return (a == null) ? (b == null) : a.equals(b);
+ }
+} \ No newline at end of file
diff --git a/chrome/android/java/strings/android_chrome_strings.grd b/chrome/android/java/strings/android_chrome_strings.grd
index 99c84b4..b1802d1 100644
--- a/chrome/android/java/strings/android_chrome_strings.grd
+++ b/chrome/android/java/strings/android_chrome_strings.grd
@@ -1482,6 +1482,54 @@ Drag from top to exit.
Your tabs and apps are now in the same place for easier access. You can turn this feature off in <ph name="BEGIN_LINK">&lt;link&gt;</ph>Settings<ph name="END_LINK">&lt;/link&gt;</ph>.
</message>
+ <message name="IDS_CAST_NOTIFICATION_STOPPED" desc="AtHome notification text when stopped. [CHAR LIMIT=40]">
+ Stopped
+ </message>
+ <message name="IDS_CAST_NOTIFICATION_FINISHED" desc="AtHome notification text when finished. [CHAR LIMIT=40]">
+ Finished
+ </message>
+ <message name="IDS_CAST_NOTIFICATION_PAUSED" desc="AtHome notification text when paused. [CHAR LIMIT=40]">
+ Paused
+ </message>
+
+ <!-- Messages for remote media playback (casting) -->
+ <message name="IDS_CAST_NOTIFICATION_LOADING" desc="AtHome notification text when loading a video. [CHAR LIMIT=40]">
+ Loading video
+ </message>
+ <message name="IDS_CAST_NOTIFICATION_PLAYING" desc="AtHome notification text when playing a video. [CHAR LIMIT=40]">
+ Playing video
+ </message>
+ <message name="IDS_CAST_NOTIFICATION_LOADING_FOR_VIDEO" desc="AtHome notification text when loading a given video. [CHAR LIMIT=40]">
+ Loading “<ph name="VIDEO_TITLE">%1$s<ex>Psy - Gangnam Style - YouTube</ex></ph>”
+ </message>
+ <message name="IDS_CAST_NOTIFICATION_PLAYING_FOR_VIDEO" desc="AtHome notification text when playing a given video. [CHAR LIMIT=40]">
+ Playing “<ph name="VIDEO_TITLE">%1$s<ex>Psy - Gangnam Style - YouTube</ex></ph>”
+ </message>
+ <message name="IDS_CAST_NOTIFICATION_PAUSED_FOR_VIDEO" desc="AtHome notification text when paused in a given video. [CHAR LIMIT=40]">
+ Paused “<ph name="VIDEO_TITLE">%1$s<ex>Psy - Gangnam Style - YouTube</ex></ph>”
+ </message>
+ <message name="IDS_CAST_NOTIFICATION_FINISHED_FOR_VIDEO" desc="AtHome notification text when finished playing a given video. [CHAR LIMIT=40]">
+ Finished “<ph name="VIDEO_TITLE">%1$s<ex>Psy - Gangnam Style - YouTube</ex></ph>”
+ </message>
+ <message name="IDS_CAST_CASTING_VIDEO" desc="AtHome text to tell user which screen casting is happening. [CHAR LIMIT=40]">
+ Casting to <ph name="SCREEN_NAME">%1$s<ex>Living Room TV</ex></ph>
+ </message>
+ <message name="IDS_CAST_ERROR_PLAYING_VIDEO" desc="The message shown to the user when playing a video to the Chromecast fails.">
+ Cannot play video on <ph name="SCREEN_NAME">%1$s<ex>Living Room TV</ex></ph>.
+ </message>
+ <message name="IDS_CAST_PERMISSION_ERROR_PLAYING_VIDEO" desc="The message shown to the user when trying to play a video that the domain does not let us cast.">
+ Unable to cast video due to site restrictions
+ </message>
+ <message name="IDS_ACCESSIBILITY_PLAY" desc="Content description for the play button that starts playing the media.">
+ Play
+ </message>
+ <message name="IDS_ACCESSIBILITY_PAUSE" desc="Content description for the pause button that pauses playing the media.">
+ Pause
+ </message>
+ <message name="IDS_ACCESSIBILITY_STOP" desc="Content description for the stop button that stops playing the media.">
+ Stop
+ </message>
+
</messages>
</release>
</grit>
diff --git a/chrome/android/shell/java/AndroidManifest.xml.jinja2 b/chrome/android/shell/java/AndroidManifest.xml.jinja2
index c91196e..49c7bae 100644
--- a/chrome/android/shell/java/AndroidManifest.xml.jinja2
+++ b/chrome/android/shell/java/AndroidManifest.xml.jinja2
@@ -192,5 +192,30 @@
<meta-data android:name="org.chromium.chrome.browser.SERVICE_TAB_LAUNCHER"
android:value="org.chromium.chrome.shell.ChromeShellServiceTabLauncher" />
+ <!-- Activity, service, and meta-data to support casting to Chromecast -->
+
+ <!-- Expanded controller activity is displayed when the Cast Notification is clicked -->
+ <activity android:name="org.chromium.chrome.browser.media.remote.ExpandedControllerActivity"
+ android:theme="@style/MainTheme"
+ android:label="Chrome.ExpandedControllerActivity"
+ android:hardwareAccelerated="true"
+ android:launchMode="singleTask"
+ android:noHistory="true"
+ android:configChanges="orientation|keyboardHidden|keyboard|screenSize"
+ android:excludeFromRecents="true">
+ </activity>
+
+ <service android:name="org.chromium.chrome.browser.media.remote.NotificationTransportControl$ListenerService" />
+
+ <!-- Media route controllers to use for remote playback (cast).
+ This is here, rather than in code, since it varies between upstream and downstream,
+ yet we need this list of classes in the notification service, which belongs upstream
+ and doesn't run the downstream Clank startup code. The Cast code will, for each media element,
+ choose the first MediaRouteController that can play it, so the order of the list can be important.
+ The most specific MediaRouteControllers should be listed first, followed by more generic ones -->
+ <meta-data android:name="org.chromium.content.browser.REMOTE_MEDIA_PLAYERS"
+ android:value="org.chromium.chrome.browser.media.remote.DefaultMediaRouteController"/>
+
+
</application>
</manifest>
diff --git a/chrome/app/android/DEPS b/chrome/app/android/DEPS
new file mode 100644
index 0000000..0e145c3
--- /dev/null
+++ b/chrome/app/android/DEPS
@@ -0,0 +1,6 @@
+specific_include_rules = {
+ # Fix layering violation crbug.com/396828
+ "chrome_main_delegate_android.cc": [
+ "+content/browser/media/android/browser_media_player_manager.h",
+ ],
+} \ No newline at end of file
diff --git a/chrome/app/android/chrome_main_delegate_android.cc b/chrome/app/android/chrome_main_delegate_android.cc
index 81cf6db..24291de 100644
--- a/chrome/app/android/chrome_main_delegate_android.cc
+++ b/chrome/app/android/chrome_main_delegate_android.cc
@@ -8,9 +8,23 @@
#include "base/trace_event/trace_event.h"
#include "chrome/browser/android/chrome_startup_flags.h"
#include "chrome/browser/android/metrics/uma_utils.h"
+#include "chrome/browser/android/metrics/uma_utils.h"
+#include "chrome/browser/media/android/remote/remote_media_player_manager.h"
#include "components/startup_metric_utils/startup_metric_utils.h"
+#include "content/browser/media/android/browser_media_player_manager.h"
#include "content/public/browser/browser_main_runner.h"
+namespace {
+
+content::BrowserMediaPlayerManager* CreateRemoteMediaPlayerManager(
+ content::RenderFrameHost* render_frame_host,
+ content::MediaPlayersObserver* audio_monitor) {
+ return new remote_media::RemoteMediaPlayerManager(render_frame_host,
+ audio_monitor);
+}
+
+} // namespace
+
// ChromeMainDelegateAndroid is created when the library is loaded. It is always
// done in the process's main Java thread. But for non browser process, e.g.
// renderer process, it is not the native Chrome's main thread.
@@ -19,7 +33,6 @@ ChromeMainDelegateAndroid::ChromeMainDelegateAndroid() {
ChromeMainDelegateAndroid::~ChromeMainDelegateAndroid() {
}
-
void ChromeMainDelegateAndroid::SandboxInitialized(
const std::string& process_type) {
ChromeMainDelegate::SandboxInitialized(process_type);
@@ -49,5 +62,9 @@ int ChromeMainDelegateAndroid::RunProcess(
bool ChromeMainDelegateAndroid::BasicStartupComplete(int* exit_code) {
SetChromeSpecificCommandLineFlags();
+
+ content::BrowserMediaPlayerManager::RegisterFactory(
+ &CreateRemoteMediaPlayerManager);
+
return ChromeMainDelegate::BasicStartupComplete(exit_code);
}
diff --git a/chrome/browser/android/chrome_jni_registrar.cc b/chrome/browser/android/chrome_jni_registrar.cc
index 5a27503..7999199 100644
--- a/chrome/browser/android/chrome_jni_registrar.cc
+++ b/chrome/browser/android/chrome_jni_registrar.cc
@@ -67,6 +67,8 @@
#include "chrome/browser/history/android/sqlite_cursor.h"
#include "chrome/browser/invalidation/invalidation_service_factory_android.h"
#include "chrome/browser/lifetime/application_lifetime_android.h"
+#include "chrome/browser/media/android/remote/record_cast_action.h"
+#include "chrome/browser/media/android/remote/remote_media_player_bridge.h"
#include "chrome/browser/net/spdyproxy/data_reduction_proxy_settings_android.h"
#include "chrome/browser/notifications/notification_ui_manager_android.h"
#include "chrome/browser/password_manager/credential_android.h"
@@ -228,6 +230,9 @@ static base::android::RegistrationMethod kChromeRegisteredMethods[] = {
{"ProfileDownloader", RegisterProfileDownloader},
{"ProfileSyncService", ProfileSyncServiceAndroid::Register},
{"RecentlyClosedBridge", RecentlyClosedTabsBridge::Register},
+ {"RecordCastAction", remote_media::RegisterRecordCastAction},
+ {"RemoteMediaPlayerBridge",
+ remote_media::RemoteMediaPlayerBridge::RegisterRemoteMediaPlayerBridge},
{"SavePasswordInfoBar", SavePasswordInfoBar::Register},
{"SceneLayer", chrome::android::RegisterSceneLayer},
{"ServiceTabLauncher", ServiceTabLauncher::RegisterServiceTabLauncher},
diff --git a/chrome/browser/media/android/remote/DEPS b/chrome/browser/media/android/remote/DEPS
new file mode 100644
index 0000000..ecbee67
--- /dev/null
+++ b/chrome/browser/media/android/remote/DEPS
@@ -0,0 +1,16 @@
+specific_include_rules = {
+ # TODO(aberent): Fix layering violation crbug.com/396828
+ "remote_media_player_manager\.": [
+ "+content/browser/media/android/browser_media_player_manager.h",
+ ],
+
+ # TODO(aberent): Fix layering violation crbug.com/396828
+ "remote_media_player_manager\.cc": [
+ "+content/common/media/media_player_messages_android.h",
+ ],
+
+ # TODO(aberent): Fix layering violation crbug.com/396828
+ "remote_media_player_bridge\.cc": [
+ "+content/browser/android/content_view_core_impl.h"
+ ],
+} \ No newline at end of file
diff --git a/chrome/browser/media/android/remote/OWNERS b/chrome/browser/media/android/remote/OWNERS
new file mode 100644
index 0000000..d88be63
--- /dev/null
+++ b/chrome/browser/media/android/remote/OWNERS
@@ -0,0 +1,3 @@
+aberent@chromium.org
+dgn@chromium.org
+avayvod@chromium.org \ No newline at end of file
diff --git a/chrome/browser/media/android/remote/record_cast_action.cc b/chrome/browser/media/android/remote/record_cast_action.cc
new file mode 100644
index 0000000..386083b
--- /dev/null
+++ b/chrome/browser/media/android/remote/record_cast_action.cc
@@ -0,0 +1,108 @@
+// Copyright 2014 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/android/remote/record_cast_action.h"
+
+#include <jni.h>
+
+#include "base/metrics/histogram.h"
+#include "content/public/browser/user_metrics.h"
+#include "jni/RecordCastAction_jni.h"
+#include "media/base/container_names.h"
+
+using base::UserMetricsAction;
+using content::RecordAction;
+
+namespace {
+
+// When updating these values, remember to also update
+// tools/histograms/histograms.xml.
+enum CastPlayBackState {
+ YT_PLAYER_SUCCESS = 0,
+ YT_PLAYER_FAILURE = 1,
+ DEFAULT_PLAYER_SUCCESS = 2,
+ DEFAULT_PLAYER_FAILURE = 3,
+ CAST_PLAYBACK_STATE_COUNT = 4
+};
+
+// When updating these values, remember to also update
+// tools/histograms/histograms.xml.
+
+// This is actually a misnomer, it should be RemotePlaybackPlayerType, but it is
+// more important that it matches the histogram name in histograms.xml.
+// TODO(aberent) Change this once we are upstream, when can change it both here
+// and in histogram.xml in the same CL.
+enum RemotePlaybackDeviceType {
+ CAST_GENERIC = 0,
+ CAST_YOUTUBE = 1,
+ NON_CAST_YOUTUBE = 2,
+ REMOTE_PLAYBACK_DEVICE_TYPE_COUNT = 3
+};
+
+} // namespace
+
+namespace remote_media {
+static void RecordRemotePlaybackDeviceSelected(
+ JNIEnv*, jclass, jint device_type) {
+ UMA_HISTOGRAM_ENUMERATION(
+ "Cast.Sender.DeviceType", device_type, REMOTE_PLAYBACK_DEVICE_TYPE_COUNT);
+}
+
+static void RecordCastPlayRequested(JNIEnv*, jclass) {
+ RecordAction(UserMetricsAction("Cast_Sender_CastPlayRequested"));
+}
+
+static void RecordCastDefaultPlayerResult(JNIEnv*,
+ jclass,
+ jboolean cast_success) {
+ if (cast_success) {
+ UMA_HISTOGRAM_ENUMERATION("Cast.Sender.CastPlayerResult",
+ DEFAULT_PLAYER_SUCCESS,
+ CAST_PLAYBACK_STATE_COUNT);
+ } else {
+ UMA_HISTOGRAM_ENUMERATION("Cast.Sender.CastPlayerResult",
+ DEFAULT_PLAYER_FAILURE,
+ CAST_PLAYBACK_STATE_COUNT);
+ }
+}
+
+static void RecordCastYouTubePlayerResult(JNIEnv*,
+ jclass,
+ jboolean cast_success) {
+ if (cast_success) {
+ UMA_HISTOGRAM_ENUMERATION("Cast.Sender.CastPlayerResult", YT_PLAYER_SUCCESS,
+ CAST_PLAYBACK_STATE_COUNT);
+ } else {
+ UMA_HISTOGRAM_ENUMERATION("Cast.Sender.CastPlayerResult", YT_PLAYER_FAILURE,
+ CAST_PLAYBACK_STATE_COUNT);
+ }
+}
+
+static void RecordCastMediaType(JNIEnv*, jclass, jint media_type) {
+ UMA_HISTOGRAM_ENUMERATION("Cast.Sender.CastMediaType", media_type,
+ media::container_names::CONTAINER_MAX);
+}
+
+static void RecordCastEndedTimeRemaining(JNIEnv*,
+ jclass,
+ jint video_total_time,
+ jint time_left_in_video) {
+ int percent_remaining = 100;
+ if (video_total_time > 0) {
+ // Get the percentage of video remaining, but bucketize into groups of 10
+ // since we don't really need that granular of data.
+ percent_remaining = static_cast<int>(
+ 10.0 * time_left_in_video / video_total_time) * 10;
+ }
+
+ UMA_HISTOGRAM_ENUMERATION("Cast.Sender.CastTimeRemainingPercentage",
+ percent_remaining, 101);
+}
+
+// Register native methods
+bool RegisterRecordCastAction(JNIEnv* env) {
+ return RegisterNativesImpl(env);
+}
+
+} // namespace remote_media
diff --git a/chrome/browser/media/android/remote/record_cast_action.h b/chrome/browser/media/android/remote/record_cast_action.h
new file mode 100644
index 0000000..1f52d9c
--- /dev/null
+++ b/chrome/browser/media/android/remote/record_cast_action.h
@@ -0,0 +1,17 @@
+// Copyright 2014 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_ANDROID_REMOTE_RECORD_CAST_ACTION_H_
+#define CHROME_BROWSER_MEDIA_ANDROID_REMOTE_RECORD_CAST_ACTION_H_
+
+#include <jni.h>
+
+namespace remote_media {
+
+// Registers the native methods through jni
+bool RegisterRecordCastAction(JNIEnv* env);
+
+} // namespace remote_media
+
+#endif // CHROME_BROWSER_MEDIA_ANDROID_REMOTE_RECORD_CAST_ACTION_H_
diff --git a/chrome/browser/media/android/remote/remote_media_player_bridge.cc b/chrome/browser/media/android/remote/remote_media_player_bridge.cc
new file mode 100644
index 0000000..dfad5fb
--- /dev/null
+++ b/chrome/browser/media/android/remote/remote_media_player_bridge.cc
@@ -0,0 +1,512 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/media/android/remote/remote_media_player_bridge.h"
+
+#include "base/android/jni_android.h"
+#include "base/android/jni_string.h"
+#include "chrome/browser/media/android/remote/record_cast_action.h"
+#include "chrome/browser/media/android/remote/remote_media_player_manager.h"
+#include "content/browser/android/content_view_core_impl.h"
+#include "jni/RemoteMediaPlayerBridge_jni.h"
+#include "media/base/android/media_common_android.h"
+#include "media/base/android/media_resource_getter.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+#include "ui/gfx/android/java_bitmap.h"
+
+using base::android::ConvertUTF8ToJavaString;
+using base::android::ScopedJavaLocalRef;
+using base::android::AttachCurrentThread;
+
+namespace {
+/*
+ * Dummy function for RequestMediaResources callback. The callback is never
+ * actually called by MediaPlayerAndroid or RemoteMediaPlayer but is needed
+ * to compile the constructor call.
+ */
+void DoNothing(int /*i*/) {}
+}
+
+namespace remote_media {
+
+RemoteMediaPlayerBridge::RemoteMediaPlayerBridge(
+ MediaPlayerAndroid* local_player, const std::string& user_agent,
+ bool hide_url_log, RemoteMediaPlayerManager* manager)
+ : MediaPlayerAndroid(local_player->player_id(), manager,
+ base::Bind(&DoNothing),
+ local_player->frame_url()),
+ start_position_millis_(0),
+ local_player_(local_player),
+ in_use_(false),
+ prepared_(false),
+ pending_play_(false),
+ width_(0),
+ height_(0),
+ should_seek_on_prepare_(false),
+ hide_url_log_(hide_url_log),
+ volume_(-1.0),
+ url_(local_player->GetUrl()),
+ first_party_for_cookies_(local_player->GetFirstPartyForCookies()),
+ user_agent_(user_agent),
+ weak_factory_(this) {
+ if (local_player->GetCurrentTime().InMilliseconds() > 0)
+ start_position_millis_ = local_player->GetCurrentTime().InMilliseconds();
+ JNIEnv* env = base::android::AttachCurrentThread();
+ CHECK(env);
+ ScopedJavaLocalRef<jstring> j_url_string;
+ if (url_.is_valid()) {
+ // Create a Java String for the URL.
+ j_url_string = ConvertUTF8ToJavaString(env, url_.spec());
+ }
+ ScopedJavaLocalRef<jstring> j_frame_url_string;
+ if (local_player->frame_url().is_valid()) {
+ // Create a Java String for the URL.
+ j_frame_url_string = ConvertUTF8ToJavaString(
+ env, local_player->frame_url().spec());
+ }
+ java_bridge_.Reset(
+ Java_RemoteMediaPlayerBridge_create(env, reinterpret_cast<intptr_t>(this),
+ start_position_millis_,
+ j_url_string.obj(),
+ j_frame_url_string.obj()));
+}
+
+RemoteMediaPlayerBridge::~RemoteMediaPlayerBridge() {
+ JNIEnv* env = base::android::AttachCurrentThread();
+ CHECK(env);
+ Java_RemoteMediaPlayerBridge_destroy(env, java_bridge_.obj());
+ Release();
+}
+
+int RemoteMediaPlayerBridge::GetVideoWidth() {
+ return local_player_->GetVideoWidth();
+}
+
+int RemoteMediaPlayerBridge::GetVideoHeight() {
+ return local_player_->GetVideoHeight();
+}
+
+void RemoteMediaPlayerBridge::OnVideoSizeChanged(int width, int height) {
+ width_ = width;
+ height_ = height;
+ MediaPlayerAndroid::OnVideoSizeChanged(width, height);
+}
+
+void RemoteMediaPlayerBridge::OnPlaybackComplete() {
+ time_update_timer_.Stop();
+ MediaPlayerAndroid::OnPlaybackComplete();
+}
+
+void RemoteMediaPlayerBridge::OnMediaInterrupted() {}
+
+void RemoteMediaPlayerBridge::OnMediaPrepared() {
+ if (!in_use_)
+ return;
+
+ prepared_ = true;
+ duration_ = GetDuration();
+
+ // If media player was recovered from a saved state, consume all the pending
+ // events.
+ if (should_seek_on_prepare_) {
+ PendingSeekInternal(pending_seek_);
+ pending_seek_ = base::TimeDelta::FromMilliseconds(0);
+ should_seek_on_prepare_ = false;
+ }
+
+ if (pending_play_) {
+ StartInternal();
+ pending_play_ = false;
+ }
+
+ manager()->OnMediaMetadataChanged(
+ player_id(), duration_, width_, height_, true);
+}
+
+void RemoteMediaPlayerBridge::StartInternal() {
+ JNIEnv* env = AttachCurrentThread();
+ Java_RemoteMediaPlayerBridge_start(env, java_bridge_.obj());
+ if (!time_update_timer_.IsRunning()) {
+ time_update_timer_.Start(
+ FROM_HERE,
+ base::TimeDelta::FromMilliseconds(media::kTimeUpdateInterval),
+ this, &RemoteMediaPlayerBridge::OnTimeUpdateTimerFired);
+ }
+}
+
+void RemoteMediaPlayerBridge::PauseInternal() {
+ JNIEnv* env = AttachCurrentThread();
+ Java_RemoteMediaPlayerBridge_pause(env, java_bridge_.obj());
+ time_update_timer_.Stop();
+}
+
+void RemoteMediaPlayerBridge::SeekInternal(base::TimeDelta time) {
+ if (time > duration_)
+ time = duration_;
+
+ // Seeking to an invalid position may cause media player to stuck in an
+ // error state.
+ if (time < base::TimeDelta()) {
+ DCHECK_EQ(-1.0, time.InMillisecondsF());
+ return;
+ }
+
+ JNIEnv* env = AttachCurrentThread();
+ CHECK(env);
+ int time_msec = static_cast<int>(time.InMilliseconds());
+ Java_RemoteMediaPlayerBridge_seekTo(
+ env, java_bridge_.obj(), time_msec);
+}
+
+void RemoteMediaPlayerBridge::OnTimeUpdateTimerFired() {
+ manager()->OnTimeUpdate(
+ player_id(), GetCurrentTime(), base::TimeTicks::Now());
+}
+
+void RemoteMediaPlayerBridge::PendingSeekInternal(const base::TimeDelta& time) {
+ SeekInternal(time);
+}
+
+void RemoteMediaPlayerBridge::Prepare() {
+ DCHECK(!in_use_);
+ DCHECK(IsMediaPlayableRemotely());
+ in_use_ = true;
+ AttachListener(java_bridge_.obj());
+ JNIEnv* env = AttachCurrentThread();
+ CHECK(env);
+
+ if (url_.is_valid()) {
+ // Create a Java String for the URL.
+ ScopedJavaLocalRef<jstring> j_url_string =
+ ConvertUTF8ToJavaString(env, url_.spec());
+
+ jobject j_context = base::android::GetApplicationContext();
+ DCHECK(j_context);
+
+ ScopedJavaLocalRef<jstring> j_cookies = ConvertUTF8ToJavaString(
+ env, cookies_);
+ ScopedJavaLocalRef<jstring> j_user_agent = ConvertUTF8ToJavaString(
+ env, user_agent_);
+
+ if (!Java_RemoteMediaPlayerBridge_setDataSource(
+ env, java_bridge_.obj(), j_context, j_url_string.obj(),
+ j_cookies.obj(), j_user_agent.obj(), hide_url_log_)) {
+ OnMediaError(MEDIA_ERROR_FORMAT);
+ return;
+ }
+ }
+
+ if (!Java_RemoteMediaPlayerBridge_prepareAsync(env, java_bridge_.obj()))
+ OnMediaError(MEDIA_ERROR_FORMAT);
+}
+
+void RemoteMediaPlayerBridge::Pause(bool is_media_related_action) {
+ // Ignore the pause if it's not from an event that is explicitly telling
+ // the video to pause. It's possible for Pause() to be called for other
+ // reasons, such as freeing resources, etc. and during those times, the
+ // remote video playback should not be paused.
+ if (is_media_related_action) {
+ if (!in_use_) {
+ pending_play_ = false;
+ } else {
+ if (prepared_ && IsPlaying())
+ PauseInternal();
+ else
+ pending_play_ = false;
+ }
+ }
+}
+
+void RemoteMediaPlayerBridge::SetVideoSurface(gfx::ScopedJavaSurface surface) {
+ // The surface is reset whenever the fullscreen view is destroyed or created.
+ // Since the remote player doesn't use it, we forward it to the local player
+ // for the time when user disconnects and resumes local playback
+ // (see crbug.com/420690).
+ local_player_->SetVideoSurface(surface.Pass());
+}
+
+base::android::ScopedJavaLocalRef<jstring> RemoteMediaPlayerBridge::GetFrameUrl(
+ JNIEnv* env, jobject obj) {
+ return ConvertUTF8ToJavaString(env, frame_url().spec());
+}
+
+void RemoteMediaPlayerBridge::OnPlaying(JNIEnv* env, jobject obj) {
+ static_cast<RemoteMediaPlayerManager *>(manager())->OnPlaying(player_id());
+}
+
+void RemoteMediaPlayerBridge::OnPaused(JNIEnv* env, jobject obj) {
+ static_cast<RemoteMediaPlayerManager *>(manager())->OnPaused(player_id());
+}
+
+void RemoteMediaPlayerBridge::OnRouteSelected(JNIEnv* env, jobject obj,
+ jstring castingMessage) {
+ casting_message_.reset(
+ new std::string(
+ base::android::ConvertJavaStringToUTF8(env, castingMessage)));
+ static_cast<RemoteMediaPlayerManager *>(manager())->OnRemoteDeviceSelected(
+ player_id());
+}
+
+void RemoteMediaPlayerBridge::OnRouteUnselected(JNIEnv* env, jobject obj) {
+ casting_message_.reset();
+ static_cast<RemoteMediaPlayerManager *>(manager())->OnRemoteDeviceUnselected(
+ player_id());
+}
+
+void RemoteMediaPlayerBridge::OnPlaybackFinished(JNIEnv* env, jobject obj) {
+ static_cast<RemoteMediaPlayerManager *>(manager())->OnRemotePlaybackFinished(
+ player_id());
+}
+
+void RemoteMediaPlayerBridge::OnRouteAvailabilityChanged(JNIEnv* env,
+ jobject obj,
+ jboolean available) {
+ static_cast<RemoteMediaPlayerManager *>(manager())->
+ OnRouteAvailabilityChanged(player_id(), available);
+}
+
+// static
+bool RemoteMediaPlayerBridge::RegisterRemoteMediaPlayerBridge(JNIEnv* env) {
+ bool ret = RegisterNativesImpl(env);
+ DCHECK(g_RemoteMediaPlayerBridge_clazz);
+ return ret;
+}
+
+void RemoteMediaPlayerBridge::RequestRemotePlayback() {
+ JNIEnv* env = AttachCurrentThread();
+ CHECK(env);
+
+ Java_RemoteMediaPlayerBridge_requestRemotePlayback(
+ env, java_bridge_.obj());
+}
+
+void RemoteMediaPlayerBridge::RequestRemotePlaybackControl() {
+ JNIEnv* env = AttachCurrentThread();
+ CHECK(env);
+
+ Java_RemoteMediaPlayerBridge_requestRemotePlaybackControl(
+ env, java_bridge_.obj());
+}
+
+void RemoteMediaPlayerBridge::SetNativePlayer() {
+ JNIEnv* env = AttachCurrentThread();
+ CHECK(env);
+
+ Java_RemoteMediaPlayerBridge_setNativePlayer(
+ env, java_bridge_.obj());
+}
+
+void RemoteMediaPlayerBridge::OnPlayerCreated() {
+ JNIEnv* env = AttachCurrentThread();
+ CHECK(env);
+
+ Java_RemoteMediaPlayerBridge_onPlayerCreated(
+ env, java_bridge_.obj());
+}
+
+void RemoteMediaPlayerBridge::OnPlayerDestroyed() {
+ JNIEnv* env = AttachCurrentThread();
+ CHECK(env);
+
+ Java_RemoteMediaPlayerBridge_onPlayerDestroyed(
+ env, java_bridge_.obj());
+}
+
+bool RemoteMediaPlayerBridge::IsRemotePlaybackAvailable() const {
+ JNIEnv* env = AttachCurrentThread();
+ CHECK(env);
+
+ jboolean result = Java_RemoteMediaPlayerBridge_isRemotePlaybackAvailable(
+ env, java_bridge_.obj());
+
+ return result;
+}
+
+bool RemoteMediaPlayerBridge::IsRemotePlaybackPreferredForFrame() const {
+ if (in_use_) {
+ // We have already decided to use remote playback
+ return true;
+ }
+ JNIEnv* env = AttachCurrentThread();
+ CHECK(env);
+
+ jboolean result =
+ Java_RemoteMediaPlayerBridge_isRemotePlaybackPreferredForFrame(
+ env, java_bridge_.obj());
+ return result;
+}
+
+std::string RemoteMediaPlayerBridge::GetCastingMessage() {
+ return casting_message_ ?
+ *casting_message_ : std::string();
+}
+
+void RemoteMediaPlayerBridge::SetPosterBitmap(
+ const std::vector<SkBitmap>& bitmaps) {
+ JNIEnv* env = AttachCurrentThread();
+ CHECK(env);
+
+ if (bitmaps.empty()) {
+ Java_RemoteMediaPlayerBridge_setPosterBitmap(env, java_bridge_.obj(), NULL);
+ } else {
+ ScopedJavaLocalRef<jobject> j_poster_bitmap;
+ j_poster_bitmap = gfx::ConvertToJavaBitmap(&(bitmaps[0]));
+
+ Java_RemoteMediaPlayerBridge_setPosterBitmap(env, java_bridge_.obj(),
+ j_poster_bitmap.obj());
+ }
+}
+
+void RemoteMediaPlayerBridge::Start() {
+ if (!in_use_) {
+ pending_play_ = true;
+ Prepare();
+ } else {
+ if (prepared_)
+ StartInternal();
+ else
+ pending_play_ = true;
+ }
+}
+
+void RemoteMediaPlayerBridge::SeekTo(base::TimeDelta timestamp) {
+ // Record the time to seek when OnMediaPrepared() is called.
+ pending_seek_ = timestamp;
+ should_seek_on_prepare_ = true;
+
+ if (!in_use_)
+ Prepare();
+ else if (prepared_)
+ SeekInternal(timestamp);
+}
+
+void RemoteMediaPlayerBridge::Release() {
+ if (!in_use_)
+ return;
+ time_update_timer_.Stop();
+ if (prepared_) {
+ pending_seek_ = GetCurrentTime();
+ should_seek_on_prepare_ = true;
+ }
+
+ prepared_ = false;
+ pending_play_ = false;
+ JNIEnv* env = AttachCurrentThread();
+ Java_RemoteMediaPlayerBridge_release(env, java_bridge_.obj());
+ DetachListener();
+ in_use_ = false;
+}
+
+void RemoteMediaPlayerBridge::SetVolume(double volume) {
+ if (!in_use_) {
+ volume_ = volume;
+ return;
+ }
+
+ JNIEnv* env = AttachCurrentThread();
+ CHECK(env);
+ Java_RemoteMediaPlayerBridge_setVolume(
+ env, java_bridge_.obj(), volume);
+}
+
+base::TimeDelta RemoteMediaPlayerBridge::GetCurrentTime() {
+ if (!prepared_)
+ return pending_seek_;
+ JNIEnv* env = AttachCurrentThread();
+ return base::TimeDelta::FromMilliseconds(
+ Java_RemoteMediaPlayerBridge_getCurrentPosition(
+ env, java_bridge_.obj()));
+}
+
+base::TimeDelta RemoteMediaPlayerBridge::GetDuration() {
+ if (!prepared_)
+ return duration_;
+ JNIEnv* env = AttachCurrentThread();
+ const int duration_ms =
+ Java_RemoteMediaPlayerBridge_getDuration(env, java_bridge_.obj());
+ // Sometimes we can't get the duration remotely, but the local media player
+ // knows it.
+ // TODO (aberent) This is for YouTube. Remove it when the YouTube receiver is
+ // fixed.
+ if (duration_ms == 0) {
+ return local_player_->GetDuration();
+ }
+ return duration_ms < 0 ? media::kInfiniteDuration()
+ : base::TimeDelta::FromMilliseconds(duration_ms);
+}
+
+bool RemoteMediaPlayerBridge::IsPlaying() {
+ if (!prepared_)
+ return pending_play_;
+
+ JNIEnv* env = AttachCurrentThread();
+ CHECK(env);
+ jboolean result = Java_RemoteMediaPlayerBridge_isPlaying(
+ env, java_bridge_.obj());
+ return result;
+}
+
+bool RemoteMediaPlayerBridge::CanPause() {
+ return true;
+}
+
+bool RemoteMediaPlayerBridge::CanSeekForward() {
+ return true;
+}
+
+bool RemoteMediaPlayerBridge::CanSeekBackward() {
+ return true;
+}
+
+bool RemoteMediaPlayerBridge::IsPlayerReady() {
+ return prepared_;
+}
+
+GURL RemoteMediaPlayerBridge::GetUrl() {
+ return url_;
+}
+
+GURL RemoteMediaPlayerBridge::GetFirstPartyForCookies() {
+ return first_party_for_cookies_;
+}
+
+void RemoteMediaPlayerBridge::Initialize() {
+ cookies_.clear();
+ media::MediaResourceGetter* resource_getter =
+ manager()->GetMediaResourceGetter();
+ resource_getter->GetCookies(
+ url_, first_party_for_cookies_,
+ base::Bind(&RemoteMediaPlayerBridge::OnCookiesRetrieved,
+ weak_factory_.GetWeakPtr()));
+}
+
+bool RemoteMediaPlayerBridge::IsMediaPlayableRemotely() const {
+ JNIEnv* env = AttachCurrentThread();
+ CHECK(env);
+
+ return Java_RemoteMediaPlayerBridge_isMediaPlayableRemotely(
+ env, java_bridge_.obj());
+}
+
+base::android::ScopedJavaLocalRef<jstring> RemoteMediaPlayerBridge::GetTitle(
+ JNIEnv* env, jobject obj) {
+ base::string16 title;
+ content::ContentViewCoreImpl* core =
+ static_cast<RemoteMediaPlayerManager*>(manager())->GetContentViewCore();
+ if (core) {
+ content::WebContents* contents = core->GetWebContents();
+ if (contents) {
+ title = contents->GetTitle();
+ }
+ }
+ return base::android::ConvertUTF16ToJavaString(env, title);
+}
+
+void RemoteMediaPlayerBridge::OnCookiesRetrieved(const std::string& cookies) {
+ // TODO(aberent) Do we need to retrieve auth credentials for basic
+ // authentication? MediaPlayerBridge does.
+ cookies_ = cookies;
+}
+
+} // namespace remote_media
diff --git a/chrome/browser/media/android/remote/remote_media_player_bridge.h b/chrome/browser/media/android/remote/remote_media_player_bridge.h
new file mode 100644
index 0000000..f1f506c
--- /dev/null
+++ b/chrome/browser/media/android/remote/remote_media_player_bridge.h
@@ -0,0 +1,154 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_MEDIA_ANDROID_REMOTE_REMOTE_MEDIA_PLAYER_BRIDGE_H_
+#define CHROME_BROWSER_MEDIA_ANDROID_REMOTE_REMOTE_MEDIA_PLAYER_BRIDGE_H_
+
+#include <jni.h>
+#include <vector>
+
+#include "base/time/time.h"
+#include "base/timer/timer.h"
+#include "media/base/android/media_player_bridge.h"
+
+class SkBitmap;
+
+// This is the version of MediaPlayerBridge that handles the remote media
+// playback.
+
+namespace remote_media {
+
+class RemoteMediaPlayerManager;
+
+class RemoteMediaPlayerBridge : public media::MediaPlayerAndroid {
+ public:
+ RemoteMediaPlayerBridge(
+ MediaPlayerAndroid* local_player,
+ const std::string& user_agent,
+ bool hide_url_log,
+ RemoteMediaPlayerManager* manager);
+ ~RemoteMediaPlayerBridge() override;
+
+ static bool RegisterRemoteMediaPlayerBridge(JNIEnv* env);
+
+ // Initialize this object.
+ virtual void Initialize();
+
+ // MediaPlayerAndroid implementation.
+ void SetVideoSurface(gfx::ScopedJavaSurface surface) override;
+ void Start() override;
+ void Pause(bool is_media_related_action) override;
+ void SeekTo(base::TimeDelta timestamp) override;
+ void Release() override;
+ void SetVolume(double volume) override;
+ int GetVideoWidth() override;
+ int GetVideoHeight() override;
+ base::TimeDelta GetCurrentTime() override;
+ base::TimeDelta GetDuration() override;
+ bool IsPlaying() override;
+ bool CanPause() override;
+ bool CanSeekForward() override;
+ bool CanSeekBackward() override;
+ bool IsPlayerReady() override;
+ GURL GetUrl() override;
+ GURL GetFirstPartyForCookies() override;
+
+ // JNI functions
+ base::android::ScopedJavaLocalRef<jstring> GetFrameUrl(
+ JNIEnv* env, jobject obj);
+ void OnPlaying(JNIEnv* env, jobject obj);
+ void OnPaused(JNIEnv* env, jobject obj);
+ void OnRouteSelected(JNIEnv* env, jobject obj, jstring castingMessage);
+ void OnRouteUnselected(JNIEnv* env, jobject obj);
+ void OnPlaybackFinished(JNIEnv* env, jobject obj);
+ void OnRouteAvailabilityChanged(JNIEnv* env, jobject obj, jboolean available);
+ base::android::ScopedJavaLocalRef<jstring> GetTitle(JNIEnv* env, jobject obj);
+
+ // Wrappers for calls to Java used by the remote media player manager
+ void RequestRemotePlayback();
+ void RequestRemotePlaybackControl();
+ void SetNativePlayer();
+ void OnPlayerCreated();
+ void OnPlayerDestroyed();
+ bool IsRemotePlaybackAvailable() const;
+ bool IsRemotePlaybackPreferredForFrame() const;
+
+ // Returns true if the we can play the media remotely
+ bool IsMediaPlayableRemotely() const;
+
+ // Gets the message to display on the embedded player while casting.
+ std::string GetCastingMessage();
+
+ // Tell the java side about the poster image for a given media.
+ void SetPosterBitmap(const std::vector<SkBitmap>& bitmaps);
+
+ protected:
+ // MediaPlayerAndroid implementation.
+ void OnVideoSizeChanged(int width, int height) override;
+ void OnPlaybackComplete() override;
+ void OnMediaInterrupted() override;
+ void OnMediaPrepared() override;
+
+ private:
+ // Functions that implements media player control.
+ void StartInternal();
+ void PauseInternal();
+ void SeekInternal(base::TimeDelta time);
+
+ // Called when |time_update_timer_| fires.
+ void OnTimeUpdateTimerFired();
+
+ // Callback function passed to |resource_getter_|. Called when the cookies
+ // are retrieved.
+ void OnCookiesRetrieved(const std::string& cookies);
+
+ void PendingSeekInternal(const base::TimeDelta& time);
+
+ // Prepare the player for playback, asynchronously. When succeeds,
+ // OnMediaPrepared() will be called. Otherwise, OnMediaError() will
+ // be called with an error type.
+ void Prepare();
+
+ long start_position_millis_;
+ MediaPlayerAndroid* local_player_;
+ bool in_use_;
+ bool prepared_;
+ bool pending_play_;
+ int width_;
+ int height_;
+ base::RepeatingTimer<RemoteMediaPlayerBridge> time_update_timer_;
+ base::TimeDelta duration_;
+ bool should_seek_on_prepare_;
+ base::TimeDelta pending_seek_;
+
+ // Hide url log from media player.
+ bool hide_url_log_;
+
+ // Volume of playback.
+ double volume_;
+
+ // Url for playback.
+ GURL url_;
+
+ // First party url for cookies.
+ GURL first_party_for_cookies_;
+
+ // Cookies for |url_|.
+ std::string cookies_;
+
+ // User agent string to be used for media player.
+ const std::string user_agent_;
+
+ base::android::ScopedJavaGlobalRef<jobject> java_bridge_;
+ scoped_ptr<std::string> casting_message_;
+
+ // NOTE: Weak pointers must be invalidated before all other member variables.
+ base::WeakPtrFactory<RemoteMediaPlayerBridge> weak_factory_;
+
+ DISALLOW_COPY_AND_ASSIGN(RemoteMediaPlayerBridge);
+};
+
+} // namespace remote_media
+
+#endif // CHROME_BROWSER_MEDIA_ANDROID_REMOTE_REMOTE_MEDIA_PLAYER_BRIDGE_H_
diff --git a/chrome/browser/media/android/remote/remote_media_player_manager.cc b/chrome/browser/media/android/remote/remote_media_player_manager.cc
new file mode 100644
index 0000000..6c0daa4
--- /dev/null
+++ b/chrome/browser/media/android/remote/remote_media_player_manager.cc
@@ -0,0 +1,310 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/media/android/remote/remote_media_player_manager.h"
+
+#include "chrome/browser/android/tab_android.h"
+#include "chrome/common/chrome_content_client.h"
+#include "content/common/media/media_player_messages_android.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+#include "ui/gfx/android/java_bitmap.h"
+
+using media::MediaPlayerAndroid;
+
+namespace remote_media {
+
+RemoteMediaPlayerManager::RemoteMediaPlayerManager(
+ content::RenderFrameHost* render_frame_host,
+ content::MediaPlayersObserver* audio_monitor)
+ : BrowserMediaPlayerManager(render_frame_host, audio_monitor),
+ weak_ptr_factory_(this) {
+}
+
+RemoteMediaPlayerManager::~RemoteMediaPlayerManager() {}
+
+void RemoteMediaPlayerManager::OnStart(int player_id) {
+ // TODO(aberent) This assumes this is the first time we have started this
+ // video, rather than restarting after pause. There is a lot of logic here
+ // that is unnecessary if we are restarting after pause.
+ if (MaybeStartPlayingRemotely(player_id))
+ return;
+
+ ReplaceRemotePlayerWithLocal();
+ BrowserMediaPlayerManager::OnStart(player_id);
+}
+
+void RemoteMediaPlayerManager::OnInitialize(
+ const MediaPlayerHostMsg_Initialize_Params& media_params) {
+ BrowserMediaPlayerManager::OnInitialize(media_params);
+
+ MediaPlayerAndroid* player = GetPlayer(media_params.player_id);
+ if (player) {
+ CreateRemoteMediaPlayer(player);
+ RemoteMediaPlayerBridge* remote_player = GetRemotePlayer(
+ media_params.player_id);
+ if (remote_player)
+ remote_player->OnPlayerCreated();
+ }
+}
+
+void RemoteMediaPlayerManager::OnDestroyPlayer(int player_id) {
+ RemoteMediaPlayerBridge* player = GetRemotePlayer(player_id);
+ if (player)
+ player->OnPlayerDestroyed();
+ BrowserMediaPlayerManager::OnDestroyPlayer(player_id);
+}
+
+void RemoteMediaPlayerManager::OnReleaseResources(int player_id) {
+ // We only want to release resources of local players.
+ if (player_id != RemotePlayerId())
+ BrowserMediaPlayerManager::OnReleaseResources(player_id);
+}
+
+void RemoteMediaPlayerManager::OnRequestRemotePlayback(int player_id) {
+ RemoteMediaPlayerBridge* player = GetRemotePlayer(player_id);
+ if (player)
+ player->RequestRemotePlayback();
+}
+
+void RemoteMediaPlayerManager::OnRequestRemotePlaybackControl(int player_id) {
+ RemoteMediaPlayerBridge* player = GetRemotePlayer(player_id);
+ if (player)
+ player->RequestRemotePlaybackControl();
+}
+
+int RemoteMediaPlayerManager::GetTabId() {
+ if (!web_contents())
+ return -1;
+
+ TabAndroid* tab = TabAndroid::FromWebContents(web_contents());
+ if (!tab)
+ return -1;
+
+ return tab->GetAndroidId();
+}
+
+void RemoteMediaPlayerManager::OnSetPoster(int player_id, const GURL& url) {
+ RemoteMediaPlayerBridge* player = GetRemotePlayer(player_id);
+
+ if (player && url.is_empty()) {
+ player->SetPosterBitmap(std::vector<SkBitmap>());
+ } else {
+ // TODO(aberent) OnSetPoster is called when the attributes of the video
+ // element are parsed, which may be before OnInitialize is called. We are
+ // here relying on the image fetch taking longer than the delay until
+ // OnInitialize is called, and hence the player is created. This is not
+ // guaranteed.
+ content::WebContents::ImageDownloadCallback callback = base::Bind(
+ &RemoteMediaPlayerManager::DidDownloadPoster,
+ weak_ptr_factory_.GetWeakPtr(), player_id);
+ web_contents()->DownloadImage(
+ url,
+ false, // is_favicon, false so that cookies will be used.
+ 0, // max_bitmap_size, 0 means no limit.
+ false, // normal cache policy.
+ callback);
+ }
+}
+
+void RemoteMediaPlayerManager::DidDownloadPoster(
+ int player_id,
+ int id,
+ int http_status_code,
+ const GURL& image_url,
+ const std::vector<SkBitmap>& bitmaps,
+ const std::vector<gfx::Size>& original_bitmap_sizes) {
+ RemoteMediaPlayerBridge* player = GetRemotePlayer(player_id);
+ if (player)
+ player->SetPosterBitmap(bitmaps);
+}
+
+RemoteMediaPlayerBridge* RemoteMediaPlayerManager::CreateRemoteMediaPlayer(
+ MediaPlayerAndroid* local_player) {
+ RemoteMediaPlayerBridge* player = new RemoteMediaPlayerBridge(
+ local_player,
+ GetUserAgent(),
+ false,
+ this);
+ remote_players_.push_back(player);
+ player->Initialize();
+ return player;
+}
+
+int RemoteMediaPlayerManager::RemotePlayerId() {
+ // The remote player is created with the same id as the corresponding local
+ // player.
+ if (replaced_local_player_.get())
+ return replaced_local_player_->player_id();
+ else
+ return -1;
+}
+
+void RemoteMediaPlayerManager::ReplaceLocalPlayerWithRemote(
+ MediaPlayerAndroid* player) {
+ if (!player)
+ return;
+
+ int player_id = player->player_id();
+ if (player_id == RemotePlayerId()) {
+ // The player is already remote.
+ return;
+ }
+
+ // Before we replace the new remote player, put the old local player back
+ // in its place.
+ ReplaceRemotePlayerWithLocal();
+
+ // Pause the local player first before replacing it. This will allow the local
+ // player to reset its state, such as the PowerSaveBlocker.
+ // We have to pause locally as well as telling the renderer to pause, because
+ // by the time the renderer comes back to us telling us to pause we will have
+ // switched players.
+ player->Pause(true);
+ Send(new MediaPlayerMsg_DidMediaPlayerPause(RoutingID(), player_id));
+
+ // Find the remote player
+ for (auto it = remote_players_.begin(); it != remote_players_.end(); ++it) {
+ if ((*it)->player_id() == player_id) {
+ replaced_local_player_ = SwapPlayer(player_id, *it);
+
+ // Seek to the previous player's position.
+ (*it)->SeekTo(player->GetCurrentTime());
+
+ // SwapPlayers takes ownership, so we have to remove the remote player
+ // from the vector.
+ remote_players_.weak_erase(it);
+ break;
+ }
+ }
+}
+
+void RemoteMediaPlayerManager::ReplaceRemotePlayerWithLocal() {
+ int player_id = RemotePlayerId();
+ if (player_id == -1)
+ return;
+
+ Send(new MediaPlayerMsg_DidMediaPlayerPause(RoutingID(), player_id));
+ Send(new MediaPlayerMsg_DisconnectedFromRemoteDevice(RoutingID(), player_id));
+
+ scoped_ptr<MediaPlayerAndroid> remote_player =
+ SwapPlayer(player_id, replaced_local_player_.release());
+ if (remote_player) {
+ // Seek to the previous player's position.
+ GetPlayer(player_id)->SeekTo(remote_player->GetCurrentTime());
+
+ remote_player->Release();
+ // Add the remote player back into the list
+ remote_players_.push_back(
+ static_cast<RemoteMediaPlayerBridge *>(remote_player.release()));
+ }
+}
+
+bool RemoteMediaPlayerManager::MaybeStartPlayingRemotely(int player_id) {
+ MediaPlayerAndroid* player = GetPlayer(player_id);
+ if (!player)
+ return false;
+
+ RemoteMediaPlayerBridge* remote_player = GetRemotePlayer(player_id);
+
+ if (!remote_player)
+ return false;
+
+ if (remote_player->IsMediaPlayableRemotely() &&
+ remote_player->IsRemotePlaybackAvailable() &&
+ remote_player->IsRemotePlaybackPreferredForFrame()) {
+ ReplaceLocalPlayerWithRemote(player);
+
+ remote_player->SetNativePlayer();
+ remote_player->Start();
+
+ Send(new MediaPlayerMsg_DidMediaPlayerPlay(RoutingID(), player_id));
+
+ Send(new MediaPlayerMsg_ConnectedToRemoteDevice(
+ RoutingID(),
+ player_id,
+ remote_player->GetCastingMessage()));
+
+ return true;
+ }
+
+ return false;
+}
+
+void RemoteMediaPlayerManager::OnRemoteDeviceSelected(int player_id) {
+
+ MediaPlayerAndroid* player = GetPlayer(player_id);
+ if (!player)
+ return;
+
+ if (MaybeStartPlayingRemotely(player_id))
+ return;
+ OnStart(player_id);
+}
+
+void RemoteMediaPlayerManager::OnRemoteDeviceUnselected(int player_id) {
+ if (player_id == RemotePlayerId())
+ ReplaceRemotePlayerWithLocal();
+}
+
+void RemoteMediaPlayerManager::OnRemotePlaybackFinished(int player_id) {
+ if (player_id == RemotePlayerId())
+ ReplaceRemotePlayerWithLocal();
+}
+
+void RemoteMediaPlayerManager::OnRouteAvailabilityChanged(
+ int player_id, bool routes_available) {
+ Send(
+ new MediaPlayerMsg_RemoteRouteAvailabilityChanged(RoutingID(), player_id,
+ routes_available));
+}
+
+void RemoteMediaPlayerManager::ReleaseFullscreenPlayer(
+ MediaPlayerAndroid* player) {
+ // Release the original player's resources, not the current fullscreen player
+ // (which is the remote player).
+ if (replaced_local_player_.get())
+ replaced_local_player_->Release();
+ else
+ BrowserMediaPlayerManager::ReleaseFullscreenPlayer(player);
+}
+
+void RemoteMediaPlayerManager::OnPlaying(int player_id) {
+ Send(new MediaPlayerMsg_DidMediaPlayerPlay(RoutingID(),player_id));
+}
+
+void RemoteMediaPlayerManager::OnPaused(int player_id) {
+ Send(new MediaPlayerMsg_DidMediaPlayerPause(RoutingID(),player_id));
+}
+
+RemoteMediaPlayerBridge* RemoteMediaPlayerManager::GetRemotePlayer(
+ int player_id) {
+ if (player_id == RemotePlayerId()) {
+ return static_cast<RemoteMediaPlayerBridge*>(GetPlayer(player_id));
+ } else {
+ for (RemoteMediaPlayerBridge* player : remote_players_) {
+ if (player->player_id() == player_id) {
+ return player;
+ }
+ }
+ return nullptr;
+ }
+}
+
+void RemoteMediaPlayerManager::OnMediaMetadataChanged(int player_id,
+ base::TimeDelta duration,
+ int width,
+ int height,
+ bool success) {
+ if (player_id == RemotePlayerId() && replaced_local_player_) {
+ Send(new MediaPlayerMsg_MediaMetadataChanged(
+ RoutingID(), player_id, duration,
+ replaced_local_player_->GetVideoWidth(),
+ replaced_local_player_->GetVideoHeight(), success));
+ } else {
+ BrowserMediaPlayerManager::OnMediaMetadataChanged(player_id, duration,
+ width, height, success);
+ }
+}
+
+} // namespace remote_media
diff --git a/chrome/browser/media/android/remote/remote_media_player_manager.h b/chrome/browser/media/android/remote/remote_media_player_manager.h
new file mode 100644
index 0000000..dfe3d64
--- /dev/null
+++ b/chrome/browser/media/android/remote/remote_media_player_manager.h
@@ -0,0 +1,115 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_MEDIA_ANDROID_REMOTE_REMOTE_MEDIA_PLAYER_MANAGER_H_
+#define CHROME_BROWSER_MEDIA_ANDROID_REMOTE_REMOTE_MEDIA_PLAYER_MANAGER_H_
+
+#include <vector>
+
+#include "base/memory/scoped_ptr.h"
+#include "base/memory/scoped_vector.h"
+#include "chrome/browser/media/android/remote/remote_media_player_bridge.h"
+#include "content/browser/media/android/browser_media_player_manager.h"
+#include "media/base/android/media_player_android.h"
+
+struct MediaPlayerHostMsg_Initialize_Params;
+
+namespace remote_media {
+
+// media::MediaPlayerManager implementation that allows the user to play media
+// remotely.
+class RemoteMediaPlayerManager : public content::BrowserMediaPlayerManager {
+ public:
+ RemoteMediaPlayerManager(
+ content::RenderFrameHost* render_frame_host,
+ content::MediaPlayersObserver* audio_monitor);
+ ~RemoteMediaPlayerManager() override;
+
+ void OnPlaying(int player_id);
+ void OnPaused(int player_id);
+
+ // Callback to trigger when a remote device has been selected.
+ void OnRemoteDeviceSelected(int player_id);
+
+ // Callback to trigger when a remote device has been unselected.
+ void OnRemoteDeviceUnselected(int player_id);
+
+ // Callback to trigger when the video on a remote device finishes playing.
+ void OnRemotePlaybackFinished(int player_id);
+
+ // Callback to trigger when the availability of remote routes changes.
+ void OnRouteAvailabilityChanged(int tab_id, bool routes_available);
+
+ void OnMediaMetadataChanged(int player_id,
+ base::TimeDelta duration,
+ int width,
+ int height,
+ bool success) override;
+
+ protected:
+ void OnSetPoster(int player_id, const GURL& url) override;
+
+ private:
+ // Returns a MediaPlayerAndroid implementation for playing the media remotely.
+ RemoteMediaPlayerBridge* CreateRemoteMediaPlayer(
+ media::MediaPlayerAndroid* local_player);
+
+ // Replaces the given local player with the remote one. Does nothing if the
+ // player is remote already.
+ void ReplaceLocalPlayerWithRemote(media::MediaPlayerAndroid* player);
+
+ // Replaces the remote player with the local player this class is holding.
+ // Does nothing if there is no remote player.
+ void ReplaceRemotePlayerWithLocal();
+
+ // Checks if the URL managed by the player should be played remotely.
+ // Returns true if the manager should do nothing, false if it needs to
+ // proceed.
+ bool MaybeStartPlayingRemotely(int player_id);
+
+ // content::BrowserMediaPlayerManager overrides.
+ void OnStart(int player_id) override;
+ void OnInitialize(
+ const MediaPlayerHostMsg_Initialize_Params& media_player_params) override;
+ void OnDestroyPlayer(int player_id) override;
+ void OnReleaseResources(int player_id) override;
+ void OnRequestRemotePlayback(int player_id) override;
+ void OnRequestRemotePlaybackControl(int player_id) override;
+
+ void ReleaseFullscreenPlayer(media::MediaPlayerAndroid* player) override;
+
+ // Callback for when the download of poster image is done.
+ void DidDownloadPoster(
+ int player_id,
+ int id,
+ int http_status_code,
+ const GURL& image_url,
+ const std::vector<SkBitmap>& bitmaps,
+ const std::vector<gfx::Size>& original_bitmap_sizes);
+
+ // Return the ID of the tab that's associated with this controller. Returns
+ // -1 in case something goes wrong.
+ int GetTabId();
+
+ // Get the player id of current remote player, if any, or -1 if none.
+ int RemotePlayerId();
+
+ // Get the remote player for a given player id, whether or not it is currently
+ // playing remotely.
+ RemoteMediaPlayerBridge* GetRemotePlayer(int player_id);
+
+ // The local player that we have replaced with a remote player. This is NULL
+ // if we do not have a remote player currently running.
+ scoped_ptr<media::MediaPlayerAndroid> replaced_local_player_;
+
+ ScopedVector<RemoteMediaPlayerBridge> remote_players_;
+
+ base::WeakPtrFactory<RemoteMediaPlayerManager> weak_ptr_factory_;
+
+ DISALLOW_COPY_AND_ASSIGN(RemoteMediaPlayerManager);
+};
+
+} // namespace remote_media
+
+#endif // CHROME_BROWSER_MEDIA_ANDROID_REMOTE_REMOTE_MEDIA_PLAYER_MANAGER_H_
diff --git a/chrome/chrome.gyp b/chrome/chrome.gyp
index 47188f1..9aa74fc 100644
--- a/chrome/chrome.gyp
+++ b/chrome/chrome.gyp
@@ -622,10 +622,13 @@
'../components/components.gyp:variations_java',
'../components/components.gyp:web_contents_delegate_android_java',
'../content/content.gyp:content_java',
+ '../media/media.gyp:media_java',
'../printing/printing.gyp:printing_java',
'../sync/sync.gyp:sync_java',
'../third_party/android_data_chart/android_data_chart.gyp:android_data_chart_java',
+ '../third_party/android_media/android_media.gyp:android_media_java',
'../third_party/android_tools/android_tools.gyp:android_support_v7_appcompat_javalib',
+ '../third_party/android_tools/android_tools.gyp:android_support_v7_mediarouter_javalib',
'../third_party/android_tools/android_tools.gyp:android_support_v13_javalib',
'../third_party/android_tools/android_tools.gyp:google_play_services_javalib',
'../ui/android/ui_android.gyp:ui_java',
diff --git a/chrome/chrome_browser.gypi b/chrome/chrome_browser.gypi
index 061e1f1..8ac211f 100644
--- a/chrome/chrome_browser.gypi
+++ b/chrome/chrome_browser.gypi
@@ -500,6 +500,12 @@
'browser/mac/security_wrappers.h',
'browser/manifest/manifest_icon_selector.cc',
'browser/manifest/manifest_icon_selector.h',
+ 'browser/media/android/remote/record_cast_action.cc',
+ 'browser/media/android/remote/record_cast_action.h',
+ 'browser/media/android/remote/remote_media_player_bridge.cc',
+ 'browser/media/android/remote/remote_media_player_bridge.h',
+ 'browser/media/android/remote/remote_media_player_manager.cc',
+ 'browser/media/android/remote/remote_media_player_manager.h',
'browser/media/desktop_media_list.h',
'browser/media/desktop_media_picker.h',
'browser/media/desktop_streams_registry.cc',
@@ -1641,6 +1647,8 @@
'android/java/src/org/chromium/chrome/browser/NewTabPagePrefs.java',
'android/java/src/org/chromium/chrome/browser/IntentHelper.java',
'android/java/src/org/chromium/chrome/browser/JavascriptAppModalDialog.java',
+ 'android/java/src/org/chromium/chrome/browser/media/remote/RecordCastAction.java',
+ 'android/java/src/org/chromium/chrome/browser/media/remote/RemoteMediaPlayerBridge.java',
'android/java/src/org/chromium/chrome/browser/metrics/UmaBridge.java',
'android/java/src/org/chromium/chrome/browser/metrics/UmaSessionStats.java',
'android/java/src/org/chromium/chrome/browser/metrics/UmaUtils.java',
diff --git a/content/browser/media/android/browser_media_player_manager.cc b/content/browser/media/android/browser_media_player_manager.cc
index 23ba8c3..25aebf2 100644
--- a/content/browser/media/android/browser_media_player_manager.cc
+++ b/content/browser/media/android/browser_media_player_manager.cc
@@ -47,7 +47,12 @@ static media::MediaUrlInterceptor* media_url_interceptor_ = NULL;
// static
void BrowserMediaPlayerManager::RegisterFactory(Factory factory) {
- g_factory = factory;
+ // TODO(aberent) nullptr test is a temporary fix to simplify upstreaming Cast.
+ // Until Cast is fully upstreamed we want the downstream factory to take
+ // priority over the upstream factory. The downstream call happens first,
+ // so this will ensure that it does.
+ if (g_factory == nullptr)
+ g_factory = factory;
}
// static