Remove ExoCast
PiperOrigin-RevId: 255964199
This commit is contained in:
parent
04959ec648
commit
7798c07f64
@ -1,310 +0,0 @@
|
|||||||
# Copyright (C) 2019 The Android Open Source Project
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_library")
|
|
||||||
load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_binary")
|
|
||||||
load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_test")
|
|
||||||
load("@io_bazel_rules_closure//closure:defs.bzl", "closure_css_library")
|
|
||||||
load("@io_bazel_rules_closure//closure:defs.bzl", "closure_css_binary")
|
|
||||||
|
|
||||||
licenses(["notice"]) # Apache 2.0
|
|
||||||
|
|
||||||
# The Shaka player library - 2.5.0-beta2 (needs to be cloned from Github).
|
|
||||||
closure_js_library(
|
|
||||||
name = "shaka_player_library",
|
|
||||||
srcs = glob(
|
|
||||||
[
|
|
||||||
"external-js/shaka-player/lib/**/*.js",
|
|
||||||
"external-js/shaka-player/externs/**/*.js",
|
|
||||||
],
|
|
||||||
exclude = [
|
|
||||||
"external-js/shaka-player/lib/debug/asserts.js",
|
|
||||||
"external-js/shaka-player/externs/mediakeys.js",
|
|
||||||
"external-js/shaka-player/externs/networkinformation.js",
|
|
||||||
"external-js/shaka-player/externs/vtt_region.js",
|
|
||||||
],
|
|
||||||
),
|
|
||||||
suppress = [
|
|
||||||
"strictMissingRequire",
|
|
||||||
"missingSourcesWarnings",
|
|
||||||
"analyzerChecks",
|
|
||||||
"strictCheckTypes",
|
|
||||||
"checkTypes",
|
|
||||||
],
|
|
||||||
deps = [
|
|
||||||
"@io_bazel_rules_closure//closure/library",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
# The plain player not depending on the cast library.
|
|
||||||
closure_js_library(
|
|
||||||
name = "player_lib",
|
|
||||||
srcs = [
|
|
||||||
"externs/protocol.js",
|
|
||||||
"src/configuration_factory.js",
|
|
||||||
"src/constants.js",
|
|
||||||
"src/playback_info_view.js",
|
|
||||||
"src/player.js",
|
|
||||||
"src/timeout.js",
|
|
||||||
"src/util.js",
|
|
||||||
],
|
|
||||||
suppress = [
|
|
||||||
"missingSourcesWarnings",
|
|
||||||
"analyzerChecks",
|
|
||||||
"strictCheckTypes",
|
|
||||||
],
|
|
||||||
deps = [
|
|
||||||
":shaka_player_library",
|
|
||||||
"@io_bazel_rules_closure//closure/library",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
# A debug app to test the player with a desktop browser.
|
|
||||||
closure_js_library(
|
|
||||||
name = "app_desktop_lib",
|
|
||||||
srcs = [
|
|
||||||
"app-desktop/src/main.js",
|
|
||||||
"app-desktop/src/player_controls.js",
|
|
||||||
"app-desktop/src/samples.js",
|
|
||||||
"externs/shaka.js",
|
|
||||||
],
|
|
||||||
suppress = [
|
|
||||||
"reportUnknownTypes",
|
|
||||||
"strictCheckTypes",
|
|
||||||
],
|
|
||||||
deps = [
|
|
||||||
":player_lib",
|
|
||||||
":shaka_player_library",
|
|
||||||
"@io_bazel_rules_closure//closure/library",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Includes the javascript files of the cast receiver app.
|
|
||||||
closure_js_library(
|
|
||||||
name = "app_lib",
|
|
||||||
srcs = [
|
|
||||||
"app/src/main.js",
|
|
||||||
"app/src/message_dispatcher.js",
|
|
||||||
"app/src/receiver.js",
|
|
||||||
"app/src/validation.js",
|
|
||||||
"externs/cast.js",
|
|
||||||
"externs/shaka.js",
|
|
||||||
],
|
|
||||||
suppress = [
|
|
||||||
"missingSourcesWarnings",
|
|
||||||
"analyzerChecks",
|
|
||||||
"strictCheckTypes",
|
|
||||||
],
|
|
||||||
deps = [
|
|
||||||
":player_lib",
|
|
||||||
":shaka_player_library",
|
|
||||||
"@io_bazel_rules_closure//closure/library",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Test utils like mocks.
|
|
||||||
closure_js_library(
|
|
||||||
name = "test_util_lib",
|
|
||||||
testonly = 1,
|
|
||||||
srcs = [
|
|
||||||
"externs/protocol.js",
|
|
||||||
"test/externs.js",
|
|
||||||
"test/mocks.js",
|
|
||||||
"test/util.js",
|
|
||||||
],
|
|
||||||
suppress = [
|
|
||||||
"checkTypes",
|
|
||||||
"strictCheckTypes",
|
|
||||||
"reportUnknownTypes",
|
|
||||||
"accessControls",
|
|
||||||
"analyzerChecks",
|
|
||||||
"missingSourcesWarnings",
|
|
||||||
],
|
|
||||||
deps = [
|
|
||||||
":shaka_player_library",
|
|
||||||
"@io_bazel_rules_closure//closure/library",
|
|
||||||
"@io_bazel_rules_closure//closure/library/testing:jsunit",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Unit test for the player.
|
|
||||||
closure_js_test(
|
|
||||||
name = "player_tests",
|
|
||||||
srcs = glob([
|
|
||||||
"test/player_test.js",
|
|
||||||
]),
|
|
||||||
entry_points = [
|
|
||||||
"exoplayer.cast.test",
|
|
||||||
],
|
|
||||||
suppress = [
|
|
||||||
"checkTypes",
|
|
||||||
"strictCheckTypes",
|
|
||||||
"reportUnknownTypes",
|
|
||||||
"accessControls",
|
|
||||||
"analyzerChecks",
|
|
||||||
"missingSourcesWarnings",
|
|
||||||
],
|
|
||||||
deps = [
|
|
||||||
":app_lib",
|
|
||||||
":player_lib",
|
|
||||||
":test_util_lib",
|
|
||||||
"@io_bazel_rules_closure//closure/library/testing:asserts",
|
|
||||||
"@io_bazel_rules_closure//closure/library/testing:jsunit",
|
|
||||||
"@io_bazel_rules_closure//closure/library/testing:testsuite",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Unit test for the queue in the player.
|
|
||||||
closure_js_test(
|
|
||||||
name = "queue_tests",
|
|
||||||
srcs = glob([
|
|
||||||
"test/queue_test.js",
|
|
||||||
]),
|
|
||||||
entry_points = [
|
|
||||||
"exoplayer.cast.test.queue",
|
|
||||||
],
|
|
||||||
suppress = [
|
|
||||||
"checkTypes",
|
|
||||||
"strictCheckTypes",
|
|
||||||
"reportUnknownTypes",
|
|
||||||
"accessControls",
|
|
||||||
"analyzerChecks",
|
|
||||||
"missingSourcesWarnings",
|
|
||||||
],
|
|
||||||
deps = [
|
|
||||||
":app_lib",
|
|
||||||
":player_lib",
|
|
||||||
":test_util_lib",
|
|
||||||
"@io_bazel_rules_closure//closure/library/testing:asserts",
|
|
||||||
"@io_bazel_rules_closure//closure/library/testing:jsunit",
|
|
||||||
"@io_bazel_rules_closure//closure/library/testing:testsuite",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Unit test for the receiver.
|
|
||||||
closure_js_test(
|
|
||||||
name = "receiver_tests",
|
|
||||||
srcs = glob([
|
|
||||||
"test/receiver_test.js",
|
|
||||||
]),
|
|
||||||
entry_points = [
|
|
||||||
"exoplayer.cast.test.receiver",
|
|
||||||
],
|
|
||||||
suppress = [
|
|
||||||
"checkTypes",
|
|
||||||
"strictCheckTypes",
|
|
||||||
"reportUnknownTypes",
|
|
||||||
"accessControls",
|
|
||||||
"analyzerChecks",
|
|
||||||
"missingSourcesWarnings",
|
|
||||||
],
|
|
||||||
deps = [
|
|
||||||
":app_lib",
|
|
||||||
":player_lib",
|
|
||||||
":test_util_lib",
|
|
||||||
"@io_bazel_rules_closure//closure/library/testing:asserts",
|
|
||||||
"@io_bazel_rules_closure//closure/library/testing:jsunit",
|
|
||||||
"@io_bazel_rules_closure//closure/library/testing:testsuite",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Unit test for the validations.
|
|
||||||
closure_js_test(
|
|
||||||
name = "validation_tests",
|
|
||||||
srcs = [
|
|
||||||
"test/validation_test.js",
|
|
||||||
],
|
|
||||||
entry_points = [
|
|
||||||
"exoplayer.cast.test.validation",
|
|
||||||
],
|
|
||||||
suppress = [
|
|
||||||
"checkTypes",
|
|
||||||
"strictCheckTypes",
|
|
||||||
"reportUnknownTypes",
|
|
||||||
"accessControls",
|
|
||||||
"analyzerChecks",
|
|
||||||
"missingSourcesWarnings",
|
|
||||||
],
|
|
||||||
deps = [
|
|
||||||
":app_lib",
|
|
||||||
":player_lib",
|
|
||||||
":test_util_lib",
|
|
||||||
"@io_bazel_rules_closure//closure/library/testing:asserts",
|
|
||||||
"@io_bazel_rules_closure//closure/library/testing:jsunit",
|
|
||||||
"@io_bazel_rules_closure//closure/library/testing:testsuite",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
# The receiver app as a compiled binary.
|
|
||||||
closure_js_binary(
|
|
||||||
name = "app",
|
|
||||||
entry_points = [
|
|
||||||
"exoplayer.cast.app",
|
|
||||||
"shaka.dash.DashParser",
|
|
||||||
"shaka.hls.HlsParser",
|
|
||||||
"shaka.abr.SimpleAbrManager",
|
|
||||||
"shaka.net.HttpFetchPlugin",
|
|
||||||
"shaka.net.HttpXHRPlugin",
|
|
||||||
"shaka.media.AdaptationSetCriteria",
|
|
||||||
],
|
|
||||||
deps = [":app_lib"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# The debug app for the player as a compiled binary.
|
|
||||||
closure_js_binary(
|
|
||||||
name = "app_desktop",
|
|
||||||
entry_points = [
|
|
||||||
"exoplayer.cast.debug",
|
|
||||||
"exoplayer.cast.samples",
|
|
||||||
"shaka.dash.DashParser",
|
|
||||||
"shaka.hls.HlsParser",
|
|
||||||
"shaka.abr.SimpleAbrManager",
|
|
||||||
"shaka.net.HttpFetchPlugin",
|
|
||||||
"shaka.net.HttpXHRPlugin",
|
|
||||||
"shaka.media.AdaptationSetCriteria",
|
|
||||||
],
|
|
||||||
deps = [":app_desktop_lib"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Defines the css style of the receiver app.
|
|
||||||
closure_css_library(
|
|
||||||
name = "app_styles_lib",
|
|
||||||
srcs = [
|
|
||||||
"app/html/index.css",
|
|
||||||
"app/html/playback_info_view.css",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Defines the css styles of the debug app.
|
|
||||||
closure_css_library(
|
|
||||||
name = "app_desktop_styles_lib",
|
|
||||||
srcs = [
|
|
||||||
"app-desktop/html/index.css",
|
|
||||||
"app/html/playback_info_view.css",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Compiles the css styles of the receiver app.
|
|
||||||
closure_css_binary(
|
|
||||||
name = "app_styles",
|
|
||||||
renaming = False,
|
|
||||||
deps = ["app_styles_lib"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Compiles the css styles of the debug app.
|
|
||||||
closure_css_binary(
|
|
||||||
name = "app_desktop_styles",
|
|
||||||
renaming = False,
|
|
||||||
deps = ["app_desktop_styles_lib"],
|
|
||||||
)
|
|
@ -1,72 +0,0 @@
|
|||||||
# ExoPlayer cast receiver #
|
|
||||||
|
|
||||||
An HTML/JavaScript app which runs within a Google cast device and can be loaded
|
|
||||||
and controller by an Android app which uses the ExoPlayer cast extension
|
|
||||||
(https://github.com/google/ExoPlayer/tree/release-v2/extensions/cast).
|
|
||||||
|
|
||||||
# Build the app #
|
|
||||||
|
|
||||||
You can build and deploy the app to your web server and register the url as your
|
|
||||||
cast receiver app (see: https://developers.google.com/cast/docs/registration).
|
|
||||||
|
|
||||||
Building the app compiles JavaScript and CSS files. Dead JavaScript code of the
|
|
||||||
app itself and their dependencies (like ShakaPlayer) is removed and the
|
|
||||||
remaining code is minimized.
|
|
||||||
|
|
||||||
## Prerequisites ##
|
|
||||||
|
|
||||||
1. Install the most recent bazel release (https://bazel.build/) which is at
|
|
||||||
least 0.22.0.
|
|
||||||
|
|
||||||
From within the root of the exo_receiver_app project do the following steps:
|
|
||||||
|
|
||||||
2. Clone shaka from GitHub into the directory external-js/shaka-player:
|
|
||||||
```
|
|
||||||
# git clone https://github.com/google/shaka-player.git \
|
|
||||||
external-js/shaka-player
|
|
||||||
```
|
|
||||||
|
|
||||||
## 1. Customize html page and css (optional) ##
|
|
||||||
|
|
||||||
(Optional) Edit index.html. **Make sure you do not change the id of the video
|
|
||||||
element**.
|
|
||||||
(Optional) Customize main.css.
|
|
||||||
|
|
||||||
## 2. Build javascript and css files ##
|
|
||||||
```
|
|
||||||
# bazel build ...
|
|
||||||
```
|
|
||||||
## 3. Assemble the receiver app ##
|
|
||||||
```
|
|
||||||
# WEB_DEPLOY_DIR=www
|
|
||||||
# mkdir ${WEB_DEPLOY_DIR}
|
|
||||||
# cp bazel-bin/exo_receiver_app.js ${WEB_DEPLOY_DIR}
|
|
||||||
# cp bazel-bin/exo_receiver_styles_bin.css ${WEB_DEPLOY_DIR}
|
|
||||||
# cp html/index.html ${WEB_DEPLOY_DIR}
|
|
||||||
```
|
|
||||||
|
|
||||||
Deploy the content of ${WEB_DEPLOY_DIR} to your web server.
|
|
||||||
|
|
||||||
## 4. Assemble the debug app (optional) ##
|
|
||||||
|
|
||||||
Debugging the player in a cast device is a little bit cumbersome compared to
|
|
||||||
debugging in a desktop browser. For this reason there is a debug app which
|
|
||||||
contains the player parts which are not depending on the cast library in a
|
|
||||||
traditional HTML app which can be run in a desktop browser.
|
|
||||||
|
|
||||||
```
|
|
||||||
# WEB_DEPLOY_DIR=www
|
|
||||||
# mkdir ${WEB_DEPLOY_DIR}
|
|
||||||
# cp bazel-bin/debug_app.js ${WEB_DEPLOY_DIR}
|
|
||||||
# cp bazel-bin/debug_styles_bin.css ${WEB_DEPLOY_DIR}
|
|
||||||
# cp html/player.html ${WEB_DEPLOY_DIR}
|
|
||||||
```
|
|
||||||
|
|
||||||
Deploy the content of ${WEB_DEPLOY_DIR} to your web server.
|
|
||||||
|
|
||||||
# Unit test
|
|
||||||
|
|
||||||
Unit tests can be run by the command
|
|
||||||
```
|
|
||||||
# bazel test ...
|
|
||||||
```
|
|
@ -1,38 +0,0 @@
|
|||||||
# Copyright (C) 2019 The Android Open Source Project
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
|
|
||||||
|
|
||||||
http_archive(
|
|
||||||
name = "com_google_protobuf",
|
|
||||||
sha256 = "73fdad358857e120fd0fa19e071a96e15c0f23bb25f85d3f7009abfd4f264a2a",
|
|
||||||
strip_prefix = "protobuf-3.6.1.3",
|
|
||||||
urls = ["https://github.com/google/protobuf/archive/v3.6.1.3.tar.gz"],
|
|
||||||
)
|
|
||||||
|
|
||||||
http_archive(
|
|
||||||
name = "io_bazel_rules_closure",
|
|
||||||
sha256 = "b29a8bc2cb10513c864cb1084d6f38613ef14a143797cea0af0f91cd385f5e8c",
|
|
||||||
strip_prefix = "rules_closure-0.8.0",
|
|
||||||
urls = [
|
|
||||||
"https://mirror.bazel.build/github.com/bazelbuild/rules_closure/archive/0.8.0.tar.gz",
|
|
||||||
"https://github.com/bazelbuild/rules_closure/archive/0.8.0.tar.gz",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
load("@io_bazel_rules_closure//closure:defs.bzl", "closure_repositories")
|
|
||||||
|
|
||||||
closure_repositories(
|
|
||||||
omit_com_google_protobuf = True,
|
|
||||||
)
|
|
||||||
|
|
@ -1,156 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
html, body, section, video, div, span, ul, li {
|
|
||||||
border: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
body, html {
|
|
||||||
height: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
background-color: #333;
|
|
||||||
color: #eeeeee;
|
|
||||||
font-family: Roboto, Arial, sans-serif;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
padding-top: 24px;
|
|
||||||
}
|
|
||||||
.exo_controls {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
white-space: nowrap;
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
.exo_controls > li {
|
|
||||||
display: inline-block;
|
|
||||||
width: 72px;
|
|
||||||
}
|
|
||||||
.exo_controls > .large {
|
|
||||||
width: 140px;
|
|
||||||
}
|
|
||||||
/* an action element to add or remove a media item */
|
|
||||||
.action {
|
|
||||||
margin: 4px auto;
|
|
||||||
max-width: 640px;
|
|
||||||
}
|
|
||||||
.action.prepared {
|
|
||||||
background-color: #AA0000;
|
|
||||||
}
|
|
||||||
/** marks whether a given media item is in the queue */
|
|
||||||
.queue-marker {
|
|
||||||
background-color: #AA0000;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 1px solid #ffc0c0;
|
|
||||||
display: none;
|
|
||||||
float: right;
|
|
||||||
height: 1em;
|
|
||||||
margin-top: 1px;
|
|
||||||
width: 1em;
|
|
||||||
}
|
|
||||||
.action[data-uuid] .queue-marker {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
.action.prepared .queue-marker {
|
|
||||||
background-color: #fff900;
|
|
||||||
}
|
|
||||||
.playing .action.prepared .queue-marker {
|
|
||||||
animation-name: spin;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
animation-duration: 1.6s;
|
|
||||||
}
|
|
||||||
/* A simple button. */
|
|
||||||
.button {
|
|
||||||
background-color: #45484d;
|
|
||||||
border: 1px solid #495267;
|
|
||||||
border-radius: 3px;
|
|
||||||
color: #FFFFFF;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
padding: 10px 10px 10px 10px;
|
|
||||||
text-decoration: none;
|
|
||||||
text-shadow: -1px -1px 0 rgba(0,0,0,0.3);
|
|
||||||
-webkit-user-select: none;
|
|
||||||
}
|
|
||||||
.button:hover {
|
|
||||||
border: 1px solid #363d4c;
|
|
||||||
background-color: #2d2f32;
|
|
||||||
background-image: linear-gradient(to bottom, #2d2f32, #1a1a1a);
|
|
||||||
}
|
|
||||||
.ribbon {
|
|
||||||
background-color: #003a5dc2;
|
|
||||||
box-shadow: 2px 2px 4px #000;
|
|
||||||
left: -60px;
|
|
||||||
height: 3.3em;
|
|
||||||
padding-top: 7px;
|
|
||||||
position: absolute;
|
|
||||||
text-align: center;
|
|
||||||
top: 27px;
|
|
||||||
transform: rotateZ(-45deg);
|
|
||||||
width: 220px;
|
|
||||||
border: 1px dashed #cacaca;
|
|
||||||
outline-color: #003a5dc2;
|
|
||||||
outline-width: 2px;
|
|
||||||
outline-style: solid;
|
|
||||||
}
|
|
||||||
.ribbon a {
|
|
||||||
color: white;
|
|
||||||
text-decoration: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
}
|
|
||||||
#button_prepare {
|
|
||||||
left: 0;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
#button_stop {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
#exo_demo_view {
|
|
||||||
height: 360px;
|
|
||||||
margin: auto;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
width: 640px;
|
|
||||||
}
|
|
||||||
#video {
|
|
||||||
background-color: #000;
|
|
||||||
border-radius: 8px;
|
|
||||||
height: 100%;
|
|
||||||
margin-bottom: auto;
|
|
||||||
margin-top: auto;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
#exo_controls {
|
|
||||||
display: none;
|
|
||||||
margin: auto;
|
|
||||||
position: relative;
|
|
||||||
text-align: center;
|
|
||||||
width: 640px;
|
|
||||||
}
|
|
||||||
#media-actions {
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
from {
|
|
||||||
transform: rotateX(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotateX(180deg);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,55 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<!--
|
|
||||||
* Copyright (C) 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
-->
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<link rel="stylesheet" href="app_desktop_styles.css"/>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<ul id="log"></ul>
|
|
||||||
<section id="exo_demo_view">
|
|
||||||
<video id="video"></video>
|
|
||||||
<div id="exo_playback_info">
|
|
||||||
<div id="exo_time_bar">
|
|
||||||
<div id="exo_duration">
|
|
||||||
<div id="exo_elapsed_time"></div>
|
|
||||||
</div>
|
|
||||||
<span id="exo_elapsed_time_label" class="exo_text_label"></span>
|
|
||||||
<span id="exo_duration_label" class="exo_text_label"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="ribbon">
|
|
||||||
for debugging<br/>purpose only
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section id="exo_controls">
|
|
||||||
<ul class="exo_controls">
|
|
||||||
<li id="button_prepare" data-method="prepare" class="button">prepare</li>
|
|
||||||
<li id="button_previous" class="button" data-method="previous">prev</li>
|
|
||||||
<li data-method="rewind" class="button">rewind</li>
|
|
||||||
<li id="button_play" class="large button"
|
|
||||||
data-method="pwr_1">play</li>
|
|
||||||
<li id="button_pause" class="large button" data-method="pwr_0">pause</li>
|
|
||||||
<li data-method="fastforward" class="button">ffwd</li>
|
|
||||||
<li id="button_next" data-method="next" class="button">next</li>
|
|
||||||
<li id="button_stop" data-method="stop" class="button">stop</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
<section id="media-actions">
|
|
||||||
</section>
|
|
||||||
<script src="app_desktop.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,170 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
goog.module('exoplayer.cast.debug');
|
|
||||||
|
|
||||||
const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory');
|
|
||||||
const PlaybackInfoView = goog.require('exoplayer.cast.PlaybackInfoView');
|
|
||||||
const Player = goog.require('exoplayer.cast.Player');
|
|
||||||
const PlayerControls = goog.require('exoplayer.cast.PlayerControls');
|
|
||||||
const ShakaPlayer = goog.require('shaka.Player');
|
|
||||||
const SimpleTextDisplayer = goog.require('shaka.text.SimpleTextDisplayer');
|
|
||||||
const installAll = goog.require('shaka.polyfill.installAll');
|
|
||||||
const util = goog.require('exoplayer.cast.util');
|
|
||||||
|
|
||||||
/** @type {!Array<!MediaItem>} */
|
|
||||||
let queue = [];
|
|
||||||
/** @type {number} */
|
|
||||||
let uuidCounter = 1;
|
|
||||||
|
|
||||||
// install all polyfills for the Shaka player
|
|
||||||
installAll();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listens for player state changes and logs the state to the console.
|
|
||||||
*
|
|
||||||
* @param {!PlayerState} playerState The player state.
|
|
||||||
*/
|
|
||||||
const playerListener = function(playerState) {
|
|
||||||
util.log(['playerState: ', playerState.playbackPosition, playerState]);
|
|
||||||
queue = playerState.mediaQueue;
|
|
||||||
highlightCurrentItem(
|
|
||||||
playerState.playbackPosition && playerState.playbackPosition.uuid ?
|
|
||||||
playerState.playbackPosition.uuid :
|
|
||||||
'');
|
|
||||||
if (playerState.playWhenReady && playerState.playbackState === 'READY') {
|
|
||||||
document.body.classList.add('playing');
|
|
||||||
} else {
|
|
||||||
document.body.classList.remove('playing');
|
|
||||||
}
|
|
||||||
if (playerState.playbackState === 'IDLE' && queue.length === 0) {
|
|
||||||
// Stop has been called or player not yet prepared.
|
|
||||||
resetSampleList();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Highlights the currently playing item in the samples list.
|
|
||||||
*
|
|
||||||
* @param {string} uuid
|
|
||||||
*/
|
|
||||||
const highlightCurrentItem = function(uuid) {
|
|
||||||
const actions = /** @type {!NodeList<!HTMLElement>} */ (
|
|
||||||
document.querySelectorAll('#media-actions .action'));
|
|
||||||
for (let action of actions) {
|
|
||||||
if (action.dataset['uuid'] === uuid) {
|
|
||||||
action.classList.add('prepared');
|
|
||||||
} else {
|
|
||||||
action.classList.remove('prepared');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Makes sure all items reflect being removed from the timeline.
|
|
||||||
*/
|
|
||||||
const resetSampleList = function() {
|
|
||||||
const actions = /** @type {!NodeList<!HTMLElement>} */ (
|
|
||||||
document.querySelectorAll('#media-actions .action'));
|
|
||||||
for (let action of actions) {
|
|
||||||
action.classList.remove('prepared');
|
|
||||||
delete action.dataset['uuid'];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If the arguments provide a valid media item it is added to the player.
|
|
||||||
*
|
|
||||||
* @param {!MediaItem} item The media item.
|
|
||||||
* @return {string} The uuid which has been created for the item before adding.
|
|
||||||
*/
|
|
||||||
const addQueueItem = function(item) {
|
|
||||||
if (!(item.media && item.media.uri && item.mimeType)) {
|
|
||||||
throw Error('insufficient arguments to add a queue item');
|
|
||||||
}
|
|
||||||
item.uuid = 'uuid-' + uuidCounter++;
|
|
||||||
player.addQueueItems(queue.length, [item], /* playbackOrder= */ undefined);
|
|
||||||
return item.uuid;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An event listener which listens for actions.
|
|
||||||
*
|
|
||||||
* @param {!Event} ev The DOM event.
|
|
||||||
*/
|
|
||||||
const handleAction = (ev) => {
|
|
||||||
let target = ev.target;
|
|
||||||
while (target !== document.body && !target.dataset['action']) {
|
|
||||||
target = target.parentNode;
|
|
||||||
}
|
|
||||||
if (!target || !target.dataset['action']) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
switch (target.dataset['action']) {
|
|
||||||
case 'player.addItems':
|
|
||||||
if (target.dataset['uuid']) {
|
|
||||||
player.removeQueueItems([target.dataset['uuid']]);
|
|
||||||
delete target.dataset['uuid'];
|
|
||||||
} else {
|
|
||||||
const uuid = addQueueItem(/** @type {!MediaItem} */
|
|
||||||
(JSON.parse(target.dataset['item'])));
|
|
||||||
target.dataset['uuid'] = uuid;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Appends samples to the list of media item actions.
|
|
||||||
*
|
|
||||||
* @param {!Array<!MediaItem>} mediaItems The samples to add.
|
|
||||||
*/
|
|
||||||
const appendSamples = function(mediaItems) {
|
|
||||||
const samplesList = document.getElementById('media-actions');
|
|
||||||
mediaItems.forEach((item) => {
|
|
||||||
const div = /** @type {!HTMLElement} */ (document.createElement('div'));
|
|
||||||
div.classList.add('action', 'button');
|
|
||||||
div.dataset['action'] = 'player.addItems';
|
|
||||||
div.dataset['item'] = JSON.stringify(item);
|
|
||||||
div.appendChild(document.createTextNode(item.title));
|
|
||||||
const marker = document.createElement('span');
|
|
||||||
marker.classList.add('queue-marker');
|
|
||||||
div.appendChild(marker);
|
|
||||||
samplesList.appendChild(div);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @type {!HTMLMediaElement} */
|
|
||||||
const mediaElement =
|
|
||||||
/** @type {!HTMLMediaElement} */ (document.getElementById('video'));
|
|
||||||
// Workaround for https://github.com/google/shaka-player/issues/1819
|
|
||||||
// TODO(bachinger) Remove line when better fix available.
|
|
||||||
new SimpleTextDisplayer(mediaElement);
|
|
||||||
/** @type {!ShakaPlayer} */
|
|
||||||
const shakaPlayer = new ShakaPlayer(mediaElement);
|
|
||||||
/** @type {!Player} */
|
|
||||||
const player = new Player(shakaPlayer, new ConfigurationFactory());
|
|
||||||
new PlayerControls(player, 'exo_controls');
|
|
||||||
new PlaybackInfoView(player, 'exo_playback_info');
|
|
||||||
|
|
||||||
// register listeners
|
|
||||||
document.body.addEventListener('click', handleAction);
|
|
||||||
player.addPlayerListener(playerListener);
|
|
||||||
|
|
||||||
// expose the player for debugging purposes.
|
|
||||||
window['player'] = player;
|
|
||||||
|
|
||||||
exports.appendSamples = appendSamples;
|
|
@ -1,164 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
goog.module('exoplayer.cast.PlayerControls');
|
|
||||||
|
|
||||||
const Player = goog.require('exoplayer.cast.Player');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simple UI to control the player.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
class PlayerControls {
|
|
||||||
/**
|
|
||||||
* @param {!Player} player The player.
|
|
||||||
* @param {string} containerId The id of the container element.
|
|
||||||
*/
|
|
||||||
constructor(player, containerId) {
|
|
||||||
/** @const @private {!Player} */
|
|
||||||
this.player_ = player;
|
|
||||||
/** @const @private {?Element} */
|
|
||||||
this.root_ = document.getElementById(containerId);
|
|
||||||
/** @const @private {?Element} */
|
|
||||||
this.playButton_ = this.root_.querySelector('#button_play');
|
|
||||||
/** @const @private {?Element} */
|
|
||||||
this.pauseButton_ = this.root_.querySelector('#button_pause');
|
|
||||||
/** @const @private {?Element} */
|
|
||||||
this.previousButton_ = this.root_.querySelector('#button_previous');
|
|
||||||
/** @const @private {?Element} */
|
|
||||||
this.nextButton_ = this.root_.querySelector('#button_next');
|
|
||||||
|
|
||||||
const previous = () => {
|
|
||||||
const index = player.getPreviousWindowIndex();
|
|
||||||
if (index !== -1) {
|
|
||||||
player.seekToWindow(index, 0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const next = () => {
|
|
||||||
const index = player.getNextWindowIndex();
|
|
||||||
if (index !== -1) {
|
|
||||||
player.seekToWindow(index, 0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const rewind = () => {
|
|
||||||
player.seekToWindow(
|
|
||||||
player.getCurrentWindowIndex(),
|
|
||||||
player.getCurrentPositionMs() - 15000);
|
|
||||||
};
|
|
||||||
const fastForward = () => {
|
|
||||||
player.seekToWindow(
|
|
||||||
player.getCurrentWindowIndex(),
|
|
||||||
player.getCurrentPositionMs() + 30000);
|
|
||||||
};
|
|
||||||
const actions = {
|
|
||||||
'pwr_1': (ev) => player.setPlayWhenReady(true),
|
|
||||||
'pwr_0': (ev) => player.setPlayWhenReady(false),
|
|
||||||
'rewind': rewind,
|
|
||||||
'fastforward': fastForward,
|
|
||||||
'previous': previous,
|
|
||||||
'next': next,
|
|
||||||
'prepare': (ev) => player.prepare(),
|
|
||||||
'stop': (ev) => player.stop(true),
|
|
||||||
'remove_queue_item': (ev) => {
|
|
||||||
player.removeQueueItems([ev.target.dataset.id]);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* @param {!Event} ev The key event.
|
|
||||||
* @return {boolean} true if the key event has been handled.
|
|
||||||
*/
|
|
||||||
const keyListener = (ev) => {
|
|
||||||
const key = /** @type {!KeyboardEvent} */ (ev).key;
|
|
||||||
switch (key) {
|
|
||||||
case 'ArrowUp':
|
|
||||||
case 'k':
|
|
||||||
previous();
|
|
||||||
ev.preventDefault();
|
|
||||||
return true;
|
|
||||||
case 'ArrowDown':
|
|
||||||
case 'j':
|
|
||||||
next();
|
|
||||||
ev.preventDefault();
|
|
||||||
return true;
|
|
||||||
case 'ArrowLeft':
|
|
||||||
case 'h':
|
|
||||||
rewind();
|
|
||||||
ev.preventDefault();
|
|
||||||
return true;
|
|
||||||
case 'ArrowRight':
|
|
||||||
case 'l':
|
|
||||||
fastForward();
|
|
||||||
ev.preventDefault();
|
|
||||||
return true;
|
|
||||||
case ' ':
|
|
||||||
case 'p':
|
|
||||||
player.setPlayWhenReady(!player.getPlayWhenReady());
|
|
||||||
ev.preventDefault();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
document.addEventListener('keydown', keyListener);
|
|
||||||
this.root_.addEventListener('click', function(ev) {
|
|
||||||
const method = ev.target['dataset']['method'];
|
|
||||||
if (actions[method]) {
|
|
||||||
actions[method](ev);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
player.addPlayerListener((playerState) => this.updateUi(playerState));
|
|
||||||
player.invalidate();
|
|
||||||
this.setVisible_(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Syncs the ui with the player state.
|
|
||||||
*
|
|
||||||
* @param {!PlayerState} playerState The state of the player to be reflected
|
|
||||||
* by the UI.
|
|
||||||
*/
|
|
||||||
updateUi(playerState) {
|
|
||||||
if (playerState.playWhenReady) {
|
|
||||||
this.playButton_.style.display = 'none';
|
|
||||||
this.pauseButton_.style.display = 'inline-block';
|
|
||||||
} else {
|
|
||||||
this.playButton_.style.display = 'inline-block';
|
|
||||||
this.pauseButton_.style.display = 'none';
|
|
||||||
}
|
|
||||||
if (this.player_.getNextWindowIndex() === -1) {
|
|
||||||
this.nextButton_.style.visibility = 'hidden';
|
|
||||||
} else {
|
|
||||||
this.nextButton_.style.visibility = 'visible';
|
|
||||||
}
|
|
||||||
if (this.player_.getPreviousWindowIndex() === -1) {
|
|
||||||
this.previousButton_.style.visibility = 'hidden';
|
|
||||||
} else {
|
|
||||||
this.previousButton_.style.visibility = 'visible';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
* @param {boolean} visible If `true` thie controls are shown. If `false` the
|
|
||||||
* controls are hidden.
|
|
||||||
*/
|
|
||||||
setVisible_(visible) {
|
|
||||||
if (this.root_) {
|
|
||||||
this.root_.style.display = visible ? 'block' : 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports = PlayerControls;
|
|
@ -1,70 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2019 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
goog.module('exoplayer.cast.samples');
|
|
||||||
|
|
||||||
const {appendSamples} = goog.require('exoplayer.cast.debug');
|
|
||||||
|
|
||||||
appendSamples([
|
|
||||||
{
|
|
||||||
title: 'DASH: multi-period',
|
|
||||||
mimeType: 'application/dash+xml',
|
|
||||||
media: {
|
|
||||||
uri: 'https://storage.googleapis.com/exoplayer-test-media-internal-6383' +
|
|
||||||
'4241aced7884c2544af1a3452e01/dash/multi-period/two-periods-minimal' +
|
|
||||||
'-duration.mpd',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'HLS: Angel one',
|
|
||||||
mimeType: 'application/vnd.apple.mpegurl',
|
|
||||||
media: {
|
|
||||||
uri: 'https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hl' +
|
|
||||||
's.m3u8',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'MP4: Elephants dream',
|
|
||||||
mimeType: 'video/*',
|
|
||||||
media: {
|
|
||||||
uri: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/' +
|
|
||||||
'ElephantsDream.mp4',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'MKV: Android screens',
|
|
||||||
mimeType: 'video/*',
|
|
||||||
media: {
|
|
||||||
uri: 'https://storage.googleapis.com/exoplayer-test-media-1/mkv/android' +
|
|
||||||
'-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'WV: HDCP not specified',
|
|
||||||
mimeType: 'application/dash+xml',
|
|
||||||
media: {
|
|
||||||
uri: 'https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd',
|
|
||||||
},
|
|
||||||
drmSchemes: [
|
|
||||||
{
|
|
||||||
licenseServer: {
|
|
||||||
uri: 'https://proxy.uat.widevine.com/proxy?video_id=d286538032258a1' +
|
|
||||||
'c&provider=widevine_test',
|
|
||||||
},
|
|
||||||
uuid: 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]);
|
|
@ -1,79 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2019 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
goog.module('exoplayer.cast.samplesinternal');
|
|
||||||
|
|
||||||
const {appendSamples} = goog.require('exoplayer.cast.debug');
|
|
||||||
|
|
||||||
appendSamples([
|
|
||||||
{
|
|
||||||
title: 'DAS: VOD',
|
|
||||||
mimeType: 'application/dash+xml',
|
|
||||||
media: {
|
|
||||||
uri: 'https://demo-dash-pvr.zahs.tv/hd/manifest.mpd',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'MP3',
|
|
||||||
mimeType: 'audio/*',
|
|
||||||
media: {
|
|
||||||
uri: 'http://www.noiseaddicts.com/samples_1w72b820/4190.mp3',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'DASH: live',
|
|
||||||
mimeType: 'application/dash+xml',
|
|
||||||
media: {
|
|
||||||
uri: 'https://demo-dash-live.zahs.tv/sd/manifest.mpd',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'HLS: live',
|
|
||||||
mimeType: 'application/vnd.apple.mpegurl',
|
|
||||||
media: {
|
|
||||||
uri: 'https://demo-hls5-live.zahs.tv/sd/master.m3u8',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Live DASH (HD/Widevine)',
|
|
||||||
mimeType: 'application/dash+xml',
|
|
||||||
media: {
|
|
||||||
uri: 'https://demo-dashenc-live.zahs.tv/hd/widevine.mpd',
|
|
||||||
},
|
|
||||||
drmSchemes: [
|
|
||||||
{
|
|
||||||
licenseServer: {
|
|
||||||
uri: 'https://demo-dashenc-live.zahs.tv/hd/widevine-license',
|
|
||||||
},
|
|
||||||
uuid: 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'VOD DASH (HD/Widevine)',
|
|
||||||
mimeType: 'application/dash+xml',
|
|
||||||
media: {
|
|
||||||
uri: 'https://demo-dashenc-pvr.zahs.tv/hd/widevine.mpd',
|
|
||||||
},
|
|
||||||
drmSchemes: [
|
|
||||||
{
|
|
||||||
licenseServer: {
|
|
||||||
uri: 'https://demo-dashenc-live.zahs.tv/hd/widevine-license',
|
|
||||||
},
|
|
||||||
uuid: 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]);
|
|
@ -1,39 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
section, video, div, span, body, html {
|
|
||||||
border: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
html, body {
|
|
||||||
background-color: #000;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#exo_player_view {
|
|
||||||
background-color: #000;
|
|
||||||
height: 100%;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
#exo_video {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<!--
|
|
||||||
* Copyright (C) 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
-->
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<link rel="stylesheet" href="app_styles.css"/>
|
|
||||||
<script type="text/javascript"
|
|
||||||
src="//www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js">
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<section id="exo_player_view">
|
|
||||||
<video id="exo_video"></video>
|
|
||||||
<div id="exo_playback_info">
|
|
||||||
<div id="exo_time_bar">
|
|
||||||
<div id="exo_duration">
|
|
||||||
<div id="exo_elapsed_time"></div>
|
|
||||||
</div>
|
|
||||||
<span id="exo_elapsed_time_label" class="exo_text_label"></span>
|
|
||||||
<span id="exo_duration_label" class="exo_text_label"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<script src="app.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,59 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2019 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.exo_text_label {
|
|
||||||
color: #fff;
|
|
||||||
font-family: Roboto, Arial, sans-serif;
|
|
||||||
font-size: 1em;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#exo_playback_info {
|
|
||||||
bottom: 5%;
|
|
||||||
display: none;
|
|
||||||
left: 4%;
|
|
||||||
position: absolute;
|
|
||||||
right: 4%;
|
|
||||||
width: 92%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#exo_time_bar {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#exo_duration {
|
|
||||||
background-color: rgba(255, 255, 255, 0.4);
|
|
||||||
height: 0.5em;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#exo_elapsed_time {
|
|
||||||
background-color: rgb(73, 128, 218);
|
|
||||||
height: 100%;
|
|
||||||
opacity: 1;
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#exo_duration_label {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
#exo_elapsed_time_label {
|
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
goog.module('exoplayer.cast.app');
|
|
||||||
|
|
||||||
const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory');
|
|
||||||
const MessageDispatcher = goog.require('exoplayer.cast.MessageDispatcher');
|
|
||||||
const PlaybackInfoView = goog.require('exoplayer.cast.PlaybackInfoView');
|
|
||||||
const Player = goog.require('exoplayer.cast.Player');
|
|
||||||
const Receiver = goog.require('exoplayer.cast.Receiver');
|
|
||||||
const ShakaPlayer = goog.require('shaka.Player');
|
|
||||||
const SimpleTextDisplayer = goog.require('shaka.text.SimpleTextDisplayer');
|
|
||||||
const installAll = goog.require('shaka.polyfill.installAll');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The ExoPlayer namespace for messages sent and received via cast message bus.
|
|
||||||
*/
|
|
||||||
const MESSAGE_NAMESPACE_EXOPLAYER = 'urn:x-cast:com.google.exoplayer.cast';
|
|
||||||
|
|
||||||
// installs all polyfills for the Shaka player
|
|
||||||
installAll();
|
|
||||||
/** @type {?HTMLMediaElement} */
|
|
||||||
const videoElement =
|
|
||||||
/** @type {?HTMLMediaElement} */ (document.getElementById('exo_video'));
|
|
||||||
if (videoElement !== null) {
|
|
||||||
// Workaround for https://github.com/google/shaka-player/issues/1819
|
|
||||||
// TODO(bachinger) Remove line when better fix available.
|
|
||||||
new SimpleTextDisplayer(videoElement);
|
|
||||||
/** @type {!cast.framework.CastReceiverContext} */
|
|
||||||
const castReceiverContext = cast.framework.CastReceiverContext.getInstance();
|
|
||||||
const shakaPlayer = new ShakaPlayer(/** @type {!HTMLMediaElement} */
|
|
||||||
(videoElement));
|
|
||||||
const player = new Player(shakaPlayer, new ConfigurationFactory());
|
|
||||||
new PlaybackInfoView(player, 'exo_playback_info');
|
|
||||||
if (castReceiverContext !== null) {
|
|
||||||
const messageDispatcher =
|
|
||||||
new MessageDispatcher(MESSAGE_NAMESPACE_EXOPLAYER, castReceiverContext);
|
|
||||||
new Receiver(player, castReceiverContext, messageDispatcher);
|
|
||||||
}
|
|
||||||
// expose player for debugging purposes.
|
|
||||||
window['player'] = player;
|
|
||||||
}
|
|
@ -1,234 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
goog.module('exoplayer.cast.MessageDispatcher');
|
|
||||||
|
|
||||||
const validation = goog.require('exoplayer.cast.validation');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A callback function which is called by an action handler to indicate when
|
|
||||||
* processing has completed.
|
|
||||||
*
|
|
||||||
* @typedef {function(?PlayerState): undefined}
|
|
||||||
*/
|
|
||||||
const Callback = undefined;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles an action sent by a sender app.
|
|
||||||
*
|
|
||||||
* @typedef {function(!Object<string,?>, number, string, !Callback): undefined}
|
|
||||||
*/
|
|
||||||
const ActionHandler = undefined;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispatches messages of a cast message bus to registered action handlers.
|
|
||||||
*
|
|
||||||
* <p>The dispatcher listens to events of a CastMessageBus for the namespace
|
|
||||||
* passed to the constructor. The <code>data</code> property of the event is
|
|
||||||
* parsed as a json document and delegated to a handler registered for the given
|
|
||||||
* method.
|
|
||||||
*/
|
|
||||||
class MessageDispatcher {
|
|
||||||
/**
|
|
||||||
* @param {string} namespace The message namespace.
|
|
||||||
* @param {!cast.framework.CastReceiverContext} castReceiverContext The cast
|
|
||||||
* receiver manager.
|
|
||||||
*/
|
|
||||||
constructor(namespace, castReceiverContext) {
|
|
||||||
/** @private @const {string} */
|
|
||||||
this.namespace_ = namespace;
|
|
||||||
/** @private @const {!cast.framework.CastReceiverContext} */
|
|
||||||
this.castReceiverContext_ = castReceiverContext;
|
|
||||||
/** @private @const {!Array<!Message>} */
|
|
||||||
this.messageQueue_ = [];
|
|
||||||
/** @private @const {!Object} */
|
|
||||||
this.actions_ = {};
|
|
||||||
/** @private @const {!Object<string, number>} */
|
|
||||||
this.senderSequences_ = {};
|
|
||||||
/** @private @const {function(string, *)} */
|
|
||||||
this.jsonStringifyReplacer_ = (key, value) => {
|
|
||||||
if (value === Infinity || value === null) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
this.castReceiverContext_.addCustomMessageListener(
|
|
||||||
this.namespace_, this.onMessage.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers a handler of a given action.
|
|
||||||
*
|
|
||||||
* @param {string} method The method name for which to register the handler.
|
|
||||||
* @param {!Array<!Array<string>>} argDefs The name and type of each argument
|
|
||||||
* or an empty array if the method has no arguments.
|
|
||||||
* @param {!ActionHandler} handler A function to process the action.
|
|
||||||
*/
|
|
||||||
registerActionHandler(method, argDefs, handler) {
|
|
||||||
this.actions_[method] = {
|
|
||||||
method,
|
|
||||||
argDefs,
|
|
||||||
handler,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unregisters the handler of the given action.
|
|
||||||
*
|
|
||||||
* @param {string} action The action to unregister.
|
|
||||||
*/
|
|
||||||
unregisterActionHandler(action) {
|
|
||||||
delete this.actions_[action];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback to receive messages sent by sender apps.
|
|
||||||
*
|
|
||||||
* @param {!cast.framework.system.Event} event The event received from the
|
|
||||||
* sender app.
|
|
||||||
*/
|
|
||||||
onMessage(event) {
|
|
||||||
console.log('message arrived from sender', this.namespace_, event);
|
|
||||||
const message = /** @type {!ExoCastMessage} */ (event.data);
|
|
||||||
const action = this.actions_[message.method];
|
|
||||||
if (action) {
|
|
||||||
const args = message.args;
|
|
||||||
for (let i = 0; i < action.argDefs.length; i++) {
|
|
||||||
if (!validation.validateProperty(
|
|
||||||
args, action.argDefs[i][0], action.argDefs[i][1])) {
|
|
||||||
console.warn('invalid method call', message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.messageQueue_.push({
|
|
||||||
senderId: event.senderId,
|
|
||||||
message: message,
|
|
||||||
handler: action.handler
|
|
||||||
});
|
|
||||||
if (this.messageQueue_.length === 1) {
|
|
||||||
this.executeNext();
|
|
||||||
} else {
|
|
||||||
// Do nothing. An action is executing asynchronously and will call
|
|
||||||
// executeNext when finished.
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn('handler of method not found', message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executes the next message in the queue.
|
|
||||||
*/
|
|
||||||
executeNext() {
|
|
||||||
if (this.messageQueue_.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const head = this.messageQueue_[0];
|
|
||||||
const message = head.message;
|
|
||||||
const senderSequence = message.sequenceNumber;
|
|
||||||
this.senderSequences_[head.senderId] = senderSequence;
|
|
||||||
try {
|
|
||||||
head.handler(message.args, senderSequence, head.senderId, (response) => {
|
|
||||||
if (response) {
|
|
||||||
this.send(head.senderId, response);
|
|
||||||
}
|
|
||||||
this.shiftPendingMessage_(head);
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
this.shiftPendingMessage_(head);
|
|
||||||
console.error('error while executing method : ' + message.method, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Broadcasts the sender state to all sender apps registered for the
|
|
||||||
* given message namespace.
|
|
||||||
*
|
|
||||||
* @param {!PlayerState} playerState The player state to be sent.
|
|
||||||
*/
|
|
||||||
broadcast(playerState) {
|
|
||||||
this.castReceiverContext_.getSenders().forEach((sender) => {
|
|
||||||
this.send(sender.id, playerState);
|
|
||||||
});
|
|
||||||
delete playerState.sequenceNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends the PlayerState to the given sender.
|
|
||||||
*
|
|
||||||
* @param {string} senderId The id of the sender.
|
|
||||||
* @param {!PlayerState} playerState The message to send.
|
|
||||||
*/
|
|
||||||
send(senderId, playerState) {
|
|
||||||
playerState.sequenceNumber = this.senderSequences_[senderId] || -1;
|
|
||||||
this.castReceiverContext_.sendCustomMessage(
|
|
||||||
this.namespace_, senderId,
|
|
||||||
// TODO(bachinger) Find a better solution.
|
|
||||||
JSON.parse(JSON.stringify(playerState, this.jsonStringifyReplacer_)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notifies the message dispatcher that a given sender has disconnected from
|
|
||||||
* the receiver.
|
|
||||||
*
|
|
||||||
* @param {string} senderId The id of the sender.
|
|
||||||
*/
|
|
||||||
notifySenderDisconnected(senderId) {
|
|
||||||
delete this.senderSequences_[senderId];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shifts the pending message and executes the next if any.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @param {!Message} pendingMessage The pending message.
|
|
||||||
*/
|
|
||||||
shiftPendingMessage_(pendingMessage) {
|
|
||||||
if (pendingMessage === this.messageQueue_[0]) {
|
|
||||||
this.messageQueue_.shift();
|
|
||||||
this.executeNext();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An item in the message queue.
|
|
||||||
*
|
|
||||||
* @record
|
|
||||||
*/
|
|
||||||
function Message() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The sender id.
|
|
||||||
*
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
Message.prototype.senderId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The ExoCastMessage sent by the sender app.
|
|
||||||
*
|
|
||||||
* @type {!ExoCastMessage}
|
|
||||||
*/
|
|
||||||
Message.prototype.message;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The handler function handling the message.
|
|
||||||
*
|
|
||||||
* @type {!ActionHandler}
|
|
||||||
*/
|
|
||||||
Message.prototype.handler;
|
|
||||||
|
|
||||||
exports = MessageDispatcher;
|
|
@ -1,191 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (C) 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
goog.module('exoplayer.cast.Receiver');
|
|
||||||
|
|
||||||
const MessageDispatcher = goog.require('exoplayer.cast.MessageDispatcher');
|
|
||||||
const Player = goog.require('exoplayer.cast.Player');
|
|
||||||
const validation = goog.require('exoplayer.cast.validation');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Receiver receives messages from a message bus and delegates to
|
|
||||||
* the player.
|
|
||||||
*
|
|
||||||
* @constructor
|
|
||||||
* @param {!Player} player The player.
|
|
||||||
* @param {!cast.framework.CastReceiverContext} context The cast receiver
|
|
||||||
* context.
|
|
||||||
* @param {!MessageDispatcher} messageDispatcher The message dispatcher to use.
|
|
||||||
*/
|
|
||||||
const Receiver = function(player, context, messageDispatcher) {
|
|
||||||
addPlayerActions(messageDispatcher, player);
|
|
||||||
addQueueActions(messageDispatcher, player);
|
|
||||||
player.addPlayerListener((playerState) => {
|
|
||||||
messageDispatcher.broadcast(playerState);
|
|
||||||
});
|
|
||||||
|
|
||||||
context.addEventListener(
|
|
||||||
cast.framework.system.EventType.SENDER_CONNECTED, (event) => {
|
|
||||||
messageDispatcher.send(event.senderId, player.getPlayerState());
|
|
||||||
});
|
|
||||||
|
|
||||||
context.addEventListener(
|
|
||||||
cast.framework.system.EventType.SENDER_DISCONNECTED, (event) => {
|
|
||||||
messageDispatcher.notifySenderDisconnected(event.senderId);
|
|
||||||
if (event.reason ===
|
|
||||||
cast.framework.system.DisconnectReason.REQUESTED_BY_SENDER &&
|
|
||||||
context.getSenders().length === 0) {
|
|
||||||
window.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start the cast receiver context.
|
|
||||||
context.start();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers action handlers for playback messages sent by the sender app.
|
|
||||||
*
|
|
||||||
* @param {!MessageDispatcher} messageDispatcher The dispatcher.
|
|
||||||
* @param {!Player} player The player.
|
|
||||||
*/
|
|
||||||
const addPlayerActions = function(messageDispatcher, player) {
|
|
||||||
messageDispatcher.registerActionHandler(
|
|
||||||
'player.setPlayWhenReady', [['playWhenReady', 'boolean']],
|
|
||||||
(args, senderSequence, senderId, callback) => {
|
|
||||||
const playWhenReady = args['playWhenReady'];
|
|
||||||
callback(
|
|
||||||
!player.setPlayWhenReady(playWhenReady) ?
|
|
||||||
player.getPlayerState() :
|
|
||||||
null);
|
|
||||||
});
|
|
||||||
messageDispatcher.registerActionHandler(
|
|
||||||
'player.seekTo',
|
|
||||||
[
|
|
||||||
['uuid', 'string'],
|
|
||||||
['positionMs', '?number'],
|
|
||||||
],
|
|
||||||
(args, senderSequence, senderId, callback) => {
|
|
||||||
callback(
|
|
||||||
!player.seekToUuid(args['uuid'], args['positionMs']) ?
|
|
||||||
player.getPlayerState() :
|
|
||||||
null);
|
|
||||||
});
|
|
||||||
messageDispatcher.registerActionHandler(
|
|
||||||
'player.setRepeatMode', [['repeatMode', 'RepeatMode']],
|
|
||||||
(args, senderSequence, senderId, callback) => {
|
|
||||||
callback(
|
|
||||||
!player.setRepeatMode(args['repeatMode']) ?
|
|
||||||
player.getPlayerState() :
|
|
||||||
null);
|
|
||||||
});
|
|
||||||
messageDispatcher.registerActionHandler(
|
|
||||||
'player.setShuffleModeEnabled', [['shuffleModeEnabled', 'boolean']],
|
|
||||||
(args, senderSequence, senderId, callback) => {
|
|
||||||
callback(
|
|
||||||
!player.setShuffleModeEnabled(args['shuffleModeEnabled']) ?
|
|
||||||
player.getPlayerState() :
|
|
||||||
null);
|
|
||||||
});
|
|
||||||
messageDispatcher.registerActionHandler(
|
|
||||||
'player.onClientConnected', [],
|
|
||||||
(args, senderSequence, senderId, callback) => {
|
|
||||||
callback(player.getPlayerState());
|
|
||||||
});
|
|
||||||
messageDispatcher.registerActionHandler(
|
|
||||||
'player.stop', [['reset', 'boolean']],
|
|
||||||
(args, senderSequence, senderId, callback) => {
|
|
||||||
player.stop(args['reset']).then(() => {
|
|
||||||
callback(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
messageDispatcher.registerActionHandler(
|
|
||||||
'player.prepare', [], (args, senderSequence, senderId, callback) => {
|
|
||||||
player.prepare();
|
|
||||||
callback(null);
|
|
||||||
});
|
|
||||||
messageDispatcher.registerActionHandler(
|
|
||||||
'player.setTrackSelectionParameters',
|
|
||||||
[
|
|
||||||
['preferredAudioLanguage', 'string'],
|
|
||||||
['preferredTextLanguage', 'string'],
|
|
||||||
['disabledTextTrackSelectionFlags', 'Array'],
|
|
||||||
['selectUndeterminedTextLanguage', 'boolean'],
|
|
||||||
],
|
|
||||||
(args, senderSequence, senderId, callback) => {
|
|
||||||
const trackSelectionParameters =
|
|
||||||
/** @type {!TrackSelectionParameters} */ ({
|
|
||||||
preferredAudioLanguage: args['preferredAudioLanguage'],
|
|
||||||
preferredTextLanguage: args['preferredTextLanguage'],
|
|
||||||
disabledTextTrackSelectionFlags:
|
|
||||||
args['disabledTextTrackSelectionFlags'],
|
|
||||||
selectUndeterminedTextLanguage:
|
|
||||||
args['selectUndeterminedTextLanguage'],
|
|
||||||
});
|
|
||||||
callback(
|
|
||||||
!player.setTrackSelectionParameters(trackSelectionParameters) ?
|
|
||||||
player.getPlayerState() :
|
|
||||||
null);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers action handlers for queue management messages sent by the sender
|
|
||||||
* app.
|
|
||||||
*
|
|
||||||
* @param {!MessageDispatcher} messageDispatcher The dispatcher.
|
|
||||||
* @param {!Player} player The player.
|
|
||||||
*/
|
|
||||||
const addQueueActions =
|
|
||||||
function (messageDispatcher, player) {
|
|
||||||
messageDispatcher.registerActionHandler(
|
|
||||||
'player.addItems',
|
|
||||||
[
|
|
||||||
['index', '?number'],
|
|
||||||
['items', 'Array'],
|
|
||||||
['shuffleOrder', 'Array'],
|
|
||||||
],
|
|
||||||
(args, senderSequence, senderId, callback) => {
|
|
||||||
const mediaItems = args['items'];
|
|
||||||
const index = args['index'] || player.getQueueSize();
|
|
||||||
let addedItemCount;
|
|
||||||
if (validation.validateMediaItems(mediaItems)) {
|
|
||||||
addedItemCount =
|
|
||||||
player.addQueueItems(index, mediaItems, args['shuffleOrder']);
|
|
||||||
}
|
|
||||||
callback(addedItemCount === 0 ? player.getPlayerState() : null);
|
|
||||||
});
|
|
||||||
messageDispatcher.registerActionHandler(
|
|
||||||
'player.removeItems', [['uuids', 'Array']],
|
|
||||||
(args, senderSequence, senderId, callback) => {
|
|
||||||
const removedItemsCount = player.removeQueueItems(args['uuids']);
|
|
||||||
callback(removedItemsCount === 0 ? player.getPlayerState() : null);
|
|
||||||
});
|
|
||||||
messageDispatcher.registerActionHandler(
|
|
||||||
'player.moveItem',
|
|
||||||
[
|
|
||||||
['uuid', 'string'],
|
|
||||||
['index', 'number'],
|
|
||||||
['shuffleOrder', 'Array'],
|
|
||||||
],
|
|
||||||
(args, senderSequence, senderId, callback) => {
|
|
||||||
const hasMoved = player.moveQueueItem(
|
|
||||||
args['uuid'], args['index'], args['shuffleOrder']);
|
|
||||||
callback(!hasMoved ? player.getPlayerState() : null);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
exports = Receiver;
|
|
@ -1,163 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (C) 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*
|
|
||||||
* @fileoverview A validator for messages received from sender apps.
|
|
||||||
*/
|
|
||||||
|
|
||||||
goog.module('exoplayer.cast.validation');
|
|
||||||
|
|
||||||
const {getPlaybackType, PlaybackType, RepeatMode} = goog.require('exoplayer.cast.constants');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Media item fields.
|
|
||||||
*
|
|
||||||
* @enum {string}
|
|
||||||
*/
|
|
||||||
const MediaItemField = {
|
|
||||||
UUID: 'uuid',
|
|
||||||
MEDIA: 'media',
|
|
||||||
MIME_TYPE: 'mimeType',
|
|
||||||
DRM_SCHEMES: 'drmSchemes',
|
|
||||||
TITLE: 'title',
|
|
||||||
DESCRIPTION: 'description',
|
|
||||||
START_POSITION_US: 'startPositionUs',
|
|
||||||
END_POSITION_US: 'endPositionUs',
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DrmScheme fields.
|
|
||||||
*
|
|
||||||
* @enum {string}
|
|
||||||
*/
|
|
||||||
const DrmSchemeField = {
|
|
||||||
UUID: 'uuid',
|
|
||||||
LICENSE_SERVER_URI: 'licenseServer',
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UriBundle fields.
|
|
||||||
*
|
|
||||||
* @enum {string}
|
|
||||||
*/
|
|
||||||
const UriBundleField = {
|
|
||||||
URI: 'uri',
|
|
||||||
REQUEST_HEADERS: 'requestHeaders',
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates an array of media items.
|
|
||||||
*
|
|
||||||
* @param {!Array<!MediaItem>} mediaItems An array of media items.
|
|
||||||
* @return {boolean} true if all media items are valid, otherwise false is
|
|
||||||
* returned.
|
|
||||||
*/
|
|
||||||
const validateMediaItems = function (mediaItems) {
|
|
||||||
for (let i = 0; i < mediaItems.length; i++) {
|
|
||||||
if (!validateMediaItem(mediaItems[i])) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a queue item sent to the receiver by a sender app.
|
|
||||||
*
|
|
||||||
* @param {!MediaItem} mediaItem The media item.
|
|
||||||
* @return {boolean} true if the media item is valid, false otherwise.
|
|
||||||
*/
|
|
||||||
const validateMediaItem = function (mediaItem) {
|
|
||||||
// validate minimal properties
|
|
||||||
if (!validateProperty(mediaItem, MediaItemField.UUID, 'string')) {
|
|
||||||
console.log('missing mandatory uuid', mediaItem.uuid);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!validateProperty(mediaItem.media, UriBundleField.URI, 'string')) {
|
|
||||||
console.log('missing mandatory', mediaItem.media ? 'uri' : 'media');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const mimeType = mediaItem.mimeType;
|
|
||||||
if (!mimeType || getPlaybackType(mimeType) === PlaybackType.UNKNOWN) {
|
|
||||||
console.log('unsupported mime type:', mimeType);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// validate optional properties
|
|
||||||
if (goog.isArray(mediaItem.drmSchemes)) {
|
|
||||||
for (let i = 0; i < mediaItem.drmSchemes.length; i++) {
|
|
||||||
let drmScheme = mediaItem.drmSchemes[i];
|
|
||||||
if (!validateProperty(drmScheme, DrmSchemeField.UUID, 'string') ||
|
|
||||||
!validateProperty(
|
|
||||||
drmScheme.licenseServer, UriBundleField.URI, 'string')) {
|
|
||||||
console.log('invalid drm scheme', drmScheme);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!validateProperty(mediaItem, MediaItemField.START_POSITION_US, '?number')
|
|
||||||
|| !validateProperty(mediaItem, MediaItemField.END_POSITION_US, '?number')
|
|
||||||
|| !validateProperty(mediaItem, MediaItemField.TITLE, '?string')
|
|
||||||
|| !validateProperty(mediaItem, MediaItemField.DESCRIPTION, '?string')) {
|
|
||||||
console.log('invalid type of one of startPositionUs, endPositionUs, title'
|
|
||||||
+ ' or description', mediaItem);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates the existence and type of a property.
|
|
||||||
*
|
|
||||||
* <p>Supported types: number, string, boolean, Array.
|
|
||||||
* <p>Prefix the type with a ? to indicate that the property is optional.
|
|
||||||
*
|
|
||||||
* @param {?Object|?MediaItem|?UriBundle} obj The object to validate.
|
|
||||||
* @param {string} propertyName The name of the property.
|
|
||||||
* @param {string} type The type of the property.
|
|
||||||
* @return {boolean} True if valid, false otherwise.
|
|
||||||
*/
|
|
||||||
const validateProperty = function (obj, propertyName, type) {
|
|
||||||
if (typeof obj === 'undefined' || obj === null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const isOptional = type.startsWith('?');
|
|
||||||
const value = obj[propertyName];
|
|
||||||
if (isOptional && typeof value === 'undefined') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
type = isOptional ? type.substring(1) : type;
|
|
||||||
switch (type) {
|
|
||||||
case 'string':
|
|
||||||
return typeof value === 'string' || value instanceof String;
|
|
||||||
case 'number':
|
|
||||||
return typeof value === 'number' && isFinite(value);
|
|
||||||
case 'Array':
|
|
||||||
return typeof value !== 'undefined' && typeof value === 'object'
|
|
||||||
&& value.constructor === Array;
|
|
||||||
case 'boolean':
|
|
||||||
return typeof value === 'boolean';
|
|
||||||
case 'RepeatMode':
|
|
||||||
return value === RepeatMode.OFF || value === RepeatMode.ONE ||
|
|
||||||
value === RepeatMode.ALL;
|
|
||||||
default:
|
|
||||||
console.warn('Unsupported type when validating an object property. ' +
|
|
||||||
'Supported types are string, number, boolean and Array.', type);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.validateMediaItem = validateMediaItem;
|
|
||||||
exports.validateMediaItems = validateMediaItems;
|
|
||||||
exports.validateProperty = validateProperty;
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Copyright (C) 2019 The Android Open Source Project
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
##
|
|
||||||
# Assembles the html, css and javascript files which have been created by the
|
|
||||||
# bazel build in a destination directory.
|
|
||||||
|
|
||||||
HTML_DIR=app/html
|
|
||||||
HTML_DEBUG_DIR=app-desktop/html
|
|
||||||
BIN=bazel-bin
|
|
||||||
|
|
||||||
function usage {
|
|
||||||
echo "usage: `basename "$0"` -d=DESTINATION_DIR"
|
|
||||||
}
|
|
||||||
|
|
||||||
for i in "$@"
|
|
||||||
do
|
|
||||||
case $i in
|
|
||||||
-d=*|--destination=*)
|
|
||||||
DESTINATION="${i#*=}"
|
|
||||||
shift # past argument=value
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
# unknown option
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ ! -d "$DESTINATION" ]; then
|
|
||||||
echo "destination directory '$DESTINATION' is not declared or is not a\
|
|
||||||
directory"
|
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "$BIN/app.js" ];then
|
|
||||||
echo "file $BIN/app.js not found. Did you build already with bazel?"
|
|
||||||
echo "-> # bazel build .. --incompatible_package_name_is_a_function=false"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "$BIN/app_desktop.js" ];then
|
|
||||||
echo "file $BIN/app_desktop.js not found. Did you build already with bazel?"
|
|
||||||
echo "-> # bazel build .. --incompatible_package_name_is_a_function=false"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "assembling receiver and desktop app in $DESTINATION"
|
|
||||||
echo "-------"
|
|
||||||
|
|
||||||
# cleaning up asset files in destination directory
|
|
||||||
FILES=(
|
|
||||||
app.js
|
|
||||||
app_desktop.js
|
|
||||||
app_styles.css
|
|
||||||
app_desktop_styles.css
|
|
||||||
index.html
|
|
||||||
player.html
|
|
||||||
)
|
|
||||||
for file in ${FILES[@]}; do
|
|
||||||
if [ -f $DESTINATION/$file ]; then
|
|
||||||
echo "deleting $file"
|
|
||||||
rm -f $DESTINATION/$file
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
echo "-------"
|
|
||||||
|
|
||||||
echo "copy html files to $DESTINATION"
|
|
||||||
cp $HTML_DIR/index.html $DESTINATION
|
|
||||||
cp $HTML_DEBUG_DIR/index.html $DESTINATION/player.html
|
|
||||||
echo "copy javascript files to $DESTINATION"
|
|
||||||
cp $BIN/app.js $BIN/app_desktop.js $DESTINATION
|
|
||||||
echo "copy css style to $DESTINATION"
|
|
||||||
cp $BIN/app_styles.css $BIN/app_desktop_styles.css $DESTINATION
|
|
||||||
echo "-------"
|
|
||||||
|
|
||||||
echo "done."
|
|
@ -1,489 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @fileoverview Externs for messages sent by a sender app in JSON format.
|
|
||||||
*
|
|
||||||
* Fields defined here are prevented from being renamed by the js compiler.
|
|
||||||
*
|
|
||||||
* @externs
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An uri bundle with an uri and request parameters.
|
|
||||||
*
|
|
||||||
* @record
|
|
||||||
*/
|
|
||||||
class UriBundle {
|
|
||||||
constructor() {
|
|
||||||
/**
|
|
||||||
* The URI.
|
|
||||||
*
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
this.uri;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The request headers.
|
|
||||||
*
|
|
||||||
* @type {?Object<string,string>}
|
|
||||||
*/
|
|
||||||
this.requestHeaders;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @record
|
|
||||||
*/
|
|
||||||
class DrmScheme {
|
|
||||||
constructor() {
|
|
||||||
/**
|
|
||||||
* The DRM UUID.
|
|
||||||
*
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
this.uuid;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The license URI.
|
|
||||||
*
|
|
||||||
* @type {?UriBundle}
|
|
||||||
*/
|
|
||||||
this.licenseServer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @record
|
|
||||||
*/
|
|
||||||
class MediaItem {
|
|
||||||
constructor() {
|
|
||||||
/**
|
|
||||||
* The uuid of the item.
|
|
||||||
*
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
this.uuid;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The mime type.
|
|
||||||
*
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
this.mimeType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The media uri bundle.
|
|
||||||
*
|
|
||||||
* @type {!UriBundle}
|
|
||||||
*/
|
|
||||||
this.media;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The DRM schemes.
|
|
||||||
*
|
|
||||||
* @type {!Array<!DrmScheme>}
|
|
||||||
*/
|
|
||||||
this.drmSchemes;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The position to start playback from.
|
|
||||||
*
|
|
||||||
* @type {number}
|
|
||||||
*/
|
|
||||||
this.startPositionUs;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The position at which to end playback.
|
|
||||||
*
|
|
||||||
* @type {number}
|
|
||||||
*/
|
|
||||||
this.endPositionUs;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The title of the media item.
|
|
||||||
*
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
this.title;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The description of the media item.
|
|
||||||
*
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
this.description;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constraint parameters for track selection.
|
|
||||||
*
|
|
||||||
* @record
|
|
||||||
*/
|
|
||||||
class TrackSelectionParameters {
|
|
||||||
constructor() {
|
|
||||||
/**
|
|
||||||
* The preferred audio language.
|
|
||||||
*
|
|
||||||
* @type {string|undefined}
|
|
||||||
*/
|
|
||||||
this.preferredAudioLanguage;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The preferred text language.
|
|
||||||
*
|
|
||||||
* @type {string|undefined}
|
|
||||||
*/
|
|
||||||
this.preferredTextLanguage;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List of selection flags that are disabled for text track selections.
|
|
||||||
*
|
|
||||||
* @type {!Array<string>}
|
|
||||||
*/
|
|
||||||
this.disabledTextTrackSelectionFlags;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether a text track with undetermined language should be selected if no
|
|
||||||
* track with `preferredTextLanguage` is available, or if
|
|
||||||
* `preferredTextLanguage` is unset.
|
|
||||||
*
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
this.selectUndeterminedTextLanguage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The PlaybackPosition defined by the position, the uuid of the media item and
|
|
||||||
* the period id.
|
|
||||||
*
|
|
||||||
* @record
|
|
||||||
*/
|
|
||||||
class PlaybackPosition {
|
|
||||||
constructor() {
|
|
||||||
/**
|
|
||||||
* The current playback position in milliseconds.
|
|
||||||
*
|
|
||||||
* @type {number}
|
|
||||||
*/
|
|
||||||
this.positionMs;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The uuid of the media item.
|
|
||||||
*
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
this.uuid;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The id of the currently playing period.
|
|
||||||
*
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
this.periodId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The reason of a position discontinuity if any.
|
|
||||||
*
|
|
||||||
* @type {?string}
|
|
||||||
*/
|
|
||||||
this.discontinuityReason;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The playback parameters.
|
|
||||||
*
|
|
||||||
* @record
|
|
||||||
*/
|
|
||||||
class PlaybackParameters {
|
|
||||||
constructor() {
|
|
||||||
/**
|
|
||||||
* The playback speed.
|
|
||||||
*
|
|
||||||
* @type {number}
|
|
||||||
*/
|
|
||||||
this.speed;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The playback pitch.
|
|
||||||
*
|
|
||||||
* @type {number}
|
|
||||||
*/
|
|
||||||
this.pitch;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether silence is skipped.
|
|
||||||
*
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
this.skipSilence;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* The player state.
|
|
||||||
*
|
|
||||||
* @record
|
|
||||||
*/
|
|
||||||
class PlayerState {
|
|
||||||
constructor() {
|
|
||||||
/**
|
|
||||||
* The playback state.
|
|
||||||
*
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
this.playbackState;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The playback parameters.
|
|
||||||
*
|
|
||||||
* @type {!PlaybackParameters}
|
|
||||||
*/
|
|
||||||
this.playbackParameters;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Playback starts when ready if true.
|
|
||||||
*
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
this.playWhenReady;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The current position within the media.
|
|
||||||
*
|
|
||||||
* @type {?PlaybackPosition}
|
|
||||||
*/
|
|
||||||
this.playbackPosition;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The current window index.
|
|
||||||
*
|
|
||||||
* @type {number}
|
|
||||||
*/
|
|
||||||
this.windowIndex;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The number of windows.
|
|
||||||
*
|
|
||||||
* @type {number}
|
|
||||||
*/
|
|
||||||
this.windowCount;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The audio tracks.
|
|
||||||
*
|
|
||||||
* @type {!Array<string>}
|
|
||||||
*/
|
|
||||||
this.audioTracks;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The video tracks in case of adaptive media.
|
|
||||||
*
|
|
||||||
* @type {!Array<!Object<string,*>>}
|
|
||||||
*/
|
|
||||||
this.videoTracks;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The repeat mode.
|
|
||||||
*
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
this.repeatMode;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the shuffle mode is enabled.
|
|
||||||
*
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
this.shuffleModeEnabled;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The playback order to use when shuffle mode is enabled.
|
|
||||||
*
|
|
||||||
* @type {!Array<number>}
|
|
||||||
*/
|
|
||||||
this.shuffleOrder;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The queue of media items.
|
|
||||||
*
|
|
||||||
* @type {!Array<!MediaItem>}
|
|
||||||
*/
|
|
||||||
this.mediaQueue;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The media item info of the queue items if available.
|
|
||||||
*
|
|
||||||
* @type {!Object<string, !MediaItemInfo>}
|
|
||||||
*/
|
|
||||||
this.mediaItemsInfo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The sequence number of the sender.
|
|
||||||
*
|
|
||||||
* @type {number}
|
|
||||||
*/
|
|
||||||
this.sequenceNumber;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The player error.
|
|
||||||
*
|
|
||||||
* @type {?PlayerError}
|
|
||||||
*/
|
|
||||||
this.error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The error description.
|
|
||||||
*
|
|
||||||
* @record
|
|
||||||
*/
|
|
||||||
class PlayerError {
|
|
||||||
constructor() {
|
|
||||||
/**
|
|
||||||
* The error message.
|
|
||||||
*
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
this.message;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The error code.
|
|
||||||
*
|
|
||||||
* @type {number}
|
|
||||||
*/
|
|
||||||
this.code;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The error category.
|
|
||||||
*
|
|
||||||
* @type {number}
|
|
||||||
*/
|
|
||||||
this.category;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A period.
|
|
||||||
*
|
|
||||||
* @record
|
|
||||||
*/
|
|
||||||
class Period {
|
|
||||||
constructor() {
|
|
||||||
/**
|
|
||||||
* The id of the period. Must be unique within a media item.
|
|
||||||
*
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
this.id;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The duration of the period in microseconds.
|
|
||||||
*
|
|
||||||
* @type {number}
|
|
||||||
*/
|
|
||||||
this.durationUs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Holds dynamic information for a MediaItem.
|
|
||||||
*
|
|
||||||
* <p>Holds information related to preparation for a specific {@link MediaItem}.
|
|
||||||
* Unprepared items are associated with an {@link #EMPTY} info object until
|
|
||||||
* prepared.
|
|
||||||
*
|
|
||||||
* @record
|
|
||||||
*/
|
|
||||||
class MediaItemInfo {
|
|
||||||
constructor() {
|
|
||||||
/**
|
|
||||||
* The duration of the window in microseconds.
|
|
||||||
*
|
|
||||||
* @type {number}
|
|
||||||
*/
|
|
||||||
this.windowDurationUs;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The default start position relative to the start of the window in
|
|
||||||
* microseconds.
|
|
||||||
*
|
|
||||||
* @type {number}
|
|
||||||
*/
|
|
||||||
this.defaultStartPositionUs;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The periods conforming the media item.
|
|
||||||
*
|
|
||||||
* @type {!Array<!Period>}
|
|
||||||
*/
|
|
||||||
this.periods;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The position of the window in the first period in microseconds.
|
|
||||||
*
|
|
||||||
* @type {number}
|
|
||||||
*/
|
|
||||||
this.positionInFirstPeriodUs;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether it is possible to seek within the window.
|
|
||||||
*
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
this.isSeekable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the window may change when the timeline is updated.
|
|
||||||
*
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
this.isDynamic;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The message envelope send by a sender app.
|
|
||||||
*
|
|
||||||
* @record
|
|
||||||
*/
|
|
||||||
class ExoCastMessage {
|
|
||||||
constructor() {
|
|
||||||
/**
|
|
||||||
* The clients message sequenec number.
|
|
||||||
*
|
|
||||||
* @type {number}
|
|
||||||
*/
|
|
||||||
this.sequenceNumber;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The name of the method.
|
|
||||||
*
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
this.method;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The arguments of the method.
|
|
||||||
*
|
|
||||||
* @type {!Object<string,*>}
|
|
||||||
*/
|
|
||||||
this.args;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2019 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @fileoverview Externs of the Shaka configuration.
|
|
||||||
*
|
|
||||||
* @externs
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The drm configuration for the Shaka player.
|
|
||||||
*
|
|
||||||
* @record
|
|
||||||
*/
|
|
||||||
class DrmConfiguration {
|
|
||||||
constructor() {
|
|
||||||
/**
|
|
||||||
* A map of license servers with the UUID of the drm system as the key and the
|
|
||||||
* license uri as the value.
|
|
||||||
*
|
|
||||||
* @type {!Object<string, string>}
|
|
||||||
*/
|
|
||||||
this.servers;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The configuration of the Shaka player.
|
|
||||||
*
|
|
||||||
* @record
|
|
||||||
*/
|
|
||||||
class PlayerConfiguration {
|
|
||||||
constructor() {
|
|
||||||
/**
|
|
||||||
* The preferred audio language.
|
|
||||||
*
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
this.preferredAudioLanguage;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The preferred text language.
|
|
||||||
*
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
this.preferredTextLanguage;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The drm configuration.
|
|
||||||
*
|
|
||||||
* @type {?DrmConfiguration}
|
|
||||||
*/
|
|
||||||
this.drm;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,90 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2019 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
goog.module('exoplayer.cast.ConfigurationFactory');
|
|
||||||
|
|
||||||
const {DRM_SYSTEMS} = goog.require('exoplayer.cast.constants');
|
|
||||||
|
|
||||||
const EMPTY_DRM_CONFIGURATION =
|
|
||||||
/** @type {!DrmConfiguration} */ (Object.freeze({
|
|
||||||
servers: {},
|
|
||||||
}));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the configuration of the Shaka player.
|
|
||||||
*/
|
|
||||||
class ConfigurationFactory {
|
|
||||||
/**
|
|
||||||
* Creates the Shaka player configuration.
|
|
||||||
*
|
|
||||||
* @param {!MediaItem} mediaItem The media item for which to create the
|
|
||||||
* configuration.
|
|
||||||
* @param {!TrackSelectionParameters} trackSelectionParameters The track
|
|
||||||
* selection parameters.
|
|
||||||
* @return {!PlayerConfiguration} The shaka player configuration.
|
|
||||||
*/
|
|
||||||
createConfiguration(mediaItem, trackSelectionParameters) {
|
|
||||||
const configuration = /** @type {!PlayerConfiguration} */ ({});
|
|
||||||
this.mapLanguageConfiguration(trackSelectionParameters, configuration);
|
|
||||||
this.mapDrmConfiguration_(mediaItem, configuration);
|
|
||||||
return configuration;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps the preferred audio and text language from the track selection
|
|
||||||
* parameters to the configuration.
|
|
||||||
*
|
|
||||||
* @param {!TrackSelectionParameters} trackSelectionParameters The selection
|
|
||||||
* parameters.
|
|
||||||
* @param {!PlayerConfiguration} playerConfiguration The player configuration.
|
|
||||||
*/
|
|
||||||
mapLanguageConfiguration(trackSelectionParameters, playerConfiguration) {
|
|
||||||
playerConfiguration.preferredAudioLanguage =
|
|
||||||
trackSelectionParameters.preferredAudioLanguage || '';
|
|
||||||
playerConfiguration.preferredTextLanguage =
|
|
||||||
trackSelectionParameters.preferredTextLanguage || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps the drm configuration from the media item to the configuration. If no
|
|
||||||
* drm is specified for the given media item, null is assigned.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @param {!MediaItem} mediaItem The media item.
|
|
||||||
* @param {!PlayerConfiguration} playerConfiguration The player configuration.
|
|
||||||
*/
|
|
||||||
mapDrmConfiguration_(mediaItem, playerConfiguration) {
|
|
||||||
if (!mediaItem.drmSchemes) {
|
|
||||||
playerConfiguration.drm = EMPTY_DRM_CONFIGURATION;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const drmConfiguration = /** @type {!DrmConfiguration} */({
|
|
||||||
servers: {},
|
|
||||||
});
|
|
||||||
let hasDrmServer = false;
|
|
||||||
mediaItem.drmSchemes.forEach((scheme) => {
|
|
||||||
const drmSystem = DRM_SYSTEMS[scheme.uuid];
|
|
||||||
if (drmSystem && scheme.licenseServer && scheme.licenseServer.uri) {
|
|
||||||
hasDrmServer = true;
|
|
||||||
drmConfiguration.servers[drmSystem] = scheme.licenseServer.uri;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
playerConfiguration.drm =
|
|
||||||
hasDrmServer ? drmConfiguration : EMPTY_DRM_CONFIGURATION;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports = ConfigurationFactory;
|
|
@ -1,140 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (C) 2019 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
goog.module('exoplayer.cast.constants');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The underyling player.
|
|
||||||
*
|
|
||||||
* @enum {number}
|
|
||||||
*/
|
|
||||||
const PlaybackType = {
|
|
||||||
VIDEO_ELEMENT: 1,
|
|
||||||
SHAKA_PLAYER: 2,
|
|
||||||
UNKNOWN: 999,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Supported mime types and their playback mode.
|
|
||||||
*
|
|
||||||
* @type {!Object<string, !PlaybackType>}
|
|
||||||
*/
|
|
||||||
const SUPPORTED_MIME_TYPES = Object.freeze({
|
|
||||||
'application/dash+xml': PlaybackType.SHAKA_PLAYER,
|
|
||||||
'application/vnd.apple.mpegurl': PlaybackType.SHAKA_PLAYER,
|
|
||||||
'application/vnd.ms-sstr+xml': PlaybackType.SHAKA_PLAYER,
|
|
||||||
'application/x-mpegURL': PlaybackType.SHAKA_PLAYER,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the playback type required for a given mime type, or
|
|
||||||
* PlaybackType.UNKNOWN if the mime type is not recognized.
|
|
||||||
*
|
|
||||||
* @param {string} mimeType The mime type.
|
|
||||||
* @return {!PlaybackType} The required playback type, or PlaybackType.UNKNOWN
|
|
||||||
* if the mime type is not recognized.
|
|
||||||
*/
|
|
||||||
const getPlaybackType = function(mimeType) {
|
|
||||||
if (mimeType.startsWith('video/') || mimeType.startsWith('audio/')) {
|
|
||||||
return PlaybackType.VIDEO_ELEMENT;
|
|
||||||
} else {
|
|
||||||
return SUPPORTED_MIME_TYPES[mimeType] || PlaybackType.UNKNOWN;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error messages.
|
|
||||||
*
|
|
||||||
* @enum {string}
|
|
||||||
*/
|
|
||||||
const ErrorMessages = {
|
|
||||||
SHAKA_LOAD_ERROR: 'Error while loading media with Shaka.',
|
|
||||||
SHAKA_UNKNOWN_ERROR: 'Shaka error event captured.',
|
|
||||||
MEDIA_ELEMENT_UNKNOWN_ERROR: 'Media element error event captured.',
|
|
||||||
UNKNOWN_FATAL_ERROR: 'Fatal playback error. Shaka instance replaced.',
|
|
||||||
UNKNOWN_ERROR: 'Unknown error',
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ExoPlayer's repeat modes.
|
|
||||||
*
|
|
||||||
* @enum {string}
|
|
||||||
*/
|
|
||||||
const RepeatMode = {
|
|
||||||
OFF: 'OFF',
|
|
||||||
ONE: 'ONE',
|
|
||||||
ALL: 'ALL',
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error categories. Error categories coming from Shaka are defined in [Shaka
|
|
||||||
* source
|
|
||||||
* code](https://shaka-player-demo.appspot.com/docs/api/shaka.util.Error.html).
|
|
||||||
*
|
|
||||||
* @enum {number}
|
|
||||||
*/
|
|
||||||
const ErrorCategory = {
|
|
||||||
MEDIA_ELEMENT: 0,
|
|
||||||
FATAL_SHAKA_ERROR: 1000,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An error object to be used if no media error is assigned to the `error`
|
|
||||||
* field of the media element when an error event is fired
|
|
||||||
*
|
|
||||||
* @type {!PlayerError}
|
|
||||||
*/
|
|
||||||
const UNKNOWN_ERROR = /** @type {!PlayerError} */ (Object.freeze({
|
|
||||||
message: ErrorMessages.UNKNOWN_ERROR,
|
|
||||||
code: 0,
|
|
||||||
category: 0,
|
|
||||||
}));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UUID for the Widevine DRM scheme.
|
|
||||||
*
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
const WIDEVINE_UUID = 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UUID for the PlayReady DRM scheme.
|
|
||||||
*
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
const PLAYREADY_UUID = '9a04f079-9840-4286-ab92-e65be0885f95';
|
|
||||||
|
|
||||||
/** @type {!Object<string, string>} */
|
|
||||||
const drmSystems = {};
|
|
||||||
drmSystems[WIDEVINE_UUID] = 'com.widevine.alpha';
|
|
||||||
drmSystems[PLAYREADY_UUID] = 'com.microsoft.playready';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The uuids of the supported DRM systems.
|
|
||||||
*
|
|
||||||
* @type {!Object<string, string>}
|
|
||||||
*/
|
|
||||||
const DRM_SYSTEMS = Object.freeze(drmSystems);
|
|
||||||
|
|
||||||
exports.PlaybackType = PlaybackType;
|
|
||||||
exports.ErrorMessages = ErrorMessages;
|
|
||||||
exports.ErrorCategory = ErrorCategory;
|
|
||||||
exports.RepeatMode = RepeatMode;
|
|
||||||
exports.getPlaybackType = getPlaybackType;
|
|
||||||
exports.WIDEVINE_UUID = WIDEVINE_UUID;
|
|
||||||
exports.PLAYREADY_UUID = PLAYREADY_UUID;
|
|
||||||
exports.DRM_SYSTEMS = DRM_SYSTEMS;
|
|
||||||
exports.UNKNOWN_ERROR = UNKNOWN_ERROR;
|
|
@ -1,233 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
goog.module('exoplayer.cast.PlaybackInfoView');
|
|
||||||
|
|
||||||
const Player = goog.require('exoplayer.cast.Player');
|
|
||||||
const Timeout = goog.require('exoplayer.cast.Timeout');
|
|
||||||
const dom = goog.require('goog.dom');
|
|
||||||
|
|
||||||
/** The default timeout for hiding the UI in milliseconds. */
|
|
||||||
const SHOW_TIMEOUT_MS = 5000;
|
|
||||||
/** The timeout for hiding the UI in audio only mode in milliseconds. */
|
|
||||||
const SHOW_TIMEOUT_MS_AUDIO = 0;
|
|
||||||
/** The timeout for updating the UI while being displayed. */
|
|
||||||
const UPDATE_TIMEOUT_MS = 1000;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats a duration in milliseconds to a string in hh:mm:ss format.
|
|
||||||
*
|
|
||||||
* @param {number} durationMs The duration in milliseconds.
|
|
||||||
* @return {string} The duration formatted as hh:mm:ss.
|
|
||||||
*/
|
|
||||||
const formatTimestampMsAsString = function (durationMs) {
|
|
||||||
const hours = Math.floor(durationMs / 1000 / 60 / 60);
|
|
||||||
const minutes = Math.floor((durationMs / 1000 / 60) % 60);
|
|
||||||
const seconds = Math.floor((durationMs / 1000) % 60) % 60;
|
|
||||||
let timeString = '';
|
|
||||||
if (hours > 0) {
|
|
||||||
timeString += hours + ':';
|
|
||||||
}
|
|
||||||
if (minutes < 10) {
|
|
||||||
timeString += '0';
|
|
||||||
}
|
|
||||||
timeString += minutes + ":";
|
|
||||||
if (seconds < 10) {
|
|
||||||
timeString += '0';
|
|
||||||
}
|
|
||||||
timeString += seconds;
|
|
||||||
return timeString;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A view to display information about the current media item and playback
|
|
||||||
* progress.
|
|
||||||
*
|
|
||||||
* @constructor
|
|
||||||
* @param {!Player} player The player of which to display the
|
|
||||||
* playback info.
|
|
||||||
* @param {string} viewId The id of the playback info view.
|
|
||||||
*/
|
|
||||||
const PlaybackInfoView = function (player, viewId) {
|
|
||||||
/** @const @private {!Player} */
|
|
||||||
this.player_ = player;
|
|
||||||
/** @const @private {?Element} */
|
|
||||||
this.container_ = document.getElementById(viewId);
|
|
||||||
/** @const @private {?Element} */
|
|
||||||
this.elapsedTimeBar_ = document.getElementById('exo_elapsed_time');
|
|
||||||
/** @const @private {?Element} */
|
|
||||||
this.elapsedTimeLabel_ = document.getElementById('exo_elapsed_time_label');
|
|
||||||
/** @const @private {?Element} */
|
|
||||||
this.durationLabel_ = document.getElementById('exo_duration_label');
|
|
||||||
/** @const @private {!Timeout} */
|
|
||||||
this.hideTimeout_ = new Timeout();
|
|
||||||
/** @const @private {!Timeout} */
|
|
||||||
this.updateTimeout_ = new Timeout();
|
|
||||||
/** @private {boolean} */
|
|
||||||
this.wasPlaying_ = player.getPlayWhenReady()
|
|
||||||
&& player.getPlaybackState() === Player.PlaybackState.READY;
|
|
||||||
/** @private {number} */
|
|
||||||
this.showTimeoutMs_ = SHOW_TIMEOUT_MS;
|
|
||||||
/** @private {number} */
|
|
||||||
this.showTimeoutMsVideo_ = this.showTimeoutMs_;
|
|
||||||
|
|
||||||
if (this.wasPlaying_) {
|
|
||||||
this.hideAfterTimeout();
|
|
||||||
} else {
|
|
||||||
this.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
player.addPlayerListener((playerState) => {
|
|
||||||
if (this.container_ === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const playbackPosition = playerState.playbackPosition;
|
|
||||||
const discontinuityReason =
|
|
||||||
playbackPosition ? playbackPosition.discontinuityReason : null;
|
|
||||||
if (discontinuityReason) {
|
|
||||||
const currentMediaItem = player.getCurrentMediaItem();
|
|
||||||
this.showTimeoutMs_ =
|
|
||||||
currentMediaItem && currentMediaItem.mimeType === 'audio/*' ?
|
|
||||||
SHOW_TIMEOUT_MS_AUDIO :
|
|
||||||
this.showTimeoutMsVideo_;
|
|
||||||
}
|
|
||||||
const playWhenReady = playerState.playWhenReady;
|
|
||||||
const state = playerState.playbackState;
|
|
||||||
const isPlaying = playWhenReady && state === Player.PlaybackState.READY;
|
|
||||||
const userSeekedInBufferedRange =
|
|
||||||
discontinuityReason === Player.DiscontinuityReason.SEEK && isPlaying;
|
|
||||||
if (!isPlaying) {
|
|
||||||
this.show();
|
|
||||||
} else if ((!this.wasPlaying_ && isPlaying) || userSeekedInBufferedRange) {
|
|
||||||
this.hideAfterTimeout();
|
|
||||||
}
|
|
||||||
this.wasPlaying_ = isPlaying;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Shows the player info view. */
|
|
||||||
PlaybackInfoView.prototype.show = function () {
|
|
||||||
if (this.container_ != null) {
|
|
||||||
this.hideTimeout_.cancel();
|
|
||||||
this.updateUi_();
|
|
||||||
this.container_.style.display = 'block';
|
|
||||||
this.startUpdateTimeout_();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Hides the player info view. */
|
|
||||||
PlaybackInfoView.prototype.hideAfterTimeout = function() {
|
|
||||||
if (this.container_ === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.show();
|
|
||||||
this.hideTimeout_.postDelayed(this.showTimeoutMs_).then(() => {
|
|
||||||
this.container_.style.display = 'none';
|
|
||||||
this.updateTimeout_.cancel();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the playback info view timeout. The playback info view is automatically
|
|
||||||
* hidden after this duration of time has elapsed without show() being called
|
|
||||||
* again. When playing streams with content type 'audio/*' the view is always
|
|
||||||
* displayed.
|
|
||||||
*
|
|
||||||
* @param {number} showTimeoutMs The duration in milliseconds. A non-positive
|
|
||||||
* value will cause the view to remain visible indefinitely.
|
|
||||||
*/
|
|
||||||
PlaybackInfoView.prototype.setShowTimeoutMs = function(showTimeoutMs) {
|
|
||||||
this.showTimeoutMs_ = showTimeoutMs;
|
|
||||||
this.showTimeoutMsVideo_ = showTimeoutMs;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates all UI components.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
PlaybackInfoView.prototype.updateUi_ = function () {
|
|
||||||
const elapsedTimeMs = this.player_.getCurrentPositionMs();
|
|
||||||
const durationMs = this.player_.getDurationMs();
|
|
||||||
if (this.elapsedTimeLabel_ !== null) {
|
|
||||||
this.updateDuration_(this.elapsedTimeLabel_, elapsedTimeMs, false);
|
|
||||||
}
|
|
||||||
if (this.durationLabel_ !== null) {
|
|
||||||
this.updateDuration_(this.durationLabel_, durationMs, true);
|
|
||||||
}
|
|
||||||
if (this.elapsedTimeBar_ !== null) {
|
|
||||||
this.updateProgressBar_(elapsedTimeMs, durationMs);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adjust the progress bar indicating the elapsed time relative to the duration.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @param {number} elapsedTimeMs The elapsed time in milliseconds.
|
|
||||||
* @param {number} durationMs The duration in milliseconds.
|
|
||||||
*/
|
|
||||||
PlaybackInfoView.prototype.updateProgressBar_ =
|
|
||||||
function(elapsedTimeMs, durationMs) {
|
|
||||||
if (elapsedTimeMs <= 0 || durationMs <= 0) {
|
|
||||||
this.elapsedTimeBar_.style.width = 0;
|
|
||||||
} else {
|
|
||||||
const widthPercentage = elapsedTimeMs / durationMs * 100;
|
|
||||||
this.elapsedTimeBar_.style.width = Math.min(100, widthPercentage) + '%';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the display value of the duration in the DOM formatted as hh:mm:ss.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @param {!Element} element The element to update.
|
|
||||||
* @param {number} durationMs The duration in milliseconds.
|
|
||||||
* @param {boolean} hideZero If true values of zero and below are not displayed.
|
|
||||||
*/
|
|
||||||
PlaybackInfoView.prototype.updateDuration_ =
|
|
||||||
function (element, durationMs, hideZero) {
|
|
||||||
while (element.firstChild) {
|
|
||||||
element.removeChild(element.firstChild);
|
|
||||||
}
|
|
||||||
if (durationMs <= 0 && !hideZero) {
|
|
||||||
element.appendChild(dom.createDom(dom.TagName.SPAN, {},
|
|
||||||
formatTimestampMsAsString(0)));
|
|
||||||
} else if (durationMs > 0) {
|
|
||||||
element.appendChild(dom.createDom(dom.TagName.SPAN, {},
|
|
||||||
formatTimestampMsAsString(durationMs)));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts a repeating timeout that updates the UI every UPDATE_TIMEOUT_MS
|
|
||||||
* milliseconds.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
PlaybackInfoView.prototype.startUpdateTimeout_ = function() {
|
|
||||||
this.updateTimeout_.cancel();
|
|
||||||
if (!this.player_.getPlayWhenReady() ||
|
|
||||||
this.player_.getPlaybackState() !== Player.PlaybackState.READY) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.updateTimeout_.postDelayed(UPDATE_TIMEOUT_MS).then(() => {
|
|
||||||
this.updateUi_();
|
|
||||||
this.startUpdateTimeout_();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
exports = PlaybackInfoView;
|
|
File diff suppressed because it is too large
Load Diff
@ -1,68 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2019 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
goog.module('exoplayer.cast.Timeout');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A timeout which can be cancelled.
|
|
||||||
*/
|
|
||||||
class Timeout {
|
|
||||||
constructor() {
|
|
||||||
/** @private {?number} */
|
|
||||||
this.timeout_ = null;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Returns a promise which resolves when the duration of time defined by
|
|
||||||
* delayMs has elapsed and cancel() has not been called earlier.
|
|
||||||
*
|
|
||||||
* If the timeout is already set, the former timeout is cancelled and a new
|
|
||||||
* one is started.
|
|
||||||
*
|
|
||||||
* @param {number} delayMs The delay after which to resolve or a non-positive
|
|
||||||
* value if it should never resolve.
|
|
||||||
* @return {!Promise<undefined>} Resolves after the given delayMs or never
|
|
||||||
* for a non-positive delay.
|
|
||||||
*/
|
|
||||||
postDelayed(delayMs) {
|
|
||||||
this.cancel();
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (delayMs <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.timeout_ = setTimeout(() => {
|
|
||||||
if (this.timeout_) {
|
|
||||||
this.timeout_ = null;
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
}, delayMs);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Cancels the timeout. */
|
|
||||||
cancel() {
|
|
||||||
if (this.timeout_) {
|
|
||||||
clearTimeout(this.timeout_);
|
|
||||||
this.timeout_ = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return {boolean} true if the timeout is currently ongoing. */
|
|
||||||
isOngoing() {
|
|
||||||
return this.timeout_ !== null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports = Timeout;
|
|
@ -1,62 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
goog.module('exoplayer.cast.util');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates whether the logging is turned on.
|
|
||||||
*/
|
|
||||||
const enableLogging = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs to the console if logging enabled.
|
|
||||||
*
|
|
||||||
* @param {!Array<*>} statements The log statements to be logged.
|
|
||||||
*/
|
|
||||||
const log = function(statements) {
|
|
||||||
if (enableLogging) {
|
|
||||||
console.log.apply(console, statements);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A comparator function for uuids.
|
|
||||||
*
|
|
||||||
* @typedef {function(string,string):number}
|
|
||||||
*/
|
|
||||||
let UuidComparator;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a comparator function which sorts uuids in descending order by the
|
|
||||||
* corresponding index of the given map.
|
|
||||||
*
|
|
||||||
* @param {!Object<string, number>} uuidIndexMap The map with uuids as the key
|
|
||||||
* and the window index as the value.
|
|
||||||
* @return {!UuidComparator} The comparator for sorting.
|
|
||||||
*/
|
|
||||||
const createUuidComparator = function(uuidIndexMap) {
|
|
||||||
return (a, b) => {
|
|
||||||
const indexA = uuidIndexMap[a] || -1;
|
|
||||||
const indexB = uuidIndexMap[b] || -1;
|
|
||||||
return indexB - indexA;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
exports = {
|
|
||||||
log,
|
|
||||||
createUuidComparator,
|
|
||||||
UuidComparator,
|
|
||||||
};
|
|
@ -1,33 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2019 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @fileoverview Declares constants which are provided by the CAF externs and
|
|
||||||
* are not included in uncompiled unit tests.
|
|
||||||
*/
|
|
||||||
cast = {
|
|
||||||
framework: {
|
|
||||||
system: {
|
|
||||||
EventType: {
|
|
||||||
SENDER_CONNECTED: 'sender_connected',
|
|
||||||
SENDER_DISCONNECTED: 'sender_disconnected',
|
|
||||||
},
|
|
||||||
DisconnectReason: {
|
|
||||||
REQUESTED_BY_SENDER: 'requested_by_sender',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
@ -1,86 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2019 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
goog.module('exoplayer.cast.test.configurationfactory');
|
|
||||||
goog.setTestOnly();
|
|
||||||
|
|
||||||
const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory');
|
|
||||||
const testSuite = goog.require('goog.testing.testSuite');
|
|
||||||
const util = goog.require('exoplayer.cast.test.util');
|
|
||||||
|
|
||||||
let configurationFactory;
|
|
||||||
|
|
||||||
testSuite({
|
|
||||||
setUp() {
|
|
||||||
configurationFactory = new ConfigurationFactory();
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests creating the most basic configuration. */
|
|
||||||
testCreateBasicConfiguration() {
|
|
||||||
/** @type {!TrackSelectionParameters} */
|
|
||||||
const selectionParameters = /** @type {!TrackSelectionParameters} */ ({
|
|
||||||
preferredAudioLanguage: 'en',
|
|
||||||
preferredTextLanguage: 'it',
|
|
||||||
});
|
|
||||||
const configuration = configurationFactory.createConfiguration(
|
|
||||||
util.queue.slice(0, 1), selectionParameters);
|
|
||||||
assertEquals('en', configuration.preferredAudioLanguage);
|
|
||||||
assertEquals('it', configuration.preferredTextLanguage);
|
|
||||||
// Assert empty drm configuration as default.
|
|
||||||
assertArrayEquals(['servers'], Object.keys(configuration.drm));
|
|
||||||
assertArrayEquals([], Object.keys(configuration.drm.servers));
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests defaults for undefined audio and text languages. */
|
|
||||||
testCreateBasicConfiguration_languagesUndefined() {
|
|
||||||
const configuration = configurationFactory.createConfiguration(
|
|
||||||
util.queue.slice(0, 1), /** @type {!TrackSelectionParameters} */ ({}));
|
|
||||||
assertEquals('', configuration.preferredAudioLanguage);
|
|
||||||
assertEquals('', configuration.preferredTextLanguage);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests creating a drm configuration */
|
|
||||||
testCreateDrmConfiguration() {
|
|
||||||
/** @type {!MediaItem} */
|
|
||||||
const mediaItem = util.queue[1];
|
|
||||||
mediaItem.drmSchemes = [
|
|
||||||
{
|
|
||||||
uuid: 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed',
|
|
||||||
licenseServer: {
|
|
||||||
uri: 'drm-uri0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
uuid: '9a04f079-9840-4286-ab92-e65be0885f95',
|
|
||||||
licenseServer: {
|
|
||||||
uri: 'drm-uri1',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
uuid: 'unsupported-drm-uuid',
|
|
||||||
licenseServer: {
|
|
||||||
uri: 'drm-uri2',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const configuration =
|
|
||||||
configurationFactory.createConfiguration(mediaItem, {});
|
|
||||||
assertEquals('drm-uri0', configuration.drm.servers['com.widevine.alpha']);
|
|
||||||
assertEquals(
|
|
||||||
'drm-uri1', configuration.drm.servers['com.microsoft.playready']);
|
|
||||||
assertEquals(2, Object.entries(configuration.drm.servers).length);
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,36 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2019 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Externs for unit tests to avoid renaming of properties.
|
|
||||||
*
|
|
||||||
* These externs are only required when building with bazel because the
|
|
||||||
* closure_js_test compiles tests as well.
|
|
||||||
*
|
|
||||||
* @externs
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** @record */
|
|
||||||
function ValidationObject() {}
|
|
||||||
|
|
||||||
/** @type {*} */
|
|
||||||
ValidationObject.prototype.field;
|
|
||||||
|
|
||||||
/** @record */
|
|
||||||
function Uuids() {}
|
|
||||||
|
|
||||||
/** @type {!Array<string>} */
|
|
||||||
Uuids.prototype.uuids;
|
|
@ -1,49 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (C) 2019 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*
|
|
||||||
* @fileoverview Unit tests for the message dispatcher.
|
|
||||||
*/
|
|
||||||
|
|
||||||
goog.module('exoplayer.cast.test.messagedispatcher');
|
|
||||||
goog.setTestOnly();
|
|
||||||
|
|
||||||
const MessageDispatcher = goog.require('exoplayer.cast.MessageDispatcher');
|
|
||||||
const mocks = goog.require('exoplayer.cast.test.mocks');
|
|
||||||
const testSuite = goog.require('goog.testing.testSuite');
|
|
||||||
|
|
||||||
let contextMock;
|
|
||||||
let messageDispatcher;
|
|
||||||
|
|
||||||
testSuite({
|
|
||||||
setUp() {
|
|
||||||
mocks.setUp();
|
|
||||||
contextMock = mocks.createCastReceiverContextFake();
|
|
||||||
messageDispatcher = new MessageDispatcher(
|
|
||||||
'urn:x-cast:com.google.exoplayer.cast', contextMock);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Test marshalling Infinity */
|
|
||||||
testStringifyInfinity() {
|
|
||||||
const senderId = 'sender0';
|
|
||||||
const name = 'Federico Vespucci';
|
|
||||||
messageDispatcher.send(senderId, {name: name, duration: Infinity});
|
|
||||||
|
|
||||||
const msg = mocks.state().outputMessages[senderId][0];
|
|
||||||
assertUndefined(msg.duration);
|
|
||||||
assertFalse(msg.hasOwnProperty('duration'));
|
|
||||||
assertEquals(name, msg.name);
|
|
||||||
assertTrue(msg.hasOwnProperty('name'));
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,277 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (C) 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*
|
|
||||||
* @fileoverview Mocks for testing cast components.
|
|
||||||
*/
|
|
||||||
|
|
||||||
goog.module('exoplayer.cast.test.mocks');
|
|
||||||
goog.setTestOnly();
|
|
||||||
|
|
||||||
const NetworkingEngine = goog.require('shaka.net.NetworkingEngine');
|
|
||||||
|
|
||||||
let mockState;
|
|
||||||
let manifest;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the state of the mocks. Needs to be called in the setUp method of
|
|
||||||
* the unit test.
|
|
||||||
*/
|
|
||||||
const setUp = function() {
|
|
||||||
mockState = {
|
|
||||||
outputMessages: {},
|
|
||||||
listeners: {},
|
|
||||||
loadedUri: null,
|
|
||||||
preferredTextLanguage: '',
|
|
||||||
preferredAudioLanguage: '',
|
|
||||||
configuration: null,
|
|
||||||
responseFilter: null,
|
|
||||||
isSilent: false,
|
|
||||||
customMessageListener: undefined,
|
|
||||||
mediaElementState: {
|
|
||||||
removedAttributes: [],
|
|
||||||
},
|
|
||||||
manifestState: {
|
|
||||||
isLive: false,
|
|
||||||
windowDuration: 20,
|
|
||||||
startTime: 0,
|
|
||||||
delay: 10,
|
|
||||||
},
|
|
||||||
getManifest: () => manifest,
|
|
||||||
setManifest: (m) => {
|
|
||||||
manifest = m;
|
|
||||||
},
|
|
||||||
shakaError: {
|
|
||||||
severity: /** CRITICAL */ 2,
|
|
||||||
code: /** not 7000 (LOAD_INTERUPTED) */ 3,
|
|
||||||
category: /** any */ 1,
|
|
||||||
},
|
|
||||||
simulateLoad: simulateLoadSuccess,
|
|
||||||
/** @type {function(boolean)} */
|
|
||||||
setShakaThrowsOnLoad: (doThrow) => {
|
|
||||||
mockState.simulateLoad = doThrow ? throwShakaError : simulateLoadSuccess;
|
|
||||||
},
|
|
||||||
simulateUnload: simulateUnloadSuccess,
|
|
||||||
/** @type {function(boolean)} */
|
|
||||||
setShakaThrowsOnUnload: (doThrow) => {
|
|
||||||
mockState.simulateUnload =
|
|
||||||
doThrow ? throwShakaError : simulateUnloadSuccess;
|
|
||||||
},
|
|
||||||
onSenderConnected: undefined,
|
|
||||||
onSenderDisconnected: undefined,
|
|
||||||
};
|
|
||||||
manifest = {
|
|
||||||
periods: [{startTime: mockState.manifestState.startTime}],
|
|
||||||
presentationTimeline: {
|
|
||||||
getDuration: () => mockState.manifestState.windowDuration,
|
|
||||||
isLive: () => mockState.manifestState.isLive,
|
|
||||||
getSegmentAvailabilityStart: () => 0,
|
|
||||||
getSegmentAvailabilityEnd: () => mockState.manifestState.windowDuration,
|
|
||||||
getSeekRangeStart: () => 0,
|
|
||||||
getSeekRangeEnd: () => mockState.manifestState.windowDuration -
|
|
||||||
mockState.manifestState.delay,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simulates a successful `shakaPlayer.load` call.
|
|
||||||
*
|
|
||||||
* @param {string} uri The uri to load.
|
|
||||||
*/
|
|
||||||
const simulateLoadSuccess = (uri) => {
|
|
||||||
mockState.loadedUri = uri;
|
|
||||||
notifyListeners('streaming');
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Simulates a successful `shakaPlayer.unload` call. */
|
|
||||||
const simulateUnloadSuccess = () => {
|
|
||||||
mockState.loadedUri = undefined;
|
|
||||||
notifyListeners('unloading');
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @throws {!ShakaError} Thrown in any case. */
|
|
||||||
const throwShakaError = () => {
|
|
||||||
throw mockState.shakaError;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a fake event listener.
|
|
||||||
*
|
|
||||||
* @param {string} type The type of the listener.
|
|
||||||
* @param {function(!Object)} listener The callback listener.
|
|
||||||
*/
|
|
||||||
const addEventListener = function(type, listener) {
|
|
||||||
mockState.listeners[type] = mockState.listeners[type] || [];
|
|
||||||
mockState.listeners[type].push(listener);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notifies the fake listeners of the given type.
|
|
||||||
*
|
|
||||||
* @param {string} type The type of the listener to notify.
|
|
||||||
*/
|
|
||||||
const notifyListeners = function(type) {
|
|
||||||
if (mockState.isSilent || !mockState.listeners[type]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (let i = 0; i < mockState.listeners[type].length; i++) {
|
|
||||||
mockState.listeners[type][i]({
|
|
||||||
type: type
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an observable for which listeners can be added.
|
|
||||||
*
|
|
||||||
* @return {!Object} An observable object.
|
|
||||||
*/
|
|
||||||
const createObservable = () => {
|
|
||||||
return {
|
|
||||||
addEventListener: (type, listener) => {
|
|
||||||
addEventListener(type, listener);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a fake for the shaka player.
|
|
||||||
*
|
|
||||||
* @return {!shaka.Player} A shaka player mock object.
|
|
||||||
*/
|
|
||||||
const createShakaFake = () => {
|
|
||||||
const shakaFake = /** @type {!shaka.Player} */(createObservable());
|
|
||||||
const mediaElement = createMediaElementFake();
|
|
||||||
/**
|
|
||||||
* @return {!HTMLMediaElement} A media element.
|
|
||||||
*/
|
|
||||||
shakaFake.getMediaElement = () => mediaElement;
|
|
||||||
shakaFake.getAudioLanguages = () => [];
|
|
||||||
shakaFake.getVariantTracks = () => [];
|
|
||||||
shakaFake.configure = (configuration) => {
|
|
||||||
mockState.configuration = configuration;
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
shakaFake.selectTextLanguage = (language) => {
|
|
||||||
mockState.preferredTextLanguage = language;
|
|
||||||
};
|
|
||||||
shakaFake.selectAudioLanguage = (language) => {
|
|
||||||
mockState.preferredAudioLanguage = language;
|
|
||||||
};
|
|
||||||
shakaFake.getManifest = () => manifest;
|
|
||||||
shakaFake.unload = async () => mockState.simulateUnload();
|
|
||||||
shakaFake.load = async (uri) => mockState.simulateLoad(uri);
|
|
||||||
shakaFake.getNetworkingEngine = () => {
|
|
||||||
return /** @type {!NetworkingEngine} */ ({
|
|
||||||
registerResponseFilter: (responseFilter) => {
|
|
||||||
mockState.responseFilter = responseFilter;
|
|
||||||
},
|
|
||||||
unregisterResponseFilter: (responseFilter) => {
|
|
||||||
if (mockState.responseFilter !== responseFilter) {
|
|
||||||
throw new Error('unregistering invalid response filter');
|
|
||||||
} else {
|
|
||||||
mockState.responseFilter = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return shakaFake;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a fake for a media element.
|
|
||||||
*
|
|
||||||
* @return {!HTMLMediaElement} A media element fake.
|
|
||||||
*/
|
|
||||||
const createMediaElementFake = () => {
|
|
||||||
const mediaElementFake = /** @type {!HTMLMediaElement} */(createObservable());
|
|
||||||
mediaElementFake.load = () => {
|
|
||||||
// Do nothing.
|
|
||||||
};
|
|
||||||
mediaElementFake.play = () => {
|
|
||||||
mediaElementFake.paused = false;
|
|
||||||
notifyListeners('playing');
|
|
||||||
return Promise.resolve();
|
|
||||||
};
|
|
||||||
mediaElementFake.pause = () => {
|
|
||||||
mediaElementFake.paused = true;
|
|
||||||
notifyListeners('pause');
|
|
||||||
};
|
|
||||||
mediaElementFake.seekable = /** @type {!TimeRanges} */({
|
|
||||||
length: 1,
|
|
||||||
start: (index) => mockState.manifestState.startTime,
|
|
||||||
end: (index) => mockState.manifestState.windowDuration,
|
|
||||||
});
|
|
||||||
mediaElementFake.removeAttribute = (name) => {
|
|
||||||
mockState.mediaElementState.removedAttributes.push(name);
|
|
||||||
if (name === 'src') {
|
|
||||||
mockState.loadedUri = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
mediaElementFake.hasAttribute = (name) => {
|
|
||||||
return name === 'src' && !!mockState.loadedUri;
|
|
||||||
};
|
|
||||||
mediaElementFake.buffered = /** @type {!TimeRanges} */ ({
|
|
||||||
length: 0,
|
|
||||||
start: (index) => null,
|
|
||||||
end: (index) => null,
|
|
||||||
});
|
|
||||||
mediaElementFake.paused = true;
|
|
||||||
return mediaElementFake;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a cast receiver manager fake.
|
|
||||||
*
|
|
||||||
* @return {!Object} A cast receiver manager fake.
|
|
||||||
*/
|
|
||||||
const createCastReceiverContextFake = () => {
|
|
||||||
return {
|
|
||||||
addCustomMessageListener: (namespace, listener) => {
|
|
||||||
mockState.customMessageListener = listener;
|
|
||||||
},
|
|
||||||
sendCustomMessage: (namespace, senderId, message) => {
|
|
||||||
mockState.outputMessages[senderId] =
|
|
||||||
mockState.outputMessages[senderId] || [];
|
|
||||||
mockState.outputMessages[senderId].push(message);
|
|
||||||
},
|
|
||||||
addEventListener: (eventName, listener) => {
|
|
||||||
switch (eventName) {
|
|
||||||
case 'sender_connected':
|
|
||||||
mockState.onSenderConnected = listener;
|
|
||||||
break;
|
|
||||||
case 'sender_disconnected':
|
|
||||||
mockState.onSenderDisconnected = listener;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getSenders: () => [{id: 'sender0'}],
|
|
||||||
start: () => {},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the state of the mocks.
|
|
||||||
*
|
|
||||||
* @return {?Object}
|
|
||||||
*/
|
|
||||||
const state = () => mockState;
|
|
||||||
|
|
||||||
exports.createCastReceiverContextFake = createCastReceiverContextFake;
|
|
||||||
exports.createShakaFake = createShakaFake;
|
|
||||||
exports.notifyListeners = notifyListeners;
|
|
||||||
exports.setUp = setUp;
|
|
||||||
exports.state = state;
|
|
@ -1,242 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (C) 2019 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*
|
|
||||||
* @fileoverview Unit tests for the playback info view.
|
|
||||||
*/
|
|
||||||
|
|
||||||
goog.module('exoplayer.cast.test.PlaybackInfoView');
|
|
||||||
goog.setTestOnly();
|
|
||||||
|
|
||||||
const PlaybackInfoView = goog.require('exoplayer.cast.PlaybackInfoView');
|
|
||||||
const Player = goog.require('exoplayer.cast.Player');
|
|
||||||
const testSuite = goog.require('goog.testing.testSuite');
|
|
||||||
|
|
||||||
/** The state of the player mock */
|
|
||||||
let mockState;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the state of the mock. Needs to be called in the setUp method of
|
|
||||||
* the unit test.
|
|
||||||
*/
|
|
||||||
const setUpMockState = function() {
|
|
||||||
mockState = {
|
|
||||||
playWhenReady: false,
|
|
||||||
currentPositionMs: 1000,
|
|
||||||
durationMs: 10 * 1000,
|
|
||||||
playbackState: 'READY',
|
|
||||||
discontinuityReason: undefined,
|
|
||||||
listeners: [],
|
|
||||||
currentMediaItem: {
|
|
||||||
mimeType: 'video/*',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Notifies registered listeners with the current player state. */
|
|
||||||
const notifyListeners = function() {
|
|
||||||
if (!mockState) {
|
|
||||||
console.warn(
|
|
||||||
'mock state not initialized. Did you call setUp ' +
|
|
||||||
'when setting up the test case?');
|
|
||||||
}
|
|
||||||
mockState.listeners.forEach((listener) => {
|
|
||||||
listener({
|
|
||||||
playWhenReady: mockState.playWhenReady,
|
|
||||||
playbackState: mockState.playbackState,
|
|
||||||
playbackPosition: {
|
|
||||||
currentPositionMs: mockState.currentPositionMs,
|
|
||||||
discontinuityReason: mockState.discontinuityReason,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a sufficient mock of the Player.
|
|
||||||
*
|
|
||||||
* @return {!Player}
|
|
||||||
*/
|
|
||||||
const createPlayerMock = function() {
|
|
||||||
return /** @type {!Player} */ ({
|
|
||||||
addPlayerListener: (listener) => {
|
|
||||||
mockState.listeners.push(listener);
|
|
||||||
},
|
|
||||||
getPlayWhenReady: () => mockState.playWhenReady,
|
|
||||||
getPlaybackState: () => mockState.playbackState,
|
|
||||||
getCurrentPositionMs: () => mockState.currentPositionMs,
|
|
||||||
getDurationMs: () => mockState.durationMs,
|
|
||||||
getCurrentMediaItem: () => mockState.currentMediaItem,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Inserts the DOM structure the playback info view needs. */
|
|
||||||
const insertComponentDom = function() {
|
|
||||||
const container = appendChild(document.body, 'div', 'container-id');
|
|
||||||
appendChild(container, 'div', 'exo_elapsed_time');
|
|
||||||
appendChild(container, 'div', 'exo_elapsed_time_label');
|
|
||||||
appendChild(container, 'div', 'exo_duration_label');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates and appends a child to the parent element.
|
|
||||||
*
|
|
||||||
* @param {!Element} parent The parent element.
|
|
||||||
* @param {string} tagName The tag name of the child element.
|
|
||||||
* @param {string} id The id of the child element.
|
|
||||||
* @return {!Element} The appended child element.
|
|
||||||
*/
|
|
||||||
const appendChild = function(parent, tagName, id) {
|
|
||||||
const child = document.createElement(tagName);
|
|
||||||
child.id = id;
|
|
||||||
parent.appendChild(child);
|
|
||||||
return child;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Removes the inserted elements from the DOM again. */
|
|
||||||
const removeComponentDom = function() {
|
|
||||||
const container = document.getElementById('container-id');
|
|
||||||
if (container) {
|
|
||||||
container.parentNode.removeChild(container);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let playbackInfoView;
|
|
||||||
|
|
||||||
testSuite({
|
|
||||||
setUp() {
|
|
||||||
insertComponentDom();
|
|
||||||
setUpMockState();
|
|
||||||
playbackInfoView = new PlaybackInfoView(
|
|
||||||
createPlayerMock(), /** containerId= */ 'container-id');
|
|
||||||
playbackInfoView.setShowTimeoutMs(1);
|
|
||||||
},
|
|
||||||
|
|
||||||
tearDown() {
|
|
||||||
removeComponentDom();
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests setting the show timeout. */
|
|
||||||
testSetShowTimeout() {
|
|
||||||
assertEquals(1, playbackInfoView.showTimeoutMs_);
|
|
||||||
playbackInfoView.setShowTimeoutMs(10);
|
|
||||||
assertEquals(10, playbackInfoView.showTimeoutMs_);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests rendering the duration to the DOM. */
|
|
||||||
testRenderDuration() {
|
|
||||||
const el = document.getElementById('exo_duration_label');
|
|
||||||
assertEquals('00:10', el.firstChild.firstChild.nodeValue);
|
|
||||||
mockState.durationMs = 35 * 1000;
|
|
||||||
notifyListeners();
|
|
||||||
assertEquals('00:35', el.firstChild.firstChild.nodeValue);
|
|
||||||
|
|
||||||
mockState.durationMs =
|
|
||||||
(12 * 60 * 60 * 1000) + (20 * 60 * 1000) + (13 * 1000);
|
|
||||||
notifyListeners();
|
|
||||||
assertEquals('12:20:13', el.firstChild.firstChild.nodeValue);
|
|
||||||
|
|
||||||
mockState.durationMs = -1000;
|
|
||||||
notifyListeners();
|
|
||||||
assertNull(el.nodeValue);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests rendering the playback position to the DOM. */
|
|
||||||
testRenderPlaybackPosition() {
|
|
||||||
const el = document.getElementById('exo_elapsed_time_label');
|
|
||||||
assertEquals('00:01', el.firstChild.firstChild.nodeValue);
|
|
||||||
mockState.currentPositionMs = 2000;
|
|
||||||
notifyListeners();
|
|
||||||
assertEquals('00:02', el.firstChild.firstChild.nodeValue);
|
|
||||||
|
|
||||||
mockState.currentPositionMs =
|
|
||||||
(12 * 60 * 60 * 1000) + (20 * 60 * 1000) + (13 * 1000);
|
|
||||||
notifyListeners();
|
|
||||||
assertEquals('12:20:13', el.firstChild.firstChild.nodeValue);
|
|
||||||
|
|
||||||
mockState.currentPositionMs = -1000;
|
|
||||||
notifyListeners();
|
|
||||||
assertNull(el.nodeValue);
|
|
||||||
|
|
||||||
mockState.currentPositionMs = 0;
|
|
||||||
notifyListeners();
|
|
||||||
assertEquals('00:00', el.firstChild.firstChild.nodeValue);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests rendering the timebar width reflects position and duration. */
|
|
||||||
testRenderTimebar() {
|
|
||||||
const el = document.getElementById('exo_elapsed_time');
|
|
||||||
assertEquals('10%', el.style.width);
|
|
||||||
|
|
||||||
mockState.currentPositionMs = 0;
|
|
||||||
notifyListeners();
|
|
||||||
assertEquals('0px', el.style.width);
|
|
||||||
|
|
||||||
mockState.currentPositionMs = 5 * 1000;
|
|
||||||
notifyListeners();
|
|
||||||
assertEquals('50%', el.style.width);
|
|
||||||
|
|
||||||
mockState.currentPositionMs = mockState.durationMs * 2;
|
|
||||||
notifyListeners();
|
|
||||||
assertEquals('100%', el.style.width);
|
|
||||||
|
|
||||||
mockState.currentPositionMs = -1;
|
|
||||||
notifyListeners();
|
|
||||||
assertEquals('0px', el.style.width);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests whether the update timeout is set and removed. */
|
|
||||||
testUpdateTimeout_setAndRemoved() {
|
|
||||||
assertFalse(playbackInfoView.updateTimeout_.isOngoing());
|
|
||||||
|
|
||||||
mockState.playWhenReady = true;
|
|
||||||
notifyListeners();
|
|
||||||
assertTrue(playbackInfoView.updateTimeout_.isOngoing());
|
|
||||||
|
|
||||||
mockState.playWhenReady = false;
|
|
||||||
notifyListeners();
|
|
||||||
assertFalse(playbackInfoView.updateTimeout_.isOngoing());
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests whether the show timeout is set when playback starts. */
|
|
||||||
testHideTimeout_setAndRemoved() {
|
|
||||||
assertFalse(playbackInfoView.hideTimeout_.isOngoing());
|
|
||||||
|
|
||||||
mockState.playWhenReady = true;
|
|
||||||
notifyListeners();
|
|
||||||
assertNotUndefined(playbackInfoView.hideTimeout_);
|
|
||||||
assertTrue(playbackInfoView.hideTimeout_.isOngoing());
|
|
||||||
|
|
||||||
mockState.playWhenReady = false;
|
|
||||||
notifyListeners();
|
|
||||||
assertFalse(playbackInfoView.hideTimeout_.isOngoing());
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Test whether the view switches to always on for audio media. */
|
|
||||||
testAlwaysOnForAudio() {
|
|
||||||
playbackInfoView.setShowTimeoutMs(50);
|
|
||||||
assertEquals(50, playbackInfoView.showTimeoutMs_);
|
|
||||||
// The player transitions from video to audio stream.
|
|
||||||
mockState.discontinuityReason = 'PERIOD_TRANSITION';
|
|
||||||
mockState.currentMediaItem.mimeType = 'audio/*';
|
|
||||||
notifyListeners();
|
|
||||||
assertEquals(0, playbackInfoView.showTimeoutMs_);
|
|
||||||
|
|
||||||
mockState.discontinuityReason = 'PERIOD_TRANSITION';
|
|
||||||
mockState.currentMediaItem.mimeType = 'video/*';
|
|
||||||
notifyListeners();
|
|
||||||
assertEquals(50, playbackInfoView.showTimeoutMs_);
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
@ -1,470 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (C) 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*
|
|
||||||
* @fileoverview Unit tests for playback methods.
|
|
||||||
*/
|
|
||||||
|
|
||||||
goog.module('exoplayer.cast.test');
|
|
||||||
goog.setTestOnly();
|
|
||||||
|
|
||||||
const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory');
|
|
||||||
const Player = goog.require('exoplayer.cast.Player');
|
|
||||||
const mocks = goog.require('exoplayer.cast.test.mocks');
|
|
||||||
const testSuite = goog.require('goog.testing.testSuite');
|
|
||||||
const util = goog.require('exoplayer.cast.test.util');
|
|
||||||
|
|
||||||
let player;
|
|
||||||
let shakaFake;
|
|
||||||
|
|
||||||
testSuite({
|
|
||||||
setUp() {
|
|
||||||
mocks.setUp();
|
|
||||||
shakaFake = mocks.createShakaFake();
|
|
||||||
player = new Player(shakaFake, new ConfigurationFactory());
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests the player initialisation */
|
|
||||||
testPlayerInitialisation() {
|
|
||||||
mocks.state().isSilent = true;
|
|
||||||
const states = [];
|
|
||||||
let stateCounter = 0;
|
|
||||||
let currentState;
|
|
||||||
player.addPlayerListener((playerState) => {
|
|
||||||
states.push(playerState);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Dump the initial state manually.
|
|
||||||
player.invalidate();
|
|
||||||
stateCounter++;
|
|
||||||
assertEquals(stateCounter, states.length);
|
|
||||||
currentState = states[stateCounter - 1];
|
|
||||||
assertEquals(0, currentState.mediaQueue.length);
|
|
||||||
assertEquals(0, currentState.windowIndex);
|
|
||||||
assertNull(currentState.playbackPosition);
|
|
||||||
|
|
||||||
// Seek with uuid to prepare with later
|
|
||||||
const uuid = 'uuid1';
|
|
||||||
player.seekToUuid(uuid, 30 * 1000);
|
|
||||||
stateCounter++;
|
|
||||||
assertEquals(stateCounter, states.length);
|
|
||||||
currentState = states[stateCounter - 1];
|
|
||||||
assertEquals(30 * 1000, player.getCurrentPositionMs());
|
|
||||||
assertEquals(0, player.getCurrentWindowIndex());
|
|
||||||
assertEquals(-1, player.windowIndex_);
|
|
||||||
assertEquals(1, currentState.playbackPosition.periodId);
|
|
||||||
assertEquals(uuid, currentState.playbackPosition.uuid);
|
|
||||||
assertEquals(uuid, player.uuidToPrepare_);
|
|
||||||
|
|
||||||
// Add a DASH media item.
|
|
||||||
player.addQueueItems(0, util.queue.slice(0, 2));
|
|
||||||
stateCounter++;
|
|
||||||
assertEquals(stateCounter, states.length);
|
|
||||||
currentState = states[stateCounter - 1];
|
|
||||||
assertEquals('IDLE', currentState.playbackState);
|
|
||||||
assertNotNull(currentState.playbackPosition);
|
|
||||||
util.assertUuidIndexMap(player.queueUuidIndexMap_, currentState.mediaQueue);
|
|
||||||
|
|
||||||
// Prepare.
|
|
||||||
player.prepare();
|
|
||||||
stateCounter++;
|
|
||||||
assertEquals(stateCounter, states.length);
|
|
||||||
currentState = states[stateCounter - 1];
|
|
||||||
assertEquals(2, currentState.mediaQueue.length);
|
|
||||||
assertEquals('BUFFERING', currentState.playbackState);
|
|
||||||
assertEquals(
|
|
||||||
Player.DUMMY_MEDIA_ITEM_INFO, currentState.mediaItemsInfo[uuid]);
|
|
||||||
assertNull(player.uuidToPrepare_);
|
|
||||||
|
|
||||||
// The video element starts waiting.
|
|
||||||
mocks.state().isSilent = false;
|
|
||||||
mocks.notifyListeners('waiting');
|
|
||||||
// Nothing happens, masked buffering state after preparing.
|
|
||||||
assertEquals(stateCounter, states.length);
|
|
||||||
|
|
||||||
// The manifest arrives.
|
|
||||||
mocks.notifyListeners('streaming');
|
|
||||||
stateCounter++;
|
|
||||||
assertEquals(stateCounter, states.length);
|
|
||||||
currentState = states[stateCounter - 1];
|
|
||||||
assertEquals(2, currentState.mediaQueue.length);
|
|
||||||
assertEquals('BUFFERING', currentState.playbackState);
|
|
||||||
assertEquals(uuid, currentState.playbackPosition.uuid);
|
|
||||||
assertEquals(0, currentState.playbackPosition.periodId);
|
|
||||||
assertEquals(30 * 1000, currentState.playbackPosition.positionMs);
|
|
||||||
// The dummy media item info has been replaced by the real one.
|
|
||||||
assertEquals(20000000, currentState.mediaItemsInfo[uuid].windowDurationUs);
|
|
||||||
assertEquals(0, currentState.mediaItemsInfo[uuid].defaultStartPositionUs);
|
|
||||||
assertEquals(0, currentState.mediaItemsInfo[uuid].positionInFirstPeriodUs);
|
|
||||||
assertTrue(currentState.mediaItemsInfo[uuid].isSeekable);
|
|
||||||
assertFalse(currentState.mediaItemsInfo[uuid].isDynamic);
|
|
||||||
|
|
||||||
// Tracks have initially changed.
|
|
||||||
mocks.notifyListeners('trackschanged');
|
|
||||||
// Nothing happens because the media item info remains the same.
|
|
||||||
assertEquals(stateCounter, states.length);
|
|
||||||
|
|
||||||
// The video element reports the first frame rendered.
|
|
||||||
mocks.notifyListeners('loadeddata');
|
|
||||||
stateCounter++;
|
|
||||||
assertEquals(stateCounter, states.length);
|
|
||||||
currentState = states[stateCounter - 1];
|
|
||||||
assertEquals(2, currentState.mediaQueue.length);
|
|
||||||
assertEquals('READY', currentState.playbackState);
|
|
||||||
assertEquals(uuid, currentState.playbackPosition.uuid);
|
|
||||||
assertEquals(0, currentState.playbackPosition.periodId);
|
|
||||||
assertEquals(30 * 1000, currentState.playbackPosition.positionMs);
|
|
||||||
|
|
||||||
// Playback starts.
|
|
||||||
mocks.notifyListeners('playing');
|
|
||||||
// Nothing happens; we are ready already.
|
|
||||||
assertEquals(stateCounter, states.length);
|
|
||||||
|
|
||||||
// Add another queue item.
|
|
||||||
player.addQueueItems(1, util.queue.slice(3, 4));
|
|
||||||
stateCounter++;
|
|
||||||
assertEquals(stateCounter, states.length);
|
|
||||||
mocks.state().isSilent = true;
|
|
||||||
// Seek to the next queue item.
|
|
||||||
player.seekToWindow(1, 0);
|
|
||||||
stateCounter++;
|
|
||||||
assertEquals(stateCounter, states.length);
|
|
||||||
currentState = states[stateCounter - 1];
|
|
||||||
const uuid1 = currentState.mediaQueue[1].uuid;
|
|
||||||
assertEquals(
|
|
||||||
Player.DUMMY_MEDIA_ITEM_INFO, currentState.mediaItemsInfo[uuid1]);
|
|
||||||
util.assertUuidIndexMap(player.queueUuidIndexMap_, currentState.mediaQueue);
|
|
||||||
|
|
||||||
// The video element starts waiting.
|
|
||||||
mocks.state().isSilent = false;
|
|
||||||
mocks.notifyListeners('waiting');
|
|
||||||
// Nothing happens, masked buffering state after preparing.
|
|
||||||
assertEquals(stateCounter, states.length);
|
|
||||||
|
|
||||||
// The manifest arrives.
|
|
||||||
mocks.notifyListeners('streaming');
|
|
||||||
stateCounter++;
|
|
||||||
assertEquals(stateCounter, states.length);
|
|
||||||
currentState = states[stateCounter - 1];
|
|
||||||
// The dummy media item info has been replaced by the real one.
|
|
||||||
assertEquals(20000000, currentState.mediaItemsInfo[uuid].windowDurationUs);
|
|
||||||
assertEquals(0, currentState.mediaItemsInfo[uuid].defaultStartPositionUs);
|
|
||||||
assertEquals(0, currentState.mediaItemsInfo[uuid].positionInFirstPeriodUs);
|
|
||||||
assertTrue(currentState.mediaItemsInfo[uuid].isSeekable);
|
|
||||||
assertFalse(currentState.mediaItemsInfo[uuid].isDynamic);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests next and previous window when not yet prepared. */
|
|
||||||
testNextPreviousWindow_notPrepared() {
|
|
||||||
assertEquals(-1, player.getNextWindowIndex());
|
|
||||||
assertEquals(-1, player.getPreviousWindowIndex());
|
|
||||||
player.addQueueItems(0, util.queue.slice(0, 2));
|
|
||||||
assertEquals(-1, player.getNextWindowIndex());
|
|
||||||
assertEquals(-1, player.getPreviousWindowIndex());
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests setting play when ready. */
|
|
||||||
testPlayWhenReady() {
|
|
||||||
player.addQueueItems(0, util.queue.slice(0, 3));
|
|
||||||
let playWhenReady = false;
|
|
||||||
player.addPlayerListener((state) => {
|
|
||||||
playWhenReady = state.playWhenReady;
|
|
||||||
});
|
|
||||||
|
|
||||||
assertEquals(false, player.getPlayWhenReady());
|
|
||||||
assertEquals(false, playWhenReady);
|
|
||||||
|
|
||||||
player.setPlayWhenReady(true);
|
|
||||||
assertEquals(true, player.getPlayWhenReady());
|
|
||||||
assertEquals(true, playWhenReady);
|
|
||||||
|
|
||||||
player.setPlayWhenReady(false);
|
|
||||||
assertEquals(false, player.getPlayWhenReady());
|
|
||||||
assertEquals(false, playWhenReady);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests seeking to another position in the actual window. */
|
|
||||||
async testSeek_inWindow() {
|
|
||||||
player.addQueueItems(0, util.queue.slice(0, 3));
|
|
||||||
await player.seekToWindow(0, 1000);
|
|
||||||
|
|
||||||
assertEquals(1, shakaFake.getMediaElement().currentTime);
|
|
||||||
assertEquals(1000, player.getCurrentPositionMs());
|
|
||||||
assertEquals(0, player.getCurrentWindowIndex());
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests seeking to another window. */
|
|
||||||
async testSeek_nextWindow() {
|
|
||||||
player.addQueueItems(0, util.queue.slice(0, 3));
|
|
||||||
await player.prepare();
|
|
||||||
assertEquals(util.queue[0].media.uri, shakaFake.getMediaElement().src);
|
|
||||||
assertEquals(-1, player.getPreviousWindowIndex());
|
|
||||||
assertEquals(1, player.getNextWindowIndex());
|
|
||||||
|
|
||||||
player.seekToWindow(1, 2000);
|
|
||||||
assertEquals(0, player.getPreviousWindowIndex());
|
|
||||||
assertEquals(2, player.getNextWindowIndex());
|
|
||||||
assertEquals(2000, player.getCurrentPositionMs());
|
|
||||||
assertEquals(1, player.getCurrentWindowIndex());
|
|
||||||
assertEquals(util.queue[1].media.uri, mocks.state().loadedUri);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests the repeat mode 'none' */
|
|
||||||
testRepeatMode_none() {
|
|
||||||
player.addQueueItems(0, util.queue.slice(0, 3));
|
|
||||||
player.prepare();
|
|
||||||
assertEquals(Player.RepeatMode.OFF, player.getRepeatMode());
|
|
||||||
assertEquals(-1, player.getPreviousWindowIndex());
|
|
||||||
assertEquals(1, player.getNextWindowIndex());
|
|
||||||
|
|
||||||
player.seekToWindow(2, 0);
|
|
||||||
assertEquals(1, player.getPreviousWindowIndex());
|
|
||||||
assertEquals(-1, player.getNextWindowIndex());
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests the repeat mode 'all'. */
|
|
||||||
testRepeatMode_all() {
|
|
||||||
let repeatMode;
|
|
||||||
player.addQueueItems(0, util.queue.slice(0, 3));
|
|
||||||
player.prepare();
|
|
||||||
player.addPlayerListener((state) => {
|
|
||||||
repeatMode = state.repeatMode;
|
|
||||||
});
|
|
||||||
player.setRepeatMode(Player.RepeatMode.ALL);
|
|
||||||
assertEquals(Player.RepeatMode.ALL, repeatMode);
|
|
||||||
|
|
||||||
player.seekToWindow(0,0);
|
|
||||||
assertEquals(2, player.getPreviousWindowIndex());
|
|
||||||
assertEquals(1, player.getNextWindowIndex());
|
|
||||||
|
|
||||||
player.seekToWindow(2, 0);
|
|
||||||
assertEquals(1, player.getPreviousWindowIndex());
|
|
||||||
assertEquals(0, player.getNextWindowIndex());
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests navigation within the queue when repeat mode and shuffle mode is on.
|
|
||||||
*/
|
|
||||||
testRepeatMode_all_inShuffleMode() {
|
|
||||||
const initialOrder = [2, 1, 0];
|
|
||||||
let shuffleOrder;
|
|
||||||
let windowIndex;
|
|
||||||
player.addQueueItems(0, util.queue.slice(0, 3), initialOrder);
|
|
||||||
player.prepare();
|
|
||||||
player.addPlayerListener((state) => {
|
|
||||||
shuffleOrder = state.shuffleOrder;
|
|
||||||
windowIndex = state.windowIndex;
|
|
||||||
});
|
|
||||||
player.setRepeatMode(Player.RepeatMode.ALL);
|
|
||||||
player.setShuffleModeEnabled(true);
|
|
||||||
assertEquals(windowIndex, player.shuffleOrder_[player.shuffleIndex_]);
|
|
||||||
assertArrayEquals(initialOrder, shuffleOrder);
|
|
||||||
|
|
||||||
player.seekToWindow(shuffleOrder[2], 0);
|
|
||||||
assertEquals(shuffleOrder[2], windowIndex);
|
|
||||||
assertEquals(shuffleOrder[0], player.getNextWindowIndex());
|
|
||||||
assertEquals(shuffleOrder[1], player.getPreviousWindowIndex());
|
|
||||||
|
|
||||||
player.seekToWindow(shuffleOrder[0], 0);
|
|
||||||
assertEquals(shuffleOrder[0], windowIndex);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests the repeat mode 'one' */
|
|
||||||
testRepeatMode_one() {
|
|
||||||
let repeatMode;
|
|
||||||
player.addQueueItems(0, util.queue.slice(0, 3));
|
|
||||||
player.prepare();
|
|
||||||
player.addPlayerListener((state) => {
|
|
||||||
repeatMode = state.repeatMode;
|
|
||||||
});
|
|
||||||
player.setRepeatMode(Player.RepeatMode.ONE);
|
|
||||||
assertEquals(Player.RepeatMode.ONE, repeatMode);
|
|
||||||
assertEquals(0, player.getPreviousWindowIndex());
|
|
||||||
assertEquals(0, player.getNextWindowIndex());
|
|
||||||
|
|
||||||
player.seekToWindow(1, 0);
|
|
||||||
assertEquals(1, player.getPreviousWindowIndex());
|
|
||||||
assertEquals(1, player.getNextWindowIndex());
|
|
||||||
|
|
||||||
player.setShuffleModeEnabled(true);
|
|
||||||
assertEquals(1, player.getPreviousWindowIndex());
|
|
||||||
assertEquals(1, player.getNextWindowIndex());
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests building a media item info from the manifest. */
|
|
||||||
testBuildMediaItemInfo_fromManifest() {
|
|
||||||
let mediaItemInfos = null;
|
|
||||||
player.addQueueItems(0, util.queue.slice(0, 3));
|
|
||||||
player.addPlayerListener((state) => {
|
|
||||||
mediaItemInfos = state.mediaItemsInfo;
|
|
||||||
});
|
|
||||||
player.seekToWindow(1, 0);
|
|
||||||
player.prepare();
|
|
||||||
assertUndefined(mediaItemInfos['uuid0']);
|
|
||||||
const mediaItemInfo = mediaItemInfos['uuid1'];
|
|
||||||
assertNotUndefined(mediaItemInfo);
|
|
||||||
assertFalse(mediaItemInfo.isDynamic);
|
|
||||||
assertTrue(mediaItemInfo.isSeekable);
|
|
||||||
assertEquals(0, mediaItemInfo.defaultStartPositionUs);
|
|
||||||
assertEquals(20 * 1000 * 1000, mediaItemInfo.windowDurationUs);
|
|
||||||
assertEquals(1, mediaItemInfo.periods.length);
|
|
||||||
assertEquals(20 * 1000 * 1000, mediaItemInfo.periods[0].durationUs);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests building a media item info with multiple periods. */
|
|
||||||
testBuildMediaItemInfo_fromManifest_multiPeriod() {
|
|
||||||
let mediaItemInfos = null;
|
|
||||||
player.addQueueItems(0, util.queue.slice(0, 3));
|
|
||||||
player.addPlayerListener((state) => {
|
|
||||||
mediaItemInfos = state.mediaItemsInfo;
|
|
||||||
});
|
|
||||||
// Setting manifest properties to emulate a multiperiod stream manifest.
|
|
||||||
mocks.state().getManifest().periods.push({startTime: 20});
|
|
||||||
mocks.state().manifestState.windowDuration = 50;
|
|
||||||
player.seekToWindow(1, 0);
|
|
||||||
player.prepare();
|
|
||||||
|
|
||||||
const mediaItemInfo = mediaItemInfos['uuid1'];
|
|
||||||
assertNotUndefined(mediaItemInfo);
|
|
||||||
assertFalse(mediaItemInfo.isDynamic);
|
|
||||||
assertTrue(mediaItemInfo.isSeekable);
|
|
||||||
assertEquals(0, mediaItemInfo.defaultStartPositionUs);
|
|
||||||
assertEquals(50 * 1000 * 1000, mediaItemInfo.windowDurationUs);
|
|
||||||
assertEquals(2, mediaItemInfo.periods.length);
|
|
||||||
assertEquals(20 * 1000 * 1000, mediaItemInfo.periods[0].durationUs);
|
|
||||||
assertEquals(30 * 1000 * 1000, mediaItemInfo.periods[1].durationUs);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests building a media item info from a live manifest. */
|
|
||||||
testBuildMediaItemInfo_fromManifest_live() {
|
|
||||||
let mediaItemInfos = null;
|
|
||||||
player.addQueueItems(0, util.queue.slice(0, 3));
|
|
||||||
player.addPlayerListener((state) => {
|
|
||||||
mediaItemInfos = state.mediaItemsInfo;
|
|
||||||
});
|
|
||||||
// Setting manifest properties to emulate a live stream manifest.
|
|
||||||
mocks.state().manifestState.isLive = true;
|
|
||||||
mocks.state().manifestState.windowDuration = 30;
|
|
||||||
mocks.state().manifestState.delay = 10;
|
|
||||||
mocks.state().getManifest().periods.push({startTime: 20});
|
|
||||||
player.seekToWindow(1, 0);
|
|
||||||
player.prepare();
|
|
||||||
|
|
||||||
const mediaItemInfo = mediaItemInfos['uuid1'];
|
|
||||||
assertNotUndefined(mediaItemInfo);
|
|
||||||
assertTrue(mediaItemInfo.isDynamic);
|
|
||||||
assertTrue(mediaItemInfo.isSeekable);
|
|
||||||
assertEquals(20 * 1000 * 1000, mediaItemInfo.defaultStartPositionUs);
|
|
||||||
assertEquals(20 * 1000 * 1000, mediaItemInfo.windowDurationUs);
|
|
||||||
assertEquals(2, mediaItemInfo.periods.length);
|
|
||||||
assertEquals(20 * 1000 * 1000, mediaItemInfo.periods[0].durationUs);
|
|
||||||
assertEquals(Infinity, mediaItemInfo.periods[1].durationUs);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests whether the shaka request filter is set for life streams. */
|
|
||||||
testRequestFilterIsSetAndRemovedForLive() {
|
|
||||||
player.addQueueItems(0, util.queue.slice(0, 3));
|
|
||||||
|
|
||||||
// Set manifest properties to emulate a live stream manifest.
|
|
||||||
mocks.state().manifestState.isLive = true;
|
|
||||||
mocks.state().manifestState.windowDuration = 30;
|
|
||||||
mocks.state().manifestState.delay = 10;
|
|
||||||
mocks.state().getManifest().periods.push({startTime: 20});
|
|
||||||
|
|
||||||
assertNull(mocks.state().responseFilter);
|
|
||||||
assertFalse(player.isManifestFilterRegistered_);
|
|
||||||
player.seekToWindow(1, 0);
|
|
||||||
player.prepare();
|
|
||||||
assertNotNull(mocks.state().responseFilter);
|
|
||||||
assertTrue(player.isManifestFilterRegistered_);
|
|
||||||
|
|
||||||
// Set manifest properties to emulate a non-live stream */
|
|
||||||
mocks.state().manifestState.isLive = false;
|
|
||||||
mocks.state().manifestState.windowDuration = 20;
|
|
||||||
mocks.state().manifestState.delay = 0;
|
|
||||||
mocks.state().getManifest().periods.push({startTime: 20});
|
|
||||||
|
|
||||||
player.seekToWindow(0, 0);
|
|
||||||
assertNull(mocks.state().responseFilter);
|
|
||||||
assertFalse(player.isManifestFilterRegistered_);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests whether the media info is removed when queue item is removed. */
|
|
||||||
testRemoveMediaItemInfo() {
|
|
||||||
let mediaItemInfos = null;
|
|
||||||
player.addQueueItems(0, util.queue.slice(0, 3));
|
|
||||||
player.addPlayerListener((state) => {
|
|
||||||
mediaItemInfos = state.mediaItemsInfo;
|
|
||||||
});
|
|
||||||
player.seekToWindow(1, 0);
|
|
||||||
player.prepare();
|
|
||||||
assertNotUndefined(mediaItemInfos['uuid1']);
|
|
||||||
player.removeQueueItems(['uuid1']);
|
|
||||||
assertUndefined(mediaItemInfos['uuid1']);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests shuffling. */
|
|
||||||
testSetShuffeModeEnabled() {
|
|
||||||
let shuffleModeEnabled = false;
|
|
||||||
player.addQueueItems(0, util.queue.slice(0, 3));
|
|
||||||
player.addPlayerListener((state) => {
|
|
||||||
shuffleModeEnabled = state.shuffleModeEnabled;
|
|
||||||
});
|
|
||||||
player.setShuffleModeEnabled(true);
|
|
||||||
assertTrue(shuffleModeEnabled);
|
|
||||||
|
|
||||||
player.setShuffleModeEnabled(false);
|
|
||||||
assertFalse(shuffleModeEnabled);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests setting a new playback order. */
|
|
||||||
async testSetShuffleOrder() {
|
|
||||||
const defaultOrder = [0, 1, 2];
|
|
||||||
let shuffleOrder;
|
|
||||||
player.addPlayerListener((state) => {
|
|
||||||
shuffleOrder = state.shuffleOrder;
|
|
||||||
});
|
|
||||||
await player.addQueueItems(0, util.queue.slice(0, 3), defaultOrder);
|
|
||||||
assertArrayEquals(defaultOrder, shuffleOrder);
|
|
||||||
|
|
||||||
player.setShuffleOrder_([2, 1, 0]);
|
|
||||||
assertArrayEquals([2, 1, 0], player.shuffleOrder_);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests setting a new playback order with incorrect length. */
|
|
||||||
async testSetShuffleOrder_incorrectLength() {
|
|
||||||
const defaultOrder = [0, 1, 2];
|
|
||||||
let shuffleOrder;
|
|
||||||
player.addPlayerListener((state) => {
|
|
||||||
shuffleOrder = state.shuffleOrder;
|
|
||||||
});
|
|
||||||
await player.addQueueItems(0, util.queue.slice(0, 3), defaultOrder);
|
|
||||||
assertArrayEquals(defaultOrder, shuffleOrder);
|
|
||||||
|
|
||||||
shuffleOrder = undefined;
|
|
||||||
player.setShuffleOrder_([2, 1]);
|
|
||||||
assertUndefined(shuffleOrder);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests falling into ENDED when prepared with empty queue. */
|
|
||||||
testPrepare_withEmptyQueue() {
|
|
||||||
player.seekToUuid('uuid1000', 1000);
|
|
||||||
assertEquals('uuid1000', player.uuidToPrepare_);
|
|
||||||
player.prepare();
|
|
||||||
assertEquals('ENDED', player.getPlaybackState());
|
|
||||||
assertNull(player.uuidToPrepare_);
|
|
||||||
player.seekToUuid('uuid1000', 1000);
|
|
||||||
assertNull(player.uuidToPrepare_);
|
|
||||||
},
|
|
||||||
});
|
|
@ -1,166 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (C) 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*
|
|
||||||
* @fileoverview Unit tests for queue manipulations.
|
|
||||||
*/
|
|
||||||
|
|
||||||
goog.module('exoplayer.cast.test.queue');
|
|
||||||
goog.setTestOnly();
|
|
||||||
|
|
||||||
const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory');
|
|
||||||
const Player = goog.require('exoplayer.cast.Player');
|
|
||||||
const mocks = goog.require('exoplayer.cast.test.mocks');
|
|
||||||
const testSuite = goog.require('goog.testing.testSuite');
|
|
||||||
const util = goog.require('exoplayer.cast.test.util');
|
|
||||||
|
|
||||||
let player;
|
|
||||||
|
|
||||||
testSuite({
|
|
||||||
setUp() {
|
|
||||||
mocks.setUp();
|
|
||||||
player = new Player(mocks.createShakaFake(), new ConfigurationFactory());
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests adding queue items. */
|
|
||||||
testAddQueueItem() {
|
|
||||||
let queue = [];
|
|
||||||
player.addPlayerListener((state) => {
|
|
||||||
queue = state.mediaQueue;
|
|
||||||
});
|
|
||||||
assertEquals(0, queue.length);
|
|
||||||
player.addQueueItems(0, util.queue.slice(0, 3));
|
|
||||||
assertEquals(util.queue[0].media.uri, queue[0].media.uri);
|
|
||||||
assertEquals(util.queue[1].media.uri, queue[1].media.uri);
|
|
||||||
assertEquals(util.queue[2].media.uri, queue[2].media.uri);
|
|
||||||
util.assertUuidIndexMap(player.queueUuidIndexMap_, queue);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests that duplicate queue items are ignored. */
|
|
||||||
testAddDuplicateQueueItem() {
|
|
||||||
let queue = [];
|
|
||||||
player.addPlayerListener((state) => {
|
|
||||||
queue = state.mediaQueue;
|
|
||||||
});
|
|
||||||
assertEquals(0, queue.length);
|
|
||||||
// Insert three items.
|
|
||||||
player.addQueueItems(0, util.queue.slice(0, 3));
|
|
||||||
// Insert two of which the first is a duplicate.
|
|
||||||
player.addQueueItems(1, util.queue.slice(2, 4));
|
|
||||||
assertEquals(4, queue.length);
|
|
||||||
assertArrayEquals(
|
|
||||||
['uuid0', 'uuid3', 'uuid1', 'uuid2'], queue.slice().map((i) => i.uuid));
|
|
||||||
util.assertUuidIndexMap(player.queueUuidIndexMap_, queue);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests moving queue items. */
|
|
||||||
testMoveQueueItem() {
|
|
||||||
const shuffleOrder = [0, 2, 1];
|
|
||||||
let queue = [];
|
|
||||||
player.addPlayerListener((state) => {
|
|
||||||
queue = state.mediaQueue;
|
|
||||||
});
|
|
||||||
player.addQueueItems(0, util.queue.slice(0, 3));
|
|
||||||
player.moveQueueItem('uuid0', 1, shuffleOrder);
|
|
||||||
assertEquals(util.queue[1].media.uri, queue[0].media.uri);
|
|
||||||
assertEquals(util.queue[0].media.uri, queue[1].media.uri);
|
|
||||||
assertEquals(util.queue[2].media.uri, queue[2].media.uri);
|
|
||||||
util.assertUuidIndexMap(player.queueUuidIndexMap_, queue);
|
|
||||||
|
|
||||||
queue = undefined;
|
|
||||||
// invalid to index
|
|
||||||
player.moveQueueItem('uuid0', 11, [0, 1, 2]);
|
|
||||||
assertTrue(typeof queue === 'undefined');
|
|
||||||
assertArrayEquals(shuffleOrder, player.shuffleOrder_);
|
|
||||||
util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_);
|
|
||||||
// negative to index
|
|
||||||
player.moveQueueItem('uuid0', -11, shuffleOrder);
|
|
||||||
assertTrue(typeof queue === 'undefined');
|
|
||||||
assertArrayEquals(shuffleOrder, player.shuffleOrder_);
|
|
||||||
util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_);
|
|
||||||
// unknown uuid
|
|
||||||
player.moveQueueItem('unknown', 1, shuffleOrder);
|
|
||||||
assertTrue(typeof queue === 'undefined');
|
|
||||||
assertArrayEquals(shuffleOrder, player.shuffleOrder_);
|
|
||||||
util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests removing queue items. */
|
|
||||||
testRemoveQueueItems() {
|
|
||||||
let queue = [];
|
|
||||||
player.addPlayerListener((state) => {
|
|
||||||
queue = state.mediaQueue;
|
|
||||||
});
|
|
||||||
player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]);
|
|
||||||
player.prepare();
|
|
||||||
player.seekToWindow(1, 0);
|
|
||||||
assertEquals(1, player.getCurrentWindowIndex());
|
|
||||||
util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_);
|
|
||||||
|
|
||||||
// Remove the first item.
|
|
||||||
player.removeQueueItems(['uuid0']);
|
|
||||||
assertEquals(2, queue.length);
|
|
||||||
assertEquals(util.queue[1].media.uri, queue[0].media.uri);
|
|
||||||
assertEquals(util.queue[2].media.uri, queue[1].media.uri);
|
|
||||||
assertEquals(0, player.getCurrentWindowIndex());
|
|
||||||
assertArrayEquals([1,0], player.shuffleOrder_);
|
|
||||||
util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_);
|
|
||||||
|
|
||||||
// Calling stop without reseting preserves the queue.
|
|
||||||
player.stop(false);
|
|
||||||
assertEquals('uuid1', player.uuidToPrepare_);
|
|
||||||
util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_);
|
|
||||||
|
|
||||||
// Remove the item at the end of the queue.
|
|
||||||
player.removeQueueItems(['uuid2']);
|
|
||||||
util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_);
|
|
||||||
|
|
||||||
// Remove the last remaining item in the queue.
|
|
||||||
player.removeQueueItems(['uuid1']);
|
|
||||||
assertEquals(0, queue.length);
|
|
||||||
assertEquals('IDLE', player.getPlaybackState());
|
|
||||||
assertEquals(0, player.getCurrentWindowIndex());
|
|
||||||
assertArrayEquals([], player.shuffleOrder_);
|
|
||||||
assertNull(player.uuidToPrepare_);
|
|
||||||
util.assertUuidIndexMap(player.queueUuidIndexMap_, player.queue_);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests removing multiple unordered queue items at once. */
|
|
||||||
testRemoveQueueItems_multiple() {
|
|
||||||
let queue = [];
|
|
||||||
player.addPlayerListener((state) => {
|
|
||||||
queue = state.mediaQueue;
|
|
||||||
});
|
|
||||||
player.addQueueItems(0, util.queue.slice(0, 6), []);
|
|
||||||
player.prepare();
|
|
||||||
|
|
||||||
assertEquals(6, queue.length);
|
|
||||||
player.removeQueueItems(['uuid1', 'uuid5', 'uuid3']);
|
|
||||||
assertArrayEquals(['uuid0', 'uuid2', 'uuid4'], queue.map((i) => i.uuid));
|
|
||||||
util.assertUuidIndexMap(player.queueUuidIndexMap_, queue);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests whether stopping with reset=true resets queue and uuidToIndexMap */
|
|
||||||
testStop_resetTrue() {
|
|
||||||
let queue = [];
|
|
||||||
player.addPlayerListener((state) => {
|
|
||||||
queue = state.mediaQueue;
|
|
||||||
});
|
|
||||||
player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]);
|
|
||||||
player.prepare();
|
|
||||||
player.stop(true);
|
|
||||||
assertEquals(0, player.queue_.length);
|
|
||||||
util.assertUuidIndexMap(player.queueUuidIndexMap_, queue);
|
|
||||||
},
|
|
||||||
});
|
|
File diff suppressed because it is too large
Load Diff
@ -1,84 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (C) 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*
|
|
||||||
* @fileoverview Unit tests for playback methods.
|
|
||||||
*/
|
|
||||||
|
|
||||||
goog.module('exoplayer.cast.test.shaka');
|
|
||||||
goog.setTestOnly();
|
|
||||||
|
|
||||||
const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory');
|
|
||||||
const Player = goog.require('exoplayer.cast.Player');
|
|
||||||
const mocks = goog.require('exoplayer.cast.test.mocks');
|
|
||||||
const testSuite = goog.require('goog.testing.testSuite');
|
|
||||||
const util = goog.require('exoplayer.cast.test.util');
|
|
||||||
|
|
||||||
let player;
|
|
||||||
let shakaFake;
|
|
||||||
|
|
||||||
testSuite({
|
|
||||||
setUp() {
|
|
||||||
mocks.setUp();
|
|
||||||
shakaFake = mocks.createShakaFake();
|
|
||||||
player = new Player(shakaFake, new ConfigurationFactory());
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests Shaka critical error handling on load. */
|
|
||||||
async testShakaCriticalError_onload() {
|
|
||||||
mocks.state().isSilent = true;
|
|
||||||
mocks.state().setShakaThrowsOnLoad(true);
|
|
||||||
let playerState;
|
|
||||||
player.addPlayerListener((state) => {
|
|
||||||
playerState = state;
|
|
||||||
});
|
|
||||||
player.addQueueItems(0, util.queue.slice(0, 2));
|
|
||||||
player.seekToUuid('uuid1', 2000);
|
|
||||||
player.setPlayWhenReady(true);
|
|
||||||
// Calling prepare triggers a critical Shaka error.
|
|
||||||
await player.prepare();
|
|
||||||
// Assert player state after error.
|
|
||||||
assertEquals('IDLE', playerState.playbackState);
|
|
||||||
assertEquals(mocks.state().shakaError.category, playerState.error.category);
|
|
||||||
assertEquals(mocks.state().shakaError.code, playerState.error.code);
|
|
||||||
assertEquals(
|
|
||||||
'loading failed for uri: http://example1.com',
|
|
||||||
playerState.error.message);
|
|
||||||
assertEquals(999, player.playbackType_);
|
|
||||||
// Assert player properties are preserved.
|
|
||||||
assertEquals(2000, player.getCurrentPositionMs());
|
|
||||||
assertTrue(player.getPlayWhenReady());
|
|
||||||
assertEquals(1, player.getCurrentWindowIndex());
|
|
||||||
assertEquals(1, player.windowIndex_);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests Shaka critical error handling on unload. */
|
|
||||||
async testShakaCriticalError_onunload() {
|
|
||||||
mocks.state().isSilent = true;
|
|
||||||
mocks.state().setShakaThrowsOnUnload(true);
|
|
||||||
let playerState;
|
|
||||||
player.addPlayerListener((state) => {
|
|
||||||
playerState = state;
|
|
||||||
});
|
|
||||||
player.addQueueItems(0, util.queue.slice(0, 2));
|
|
||||||
player.setPlayWhenReady(true);
|
|
||||||
assertUndefined(player.videoElement_.src);
|
|
||||||
// Calling prepare triggers a critical Shaka error.
|
|
||||||
await player.prepare();
|
|
||||||
// Assert player state after caught and ignored error.
|
|
||||||
await assertEquals('BUFFERING', playerState.playbackState);
|
|
||||||
assertEquals('http://example.com', player.videoElement_.src);
|
|
||||||
assertEquals(1, player.playbackType_);
|
|
||||||
},
|
|
||||||
});
|
|
@ -1,87 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (C) 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*
|
|
||||||
* @fileoverview Description of this file.
|
|
||||||
*/
|
|
||||||
|
|
||||||
goog.module('exoplayer.cast.test.util');
|
|
||||||
goog.setTestOnly();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The queue of sample media items
|
|
||||||
*
|
|
||||||
* @type {!Array<!MediaItem>}
|
|
||||||
*/
|
|
||||||
const queue = [
|
|
||||||
{
|
|
||||||
uuid: 'uuid0',
|
|
||||||
media: {
|
|
||||||
uri: 'http://example.com',
|
|
||||||
},
|
|
||||||
mimeType: 'video/*',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
uuid: 'uuid1',
|
|
||||||
media: {
|
|
||||||
uri: 'http://example1.com',
|
|
||||||
},
|
|
||||||
mimeType: 'application/dash+xml',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
uuid: 'uuid2',
|
|
||||||
media: {
|
|
||||||
uri: 'http://example2.com',
|
|
||||||
},
|
|
||||||
mimeType: 'video/*',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
uuid: 'uuid3',
|
|
||||||
media: {
|
|
||||||
uri: 'http://example3.com',
|
|
||||||
},
|
|
||||||
mimeType: 'application/dash+xml',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
uuid: 'uuid4',
|
|
||||||
media: {
|
|
||||||
uri: 'http://example4.com',
|
|
||||||
},
|
|
||||||
mimeType: 'video/*',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
uuid: 'uuid5',
|
|
||||||
media: {
|
|
||||||
uri: 'http://example5.com',
|
|
||||||
},
|
|
||||||
mimeType: 'application/dash+xml',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asserts whether the map of uuids is complete and points to the correct
|
|
||||||
* indices.
|
|
||||||
*
|
|
||||||
* @param {!Object<string, number>} uuidIndexMap The uuid to index map.
|
|
||||||
* @param {!Array<!MediaItem>} queue The media item queue.
|
|
||||||
*/
|
|
||||||
const assertUuidIndexMap = (uuidIndexMap, queue) => {
|
|
||||||
assertEquals(queue.length, Object.entries(uuidIndexMap).length);
|
|
||||||
queue.forEach((mediaItem, index) => {
|
|
||||||
assertEquals(uuidIndexMap[mediaItem.uuid], index);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.queue = queue;
|
|
||||||
exports.assertUuidIndexMap = assertUuidIndexMap;
|
|
@ -1,266 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (C) 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*
|
|
||||||
* @fileoverview Unit tests for queue manipulations.
|
|
||||||
*/
|
|
||||||
|
|
||||||
goog.module('exoplayer.cast.test.validation');
|
|
||||||
goog.setTestOnly();
|
|
||||||
|
|
||||||
const testSuite = goog.require('goog.testing.testSuite');
|
|
||||||
const validation = goog.require('exoplayer.cast.validation');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a sample drm media for validation tests.
|
|
||||||
*
|
|
||||||
* @return {!Object} A dummy media item with a drm scheme.
|
|
||||||
*/
|
|
||||||
const createDrmMedia = function() {
|
|
||||||
return {
|
|
||||||
uuid: 'string',
|
|
||||||
media: {
|
|
||||||
uri: 'string',
|
|
||||||
},
|
|
||||||
mimeType: 'application/dash+xml',
|
|
||||||
drmSchemes: [
|
|
||||||
{
|
|
||||||
uuid: 'string',
|
|
||||||
licenseServer: {
|
|
||||||
uri: 'string',
|
|
||||||
requestHeaders: {
|
|
||||||
'string': 'string',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
testSuite({
|
|
||||||
|
|
||||||
/** Tests minimal valid media item. */
|
|
||||||
testValidateMediaItem_minimal() {
|
|
||||||
const mediaItem = {
|
|
||||||
uuid: 'string',
|
|
||||||
media: {
|
|
||||||
uri: 'string',
|
|
||||||
},
|
|
||||||
mimeType: 'application/dash+xml',
|
|
||||||
};
|
|
||||||
assertTrue(validation.validateMediaItem(mediaItem));
|
|
||||||
|
|
||||||
const uuid = mediaItem.uuid;
|
|
||||||
delete mediaItem.uuid;
|
|
||||||
assertFalse(validation.validateMediaItem(mediaItem));
|
|
||||||
mediaItem.uuid = uuid;
|
|
||||||
assertTrue(validation.validateMediaItem(mediaItem));
|
|
||||||
|
|
||||||
const mimeType = mediaItem.mimeType;
|
|
||||||
delete mediaItem.mimeType;
|
|
||||||
assertFalse(validation.validateMediaItem(mediaItem));
|
|
||||||
mediaItem.mimeType = mimeType;
|
|
||||||
assertTrue(validation.validateMediaItem(mediaItem));
|
|
||||||
|
|
||||||
const media = mediaItem.media;
|
|
||||||
delete mediaItem.media;
|
|
||||||
assertFalse(validation.validateMediaItem(mediaItem));
|
|
||||||
mediaItem.media = media;
|
|
||||||
assertTrue(validation.validateMediaItem(mediaItem));
|
|
||||||
|
|
||||||
const uri = mediaItem.media.uri;
|
|
||||||
delete mediaItem.media.uri;
|
|
||||||
assertFalse(validation.validateMediaItem(mediaItem));
|
|
||||||
mediaItem.media.uri = uri;
|
|
||||||
assertTrue(validation.validateMediaItem(mediaItem));
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests media item drm property validation. */
|
|
||||||
testValidateMediaItem_drmSchemes() {
|
|
||||||
const mediaItem = createDrmMedia();
|
|
||||||
assertTrue(validation.validateMediaItem(mediaItem));
|
|
||||||
|
|
||||||
const uuid = mediaItem.drmSchemes[0].uuid;
|
|
||||||
delete mediaItem.drmSchemes[0].uuid;
|
|
||||||
assertFalse(validation.validateMediaItem(mediaItem));
|
|
||||||
mediaItem.drmSchemes[0].uuid = uuid;
|
|
||||||
assertTrue(validation.validateMediaItem(mediaItem));
|
|
||||||
|
|
||||||
const licenseServer = mediaItem.drmSchemes[0].licenseServer;
|
|
||||||
delete mediaItem.drmSchemes[0].licenseServer;
|
|
||||||
assertFalse(validation.validateMediaItem(mediaItem));
|
|
||||||
mediaItem.drmSchemes[0].licenseServer = licenseServer;
|
|
||||||
assertTrue(validation.validateMediaItem(mediaItem));
|
|
||||||
|
|
||||||
const uri = mediaItem.drmSchemes[0].licenseServer.uri;
|
|
||||||
delete mediaItem.drmSchemes[0].licenseServer.uri;
|
|
||||||
assertFalse(validation.validateMediaItem(mediaItem));
|
|
||||||
mediaItem.drmSchemes[0].licenseServer.uri = uri;
|
|
||||||
assertTrue(validation.validateMediaItem(mediaItem));
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests validation of startPositionUs and endPositionUs. */
|
|
||||||
testValidateMediaItem_endAndStartPositionUs() {
|
|
||||||
const mediaItem = createDrmMedia();
|
|
||||||
|
|
||||||
mediaItem.endPositionUs = 0;
|
|
||||||
mediaItem.startPositionUs = 120 * 1000;
|
|
||||||
assertTrue(validation.validateMediaItem(mediaItem));
|
|
||||||
|
|
||||||
mediaItem.endPositionUs = '0';
|
|
||||||
assertFalse(validation.validateMediaItem(mediaItem));
|
|
||||||
|
|
||||||
mediaItem.endPositionUs = 0;
|
|
||||||
assertTrue(validation.validateMediaItem(mediaItem));
|
|
||||||
|
|
||||||
mediaItem.startPositionUs = true;
|
|
||||||
assertFalse(validation.validateMediaItem(mediaItem));
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests validation of the title. */
|
|
||||||
testValidateMediaItem_title() {
|
|
||||||
const mediaItem = createDrmMedia();
|
|
||||||
|
|
||||||
mediaItem.title = '0';
|
|
||||||
assertTrue(validation.validateMediaItem(mediaItem));
|
|
||||||
|
|
||||||
mediaItem.title = 0;
|
|
||||||
assertFalse(validation.validateMediaItem(mediaItem));
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests validation of the description. */
|
|
||||||
testValidateMediaItem_description() {
|
|
||||||
const mediaItem = createDrmMedia();
|
|
||||||
|
|
||||||
mediaItem.description = '0';
|
|
||||||
assertTrue(validation.validateMediaItem(mediaItem));
|
|
||||||
|
|
||||||
mediaItem.description = 0;
|
|
||||||
assertFalse(validation.validateMediaItem(mediaItem));
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests validating property of type string. */
|
|
||||||
testValidateProperty_string() {
|
|
||||||
const obj = {
|
|
||||||
field: 'string',
|
|
||||||
};
|
|
||||||
assertTrue(validation.validateProperty(obj, 'field', 'string'));
|
|
||||||
assertTrue(validation.validateProperty(obj, 'field', '?string'));
|
|
||||||
|
|
||||||
obj.field = 0;
|
|
||||||
assertFalse(validation.validateProperty(obj, 'field', 'string'));
|
|
||||||
assertFalse(validation.validateProperty(obj, 'field', '?string'));
|
|
||||||
|
|
||||||
obj.field = true;
|
|
||||||
assertFalse(validation.validateProperty(obj, 'field', 'string'));
|
|
||||||
assertFalse(validation.validateProperty(obj, 'field', '?string'));
|
|
||||||
|
|
||||||
obj.field = {};
|
|
||||||
assertFalse(validation.validateProperty(obj, 'field', 'string'));
|
|
||||||
assertFalse(validation.validateProperty(obj, 'field', '?string'));
|
|
||||||
|
|
||||||
delete obj.field;
|
|
||||||
assertFalse(validation.validateProperty(obj, 'field', 'string'));
|
|
||||||
assertTrue(validation.validateProperty(obj, 'field', '?string'));
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests validating property of type number. */
|
|
||||||
testValidateProperty_number() {
|
|
||||||
const obj = {
|
|
||||||
field: 0,
|
|
||||||
};
|
|
||||||
assertTrue(validation.validateProperty(obj, 'field', 'number'));
|
|
||||||
assertTrue(validation.validateProperty(obj, 'field', '?number'));
|
|
||||||
|
|
||||||
obj.field = '0';
|
|
||||||
assertFalse(validation.validateProperty(obj, 'field', 'number'));
|
|
||||||
assertFalse(validation.validateProperty(obj, 'field', '?number'));
|
|
||||||
|
|
||||||
obj.field = true;
|
|
||||||
assertFalse(validation.validateProperty(obj, 'field', 'number'));
|
|
||||||
assertFalse(validation.validateProperty(obj, 'field', '?number'));
|
|
||||||
|
|
||||||
obj.field = {};
|
|
||||||
assertFalse(validation.validateProperty(obj, 'field', 'number'));
|
|
||||||
assertFalse(validation.validateProperty(obj, 'field', '?number'));
|
|
||||||
|
|
||||||
delete obj.field;
|
|
||||||
assertFalse(validation.validateProperty(obj, 'field', 'number'));
|
|
||||||
assertTrue(validation.validateProperty(obj, 'field', '?number'));
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests validating property of type boolean. */
|
|
||||||
testValidateProperty_boolean() {
|
|
||||||
const obj = {
|
|
||||||
field: true,
|
|
||||||
};
|
|
||||||
assertTrue(validation.validateProperty(obj, 'field', 'boolean'));
|
|
||||||
assertTrue(validation.validateProperty(obj, 'field', '?boolean'));
|
|
||||||
|
|
||||||
obj.field = '0';
|
|
||||||
assertFalse(validation.validateProperty(obj, 'field', 'boolean'));
|
|
||||||
assertFalse(validation.validateProperty(obj, 'field', '?boolean'));
|
|
||||||
|
|
||||||
obj.field = 1000;
|
|
||||||
assertFalse(validation.validateProperty(obj, 'field', 'boolean'));
|
|
||||||
assertFalse(validation.validateProperty(obj, 'field', '?boolean'));
|
|
||||||
|
|
||||||
obj.field = [true];
|
|
||||||
assertFalse(validation.validateProperty(obj, 'field', 'boolean'));
|
|
||||||
assertFalse(validation.validateProperty(obj, 'field', '?boolean'));
|
|
||||||
|
|
||||||
delete obj.field;
|
|
||||||
assertFalse(validation.validateProperty(obj, 'field', 'boolean'));
|
|
||||||
assertTrue(validation.validateProperty(obj, 'field', '?boolean'));
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests validating property of type array. */
|
|
||||||
testValidateProperty_array() {
|
|
||||||
const obj = {
|
|
||||||
field: [],
|
|
||||||
};
|
|
||||||
assertTrue(validation.validateProperty(obj, 'field', 'Array'));
|
|
||||||
assertTrue(validation.validateProperty(obj, 'field', '?Array'));
|
|
||||||
|
|
||||||
obj.field = '0';
|
|
||||||
assertFalse(validation.validateProperty(obj, 'field', 'Array'));
|
|
||||||
assertFalse(validation.validateProperty(obj, 'field', '?Array'));
|
|
||||||
|
|
||||||
obj.field = 1000;
|
|
||||||
assertFalse(validation.validateProperty(obj, 'field', 'Array'));
|
|
||||||
assertFalse(validation.validateProperty(obj, 'field', '?Array'));
|
|
||||||
|
|
||||||
obj.field = true;
|
|
||||||
assertFalse(validation.validateProperty(obj, 'field', 'Array'));
|
|
||||||
assertFalse(validation.validateProperty(obj, 'field', '?Array'));
|
|
||||||
|
|
||||||
delete obj.field;
|
|
||||||
assertFalse(validation.validateProperty(obj, 'field', 'Array'));
|
|
||||||
assertTrue(validation.validateProperty(obj, 'field', '?Array'));
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Tests validating properties of type RepeatMode */
|
|
||||||
testValidateProperty_repeatMode() {
|
|
||||||
const obj = {
|
|
||||||
off: 'OFF',
|
|
||||||
one: 'ONE',
|
|
||||||
all: 'ALL',
|
|
||||||
invalid: 'invalid',
|
|
||||||
};
|
|
||||||
assertTrue(validation.validateProperty(obj, 'off', 'RepeatMode'));
|
|
||||||
assertTrue(validation.validateProperty(obj, 'one', 'RepeatMode'));
|
|
||||||
assertTrue(validation.validateProperty(obj, 'all', 'RepeatMode'));
|
|
||||||
assertFalse(validation.validateProperty(obj, 'invalid', 'RepeatMode'));
|
|
||||||
},
|
|
||||||
});
|
|
@ -1,437 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2017 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.exoplayer2.castdemo;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.net.Uri;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import android.view.KeyEvent;
|
|
||||||
import android.view.View;
|
|
||||||
import com.google.android.exoplayer2.C;
|
|
||||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
|
||||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
|
||||||
import com.google.android.exoplayer2.Player;
|
|
||||||
import com.google.android.exoplayer2.Player.DiscontinuityReason;
|
|
||||||
import com.google.android.exoplayer2.Player.EventListener;
|
|
||||||
import com.google.android.exoplayer2.Player.TimelineChangeReason;
|
|
||||||
import com.google.android.exoplayer2.RenderersFactory;
|
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
|
||||||
import com.google.android.exoplayer2.Timeline;
|
|
||||||
import com.google.android.exoplayer2.Timeline.Period;
|
|
||||||
import com.google.android.exoplayer2.ext.cast.CastPlayer;
|
|
||||||
import com.google.android.exoplayer2.ext.cast.MediaItem;
|
|
||||||
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener;
|
|
||||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
|
||||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
|
||||||
import com.google.android.exoplayer2.ui.PlayerControlView;
|
|
||||||
import com.google.android.exoplayer2.ui.PlayerView;
|
|
||||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
|
||||||
import com.google.android.gms.cast.MediaInfo;
|
|
||||||
import com.google.android.gms.cast.MediaMetadata;
|
|
||||||
import com.google.android.gms.cast.MediaQueueItem;
|
|
||||||
import com.google.android.gms.cast.framework.CastContext;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
/** Manages players and an internal media queue for the ExoPlayer/Cast demo app. */
|
|
||||||
/* package */ class DefaultReceiverPlayerManager
|
|
||||||
implements PlayerManager, EventListener, SessionAvailabilityListener {
|
|
||||||
|
|
||||||
private static final String USER_AGENT = "ExoCastDemoPlayer";
|
|
||||||
private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY =
|
|
||||||
new DefaultHttpDataSourceFactory(USER_AGENT);
|
|
||||||
|
|
||||||
private final PlayerView localPlayerView;
|
|
||||||
private final PlayerControlView castControlView;
|
|
||||||
private final SimpleExoPlayer exoPlayer;
|
|
||||||
private final CastPlayer castPlayer;
|
|
||||||
private final ArrayList<MediaItem> mediaQueue;
|
|
||||||
private final Listener listener;
|
|
||||||
private final ConcatenatingMediaSource concatenatingMediaSource;
|
|
||||||
|
|
||||||
private int currentItemIndex;
|
|
||||||
private Player currentPlayer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new manager for {@link SimpleExoPlayer} and {@link CastPlayer}.
|
|
||||||
*
|
|
||||||
* @param listener A {@link Listener} for queue position changes.
|
|
||||||
* @param localPlayerView The {@link PlayerView} for local playback.
|
|
||||||
* @param castControlView The {@link PlayerControlView} to control remote playback.
|
|
||||||
* @param context A {@link Context}.
|
|
||||||
* @param castContext The {@link CastContext}.
|
|
||||||
*/
|
|
||||||
public DefaultReceiverPlayerManager(
|
|
||||||
Listener listener,
|
|
||||||
PlayerView localPlayerView,
|
|
||||||
PlayerControlView castControlView,
|
|
||||||
Context context,
|
|
||||||
CastContext castContext) {
|
|
||||||
this.listener = listener;
|
|
||||||
this.localPlayerView = localPlayerView;
|
|
||||||
this.castControlView = castControlView;
|
|
||||||
mediaQueue = new ArrayList<>();
|
|
||||||
currentItemIndex = C.INDEX_UNSET;
|
|
||||||
concatenatingMediaSource = new ConcatenatingMediaSource();
|
|
||||||
|
|
||||||
DefaultTrackSelector trackSelector = new DefaultTrackSelector();
|
|
||||||
RenderersFactory renderersFactory = new DefaultRenderersFactory(context);
|
|
||||||
exoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector);
|
|
||||||
exoPlayer.addListener(this);
|
|
||||||
localPlayerView.setPlayer(exoPlayer);
|
|
||||||
|
|
||||||
castPlayer = new CastPlayer(castContext);
|
|
||||||
castPlayer.addListener(this);
|
|
||||||
castPlayer.setSessionAvailabilityListener(this);
|
|
||||||
castControlView.setPlayer(castPlayer);
|
|
||||||
|
|
||||||
setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Queue manipulation methods.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Plays a specified queue item in the current player.
|
|
||||||
*
|
|
||||||
* @param itemIndex The index of the item to play.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void selectQueueItem(int itemIndex) {
|
|
||||||
setCurrentItem(itemIndex, C.TIME_UNSET, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the index of the currently played item. */
|
|
||||||
@Override
|
|
||||||
public int getCurrentItemIndex() {
|
|
||||||
return currentItemIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Appends {@code item} to the media queue.
|
|
||||||
*
|
|
||||||
* @param item The {@link MediaItem} to append.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void addItem(MediaItem item) {
|
|
||||||
mediaQueue.add(item);
|
|
||||||
concatenatingMediaSource.addMediaSource(buildMediaSource(item));
|
|
||||||
if (currentPlayer == castPlayer) {
|
|
||||||
castPlayer.addItems(buildMediaQueueItem(item));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the size of the media queue. */
|
|
||||||
@Override
|
|
||||||
public int getMediaQueueSize() {
|
|
||||||
return mediaQueue.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the item at the given index in the media queue.
|
|
||||||
*
|
|
||||||
* @param position The index of the item.
|
|
||||||
* @return The item at the given index in the media queue.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public MediaItem getItem(int position) {
|
|
||||||
return mediaQueue.get(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the item at the given index from the media queue.
|
|
||||||
*
|
|
||||||
* @param item The item to remove.
|
|
||||||
* @return Whether the removal was successful.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public boolean removeItem(MediaItem item) {
|
|
||||||
int itemIndex = mediaQueue.indexOf(item);
|
|
||||||
if (itemIndex == -1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
concatenatingMediaSource.removeMediaSource(itemIndex);
|
|
||||||
if (currentPlayer == castPlayer) {
|
|
||||||
if (castPlayer.getPlaybackState() != Player.STATE_IDLE) {
|
|
||||||
Timeline castTimeline = castPlayer.getCurrentTimeline();
|
|
||||||
if (castTimeline.getPeriodCount() <= itemIndex) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
castPlayer.removeItem((int) castTimeline.getPeriod(itemIndex, new Period()).id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mediaQueue.remove(itemIndex);
|
|
||||||
if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) {
|
|
||||||
maybeSetCurrentItemAndNotify(C.INDEX_UNSET);
|
|
||||||
} else if (itemIndex < currentItemIndex) {
|
|
||||||
maybeSetCurrentItemAndNotify(currentItemIndex - 1);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Moves an item within the queue.
|
|
||||||
*
|
|
||||||
* @param item The item to move.
|
|
||||||
* @param toIndex The target index of the item in the queue.
|
|
||||||
* @return Whether the item move was successful.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public boolean moveItem(MediaItem item, int toIndex) {
|
|
||||||
int fromIndex = mediaQueue.indexOf(item);
|
|
||||||
if (fromIndex == -1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// Player update.
|
|
||||||
concatenatingMediaSource.moveMediaSource(fromIndex, toIndex);
|
|
||||||
if (currentPlayer == castPlayer && castPlayer.getPlaybackState() != Player.STATE_IDLE) {
|
|
||||||
Timeline castTimeline = castPlayer.getCurrentTimeline();
|
|
||||||
int periodCount = castTimeline.getPeriodCount();
|
|
||||||
if (periodCount <= fromIndex || periodCount <= toIndex) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
int elementId = (int) castTimeline.getPeriod(fromIndex, new Period()).id;
|
|
||||||
castPlayer.moveItem(elementId, toIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
mediaQueue.add(toIndex, mediaQueue.remove(fromIndex));
|
|
||||||
|
|
||||||
// Index update.
|
|
||||||
if (fromIndex == currentItemIndex) {
|
|
||||||
maybeSetCurrentItemAndNotify(toIndex);
|
|
||||||
} else if (fromIndex < currentItemIndex && toIndex >= currentItemIndex) {
|
|
||||||
maybeSetCurrentItemAndNotify(currentItemIndex - 1);
|
|
||||||
} else if (fromIndex > currentItemIndex && toIndex <= currentItemIndex) {
|
|
||||||
maybeSetCurrentItemAndNotify(currentItemIndex + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispatches a given {@link KeyEvent} to the corresponding view of the current player.
|
|
||||||
*
|
|
||||||
* @param event The {@link KeyEvent}.
|
|
||||||
* @return Whether the event was handled by the target view.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
|
||||||
if (currentPlayer == exoPlayer) {
|
|
||||||
return localPlayerView.dispatchKeyEvent(event);
|
|
||||||
} else /* currentPlayer == castPlayer */ {
|
|
||||||
return castControlView.dispatchKeyEvent(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Releases the manager and the players that it holds. */
|
|
||||||
@Override
|
|
||||||
public void release() {
|
|
||||||
currentItemIndex = C.INDEX_UNSET;
|
|
||||||
mediaQueue.clear();
|
|
||||||
concatenatingMediaSource.clear();
|
|
||||||
castPlayer.setSessionAvailabilityListener(null);
|
|
||||||
castPlayer.release();
|
|
||||||
localPlayerView.setPlayer(null);
|
|
||||||
exoPlayer.release();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Player.EventListener implementation.
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
|
|
||||||
updateCurrentItemIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
|
|
||||||
updateCurrentItemIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTimelineChanged(
|
|
||||||
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
|
|
||||||
updateCurrentItemIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
// CastPlayer.SessionAvailabilityListener implementation.
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCastSessionAvailable() {
|
|
||||||
setCurrentPlayer(castPlayer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCastSessionUnavailable() {
|
|
||||||
setCurrentPlayer(exoPlayer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Internal methods.
|
|
||||||
|
|
||||||
private void updateCurrentItemIndex() {
|
|
||||||
int playbackState = currentPlayer.getPlaybackState();
|
|
||||||
maybeSetCurrentItemAndNotify(
|
|
||||||
playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED
|
|
||||||
? currentPlayer.getCurrentWindowIndex()
|
|
||||||
: C.INDEX_UNSET);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setCurrentPlayer(Player currentPlayer) {
|
|
||||||
if (this.currentPlayer == currentPlayer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// View management.
|
|
||||||
if (currentPlayer == exoPlayer) {
|
|
||||||
localPlayerView.setVisibility(View.VISIBLE);
|
|
||||||
castControlView.hide();
|
|
||||||
} else /* currentPlayer == castPlayer */ {
|
|
||||||
localPlayerView.setVisibility(View.GONE);
|
|
||||||
castControlView.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Player state management.
|
|
||||||
long playbackPositionMs = C.TIME_UNSET;
|
|
||||||
int windowIndex = C.INDEX_UNSET;
|
|
||||||
boolean playWhenReady = false;
|
|
||||||
if (this.currentPlayer != null) {
|
|
||||||
int playbackState = this.currentPlayer.getPlaybackState();
|
|
||||||
if (playbackState != Player.STATE_ENDED) {
|
|
||||||
playbackPositionMs = this.currentPlayer.getCurrentPosition();
|
|
||||||
playWhenReady = this.currentPlayer.getPlayWhenReady();
|
|
||||||
windowIndex = this.currentPlayer.getCurrentWindowIndex();
|
|
||||||
if (windowIndex != currentItemIndex) {
|
|
||||||
playbackPositionMs = C.TIME_UNSET;
|
|
||||||
windowIndex = currentItemIndex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.currentPlayer.stop(true);
|
|
||||||
} else {
|
|
||||||
// This is the initial setup. No need to save any state.
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentPlayer = currentPlayer;
|
|
||||||
|
|
||||||
// Media queue management.
|
|
||||||
if (currentPlayer == exoPlayer) {
|
|
||||||
exoPlayer.prepare(concatenatingMediaSource);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Playback transition.
|
|
||||||
if (windowIndex != C.INDEX_UNSET) {
|
|
||||||
setCurrentItem(windowIndex, playbackPositionMs, playWhenReady);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts playback of the item at the given position.
|
|
||||||
*
|
|
||||||
* @param itemIndex The index of the item to play.
|
|
||||||
* @param positionMs The position at which playback should start.
|
|
||||||
* @param playWhenReady Whether the player should proceed when ready to do so.
|
|
||||||
*/
|
|
||||||
private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) {
|
|
||||||
maybeSetCurrentItemAndNotify(itemIndex);
|
|
||||||
if (currentPlayer == castPlayer && castPlayer.getCurrentTimeline().isEmpty()) {
|
|
||||||
MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()];
|
|
||||||
for (int i = 0; i < items.length; i++) {
|
|
||||||
items[i] = buildMediaQueueItem(mediaQueue.get(i));
|
|
||||||
}
|
|
||||||
castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF);
|
|
||||||
} else {
|
|
||||||
currentPlayer.seekTo(itemIndex, positionMs);
|
|
||||||
currentPlayer.setPlayWhenReady(playWhenReady);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void maybeSetCurrentItemAndNotify(int currentItemIndex) {
|
|
||||||
if (this.currentItemIndex != currentItemIndex) {
|
|
||||||
int oldIndex = this.currentItemIndex;
|
|
||||||
this.currentItemIndex = currentItemIndex;
|
|
||||||
listener.onQueuePositionChanged(oldIndex, currentItemIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MediaSource buildMediaSource(MediaItem item) {
|
|
||||||
Uri uri = item.media.uri;
|
|
||||||
switch (item.mimeType) {
|
|
||||||
case DemoUtil.MIME_TYPE_SS:
|
|
||||||
return new SsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
|
||||||
case DemoUtil.MIME_TYPE_DASH:
|
|
||||||
return new DashMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
|
||||||
case DemoUtil.MIME_TYPE_HLS:
|
|
||||||
return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
|
||||||
case DemoUtil.MIME_TYPE_VIDEO_MP4:
|
|
||||||
return new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
|
||||||
default:
|
|
||||||
{
|
|
||||||
throw new IllegalStateException("Unsupported type: " + item.mimeType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MediaQueueItem buildMediaQueueItem(MediaItem item) {
|
|
||||||
MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
|
|
||||||
movieMetadata.putString(MediaMetadata.KEY_TITLE, item.title);
|
|
||||||
MediaInfo.Builder mediaInfoBuilder =
|
|
||||||
new MediaInfo.Builder(item.media.uri.toString())
|
|
||||||
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
|
|
||||||
.setContentType(item.mimeType)
|
|
||||||
.setMetadata(movieMetadata);
|
|
||||||
if (!item.drmSchemes.isEmpty()) {
|
|
||||||
MediaItem.DrmScheme scheme = item.drmSchemes.get(0);
|
|
||||||
try {
|
|
||||||
// This configuration is only intended for testing and should *not* be used in production
|
|
||||||
// environments. See comment in the Cast Demo app's options provider.
|
|
||||||
JSONObject drmConfiguration = getDrmConfigurationJson(scheme);
|
|
||||||
if (drmConfiguration != null) {
|
|
||||||
mediaInfoBuilder.setCustomData(drmConfiguration);
|
|
||||||
}
|
|
||||||
} catch (JSONException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new MediaQueueItem.Builder(mediaInfoBuilder.build()).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private static JSONObject getDrmConfigurationJson(MediaItem.DrmScheme scheme)
|
|
||||||
throws JSONException {
|
|
||||||
String drmScheme;
|
|
||||||
if (C.WIDEVINE_UUID.equals(scheme.uuid)) {
|
|
||||||
drmScheme = "widevine";
|
|
||||||
} else if (C.PLAYREADY_UUID.equals(scheme.uuid)) {
|
|
||||||
drmScheme = "playready";
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
MediaItem.UriBundle licenseServer = Assertions.checkNotNull(scheme.licenseServer);
|
|
||||||
JSONObject exoplayerConfig =
|
|
||||||
new JSONObject().put("withCredentials", false).put("protectionSystem", drmScheme);
|
|
||||||
if (!licenseServer.uri.equals(Uri.EMPTY)) {
|
|
||||||
exoplayerConfig.put("licenseUrl", licenseServer.uri.toString());
|
|
||||||
}
|
|
||||||
if (!licenseServer.requestHeaders.isEmpty()) {
|
|
||||||
exoplayerConfig.put("headers", new JSONObject(licenseServer.requestHeaders));
|
|
||||||
}
|
|
||||||
return new JSONObject().put("exoPlayerConfig", exoplayerConfig);
|
|
||||||
}
|
|
||||||
}
|
|
@ -98,6 +98,11 @@ import java.util.UUID;
|
|||||||
"https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd",
|
"https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd",
|
||||||
"Clear DASH: Tears",
|
"Clear DASH: Tears",
|
||||||
MIME_TYPE_DASH));
|
MIME_TYPE_DASH));
|
||||||
|
samples.add(
|
||||||
|
new Sample(
|
||||||
|
"https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8",
|
||||||
|
"Clear HLS: Angel one",
|
||||||
|
MIME_TYPE_HLS));
|
||||||
samples.add(
|
samples.add(
|
||||||
new Sample(
|
new Sample(
|
||||||
"https://html5demos.com/assets/dizzy.mp4", "Clear MP4: Dizzy", MIME_TYPE_VIDEO_MP4));
|
"https://html5demos.com/assets/dizzy.mp4", "Clear MP4: Dizzy", MIME_TYPE_VIDEO_MP4));
|
||||||
|
@ -1,421 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.exoplayer2.castdemo;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.net.Uri;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import android.view.KeyEvent;
|
|
||||||
import android.view.View;
|
|
||||||
import com.google.android.exoplayer2.C;
|
|
||||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
|
||||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
|
||||||
import com.google.android.exoplayer2.Player;
|
|
||||||
import com.google.android.exoplayer2.Player.DiscontinuityReason;
|
|
||||||
import com.google.android.exoplayer2.Player.EventListener;
|
|
||||||
import com.google.android.exoplayer2.Player.TimelineChangeReason;
|
|
||||||
import com.google.android.exoplayer2.RenderersFactory;
|
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
|
||||||
import com.google.android.exoplayer2.Timeline;
|
|
||||||
import com.google.android.exoplayer2.ext.cast.DefaultCastSessionManager;
|
|
||||||
import com.google.android.exoplayer2.ext.cast.ExoCastPlayer;
|
|
||||||
import com.google.android.exoplayer2.ext.cast.MediaItem;
|
|
||||||
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener;
|
|
||||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
|
||||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
|
||||||
import com.google.android.exoplayer2.ui.PlayerControlView;
|
|
||||||
import com.google.android.exoplayer2.ui.PlayerView;
|
|
||||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
|
||||||
import com.google.android.exoplayer2.util.Log;
|
|
||||||
import com.google.android.gms.cast.framework.CastContext;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
|
|
||||||
/** Manages players and an internal media queue for the Cast demo app. */
|
|
||||||
/* package */ class ExoCastPlayerManager
|
|
||||||
implements PlayerManager, EventListener, SessionAvailabilityListener {
|
|
||||||
|
|
||||||
private static final String TAG = "ExoCastPlayerManager";
|
|
||||||
private static final String USER_AGENT = "ExoCastDemoPlayer";
|
|
||||||
private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY =
|
|
||||||
new DefaultHttpDataSourceFactory(USER_AGENT);
|
|
||||||
|
|
||||||
private final PlayerView localPlayerView;
|
|
||||||
private final PlayerControlView castControlView;
|
|
||||||
private final SimpleExoPlayer exoPlayer;
|
|
||||||
private final ExoCastPlayer exoCastPlayer;
|
|
||||||
private final ArrayList<MediaItem> mediaQueue;
|
|
||||||
private final Listener listener;
|
|
||||||
private final ConcatenatingMediaSource concatenatingMediaSource;
|
|
||||||
|
|
||||||
private int currentItemIndex;
|
|
||||||
private Player currentPlayer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new manager for {@link SimpleExoPlayer} and {@link ExoCastPlayer}.
|
|
||||||
*
|
|
||||||
* @param listener A {@link Listener}.
|
|
||||||
* @param localPlayerView The {@link PlayerView} for local playback.
|
|
||||||
* @param castControlView The {@link PlayerControlView} to control remote playback.
|
|
||||||
* @param context A {@link Context}.
|
|
||||||
* @param castContext The {@link CastContext}.
|
|
||||||
*/
|
|
||||||
public ExoCastPlayerManager(
|
|
||||||
Listener listener,
|
|
||||||
PlayerView localPlayerView,
|
|
||||||
PlayerControlView castControlView,
|
|
||||||
Context context,
|
|
||||||
CastContext castContext) {
|
|
||||||
this.listener = listener;
|
|
||||||
this.localPlayerView = localPlayerView;
|
|
||||||
this.castControlView = castControlView;
|
|
||||||
mediaQueue = new ArrayList<>();
|
|
||||||
currentItemIndex = C.INDEX_UNSET;
|
|
||||||
concatenatingMediaSource = new ConcatenatingMediaSource();
|
|
||||||
|
|
||||||
DefaultTrackSelector trackSelector = new DefaultTrackSelector();
|
|
||||||
RenderersFactory renderersFactory = new DefaultRenderersFactory(context);
|
|
||||||
exoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector);
|
|
||||||
exoPlayer.addListener(this);
|
|
||||||
localPlayerView.setPlayer(exoPlayer);
|
|
||||||
|
|
||||||
exoCastPlayer =
|
|
||||||
new ExoCastPlayer(
|
|
||||||
sessionManagerListener ->
|
|
||||||
new DefaultCastSessionManager(castContext, sessionManagerListener));
|
|
||||||
exoCastPlayer.addListener(this);
|
|
||||||
exoCastPlayer.setSessionAvailabilityListener(this);
|
|
||||||
castControlView.setPlayer(exoCastPlayer);
|
|
||||||
|
|
||||||
setCurrentPlayer(exoCastPlayer.isCastSessionAvailable() ? exoCastPlayer : exoPlayer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Queue manipulation methods.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Plays a specified queue item in the current player.
|
|
||||||
*
|
|
||||||
* @param itemIndex The index of the item to play.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void selectQueueItem(int itemIndex) {
|
|
||||||
setCurrentItem(itemIndex, C.TIME_UNSET, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the index of the currently played item. */
|
|
||||||
@Override
|
|
||||||
public int getCurrentItemIndex() {
|
|
||||||
return currentItemIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Appends {@code item} to the media queue.
|
|
||||||
*
|
|
||||||
* @param item The {@link MediaItem} to append.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void addItem(MediaItem item) {
|
|
||||||
mediaQueue.add(item);
|
|
||||||
concatenatingMediaSource.addMediaSource(buildMediaSource(item));
|
|
||||||
if (currentPlayer == exoCastPlayer) {
|
|
||||||
exoCastPlayer.addItemsToQueue(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the size of the media queue. */
|
|
||||||
@Override
|
|
||||||
public int getMediaQueueSize() {
|
|
||||||
return mediaQueue.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the item at the given index in the media queue.
|
|
||||||
*
|
|
||||||
* @param position The index of the item.
|
|
||||||
* @return The item at the given index in the media queue.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public MediaItem getItem(int position) {
|
|
||||||
return mediaQueue.get(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the item at the given index from the media queue.
|
|
||||||
*
|
|
||||||
* @param item The item to remove.
|
|
||||||
* @return Whether the removal was successful.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public boolean removeItem(MediaItem item) {
|
|
||||||
int itemIndex = mediaQueue.indexOf(item);
|
|
||||||
if (itemIndex == -1) {
|
|
||||||
// This may happen if another sender app removes items while this sender app is in "swiping
|
|
||||||
// an item" state.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
concatenatingMediaSource.removeMediaSource(itemIndex);
|
|
||||||
mediaQueue.remove(itemIndex);
|
|
||||||
if (currentPlayer == exoCastPlayer) {
|
|
||||||
exoCastPlayer.removeItemFromQueue(itemIndex);
|
|
||||||
}
|
|
||||||
if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) {
|
|
||||||
maybeSetCurrentItemAndNotify(C.INDEX_UNSET);
|
|
||||||
} else if (itemIndex < currentItemIndex) {
|
|
||||||
maybeSetCurrentItemAndNotify(currentItemIndex - 1);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Moves an item within the queue.
|
|
||||||
*
|
|
||||||
* @param item The item to move. This method does nothing if {@code item} is not contained in the
|
|
||||||
* queue.
|
|
||||||
* @param toIndex The target index of the item in the queue. If {@code toIndex} exceeds the last
|
|
||||||
* position in the queue, {@code toIndex} is clamped to match the largest possible value.
|
|
||||||
* @return True if {@code item} was contained in the queue, and {@code toIndex} was a valid
|
|
||||||
* position. False otherwise.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public boolean moveItem(MediaItem item, int toIndex) {
|
|
||||||
int indexOfItem = mediaQueue.indexOf(item);
|
|
||||||
if (indexOfItem == -1) {
|
|
||||||
// This may happen if another sender app removes items while this sender app is in "dragging
|
|
||||||
// an item" state.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
int clampedToIndex = Math.min(toIndex, mediaQueue.size() - 1);
|
|
||||||
mediaQueue.add(clampedToIndex, mediaQueue.remove(indexOfItem));
|
|
||||||
concatenatingMediaSource.moveMediaSource(indexOfItem, clampedToIndex);
|
|
||||||
if (currentPlayer == exoCastPlayer) {
|
|
||||||
exoCastPlayer.moveItemInQueue(indexOfItem, clampedToIndex);
|
|
||||||
}
|
|
||||||
// Index update.
|
|
||||||
maybeSetCurrentItemAndNotify(currentPlayer.getCurrentWindowIndex());
|
|
||||||
return clampedToIndex == toIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
|
||||||
if (currentPlayer == exoPlayer) {
|
|
||||||
return localPlayerView.dispatchKeyEvent(event);
|
|
||||||
} else /* currentPlayer == exoCastPlayer */ {
|
|
||||||
return castControlView.dispatchKeyEvent(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Releases the manager and the players that it holds. */
|
|
||||||
@Override
|
|
||||||
public void release() {
|
|
||||||
currentItemIndex = C.INDEX_UNSET;
|
|
||||||
mediaQueue.clear();
|
|
||||||
concatenatingMediaSource.clear();
|
|
||||||
exoCastPlayer.setSessionAvailabilityListener(null);
|
|
||||||
exoCastPlayer.release();
|
|
||||||
localPlayerView.setPlayer(null);
|
|
||||||
exoPlayer.release();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Player.EventListener implementation.
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
|
|
||||||
updateCurrentItemIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
|
|
||||||
updateCurrentItemIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTimelineChanged(
|
|
||||||
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
|
|
||||||
if (currentPlayer == exoCastPlayer && reason != Player.TIMELINE_CHANGE_REASON_RESET) {
|
|
||||||
maybeUpdateLocalQueueWithRemoteQueueAndNotify();
|
|
||||||
}
|
|
||||||
updateCurrentItemIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPlayerError(ExoPlaybackException error) {
|
|
||||||
Log.e(TAG, "The player encountered an error.", error);
|
|
||||||
listener.onPlayerError();
|
|
||||||
}
|
|
||||||
|
|
||||||
// CastPlayer.SessionAvailabilityListener implementation.
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCastSessionAvailable() {
|
|
||||||
setCurrentPlayer(exoCastPlayer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCastSessionUnavailable() {
|
|
||||||
setCurrentPlayer(exoPlayer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Internal methods.
|
|
||||||
|
|
||||||
private void maybeUpdateLocalQueueWithRemoteQueueAndNotify() {
|
|
||||||
Assertions.checkState(currentPlayer == exoCastPlayer);
|
|
||||||
boolean mediaQueuesMatch = mediaQueue.size() == exoCastPlayer.getQueueSize();
|
|
||||||
for (int i = 0; mediaQueuesMatch && i < mediaQueue.size(); i++) {
|
|
||||||
mediaQueuesMatch = mediaQueue.get(i).uuid.equals(exoCastPlayer.getQueueItem(i).uuid);
|
|
||||||
}
|
|
||||||
if (mediaQueuesMatch) {
|
|
||||||
// The media queues match. Do nothing.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mediaQueue.clear();
|
|
||||||
concatenatingMediaSource.clear();
|
|
||||||
for (int i = 0; i < exoCastPlayer.getQueueSize(); i++) {
|
|
||||||
MediaItem item = exoCastPlayer.getQueueItem(i);
|
|
||||||
mediaQueue.add(item);
|
|
||||||
concatenatingMediaSource.addMediaSource(buildMediaSource(item));
|
|
||||||
}
|
|
||||||
listener.onQueueContentsExternallyChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateCurrentItemIndex() {
|
|
||||||
int playbackState = currentPlayer.getPlaybackState();
|
|
||||||
maybeSetCurrentItemAndNotify(
|
|
||||||
playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED
|
|
||||||
? currentPlayer.getCurrentWindowIndex()
|
|
||||||
: C.INDEX_UNSET);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setCurrentPlayer(Player currentPlayer) {
|
|
||||||
if (this.currentPlayer == currentPlayer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// View management.
|
|
||||||
if (currentPlayer == exoPlayer) {
|
|
||||||
localPlayerView.setVisibility(View.VISIBLE);
|
|
||||||
castControlView.hide();
|
|
||||||
} else /* currentPlayer == exoCastPlayer */ {
|
|
||||||
localPlayerView.setVisibility(View.GONE);
|
|
||||||
castControlView.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Player state management.
|
|
||||||
long playbackPositionMs = C.TIME_UNSET;
|
|
||||||
int windowIndex = C.INDEX_UNSET;
|
|
||||||
boolean playWhenReady = false;
|
|
||||||
if (this.currentPlayer != null) {
|
|
||||||
int playbackState = this.currentPlayer.getPlaybackState();
|
|
||||||
if (playbackState != Player.STATE_ENDED) {
|
|
||||||
playbackPositionMs = this.currentPlayer.getCurrentPosition();
|
|
||||||
playWhenReady = this.currentPlayer.getPlayWhenReady();
|
|
||||||
windowIndex = this.currentPlayer.getCurrentWindowIndex();
|
|
||||||
if (windowIndex != currentItemIndex) {
|
|
||||||
playbackPositionMs = C.TIME_UNSET;
|
|
||||||
windowIndex = currentItemIndex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.currentPlayer.stop(true);
|
|
||||||
} else {
|
|
||||||
// This is the initial setup. No need to save any state.
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentPlayer = currentPlayer;
|
|
||||||
|
|
||||||
// Media queue management.
|
|
||||||
boolean shouldSeekInNewCurrentPlayer;
|
|
||||||
if (currentPlayer == exoPlayer) {
|
|
||||||
exoPlayer.prepare(concatenatingMediaSource);
|
|
||||||
shouldSeekInNewCurrentPlayer = true;
|
|
||||||
} else /* currentPlayer == exoCastPlayer */ {
|
|
||||||
if (exoCastPlayer.getPlaybackState() == Player.STATE_IDLE) {
|
|
||||||
exoCastPlayer.prepare();
|
|
||||||
}
|
|
||||||
if (mediaQueue.isEmpty()) {
|
|
||||||
// Casting started with no local queue. We take the receiver app's queue as our own.
|
|
||||||
maybeUpdateLocalQueueWithRemoteQueueAndNotify();
|
|
||||||
shouldSeekInNewCurrentPlayer = false;
|
|
||||||
} else {
|
|
||||||
// Casting started when the sender app had no queue. We just load our items into the
|
|
||||||
// receiver app's queue. If the receiver had no items in its queue, we also seek to wherever
|
|
||||||
// the sender app was playing.
|
|
||||||
int currentExoCastPlayerState = exoCastPlayer.getPlaybackState();
|
|
||||||
shouldSeekInNewCurrentPlayer =
|
|
||||||
currentExoCastPlayerState == Player.STATE_IDLE
|
|
||||||
|| currentExoCastPlayerState == Player.STATE_ENDED;
|
|
||||||
exoCastPlayer.addItemsToQueue(mediaQueue.toArray(new MediaItem[0]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Playback transition.
|
|
||||||
if (shouldSeekInNewCurrentPlayer && windowIndex != C.INDEX_UNSET) {
|
|
||||||
setCurrentItem(windowIndex, playbackPositionMs, playWhenReady);
|
|
||||||
} else if (getMediaQueueSize() > 0) {
|
|
||||||
maybeSetCurrentItemAndNotify(currentPlayer.getCurrentWindowIndex());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts playback of the item at the given position.
|
|
||||||
*
|
|
||||||
* @param itemIndex The index of the item to play.
|
|
||||||
* @param positionMs The position at which playback should start.
|
|
||||||
* @param playWhenReady Whether the player should proceed when ready to do so.
|
|
||||||
*/
|
|
||||||
private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) {
|
|
||||||
maybeSetCurrentItemAndNotify(itemIndex);
|
|
||||||
currentPlayer.seekTo(itemIndex, positionMs);
|
|
||||||
if (currentPlayer.getPlaybackState() == Player.STATE_IDLE) {
|
|
||||||
if (currentPlayer == exoCastPlayer) {
|
|
||||||
exoCastPlayer.prepare();
|
|
||||||
} else {
|
|
||||||
exoPlayer.prepare(concatenatingMediaSource);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
currentPlayer.setPlayWhenReady(playWhenReady);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void maybeSetCurrentItemAndNotify(int currentItemIndex) {
|
|
||||||
if (this.currentItemIndex != currentItemIndex) {
|
|
||||||
int oldIndex = this.currentItemIndex;
|
|
||||||
this.currentItemIndex = currentItemIndex;
|
|
||||||
listener.onQueuePositionChanged(oldIndex, currentItemIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MediaSource buildMediaSource(MediaItem item) {
|
|
||||||
Uri uri = item.media.uri;
|
|
||||||
switch (item.mimeType) {
|
|
||||||
case DemoUtil.MIME_TYPE_SS:
|
|
||||||
return new SsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
|
||||||
case DemoUtil.MIME_TYPE_DASH:
|
|
||||||
return new DashMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
|
||||||
case DemoUtil.MIME_TYPE_HLS:
|
|
||||||
return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
|
||||||
case DemoUtil.MIME_TYPE_VIDEO_MP4:
|
|
||||||
return new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
|
||||||
default:
|
|
||||||
{
|
|
||||||
throw new IllegalStateException("Unsupported type: " + item.mimeType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -34,14 +34,11 @@ import android.view.ViewGroup;
|
|||||||
import android.widget.ArrayAdapter;
|
import android.widget.ArrayAdapter;
|
||||||
import android.widget.ListView;
|
import android.widget.ListView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
import com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider;
|
|
||||||
import com.google.android.exoplayer2.ext.cast.MediaItem;
|
import com.google.android.exoplayer2.ext.cast.MediaItem;
|
||||||
import com.google.android.exoplayer2.ui.PlayerControlView;
|
import com.google.android.exoplayer2.ui.PlayerControlView;
|
||||||
import com.google.android.exoplayer2.ui.PlayerView;
|
import com.google.android.exoplayer2.ui.PlayerView;
|
||||||
import com.google.android.gms.cast.CastMediaControlIntent;
|
|
||||||
import com.google.android.gms.cast.framework.CastButtonFactory;
|
import com.google.android.gms.cast.framework.CastButtonFactory;
|
||||||
import com.google.android.gms.cast.framework.CastContext;
|
import com.google.android.gms.cast.framework.CastContext;
|
||||||
import com.google.android.gms.dynamite.DynamiteModule;
|
import com.google.android.gms.dynamite.DynamiteModule;
|
||||||
@ -120,21 +117,13 @@ public class MainActivity extends AppCompatActivity
|
|||||||
// There is no Cast context to work with. Do nothing.
|
// There is no Cast context to work with. Do nothing.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String applicationId = castContext.getCastOptions().getReceiverApplicationId();
|
playerManager =
|
||||||
switch (applicationId) {
|
new PlayerManager(
|
||||||
case CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID:
|
/* listener= */ this,
|
||||||
case DefaultCastOptionsProvider.APP_ID_DEFAULT_RECEIVER_WITH_DRM:
|
localPlayerView,
|
||||||
playerManager =
|
castControlView,
|
||||||
new DefaultReceiverPlayerManager(
|
/* context= */ this,
|
||||||
/* listener= */ this,
|
castContext);
|
||||||
localPlayerView,
|
|
||||||
castControlView,
|
|
||||||
/* context= */ this,
|
|
||||||
castContext);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IllegalStateException("Illegal receiver app id: " + applicationId);
|
|
||||||
}
|
|
||||||
mediaQueueList.setAdapter(mediaQueueListAdapter);
|
mediaQueueList.setAdapter(mediaQueueListAdapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,16 +170,6 @@ public class MainActivity extends AppCompatActivity
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onQueueContentsExternallyChanged() {
|
|
||||||
mediaQueueListAdapter.notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPlayerError() {
|
|
||||||
Toast.makeText(getApplicationContext(), R.string.player_error_msg, Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Internal methods.
|
// Internal methods.
|
||||||
|
|
||||||
private View buildSampleListView() {
|
private View buildSampleListView() {
|
||||||
|
@ -15,53 +15,419 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.castdemo;
|
package com.google.android.exoplayer2.castdemo;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
|
import android.view.View;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||||
|
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||||
|
import com.google.android.exoplayer2.Player;
|
||||||
|
import com.google.android.exoplayer2.Player.DiscontinuityReason;
|
||||||
|
import com.google.android.exoplayer2.Player.EventListener;
|
||||||
|
import com.google.android.exoplayer2.Player.TimelineChangeReason;
|
||||||
|
import com.google.android.exoplayer2.RenderersFactory;
|
||||||
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
|
import com.google.android.exoplayer2.Timeline;
|
||||||
|
import com.google.android.exoplayer2.Timeline.Period;
|
||||||
|
import com.google.android.exoplayer2.ext.cast.CastPlayer;
|
||||||
import com.google.android.exoplayer2.ext.cast.MediaItem;
|
import com.google.android.exoplayer2.ext.cast.MediaItem;
|
||||||
|
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener;
|
||||||
|
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||||
|
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||||
|
import com.google.android.exoplayer2.ui.PlayerControlView;
|
||||||
|
import com.google.android.exoplayer2.ui.PlayerView;
|
||||||
|
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||||
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
|
import com.google.android.gms.cast.MediaInfo;
|
||||||
|
import com.google.android.gms.cast.MediaMetadata;
|
||||||
|
import com.google.android.gms.cast.MediaQueueItem;
|
||||||
|
import com.google.android.gms.cast.framework.CastContext;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
/** Manages the players in the Cast demo app. */
|
/** Manages players and an internal media queue for the demo app. */
|
||||||
/* package */ interface PlayerManager {
|
/* package */ class PlayerManager implements EventListener, SessionAvailabilityListener {
|
||||||
|
|
||||||
/** Listener for events. */
|
/** Listener for events. */
|
||||||
interface Listener {
|
interface Listener {
|
||||||
|
|
||||||
/** Called when the currently played item of the media queue changes. */
|
/** Called when the currently played item of the media queue changes. */
|
||||||
void onQueuePositionChanged(int previousIndex, int newIndex);
|
void onQueuePositionChanged(int previousIndex, int newIndex);
|
||||||
|
|
||||||
/** Called when the media queue changes due to modifications not caused by this manager. */
|
|
||||||
void onQueueContentsExternallyChanged();
|
|
||||||
|
|
||||||
/** Called when an error occurs in the current player. */
|
|
||||||
void onPlayerError();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Redirects the given {@code keyEvent} to the active player. */
|
private static final String USER_AGENT = "ExoCastDemoPlayer";
|
||||||
boolean dispatchKeyEvent(KeyEvent keyEvent);
|
private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY =
|
||||||
|
new DefaultHttpDataSourceFactory(USER_AGENT);
|
||||||
|
|
||||||
/** Appends the given {@link MediaItem} to the media queue. */
|
private final PlayerView localPlayerView;
|
||||||
void addItem(MediaItem mediaItem);
|
private final PlayerControlView castControlView;
|
||||||
|
private final SimpleExoPlayer exoPlayer;
|
||||||
|
private final CastPlayer castPlayer;
|
||||||
|
private final ArrayList<MediaItem> mediaQueue;
|
||||||
|
private final Listener listener;
|
||||||
|
private final ConcatenatingMediaSource concatenatingMediaSource;
|
||||||
|
|
||||||
/** Returns the number of items in the media queue. */
|
private int currentItemIndex;
|
||||||
int getMediaQueueSize();
|
private Player currentPlayer;
|
||||||
|
|
||||||
/** Selects the item at the given position for playback. */
|
|
||||||
void selectQueueItem(int position);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the position of the item currently being played, or {@link C#INDEX_UNSET} if no item is
|
* Creates a new manager for {@link SimpleExoPlayer} and {@link CastPlayer}.
|
||||||
* being played.
|
*
|
||||||
|
* @param listener A {@link Listener} for queue position changes.
|
||||||
|
* @param localPlayerView The {@link PlayerView} for local playback.
|
||||||
|
* @param castControlView The {@link PlayerControlView} to control remote playback.
|
||||||
|
* @param context A {@link Context}.
|
||||||
|
* @param castContext The {@link CastContext}.
|
||||||
*/
|
*/
|
||||||
int getCurrentItemIndex();
|
public PlayerManager(
|
||||||
|
Listener listener,
|
||||||
|
PlayerView localPlayerView,
|
||||||
|
PlayerControlView castControlView,
|
||||||
|
Context context,
|
||||||
|
CastContext castContext) {
|
||||||
|
this.listener = listener;
|
||||||
|
this.localPlayerView = localPlayerView;
|
||||||
|
this.castControlView = castControlView;
|
||||||
|
mediaQueue = new ArrayList<>();
|
||||||
|
currentItemIndex = C.INDEX_UNSET;
|
||||||
|
concatenatingMediaSource = new ConcatenatingMediaSource();
|
||||||
|
|
||||||
/** Returns the {@link MediaItem} at the given {@code position}. */
|
DefaultTrackSelector trackSelector = new DefaultTrackSelector();
|
||||||
MediaItem getItem(int position);
|
RenderersFactory renderersFactory = new DefaultRenderersFactory(context);
|
||||||
|
exoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector);
|
||||||
|
exoPlayer.addListener(this);
|
||||||
|
localPlayerView.setPlayer(exoPlayer);
|
||||||
|
|
||||||
/** Moves the item at position {@code from} to position {@code to}. */
|
castPlayer = new CastPlayer(castContext);
|
||||||
boolean moveItem(MediaItem item, int to);
|
castPlayer.addListener(this);
|
||||||
|
castPlayer.setSessionAvailabilityListener(this);
|
||||||
|
castControlView.setPlayer(castPlayer);
|
||||||
|
|
||||||
/** Removes the item at position {@code index}. */
|
setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer);
|
||||||
boolean removeItem(MediaItem item);
|
}
|
||||||
|
|
||||||
/** Releases any acquired resources. */
|
// Queue manipulation methods.
|
||||||
void release();
|
|
||||||
|
/**
|
||||||
|
* Plays a specified queue item in the current player.
|
||||||
|
*
|
||||||
|
* @param itemIndex The index of the item to play.
|
||||||
|
*/
|
||||||
|
public void selectQueueItem(int itemIndex) {
|
||||||
|
setCurrentItem(itemIndex, C.TIME_UNSET, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the index of the currently played item. */
|
||||||
|
public int getCurrentItemIndex() {
|
||||||
|
return currentItemIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends {@code item} to the media queue.
|
||||||
|
*
|
||||||
|
* @param item The {@link MediaItem} to append.
|
||||||
|
*/
|
||||||
|
public void addItem(MediaItem item) {
|
||||||
|
mediaQueue.add(item);
|
||||||
|
concatenatingMediaSource.addMediaSource(buildMediaSource(item));
|
||||||
|
if (currentPlayer == castPlayer) {
|
||||||
|
castPlayer.addItems(buildMediaQueueItem(item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the size of the media queue. */
|
||||||
|
public int getMediaQueueSize() {
|
||||||
|
return mediaQueue.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the item at the given index in the media queue.
|
||||||
|
*
|
||||||
|
* @param position The index of the item.
|
||||||
|
* @return The item at the given index in the media queue.
|
||||||
|
*/
|
||||||
|
public MediaItem getItem(int position) {
|
||||||
|
return mediaQueue.get(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the item at the given index from the media queue.
|
||||||
|
*
|
||||||
|
* @param item The item to remove.
|
||||||
|
* @return Whether the removal was successful.
|
||||||
|
*/
|
||||||
|
public boolean removeItem(MediaItem item) {
|
||||||
|
int itemIndex = mediaQueue.indexOf(item);
|
||||||
|
if (itemIndex == -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
concatenatingMediaSource.removeMediaSource(itemIndex);
|
||||||
|
if (currentPlayer == castPlayer) {
|
||||||
|
if (castPlayer.getPlaybackState() != Player.STATE_IDLE) {
|
||||||
|
Timeline castTimeline = castPlayer.getCurrentTimeline();
|
||||||
|
if (castTimeline.getPeriodCount() <= itemIndex) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
castPlayer.removeItem((int) castTimeline.getPeriod(itemIndex, new Period()).id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mediaQueue.remove(itemIndex);
|
||||||
|
if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) {
|
||||||
|
maybeSetCurrentItemAndNotify(C.INDEX_UNSET);
|
||||||
|
} else if (itemIndex < currentItemIndex) {
|
||||||
|
maybeSetCurrentItemAndNotify(currentItemIndex - 1);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves an item within the queue.
|
||||||
|
*
|
||||||
|
* @param item The item to move.
|
||||||
|
* @param toIndex The target index of the item in the queue.
|
||||||
|
* @return Whether the item move was successful.
|
||||||
|
*/
|
||||||
|
public boolean moveItem(MediaItem item, int toIndex) {
|
||||||
|
int fromIndex = mediaQueue.indexOf(item);
|
||||||
|
if (fromIndex == -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Player update.
|
||||||
|
concatenatingMediaSource.moveMediaSource(fromIndex, toIndex);
|
||||||
|
if (currentPlayer == castPlayer && castPlayer.getPlaybackState() != Player.STATE_IDLE) {
|
||||||
|
Timeline castTimeline = castPlayer.getCurrentTimeline();
|
||||||
|
int periodCount = castTimeline.getPeriodCount();
|
||||||
|
if (periodCount <= fromIndex || periodCount <= toIndex) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int elementId = (int) castTimeline.getPeriod(fromIndex, new Period()).id;
|
||||||
|
castPlayer.moveItem(elementId, toIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaQueue.add(toIndex, mediaQueue.remove(fromIndex));
|
||||||
|
|
||||||
|
// Index update.
|
||||||
|
if (fromIndex == currentItemIndex) {
|
||||||
|
maybeSetCurrentItemAndNotify(toIndex);
|
||||||
|
} else if (fromIndex < currentItemIndex && toIndex >= currentItemIndex) {
|
||||||
|
maybeSetCurrentItemAndNotify(currentItemIndex - 1);
|
||||||
|
} else if (fromIndex > currentItemIndex && toIndex <= currentItemIndex) {
|
||||||
|
maybeSetCurrentItemAndNotify(currentItemIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches a given {@link KeyEvent} to the corresponding view of the current player.
|
||||||
|
*
|
||||||
|
* @param event The {@link KeyEvent}.
|
||||||
|
* @return Whether the event was handled by the target view.
|
||||||
|
*/
|
||||||
|
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||||
|
if (currentPlayer == exoPlayer) {
|
||||||
|
return localPlayerView.dispatchKeyEvent(event);
|
||||||
|
} else /* currentPlayer == castPlayer */ {
|
||||||
|
return castControlView.dispatchKeyEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Releases the manager and the players that it holds. */
|
||||||
|
public void release() {
|
||||||
|
currentItemIndex = C.INDEX_UNSET;
|
||||||
|
mediaQueue.clear();
|
||||||
|
concatenatingMediaSource.clear();
|
||||||
|
castPlayer.setSessionAvailabilityListener(null);
|
||||||
|
castPlayer.release();
|
||||||
|
localPlayerView.setPlayer(null);
|
||||||
|
exoPlayer.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Player.EventListener implementation.
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
|
||||||
|
updateCurrentItemIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
|
||||||
|
updateCurrentItemIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTimelineChanged(
|
||||||
|
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
|
||||||
|
updateCurrentItemIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
// CastPlayer.SessionAvailabilityListener implementation.
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCastSessionAvailable() {
|
||||||
|
setCurrentPlayer(castPlayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCastSessionUnavailable() {
|
||||||
|
setCurrentPlayer(exoPlayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal methods.
|
||||||
|
|
||||||
|
private void updateCurrentItemIndex() {
|
||||||
|
int playbackState = currentPlayer.getPlaybackState();
|
||||||
|
maybeSetCurrentItemAndNotify(
|
||||||
|
playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED
|
||||||
|
? currentPlayer.getCurrentWindowIndex()
|
||||||
|
: C.INDEX_UNSET);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setCurrentPlayer(Player currentPlayer) {
|
||||||
|
if (this.currentPlayer == currentPlayer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// View management.
|
||||||
|
if (currentPlayer == exoPlayer) {
|
||||||
|
localPlayerView.setVisibility(View.VISIBLE);
|
||||||
|
castControlView.hide();
|
||||||
|
} else /* currentPlayer == castPlayer */ {
|
||||||
|
localPlayerView.setVisibility(View.GONE);
|
||||||
|
castControlView.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Player state management.
|
||||||
|
long playbackPositionMs = C.TIME_UNSET;
|
||||||
|
int windowIndex = C.INDEX_UNSET;
|
||||||
|
boolean playWhenReady = false;
|
||||||
|
|
||||||
|
Player previousPlayer = this.currentPlayer;
|
||||||
|
if (previousPlayer != null) {
|
||||||
|
// Save state from the previous player.
|
||||||
|
int playbackState = previousPlayer.getPlaybackState();
|
||||||
|
if (playbackState != Player.STATE_ENDED) {
|
||||||
|
playbackPositionMs = previousPlayer.getCurrentPosition();
|
||||||
|
playWhenReady = previousPlayer.getPlayWhenReady();
|
||||||
|
windowIndex = previousPlayer.getCurrentWindowIndex();
|
||||||
|
if (windowIndex != currentItemIndex) {
|
||||||
|
playbackPositionMs = C.TIME_UNSET;
|
||||||
|
windowIndex = currentItemIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
previousPlayer.stop(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentPlayer = currentPlayer;
|
||||||
|
|
||||||
|
// Media queue management.
|
||||||
|
if (currentPlayer == exoPlayer) {
|
||||||
|
exoPlayer.prepare(concatenatingMediaSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Playback transition.
|
||||||
|
if (windowIndex != C.INDEX_UNSET) {
|
||||||
|
setCurrentItem(windowIndex, playbackPositionMs, playWhenReady);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts playback of the item at the given position.
|
||||||
|
*
|
||||||
|
* @param itemIndex The index of the item to play.
|
||||||
|
* @param positionMs The position at which playback should start.
|
||||||
|
* @param playWhenReady Whether the player should proceed when ready to do so.
|
||||||
|
*/
|
||||||
|
private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) {
|
||||||
|
maybeSetCurrentItemAndNotify(itemIndex);
|
||||||
|
if (currentPlayer == castPlayer && castPlayer.getCurrentTimeline().isEmpty()) {
|
||||||
|
MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()];
|
||||||
|
for (int i = 0; i < items.length; i++) {
|
||||||
|
items[i] = buildMediaQueueItem(mediaQueue.get(i));
|
||||||
|
}
|
||||||
|
castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF);
|
||||||
|
} else {
|
||||||
|
currentPlayer.seekTo(itemIndex, positionMs);
|
||||||
|
currentPlayer.setPlayWhenReady(playWhenReady);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void maybeSetCurrentItemAndNotify(int currentItemIndex) {
|
||||||
|
if (this.currentItemIndex != currentItemIndex) {
|
||||||
|
int oldIndex = this.currentItemIndex;
|
||||||
|
this.currentItemIndex = currentItemIndex;
|
||||||
|
listener.onQueuePositionChanged(oldIndex, currentItemIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MediaSource buildMediaSource(MediaItem item) {
|
||||||
|
Uri uri = item.media.uri;
|
||||||
|
switch (item.mimeType) {
|
||||||
|
case DemoUtil.MIME_TYPE_SS:
|
||||||
|
return new SsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
||||||
|
case DemoUtil.MIME_TYPE_DASH:
|
||||||
|
return new DashMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
||||||
|
case DemoUtil.MIME_TYPE_HLS:
|
||||||
|
return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
||||||
|
case DemoUtil.MIME_TYPE_VIDEO_MP4:
|
||||||
|
return new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
||||||
|
default:
|
||||||
|
throw new IllegalStateException("Unsupported type: " + item.mimeType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MediaQueueItem buildMediaQueueItem(MediaItem item) {
|
||||||
|
MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
|
||||||
|
movieMetadata.putString(MediaMetadata.KEY_TITLE, item.title);
|
||||||
|
MediaInfo.Builder mediaInfoBuilder =
|
||||||
|
new MediaInfo.Builder(item.media.uri.toString())
|
||||||
|
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
|
||||||
|
.setContentType(item.mimeType)
|
||||||
|
.setMetadata(movieMetadata);
|
||||||
|
if (!item.drmSchemes.isEmpty()) {
|
||||||
|
MediaItem.DrmScheme scheme = item.drmSchemes.get(0);
|
||||||
|
try {
|
||||||
|
// This configuration is only intended for testing and should *not* be used in production
|
||||||
|
// environments. See comment in the Cast Demo app's options provider.
|
||||||
|
JSONObject drmConfiguration = getDrmConfigurationJson(scheme);
|
||||||
|
if (drmConfiguration != null) {
|
||||||
|
mediaInfoBuilder.setCustomData(drmConfiguration);
|
||||||
|
}
|
||||||
|
} catch (JSONException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new MediaQueueItem.Builder(mediaInfoBuilder.build()).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static JSONObject getDrmConfigurationJson(MediaItem.DrmScheme scheme)
|
||||||
|
throws JSONException {
|
||||||
|
String drmScheme;
|
||||||
|
if (C.WIDEVINE_UUID.equals(scheme.uuid)) {
|
||||||
|
drmScheme = "widevine";
|
||||||
|
} else if (C.PLAYREADY_UUID.equals(scheme.uuid)) {
|
||||||
|
drmScheme = "playready";
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
MediaItem.UriBundle licenseServer = Assertions.checkNotNull(scheme.licenseServer);
|
||||||
|
JSONObject exoplayerConfig =
|
||||||
|
new JSONObject().put("withCredentials", false).put("protectionSystem", drmScheme);
|
||||||
|
if (!licenseServer.uri.equals(Uri.EMPTY)) {
|
||||||
|
exoplayerConfig.put("licenseUrl", licenseServer.uri.toString());
|
||||||
|
}
|
||||||
|
if (!licenseServer.requestHeaders.isEmpty()) {
|
||||||
|
exoplayerConfig.put("headers", new JSONObject(licenseServer.requestHeaders));
|
||||||
|
}
|
||||||
|
return new JSONObject().put("exoPlayerConfig", exoplayerConfig);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,4 @@
|
|||||||
|
|
||||||
<string name="cast_context_error">Failed to get Cast context. Try updating Google Play Services and restart the app.</string>
|
<string name="cast_context_error">Failed to get Cast context. Try updating Google Play Services and restart the app.</string>
|
||||||
|
|
||||||
<string name="player_error_msg">Player error encountered. Select a queue item to reprepare. Check the logcat and receiver app\'s console for more info.</string>
|
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -1,86 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.exoplayer2.ext.cast;
|
|
||||||
|
|
||||||
/** Handles communication with the receiver app using a cast session. */
|
|
||||||
public interface CastSessionManager {
|
|
||||||
|
|
||||||
/** Factory for {@link CastSessionManager} instances. */
|
|
||||||
interface Factory {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a {@link CastSessionManager} instance with the given listener.
|
|
||||||
*
|
|
||||||
* @param listener The listener to notify on receiver app and session state updates.
|
|
||||||
* @return The created instance.
|
|
||||||
*/
|
|
||||||
CastSessionManager create(StateListener listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extends {@link SessionAvailabilityListener} by adding receiver app state notifications.
|
|
||||||
*
|
|
||||||
* <p>Receiver app state notifications contain a sequence number that matches the sequence number
|
|
||||||
* of the last {@link ExoCastMessage} sent (using {@link #send(ExoCastMessage)}) by this session
|
|
||||||
* manager and processed by the receiver app. Sequence numbers are non-negative numbers.
|
|
||||||
*/
|
|
||||||
interface StateListener extends SessionAvailabilityListener {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when a status update is received from the Cast Receiver app.
|
|
||||||
*
|
|
||||||
* @param stateUpdate A {@link ReceiverAppStateUpdate} containing the fields included in the
|
|
||||||
* message.
|
|
||||||
*/
|
|
||||||
void onStateUpdateFromReceiverApp(ReceiverAppStateUpdate stateUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Special constant representing an unset sequence number. It is guaranteed to be a negative
|
|
||||||
* value.
|
|
||||||
*/
|
|
||||||
long SEQUENCE_NUMBER_UNSET = Long.MIN_VALUE;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Connects the session manager to the cast message bus and starts listening for session
|
|
||||||
* availability changes. Also announces that this sender app is connected to the message bus.
|
|
||||||
*/
|
|
||||||
void start();
|
|
||||||
|
|
||||||
/** Stops tracking the state of the cast session and closes any existing session. */
|
|
||||||
void stopTrackingSession();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Same as {@link #stopTrackingSession()}, but also stops the receiver app if a session is
|
|
||||||
* currently available.
|
|
||||||
*/
|
|
||||||
void stopTrackingSessionAndCasting();
|
|
||||||
|
|
||||||
/** Whether a cast session is available. */
|
|
||||||
boolean isCastSessionAvailable();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends an {@link ExoCastMessage} to the receiver app.
|
|
||||||
*
|
|
||||||
* <p>A sequence number is assigned to every sent message. Message senders may mask the local
|
|
||||||
* state until a status update from the receiver app (see {@link StateListener}) is received with
|
|
||||||
* a greater or equal sequence number.
|
|
||||||
*
|
|
||||||
* @param message The message to send.
|
|
||||||
* @return The sequence number assigned to the message.
|
|
||||||
*/
|
|
||||||
long send(ExoCastMessage message);
|
|
||||||
}
|
|
@ -44,7 +44,7 @@ public final class DefaultCastOptionsProvider implements OptionsProvider {
|
|||||||
* do not require DRM, the default receiver app should be used (see {@link
|
* do not require DRM, the default receiver app should be used (see {@link
|
||||||
* #APP_ID_DEFAULT_RECEIVER}).
|
* #APP_ID_DEFAULT_RECEIVER}).
|
||||||
*/
|
*/
|
||||||
// TODO: Add a documentation resource link for DRM support in the receiver app [Internal ref:
|
// TODO: Add a documentation resource link for DRM support in the receiver app [Internal ref:
|
||||||
// b/128603245].
|
// b/128603245].
|
||||||
public static final String APP_ID_DEFAULT_RECEIVER_WITH_DRM = "A12D4273";
|
public static final String APP_ID_DEFAULT_RECEIVER_WITH_DRM = "A12D4273";
|
||||||
|
|
||||||
|
@ -1,187 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.exoplayer2.ext.cast;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
|
||||||
import com.google.android.exoplayer2.util.Log;
|
|
||||||
import com.google.android.gms.cast.Cast;
|
|
||||||
import com.google.android.gms.cast.CastDevice;
|
|
||||||
import com.google.android.gms.cast.framework.CastContext;
|
|
||||||
import com.google.android.gms.cast.framework.CastSession;
|
|
||||||
import com.google.android.gms.cast.framework.SessionManager;
|
|
||||||
import com.google.android.gms.cast.framework.SessionManagerListener;
|
|
||||||
import java.io.IOException;
|
|
||||||
import org.json.JSONException;
|
|
||||||
|
|
||||||
/** Implements {@link CastSessionManager} by using JSON message passing. */
|
|
||||||
public class DefaultCastSessionManager implements CastSessionManager {
|
|
||||||
|
|
||||||
private static final String TAG = "DefaultCastSessionManager";
|
|
||||||
private static final String EXOPLAYER_CAST_NAMESPACE = "urn:x-cast:com.google.exoplayer.cast";
|
|
||||||
|
|
||||||
private final SessionManager sessionManager;
|
|
||||||
private final CastSessionListener castSessionListener;
|
|
||||||
private final StateListener stateListener;
|
|
||||||
private final Cast.MessageReceivedCallback messageReceivedCallback;
|
|
||||||
|
|
||||||
private boolean started;
|
|
||||||
private long sequenceNumber;
|
|
||||||
private long expectedInitialStateUpdateSequence;
|
|
||||||
@Nullable private CastSession currentSession;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param context The Cast context from which the cast session is obtained.
|
|
||||||
* @param stateListener The listener to notify of state changes.
|
|
||||||
*/
|
|
||||||
public DefaultCastSessionManager(CastContext context, StateListener stateListener) {
|
|
||||||
this.stateListener = stateListener;
|
|
||||||
sessionManager = context.getSessionManager();
|
|
||||||
currentSession = sessionManager.getCurrentCastSession();
|
|
||||||
castSessionListener = new CastSessionListener();
|
|
||||||
messageReceivedCallback = new CastMessageCallback();
|
|
||||||
expectedInitialStateUpdateSequence = SEQUENCE_NUMBER_UNSET;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void start() {
|
|
||||||
started = true;
|
|
||||||
sessionManager.addSessionManagerListener(castSessionListener, CastSession.class);
|
|
||||||
currentSession = sessionManager.getCurrentCastSession();
|
|
||||||
if (currentSession != null) {
|
|
||||||
setMessageCallbackOnSession();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void stopTrackingSession() {
|
|
||||||
stop(/* stopCasting= */ false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void stopTrackingSessionAndCasting() {
|
|
||||||
stop(/* stopCasting= */ true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isCastSessionAvailable() {
|
|
||||||
return currentSession != null && expectedInitialStateUpdateSequence == SEQUENCE_NUMBER_UNSET;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long send(ExoCastMessage message) {
|
|
||||||
if (currentSession != null) {
|
|
||||||
currentSession.sendMessage(EXOPLAYER_CAST_NAMESPACE, message.toJsonString(sequenceNumber));
|
|
||||||
} else {
|
|
||||||
Log.w(TAG, "Tried to send a message with no established session. Method: " + message.method);
|
|
||||||
}
|
|
||||||
return sequenceNumber++;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void stop(boolean stopCasting) {
|
|
||||||
sessionManager.removeSessionManagerListener(castSessionListener, CastSession.class);
|
|
||||||
if (currentSession != null) {
|
|
||||||
sessionManager.endCurrentSession(stopCasting);
|
|
||||||
}
|
|
||||||
currentSession = null;
|
|
||||||
started = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setCastSession(@Nullable CastSession session) {
|
|
||||||
Assertions.checkState(started);
|
|
||||||
boolean hadSession = currentSession != null;
|
|
||||||
currentSession = session;
|
|
||||||
if (!hadSession && session != null) {
|
|
||||||
setMessageCallbackOnSession();
|
|
||||||
} else if (hadSession && session == null) {
|
|
||||||
stateListener.onCastSessionUnavailable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setMessageCallbackOnSession() {
|
|
||||||
try {
|
|
||||||
Assertions.checkNotNull(currentSession)
|
|
||||||
.setMessageReceivedCallbacks(EXOPLAYER_CAST_NAMESPACE, messageReceivedCallback);
|
|
||||||
expectedInitialStateUpdateSequence = send(new ExoCastMessage.OnClientConnected());
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new IllegalStateException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Listens for Cast session state changes. */
|
|
||||||
private class CastSessionListener implements SessionManagerListener<CastSession> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSessionStarting(CastSession castSession) {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSessionStarted(CastSession castSession, String sessionId) {
|
|
||||||
setCastSession(castSession);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSessionStartFailed(CastSession castSession, int error) {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSessionEnding(CastSession castSession) {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSessionEnded(CastSession castSession, int error) {
|
|
||||||
setCastSession(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSessionResuming(CastSession castSession, String sessionId) {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSessionResumed(CastSession castSession, boolean wasSuspended) {
|
|
||||||
setCastSession(castSession);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSessionResumeFailed(CastSession castSession, int error) {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSessionSuspended(CastSession castSession, int reason) {
|
|
||||||
setCastSession(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class CastMessageCallback implements Cast.MessageReceivedCallback {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onMessageReceived(CastDevice castDevice, String namespace, String message) {
|
|
||||||
if (!EXOPLAYER_CAST_NAMESPACE.equals(namespace)) {
|
|
||||||
// Non-matching namespace. Ignore.
|
|
||||||
Log.e(TAG, String.format("Unrecognized namespace: '%s'.", namespace));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
ReceiverAppStateUpdate receivedUpdate = ReceiverAppStateUpdate.fromJsonMessage(message);
|
|
||||||
if (expectedInitialStateUpdateSequence == SEQUENCE_NUMBER_UNSET
|
|
||||||
|| receivedUpdate.sequenceNumber >= expectedInitialStateUpdateSequence) {
|
|
||||||
stateListener.onStateUpdateFromReceiverApp(receivedUpdate);
|
|
||||||
if (expectedInitialStateUpdateSequence != SEQUENCE_NUMBER_UNSET) {
|
|
||||||
expectedInitialStateUpdateSequence = SEQUENCE_NUMBER_UNSET;
|
|
||||||
stateListener.onCastSessionAvailable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (JSONException e) {
|
|
||||||
Log.e(TAG, "Error while parsing state update from receiver: ", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,118 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.exoplayer2.ext.cast;
|
|
||||||
|
|
||||||
/** Defines constants used by the Cast extension. */
|
|
||||||
public final class ExoCastConstants {
|
|
||||||
|
|
||||||
private ExoCastConstants() {}
|
|
||||||
|
|
||||||
public static final int PROTOCOL_VERSION = 0;
|
|
||||||
|
|
||||||
// String representations.
|
|
||||||
|
|
||||||
public static final String STR_STATE_IDLE = "IDLE";
|
|
||||||
public static final String STR_STATE_BUFFERING = "BUFFERING";
|
|
||||||
public static final String STR_STATE_READY = "READY";
|
|
||||||
public static final String STR_STATE_ENDED = "ENDED";
|
|
||||||
|
|
||||||
public static final String STR_REPEAT_MODE_OFF = "OFF";
|
|
||||||
public static final String STR_REPEAT_MODE_ONE = "ONE";
|
|
||||||
public static final String STR_REPEAT_MODE_ALL = "ALL";
|
|
||||||
|
|
||||||
public static final String STR_DISCONTINUITY_REASON_PERIOD_TRANSITION = "PERIOD_TRANSITION";
|
|
||||||
public static final String STR_DISCONTINUITY_REASON_SEEK = "SEEK";
|
|
||||||
public static final String STR_DISCONTINUITY_REASON_SEEK_ADJUSTMENT = "SEEK_ADJUSTMENT";
|
|
||||||
public static final String STR_DISCONTINUITY_REASON_AD_INSERTION = "AD_INSERTION";
|
|
||||||
public static final String STR_DISCONTINUITY_REASON_INTERNAL = "INTERNAL";
|
|
||||||
|
|
||||||
public static final String STR_SELECTION_FLAG_DEFAULT = "DEFAULT";
|
|
||||||
public static final String STR_SELECTION_FLAG_FORCED = "FORCED";
|
|
||||||
public static final String STR_SELECTION_FLAG_AUTOSELECT = "AUTOSELECT";
|
|
||||||
|
|
||||||
// Methods.
|
|
||||||
|
|
||||||
public static final String METHOD_BASE = "player.";
|
|
||||||
|
|
||||||
public static final String METHOD_ON_CLIENT_CONNECTED = METHOD_BASE + "onClientConnected";
|
|
||||||
public static final String METHOD_ADD_ITEMS = METHOD_BASE + "addItems";
|
|
||||||
public static final String METHOD_MOVE_ITEM = METHOD_BASE + "moveItem";
|
|
||||||
public static final String METHOD_PREPARE = METHOD_BASE + "prepare";
|
|
||||||
public static final String METHOD_REMOVE_ITEMS = METHOD_BASE + "removeItems";
|
|
||||||
public static final String METHOD_SET_PLAY_WHEN_READY = METHOD_BASE + "setPlayWhenReady";
|
|
||||||
public static final String METHOD_SET_REPEAT_MODE = METHOD_BASE + "setRepeatMode";
|
|
||||||
public static final String METHOD_SET_SHUFFLE_MODE_ENABLED =
|
|
||||||
METHOD_BASE + "setShuffleModeEnabled";
|
|
||||||
public static final String METHOD_SEEK_TO = METHOD_BASE + "seekTo";
|
|
||||||
public static final String METHOD_SET_PLAYBACK_PARAMETERS = METHOD_BASE + "setPlaybackParameters";
|
|
||||||
public static final String METHOD_SET_TRACK_SELECTION_PARAMETERS =
|
|
||||||
METHOD_BASE + ".setTrackSelectionParameters";
|
|
||||||
public static final String METHOD_STOP = METHOD_BASE + "stop";
|
|
||||||
|
|
||||||
// JSON message keys.
|
|
||||||
|
|
||||||
public static final String KEY_ARGS = "args";
|
|
||||||
public static final String KEY_DEFAULT_START_POSITION_US = "defaultStartPositionUs";
|
|
||||||
public static final String KEY_DESCRIPTION = "description";
|
|
||||||
public static final String KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS =
|
|
||||||
"disabledTextTrackSelectionFlags";
|
|
||||||
public static final String KEY_DISCONTINUITY_REASON = "discontinuityReason";
|
|
||||||
public static final String KEY_DRM_SCHEMES = "drmSchemes";
|
|
||||||
public static final String KEY_DURATION_US = "durationUs";
|
|
||||||
public static final String KEY_END_POSITION_US = "endPositionUs";
|
|
||||||
public static final String KEY_ERROR_MESSAGE = "error";
|
|
||||||
public static final String KEY_ID = "id";
|
|
||||||
public static final String KEY_INDEX = "index";
|
|
||||||
public static final String KEY_IS_DYNAMIC = "isDynamic";
|
|
||||||
public static final String KEY_IS_LOADING = "isLoading";
|
|
||||||
public static final String KEY_IS_SEEKABLE = "isSeekable";
|
|
||||||
public static final String KEY_ITEMS = "items";
|
|
||||||
public static final String KEY_LICENSE_SERVER = "licenseServer";
|
|
||||||
public static final String KEY_MEDIA = "media";
|
|
||||||
public static final String KEY_MEDIA_ITEMS_INFO = "mediaItemsInfo";
|
|
||||||
public static final String KEY_MEDIA_QUEUE = "mediaQueue";
|
|
||||||
public static final String KEY_METHOD = "method";
|
|
||||||
public static final String KEY_MIME_TYPE = "mimeType";
|
|
||||||
public static final String KEY_PERIOD_ID = "periodId";
|
|
||||||
public static final String KEY_PERIODS = "periods";
|
|
||||||
public static final String KEY_PITCH = "pitch";
|
|
||||||
public static final String KEY_PLAY_WHEN_READY = "playWhenReady";
|
|
||||||
public static final String KEY_PLAYBACK_PARAMETERS = "playbackParameters";
|
|
||||||
public static final String KEY_PLAYBACK_POSITION = "playbackPosition";
|
|
||||||
public static final String KEY_PLAYBACK_STATE = "playbackState";
|
|
||||||
public static final String KEY_POSITION_IN_FIRST_PERIOD_US = "positionInFirstPeriodUs";
|
|
||||||
public static final String KEY_POSITION_MS = "positionMs";
|
|
||||||
public static final String KEY_PREFERRED_AUDIO_LANGUAGE = "preferredAudioLanguage";
|
|
||||||
public static final String KEY_PREFERRED_TEXT_LANGUAGE = "preferredTextLanguage";
|
|
||||||
public static final String KEY_PROTOCOL_VERSION = "protocolVersion";
|
|
||||||
public static final String KEY_REPEAT_MODE = "repeatMode";
|
|
||||||
public static final String KEY_REQUEST_HEADERS = "requestHeaders";
|
|
||||||
public static final String KEY_RESET = "reset";
|
|
||||||
public static final String KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE =
|
|
||||||
"selectUndeterminedTextLanguage";
|
|
||||||
public static final String KEY_SEQUENCE_NUMBER = "sequenceNumber";
|
|
||||||
public static final String KEY_SHUFFLE_MODE_ENABLED = "shuffleModeEnabled";
|
|
||||||
public static final String KEY_SHUFFLE_ORDER = "shuffleOrder";
|
|
||||||
public static final String KEY_SKIP_SILENCE = "skipSilence";
|
|
||||||
public static final String KEY_SPEED = "speed";
|
|
||||||
public static final String KEY_START_POSITION_US = "startPositionUs";
|
|
||||||
public static final String KEY_TITLE = "title";
|
|
||||||
public static final String KEY_TRACK_SELECTION_PARAMETERS = "trackSelectionParameters";
|
|
||||||
public static final String KEY_URI = "uri";
|
|
||||||
public static final String KEY_UUID = "uuid";
|
|
||||||
public static final String KEY_UUIDS = "uuids";
|
|
||||||
public static final String KEY_WINDOW_DURATION_US = "windowDurationUs";
|
|
||||||
}
|
|
@ -1,474 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.exoplayer2.ext.cast;
|
|
||||||
|
|
||||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
|
|
||||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
|
|
||||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ARGS;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DESCRIPTION;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DRM_SCHEMES;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_END_POSITION_US;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_INDEX;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ITEMS;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_LICENSE_SERVER;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MEDIA;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_METHOD;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MIME_TYPE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PITCH;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAY_WHEN_READY;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_POSITION_MS;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_AUDIO_LANGUAGE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_TEXT_LANGUAGE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PROTOCOL_VERSION;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_REPEAT_MODE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_REQUEST_HEADERS;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_RESET;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SEQUENCE_NUMBER;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_MODE_ENABLED;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_ORDER;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SKIP_SILENCE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SPEED;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_START_POSITION_US;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_TITLE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_URI;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUID;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUIDS;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_ADD_ITEMS;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_MOVE_ITEM;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_ON_CLIENT_CONNECTED;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_PREPARE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_REMOVE_ITEMS;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SEEK_TO;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_PLAYBACK_PARAMETERS;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_PLAY_WHEN_READY;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_REPEAT_MODE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_SHUFFLE_MODE_ENABLED;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_TRACK_SELECTION_PARAMETERS;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_STOP;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.PROTOCOL_VERSION;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_ALL;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_OFF;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_ONE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_SELECTION_FLAG_AUTOSELECT;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_SELECTION_FLAG_DEFAULT;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_SELECTION_FLAG_FORCED;
|
|
||||||
|
|
||||||
import com.google.android.exoplayer2.C;
|
|
||||||
import com.google.android.exoplayer2.PlaybackParameters;
|
|
||||||
import com.google.android.exoplayer2.Player;
|
|
||||||
import com.google.android.exoplayer2.ext.cast.MediaItem.UriBundle;
|
|
||||||
import com.google.android.exoplayer2.source.ShuffleOrder;
|
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelectionParameters;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.UUID;
|
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
// TODO(Internal b/118432277): Evaluate using a proto for sending to the receiver app.
|
|
||||||
/** A serializable message for operating a media player. */
|
|
||||||
public abstract class ExoCastMessage {
|
|
||||||
|
|
||||||
/** Notifies the receiver app of the connection of a sender app to the message bus. */
|
|
||||||
public static final class OnClientConnected extends ExoCastMessage {
|
|
||||||
|
|
||||||
public OnClientConnected() {
|
|
||||||
super(METHOD_ON_CLIENT_CONNECTED);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected JSONObject getArgumentsAsJsonObject() {
|
|
||||||
// No arguments needed.
|
|
||||||
return new JSONObject();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Transitions the player out of {@link Player#STATE_IDLE}. */
|
|
||||||
public static final class Prepare extends ExoCastMessage {
|
|
||||||
|
|
||||||
public Prepare() {
|
|
||||||
super(METHOD_PREPARE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected JSONObject getArgumentsAsJsonObject() {
|
|
||||||
// No arguments needed.
|
|
||||||
return new JSONObject();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Transitions the player to {@link Player#STATE_IDLE} and optionally resets its state. */
|
|
||||||
public static final class Stop extends ExoCastMessage {
|
|
||||||
|
|
||||||
/** Whether the player state should be reset. */
|
|
||||||
public final boolean reset;
|
|
||||||
|
|
||||||
public Stop(boolean reset) {
|
|
||||||
super(METHOD_STOP);
|
|
||||||
this.reset = reset;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected JSONObject getArgumentsAsJsonObject() throws JSONException {
|
|
||||||
return new JSONObject().put(KEY_RESET, reset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Adds items to a media player queue. */
|
|
||||||
public static final class AddItems extends ExoCastMessage {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The index at which the {@link #items} should be inserted. If {@link C#INDEX_UNSET}, the items
|
|
||||||
* are appended to the queue.
|
|
||||||
*/
|
|
||||||
public final int index;
|
|
||||||
/** The {@link MediaItem items} to add to the media queue. */
|
|
||||||
public final List<MediaItem> items;
|
|
||||||
/**
|
|
||||||
* The shuffle order to use for the media queue that results of adding the items to the queue.
|
|
||||||
*/
|
|
||||||
public final ShuffleOrder shuffleOrder;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param index See {@link #index}.
|
|
||||||
* @param items See {@link #items}.
|
|
||||||
* @param shuffleOrder See {@link #shuffleOrder}.
|
|
||||||
*/
|
|
||||||
public AddItems(int index, List<MediaItem> items, ShuffleOrder shuffleOrder) {
|
|
||||||
super(METHOD_ADD_ITEMS);
|
|
||||||
this.index = index;
|
|
||||||
this.items = Collections.unmodifiableList(new ArrayList<>(items));
|
|
||||||
this.shuffleOrder = shuffleOrder;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected JSONObject getArgumentsAsJsonObject() throws JSONException {
|
|
||||||
JSONObject arguments =
|
|
||||||
new JSONObject()
|
|
||||||
.put(KEY_ITEMS, getItemsAsJsonArray())
|
|
||||||
.put(KEY_SHUFFLE_ORDER, getShuffleOrderAsJson(shuffleOrder));
|
|
||||||
maybePutValue(arguments, KEY_INDEX, index, C.INDEX_UNSET);
|
|
||||||
return arguments;
|
|
||||||
}
|
|
||||||
|
|
||||||
private JSONArray getItemsAsJsonArray() throws JSONException {
|
|
||||||
JSONArray result = new JSONArray();
|
|
||||||
for (MediaItem item : items) {
|
|
||||||
result.put(mediaItemAsJsonObject(item));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Moves an item in a player media queue. */
|
|
||||||
public static final class MoveItem extends ExoCastMessage {
|
|
||||||
|
|
||||||
/** The {@link MediaItem#uuid} of the item to move. */
|
|
||||||
public final UUID uuid;
|
|
||||||
/** The index in the queue to which the item should be moved. */
|
|
||||||
public final int index;
|
|
||||||
/** The shuffle order to use for the media queue that results of moving the item. */
|
|
||||||
public ShuffleOrder shuffleOrder;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param uuid See {@link #uuid}.
|
|
||||||
* @param index See {@link #index}.
|
|
||||||
* @param shuffleOrder See {@link #shuffleOrder}.
|
|
||||||
*/
|
|
||||||
public MoveItem(UUID uuid, int index, ShuffleOrder shuffleOrder) {
|
|
||||||
super(METHOD_MOVE_ITEM);
|
|
||||||
this.uuid = uuid;
|
|
||||||
this.index = index;
|
|
||||||
this.shuffleOrder = shuffleOrder;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected JSONObject getArgumentsAsJsonObject() throws JSONException {
|
|
||||||
return new JSONObject()
|
|
||||||
.put(KEY_UUID, uuid)
|
|
||||||
.put(KEY_INDEX, index)
|
|
||||||
.put(KEY_SHUFFLE_ORDER, getShuffleOrderAsJson(shuffleOrder));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Removes items from a player queue. */
|
|
||||||
public static final class RemoveItems extends ExoCastMessage {
|
|
||||||
|
|
||||||
/** The {@link MediaItem#uuid} of the items to remove from the queue. */
|
|
||||||
public final List<UUID> uuids;
|
|
||||||
|
|
||||||
/** @param uuids See {@link #uuids}. */
|
|
||||||
public RemoveItems(List<UUID> uuids) {
|
|
||||||
super(METHOD_REMOVE_ITEMS);
|
|
||||||
this.uuids = Collections.unmodifiableList(new ArrayList<>(uuids));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected JSONObject getArgumentsAsJsonObject() throws JSONException {
|
|
||||||
return new JSONObject().put(KEY_UUIDS, new JSONArray(uuids));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** See {@link Player#setPlayWhenReady(boolean)}. */
|
|
||||||
public static final class SetPlayWhenReady extends ExoCastMessage {
|
|
||||||
|
|
||||||
/** The {@link Player#setPlayWhenReady(boolean) playWhenReady} value to set. */
|
|
||||||
public final boolean playWhenReady;
|
|
||||||
|
|
||||||
/** @param playWhenReady See {@link #playWhenReady}. */
|
|
||||||
public SetPlayWhenReady(boolean playWhenReady) {
|
|
||||||
super(METHOD_SET_PLAY_WHEN_READY);
|
|
||||||
this.playWhenReady = playWhenReady;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected JSONObject getArgumentsAsJsonObject() throws JSONException {
|
|
||||||
return new JSONObject().put(KEY_PLAY_WHEN_READY, playWhenReady);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the repeat mode of the media player.
|
|
||||||
*
|
|
||||||
* @see Player#setRepeatMode(int)
|
|
||||||
*/
|
|
||||||
public static final class SetRepeatMode extends ExoCastMessage {
|
|
||||||
|
|
||||||
/** The {@link Player#setRepeatMode(int) repeatMode} to set. */
|
|
||||||
@Player.RepeatMode public final int repeatMode;
|
|
||||||
|
|
||||||
/** @param repeatMode See {@link #repeatMode}. */
|
|
||||||
public SetRepeatMode(@Player.RepeatMode int repeatMode) {
|
|
||||||
super(METHOD_SET_REPEAT_MODE);
|
|
||||||
this.repeatMode = repeatMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected JSONObject getArgumentsAsJsonObject() throws JSONException {
|
|
||||||
return new JSONObject().put(KEY_REPEAT_MODE, repeatModeToString(repeatMode));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String repeatModeToString(@Player.RepeatMode int repeatMode) {
|
|
||||||
switch (repeatMode) {
|
|
||||||
case REPEAT_MODE_OFF:
|
|
||||||
return STR_REPEAT_MODE_OFF;
|
|
||||||
case REPEAT_MODE_ONE:
|
|
||||||
return STR_REPEAT_MODE_ONE;
|
|
||||||
case REPEAT_MODE_ALL:
|
|
||||||
return STR_REPEAT_MODE_ALL;
|
|
||||||
default:
|
|
||||||
throw new AssertionError("Illegal repeat mode: " + repeatMode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enables and disables shuffle mode in the media player.
|
|
||||||
*
|
|
||||||
* @see Player#setShuffleModeEnabled(boolean)
|
|
||||||
*/
|
|
||||||
public static final class SetShuffleModeEnabled extends ExoCastMessage {
|
|
||||||
|
|
||||||
/** The {@link Player#setShuffleModeEnabled(boolean) shuffleModeEnabled} value to set. */
|
|
||||||
public boolean shuffleModeEnabled;
|
|
||||||
|
|
||||||
/** @param shuffleModeEnabled See {@link #shuffleModeEnabled}. */
|
|
||||||
public SetShuffleModeEnabled(boolean shuffleModeEnabled) {
|
|
||||||
super(METHOD_SET_SHUFFLE_MODE_ENABLED);
|
|
||||||
this.shuffleModeEnabled = shuffleModeEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected JSONObject getArgumentsAsJsonObject() throws JSONException {
|
|
||||||
return new JSONObject().put(KEY_SHUFFLE_MODE_ENABLED, shuffleModeEnabled);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** See {@link Player#seekTo(int, long)}. */
|
|
||||||
public static final class SeekTo extends ExoCastMessage {
|
|
||||||
|
|
||||||
/** The {@link MediaItem#uuid} of the item to seek to. */
|
|
||||||
public final UUID uuid;
|
|
||||||
/**
|
|
||||||
* The seek position in milliseconds in the specified item. If {@link C#TIME_UNSET}, the target
|
|
||||||
* position is the item's default position.
|
|
||||||
*/
|
|
||||||
public final long positionMs;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param uuid See {@link #uuid}.
|
|
||||||
* @param positionMs See {@link #positionMs}.
|
|
||||||
*/
|
|
||||||
public SeekTo(UUID uuid, long positionMs) {
|
|
||||||
super(METHOD_SEEK_TO);
|
|
||||||
this.uuid = uuid;
|
|
||||||
this.positionMs = positionMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected JSONObject getArgumentsAsJsonObject() throws JSONException {
|
|
||||||
JSONObject result = new JSONObject().put(KEY_UUID, uuid);
|
|
||||||
ExoCastMessage.maybePutValue(result, KEY_POSITION_MS, positionMs, C.TIME_UNSET);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** See {@link Player#setPlaybackParameters(PlaybackParameters)}. */
|
|
||||||
public static final class SetPlaybackParameters extends ExoCastMessage {
|
|
||||||
|
|
||||||
/** The {@link Player#setPlaybackParameters(PlaybackParameters) parameters} to set. */
|
|
||||||
public final PlaybackParameters playbackParameters;
|
|
||||||
|
|
||||||
/** @param playbackParameters See {@link #playbackParameters}. */
|
|
||||||
public SetPlaybackParameters(PlaybackParameters playbackParameters) {
|
|
||||||
super(METHOD_SET_PLAYBACK_PARAMETERS);
|
|
||||||
this.playbackParameters = playbackParameters;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected JSONObject getArgumentsAsJsonObject() throws JSONException {
|
|
||||||
return new JSONObject()
|
|
||||||
.put(KEY_SPEED, playbackParameters.speed)
|
|
||||||
.put(KEY_PITCH, playbackParameters.pitch)
|
|
||||||
.put(KEY_SKIP_SILENCE, playbackParameters.skipSilence);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** See {@link ExoCastPlayer#setTrackSelectionParameters(TrackSelectionParameters)}. */
|
|
||||||
public static final class SetTrackSelectionParameters extends ExoCastMessage {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The {@link ExoCastPlayer#setTrackSelectionParameters(TrackSelectionParameters) parameters} to
|
|
||||||
* set
|
|
||||||
*/
|
|
||||||
public final TrackSelectionParameters trackSelectionParameters;
|
|
||||||
|
|
||||||
public SetTrackSelectionParameters(TrackSelectionParameters trackSelectionParameters) {
|
|
||||||
super(METHOD_SET_TRACK_SELECTION_PARAMETERS);
|
|
||||||
this.trackSelectionParameters = trackSelectionParameters;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected JSONObject getArgumentsAsJsonObject() throws JSONException {
|
|
||||||
JSONArray disabledTextSelectionFlagsJson = new JSONArray();
|
|
||||||
int disabledSelectionFlags = trackSelectionParameters.disabledTextTrackSelectionFlags;
|
|
||||||
if ((disabledSelectionFlags & C.SELECTION_FLAG_AUTOSELECT) != 0) {
|
|
||||||
disabledTextSelectionFlagsJson.put(STR_SELECTION_FLAG_AUTOSELECT);
|
|
||||||
}
|
|
||||||
if ((disabledSelectionFlags & C.SELECTION_FLAG_FORCED) != 0) {
|
|
||||||
disabledTextSelectionFlagsJson.put(STR_SELECTION_FLAG_FORCED);
|
|
||||||
}
|
|
||||||
if ((disabledSelectionFlags & C.SELECTION_FLAG_DEFAULT) != 0) {
|
|
||||||
disabledTextSelectionFlagsJson.put(STR_SELECTION_FLAG_DEFAULT);
|
|
||||||
}
|
|
||||||
return new JSONObject()
|
|
||||||
.put(KEY_PREFERRED_AUDIO_LANGUAGE, trackSelectionParameters.preferredAudioLanguage)
|
|
||||||
.put(KEY_PREFERRED_TEXT_LANGUAGE, trackSelectionParameters.preferredTextLanguage)
|
|
||||||
.put(KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS, disabledTextSelectionFlagsJson)
|
|
||||||
.put(
|
|
||||||
KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE,
|
|
||||||
trackSelectionParameters.selectUndeterminedTextLanguage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public final String method;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a message with the given method.
|
|
||||||
*
|
|
||||||
* @param method The method of the message.
|
|
||||||
*/
|
|
||||||
protected ExoCastMessage(String method) {
|
|
||||||
this.method = method;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a string containing a JSON representation of this message.
|
|
||||||
*
|
|
||||||
* @param sequenceNumber The sequence number to associate with this message.
|
|
||||||
* @return A string containing a JSON representation of this message.
|
|
||||||
*/
|
|
||||||
public final String toJsonString(long sequenceNumber) {
|
|
||||||
try {
|
|
||||||
JSONObject message =
|
|
||||||
new JSONObject()
|
|
||||||
.put(KEY_PROTOCOL_VERSION, PROTOCOL_VERSION)
|
|
||||||
.put(KEY_METHOD, method)
|
|
||||||
.put(KEY_SEQUENCE_NUMBER, sequenceNumber)
|
|
||||||
.put(KEY_ARGS, getArgumentsAsJsonObject());
|
|
||||||
return message.toString();
|
|
||||||
} catch (JSONException e) {
|
|
||||||
throw new AssertionError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns a {@link JSONObject} representation of the given item. */
|
|
||||||
protected static JSONObject mediaItemAsJsonObject(MediaItem item) throws JSONException {
|
|
||||||
JSONObject itemAsJson = new JSONObject();
|
|
||||||
itemAsJson.put(KEY_UUID, item.uuid);
|
|
||||||
itemAsJson.put(KEY_TITLE, item.title);
|
|
||||||
itemAsJson.put(KEY_DESCRIPTION, item.description);
|
|
||||||
itemAsJson.put(KEY_MEDIA, uriBundleAsJsonObject(item.media));
|
|
||||||
// TODO(Internal b/118431961): Add attachment management.
|
|
||||||
|
|
||||||
JSONArray drmSchemesAsJson = new JSONArray();
|
|
||||||
for (MediaItem.DrmScheme drmScheme : item.drmSchemes) {
|
|
||||||
JSONObject drmSchemeAsJson = new JSONObject();
|
|
||||||
drmSchemeAsJson.put(KEY_UUID, drmScheme.uuid);
|
|
||||||
if (drmScheme.licenseServer != null) {
|
|
||||||
drmSchemeAsJson.put(KEY_LICENSE_SERVER, uriBundleAsJsonObject(drmScheme.licenseServer));
|
|
||||||
}
|
|
||||||
drmSchemesAsJson.put(drmSchemeAsJson);
|
|
||||||
}
|
|
||||||
itemAsJson.put(KEY_DRM_SCHEMES, drmSchemesAsJson);
|
|
||||||
maybePutValue(itemAsJson, KEY_START_POSITION_US, item.startPositionUs, C.TIME_UNSET);
|
|
||||||
maybePutValue(itemAsJson, KEY_END_POSITION_US, item.endPositionUs, C.TIME_UNSET);
|
|
||||||
itemAsJson.put(KEY_MIME_TYPE, item.mimeType);
|
|
||||||
return itemAsJson;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns a {@link JSONObject JSON object} containing the arguments of the message. */
|
|
||||||
protected abstract JSONObject getArgumentsAsJsonObject() throws JSONException;
|
|
||||||
|
|
||||||
/** Returns a JSON representation of the given {@link UriBundle}. */
|
|
||||||
protected static JSONObject uriBundleAsJsonObject(UriBundle uriBundle) throws JSONException {
|
|
||||||
return new JSONObject()
|
|
||||||
.put(KEY_URI, uriBundle.uri)
|
|
||||||
.put(KEY_REQUEST_HEADERS, new JSONObject(uriBundle.requestHeaders));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static JSONArray getShuffleOrderAsJson(ShuffleOrder shuffleOrder) {
|
|
||||||
JSONArray shuffleOrderJson = new JSONArray();
|
|
||||||
int index = shuffleOrder.getFirstIndex();
|
|
||||||
while (index != C.INDEX_UNSET) {
|
|
||||||
shuffleOrderJson.put(index);
|
|
||||||
index = shuffleOrder.getNextIndex(index);
|
|
||||||
}
|
|
||||||
return shuffleOrderJson;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void maybePutValue(JSONObject target, String key, long value, long unsetValue)
|
|
||||||
throws JSONException {
|
|
||||||
if (value != unsetValue) {
|
|
||||||
target.put(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.exoplayer2.ext.cast;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import com.google.android.gms.cast.framework.CastOptions;
|
|
||||||
import com.google.android.gms.cast.framework.OptionsProvider;
|
|
||||||
import com.google.android.gms.cast.framework.SessionProvider;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/** Cast options provider to target ExoPlayer's custom receiver app. */
|
|
||||||
public final class ExoCastOptionsProvider implements OptionsProvider {
|
|
||||||
|
|
||||||
public static final String RECEIVER_ID = "365DCC88";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CastOptions getCastOptions(Context context) {
|
|
||||||
return new CastOptions.Builder().setReceiverApplicationId(RECEIVER_ID).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nullable
|
|
||||||
public List<SessionProvider> getAdditionalSessionProviders(Context context) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,958 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.exoplayer2.ext.cast;
|
|
||||||
|
|
||||||
import android.os.Looper;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import com.google.android.exoplayer2.BasePlayer;
|
|
||||||
import com.google.android.exoplayer2.C;
|
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
|
||||||
import com.google.android.exoplayer2.IllegalSeekPositionException;
|
|
||||||
import com.google.android.exoplayer2.PlaybackParameters;
|
|
||||||
import com.google.android.exoplayer2.Player;
|
|
||||||
import com.google.android.exoplayer2.Timeline;
|
|
||||||
import com.google.android.exoplayer2.ext.cast.ExoCastMessage.AddItems;
|
|
||||||
import com.google.android.exoplayer2.ext.cast.ExoCastMessage.MoveItem;
|
|
||||||
import com.google.android.exoplayer2.ext.cast.ExoCastMessage.RemoveItems;
|
|
||||||
import com.google.android.exoplayer2.ext.cast.ExoCastMessage.SetRepeatMode;
|
|
||||||
import com.google.android.exoplayer2.ext.cast.ExoCastMessage.SetShuffleModeEnabled;
|
|
||||||
import com.google.android.exoplayer2.ext.cast.ExoCastMessage.SetTrackSelectionParameters;
|
|
||||||
import com.google.android.exoplayer2.ext.cast.ExoCastTimeline.PeriodUid;
|
|
||||||
import com.google.android.exoplayer2.source.ShuffleOrder;
|
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelectionParameters;
|
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
|
||||||
import com.google.android.exoplayer2.util.Clock;
|
|
||||||
import com.google.android.exoplayer2.util.Util;
|
|
||||||
import java.util.ArrayDeque;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
|
||||||
import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Plays media in a Cast receiver app that implements the ExoCast message protocol.
|
|
||||||
*
|
|
||||||
* <p>The ExoCast communication protocol consists in exchanging serialized {@link ExoCastMessage
|
|
||||||
* ExoCastMessages} and {@link ReceiverAppStateUpdate receiver app state updates}.
|
|
||||||
*
|
|
||||||
* <p>All methods in this class must be invoked on the main thread. Operations that change the state
|
|
||||||
* of the receiver app are masked locally as if their effect was immediate in the receiver app.
|
|
||||||
*
|
|
||||||
* <p>Methods that change the state of the player must only be invoked when a session is available,
|
|
||||||
* according to {@link CastSessionManager#isCastSessionAvailable()}.
|
|
||||||
*/
|
|
||||||
public final class ExoCastPlayer extends BasePlayer {
|
|
||||||
|
|
||||||
private static final String TAG = "ExoCastPlayer";
|
|
||||||
|
|
||||||
private static final int RENDERER_COUNT = 4;
|
|
||||||
private static final int RENDERER_INDEX_VIDEO = 0;
|
|
||||||
private static final int RENDERER_INDEX_AUDIO = 1;
|
|
||||||
private static final int RENDERER_INDEX_TEXT = 2;
|
|
||||||
private static final int RENDERER_INDEX_METADATA = 3;
|
|
||||||
|
|
||||||
private final Clock clock;
|
|
||||||
private final CastSessionManager castSessionManager;
|
|
||||||
private final CopyOnWriteArrayList<ListenerHolder> listeners;
|
|
||||||
private final ArrayList<ListenerNotificationTask> notificationsBatch;
|
|
||||||
private final ArrayDeque<ListenerNotificationTask> ongoingNotificationsTasks;
|
|
||||||
private final Timeline.Period scratchPeriod;
|
|
||||||
@Nullable private SessionAvailabilityListener sessionAvailabilityListener;
|
|
||||||
|
|
||||||
// Player state.
|
|
||||||
|
|
||||||
private final List<MediaItem> mediaItems;
|
|
||||||
private final StateHolder<ExoCastTimeline> currentTimeline;
|
|
||||||
private ShuffleOrder currentShuffleOrder;
|
|
||||||
|
|
||||||
private final StateHolder<Integer> playbackState;
|
|
||||||
private final StateHolder<Boolean> playWhenReady;
|
|
||||||
private final StateHolder<Integer> repeatMode;
|
|
||||||
private final StateHolder<Boolean> shuffleModeEnabled;
|
|
||||||
private final StateHolder<Boolean> isLoading;
|
|
||||||
private final StateHolder<PlaybackParameters> playbackParameters;
|
|
||||||
private final StateHolder<TrackSelectionParameters> trackselectionParameters;
|
|
||||||
private final StateHolder<TrackGroupArray> currentTrackGroups;
|
|
||||||
private final StateHolder<TrackSelectionArray> currentTrackSelections;
|
|
||||||
private final StateHolder<@NullableType Object> currentManifest;
|
|
||||||
private final StateHolder<@NullableType PeriodUid> currentPeriodUid;
|
|
||||||
private final StateHolder<Long> playbackPositionMs;
|
|
||||||
private final HashMap<UUID, MediaItemInfo> currentMediaItemInfoMap;
|
|
||||||
private long lastPlaybackPositionChangeTimeMs;
|
|
||||||
@Nullable private ExoPlaybackException playbackError;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an instance using the system clock for calculating time deltas.
|
|
||||||
*
|
|
||||||
* @param castSessionManagerFactory Factory to create the {@link CastSessionManager}.
|
|
||||||
*/
|
|
||||||
public ExoCastPlayer(CastSessionManager.Factory castSessionManagerFactory) {
|
|
||||||
this(castSessionManagerFactory, Clock.DEFAULT);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an instance using a custom {@link Clock} implementation.
|
|
||||||
*
|
|
||||||
* @param castSessionManagerFactory Factory to create the {@link CastSessionManager}.
|
|
||||||
* @param clock The clock to use for time delta calculations.
|
|
||||||
*/
|
|
||||||
public ExoCastPlayer(CastSessionManager.Factory castSessionManagerFactory, Clock clock) {
|
|
||||||
this.clock = clock;
|
|
||||||
castSessionManager = castSessionManagerFactory.create(new SessionManagerStateListener());
|
|
||||||
listeners = new CopyOnWriteArrayList<>();
|
|
||||||
notificationsBatch = new ArrayList<>();
|
|
||||||
ongoingNotificationsTasks = new ArrayDeque<>();
|
|
||||||
scratchPeriod = new Timeline.Period();
|
|
||||||
mediaItems = new ArrayList<>();
|
|
||||||
currentShuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ mediaItems.size());
|
|
||||||
playbackState = new StateHolder<>(STATE_IDLE);
|
|
||||||
playWhenReady = new StateHolder<>(false);
|
|
||||||
repeatMode = new StateHolder<>(REPEAT_MODE_OFF);
|
|
||||||
shuffleModeEnabled = new StateHolder<>(false);
|
|
||||||
isLoading = new StateHolder<>(false);
|
|
||||||
playbackParameters = new StateHolder<>(PlaybackParameters.DEFAULT);
|
|
||||||
trackselectionParameters = new StateHolder<>(TrackSelectionParameters.DEFAULT);
|
|
||||||
currentTrackGroups = new StateHolder<>(TrackGroupArray.EMPTY);
|
|
||||||
currentTrackSelections = new StateHolder<>(new TrackSelectionArray(null, null, null, null));
|
|
||||||
currentManifest = new StateHolder<>(null);
|
|
||||||
currentTimeline = new StateHolder<>(ExoCastTimeline.EMPTY);
|
|
||||||
playbackPositionMs = new StateHolder<>(0L);
|
|
||||||
currentPeriodUid = new StateHolder<>(null);
|
|
||||||
currentMediaItemInfoMap = new HashMap<>();
|
|
||||||
castSessionManager.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns whether a Cast session is available. */
|
|
||||||
public boolean isCastSessionAvailable() {
|
|
||||||
return castSessionManager.isCastSessionAvailable();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets a listener for updates on the Cast session availability.
|
|
||||||
*
|
|
||||||
* @param listener The {@link SessionAvailabilityListener}.
|
|
||||||
*/
|
|
||||||
public void setSessionAvailabilityListener(@Nullable SessionAvailabilityListener listener) {
|
|
||||||
sessionAvailabilityListener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prepares the player for playback.
|
|
||||||
*
|
|
||||||
* <p>Sends a preparation message to the receiver. If the player is in {@link #STATE_IDLE},
|
|
||||||
* updates the timeline with the media queue contents.
|
|
||||||
*/
|
|
||||||
public void prepare() {
|
|
||||||
long sequence = castSessionManager.send(new ExoCastMessage.Prepare());
|
|
||||||
if (playbackState.value == STATE_IDLE) {
|
|
||||||
playbackState.sequence = sequence;
|
|
||||||
setPlaybackStateInternal(mediaItems.isEmpty() ? STATE_ENDED : STATE_BUFFERING);
|
|
||||||
if (!currentTimeline.value.representsMediaQueue(
|
|
||||||
mediaItems, currentMediaItemInfoMap, currentShuffleOrder)) {
|
|
||||||
updateTimelineInternal(TIMELINE_CHANGE_REASON_PREPARED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
flushNotifications();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the item at the given index.
|
|
||||||
*
|
|
||||||
* @param index The index of the item to retrieve.
|
|
||||||
* @return The item at the given index.
|
|
||||||
*/
|
|
||||||
public MediaItem getQueueItem(int index) {
|
|
||||||
return mediaItems.get(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Equivalent to {@link #addItemsToQueue(int, MediaItem...) addItemsToQueue(C.INDEX_UNSET,
|
|
||||||
* items)}.
|
|
||||||
*/
|
|
||||||
public void addItemsToQueue(MediaItem... items) {
|
|
||||||
addItemsToQueue(C.INDEX_UNSET, items);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds the given sequence of items to the queue at the given position, so that the first of
|
|
||||||
* {@code items} is placed at the given index.
|
|
||||||
*
|
|
||||||
* <p>This method discards {@code items} with a uuid that already appears in the media queue. This
|
|
||||||
* method does nothing if {@code items} contains no new items.
|
|
||||||
*
|
|
||||||
* @param optionalIndex The index at which {@code items} will be inserted. If {@link
|
|
||||||
* C#INDEX_UNSET} is passed, the items are appended to the media queue.
|
|
||||||
* @param items The sequence of items to append. {@code items} must not contain items with
|
|
||||||
* matching uuids.
|
|
||||||
* @throws IllegalArgumentException If two or more elements in {@code items} contain matching
|
|
||||||
* uuids.
|
|
||||||
*/
|
|
||||||
public void addItemsToQueue(int optionalIndex, MediaItem... items) {
|
|
||||||
// Filter out items whose uuid already appears in the queue.
|
|
||||||
ArrayList<MediaItem> itemsToAdd = new ArrayList<>();
|
|
||||||
HashSet<UUID> addedUuids = new HashSet<>();
|
|
||||||
for (MediaItem item : items) {
|
|
||||||
Assertions.checkArgument(
|
|
||||||
addedUuids.add(item.uuid), "Added items must contain distinct uuids");
|
|
||||||
if (playbackState.value == STATE_IDLE
|
|
||||||
|| currentTimeline.value.getWindowIndexFromUuid(item.uuid) == C.INDEX_UNSET) {
|
|
||||||
// Prevent adding items that exist in the timeline. If the player is not yet prepared,
|
|
||||||
// ignore this check, since the timeline may not reflect the current media queue.
|
|
||||||
// Preparation will filter any duplicates.
|
|
||||||
itemsToAdd.add(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (itemsToAdd.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int normalizedIndex;
|
|
||||||
if (optionalIndex != C.INDEX_UNSET) {
|
|
||||||
normalizedIndex = optionalIndex;
|
|
||||||
mediaItems.addAll(optionalIndex, itemsToAdd);
|
|
||||||
} else {
|
|
||||||
normalizedIndex = mediaItems.size();
|
|
||||||
mediaItems.addAll(itemsToAdd);
|
|
||||||
}
|
|
||||||
currentShuffleOrder = currentShuffleOrder.cloneAndInsert(normalizedIndex, itemsToAdd.size());
|
|
||||||
long sequence =
|
|
||||||
castSessionManager.send(new AddItems(optionalIndex, itemsToAdd, currentShuffleOrder));
|
|
||||||
if (playbackState.value != STATE_IDLE) {
|
|
||||||
currentTimeline.sequence = sequence;
|
|
||||||
updateTimelineInternal(TIMELINE_CHANGE_REASON_DYNAMIC);
|
|
||||||
}
|
|
||||||
flushNotifications();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Moves an existing item within the queue.
|
|
||||||
*
|
|
||||||
* <p>Calling this method is equivalent to removing the item at position {@code indexFrom} and
|
|
||||||
* immediately inserting it at position {@code indexTo}. If the moved item is being played at the
|
|
||||||
* moment of the invocation, playback will stick with the moved item.
|
|
||||||
*
|
|
||||||
* @param index The index of the item to move.
|
|
||||||
* @param newIndex The index at which the item will be placed after this operation.
|
|
||||||
*/
|
|
||||||
public void moveItemInQueue(int index, int newIndex) {
|
|
||||||
MediaItem movedItem = mediaItems.remove(index);
|
|
||||||
mediaItems.add(newIndex, movedItem);
|
|
||||||
currentShuffleOrder =
|
|
||||||
currentShuffleOrder
|
|
||||||
.cloneAndRemove(index, index + 1)
|
|
||||||
.cloneAndInsert(newIndex, /* insertionCount= */ 1);
|
|
||||||
long sequence =
|
|
||||||
castSessionManager.send(new MoveItem(movedItem.uuid, newIndex, currentShuffleOrder));
|
|
||||||
if (playbackState.value != STATE_IDLE) {
|
|
||||||
currentTimeline.sequence = sequence;
|
|
||||||
updateTimelineInternal(TIMELINE_CHANGE_REASON_DYNAMIC);
|
|
||||||
}
|
|
||||||
flushNotifications();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes an item from the queue.
|
|
||||||
*
|
|
||||||
* @param index The index of the item to remove from the queue.
|
|
||||||
*/
|
|
||||||
public void removeItemFromQueue(int index) {
|
|
||||||
removeRangeFromQueue(index, index + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a range of items from the queue.
|
|
||||||
*
|
|
||||||
* <p>If the currently-playing item is removed, the playback position moves to the item following
|
|
||||||
* the removed range. If no item follows the removed range, the position is set to the last item
|
|
||||||
* in the queue and the player state transitions to {@link #STATE_ENDED}. Does nothing if an empty
|
|
||||||
* range ({@code from == exclusiveTo}) is passed.
|
|
||||||
*
|
|
||||||
* @param indexFrom The inclusive index at which the range to remove starts.
|
|
||||||
* @param indexExclusiveTo The exclusive index at which the range to remove ends.
|
|
||||||
*/
|
|
||||||
public void removeRangeFromQueue(int indexFrom, int indexExclusiveTo) {
|
|
||||||
UUID[] uuidsToRemove = new UUID[indexExclusiveTo - indexFrom];
|
|
||||||
for (int i = 0; i < uuidsToRemove.length; i++) {
|
|
||||||
uuidsToRemove[i] = mediaItems.get(i + indexFrom).uuid;
|
|
||||||
}
|
|
||||||
|
|
||||||
int windowIndexBeforeRemoval = getCurrentWindowIndex();
|
|
||||||
boolean currentItemWasRemoved =
|
|
||||||
windowIndexBeforeRemoval >= indexFrom && windowIndexBeforeRemoval < indexExclusiveTo;
|
|
||||||
boolean shouldTransitionToEnded =
|
|
||||||
currentItemWasRemoved && indexExclusiveTo == mediaItems.size();
|
|
||||||
|
|
||||||
Util.removeRange(mediaItems, indexFrom, indexExclusiveTo);
|
|
||||||
long sequence = castSessionManager.send(new RemoveItems(Arrays.asList(uuidsToRemove)));
|
|
||||||
currentShuffleOrder = currentShuffleOrder.cloneAndRemove(indexFrom, indexExclusiveTo);
|
|
||||||
|
|
||||||
if (playbackState.value != STATE_IDLE) {
|
|
||||||
currentTimeline.sequence = sequence;
|
|
||||||
updateTimelineInternal(TIMELINE_CHANGE_REASON_DYNAMIC);
|
|
||||||
if (currentItemWasRemoved) {
|
|
||||||
int newWindowIndex = Math.max(0, indexFrom - (shouldTransitionToEnded ? 1 : 0));
|
|
||||||
PeriodUid periodUid =
|
|
||||||
currentTimeline.value.isEmpty()
|
|
||||||
? null
|
|
||||||
: (PeriodUid)
|
|
||||||
currentTimeline.value.getPeriodPosition(
|
|
||||||
window,
|
|
||||||
scratchPeriod,
|
|
||||||
newWindowIndex,
|
|
||||||
/* windowPositionUs= */ C.TIME_UNSET)
|
|
||||||
.first;
|
|
||||||
currentPeriodUid.sequence = sequence;
|
|
||||||
playbackPositionMs.sequence = sequence;
|
|
||||||
setPlaybackPositionInternal(
|
|
||||||
periodUid,
|
|
||||||
/* positionMs= */ C.TIME_UNSET,
|
|
||||||
/* discontinuityReason= */ DISCONTINUITY_REASON_SEEK);
|
|
||||||
}
|
|
||||||
playbackState.sequence = sequence;
|
|
||||||
setPlaybackStateInternal(shouldTransitionToEnded ? STATE_ENDED : STATE_BUFFERING);
|
|
||||||
}
|
|
||||||
flushNotifications();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Removes all items in the queue. */
|
|
||||||
public void clearQueue() {
|
|
||||||
removeRangeFromQueue(0, getQueueSize());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the number of items in this queue. */
|
|
||||||
public int getQueueSize() {
|
|
||||||
return mediaItems.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track selection.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides a set of constrains for the receiver app to execute track selection.
|
|
||||||
*
|
|
||||||
* <p>{@link TrackSelectionParameters} passed to this method may be {@link
|
|
||||||
* TrackSelectionParameters#buildUpon() built upon} by this player as a result of a remote
|
|
||||||
* operation, which means {@link TrackSelectionParameters} obtained from {@link
|
|
||||||
* #getTrackSelectionParameters()} may have field differences with {@code parameters} passed to
|
|
||||||
* this method. However, only fields modified remotely will present differences. Other fields will
|
|
||||||
* remain unchanged.
|
|
||||||
*/
|
|
||||||
public void setTrackSelectionParameters(TrackSelectionParameters trackselectionParameters) {
|
|
||||||
this.trackselectionParameters.value = trackselectionParameters;
|
|
||||||
this.trackselectionParameters.sequence =
|
|
||||||
castSessionManager.send(new SetTrackSelectionParameters(trackselectionParameters));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the current {@link TrackSelectionParameters}. See {@link
|
|
||||||
* #setTrackSelectionParameters(TrackSelectionParameters)}.
|
|
||||||
*/
|
|
||||||
public TrackSelectionParameters getTrackSelectionParameters() {
|
|
||||||
return trackselectionParameters.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Player Implementation.
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nullable
|
|
||||||
public AudioComponent getAudioComponent() {
|
|
||||||
// TODO: Implement volume controls using the audio component.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nullable
|
|
||||||
public VideoComponent getVideoComponent() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nullable
|
|
||||||
public TextComponent getTextComponent() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nullable
|
|
||||||
public MetadataComponent getMetadataComponent() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Looper getApplicationLooper() {
|
|
||||||
return Looper.getMainLooper();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void addListener(EventListener listener) {
|
|
||||||
listeners.addIfAbsent(new ListenerHolder(listener));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void removeListener(EventListener listener) {
|
|
||||||
for (ListenerHolder listenerHolder : listeners) {
|
|
||||||
if (listenerHolder.listener.equals(listener)) {
|
|
||||||
listenerHolder.release();
|
|
||||||
listeners.remove(listenerHolder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Player.State
|
|
||||||
public int getPlaybackState() {
|
|
||||||
return playbackState.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public ExoPlaybackException getPlaybackError() {
|
|
||||||
return playbackError;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setPlayWhenReady(boolean playWhenReady) {
|
|
||||||
this.playWhenReady.sequence =
|
|
||||||
castSessionManager.send(new ExoCastMessage.SetPlayWhenReady(playWhenReady));
|
|
||||||
// Take a snapshot of the playback position before pausing to ensure future calculations are
|
|
||||||
// correct.
|
|
||||||
setPlaybackPositionInternal(
|
|
||||||
currentPeriodUid.value, getCurrentPosition(), /* discontinuityReason= */ null);
|
|
||||||
setPlayWhenReadyInternal(playWhenReady);
|
|
||||||
flushNotifications();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean getPlayWhenReady() {
|
|
||||||
return playWhenReady.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setRepeatMode(@RepeatMode int repeatMode) {
|
|
||||||
this.repeatMode.sequence = castSessionManager.send(new SetRepeatMode(repeatMode));
|
|
||||||
setRepeatModeInternal(repeatMode);
|
|
||||||
flushNotifications();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@RepeatMode
|
|
||||||
public int getRepeatMode() {
|
|
||||||
return repeatMode.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setShuffleModeEnabled(boolean shuffleModeEnabled) {
|
|
||||||
this.shuffleModeEnabled.sequence =
|
|
||||||
castSessionManager.send(new SetShuffleModeEnabled(shuffleModeEnabled));
|
|
||||||
setShuffleModeEnabledInternal(shuffleModeEnabled);
|
|
||||||
flushNotifications();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean getShuffleModeEnabled() {
|
|
||||||
return shuffleModeEnabled.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isLoading() {
|
|
||||||
return isLoading.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void seekTo(int windowIndex, long positionMs) {
|
|
||||||
if (mediaItems.isEmpty()) {
|
|
||||||
// TODO: Handle seeking in empty timeline.
|
|
||||||
setPlaybackPositionInternal(/* periodUid= */ null, 0, DISCONTINUITY_REASON_SEEK);
|
|
||||||
return;
|
|
||||||
} else if (windowIndex >= mediaItems.size()) {
|
|
||||||
throw new IllegalSeekPositionException(currentTimeline.value, windowIndex, positionMs);
|
|
||||||
}
|
|
||||||
long sequence =
|
|
||||||
castSessionManager.send(
|
|
||||||
new ExoCastMessage.SeekTo(mediaItems.get(windowIndex).uuid, positionMs));
|
|
||||||
|
|
||||||
currentPeriodUid.sequence = sequence;
|
|
||||||
playbackPositionMs.sequence = sequence;
|
|
||||||
|
|
||||||
PeriodUid periodUid =
|
|
||||||
(PeriodUid)
|
|
||||||
currentTimeline.value.getPeriodPosition(
|
|
||||||
window, scratchPeriod, windowIndex, C.msToUs(positionMs))
|
|
||||||
.first;
|
|
||||||
setPlaybackPositionInternal(periodUid, positionMs, DISCONTINUITY_REASON_SEEK);
|
|
||||||
if (playbackState.value != STATE_IDLE) {
|
|
||||||
playbackState.sequence = sequence;
|
|
||||||
setPlaybackStateInternal(STATE_BUFFERING);
|
|
||||||
}
|
|
||||||
flushNotifications();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) {
|
|
||||||
playbackParameters =
|
|
||||||
playbackParameters != null ? playbackParameters : PlaybackParameters.DEFAULT;
|
|
||||||
this.playbackParameters.value = playbackParameters;
|
|
||||||
this.playbackParameters.sequence =
|
|
||||||
castSessionManager.send(new ExoCastMessage.SetPlaybackParameters(playbackParameters));
|
|
||||||
this.playbackParameters.value = playbackParameters;
|
|
||||||
// Note: This method, unlike others, does not immediately notify the change. See the Player
|
|
||||||
// interface for more information.
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public PlaybackParameters getPlaybackParameters() {
|
|
||||||
return playbackParameters.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void stop(boolean reset) {
|
|
||||||
long sequence = castSessionManager.send(new ExoCastMessage.Stop(reset));
|
|
||||||
playbackState.sequence = sequence;
|
|
||||||
setPlaybackStateInternal(STATE_IDLE);
|
|
||||||
if (reset) {
|
|
||||||
currentTimeline.sequence = sequence;
|
|
||||||
mediaItems.clear();
|
|
||||||
currentShuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length =*/ 0);
|
|
||||||
setPlaybackPositionInternal(
|
|
||||||
/* periodUid= */ null, /* positionMs= */ 0, DISCONTINUITY_REASON_INTERNAL);
|
|
||||||
updateTimelineInternal(TIMELINE_CHANGE_REASON_RESET);
|
|
||||||
}
|
|
||||||
flushNotifications();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void release() {
|
|
||||||
setSessionAvailabilityListener(null);
|
|
||||||
castSessionManager.stopTrackingSession();
|
|
||||||
flushNotifications();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getRendererCount() {
|
|
||||||
return RENDERER_COUNT;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getRendererType(int index) {
|
|
||||||
switch (index) {
|
|
||||||
case RENDERER_INDEX_VIDEO:
|
|
||||||
return C.TRACK_TYPE_VIDEO;
|
|
||||||
case RENDERER_INDEX_AUDIO:
|
|
||||||
return C.TRACK_TYPE_AUDIO;
|
|
||||||
case RENDERER_INDEX_TEXT:
|
|
||||||
return C.TRACK_TYPE_TEXT;
|
|
||||||
case RENDERER_INDEX_METADATA:
|
|
||||||
return C.TRACK_TYPE_METADATA;
|
|
||||||
default:
|
|
||||||
throw new IndexOutOfBoundsException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TrackGroupArray getCurrentTrackGroups() {
|
|
||||||
// TODO (Internal b/62080507): Implement using track information from currentMediaItemInfoMap.
|
|
||||||
return currentTrackGroups.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TrackSelectionArray getCurrentTrackSelections() {
|
|
||||||
// TODO (Internal b/62080507): Implement using track information from currentMediaItemInfoMap.
|
|
||||||
return currentTrackSelections.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nullable
|
|
||||||
public Object getCurrentManifest() {
|
|
||||||
// TODO (Internal b/62080507): Implement using track information from currentMediaItemInfoMap.
|
|
||||||
return currentManifest.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Timeline getCurrentTimeline() {
|
|
||||||
return currentTimeline.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getCurrentPeriodIndex() {
|
|
||||||
int periodIndex =
|
|
||||||
currentPeriodUid.value == null
|
|
||||||
? C.INDEX_UNSET
|
|
||||||
: currentTimeline.value.getIndexOfPeriod(currentPeriodUid.value);
|
|
||||||
return periodIndex != C.INDEX_UNSET ? periodIndex : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getCurrentWindowIndex() {
|
|
||||||
int windowIndex =
|
|
||||||
currentPeriodUid.value == null
|
|
||||||
? C.INDEX_UNSET
|
|
||||||
: currentTimeline.value.getWindowIndexContainingPeriod(currentPeriodUid.value);
|
|
||||||
return windowIndex != C.INDEX_UNSET ? windowIndex : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getDuration() {
|
|
||||||
return getContentDuration();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getCurrentPosition() {
|
|
||||||
return playbackPositionMs.value
|
|
||||||
+ (getPlaybackState() == STATE_READY && getPlayWhenReady()
|
|
||||||
? projectPlaybackTimeElapsedMs()
|
|
||||||
: 0L);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getBufferedPosition() {
|
|
||||||
return getCurrentPosition();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getTotalBufferedDuration() {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isPlayingAd() {
|
|
||||||
// TODO (Internal b/119293631): Add support for ads.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getCurrentAdGroupIndex() {
|
|
||||||
return C.INDEX_UNSET;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getCurrentAdIndexInAdGroup() {
|
|
||||||
return C.INDEX_UNSET;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getContentPosition() {
|
|
||||||
return getCurrentPosition();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getContentBufferedPosition() {
|
|
||||||
return getCurrentPosition();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Local state modifications.
|
|
||||||
|
|
||||||
private void setPlayWhenReadyInternal(boolean playWhenReady) {
|
|
||||||
if (this.playWhenReady.value != playWhenReady) {
|
|
||||||
this.playWhenReady.value = playWhenReady;
|
|
||||||
notificationsBatch.add(
|
|
||||||
new ListenerNotificationTask(
|
|
||||||
listener -> listener.onPlayerStateChanged(playWhenReady, playbackState.value)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setPlaybackStateInternal(int playbackState) {
|
|
||||||
if (this.playbackState.value != playbackState) {
|
|
||||||
if (this.playbackState.value == STATE_IDLE) {
|
|
||||||
// We are transitioning out of STATE_IDLE. We clear any errors.
|
|
||||||
setPlaybackErrorInternal(null);
|
|
||||||
}
|
|
||||||
this.playbackState.value = playbackState;
|
|
||||||
notificationsBatch.add(
|
|
||||||
new ListenerNotificationTask(
|
|
||||||
listener -> listener.onPlayerStateChanged(playWhenReady.value, playbackState)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setRepeatModeInternal(int repeatMode) {
|
|
||||||
if (this.repeatMode.value != repeatMode) {
|
|
||||||
this.repeatMode.value = repeatMode;
|
|
||||||
notificationsBatch.add(
|
|
||||||
new ListenerNotificationTask(listener -> listener.onRepeatModeChanged(repeatMode)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setShuffleModeEnabledInternal(boolean shuffleModeEnabled) {
|
|
||||||
if (this.shuffleModeEnabled.value != shuffleModeEnabled) {
|
|
||||||
this.shuffleModeEnabled.value = shuffleModeEnabled;
|
|
||||||
notificationsBatch.add(
|
|
||||||
new ListenerNotificationTask(
|
|
||||||
listener -> listener.onShuffleModeEnabledChanged(shuffleModeEnabled)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setIsLoadingInternal(boolean isLoading) {
|
|
||||||
if (this.isLoading.value != isLoading) {
|
|
||||||
this.isLoading.value = isLoading;
|
|
||||||
notificationsBatch.add(
|
|
||||||
new ListenerNotificationTask(listener -> listener.onLoadingChanged(isLoading)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setPlaybackParametersInternal(PlaybackParameters playbackParameters) {
|
|
||||||
if (!this.playbackParameters.value.equals(playbackParameters)) {
|
|
||||||
this.playbackParameters.value = playbackParameters;
|
|
||||||
notificationsBatch.add(
|
|
||||||
new ListenerNotificationTask(
|
|
||||||
listener -> listener.onPlaybackParametersChanged(playbackParameters)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setPlaybackErrorInternal(@Nullable String errorMessage) {
|
|
||||||
if (errorMessage != null) {
|
|
||||||
playbackError = ExoPlaybackException.createForRemote(errorMessage);
|
|
||||||
notificationsBatch.add(
|
|
||||||
new ListenerNotificationTask(
|
|
||||||
listener -> listener.onPlayerError(Assertions.checkNotNull(playbackError))));
|
|
||||||
} else {
|
|
||||||
playbackError = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setPlaybackPositionInternal(
|
|
||||||
@Nullable PeriodUid periodUid, long positionMs, @Nullable Integer discontinuityReason) {
|
|
||||||
currentPeriodUid.value = periodUid;
|
|
||||||
if (periodUid == null) {
|
|
||||||
positionMs = 0L;
|
|
||||||
} else if (positionMs == C.TIME_UNSET) {
|
|
||||||
int windowIndex = currentTimeline.value.getWindowIndexContainingPeriod(periodUid);
|
|
||||||
if (windowIndex == C.INDEX_UNSET) {
|
|
||||||
positionMs = 0;
|
|
||||||
} else {
|
|
||||||
positionMs =
|
|
||||||
C.usToMs(
|
|
||||||
currentTimeline.value.getWindow(windowIndex, window, /* setTag= */ false)
|
|
||||||
.defaultPositionUs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
playbackPositionMs.value = positionMs;
|
|
||||||
lastPlaybackPositionChangeTimeMs = clock.elapsedRealtime();
|
|
||||||
if (discontinuityReason != null) {
|
|
||||||
notificationsBatch.add(
|
|
||||||
new ListenerNotificationTask(
|
|
||||||
listener -> listener.onPositionDiscontinuity(discontinuityReason)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Internal methods.
|
|
||||||
|
|
||||||
private void updateTimelineInternal(@TimelineChangeReason int changeReason) {
|
|
||||||
currentTimeline.value =
|
|
||||||
ExoCastTimeline.createTimelineFor(mediaItems, currentMediaItemInfoMap, currentShuffleOrder);
|
|
||||||
removeStaleMediaItemInfo();
|
|
||||||
notificationsBatch.add(
|
|
||||||
new ListenerNotificationTask(
|
|
||||||
listener ->
|
|
||||||
listener.onTimelineChanged(
|
|
||||||
currentTimeline.value, /* manifest= */ null, changeReason)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private long projectPlaybackTimeElapsedMs() {
|
|
||||||
return (long)
|
|
||||||
((clock.elapsedRealtime() - lastPlaybackPositionChangeTimeMs)
|
|
||||||
* playbackParameters.value.speed);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void flushNotifications() {
|
|
||||||
boolean recursiveNotification = !ongoingNotificationsTasks.isEmpty();
|
|
||||||
ongoingNotificationsTasks.addAll(notificationsBatch);
|
|
||||||
notificationsBatch.clear();
|
|
||||||
if (recursiveNotification) {
|
|
||||||
// This will be handled once the current notification task is finished.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
while (!ongoingNotificationsTasks.isEmpty()) {
|
|
||||||
ongoingNotificationsTasks.peekFirst().execute();
|
|
||||||
ongoingNotificationsTasks.removeFirst();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the current media item information by including any extra entries received from the
|
|
||||||
* receiver app.
|
|
||||||
*
|
|
||||||
* @param mediaItemsInformation A map of media item information received from the receiver app.
|
|
||||||
*/
|
|
||||||
private void updateMediaItemsInfo(Map<UUID, MediaItemInfo> mediaItemsInformation) {
|
|
||||||
for (Map.Entry<UUID, MediaItemInfo> entry : mediaItemsInformation.entrySet()) {
|
|
||||||
MediaItemInfo currentInfoForEntry = currentMediaItemInfoMap.get(entry.getKey());
|
|
||||||
boolean shouldPutEntry =
|
|
||||||
currentInfoForEntry == null || !currentInfoForEntry.equals(entry.getValue());
|
|
||||||
if (shouldPutEntry) {
|
|
||||||
currentMediaItemInfoMap.put(entry.getKey(), entry.getValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes stale media info entries. An entry is considered stale when the corresponding media
|
|
||||||
* item is not present in the current media queue.
|
|
||||||
*/
|
|
||||||
private void removeStaleMediaItemInfo() {
|
|
||||||
for (Iterator<UUID> iterator = currentMediaItemInfoMap.keySet().iterator();
|
|
||||||
iterator.hasNext(); ) {
|
|
||||||
UUID uuid = iterator.next();
|
|
||||||
if (currentTimeline.value.getWindowIndexFromUuid(uuid) == C.INDEX_UNSET) {
|
|
||||||
iterator.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Internal classes.
|
|
||||||
|
|
||||||
private class SessionManagerStateListener implements CastSessionManager.StateListener {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCastSessionAvailable() {
|
|
||||||
if (sessionAvailabilityListener != null) {
|
|
||||||
sessionAvailabilityListener.onCastSessionAvailable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCastSessionUnavailable() {
|
|
||||||
if (sessionAvailabilityListener != null) {
|
|
||||||
sessionAvailabilityListener.onCastSessionUnavailable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStateUpdateFromReceiverApp(ReceiverAppStateUpdate stateUpdate) {
|
|
||||||
long sequence = stateUpdate.sequenceNumber;
|
|
||||||
|
|
||||||
if (stateUpdate.errorMessage != null) {
|
|
||||||
setPlaybackErrorInternal(stateUpdate.errorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sequence >= playbackState.sequence && stateUpdate.playbackState != null) {
|
|
||||||
setPlaybackStateInternal(stateUpdate.playbackState);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sequence >= currentTimeline.sequence) {
|
|
||||||
if (stateUpdate.items != null) {
|
|
||||||
mediaItems.clear();
|
|
||||||
mediaItems.addAll(stateUpdate.items);
|
|
||||||
}
|
|
||||||
|
|
||||||
currentShuffleOrder =
|
|
||||||
stateUpdate.shuffleOrder != null
|
|
||||||
? new ShuffleOrder.DefaultShuffleOrder(
|
|
||||||
Util.toArray(stateUpdate.shuffleOrder), clock.elapsedRealtime())
|
|
||||||
: currentShuffleOrder;
|
|
||||||
updateMediaItemsInfo(stateUpdate.mediaItemsInformation);
|
|
||||||
|
|
||||||
if (playbackState.value != STATE_IDLE
|
|
||||||
&& !currentTimeline.value.representsMediaQueue(
|
|
||||||
mediaItems, currentMediaItemInfoMap, currentShuffleOrder)) {
|
|
||||||
updateTimelineInternal(TIMELINE_CHANGE_REASON_DYNAMIC);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sequence >= currentPeriodUid.sequence
|
|
||||||
&& stateUpdate.currentPlayingItemUuid != null
|
|
||||||
&& stateUpdate.currentPlaybackPositionMs != null) {
|
|
||||||
PeriodUid periodUid;
|
|
||||||
if (stateUpdate.currentPlayingPeriodId == null) {
|
|
||||||
int windowIndex =
|
|
||||||
currentTimeline.value.getWindowIndexFromUuid(stateUpdate.currentPlayingItemUuid);
|
|
||||||
periodUid =
|
|
||||||
(PeriodUid)
|
|
||||||
currentTimeline.value.getPeriodPosition(
|
|
||||||
window,
|
|
||||||
scratchPeriod,
|
|
||||||
windowIndex,
|
|
||||||
C.msToUs(stateUpdate.currentPlaybackPositionMs))
|
|
||||||
.first;
|
|
||||||
} else {
|
|
||||||
periodUid =
|
|
||||||
ExoCastTimeline.createPeriodUid(
|
|
||||||
stateUpdate.currentPlayingItemUuid, stateUpdate.currentPlayingPeriodId);
|
|
||||||
}
|
|
||||||
setPlaybackPositionInternal(
|
|
||||||
periodUid, stateUpdate.currentPlaybackPositionMs, stateUpdate.discontinuityReason);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sequence >= isLoading.sequence && stateUpdate.isLoading != null) {
|
|
||||||
setIsLoadingInternal(stateUpdate.isLoading);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sequence >= playWhenReady.sequence && stateUpdate.playWhenReady != null) {
|
|
||||||
setPlayWhenReadyInternal(stateUpdate.playWhenReady);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sequence >= shuffleModeEnabled.sequence && stateUpdate.shuffleModeEnabled != null) {
|
|
||||||
setShuffleModeEnabledInternal(stateUpdate.shuffleModeEnabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sequence >= repeatMode.sequence && stateUpdate.repeatMode != null) {
|
|
||||||
setRepeatModeInternal(stateUpdate.repeatMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sequence >= playbackParameters.sequence && stateUpdate.playbackParameters != null) {
|
|
||||||
setPlaybackParametersInternal(stateUpdate.playbackParameters);
|
|
||||||
}
|
|
||||||
|
|
||||||
TrackSelectionParameters parameters = stateUpdate.trackSelectionParameters;
|
|
||||||
if (sequence >= trackselectionParameters.sequence && parameters != null) {
|
|
||||||
trackselectionParameters.value =
|
|
||||||
trackselectionParameters
|
|
||||||
.value
|
|
||||||
.buildUpon()
|
|
||||||
.setDisabledTextTrackSelectionFlags(parameters.disabledTextTrackSelectionFlags)
|
|
||||||
.setPreferredAudioLanguage(parameters.preferredAudioLanguage)
|
|
||||||
.setPreferredTextLanguage(parameters.preferredTextLanguage)
|
|
||||||
.setSelectUndeterminedTextLanguage(parameters.selectUndeterminedTextLanguage)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
flushNotifications();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class StateHolder<T> {
|
|
||||||
|
|
||||||
public T value;
|
|
||||||
public long sequence;
|
|
||||||
|
|
||||||
public StateHolder(T initialValue) {
|
|
||||||
value = initialValue;
|
|
||||||
sequence = CastSessionManager.SEQUENCE_NUMBER_UNSET;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class ListenerNotificationTask {
|
|
||||||
|
|
||||||
private final Iterator<ListenerHolder> listenersSnapshot;
|
|
||||||
private final ListenerInvocation listenerInvocation;
|
|
||||||
|
|
||||||
private ListenerNotificationTask(ListenerInvocation listenerInvocation) {
|
|
||||||
this.listenersSnapshot = listeners.iterator();
|
|
||||||
this.listenerInvocation = listenerInvocation;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void execute() {
|
|
||||||
while (listenersSnapshot.hasNext()) {
|
|
||||||
listenersSnapshot.next().invoke(listenerInvocation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,342 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.exoplayer2.ext.cast;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import com.google.android.exoplayer2.C;
|
|
||||||
import com.google.android.exoplayer2.Player;
|
|
||||||
import com.google.android.exoplayer2.Timeline;
|
|
||||||
import com.google.android.exoplayer2.source.ShuffleOrder;
|
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
|
||||||
import com.google.android.exoplayer2.util.Util;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A {@link Timeline} for Cast receiver app media queues.
|
|
||||||
*
|
|
||||||
* <p>Each {@link MediaItem} in the timeline is exposed as a window. Unprepared media items are
|
|
||||||
* exposed as an unset-duration {@link Window}, with a single unset-duration {@link Period}.
|
|
||||||
*/
|
|
||||||
/* package */ final class ExoCastTimeline extends Timeline {
|
|
||||||
|
|
||||||
/** Opaque object that uniquely identifies a period across timeline changes. */
|
|
||||||
public interface PeriodUid {}
|
|
||||||
|
|
||||||
/** A timeline for an empty media queue. */
|
|
||||||
public static final ExoCastTimeline EMPTY =
|
|
||||||
createTimelineFor(
|
|
||||||
Collections.emptyList(), Collections.emptyMap(), new ShuffleOrder.DefaultShuffleOrder(0));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates {@link PeriodUid} from the given arguments.
|
|
||||||
*
|
|
||||||
* @param itemUuid The UUID that identifies the item.
|
|
||||||
* @param periodId The id of the period for which the unique identifier is required.
|
|
||||||
* @return An opaque unique identifier for a period.
|
|
||||||
*/
|
|
||||||
public static PeriodUid createPeriodUid(UUID itemUuid, Object periodId) {
|
|
||||||
return new PeriodUidImpl(itemUuid, periodId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a new timeline representing the given media queue information.
|
|
||||||
*
|
|
||||||
* @param mediaItems The media items conforming the timeline.
|
|
||||||
* @param mediaItemInfoMap Maps {@link MediaItem media items} in {@code mediaItems} to a {@link
|
|
||||||
* MediaItemInfo} through their {@link MediaItem#uuid}. Media items may not have a {@link
|
|
||||||
* MediaItemInfo} mapped to them.
|
|
||||||
* @param shuffleOrder The {@link ShuffleOrder} of the timeline. {@link ShuffleOrder#getLength()}
|
|
||||||
* must be equal to {@code mediaItems.size()}.
|
|
||||||
* @return A new timeline representing the given media queue information.
|
|
||||||
*/
|
|
||||||
public static ExoCastTimeline createTimelineFor(
|
|
||||||
List<MediaItem> mediaItems,
|
|
||||||
Map<UUID, MediaItemInfo> mediaItemInfoMap,
|
|
||||||
ShuffleOrder shuffleOrder) {
|
|
||||||
Assertions.checkArgument(mediaItems.size() == shuffleOrder.getLength());
|
|
||||||
int[] accumulativePeriodCount = new int[mediaItems.size()];
|
|
||||||
int periodCount = 0;
|
|
||||||
for (int i = 0; i < accumulativePeriodCount.length; i++) {
|
|
||||||
periodCount += getInfoOrEmpty(mediaItemInfoMap, mediaItems.get(i).uuid).periods.size();
|
|
||||||
accumulativePeriodCount[i] = periodCount;
|
|
||||||
}
|
|
||||||
HashMap<UUID, Integer> uuidToIndex = new HashMap<>();
|
|
||||||
for (int i = 0; i < mediaItems.size(); i++) {
|
|
||||||
uuidToIndex.put(mediaItems.get(i).uuid, i);
|
|
||||||
}
|
|
||||||
return new ExoCastTimeline(
|
|
||||||
Collections.unmodifiableList(new ArrayList<>(mediaItems)),
|
|
||||||
Collections.unmodifiableMap(new HashMap<>(mediaItemInfoMap)),
|
|
||||||
Collections.unmodifiableMap(new HashMap<>(uuidToIndex)),
|
|
||||||
shuffleOrder,
|
|
||||||
accumulativePeriodCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Timeline backing information.
|
|
||||||
private final List<MediaItem> mediaItems;
|
|
||||||
private final Map<UUID, MediaItemInfo> mediaItemInfoMap;
|
|
||||||
private final ShuffleOrder shuffleOrder;
|
|
||||||
|
|
||||||
// Precomputed for quick access.
|
|
||||||
private final Map<UUID, Integer> uuidToIndex;
|
|
||||||
private final int[] accumulativePeriodCount;
|
|
||||||
|
|
||||||
private ExoCastTimeline(
|
|
||||||
List<MediaItem> mediaItems,
|
|
||||||
Map<UUID, MediaItemInfo> mediaItemInfoMap,
|
|
||||||
Map<UUID, Integer> uuidToIndex,
|
|
||||||
ShuffleOrder shuffleOrder,
|
|
||||||
int[] accumulativePeriodCount) {
|
|
||||||
this.mediaItems = mediaItems;
|
|
||||||
this.mediaItemInfoMap = mediaItemInfoMap;
|
|
||||||
this.uuidToIndex = uuidToIndex;
|
|
||||||
this.shuffleOrder = shuffleOrder;
|
|
||||||
this.accumulativePeriodCount = accumulativePeriodCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether the given media queue information would produce a timeline equivalent to this
|
|
||||||
* one.
|
|
||||||
*
|
|
||||||
* @see ExoCastTimeline#createTimelineFor(List, Map, ShuffleOrder)
|
|
||||||
*/
|
|
||||||
public boolean representsMediaQueue(
|
|
||||||
List<MediaItem> mediaItems,
|
|
||||||
Map<UUID, MediaItemInfo> mediaItemInfoMap,
|
|
||||||
ShuffleOrder shuffleOrder) {
|
|
||||||
if (this.shuffleOrder.getLength() != shuffleOrder.getLength()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
int index = shuffleOrder.getFirstIndex();
|
|
||||||
if (this.shuffleOrder.getFirstIndex() != index) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
while (index != C.INDEX_UNSET) {
|
|
||||||
int nextIndex = shuffleOrder.getNextIndex(index);
|
|
||||||
if (nextIndex != this.shuffleOrder.getNextIndex(index)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
index = nextIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mediaItems.size() != this.mediaItems.size()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for (int i = 0; i < mediaItems.size(); i++) {
|
|
||||||
UUID uuid = mediaItems.get(i).uuid;
|
|
||||||
MediaItemInfo mediaItemInfo = getInfoOrEmpty(mediaItemInfoMap, uuid);
|
|
||||||
if (!uuid.equals(this.mediaItems.get(i).uuid)
|
|
||||||
|| !mediaItemInfo.equals(getInfoOrEmpty(this.mediaItemInfoMap, uuid))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the index of the window that contains the period identified by the given {@code
|
|
||||||
* periodUid} or {@link C#INDEX_UNSET} if this timeline does not contain any period with the given
|
|
||||||
* {@code periodUid}.
|
|
||||||
*/
|
|
||||||
public int getWindowIndexContainingPeriod(PeriodUid periodUid) {
|
|
||||||
if (!(periodUid instanceof PeriodUidImpl)) {
|
|
||||||
return C.INDEX_UNSET;
|
|
||||||
}
|
|
||||||
return getWindowIndexFromUuid(((PeriodUidImpl) periodUid).itemUuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the index of the window that represents the media item with the given {@code uuid} or
|
|
||||||
* {@link C#INDEX_UNSET} if no item in this timeline has the given {@code uuid}.
|
|
||||||
*/
|
|
||||||
public int getWindowIndexFromUuid(UUID uuid) {
|
|
||||||
Integer index = uuidToIndex.get(uuid);
|
|
||||||
return index != null ? index : C.INDEX_UNSET;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Timeline implementation.
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getWindowCount() {
|
|
||||||
return mediaItems.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Window getWindow(
|
|
||||||
int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) {
|
|
||||||
MediaItem mediaItem = mediaItems.get(windowIndex);
|
|
||||||
MediaItemInfo mediaItemInfo = getInfoOrEmpty(mediaItemInfoMap, mediaItem.uuid);
|
|
||||||
return window.set(
|
|
||||||
/* tag= */ setTag ? mediaItem.attachment : null,
|
|
||||||
/* presentationStartTimeMs= */ C.TIME_UNSET,
|
|
||||||
/* windowStartTimeMs= */ C.TIME_UNSET,
|
|
||||||
/* isSeekable= */ mediaItemInfo.isSeekable,
|
|
||||||
/* isDynamic= */ mediaItemInfo.isDynamic,
|
|
||||||
/* defaultPositionUs= */ mediaItemInfo.defaultStartPositionUs,
|
|
||||||
/* durationUs= */ mediaItemInfo.windowDurationUs,
|
|
||||||
/* firstPeriodIndex= */ windowIndex == 0 ? 0 : accumulativePeriodCount[windowIndex - 1],
|
|
||||||
/* lastPeriodIndex= */ accumulativePeriodCount[windowIndex] - 1,
|
|
||||||
mediaItemInfo.positionInFirstPeriodUs);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getPeriodCount() {
|
|
||||||
return mediaItems.isEmpty() ? 0 : accumulativePeriodCount[accumulativePeriodCount.length - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Period getPeriodByUid(Object periodUidObject, Period period) {
|
|
||||||
return getPeriodInternal((PeriodUidImpl) periodUidObject, period, /* setIds= */ true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
|
|
||||||
return getPeriodInternal((PeriodUidImpl) getUidOfPeriod(periodIndex), period, setIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getIndexOfPeriod(Object uid) {
|
|
||||||
if (!(uid instanceof PeriodUidImpl)) {
|
|
||||||
return C.INDEX_UNSET;
|
|
||||||
}
|
|
||||||
PeriodUidImpl periodUid = (PeriodUidImpl) uid;
|
|
||||||
UUID uuid = periodUid.itemUuid;
|
|
||||||
Integer itemIndex = uuidToIndex.get(uuid);
|
|
||||||
if (itemIndex == null) {
|
|
||||||
return C.INDEX_UNSET;
|
|
||||||
}
|
|
||||||
int indexOfPeriodInItem =
|
|
||||||
getInfoOrEmpty(mediaItemInfoMap, uuid).getIndexOfPeriod(periodUid.periodId);
|
|
||||||
if (indexOfPeriodInItem == C.INDEX_UNSET) {
|
|
||||||
return C.INDEX_UNSET;
|
|
||||||
}
|
|
||||||
return indexOfPeriodInItem + (itemIndex == 0 ? 0 : accumulativePeriodCount[itemIndex - 1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public PeriodUid getUidOfPeriod(int periodIndex) {
|
|
||||||
int mediaItemIndex = getMediaItemIndexForPeriodIndex(periodIndex);
|
|
||||||
int periodIndexInMediaItem =
|
|
||||||
periodIndex - (mediaItemIndex > 0 ? accumulativePeriodCount[mediaItemIndex - 1] : 0);
|
|
||||||
UUID uuid = mediaItems.get(mediaItemIndex).uuid;
|
|
||||||
MediaItemInfo mediaItemInfo = getInfoOrEmpty(mediaItemInfoMap, uuid);
|
|
||||||
return new PeriodUidImpl(uuid, mediaItemInfo.periods.get(periodIndexInMediaItem).id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getFirstWindowIndex(boolean shuffleModeEnabled) {
|
|
||||||
return shuffleModeEnabled ? shuffleOrder.getFirstIndex() : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getLastWindowIndex(boolean shuffleModeEnabled) {
|
|
||||||
return shuffleModeEnabled ? shuffleOrder.getLastIndex() : mediaItems.size() - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getPreviousWindowIndex(int windowIndex, int repeatMode, boolean shuffleModeEnabled) {
|
|
||||||
if (repeatMode == Player.REPEAT_MODE_ONE) {
|
|
||||||
return windowIndex;
|
|
||||||
} else if (windowIndex == getFirstWindowIndex(shuffleModeEnabled)) {
|
|
||||||
return repeatMode == Player.REPEAT_MODE_OFF
|
|
||||||
? C.INDEX_UNSET
|
|
||||||
: getLastWindowIndex(shuffleModeEnabled);
|
|
||||||
} else if (shuffleModeEnabled) {
|
|
||||||
return shuffleOrder.getPreviousIndex(windowIndex);
|
|
||||||
} else {
|
|
||||||
return windowIndex - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getNextWindowIndex(int windowIndex, int repeatMode, boolean shuffleModeEnabled) {
|
|
||||||
if (repeatMode == Player.REPEAT_MODE_ONE) {
|
|
||||||
return windowIndex;
|
|
||||||
} else if (windowIndex == getLastWindowIndex(shuffleModeEnabled)) {
|
|
||||||
return repeatMode == Player.REPEAT_MODE_OFF
|
|
||||||
? C.INDEX_UNSET
|
|
||||||
: getFirstWindowIndex(shuffleModeEnabled);
|
|
||||||
} else if (shuffleModeEnabled) {
|
|
||||||
return shuffleOrder.getNextIndex(windowIndex);
|
|
||||||
} else {
|
|
||||||
return windowIndex + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Internal methods.
|
|
||||||
|
|
||||||
private Period getPeriodInternal(PeriodUidImpl uid, Period period, boolean setIds) {
|
|
||||||
UUID uuid = uid.itemUuid;
|
|
||||||
int itemIndex = Assertions.checkNotNull(uuidToIndex.get(uuid));
|
|
||||||
MediaItemInfo mediaItemInfo = getInfoOrEmpty(mediaItemInfoMap, uuid);
|
|
||||||
MediaItemInfo.Period mediaInfoPeriod =
|
|
||||||
mediaItemInfo.periods.get(mediaItemInfo.getIndexOfPeriod(uid.periodId));
|
|
||||||
return period.set(
|
|
||||||
setIds ? mediaInfoPeriod.id : null,
|
|
||||||
setIds ? uid : null,
|
|
||||||
/* windowIndex= */ itemIndex,
|
|
||||||
mediaInfoPeriod.durationUs,
|
|
||||||
mediaInfoPeriod.positionInWindowUs);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getMediaItemIndexForPeriodIndex(int periodIndex) {
|
|
||||||
return Util.binarySearchCeil(
|
|
||||||
accumulativePeriodCount, periodIndex, /* inclusive= */ false, /* stayInBounds= */ false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MediaItemInfo getInfoOrEmpty(Map<UUID, MediaItemInfo> map, UUID uuid) {
|
|
||||||
MediaItemInfo info = map.get(uuid);
|
|
||||||
return info != null ? info : MediaItemInfo.EMPTY;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Internal classes.
|
|
||||||
|
|
||||||
private static final class PeriodUidImpl implements PeriodUid {
|
|
||||||
|
|
||||||
public final UUID itemUuid;
|
|
||||||
public final Object periodId;
|
|
||||||
|
|
||||||
private PeriodUidImpl(UUID itemUuid, Object periodId) {
|
|
||||||
this.itemUuid = itemUuid;
|
|
||||||
this.periodId = periodId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(@Nullable Object other) {
|
|
||||||
if (this == other) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (other == null || getClass() != other.getClass()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
PeriodUidImpl periodUid = (PeriodUidImpl) other;
|
|
||||||
return itemUuid.equals(periodUid.itemUuid) && periodId.equals(periodUid.periodId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
int result = itemUuid.hashCode();
|
|
||||||
result = 31 * result + periodId.hashCode();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,160 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.exoplayer2.ext.cast;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import com.google.android.exoplayer2.C;
|
|
||||||
import com.google.android.exoplayer2.util.Util;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
// TODO (Internal b/119293631): Add ad playback state info.
|
|
||||||
/**
|
|
||||||
* Holds dynamic information for a {@link MediaItem}.
|
|
||||||
*
|
|
||||||
* <p>Holds information related to preparation for a specific {@link MediaItem}. Unprepared items
|
|
||||||
* are associated with an {@link #EMPTY} info object until prepared.
|
|
||||||
*/
|
|
||||||
public final class MediaItemInfo {
|
|
||||||
|
|
||||||
/** Placeholder information for media items that have not yet been prepared by the player. */
|
|
||||||
public static final MediaItemInfo EMPTY =
|
|
||||||
new MediaItemInfo(
|
|
||||||
/* windowDurationUs= */ C.TIME_UNSET,
|
|
||||||
/* defaultStartPositionUs= */ 0L,
|
|
||||||
Collections.singletonList(
|
|
||||||
new Period(
|
|
||||||
/* id= */ new Object(),
|
|
||||||
/* durationUs= */ C.TIME_UNSET,
|
|
||||||
/* positionInWindowUs= */ 0L)),
|
|
||||||
/* positionInFirstPeriodUs= */ 0L,
|
|
||||||
/* isSeekable= */ false,
|
|
||||||
/* isDynamic= */ true);
|
|
||||||
|
|
||||||
/** Holds the information of one of the periods of a {@link MediaItem}. */
|
|
||||||
public static final class Period {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The id of the period. Must be unique within the {@link MediaItem} but may match with periods
|
|
||||||
* in other items.
|
|
||||||
*/
|
|
||||||
public final Object id;
|
|
||||||
/** The duration of the period in microseconds. */
|
|
||||||
public final long durationUs;
|
|
||||||
/** The position of this period in the window in microseconds. */
|
|
||||||
public final long positionInWindowUs;
|
|
||||||
// TODO: Add track information.
|
|
||||||
|
|
||||||
public Period(Object id, long durationUs, long positionInWindowUs) {
|
|
||||||
this.id = id;
|
|
||||||
this.durationUs = durationUs;
|
|
||||||
this.positionInWindowUs = positionInWindowUs;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(@Nullable Object other) {
|
|
||||||
if (this == other) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (other == null || getClass() != other.getClass()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Period period = (Period) other;
|
|
||||||
return durationUs == period.durationUs
|
|
||||||
&& positionInWindowUs == period.positionInWindowUs
|
|
||||||
&& id.equals(period.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
int result = id.hashCode();
|
|
||||||
result = 31 * result + (int) (durationUs ^ (durationUs >>> 32));
|
|
||||||
result = 31 * result + (int) (positionInWindowUs ^ (positionInWindowUs >>> 32));
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The duration of the window in microseconds. */
|
|
||||||
public final long windowDurationUs;
|
|
||||||
/** The default start position relative to the start of the window, in microseconds. */
|
|
||||||
public final long defaultStartPositionUs;
|
|
||||||
/** The periods conforming the media item. */
|
|
||||||
public final List<Period> periods;
|
|
||||||
/** The position of the window in the first period in microseconds. */
|
|
||||||
public final long positionInFirstPeriodUs;
|
|
||||||
/** Whether it is possible to seek within the window. */
|
|
||||||
public final boolean isSeekable;
|
|
||||||
/** Whether the window may change when the timeline is updated. */
|
|
||||||
public final boolean isDynamic;
|
|
||||||
|
|
||||||
public MediaItemInfo(
|
|
||||||
long windowDurationUs,
|
|
||||||
long defaultStartPositionUs,
|
|
||||||
List<Period> periods,
|
|
||||||
long positionInFirstPeriodUs,
|
|
||||||
boolean isSeekable,
|
|
||||||
boolean isDynamic) {
|
|
||||||
this.windowDurationUs = windowDurationUs;
|
|
||||||
this.defaultStartPositionUs = defaultStartPositionUs;
|
|
||||||
this.periods = Collections.unmodifiableList(periods);
|
|
||||||
this.positionInFirstPeriodUs = positionInFirstPeriodUs;
|
|
||||||
this.isSeekable = isSeekable;
|
|
||||||
this.isDynamic = isDynamic;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the index of the period with {@link Period#id} equal to {@code periodId}, or {@link
|
|
||||||
* C#INDEX_UNSET} if none of the periods has the given id.
|
|
||||||
*/
|
|
||||||
public int getIndexOfPeriod(Object periodId) {
|
|
||||||
for (int i = 0; i < periods.size(); i++) {
|
|
||||||
if (Util.areEqual(periods.get(i).id, periodId)) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return C.INDEX_UNSET;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(@Nullable Object other) {
|
|
||||||
if (this == other) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (other == null || getClass() != other.getClass()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
MediaItemInfo that = (MediaItemInfo) other;
|
|
||||||
return windowDurationUs == that.windowDurationUs
|
|
||||||
&& defaultStartPositionUs == that.defaultStartPositionUs
|
|
||||||
&& positionInFirstPeriodUs == that.positionInFirstPeriodUs
|
|
||||||
&& isSeekable == that.isSeekable
|
|
||||||
&& isDynamic == that.isDynamic
|
|
||||||
&& periods.equals(that.periods);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
int result = (int) (windowDurationUs ^ (windowDurationUs >>> 32));
|
|
||||||
result = 31 * result + (int) (defaultStartPositionUs ^ (defaultStartPositionUs >>> 32));
|
|
||||||
result = 31 * result + periods.hashCode();
|
|
||||||
result = 31 * result + (int) (positionInFirstPeriodUs ^ (positionInFirstPeriodUs >>> 32));
|
|
||||||
result = 31 * result + (isSeekable ? 1 : 0);
|
|
||||||
result = 31 * result + (isDynamic ? 1 : 0);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,85 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.exoplayer2.ext.cast;
|
|
||||||
|
|
||||||
/** Represents a sequence of {@link MediaItem MediaItems}. */
|
|
||||||
public interface MediaItemQueue {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the item at the given index.
|
|
||||||
*
|
|
||||||
* @param index The index of the item to retrieve.
|
|
||||||
* @return The item at the given index.
|
|
||||||
* @throws IndexOutOfBoundsException If {@code index < 0 || index >= getSize()}.
|
|
||||||
*/
|
|
||||||
MediaItem get(int index);
|
|
||||||
|
|
||||||
/** Returns the number of items in this queue. */
|
|
||||||
int getSize();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Appends the given sequence of items to the queue.
|
|
||||||
*
|
|
||||||
* @param items The sequence of items to append.
|
|
||||||
*/
|
|
||||||
void add(MediaItem... items);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds the given sequence of items to the queue at the given position, so that the first of
|
|
||||||
* {@code items} is placed at the given index.
|
|
||||||
*
|
|
||||||
* @param index The index at which {@code items} will be inserted.
|
|
||||||
* @param items The sequence of items to append.
|
|
||||||
* @throws IndexOutOfBoundsException If {@code index < 0 || index > getSize()}.
|
|
||||||
*/
|
|
||||||
void add(int index, MediaItem... items);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Moves an existing item within the playlist.
|
|
||||||
*
|
|
||||||
* <p>Calling this method is equivalent to removing the item at position {@code indexFrom} and
|
|
||||||
* immediately inserting it at position {@code indexTo}. If the moved item is being played at the
|
|
||||||
* moment of the invocation, playback will stick with the moved item.
|
|
||||||
*
|
|
||||||
* @param indexFrom The index of the item to move.
|
|
||||||
* @param indexTo The index at which the item will be placed after this operation.
|
|
||||||
* @throws IndexOutOfBoundsException If for either index, {@code index < 0 || index >= getSize()}.
|
|
||||||
*/
|
|
||||||
void move(int indexFrom, int indexTo);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes an item from the queue.
|
|
||||||
*
|
|
||||||
* @param index The index of the item to remove from the queue.
|
|
||||||
* @throws IndexOutOfBoundsException If {@code index < 0 || index >= getSize()}.
|
|
||||||
*/
|
|
||||||
void remove(int index);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a range of items from the queue.
|
|
||||||
*
|
|
||||||
* <p>Does nothing if an empty range ({@code from == exclusiveTo}) is passed.
|
|
||||||
*
|
|
||||||
* @param from The inclusive index at which the range to remove starts.
|
|
||||||
* @param exclusiveTo The exclusive index at which the range to remove ends.
|
|
||||||
* @throws IndexOutOfBoundsException If {@code from < 0 || exclusiveTo > getSize() || from >
|
|
||||||
* exclusiveTo}.
|
|
||||||
*/
|
|
||||||
void removeRange(int from, int exclusiveTo);
|
|
||||||
|
|
||||||
/** Removes all items in the queue. */
|
|
||||||
void clear();
|
|
||||||
}
|
|
@ -1,633 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.exoplayer2.ext.cast;
|
|
||||||
|
|
||||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_AD_INSERTION;
|
|
||||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL;
|
|
||||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_PERIOD_TRANSITION;
|
|
||||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK;
|
|
||||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT;
|
|
||||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
|
|
||||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
|
|
||||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
|
|
||||||
import static com.google.android.exoplayer2.Player.STATE_BUFFERING;
|
|
||||||
import static com.google.android.exoplayer2.Player.STATE_ENDED;
|
|
||||||
import static com.google.android.exoplayer2.Player.STATE_IDLE;
|
|
||||||
import static com.google.android.exoplayer2.Player.STATE_READY;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DEFAULT_START_POSITION_US;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DESCRIPTION;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DISCONTINUITY_REASON;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DRM_SCHEMES;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DURATION_US;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_END_POSITION_US;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ERROR_MESSAGE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ID;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_IS_DYNAMIC;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_IS_LOADING;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_IS_SEEKABLE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_LICENSE_SERVER;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MEDIA;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MEDIA_ITEMS_INFO;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MEDIA_QUEUE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MIME_TYPE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PERIODS;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PERIOD_ID;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PITCH;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAYBACK_PARAMETERS;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAYBACK_POSITION;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAYBACK_STATE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAY_WHEN_READY;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_POSITION_IN_FIRST_PERIOD_US;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_POSITION_MS;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_AUDIO_LANGUAGE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_TEXT_LANGUAGE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_REPEAT_MODE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_REQUEST_HEADERS;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SEQUENCE_NUMBER;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_MODE_ENABLED;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_ORDER;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SKIP_SILENCE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SPEED;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_START_POSITION_US;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_TITLE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_TRACK_SELECTION_PARAMETERS;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_URI;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUID;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_WINDOW_DURATION_US;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_DISCONTINUITY_REASON_AD_INSERTION;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_DISCONTINUITY_REASON_INTERNAL;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_DISCONTINUITY_REASON_PERIOD_TRANSITION;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_DISCONTINUITY_REASON_SEEK;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_DISCONTINUITY_REASON_SEEK_ADJUSTMENT;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_ALL;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_OFF;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_ONE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_STATE_BUFFERING;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_STATE_ENDED;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_STATE_IDLE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_STATE_READY;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.VisibleForTesting;
|
|
||||||
import com.google.android.exoplayer2.C;
|
|
||||||
import com.google.android.exoplayer2.PlaybackParameters;
|
|
||||||
import com.google.android.exoplayer2.Player;
|
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelectionParameters;
|
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
|
||||||
import com.google.android.exoplayer2.util.Util;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.UUID;
|
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
/** Holds a playback state update from the receiver app. */
|
|
||||||
public final class ReceiverAppStateUpdate {
|
|
||||||
|
|
||||||
/** Builder for {@link ReceiverAppStateUpdate}. */
|
|
||||||
public static final class Builder {
|
|
||||||
|
|
||||||
private final long sequenceNumber;
|
|
||||||
private @MonotonicNonNull Boolean playWhenReady;
|
|
||||||
private @MonotonicNonNull Integer playbackState;
|
|
||||||
private @MonotonicNonNull List<MediaItem> items;
|
|
||||||
private @MonotonicNonNull Integer repeatMode;
|
|
||||||
private @MonotonicNonNull Boolean shuffleModeEnabled;
|
|
||||||
private @MonotonicNonNull Boolean isLoading;
|
|
||||||
private @MonotonicNonNull PlaybackParameters playbackParameters;
|
|
||||||
private @MonotonicNonNull TrackSelectionParameters trackSelectionParameters;
|
|
||||||
private @MonotonicNonNull String errorMessage;
|
|
||||||
private @MonotonicNonNull Integer discontinuityReason;
|
|
||||||
private @MonotonicNonNull UUID currentPlayingItemUuid;
|
|
||||||
private @MonotonicNonNull String currentPlayingPeriodId;
|
|
||||||
private @MonotonicNonNull Long currentPlaybackPositionMs;
|
|
||||||
private @MonotonicNonNull List<Integer> shuffleOrder;
|
|
||||||
private Map<UUID, MediaItemInfo> mediaItemsInformation;
|
|
||||||
|
|
||||||
private Builder(long sequenceNumber) {
|
|
||||||
this.sequenceNumber = sequenceNumber;
|
|
||||||
mediaItemsInformation = Collections.emptyMap();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** See {@link ReceiverAppStateUpdate#playWhenReady}. */
|
|
||||||
public Builder setPlayWhenReady(Boolean playWhenReady) {
|
|
||||||
this.playWhenReady = playWhenReady;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** See {@link ReceiverAppStateUpdate#playbackState}. */
|
|
||||||
public Builder setPlaybackState(Integer playbackState) {
|
|
||||||
this.playbackState = playbackState;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** See {@link ReceiverAppStateUpdate#items}. */
|
|
||||||
public Builder setItems(List<MediaItem> items) {
|
|
||||||
this.items = Collections.unmodifiableList(items);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** See {@link ReceiverAppStateUpdate#repeatMode}. */
|
|
||||||
public Builder setRepeatMode(Integer repeatMode) {
|
|
||||||
this.repeatMode = repeatMode;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** See {@link ReceiverAppStateUpdate#shuffleModeEnabled}. */
|
|
||||||
public Builder setShuffleModeEnabled(Boolean shuffleModeEnabled) {
|
|
||||||
this.shuffleModeEnabled = shuffleModeEnabled;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** See {@link ReceiverAppStateUpdate#isLoading}. */
|
|
||||||
public Builder setIsLoading(Boolean isLoading) {
|
|
||||||
this.isLoading = isLoading;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** See {@link ReceiverAppStateUpdate#playbackParameters}. */
|
|
||||||
public Builder setPlaybackParameters(PlaybackParameters playbackParameters) {
|
|
||||||
this.playbackParameters = playbackParameters;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** See {@link ReceiverAppStateUpdate#trackSelectionParameters} */
|
|
||||||
public Builder setTrackSelectionParameters(TrackSelectionParameters trackSelectionParameters) {
|
|
||||||
this.trackSelectionParameters = trackSelectionParameters;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** See {@link ReceiverAppStateUpdate#errorMessage}. */
|
|
||||||
public Builder setErrorMessage(String errorMessage) {
|
|
||||||
this.errorMessage = errorMessage;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** See {@link ReceiverAppStateUpdate#discontinuityReason}. */
|
|
||||||
public Builder setDiscontinuityReason(Integer discontinuityReason) {
|
|
||||||
this.discontinuityReason = discontinuityReason;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* See {@link ReceiverAppStateUpdate#currentPlayingItemUuid} and {@link
|
|
||||||
* ReceiverAppStateUpdate#currentPlaybackPositionMs}.
|
|
||||||
*/
|
|
||||||
public Builder setPlaybackPosition(
|
|
||||||
UUID currentPlayingItemUuid,
|
|
||||||
String currentPlayingPeriodId,
|
|
||||||
Long currentPlaybackPositionMs) {
|
|
||||||
this.currentPlayingItemUuid = currentPlayingItemUuid;
|
|
||||||
this.currentPlayingPeriodId = currentPlayingPeriodId;
|
|
||||||
this.currentPlaybackPositionMs = currentPlaybackPositionMs;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* See {@link ReceiverAppStateUpdate#currentPlayingItemUuid} and {@link
|
|
||||||
* ReceiverAppStateUpdate#currentPlaybackPositionMs}.
|
|
||||||
*/
|
|
||||||
public Builder setMediaItemsInformation(Map<UUID, MediaItemInfo> mediaItemsInformation) {
|
|
||||||
this.mediaItemsInformation = Collections.unmodifiableMap(mediaItemsInformation);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** See {@link ReceiverAppStateUpdate#shuffleOrder}. */
|
|
||||||
public Builder setShuffleOrder(List<Integer> shuffleOrder) {
|
|
||||||
this.shuffleOrder = Collections.unmodifiableList(shuffleOrder);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a new {@link ReceiverAppStateUpdate} instance with the current values in this
|
|
||||||
* builder.
|
|
||||||
*/
|
|
||||||
public ReceiverAppStateUpdate build() {
|
|
||||||
return new ReceiverAppStateUpdate(
|
|
||||||
sequenceNumber,
|
|
||||||
playWhenReady,
|
|
||||||
playbackState,
|
|
||||||
items,
|
|
||||||
repeatMode,
|
|
||||||
shuffleModeEnabled,
|
|
||||||
isLoading,
|
|
||||||
playbackParameters,
|
|
||||||
trackSelectionParameters,
|
|
||||||
errorMessage,
|
|
||||||
discontinuityReason,
|
|
||||||
currentPlayingItemUuid,
|
|
||||||
currentPlayingPeriodId,
|
|
||||||
currentPlaybackPositionMs,
|
|
||||||
mediaItemsInformation,
|
|
||||||
shuffleOrder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns a {@link ReceiverAppStateUpdate} builder. */
|
|
||||||
public static Builder builder(long sequenceNumber) {
|
|
||||||
return new Builder(sequenceNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an instance from parsing a state update received from the Receiver App.
|
|
||||||
*
|
|
||||||
* @param jsonMessage The state update encoded as a JSON string.
|
|
||||||
* @return The parsed state update.
|
|
||||||
* @throws JSONException If an error is encountered when parsing the {@code jsonMessage}.
|
|
||||||
*/
|
|
||||||
public static ReceiverAppStateUpdate fromJsonMessage(String jsonMessage) throws JSONException {
|
|
||||||
JSONObject stateAsJson = new JSONObject(jsonMessage);
|
|
||||||
Builder builder = builder(stateAsJson.getLong(KEY_SEQUENCE_NUMBER));
|
|
||||||
|
|
||||||
if (stateAsJson.has(KEY_PLAY_WHEN_READY)) {
|
|
||||||
builder.setPlayWhenReady(stateAsJson.getBoolean(KEY_PLAY_WHEN_READY));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stateAsJson.has(KEY_PLAYBACK_STATE)) {
|
|
||||||
builder.setPlaybackState(
|
|
||||||
playbackStateStringToConstant(stateAsJson.getString(KEY_PLAYBACK_STATE)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stateAsJson.has(KEY_MEDIA_QUEUE)) {
|
|
||||||
builder.setItems(
|
|
||||||
toMediaItemArrayList(Assertions.checkNotNull(stateAsJson.optJSONArray(KEY_MEDIA_QUEUE))));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stateAsJson.has(KEY_REPEAT_MODE)) {
|
|
||||||
builder.setRepeatMode(stringToRepeatMode(stateAsJson.getString(KEY_REPEAT_MODE)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stateAsJson.has(KEY_SHUFFLE_MODE_ENABLED)) {
|
|
||||||
builder.setShuffleModeEnabled(stateAsJson.getBoolean(KEY_SHUFFLE_MODE_ENABLED));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stateAsJson.has(KEY_IS_LOADING)) {
|
|
||||||
builder.setIsLoading(stateAsJson.getBoolean(KEY_IS_LOADING));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stateAsJson.has(KEY_PLAYBACK_PARAMETERS)) {
|
|
||||||
builder.setPlaybackParameters(
|
|
||||||
toPlaybackParameters(
|
|
||||||
Assertions.checkNotNull(stateAsJson.optJSONObject(KEY_PLAYBACK_PARAMETERS))));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stateAsJson.has(KEY_TRACK_SELECTION_PARAMETERS)) {
|
|
||||||
JSONObject trackSelectionParametersJson =
|
|
||||||
stateAsJson.getJSONObject(KEY_TRACK_SELECTION_PARAMETERS);
|
|
||||||
TrackSelectionParameters parameters =
|
|
||||||
TrackSelectionParameters.DEFAULT
|
|
||||||
.buildUpon()
|
|
||||||
.setPreferredTextLanguage(
|
|
||||||
trackSelectionParametersJson.getString(KEY_PREFERRED_TEXT_LANGUAGE))
|
|
||||||
.setPreferredAudioLanguage(
|
|
||||||
trackSelectionParametersJson.getString(KEY_PREFERRED_AUDIO_LANGUAGE))
|
|
||||||
.setSelectUndeterminedTextLanguage(
|
|
||||||
trackSelectionParametersJson.getBoolean(KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE))
|
|
||||||
.setDisabledTextTrackSelectionFlags(
|
|
||||||
jsonArrayToSelectionFlags(
|
|
||||||
trackSelectionParametersJson.getJSONArray(
|
|
||||||
KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS)))
|
|
||||||
.build();
|
|
||||||
builder.setTrackSelectionParameters(parameters);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stateAsJson.has(KEY_ERROR_MESSAGE)) {
|
|
||||||
builder.setErrorMessage(stateAsJson.getString(KEY_ERROR_MESSAGE));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stateAsJson.has(KEY_PLAYBACK_POSITION)) {
|
|
||||||
JSONObject playbackPosition = stateAsJson.getJSONObject(KEY_PLAYBACK_POSITION);
|
|
||||||
String discontinuityReason = playbackPosition.optString(KEY_DISCONTINUITY_REASON);
|
|
||||||
if (!discontinuityReason.isEmpty()) {
|
|
||||||
builder.setDiscontinuityReason(stringToDiscontinuityReason(discontinuityReason));
|
|
||||||
}
|
|
||||||
UUID currentPlayingItemUuid = UUID.fromString(playbackPosition.getString(KEY_UUID));
|
|
||||||
String currentPlayingPeriodId = playbackPosition.getString(KEY_PERIOD_ID);
|
|
||||||
Long currentPlaybackPositionMs = playbackPosition.getLong(KEY_POSITION_MS);
|
|
||||||
builder.setPlaybackPosition(
|
|
||||||
currentPlayingItemUuid, currentPlayingPeriodId, currentPlaybackPositionMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stateAsJson.has(KEY_MEDIA_ITEMS_INFO)) {
|
|
||||||
HashMap<UUID, MediaItemInfo> mediaItemInformation = new HashMap<>();
|
|
||||||
JSONObject mediaItemsInfo = stateAsJson.getJSONObject(KEY_MEDIA_ITEMS_INFO);
|
|
||||||
for (Iterator<String> i = mediaItemsInfo.keys(); i.hasNext(); ) {
|
|
||||||
String key = i.next();
|
|
||||||
mediaItemInformation.put(
|
|
||||||
UUID.fromString(key), jsonToMediaitemInfo(mediaItemsInfo.getJSONObject(key)));
|
|
||||||
}
|
|
||||||
builder.setMediaItemsInformation(mediaItemInformation);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stateAsJson.has(KEY_SHUFFLE_ORDER)) {
|
|
||||||
ArrayList<Integer> shuffleOrder = new ArrayList<>();
|
|
||||||
JSONArray shuffleOrderJson = stateAsJson.getJSONArray(KEY_SHUFFLE_ORDER);
|
|
||||||
for (int i = 0; i < shuffleOrderJson.length(); i++) {
|
|
||||||
shuffleOrder.add(shuffleOrderJson.getInt(i));
|
|
||||||
}
|
|
||||||
builder.setShuffleOrder(shuffleOrder);
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The sequence number of the status update. */
|
|
||||||
public final long sequenceNumber;
|
|
||||||
/** Optional {@link Player#getPlayWhenReady playWhenReady} value. */
|
|
||||||
@Nullable public final Boolean playWhenReady;
|
|
||||||
/** Optional {@link Player#getPlaybackState() playbackState}. */
|
|
||||||
@Nullable public final Integer playbackState;
|
|
||||||
/** Optional list of media items. */
|
|
||||||
@Nullable public final List<MediaItem> items;
|
|
||||||
/** Optional {@link Player#getRepeatMode() repeatMode}. */
|
|
||||||
@Nullable public final Integer repeatMode;
|
|
||||||
/** Optional {@link Player#getShuffleModeEnabled() shuffleMode}. */
|
|
||||||
@Nullable public final Boolean shuffleModeEnabled;
|
|
||||||
/** Optional {@link Player#isLoading() isLoading} value. */
|
|
||||||
@Nullable public final Boolean isLoading;
|
|
||||||
/** Optional {@link Player#getPlaybackParameters() playbackParameters}. */
|
|
||||||
@Nullable public final PlaybackParameters playbackParameters;
|
|
||||||
/** Optional {@link TrackSelectionParameters}. */
|
|
||||||
@Nullable public final TrackSelectionParameters trackSelectionParameters;
|
|
||||||
/** Optional error message string. */
|
|
||||||
@Nullable public final String errorMessage;
|
|
||||||
/**
|
|
||||||
* Optional reason for a {@link Player.EventListener#onPositionDiscontinuity(int) discontinuity }
|
|
||||||
* in the playback position.
|
|
||||||
*/
|
|
||||||
@Nullable public final Integer discontinuityReason;
|
|
||||||
/** Optional {@link UUID} of the {@link Player#getCurrentWindowIndex() currently played item}. */
|
|
||||||
@Nullable public final UUID currentPlayingItemUuid;
|
|
||||||
/** Optional id of the current {@link Player#getCurrentPeriodIndex() period being played}. */
|
|
||||||
@Nullable public final String currentPlayingPeriodId;
|
|
||||||
/** Optional {@link Player#getCurrentPosition() playbackPosition} in milliseconds. */
|
|
||||||
@Nullable public final Long currentPlaybackPositionMs;
|
|
||||||
/** Holds information about the {@link MediaItem media items} in the media queue. */
|
|
||||||
public final Map<UUID, MediaItemInfo> mediaItemsInformation;
|
|
||||||
/** Holds the indices of the media queue items in shuffle order. */
|
|
||||||
@Nullable public final List<Integer> shuffleOrder;
|
|
||||||
|
|
||||||
/** Creates an instance with the given values. */
|
|
||||||
private ReceiverAppStateUpdate(
|
|
||||||
long sequenceNumber,
|
|
||||||
@Nullable Boolean playWhenReady,
|
|
||||||
@Nullable Integer playbackState,
|
|
||||||
@Nullable List<MediaItem> items,
|
|
||||||
@Nullable Integer repeatMode,
|
|
||||||
@Nullable Boolean shuffleModeEnabled,
|
|
||||||
@Nullable Boolean isLoading,
|
|
||||||
@Nullable PlaybackParameters playbackParameters,
|
|
||||||
@Nullable TrackSelectionParameters trackSelectionParameters,
|
|
||||||
@Nullable String errorMessage,
|
|
||||||
@Nullable Integer discontinuityReason,
|
|
||||||
@Nullable UUID currentPlayingItemUuid,
|
|
||||||
@Nullable String currentPlayingPeriodId,
|
|
||||||
@Nullable Long currentPlaybackPositionMs,
|
|
||||||
Map<UUID, MediaItemInfo> mediaItemsInformation,
|
|
||||||
@Nullable List<Integer> shuffleOrder) {
|
|
||||||
this.sequenceNumber = sequenceNumber;
|
|
||||||
this.playWhenReady = playWhenReady;
|
|
||||||
this.playbackState = playbackState;
|
|
||||||
this.items = items;
|
|
||||||
this.repeatMode = repeatMode;
|
|
||||||
this.shuffleModeEnabled = shuffleModeEnabled;
|
|
||||||
this.isLoading = isLoading;
|
|
||||||
this.playbackParameters = playbackParameters;
|
|
||||||
this.trackSelectionParameters = trackSelectionParameters;
|
|
||||||
this.errorMessage = errorMessage;
|
|
||||||
this.discontinuityReason = discontinuityReason;
|
|
||||||
this.currentPlayingItemUuid = currentPlayingItemUuid;
|
|
||||||
this.currentPlayingPeriodId = currentPlayingPeriodId;
|
|
||||||
this.currentPlaybackPositionMs = currentPlaybackPositionMs;
|
|
||||||
this.mediaItemsInformation = mediaItemsInformation;
|
|
||||||
this.shuffleOrder = shuffleOrder;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(@Nullable Object other) {
|
|
||||||
if (this == other) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (other == null || getClass() != other.getClass()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
ReceiverAppStateUpdate that = (ReceiverAppStateUpdate) other;
|
|
||||||
|
|
||||||
return sequenceNumber == that.sequenceNumber
|
|
||||||
&& Util.areEqual(playWhenReady, that.playWhenReady)
|
|
||||||
&& Util.areEqual(playbackState, that.playbackState)
|
|
||||||
&& Util.areEqual(items, that.items)
|
|
||||||
&& Util.areEqual(repeatMode, that.repeatMode)
|
|
||||||
&& Util.areEqual(shuffleModeEnabled, that.shuffleModeEnabled)
|
|
||||||
&& Util.areEqual(isLoading, that.isLoading)
|
|
||||||
&& Util.areEqual(playbackParameters, that.playbackParameters)
|
|
||||||
&& Util.areEqual(trackSelectionParameters, that.trackSelectionParameters)
|
|
||||||
&& Util.areEqual(errorMessage, that.errorMessage)
|
|
||||||
&& Util.areEqual(discontinuityReason, that.discontinuityReason)
|
|
||||||
&& Util.areEqual(currentPlayingItemUuid, that.currentPlayingItemUuid)
|
|
||||||
&& Util.areEqual(currentPlayingPeriodId, that.currentPlayingPeriodId)
|
|
||||||
&& Util.areEqual(currentPlaybackPositionMs, that.currentPlaybackPositionMs)
|
|
||||||
&& Util.areEqual(mediaItemsInformation, that.mediaItemsInformation)
|
|
||||||
&& Util.areEqual(shuffleOrder, that.shuffleOrder);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
int result = (int) (sequenceNumber ^ (sequenceNumber >>> 32));
|
|
||||||
result = 31 * result + (playWhenReady != null ? playWhenReady.hashCode() : 0);
|
|
||||||
result = 31 * result + (playbackState != null ? playbackState.hashCode() : 0);
|
|
||||||
result = 31 * result + (items != null ? items.hashCode() : 0);
|
|
||||||
result = 31 * result + (repeatMode != null ? repeatMode.hashCode() : 0);
|
|
||||||
result = 31 * result + (shuffleModeEnabled != null ? shuffleModeEnabled.hashCode() : 0);
|
|
||||||
result = 31 * result + (isLoading != null ? isLoading.hashCode() : 0);
|
|
||||||
result = 31 * result + (playbackParameters != null ? playbackParameters.hashCode() : 0);
|
|
||||||
result =
|
|
||||||
31 * result + (trackSelectionParameters != null ? trackSelectionParameters.hashCode() : 0);
|
|
||||||
result = 31 * result + (errorMessage != null ? errorMessage.hashCode() : 0);
|
|
||||||
result = 31 * result + (discontinuityReason != null ? discontinuityReason.hashCode() : 0);
|
|
||||||
result = 31 * result + (currentPlayingItemUuid != null ? currentPlayingItemUuid.hashCode() : 0);
|
|
||||||
result = 31 * result + (currentPlayingPeriodId != null ? currentPlayingPeriodId.hashCode() : 0);
|
|
||||||
result =
|
|
||||||
31 * result
|
|
||||||
+ (currentPlaybackPositionMs != null ? currentPlaybackPositionMs.hashCode() : 0);
|
|
||||||
result = 31 * result + mediaItemsInformation.hashCode();
|
|
||||||
result = 31 * result + (shuffleOrder != null ? shuffleOrder.hashCode() : 0);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Internal methods.
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
/* package */ static List<MediaItem> toMediaItemArrayList(JSONArray mediaItemsAsJson)
|
|
||||||
throws JSONException {
|
|
||||||
ArrayList<MediaItem> mediaItems = new ArrayList<>();
|
|
||||||
for (int i = 0; i < mediaItemsAsJson.length(); i++) {
|
|
||||||
mediaItems.add(toMediaItem(mediaItemsAsJson.getJSONObject(i)));
|
|
||||||
}
|
|
||||||
return mediaItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MediaItem toMediaItem(JSONObject mediaItemAsJson) throws JSONException {
|
|
||||||
MediaItem.Builder builder = new MediaItem.Builder();
|
|
||||||
builder.setUuid(UUID.fromString(mediaItemAsJson.getString(KEY_UUID)));
|
|
||||||
builder.setTitle(mediaItemAsJson.getString(KEY_TITLE));
|
|
||||||
builder.setDescription(mediaItemAsJson.getString(KEY_DESCRIPTION));
|
|
||||||
builder.setMedia(jsonToUriBundle(mediaItemAsJson.getJSONObject(KEY_MEDIA)));
|
|
||||||
// TODO(Internal b/118431961): Add attachment management.
|
|
||||||
|
|
||||||
builder.setDrmSchemes(jsonArrayToDrmSchemes(mediaItemAsJson.getJSONArray(KEY_DRM_SCHEMES)));
|
|
||||||
if (mediaItemAsJson.has(KEY_START_POSITION_US)) {
|
|
||||||
builder.setStartPositionUs(mediaItemAsJson.getLong(KEY_START_POSITION_US));
|
|
||||||
}
|
|
||||||
if (mediaItemAsJson.has(KEY_END_POSITION_US)) {
|
|
||||||
builder.setEndPositionUs(mediaItemAsJson.getLong(KEY_END_POSITION_US));
|
|
||||||
}
|
|
||||||
builder.setMimeType(mediaItemAsJson.getString(KEY_MIME_TYPE));
|
|
||||||
return builder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static PlaybackParameters toPlaybackParameters(JSONObject parameters)
|
|
||||||
throws JSONException {
|
|
||||||
float speed = (float) parameters.getDouble(KEY_SPEED);
|
|
||||||
float pitch = (float) parameters.getDouble(KEY_PITCH);
|
|
||||||
boolean skipSilence = parameters.getBoolean(KEY_SKIP_SILENCE);
|
|
||||||
return new PlaybackParameters(speed, pitch, skipSilence);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int playbackStateStringToConstant(String string) {
|
|
||||||
switch (string) {
|
|
||||||
case STR_STATE_IDLE:
|
|
||||||
return STATE_IDLE;
|
|
||||||
case STR_STATE_BUFFERING:
|
|
||||||
return STATE_BUFFERING;
|
|
||||||
case STR_STATE_READY:
|
|
||||||
return STATE_READY;
|
|
||||||
case STR_STATE_ENDED:
|
|
||||||
return STATE_ENDED;
|
|
||||||
default:
|
|
||||||
throw new AssertionError("Unexpected state string: " + string);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Integer stringToRepeatMode(String repeatModeStr) {
|
|
||||||
switch (repeatModeStr) {
|
|
||||||
case STR_REPEAT_MODE_OFF:
|
|
||||||
return REPEAT_MODE_OFF;
|
|
||||||
case STR_REPEAT_MODE_ONE:
|
|
||||||
return REPEAT_MODE_ONE;
|
|
||||||
case STR_REPEAT_MODE_ALL:
|
|
||||||
return REPEAT_MODE_ALL;
|
|
||||||
default:
|
|
||||||
throw new AssertionError("Illegal repeat mode: " + repeatModeStr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Integer stringToDiscontinuityReason(String discontinuityReasonStr) {
|
|
||||||
switch (discontinuityReasonStr) {
|
|
||||||
case STR_DISCONTINUITY_REASON_PERIOD_TRANSITION:
|
|
||||||
return DISCONTINUITY_REASON_PERIOD_TRANSITION;
|
|
||||||
case STR_DISCONTINUITY_REASON_SEEK:
|
|
||||||
return DISCONTINUITY_REASON_SEEK;
|
|
||||||
case STR_DISCONTINUITY_REASON_SEEK_ADJUSTMENT:
|
|
||||||
return DISCONTINUITY_REASON_SEEK_ADJUSTMENT;
|
|
||||||
case STR_DISCONTINUITY_REASON_AD_INSERTION:
|
|
||||||
return DISCONTINUITY_REASON_AD_INSERTION;
|
|
||||||
case STR_DISCONTINUITY_REASON_INTERNAL:
|
|
||||||
return DISCONTINUITY_REASON_INTERNAL;
|
|
||||||
default:
|
|
||||||
throw new AssertionError("Illegal discontinuity reason: " + discontinuityReasonStr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@C.SelectionFlags
|
|
||||||
private static int jsonArrayToSelectionFlags(JSONArray array) throws JSONException {
|
|
||||||
int result = 0;
|
|
||||||
for (int i = 0; i < array.length(); i++) {
|
|
||||||
switch (array.getString(i)) {
|
|
||||||
case ExoCastConstants.STR_SELECTION_FLAG_AUTOSELECT:
|
|
||||||
result |= C.SELECTION_FLAG_AUTOSELECT;
|
|
||||||
break;
|
|
||||||
case ExoCastConstants.STR_SELECTION_FLAG_FORCED:
|
|
||||||
result |= C.SELECTION_FLAG_FORCED;
|
|
||||||
break;
|
|
||||||
case ExoCastConstants.STR_SELECTION_FLAG_DEFAULT:
|
|
||||||
result |= C.SELECTION_FLAG_DEFAULT;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// Do nothing.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<MediaItem.DrmScheme> jsonArrayToDrmSchemes(JSONArray drmSchemesAsJson)
|
|
||||||
throws JSONException {
|
|
||||||
ArrayList<MediaItem.DrmScheme> drmSchemes = new ArrayList<>();
|
|
||||||
for (int i = 0; i < drmSchemesAsJson.length(); i++) {
|
|
||||||
JSONObject drmSchemeAsJson = drmSchemesAsJson.getJSONObject(i);
|
|
||||||
MediaItem.UriBundle uriBundle =
|
|
||||||
drmSchemeAsJson.has(KEY_LICENSE_SERVER)
|
|
||||||
? jsonToUriBundle(drmSchemeAsJson.getJSONObject(KEY_LICENSE_SERVER))
|
|
||||||
: null;
|
|
||||||
drmSchemes.add(
|
|
||||||
new MediaItem.DrmScheme(UUID.fromString(drmSchemeAsJson.getString(KEY_UUID)), uriBundle));
|
|
||||||
}
|
|
||||||
return Collections.unmodifiableList(drmSchemes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MediaItem.UriBundle jsonToUriBundle(JSONObject json) throws JSONException {
|
|
||||||
Uri uri = Uri.parse(json.getString(KEY_URI));
|
|
||||||
JSONObject requestHeadersAsJson = json.getJSONObject(KEY_REQUEST_HEADERS);
|
|
||||||
HashMap<String, String> requestHeaders = new HashMap<>();
|
|
||||||
for (Iterator<String> i = requestHeadersAsJson.keys(); i.hasNext(); ) {
|
|
||||||
String key = i.next();
|
|
||||||
requestHeaders.put(key, requestHeadersAsJson.getString(key));
|
|
||||||
}
|
|
||||||
return new MediaItem.UriBundle(uri, requestHeaders);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MediaItemInfo jsonToMediaitemInfo(JSONObject json) throws JSONException {
|
|
||||||
long durationUs = json.getLong(KEY_WINDOW_DURATION_US);
|
|
||||||
long defaultPositionUs = json.optLong(KEY_DEFAULT_START_POSITION_US, /* fallback= */ 0L);
|
|
||||||
JSONArray periodsJson = json.getJSONArray(KEY_PERIODS);
|
|
||||||
ArrayList<MediaItemInfo.Period> periods = new ArrayList<>();
|
|
||||||
long positionInFirstPeriodUs = json.getLong(KEY_POSITION_IN_FIRST_PERIOD_US);
|
|
||||||
|
|
||||||
long windowPositionUs = -positionInFirstPeriodUs;
|
|
||||||
for (int i = 0; i < periodsJson.length(); i++) {
|
|
||||||
JSONObject periodJson = periodsJson.getJSONObject(i);
|
|
||||||
long periodDurationUs = periodJson.optLong(KEY_DURATION_US, C.TIME_UNSET);
|
|
||||||
periods.add(
|
|
||||||
new MediaItemInfo.Period(
|
|
||||||
periodJson.getString(KEY_ID), periodDurationUs, windowPositionUs));
|
|
||||||
windowPositionUs += periodDurationUs;
|
|
||||||
}
|
|
||||||
boolean isDynamic = json.getBoolean(KEY_IS_DYNAMIC);
|
|
||||||
boolean isSeekable = json.getBoolean(KEY_IS_SEEKABLE);
|
|
||||||
return new MediaItemInfo(
|
|
||||||
durationUs, defaultPositionUs, periods, positionInFirstPeriodUs, isSeekable, isDynamic);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,436 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.exoplayer2.ext.cast;
|
|
||||||
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ARGS;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DESCRIPTION;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DRM_SCHEMES;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_END_POSITION_US;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_INDEX;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ITEMS;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_LICENSE_SERVER;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MEDIA;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_METHOD;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MIME_TYPE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PITCH;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAY_WHEN_READY;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_POSITION_MS;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_AUDIO_LANGUAGE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_TEXT_LANGUAGE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_REPEAT_MODE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_REQUEST_HEADERS;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SEQUENCE_NUMBER;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_MODE_ENABLED;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_ORDER;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SKIP_SILENCE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SPEED;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_START_POSITION_US;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_TITLE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_URI;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUID;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUIDS;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_ADD_ITEMS;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_MOVE_ITEM;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_REMOVE_ITEMS;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SEEK_TO;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_PLAYBACK_PARAMETERS;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_PLAY_WHEN_READY;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_REPEAT_MODE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_SHUFFLE_MODE_ENABLED;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.METHOD_SET_TRACK_SELECTION_PARAMETERS;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_ALL;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_OFF;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_ONE;
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|
||||||
import com.google.android.exoplayer2.C;
|
|
||||||
import com.google.android.exoplayer2.PlaybackParameters;
|
|
||||||
import com.google.android.exoplayer2.Player;
|
|
||||||
import com.google.android.exoplayer2.ext.cast.MediaItem.DrmScheme;
|
|
||||||
import com.google.android.exoplayer2.ext.cast.MediaItem.UriBundle;
|
|
||||||
import com.google.android.exoplayer2.source.ShuffleOrder;
|
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelectionParameters;
|
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.UUID;
|
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
|
|
||||||
/** Unit tests for {@link ExoCastMessage}. */
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
|
||||||
public class ExoCastMessageTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void addItems_withUnsetIndex_doesNotAddIndexToJson() throws JSONException {
|
|
||||||
MediaItem sampleItem = new MediaItem.Builder().build();
|
|
||||||
ExoCastMessage message =
|
|
||||||
new ExoCastMessage.AddItems(
|
|
||||||
C.INDEX_UNSET,
|
|
||||||
Collections.singletonList(sampleItem),
|
|
||||||
new ShuffleOrder.UnshuffledShuffleOrder(1));
|
|
||||||
JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0));
|
|
||||||
JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS);
|
|
||||||
JSONArray items = arguments.getJSONArray(KEY_ITEMS);
|
|
||||||
|
|
||||||
assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0);
|
|
||||||
assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_ADD_ITEMS);
|
|
||||||
assertThat(arguments.has(KEY_INDEX)).isFalse();
|
|
||||||
assertThat(items.length()).isEqualTo(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void addItems_withMultipleItems_producesExpectedJsonList() throws JSONException {
|
|
||||||
MediaItem sampleItem1 = new MediaItem.Builder().build();
|
|
||||||
MediaItem sampleItem2 = new MediaItem.Builder().build();
|
|
||||||
ExoCastMessage message =
|
|
||||||
new ExoCastMessage.AddItems(
|
|
||||||
1, Arrays.asList(sampleItem2, sampleItem1), new ShuffleOrder.UnshuffledShuffleOrder(2));
|
|
||||||
JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 1));
|
|
||||||
JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS);
|
|
||||||
JSONArray items = arguments.getJSONArray(KEY_ITEMS);
|
|
||||||
|
|
||||||
assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(1);
|
|
||||||
assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_ADD_ITEMS);
|
|
||||||
assertThat(arguments.getInt(KEY_INDEX)).isEqualTo(1);
|
|
||||||
assertThat(items.length()).isEqualTo(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void addItems_withoutItemOptionalFields_doesNotAddFieldsToJson() throws JSONException {
|
|
||||||
MediaItem itemWithoutOptionalFields =
|
|
||||||
new MediaItem.Builder()
|
|
||||||
.setTitle("title")
|
|
||||||
.setMimeType(MimeTypes.AUDIO_MP4)
|
|
||||||
.setDescription("desc")
|
|
||||||
.setDrmSchemes(Collections.singletonList(new DrmScheme(C.WIDEVINE_UUID, null)))
|
|
||||||
.setMedia("www.google.com")
|
|
||||||
.build();
|
|
||||||
ExoCastMessage message =
|
|
||||||
new ExoCastMessage.AddItems(
|
|
||||||
C.INDEX_UNSET,
|
|
||||||
Collections.singletonList(itemWithoutOptionalFields),
|
|
||||||
new ShuffleOrder.UnshuffledShuffleOrder(1));
|
|
||||||
JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0));
|
|
||||||
JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS);
|
|
||||||
JSONArray items = arguments.getJSONArray(KEY_ITEMS);
|
|
||||||
|
|
||||||
assertJsonEqualsMediaItem(items.getJSONObject(/* index= */ 0), itemWithoutOptionalFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void addItems_withAllItemFields_addsFieldsToJson() throws JSONException {
|
|
||||||
HashMap<String, String> headersMedia = new HashMap<>();
|
|
||||||
headersMedia.put("header1", "value1");
|
|
||||||
headersMedia.put("header2", "value2");
|
|
||||||
UriBundle media = new UriBundle(Uri.parse("www.google.com"), headersMedia);
|
|
||||||
|
|
||||||
HashMap<String, String> headersWidevine = new HashMap<>();
|
|
||||||
headersWidevine.put("widevine", "value");
|
|
||||||
UriBundle widevingUriBundle = new UriBundle(Uri.parse("www.widevine.com"), headersWidevine);
|
|
||||||
|
|
||||||
HashMap<String, String> headersPlayready = new HashMap<>();
|
|
||||||
headersPlayready.put("playready", "value");
|
|
||||||
UriBundle playreadyUriBundle = new UriBundle(Uri.parse("www.playready.com"), headersPlayready);
|
|
||||||
|
|
||||||
DrmScheme[] drmSchemes =
|
|
||||||
new DrmScheme[] {
|
|
||||||
new DrmScheme(C.WIDEVINE_UUID, widevingUriBundle),
|
|
||||||
new DrmScheme(C.PLAYREADY_UUID, playreadyUriBundle)
|
|
||||||
};
|
|
||||||
MediaItem itemWithAllFields =
|
|
||||||
new MediaItem.Builder()
|
|
||||||
.setTitle("title")
|
|
||||||
.setMimeType(MimeTypes.VIDEO_MP4)
|
|
||||||
.setDescription("desc")
|
|
||||||
.setStartPositionUs(3)
|
|
||||||
.setEndPositionUs(10)
|
|
||||||
.setDrmSchemes(Arrays.asList(drmSchemes))
|
|
||||||
.setMedia(media)
|
|
||||||
.build();
|
|
||||||
ExoCastMessage message =
|
|
||||||
new ExoCastMessage.AddItems(
|
|
||||||
C.INDEX_UNSET,
|
|
||||||
Collections.singletonList(itemWithAllFields),
|
|
||||||
new ShuffleOrder.UnshuffledShuffleOrder(1));
|
|
||||||
JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0));
|
|
||||||
JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS);
|
|
||||||
JSONArray items = arguments.getJSONArray(KEY_ITEMS);
|
|
||||||
|
|
||||||
assertJsonEqualsMediaItem(items.getJSONObject(/* index= */ 0), itemWithAllFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void addItems_withShuffleOrder_producesExpectedJson() throws JSONException {
|
|
||||||
MediaItem.Builder builder = new MediaItem.Builder();
|
|
||||||
MediaItem sampleItem1 = builder.build();
|
|
||||||
MediaItem sampleItem2 = builder.build();
|
|
||||||
MediaItem sampleItem3 = builder.build();
|
|
||||||
MediaItem sampleItem4 = builder.build();
|
|
||||||
|
|
||||||
ExoCastMessage message =
|
|
||||||
new ExoCastMessage.AddItems(
|
|
||||||
C.INDEX_UNSET,
|
|
||||||
Arrays.asList(sampleItem1, sampleItem2, sampleItem3, sampleItem4),
|
|
||||||
new ShuffleOrder.DefaultShuffleOrder(new int[] {2, 1, 3, 0}, /* randomSeed= */ 0));
|
|
||||||
JSONObject arguments =
|
|
||||||
new JSONObject(message.toJsonString(/* sequenceNumber= */ 0)).getJSONObject(KEY_ARGS);
|
|
||||||
JSONArray shuffledIndices = arguments.getJSONArray(KEY_SHUFFLE_ORDER);
|
|
||||||
assertThat(shuffledIndices.getInt(0)).isEqualTo(2);
|
|
||||||
assertThat(shuffledIndices.getInt(1)).isEqualTo(1);
|
|
||||||
assertThat(shuffledIndices.getInt(2)).isEqualTo(3);
|
|
||||||
assertThat(shuffledIndices.getInt(3)).isEqualTo(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void moveItem_producesExpectedJson() throws JSONException {
|
|
||||||
ExoCastMessage message =
|
|
||||||
new ExoCastMessage.MoveItem(
|
|
||||||
new UUID(0, 1),
|
|
||||||
/* index= */ 3,
|
|
||||||
new ShuffleOrder.DefaultShuffleOrder(new int[] {2, 1, 3, 0}, /* randomSeed= */ 0));
|
|
||||||
JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 1));
|
|
||||||
JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS);
|
|
||||||
|
|
||||||
assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(1);
|
|
||||||
assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_MOVE_ITEM);
|
|
||||||
assertThat(arguments.getString(KEY_UUID)).isEqualTo(new UUID(0, 1).toString());
|
|
||||||
assertThat(arguments.getInt(KEY_INDEX)).isEqualTo(3);
|
|
||||||
JSONArray shuffledIndices = arguments.getJSONArray(KEY_SHUFFLE_ORDER);
|
|
||||||
assertThat(shuffledIndices.getInt(0)).isEqualTo(2);
|
|
||||||
assertThat(shuffledIndices.getInt(1)).isEqualTo(1);
|
|
||||||
assertThat(shuffledIndices.getInt(2)).isEqualTo(3);
|
|
||||||
assertThat(shuffledIndices.getInt(3)).isEqualTo(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void removeItems_withSingleItem_producesExpectedJson() throws JSONException {
|
|
||||||
ExoCastMessage message =
|
|
||||||
new ExoCastMessage.RemoveItems(Collections.singletonList(new UUID(0, 1)));
|
|
||||||
JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0));
|
|
||||||
JSONArray uuids = messageAsJson.getJSONObject(KEY_ARGS).getJSONArray(KEY_UUIDS);
|
|
||||||
|
|
||||||
assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0);
|
|
||||||
assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_REMOVE_ITEMS);
|
|
||||||
assertThat(uuids.length()).isEqualTo(1);
|
|
||||||
assertThat(uuids.getString(0)).isEqualTo(new UUID(0, 1).toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void removeItems_withMultipleItems_producesExpectedJson() throws JSONException {
|
|
||||||
ExoCastMessage message =
|
|
||||||
new ExoCastMessage.RemoveItems(
|
|
||||||
Arrays.asList(new UUID(0, 1), new UUID(0, 2), new UUID(0, 3)));
|
|
||||||
JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0));
|
|
||||||
JSONArray uuids = messageAsJson.getJSONObject(KEY_ARGS).getJSONArray(KEY_UUIDS);
|
|
||||||
|
|
||||||
assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0);
|
|
||||||
assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_REMOVE_ITEMS);
|
|
||||||
assertThat(uuids.length()).isEqualTo(3);
|
|
||||||
assertThat(uuids.getString(0)).isEqualTo(new UUID(0, 1).toString());
|
|
||||||
assertThat(uuids.getString(1)).isEqualTo(new UUID(0, 2).toString());
|
|
||||||
assertThat(uuids.getString(2)).isEqualTo(new UUID(0, 3).toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void setPlayWhenReady_producesExpectedJson() throws JSONException {
|
|
||||||
ExoCastMessage message = new ExoCastMessage.SetPlayWhenReady(true);
|
|
||||||
JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0));
|
|
||||||
|
|
||||||
assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0);
|
|
||||||
assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SET_PLAY_WHEN_READY);
|
|
||||||
assertThat(messageAsJson.getJSONObject(KEY_ARGS).getBoolean(KEY_PLAY_WHEN_READY)).isTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void setRepeatMode_withRepeatModeOff_producesExpectedJson() throws JSONException {
|
|
||||||
ExoCastMessage message = new ExoCastMessage.SetRepeatMode(Player.REPEAT_MODE_OFF);
|
|
||||||
JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0));
|
|
||||||
|
|
||||||
assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0);
|
|
||||||
assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SET_REPEAT_MODE);
|
|
||||||
assertThat(messageAsJson.getJSONObject(KEY_ARGS).getString(KEY_REPEAT_MODE))
|
|
||||||
.isEqualTo(STR_REPEAT_MODE_OFF);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void setRepeatMode_withRepeatModeOne_producesExpectedJson() throws JSONException {
|
|
||||||
ExoCastMessage message = new ExoCastMessage.SetRepeatMode(Player.REPEAT_MODE_ONE);
|
|
||||||
JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0));
|
|
||||||
|
|
||||||
assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0);
|
|
||||||
assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SET_REPEAT_MODE);
|
|
||||||
assertThat(messageAsJson.getJSONObject(KEY_ARGS).getString(KEY_REPEAT_MODE))
|
|
||||||
.isEqualTo(STR_REPEAT_MODE_ONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void setRepeatMode_withRepeatModeAll_producesExpectedJson() throws JSONException {
|
|
||||||
ExoCastMessage message = new ExoCastMessage.SetRepeatMode(Player.REPEAT_MODE_ALL);
|
|
||||||
JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0));
|
|
||||||
|
|
||||||
assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0);
|
|
||||||
assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SET_REPEAT_MODE);
|
|
||||||
assertThat(messageAsJson.getJSONObject(KEY_ARGS).getString(KEY_REPEAT_MODE))
|
|
||||||
.isEqualTo(STR_REPEAT_MODE_ALL);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void setShuffleModeEnabled_producesExpectedJson() throws JSONException {
|
|
||||||
ExoCastMessage message =
|
|
||||||
new ExoCastMessage.SetShuffleModeEnabled(/* shuffleModeEnabled= */ false);
|
|
||||||
JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0));
|
|
||||||
|
|
||||||
assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0);
|
|
||||||
assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SET_SHUFFLE_MODE_ENABLED);
|
|
||||||
assertThat(messageAsJson.getJSONObject(KEY_ARGS).getBoolean(KEY_SHUFFLE_MODE_ENABLED))
|
|
||||||
.isFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void seekTo_withPositionInItem_addsPositionField() throws JSONException {
|
|
||||||
ExoCastMessage message = new ExoCastMessage.SeekTo(new UUID(0, 1), /* positionMs= */ 10);
|
|
||||||
JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0));
|
|
||||||
JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS);
|
|
||||||
|
|
||||||
assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0);
|
|
||||||
assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SEEK_TO);
|
|
||||||
assertThat(arguments.getString(KEY_UUID)).isEqualTo(new UUID(0, 1).toString());
|
|
||||||
assertThat(arguments.getLong(KEY_POSITION_MS)).isEqualTo(10);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void seekTo_withUnsetPosition_doesNotAddPositionField() throws JSONException {
|
|
||||||
ExoCastMessage message =
|
|
||||||
new ExoCastMessage.SeekTo(new UUID(0, 1), /* positionMs= */ C.TIME_UNSET);
|
|
||||||
JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0));
|
|
||||||
JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS);
|
|
||||||
|
|
||||||
assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0);
|
|
||||||
assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SEEK_TO);
|
|
||||||
assertThat(arguments.getString(KEY_UUID)).isEqualTo(new UUID(0, 1).toString());
|
|
||||||
assertThat(arguments.has(KEY_POSITION_MS)).isFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void setPlaybackParameters_producesExpectedJson() throws JSONException {
|
|
||||||
ExoCastMessage message =
|
|
||||||
new ExoCastMessage.SetPlaybackParameters(
|
|
||||||
new PlaybackParameters(/* speed= */ 0.5f, /* pitch= */ 2, /* skipSilence= */ false));
|
|
||||||
JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0));
|
|
||||||
JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS);
|
|
||||||
|
|
||||||
assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0);
|
|
||||||
assertThat(messageAsJson.getString(KEY_METHOD)).isEqualTo(METHOD_SET_PLAYBACK_PARAMETERS);
|
|
||||||
assertThat(arguments.getDouble(KEY_SPEED)).isEqualTo(0.5);
|
|
||||||
assertThat(arguments.getDouble(KEY_PITCH)).isEqualTo(2.0);
|
|
||||||
assertThat(arguments.getBoolean(KEY_SKIP_SILENCE)).isFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void setSelectionParameters_producesExpectedJson() throws JSONException {
|
|
||||||
ExoCastMessage message =
|
|
||||||
new ExoCastMessage.SetTrackSelectionParameters(
|
|
||||||
TrackSelectionParameters.DEFAULT
|
|
||||||
.buildUpon()
|
|
||||||
.setDisabledTextTrackSelectionFlags(
|
|
||||||
C.SELECTION_FLAG_AUTOSELECT | C.SELECTION_FLAG_DEFAULT)
|
|
||||||
.setSelectUndeterminedTextLanguage(true)
|
|
||||||
.setPreferredAudioLanguage("esp")
|
|
||||||
.setPreferredTextLanguage("deu")
|
|
||||||
.build());
|
|
||||||
JSONObject messageAsJson = new JSONObject(message.toJsonString(/* sequenceNumber= */ 0));
|
|
||||||
JSONObject arguments = messageAsJson.getJSONObject(KEY_ARGS);
|
|
||||||
|
|
||||||
assertThat(messageAsJson.getLong(KEY_SEQUENCE_NUMBER)).isEqualTo(0);
|
|
||||||
assertThat(messageAsJson.getString(KEY_METHOD))
|
|
||||||
.isEqualTo(METHOD_SET_TRACK_SELECTION_PARAMETERS);
|
|
||||||
assertThat(arguments.getBoolean(KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE)).isTrue();
|
|
||||||
assertThat(arguments.getString(KEY_PREFERRED_AUDIO_LANGUAGE)).isEqualTo("esp");
|
|
||||||
assertThat(arguments.getString(KEY_PREFERRED_TEXT_LANGUAGE)).isEqualTo("deu");
|
|
||||||
ArrayList<String> selectionFlagStrings = new ArrayList<>();
|
|
||||||
JSONArray selectionFlagsJson = arguments.getJSONArray(KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS);
|
|
||||||
for (int i = 0; i < selectionFlagsJson.length(); i++) {
|
|
||||||
selectionFlagStrings.add(selectionFlagsJson.getString(i));
|
|
||||||
}
|
|
||||||
assertThat(selectionFlagStrings).contains(ExoCastConstants.STR_SELECTION_FLAG_AUTOSELECT);
|
|
||||||
assertThat(selectionFlagStrings).doesNotContain(ExoCastConstants.STR_SELECTION_FLAG_FORCED);
|
|
||||||
assertThat(selectionFlagStrings).contains(ExoCastConstants.STR_SELECTION_FLAG_DEFAULT);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void assertJsonEqualsMediaItem(JSONObject itemAsJson, MediaItem mediaItem)
|
|
||||||
throws JSONException {
|
|
||||||
assertThat(itemAsJson.getString(KEY_UUID)).isEqualTo(mediaItem.uuid.toString());
|
|
||||||
assertThat(itemAsJson.getString(KEY_TITLE)).isEqualTo(mediaItem.title);
|
|
||||||
assertThat(itemAsJson.getString(KEY_MIME_TYPE)).isEqualTo(mediaItem.mimeType);
|
|
||||||
assertThat(itemAsJson.getString(KEY_DESCRIPTION)).isEqualTo(mediaItem.description);
|
|
||||||
assertJsonMatchesTimestamp(itemAsJson, KEY_START_POSITION_US, mediaItem.startPositionUs);
|
|
||||||
assertJsonMatchesTimestamp(itemAsJson, KEY_END_POSITION_US, mediaItem.endPositionUs);
|
|
||||||
assertJsonMatchesUriBundle(itemAsJson, KEY_MEDIA, mediaItem.media);
|
|
||||||
|
|
||||||
List<DrmScheme> drmSchemes = mediaItem.drmSchemes;
|
|
||||||
int drmSchemesLength = drmSchemes.size();
|
|
||||||
JSONArray drmSchemesAsJson = itemAsJson.getJSONArray(KEY_DRM_SCHEMES);
|
|
||||||
|
|
||||||
assertThat(drmSchemesAsJson.length()).isEqualTo(drmSchemesLength);
|
|
||||||
for (int i = 0; i < drmSchemesLength; i++) {
|
|
||||||
DrmScheme drmScheme = drmSchemes.get(i);
|
|
||||||
JSONObject drmSchemeAsJson = drmSchemesAsJson.getJSONObject(i);
|
|
||||||
|
|
||||||
assertThat(drmSchemeAsJson.getString(KEY_UUID)).isEqualTo(drmScheme.uuid.toString());
|
|
||||||
assertJsonMatchesUriBundle(drmSchemeAsJson, KEY_LICENSE_SERVER, drmScheme.licenseServer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void assertJsonMatchesUriBundle(
|
|
||||||
JSONObject jsonObject, String key, @Nullable UriBundle uriBundle) throws JSONException {
|
|
||||||
if (uriBundle == null) {
|
|
||||||
assertThat(jsonObject.has(key)).isFalse();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
JSONObject uriBundleAsJson = jsonObject.getJSONObject(key);
|
|
||||||
assertThat(uriBundleAsJson.getString(KEY_URI)).isEqualTo(uriBundle.uri.toString());
|
|
||||||
Map<String, String> requestHeaders = uriBundle.requestHeaders;
|
|
||||||
JSONObject requestHeadersAsJson = uriBundleAsJson.getJSONObject(KEY_REQUEST_HEADERS);
|
|
||||||
|
|
||||||
assertThat(requestHeadersAsJson.length()).isEqualTo(requestHeaders.size());
|
|
||||||
for (String headerKey : requestHeaders.keySet()) {
|
|
||||||
assertThat(requestHeadersAsJson.getString(headerKey))
|
|
||||||
.isEqualTo(requestHeaders.get(headerKey));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void assertJsonMatchesTimestamp(JSONObject object, String key, long timestamp)
|
|
||||||
throws JSONException {
|
|
||||||
if (timestamp == C.TIME_UNSET) {
|
|
||||||
assertThat(object.has(key)).isFalse();
|
|
||||||
} else {
|
|
||||||
assertThat(object.getLong(key)).isEqualTo(timestamp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -1,466 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.exoplayer2.ext.cast;
|
|
||||||
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|
||||||
import com.google.android.exoplayer2.C;
|
|
||||||
import com.google.android.exoplayer2.Player;
|
|
||||||
import com.google.android.exoplayer2.Timeline;
|
|
||||||
import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.UUID;
|
|
||||||
import org.junit.Before;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
|
|
||||||
/** Unit test for {@link ExoCastTimeline}. */
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
|
||||||
public class ExoCastTimelineTest {
|
|
||||||
|
|
||||||
private MediaItem mediaItem1;
|
|
||||||
private MediaItem mediaItem2;
|
|
||||||
private MediaItem mediaItem3;
|
|
||||||
private MediaItem mediaItem4;
|
|
||||||
private MediaItem mediaItem5;
|
|
||||||
|
|
||||||
@Before
|
|
||||||
public void setUp() {
|
|
||||||
MediaItem.Builder builder = new MediaItem.Builder();
|
|
||||||
mediaItem1 = builder.setUuid(asUUID(1)).build();
|
|
||||||
mediaItem2 = builder.setUuid(asUUID(2)).build();
|
|
||||||
mediaItem3 = builder.setUuid(asUUID(3)).build();
|
|
||||||
mediaItem4 = builder.setUuid(asUUID(4)).build();
|
|
||||||
mediaItem5 = builder.setUuid(asUUID(5)).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void getWindowCount_withNoItems_producesExpectedCount() {
|
|
||||||
ExoCastTimeline timeline =
|
|
||||||
ExoCastTimeline.createTimelineFor(
|
|
||||||
Collections.emptyList(), Collections.emptyMap(), new DefaultShuffleOrder(0));
|
|
||||||
|
|
||||||
assertThat(timeline.getWindowCount()).isEqualTo(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void getWindowCount_withFiveItems_producesExpectedCount() {
|
|
||||||
ExoCastTimeline timeline =
|
|
||||||
ExoCastTimeline.createTimelineFor(
|
|
||||||
Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5),
|
|
||||||
Collections.emptyMap(),
|
|
||||||
new DefaultShuffleOrder(5));
|
|
||||||
|
|
||||||
assertThat(timeline.getWindowCount()).isEqualTo(5);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void getWindow_withNoMediaItemInfo_returnsEmptyWindow() {
|
|
||||||
ExoCastTimeline timeline =
|
|
||||||
ExoCastTimeline.createTimelineFor(
|
|
||||||
Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5),
|
|
||||||
Collections.emptyMap(),
|
|
||||||
new DefaultShuffleOrder(5));
|
|
||||||
Timeline.Window window = timeline.getWindow(2, new Timeline.Window(), /* setTag= */ true);
|
|
||||||
|
|
||||||
assertThat(window.tag).isNull();
|
|
||||||
assertThat(window.presentationStartTimeMs).isEqualTo(C.TIME_UNSET);
|
|
||||||
assertThat(window.windowStartTimeMs).isEqualTo(C.TIME_UNSET);
|
|
||||||
assertThat(window.isSeekable).isFalse();
|
|
||||||
assertThat(window.isDynamic).isTrue();
|
|
||||||
assertThat(window.defaultPositionUs).isEqualTo(0L);
|
|
||||||
assertThat(window.durationUs).isEqualTo(C.TIME_UNSET);
|
|
||||||
assertThat(window.firstPeriodIndex).isEqualTo(2);
|
|
||||||
assertThat(window.lastPeriodIndex).isEqualTo(2);
|
|
||||||
assertThat(window.positionInFirstPeriodUs).isEqualTo(0L);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void getWindow_withMediaItemInfo_returnsPopulatedWindow() {
|
|
||||||
MediaItem populatedMediaItem = new MediaItem.Builder().setAttachment("attachment").build();
|
|
||||||
HashMap<UUID, MediaItemInfo> mediaItemInfos = new HashMap<>();
|
|
||||||
MediaItemInfo.Period period1 =
|
|
||||||
new MediaItemInfo.Period("id1", /* durationUs= */ 1000000L, /* positionInWindowUs= */ 0L);
|
|
||||||
MediaItemInfo.Period period2 =
|
|
||||||
new MediaItemInfo.Period(
|
|
||||||
"id2", /* durationUs= */ 5000000L, /* positionInWindowUs= */ 1000000L);
|
|
||||||
mediaItemInfos.put(
|
|
||||||
populatedMediaItem.uuid,
|
|
||||||
new MediaItemInfo(
|
|
||||||
/* windowDurationUs= */ 4000000L,
|
|
||||||
/* defaultStartPositionUs= */ 20L,
|
|
||||||
Arrays.asList(period1, period2),
|
|
||||||
/* positionInFirstPeriodUs= */ 500L,
|
|
||||||
/* isSeekable= */ true,
|
|
||||||
/* isDynamic= */ false));
|
|
||||||
ExoCastTimeline timeline =
|
|
||||||
ExoCastTimeline.createTimelineFor(
|
|
||||||
Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, populatedMediaItem),
|
|
||||||
mediaItemInfos,
|
|
||||||
new DefaultShuffleOrder(5));
|
|
||||||
Timeline.Window window = timeline.getWindow(4, new Timeline.Window(), /* setTag= */ true);
|
|
||||||
|
|
||||||
assertThat(window.tag).isSameInstanceAs(populatedMediaItem.attachment);
|
|
||||||
assertThat(window.presentationStartTimeMs).isEqualTo(C.TIME_UNSET);
|
|
||||||
assertThat(window.windowStartTimeMs).isEqualTo(C.TIME_UNSET);
|
|
||||||
assertThat(window.isSeekable).isTrue();
|
|
||||||
assertThat(window.isDynamic).isFalse();
|
|
||||||
assertThat(window.defaultPositionUs).isEqualTo(20L);
|
|
||||||
assertThat(window.durationUs).isEqualTo(4000000L);
|
|
||||||
assertThat(window.firstPeriodIndex).isEqualTo(4);
|
|
||||||
assertThat(window.lastPeriodIndex).isEqualTo(5);
|
|
||||||
assertThat(window.positionInFirstPeriodUs).isEqualTo(500L);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void getPeriodCount_producesExpectedOutput() {
|
|
||||||
HashMap<UUID, MediaItemInfo> mediaItemInfos = new HashMap<>();
|
|
||||||
MediaItemInfo.Period period1 =
|
|
||||||
new MediaItemInfo.Period(
|
|
||||||
"id1", /* durationUs= */ 5000000L, /* positionInWindowUs= */ 1000000L);
|
|
||||||
MediaItemInfo.Period period2 =
|
|
||||||
new MediaItemInfo.Period(
|
|
||||||
"id2", /* durationUs= */ 5000000L, /* positionInWindowUs= */ 6000000L);
|
|
||||||
mediaItemInfos.put(
|
|
||||||
asUUID(2),
|
|
||||||
new MediaItemInfo(
|
|
||||||
/* windowDurationUs= */ 7000000L,
|
|
||||||
/* defaultStartPositionUs= */ 20L,
|
|
||||||
Arrays.asList(period1, period2),
|
|
||||||
/* positionInFirstPeriodUs= */ 0L,
|
|
||||||
/* isSeekable= */ true,
|
|
||||||
/* isDynamic= */ false));
|
|
||||||
ExoCastTimeline timeline =
|
|
||||||
ExoCastTimeline.createTimelineFor(
|
|
||||||
Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5),
|
|
||||||
mediaItemInfos,
|
|
||||||
new DefaultShuffleOrder(5));
|
|
||||||
|
|
||||||
assertThat(timeline.getPeriodCount()).isEqualTo(6);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void getPeriod_forPopulatedPeriod_producesExpectedOutput() {
|
|
||||||
HashMap<UUID, MediaItemInfo> mediaItemInfos = new HashMap<>();
|
|
||||||
MediaItemInfo.Period period1 =
|
|
||||||
new MediaItemInfo.Period(
|
|
||||||
"id1", /* durationUs= */ 4000000L, /* positionInWindowUs= */ 1000000L);
|
|
||||||
MediaItemInfo.Period period2 =
|
|
||||||
new MediaItemInfo.Period(
|
|
||||||
"id2", /* durationUs= */ 5000000L, /* positionInWindowUs= */ 6000000L);
|
|
||||||
mediaItemInfos.put(
|
|
||||||
asUUID(5),
|
|
||||||
new MediaItemInfo(
|
|
||||||
/* windowDurationUs= */ 7000000L,
|
|
||||||
/* defaultStartPositionUs= */ 20L,
|
|
||||||
Arrays.asList(period1, period2),
|
|
||||||
/* positionInFirstPeriodUs= */ 0L,
|
|
||||||
/* isSeekable= */ true,
|
|
||||||
/* isDynamic= */ false));
|
|
||||||
ExoCastTimeline timeline =
|
|
||||||
ExoCastTimeline.createTimelineFor(
|
|
||||||
Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5),
|
|
||||||
mediaItemInfos,
|
|
||||||
new DefaultShuffleOrder(5));
|
|
||||||
Timeline.Period period =
|
|
||||||
timeline.getPeriod(/* periodIndex= */ 5, new Timeline.Period(), /* setIds= */ true);
|
|
||||||
Object periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 5);
|
|
||||||
|
|
||||||
assertThat(period.durationUs).isEqualTo(5000000L);
|
|
||||||
assertThat(period.windowIndex).isEqualTo(4);
|
|
||||||
assertThat(period.id).isEqualTo("id2");
|
|
||||||
assertThat(period.uid).isEqualTo(periodUid);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void getPeriod_forEmptyPeriod_producesExpectedOutput() {
|
|
||||||
ExoCastTimeline timeline =
|
|
||||||
ExoCastTimeline.createTimelineFor(
|
|
||||||
Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5),
|
|
||||||
Collections.emptyMap(),
|
|
||||||
new DefaultShuffleOrder(5));
|
|
||||||
Timeline.Period period = timeline.getPeriod(2, new Timeline.Period(), /* setIds= */ true);
|
|
||||||
Object uid = timeline.getUidOfPeriod(/* periodIndex= */ 2);
|
|
||||||
|
|
||||||
assertThat(period.durationUs).isEqualTo(C.TIME_UNSET);
|
|
||||||
assertThat(period.windowIndex).isEqualTo(2);
|
|
||||||
assertThat(period.id).isEqualTo(MediaItemInfo.EMPTY.periods.get(0).id);
|
|
||||||
assertThat(period.uid).isEqualTo(uid);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void getIndexOfPeriod_worksAcrossDifferentTimelines() {
|
|
||||||
MediaItemInfo.Period period1 =
|
|
||||||
new MediaItemInfo.Period(
|
|
||||||
"id1", /* durationUs= */ 4000000L, /* positionInWindowUs= */ 1000000L);
|
|
||||||
MediaItemInfo.Period period2 =
|
|
||||||
new MediaItemInfo.Period(
|
|
||||||
"id2", /* durationUs= */ 5000000L, /* positionInWindowUs= */ 1000000L);
|
|
||||||
|
|
||||||
HashMap<UUID, MediaItemInfo> mediaItemInfos1 = new HashMap<>();
|
|
||||||
mediaItemInfos1.put(
|
|
||||||
asUUID(1),
|
|
||||||
new MediaItemInfo(
|
|
||||||
/* windowDurationUs= */ 5000000L,
|
|
||||||
/* defaultStartPositionUs= */ 20L,
|
|
||||||
Collections.singletonList(period2),
|
|
||||||
/* positionInFirstPeriodUs= */ 0L,
|
|
||||||
/* isSeekable= */ true,
|
|
||||||
/* isDynamic= */ false));
|
|
||||||
ExoCastTimeline timeline1 =
|
|
||||||
ExoCastTimeline.createTimelineFor(
|
|
||||||
Arrays.asList(mediaItem1, mediaItem2), mediaItemInfos1, new DefaultShuffleOrder(2));
|
|
||||||
|
|
||||||
HashMap<UUID, MediaItemInfo> mediaItemInfos2 = new HashMap<>();
|
|
||||||
mediaItemInfos2.put(
|
|
||||||
asUUID(1),
|
|
||||||
new MediaItemInfo(
|
|
||||||
/* windowDurationUs= */ 7000000L,
|
|
||||||
/* defaultStartPositionUs= */ 20L,
|
|
||||||
Arrays.asList(period1, period2),
|
|
||||||
/* positionInFirstPeriodUs= */ 0L,
|
|
||||||
/* isSeekable= */ true,
|
|
||||||
/* isDynamic= */ false));
|
|
||||||
ExoCastTimeline timeline2 =
|
|
||||||
ExoCastTimeline.createTimelineFor(
|
|
||||||
Arrays.asList(mediaItem2, mediaItem1, mediaItem3, mediaItem4, mediaItem5),
|
|
||||||
mediaItemInfos2,
|
|
||||||
new DefaultShuffleOrder(5));
|
|
||||||
Object uidOfFirstPeriod = timeline1.getUidOfPeriod(0);
|
|
||||||
|
|
||||||
assertThat(timeline1.getIndexOfPeriod(uidOfFirstPeriod)).isEqualTo(0);
|
|
||||||
assertThat(timeline2.getIndexOfPeriod(uidOfFirstPeriod)).isEqualTo(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void getIndexOfPeriod_forLastPeriod_producesExpectedOutput() {
|
|
||||||
MediaItemInfo.Period period1 =
|
|
||||||
new MediaItemInfo.Period(
|
|
||||||
"id1", /* durationUs= */ 4000000L, /* positionInWindowUs= */ 1000000L);
|
|
||||||
MediaItemInfo.Period period2 =
|
|
||||||
new MediaItemInfo.Period(
|
|
||||||
"id2", /* durationUs= */ 5000000L, /* positionInWindowUs= */ 1000000L);
|
|
||||||
|
|
||||||
HashMap<UUID, MediaItemInfo> mediaItemInfos1 = new HashMap<>();
|
|
||||||
mediaItemInfos1.put(
|
|
||||||
asUUID(5),
|
|
||||||
new MediaItemInfo(
|
|
||||||
/* windowDurationUs= */ 4000000L,
|
|
||||||
/* defaultStartPositionUs= */ 20L,
|
|
||||||
Collections.singletonList(period2),
|
|
||||||
/* positionInFirstPeriodUs= */ 0L,
|
|
||||||
/* isSeekable= */ true,
|
|
||||||
/* isDynamic= */ false));
|
|
||||||
ExoCastTimeline singlePeriodTimeline =
|
|
||||||
ExoCastTimeline.createTimelineFor(
|
|
||||||
Collections.singletonList(mediaItem5), mediaItemInfos1, new DefaultShuffleOrder(1));
|
|
||||||
Object periodUid = singlePeriodTimeline.getUidOfPeriod(0);
|
|
||||||
|
|
||||||
HashMap<UUID, MediaItemInfo> mediaItemInfos2 = new HashMap<>();
|
|
||||||
mediaItemInfos2.put(
|
|
||||||
asUUID(5),
|
|
||||||
new MediaItemInfo(
|
|
||||||
/* windowDurationUs= */ 7000000L,
|
|
||||||
/* defaultStartPositionUs= */ 20L,
|
|
||||||
Arrays.asList(period1, period2),
|
|
||||||
/* positionInFirstPeriodUs= */ 0L,
|
|
||||||
/* isSeekable= */ true,
|
|
||||||
/* isDynamic= */ false));
|
|
||||||
ExoCastTimeline timeline =
|
|
||||||
ExoCastTimeline.createTimelineFor(
|
|
||||||
Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5),
|
|
||||||
mediaItemInfos2,
|
|
||||||
new DefaultShuffleOrder(5));
|
|
||||||
|
|
||||||
assertThat(timeline.getIndexOfPeriod(periodUid)).isEqualTo(5);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void getUidOfPeriod_withInvalidUid_returnsUnsetIndex() {
|
|
||||||
ExoCastTimeline timeline =
|
|
||||||
ExoCastTimeline.createTimelineFor(
|
|
||||||
Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5),
|
|
||||||
Collections.emptyMap(),
|
|
||||||
new DefaultShuffleOrder(/* length= */ 5));
|
|
||||||
|
|
||||||
assertThat(timeline.getIndexOfPeriod(new Object())).isEqualTo(C.INDEX_UNSET);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void getFirstWindowIndex_returnsIndexAccordingToShuffleMode() {
|
|
||||||
ExoCastTimeline timeline =
|
|
||||||
ExoCastTimeline.createTimelineFor(
|
|
||||||
Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5),
|
|
||||||
Collections.emptyMap(),
|
|
||||||
new DefaultShuffleOrder(new int[] {1, 2, 0, 4, 3}, /* randomSeed= */ 0));
|
|
||||||
|
|
||||||
assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ false)).isEqualTo(0);
|
|
||||||
assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void getLastWindowIndex_returnsIndexAccordingToShuffleMode() {
|
|
||||||
ExoCastTimeline timeline =
|
|
||||||
ExoCastTimeline.createTimelineFor(
|
|
||||||
Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5),
|
|
||||||
Collections.emptyMap(),
|
|
||||||
new DefaultShuffleOrder(new int[] {1, 2, 0, 4, 3}, /* randomSeed= */ 0));
|
|
||||||
|
|
||||||
assertThat(timeline.getLastWindowIndex(/* shuffleModeEnabled= */ false)).isEqualTo(4);
|
|
||||||
assertThat(timeline.getLastWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void getNextWindowIndex_repeatModeOne_returnsSameIndex() {
|
|
||||||
ExoCastTimeline timeline =
|
|
||||||
ExoCastTimeline.createTimelineFor(
|
|
||||||
Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5),
|
|
||||||
Collections.emptyMap(),
|
|
||||||
new DefaultShuffleOrder(5));
|
|
||||||
|
|
||||||
for (int i = 0; i < 5; i++) {
|
|
||||||
assertThat(
|
|
||||||
timeline.getNextWindowIndex(
|
|
||||||
i, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true))
|
|
||||||
.isEqualTo(i);
|
|
||||||
assertThat(
|
|
||||||
timeline.getNextWindowIndex(
|
|
||||||
i, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false))
|
|
||||||
.isEqualTo(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void getNextWindowIndex_onLastIndex_returnsExpectedIndex() {
|
|
||||||
ExoCastTimeline timeline =
|
|
||||||
ExoCastTimeline.createTimelineFor(
|
|
||||||
Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5),
|
|
||||||
Collections.emptyMap(),
|
|
||||||
new DefaultShuffleOrder(new int[] {1, 2, 0, 4, 3}, /* randomSeed= */ 0));
|
|
||||||
|
|
||||||
// Shuffle mode disabled:
|
|
||||||
assertThat(
|
|
||||||
timeline.getNextWindowIndex(
|
|
||||||
/* windowIndex= */ 4, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false))
|
|
||||||
.isEqualTo(C.INDEX_UNSET);
|
|
||||||
assertThat(
|
|
||||||
timeline.getNextWindowIndex(
|
|
||||||
/* windowIndex= */ 4, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false))
|
|
||||||
.isEqualTo(0);
|
|
||||||
// Shuffle mode enabled:
|
|
||||||
assertThat(
|
|
||||||
timeline.getNextWindowIndex(
|
|
||||||
/* windowIndex= */ 3, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true))
|
|
||||||
.isEqualTo(C.INDEX_UNSET);
|
|
||||||
assertThat(
|
|
||||||
timeline.getNextWindowIndex(
|
|
||||||
/* windowIndex= */ 3, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true))
|
|
||||||
.isEqualTo(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void getNextWindowIndex_inMiddleOfQueue_returnsNextIndex() {
|
|
||||||
ExoCastTimeline timeline =
|
|
||||||
ExoCastTimeline.createTimelineFor(
|
|
||||||
Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5),
|
|
||||||
Collections.emptyMap(),
|
|
||||||
new DefaultShuffleOrder(new int[] {1, 2, 0, 4, 3}, /* randomSeed= */ 0));
|
|
||||||
|
|
||||||
// Shuffle mode disabled:
|
|
||||||
assertThat(
|
|
||||||
timeline.getNextWindowIndex(
|
|
||||||
/* windowIndex= */ 2, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false))
|
|
||||||
.isEqualTo(3);
|
|
||||||
// Shuffle mode enabled:
|
|
||||||
assertThat(
|
|
||||||
timeline.getNextWindowIndex(
|
|
||||||
/* windowIndex= */ 2, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true))
|
|
||||||
.isEqualTo(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void getPreviousWindowIndex_repeatModeOne_returnsSameIndex() {
|
|
||||||
ExoCastTimeline timeline =
|
|
||||||
ExoCastTimeline.createTimelineFor(
|
|
||||||
Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5),
|
|
||||||
Collections.emptyMap(),
|
|
||||||
new DefaultShuffleOrder(new int[] {1, 2, 0, 4, 3}, /* randomSeed= */ 0));
|
|
||||||
|
|
||||||
for (int i = 0; i < 5; i++) {
|
|
||||||
assertThat(
|
|
||||||
timeline.getPreviousWindowIndex(
|
|
||||||
/* windowIndex= */ i, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true))
|
|
||||||
.isEqualTo(i);
|
|
||||||
assertThat(
|
|
||||||
timeline.getPreviousWindowIndex(
|
|
||||||
/* windowIndex= */ i, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false))
|
|
||||||
.isEqualTo(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void getPreviousWindowIndex_onFirstIndex_returnsExpectedIndex() {
|
|
||||||
ExoCastTimeline timeline =
|
|
||||||
ExoCastTimeline.createTimelineFor(
|
|
||||||
Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5),
|
|
||||||
Collections.emptyMap(),
|
|
||||||
new DefaultShuffleOrder(new int[] {1, 2, 0, 4, 3}, /* randomSeed= */ 0));
|
|
||||||
|
|
||||||
// Shuffle mode disabled:
|
|
||||||
assertThat(
|
|
||||||
timeline.getPreviousWindowIndex(
|
|
||||||
/* windowIndex= */ 0, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false))
|
|
||||||
.isEqualTo(C.INDEX_UNSET);
|
|
||||||
assertThat(
|
|
||||||
timeline.getPreviousWindowIndex(
|
|
||||||
/* windowIndex= */ 0, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false))
|
|
||||||
.isEqualTo(4);
|
|
||||||
// Shuffle mode enabled:
|
|
||||||
assertThat(
|
|
||||||
timeline.getPreviousWindowIndex(
|
|
||||||
/* windowIndex= */ 1, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true))
|
|
||||||
.isEqualTo(C.INDEX_UNSET);
|
|
||||||
assertThat(
|
|
||||||
timeline.getPreviousWindowIndex(
|
|
||||||
/* windowIndex= */ 1, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true))
|
|
||||||
.isEqualTo(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void getPreviousWindowIndex_inMiddleOfQueue_returnsPreviousIndex() {
|
|
||||||
ExoCastTimeline timeline =
|
|
||||||
ExoCastTimeline.createTimelineFor(
|
|
||||||
Arrays.asList(mediaItem1, mediaItem2, mediaItem3, mediaItem4, mediaItem5),
|
|
||||||
Collections.emptyMap(),
|
|
||||||
new DefaultShuffleOrder(new int[] {1, 2, 0, 4, 3}, /* randomSeed= */ 0));
|
|
||||||
|
|
||||||
assertThat(
|
|
||||||
timeline.getPreviousWindowIndex(
|
|
||||||
/* windowIndex= */ 4, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false))
|
|
||||||
.isEqualTo(3);
|
|
||||||
assertThat(
|
|
||||||
timeline.getPreviousWindowIndex(
|
|
||||||
/* windowIndex= */ 4, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true))
|
|
||||||
.isEqualTo(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static UUID asUUID(long number) {
|
|
||||||
return new UUID(/* mostSigBits= */ 0L, number);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,378 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.exoplayer2.ext.cast;
|
|
||||||
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DEFAULT_START_POSITION_US;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DISCONTINUITY_REASON;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_DURATION_US;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ERROR_MESSAGE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ID;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_IS_DYNAMIC;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_IS_LOADING;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_IS_SEEKABLE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MEDIA_ITEMS_INFO;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_MEDIA_QUEUE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PERIODS;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PERIOD_ID;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PITCH;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAYBACK_PARAMETERS;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAYBACK_POSITION;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAYBACK_STATE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PLAY_WHEN_READY;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_POSITION_IN_FIRST_PERIOD_US;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_POSITION_MS;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_AUDIO_LANGUAGE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_PREFERRED_TEXT_LANGUAGE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_REPEAT_MODE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SEQUENCE_NUMBER;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_MODE_ENABLED;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SHUFFLE_ORDER;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SKIP_SILENCE;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_SPEED;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_TRACK_SELECTION_PARAMETERS;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_UUID;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_WINDOW_DURATION_US;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_DISCONTINUITY_REASON_SEEK;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_REPEAT_MODE_OFF;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_SELECTION_FLAG_FORCED;
|
|
||||||
import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.STR_STATE_BUFFERING;
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|
||||||
import com.google.android.exoplayer2.C;
|
|
||||||
import com.google.android.exoplayer2.PlaybackParameters;
|
|
||||||
import com.google.android.exoplayer2.Player;
|
|
||||||
import com.google.android.exoplayer2.Timeline;
|
|
||||||
import com.google.android.exoplayer2.source.ShuffleOrder;
|
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelectionParameters;
|
|
||||||
import com.google.android.exoplayer2.util.Util;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.UUID;
|
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
|
|
||||||
/** Unit tests for {@link ReceiverAppStateUpdate}. */
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
|
||||||
public class ReceiverAppStateUpdateTest {
|
|
||||||
|
|
||||||
private static final long MOCK_SEQUENCE_NUMBER = 1;
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void statusUpdate_withPlayWhenReady_producesExpectedUpdate() throws JSONException {
|
|
||||||
ReceiverAppStateUpdate stateUpdate =
|
|
||||||
ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER).setPlayWhenReady(true).build();
|
|
||||||
JSONObject stateMessage = createStateMessage().put(KEY_PLAY_WHEN_READY, true);
|
|
||||||
|
|
||||||
assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString()))
|
|
||||||
.isEqualTo(stateUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void statusUpdate_withPlaybackState_producesExpectedUpdate() throws JSONException {
|
|
||||||
ReceiverAppStateUpdate stateUpdate =
|
|
||||||
ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER)
|
|
||||||
.setPlaybackState(Player.STATE_BUFFERING)
|
|
||||||
.build();
|
|
||||||
JSONObject stateMessage = createStateMessage().put(KEY_PLAYBACK_STATE, STR_STATE_BUFFERING);
|
|
||||||
|
|
||||||
assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString()))
|
|
||||||
.isEqualTo(stateUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void statusUpdate_withMediaQueue_producesExpectedUpdate() throws JSONException {
|
|
||||||
HashMap<String, String> requestHeaders = new HashMap<>();
|
|
||||||
requestHeaders.put("key", "value");
|
|
||||||
MediaItem.UriBundle media = new MediaItem.UriBundle(Uri.parse("www.media.com"), requestHeaders);
|
|
||||||
MediaItem.DrmScheme drmScheme1 =
|
|
||||||
new MediaItem.DrmScheme(
|
|
||||||
C.WIDEVINE_UUID,
|
|
||||||
new MediaItem.UriBundle(Uri.parse("www.widevine.com"), requestHeaders));
|
|
||||||
MediaItem.DrmScheme drmScheme2 =
|
|
||||||
new MediaItem.DrmScheme(
|
|
||||||
C.PLAYREADY_UUID,
|
|
||||||
new MediaItem.UriBundle(Uri.parse("www.playready.com"), requestHeaders));
|
|
||||||
MediaItem item =
|
|
||||||
new MediaItem.Builder()
|
|
||||||
.setTitle("title")
|
|
||||||
.setDescription("description")
|
|
||||||
.setMedia(media)
|
|
||||||
.setDrmSchemes(Arrays.asList(drmScheme1, drmScheme2))
|
|
||||||
.setStartPositionUs(10)
|
|
||||||
.setEndPositionUs(20)
|
|
||||||
.build();
|
|
||||||
ReceiverAppStateUpdate stateUpdate =
|
|
||||||
ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER)
|
|
||||||
.setItems(Collections.singletonList(item))
|
|
||||||
.build();
|
|
||||||
JSONObject object =
|
|
||||||
createStateMessage()
|
|
||||||
.put(KEY_MEDIA_QUEUE, new JSONArray().put(ExoCastMessage.mediaItemAsJsonObject(item)));
|
|
||||||
|
|
||||||
assertThat(ReceiverAppStateUpdate.fromJsonMessage(object.toString())).isEqualTo(stateUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void statusUpdate_withRepeatMode_producesExpectedUpdate() throws JSONException {
|
|
||||||
ReceiverAppStateUpdate stateUpdate =
|
|
||||||
ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER)
|
|
||||||
.setRepeatMode(Player.REPEAT_MODE_OFF)
|
|
||||||
.build();
|
|
||||||
JSONObject stateMessage = createStateMessage().put(KEY_REPEAT_MODE, STR_REPEAT_MODE_OFF);
|
|
||||||
|
|
||||||
assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString()))
|
|
||||||
.isEqualTo(stateUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void statusUpdate_withShuffleModeEnabled_producesExpectedUpdate() throws JSONException {
|
|
||||||
ReceiverAppStateUpdate stateUpdate =
|
|
||||||
ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER).setShuffleModeEnabled(false).build();
|
|
||||||
JSONObject stateMessage = createStateMessage().put(KEY_SHUFFLE_MODE_ENABLED, false);
|
|
||||||
|
|
||||||
assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString()))
|
|
||||||
.isEqualTo(stateUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void statusUpdate_withIsLoading_producesExpectedUpdate() throws JSONException {
|
|
||||||
ReceiverAppStateUpdate stateUpdate =
|
|
||||||
ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER).setIsLoading(true).build();
|
|
||||||
JSONObject stateMessage = createStateMessage().put(KEY_IS_LOADING, true);
|
|
||||||
|
|
||||||
assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString()))
|
|
||||||
.isEqualTo(stateUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void statusUpdate_withPlaybackParameters_producesExpectedUpdate() throws JSONException {
|
|
||||||
ReceiverAppStateUpdate stateUpdate =
|
|
||||||
ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER)
|
|
||||||
.setPlaybackParameters(
|
|
||||||
new PlaybackParameters(
|
|
||||||
/* speed= */ .5f, /* pitch= */ .25f, /* skipSilence= */ false))
|
|
||||||
.build();
|
|
||||||
JSONObject playbackParamsJson =
|
|
||||||
new JSONObject().put(KEY_SPEED, .5).put(KEY_PITCH, .25).put(KEY_SKIP_SILENCE, false);
|
|
||||||
JSONObject stateMessage = createStateMessage().put(KEY_PLAYBACK_PARAMETERS, playbackParamsJson);
|
|
||||||
|
|
||||||
assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString()))
|
|
||||||
.isEqualTo(stateUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void statusUpdate_withTrackSelectionParameters_producesExpectedUpdate()
|
|
||||||
throws JSONException {
|
|
||||||
ReceiverAppStateUpdate stateUpdate =
|
|
||||||
ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER)
|
|
||||||
.setTrackSelectionParameters(
|
|
||||||
TrackSelectionParameters.DEFAULT
|
|
||||||
.buildUpon()
|
|
||||||
.setDisabledTextTrackSelectionFlags(
|
|
||||||
C.SELECTION_FLAG_FORCED | C.SELECTION_FLAG_DEFAULT)
|
|
||||||
.setPreferredAudioLanguage("esp")
|
|
||||||
.setPreferredTextLanguage("deu")
|
|
||||||
.setSelectUndeterminedTextLanguage(true)
|
|
||||||
.build())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
JSONArray selectionFlagsJson =
|
|
||||||
new JSONArray()
|
|
||||||
.put(ExoCastConstants.STR_SELECTION_FLAG_DEFAULT)
|
|
||||||
.put(STR_SELECTION_FLAG_FORCED);
|
|
||||||
JSONObject playbackParamsJson =
|
|
||||||
new JSONObject()
|
|
||||||
.put(KEY_DISABLED_TEXT_TRACK_SELECTION_FLAGS, selectionFlagsJson)
|
|
||||||
.put(KEY_PREFERRED_AUDIO_LANGUAGE, "esp")
|
|
||||||
.put(KEY_PREFERRED_TEXT_LANGUAGE, "deu")
|
|
||||||
.put(KEY_SELECT_UNDETERMINED_TEXT_LANGUAGE, true);
|
|
||||||
JSONObject object =
|
|
||||||
createStateMessage().put(KEY_TRACK_SELECTION_PARAMETERS, playbackParamsJson);
|
|
||||||
|
|
||||||
assertThat(ReceiverAppStateUpdate.fromJsonMessage(object.toString())).isEqualTo(stateUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void statusUpdate_withError_producesExpectedUpdate() throws JSONException {
|
|
||||||
ReceiverAppStateUpdate stateUpdate =
|
|
||||||
ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER)
|
|
||||||
.setErrorMessage("error message")
|
|
||||||
.build();
|
|
||||||
JSONObject stateMessage = createStateMessage().put(KEY_ERROR_MESSAGE, "error message");
|
|
||||||
|
|
||||||
assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString()))
|
|
||||||
.isEqualTo(stateUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void statusUpdate_withPlaybackPosition_producesExpectedUpdate() throws JSONException {
|
|
||||||
ReceiverAppStateUpdate stateUpdate =
|
|
||||||
ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER)
|
|
||||||
.setPlaybackPosition(
|
|
||||||
new UUID(/* mostSigBits= */ 0, /* leastSigBits= */ 1), "period", 10L)
|
|
||||||
.build();
|
|
||||||
JSONObject positionJson =
|
|
||||||
new JSONObject()
|
|
||||||
.put(KEY_UUID, new UUID(0, 1))
|
|
||||||
.put(KEY_POSITION_MS, 10)
|
|
||||||
.put(KEY_PERIOD_ID, "period");
|
|
||||||
JSONObject stateMessage = createStateMessage().put(KEY_PLAYBACK_POSITION, positionJson);
|
|
||||||
|
|
||||||
assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString()))
|
|
||||||
.isEqualTo(stateUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void statusUpdate_withDiscontinuity_producesExpectedUpdate() throws JSONException {
|
|
||||||
ReceiverAppStateUpdate stateUpdate =
|
|
||||||
ReceiverAppStateUpdate.builder(MOCK_SEQUENCE_NUMBER)
|
|
||||||
.setPlaybackPosition(
|
|
||||||
new UUID(/* mostSigBits= */ 0, /* leastSigBits= */ 1), "period", 10L)
|
|
||||||
.setDiscontinuityReason(Player.DISCONTINUITY_REASON_SEEK)
|
|
||||||
.build();
|
|
||||||
JSONObject positionJson =
|
|
||||||
new JSONObject()
|
|
||||||
.put(KEY_UUID, new UUID(0, 1))
|
|
||||||
.put(KEY_POSITION_MS, 10)
|
|
||||||
.put(KEY_PERIOD_ID, "period")
|
|
||||||
.put(KEY_DISCONTINUITY_REASON, STR_DISCONTINUITY_REASON_SEEK);
|
|
||||||
JSONObject stateMessage = createStateMessage().put(KEY_PLAYBACK_POSITION, positionJson);
|
|
||||||
|
|
||||||
assertThat(ReceiverAppStateUpdate.fromJsonMessage(stateMessage.toString()))
|
|
||||||
.isEqualTo(stateUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void statusUpdate_withMediaItemInfo_producesExpectedTimeline() throws JSONException {
|
|
||||||
MediaItem.Builder builder = new MediaItem.Builder();
|
|
||||||
MediaItem item1 = builder.setUuid(new UUID(0, 1)).build();
|
|
||||||
MediaItem item2 = builder.setUuid(new UUID(0, 2)).build();
|
|
||||||
|
|
||||||
JSONArray periodsJson = new JSONArray();
|
|
||||||
periodsJson
|
|
||||||
.put(new JSONObject().put(KEY_ID, "id1").put(KEY_DURATION_US, 5000000L))
|
|
||||||
.put(new JSONObject().put(KEY_ID, "id2").put(KEY_DURATION_US, 7000000L))
|
|
||||||
.put(new JSONObject().put(KEY_ID, "id3").put(KEY_DURATION_US, 6000000L));
|
|
||||||
JSONObject mediaItemInfoForUuid1 = new JSONObject();
|
|
||||||
mediaItemInfoForUuid1
|
|
||||||
.put(KEY_WINDOW_DURATION_US, 10000000L)
|
|
||||||
.put(KEY_DEFAULT_START_POSITION_US, 1000000L)
|
|
||||||
.put(KEY_PERIODS, periodsJson)
|
|
||||||
.put(KEY_POSITION_IN_FIRST_PERIOD_US, 2000000L)
|
|
||||||
.put(KEY_IS_DYNAMIC, false)
|
|
||||||
.put(KEY_IS_SEEKABLE, true);
|
|
||||||
JSONObject mediaItemInfoMapJson =
|
|
||||||
new JSONObject().put(new UUID(0, 1).toString(), mediaItemInfoForUuid1);
|
|
||||||
|
|
||||||
JSONObject receiverAppStateUpdateJson =
|
|
||||||
createStateMessage().put(KEY_MEDIA_ITEMS_INFO, mediaItemInfoMapJson);
|
|
||||||
ReceiverAppStateUpdate receiverAppStateUpdate =
|
|
||||||
ReceiverAppStateUpdate.fromJsonMessage(receiverAppStateUpdateJson.toString());
|
|
||||||
ExoCastTimeline timeline =
|
|
||||||
ExoCastTimeline.createTimelineFor(
|
|
||||||
Arrays.asList(item1, item2),
|
|
||||||
receiverAppStateUpdate.mediaItemsInformation,
|
|
||||||
new ShuffleOrder.DefaultShuffleOrder(
|
|
||||||
/* shuffledIndices= */ new int[] {1, 0}, /* randomSeed= */ 0));
|
|
||||||
Timeline.Window window0 =
|
|
||||||
timeline.getWindow(/* windowIndex= */ 0, new Timeline.Window(), /* setTag= */ true);
|
|
||||||
Timeline.Window window1 =
|
|
||||||
timeline.getWindow(/* windowIndex= */ 1, new Timeline.Window(), /* setTag= */ true);
|
|
||||||
Timeline.Period[] periods = new Timeline.Period[4];
|
|
||||||
for (int i = 0; i < 4; i++) {
|
|
||||||
periods[i] =
|
|
||||||
timeline.getPeriod(/* periodIndex= */ i, new Timeline.Period(), /* setIds= */ true);
|
|
||||||
}
|
|
||||||
|
|
||||||
assertThat(timeline.getWindowCount()).isEqualTo(2);
|
|
||||||
assertThat(window0.positionInFirstPeriodUs).isEqualTo(2000000L);
|
|
||||||
assertThat(window0.durationUs).isEqualTo(10000000L);
|
|
||||||
assertThat(window0.isDynamic).isFalse();
|
|
||||||
assertThat(window0.isSeekable).isTrue();
|
|
||||||
assertThat(window0.defaultPositionUs).isEqualTo(1000000L);
|
|
||||||
assertThat(window1.positionInFirstPeriodUs).isEqualTo(0L);
|
|
||||||
assertThat(window1.durationUs).isEqualTo(C.TIME_UNSET);
|
|
||||||
assertThat(window1.isDynamic).isTrue();
|
|
||||||
assertThat(window1.isSeekable).isFalse();
|
|
||||||
assertThat(window1.defaultPositionUs).isEqualTo(0L);
|
|
||||||
|
|
||||||
assertThat(timeline.getPeriodCount()).isEqualTo(4);
|
|
||||||
assertThat(periods[0].id).isEqualTo("id1");
|
|
||||||
assertThat(periods[0].getPositionInWindowUs()).isEqualTo(-2000000L);
|
|
||||||
assertThat(periods[0].durationUs).isEqualTo(5000000L);
|
|
||||||
assertThat(periods[1].id).isEqualTo("id2");
|
|
||||||
assertThat(periods[1].durationUs).isEqualTo(7000000L);
|
|
||||||
assertThat(periods[1].getPositionInWindowUs()).isEqualTo(3000000L);
|
|
||||||
assertThat(periods[2].id).isEqualTo("id3");
|
|
||||||
assertThat(periods[2].durationUs).isEqualTo(6000000L);
|
|
||||||
assertThat(periods[2].getPositionInWindowUs()).isEqualTo(10000000L);
|
|
||||||
assertThat(periods[3].durationUs).isEqualTo(C.TIME_UNSET);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void statusUpdate_withShuffleOrder_producesExpectedTimeline() throws JSONException {
|
|
||||||
MediaItem.Builder builder = new MediaItem.Builder();
|
|
||||||
JSONObject receiverAppStateUpdateJson =
|
|
||||||
createStateMessage().put(KEY_SHUFFLE_ORDER, new JSONArray(Arrays.asList(2, 3, 1, 0)));
|
|
||||||
ReceiverAppStateUpdate receiverAppStateUpdate =
|
|
||||||
ReceiverAppStateUpdate.fromJsonMessage(receiverAppStateUpdateJson.toString());
|
|
||||||
ExoCastTimeline timeline =
|
|
||||||
ExoCastTimeline.createTimelineFor(
|
|
||||||
/* mediaItems= */ Arrays.asList(
|
|
||||||
builder.build(), builder.build(), builder.build(), builder.build()),
|
|
||||||
/* mediaItemInfoMap= */ Collections.emptyMap(),
|
|
||||||
/* shuffleOrder= */ new ShuffleOrder.DefaultShuffleOrder(
|
|
||||||
Util.toArray(receiverAppStateUpdate.shuffleOrder), /* randomSeed= */ 0));
|
|
||||||
|
|
||||||
assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(2);
|
|
||||||
assertThat(
|
|
||||||
timeline.getNextWindowIndex(
|
|
||||||
/* windowIndex= */ 2,
|
|
||||||
/* repeatMode= */ Player.REPEAT_MODE_OFF,
|
|
||||||
/* shuffleModeEnabled= */ true))
|
|
||||||
.isEqualTo(3);
|
|
||||||
assertThat(
|
|
||||||
timeline.getNextWindowIndex(
|
|
||||||
/* windowIndex= */ 3,
|
|
||||||
/* repeatMode= */ Player.REPEAT_MODE_OFF,
|
|
||||||
/* shuffleModeEnabled= */ true))
|
|
||||||
.isEqualTo(1);
|
|
||||||
assertThat(
|
|
||||||
timeline.getNextWindowIndex(
|
|
||||||
/* windowIndex= */ 1,
|
|
||||||
/* repeatMode= */ Player.REPEAT_MODE_OFF,
|
|
||||||
/* shuffleModeEnabled= */ true))
|
|
||||||
.isEqualTo(0);
|
|
||||||
assertThat(
|
|
||||||
timeline.getNextWindowIndex(
|
|
||||||
/* windowIndex= */ 0,
|
|
||||||
/* repeatMode= */ Player.REPEAT_MODE_OFF,
|
|
||||||
/* shuffleModeEnabled= */ true))
|
|
||||||
.isEqualTo(C.INDEX_UNSET);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static JSONObject createStateMessage() throws JSONException {
|
|
||||||
return new JSONObject().put(KEY_SEQUENCE_NUMBER, MOCK_SEQUENCE_NUMBER);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user