diff --git a/cast_receiver_app/BUILD b/cast_receiver_app/BUILD deleted file mode 100644 index 2bd0526cdd..0000000000 --- a/cast_receiver_app/BUILD +++ /dev/null @@ -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"], -) diff --git a/cast_receiver_app/README.md b/cast_receiver_app/README.md deleted file mode 100644 index 6504cb4f94..0000000000 --- a/cast_receiver_app/README.md +++ /dev/null @@ -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 ... -``` diff --git a/cast_receiver_app/WORKSPACE b/cast_receiver_app/WORKSPACE deleted file mode 100644 index e6be3b9026..0000000000 --- a/cast_receiver_app/WORKSPACE +++ /dev/null @@ -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, -) - diff --git a/cast_receiver_app/app-desktop/html/index.css b/cast_receiver_app/app-desktop/html/index.css deleted file mode 100644 index ff77e1cbfa..0000000000 --- a/cast_receiver_app/app-desktop/html/index.css +++ /dev/null @@ -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); - } -} diff --git a/cast_receiver_app/app-desktop/html/index.html b/cast_receiver_app/app-desktop/html/index.html deleted file mode 100644 index 19a118913b..0000000000 --- a/cast_receiver_app/app-desktop/html/index.html +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - -
- -
-
-
-
-
- - -
-
-
- for debugging
purpose only -
-
-
- -
-
-
- - - diff --git a/cast_receiver_app/app-desktop/src/main.js b/cast_receiver_app/app-desktop/src/main.js deleted file mode 100644 index 5645d70787..0000000000 --- a/cast_receiver_app/app-desktop/src/main.js +++ /dev/null @@ -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} */ -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} */ ( - 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} */ ( - 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} 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; diff --git a/cast_receiver_app/app-desktop/src/player_controls.js b/cast_receiver_app/app-desktop/src/player_controls.js deleted file mode 100644 index e29f74148c..0000000000 --- a/cast_receiver_app/app-desktop/src/player_controls.js +++ /dev/null @@ -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; diff --git a/cast_receiver_app/app-desktop/src/samples.js b/cast_receiver_app/app-desktop/src/samples.js deleted file mode 100644 index 2d190bdef4..0000000000 --- a/cast_receiver_app/app-desktop/src/samples.js +++ /dev/null @@ -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', - }, - ], - }, -]); diff --git a/cast_receiver_app/app-desktop/src/samples_internal.js b/cast_receiver_app/app-desktop/src/samples_internal.js deleted file mode 100644 index 71b05eb2c1..0000000000 --- a/cast_receiver_app/app-desktop/src/samples_internal.js +++ /dev/null @@ -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', - }, - ], - }, -]); diff --git a/cast_receiver_app/app/html/index.css b/cast_receiver_app/app/html/index.css deleted file mode 100644 index dfc9b4e0e5..0000000000 --- a/cast_receiver_app/app/html/index.css +++ /dev/null @@ -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%; -} - diff --git a/cast_receiver_app/app/html/index.html b/cast_receiver_app/app/html/index.html deleted file mode 100644 index 64de3e8a8e..0000000000 --- a/cast_receiver_app/app/html/index.html +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - -
- -
-
-
-
-
- - -
-
-
- - - diff --git a/cast_receiver_app/app/html/playback_info_view.css b/cast_receiver_app/app/html/playback_info_view.css deleted file mode 100644 index f70695d873..0000000000 --- a/cast_receiver_app/app/html/playback_info_view.css +++ /dev/null @@ -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; -} - diff --git a/cast_receiver_app/app/src/main.js b/cast_receiver_app/app/src/main.js deleted file mode 100644 index 37c6fd41eb..0000000000 --- a/cast_receiver_app/app/src/main.js +++ /dev/null @@ -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; -} diff --git a/cast_receiver_app/app/src/message_dispatcher.js b/cast_receiver_app/app/src/message_dispatcher.js deleted file mode 100644 index 151ac87fbe..0000000000 --- a/cast_receiver_app/app/src/message_dispatcher.js +++ /dev/null @@ -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, number, string, !Callback): undefined} - */ -const ActionHandler = undefined; - -/** - * Dispatches messages of a cast message bus to registered action handlers. - * - *

The dispatcher listens to events of a CastMessageBus for the namespace - * passed to the constructor. The data 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} */ - this.messageQueue_ = []; - /** @private @const {!Object} */ - this.actions_ = {}; - /** @private @const {!Object} */ - 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>} 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; diff --git a/cast_receiver_app/app/src/receiver.js b/cast_receiver_app/app/src/receiver.js deleted file mode 100644 index 5e67219e75..0000000000 --- a/cast_receiver_app/app/src/receiver.js +++ /dev/null @@ -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; diff --git a/cast_receiver_app/app/src/validation.js b/cast_receiver_app/app/src/validation.js deleted file mode 100644 index 23e2708f8e..0000000000 --- a/cast_receiver_app/app/src/validation.js +++ /dev/null @@ -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} 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. - * - *

Supported types: number, string, boolean, Array. - *

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; - diff --git a/cast_receiver_app/assemble.bazel.sh b/cast_receiver_app/assemble.bazel.sh deleted file mode 100755 index d2039a5152..0000000000 --- a/cast_receiver_app/assemble.bazel.sh +++ /dev/null @@ -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." diff --git a/cast_receiver_app/externs/protocol.js b/cast_receiver_app/externs/protocol.js deleted file mode 100644 index d6544a6f37..0000000000 --- a/cast_receiver_app/externs/protocol.js +++ /dev/null @@ -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} - */ - 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} - */ - 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} - */ - 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} - */ - this.audioTracks; - - /** - * The video tracks in case of adaptive media. - * - * @type {!Array>} - */ - 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} - */ - this.shuffleOrder; - - /** - * The queue of media items. - * - * @type {!Array} - */ - this.mediaQueue; - - /** - * The media item info of the queue items if available. - * - * @type {!Object} - */ - 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. - * - *

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} - */ - 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} - */ - this.args; - } -}; - diff --git a/cast_receiver_app/externs/shaka.js b/cast_receiver_app/externs/shaka.js deleted file mode 100644 index 0af36d7b8c..0000000000 --- a/cast_receiver_app/externs/shaka.js +++ /dev/null @@ -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} - */ - 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; - } -} diff --git a/cast_receiver_app/src/configuration_factory.js b/cast_receiver_app/src/configuration_factory.js deleted file mode 100644 index 819e52a755..0000000000 --- a/cast_receiver_app/src/configuration_factory.js +++ /dev/null @@ -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; diff --git a/cast_receiver_app/src/constants.js b/cast_receiver_app/src/constants.js deleted file mode 100644 index e9600429f0..0000000000 --- a/cast_receiver_app/src/constants.js +++ /dev/null @@ -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} - */ -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} */ -const drmSystems = {}; -drmSystems[WIDEVINE_UUID] = 'com.widevine.alpha'; -drmSystems[PLAYREADY_UUID] = 'com.microsoft.playready'; - -/** - * The uuids of the supported DRM systems. - * - * @type {!Object} - */ -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; diff --git a/cast_receiver_app/src/playback_info_view.js b/cast_receiver_app/src/playback_info_view.js deleted file mode 100644 index 22e2b8ded5..0000000000 --- a/cast_receiver_app/src/playback_info_view.js +++ /dev/null @@ -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; diff --git a/cast_receiver_app/src/player.js b/cast_receiver_app/src/player.js deleted file mode 100644 index d7ffc58f4c..0000000000 --- a/cast_receiver_app/src/player.js +++ /dev/null @@ -1,1522 +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.Player'); - -const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory'); -const NetworkingEngine = goog.require('shaka.net.NetworkingEngine'); -const ShakaError = goog.require('shaka.util.Error'); -const ShakaPlayer = goog.require('shaka.Player'); -const asserts = goog.require('goog.dom.asserts'); -const googArray = goog.require('goog.array'); -const safedom = goog.require('goog.dom.safe'); -const {ErrorMessages, ErrorCategory, PlaybackType, RepeatMode, getPlaybackType, UNKNOWN_ERROR} = goog.require('exoplayer.cast.constants'); -const {UuidComparator, createUuidComparator, log} = goog.require('exoplayer.cast.util'); -const {assert, fail} = goog.require('goog.asserts'); -const {clamp} = goog.require('goog.math'); - -/** - * Value indicating that no window index is currently set. - */ -const INDEX_UNSET = -1; - -/** - * Estimated time for processing the manifest after download in millisecconds. - * - * See: https://github.com/google/shaka-player/issues/1734 - */ -const MANIFEST_PROCESSING_ESTIMATE_MS = 350; - -/** - * Media element events to listen to. - * - * @enum {string} - */ -const MediaElementEvent = { - ERROR: 'error', - LOADED_DATA: 'loadeddata', - PAUSE: 'pause', - PLAYING: 'playing', - SEEKED: 'seeked', - SEEKING: 'seeking', - WAITING: 'waiting', -}; - -/** - * Shaka events to listen to. - * - * @enum {string} - */ -const ShakaEvent = { - ERROR: 'error', - STREAMING: 'streaming', - TRACKS_CHANGED: 'trackschanged', -}; - -/** - * ExoPlayer's playback states. - * - * @enum {string} - */ -const PlaybackState = { - IDLE: 'IDLE', - BUFFERING: 'BUFFERING', - READY: 'READY', - ENDED: 'ENDED', -}; - -/** - * ExoPlayer's position discontinuity reasons. - * - * @enum {string} - */ -const DiscontinuityReason = { - PERIOD_TRANSITION: 'PERIOD_TRANSITION', - SEEK: 'SEEK', -}; - -/** - * A dummy `MediaIteminfo` to be used while the actual period is not - * yet available. - * - * @const - * @type {!MediaItemInfo} - */ -const DUMMY_MEDIA_ITEM_INFO = Object.freeze({ - isSeekable: false, - isDynamic: true, - positionInFirstPeriodUs: 0, - defaultStartPositionUs: 0, - windowDurationUs: 0, - periods: [{ - id: 1, - durationUs: 0, - }], -}); - -/** - * The Player wraps a Shaka player and maintains a queue of media items. - * - * After construction the player is in `IDLE` state. Calling `#prepare` prepares - * the player with the queue item at the given window index and position. The - * state transitions to `BUFFERING`. When 'playWhenReady' is set to `true` - * playback start when the player becomes 'READY'. - * - * When the player needs to rebuffer the state goes to 'BUFFERING' and becomes - * 'READY' again when playback can be resumed. - * - * The state transitions to `ENDED` when playback reached the end of the last - * item in the queue, when the last item has been removed from the queue if - * `!IDLE`, or when `prepare` is called with an empty queue. Seeking makes the - * player transition away from `ENDED` again. - * - * When `#stop` is called or when a fatal playback error occurs, the player - * transition to `IDLE` state and needs to be prepared again to resume playback. - * - * `playWhenReady`, `repeatMode`, `shuffleModeEnabled` can be manipulated in any - * state, just as media items can be added, moved and removed. - * - * @constructor - * @param {!ShakaPlayer} shakaPlayer The shaka player to wrap. - * @param {!ConfigurationFactory} configurationFactory A factory to create a - * configuration for the Shaka player. - */ -const Player = function(shakaPlayer, configurationFactory) { - /** @private @const {?HTMLMediaElement} */ - this.videoElement_ = shakaPlayer.getMediaElement(); - /** @private @const {!ConfigurationFactory} */ - this.configurationFactory_ = configurationFactory; - /** @private @const {!Array} */ - this.playerListeners_ = []; - /** - * @private - * @const - * {?function(NetworkingEngine.RequestType, (?|null))} - */ - this.manifestResponseFilter_ = (type, response) => { - if (type === NetworkingEngine.RequestType.MANIFEST) { - setTimeout(() => { - this.updateWindowMediaItemInfo_(); - this.invalidate(); - }, MANIFEST_PROCESSING_ESTIMATE_MS); - } - }; - - /** @private {!ShakaPlayer} */ - this.shakaPlayer_ = shakaPlayer; - /** @private {boolean} */ - this.playWhenReady_ = false; - /** @private {boolean} */ - this.shuffleModeEnabled_ = false; - /** @private {!RepeatMode} */ - this.repeatMode_ = RepeatMode.OFF; - /** @private {!TrackSelectionParameters} */ - this.trackSelectionParameters_ = /** @type {!TrackSelectionParameters} */ ({ - preferredAudioLanguage: '', - preferredTextLanguage: '', - disabledTextTrackSelectionFlags: [], - selectUndeterminedTextLanguage: false, - }); - /** @private {number} */ - this.windowIndex_ = INDEX_UNSET; - /** @private {!Array} */ - this.queue_ = []; - /** @private {!Object} */ - this.queueUuidIndexMap_ = {}; - /** @private {!UuidComparator} */ - this.uuidComparator_ = createUuidComparator(this.queueUuidIndexMap_); - - /** @private {!PlaybackState} */ - this.playbackState_ = PlaybackState.IDLE; - /** @private {!MediaItemInfo} */ - this.windowMediaItemInfo_ = DUMMY_MEDIA_ITEM_INFO; - /** @private {number} */ - this.windowPeriodIndex_ = 0; - /** @private {!Object} */ - this.mediaItemInfoMap_ = {}; - /** @private {?PlayerError} */ - this.playbackError_ = null; - /** @private {?DiscontinuityReason} */ - this.discontinuityReason_ = null; - /** @private {!Array} */ - this.shuffleOrder_ = []; - /** @private {number} */ - this.shuffleIndex_ = 0; - /** @private {!PlaybackType} */ - this.playbackType_ = PlaybackType.UNKNOWN; - /** @private {boolean} */ - this.isManifestFilterRegistered_ = false; - /** @private {?string} */ - this.uuidToPrepare_ = null; - - if (!this.shakaPlayer_ || !this.videoElement_) { - throw new Error('an instance of Shaka player with a media element ' + - 'attached to it needs to be passed to the constructor.'); - } - - /** @private @const {function(!Event)} */ - this.playbackStateListener_ = (ev) => { - log(['handle event: ', ev.type]); - let invalid = false; - switch (ev.type) { - case ShakaEvent.STREAMING: { - // Arrives once after prepare when the manifest is available. - const uuid = this.queue_[this.windowIndex_].uuid; - const cachedMediaItemInfo = this.mediaItemInfoMap_[uuid]; - if (!cachedMediaItemInfo || cachedMediaItemInfo.isDynamic) { - this.updateWindowMediaItemInfo_(); - if (this.windowMediaItemInfo_.isDynamic) { - this.registerManifestResponseFilter_(); - } - invalid = true; - } - break; - } - case ShakaEvent.TRACKS_CHANGED: { - // Arrives when tracks have changed either initially or at a period - // boundary. - const periods = this.windowMediaItemInfo_.periods; - const previousPeriodIndex = this.windowPeriodIndex_; - this.evaluateAndSetCurrentPeriod_(periods); - invalid = previousPeriodIndex !== this.windowPeriodIndex_; - if (periods.length && this.windowPeriodIndex_ > 0) { - // Player transitions to next period in multiperiod stream. - this.discontinuityReason_ = this.discontinuityReason_ || - DiscontinuityReason.PERIOD_TRANSITION; - invalid = true; - } - if (this.videoElement_.paused && this.playWhenReady_) { - this.videoElement_.play(); - } - break; - } - case MediaElementEvent.LOADED_DATA: { - // Arrives once when the first frame has been rendered. - if (this.playbackType_ === PlaybackType.VIDEO_ELEMENT) { - const uuid = this.queue_[this.windowIndex_].uuid; - let mediaItemInfo = this.mediaItemInfoMap_[uuid]; - if (!mediaItemInfo || mediaItemInfo.isDynamic) { - mediaItemInfo = this.buildMediaItemInfoFromElement_(); - if (mediaItemInfo !== null) { - this.mediaItemInfoMap_[uuid] = mediaItemInfo; - this.windowMediaItemInfo_ = mediaItemInfo; - } - } - this.evaluateAndSetCurrentPeriod_(mediaItemInfo.periods); - invalid = true; - } - if (this.videoElement_.paused && this.playWhenReady_) { - // Restart after automatic skip to next queue item. - this.videoElement_.play(); - } else if (this.videoElement_.paused) { - // If paused, the PLAYING event will not be fired, hence we transition - // to state READY right here. - this.playbackState_ = PlaybackState.READY; - invalid = true; - } - break; - } - case MediaElementEvent.WAITING: - case MediaElementEvent.SEEKING: { - // Arrives at a user seek or when re-buffering starts. - if (this.playbackState_ !== PlaybackState.BUFFERING) { - this.playbackState_ = PlaybackState.BUFFERING; - invalid = true; - } - break; - } - case MediaElementEvent.PLAYING: - case MediaElementEvent.SEEKED: { - // Arrives at the end of a user seek or after re-buffering. - if (this.playbackState_ !== PlaybackState.READY) { - this.playbackState_ = PlaybackState.READY; - invalid = true; - } - break; - } - case MediaElementEvent.PAUSE: { - // Detects end of media and either skips to next or transitions to ended - // state. - if (this.videoElement_.ended) { - let nextWindowIndex = this.getNextWindowIndex(); - if (nextWindowIndex !== INDEX_UNSET) { - this.seekToWindowInternal_(nextWindowIndex, undefined); - } else { - this.playbackState_ = PlaybackState.ENDED; - invalid = true; - } - } - break; - } - } - if (invalid) { - this.invalidate(); - } - }; - /** @private @const {function(!Event)} */ - this.mediaElementErrorHandler_ = (ev) => { - console.error('Media element error reported in handler'); - this.playbackError_ = !this.videoElement_.error ? UNKNOWN_ERROR : { - message: this.videoElement_.error.message, - code: this.videoElement_.error.code, - category: ErrorCategory.MEDIA_ELEMENT, - }; - this.playbackState_ = PlaybackState.IDLE; - this.uuidToPrepare_ = this.queue_[this.windowIndex_] ? - this.queue_[this.windowIndex_].uuid : - null; - this.invalidate(); - }; - /** @private @const {function(!Event)} */ - this.shakaErrorHandler_ = (ev) => { - const shakaError = /** @type {!ShakaError} */ (ev['detail']); - if (shakaError.severity !== ShakaError.Severity.RECOVERABLE) { - this.fatalShakaError_(shakaError, 'Shaka error reported by error event'); - this.invalidate(); - } else { - console.error('Recoverable Shaka error reported in handler'); - } - }; - - this.shakaPlayer_.addEventListener( - ShakaEvent.STREAMING, this.playbackStateListener_); - this.shakaPlayer_.addEventListener( - ShakaEvent.TRACKS_CHANGED, this.playbackStateListener_); - - this.videoElement_.addEventListener( - MediaElementEvent.LOADED_DATA, this.playbackStateListener_); - this.videoElement_.addEventListener( - MediaElementEvent.WAITING, this.playbackStateListener_); - this.videoElement_.addEventListener( - MediaElementEvent.PLAYING, this.playbackStateListener_); - this.videoElement_.addEventListener( - MediaElementEvent.PAUSE, this.playbackStateListener_); - this.videoElement_.addEventListener( - MediaElementEvent.SEEKING, this.playbackStateListener_); - this.videoElement_.addEventListener( - MediaElementEvent.SEEKED, this.playbackStateListener_); - - // Attach error handlers. - this.shakaPlayer_.addEventListener(ShakaEvent.ERROR, this.shakaErrorHandler_); - this.videoElement_.addEventListener( - MediaElementEvent.ERROR, this.mediaElementErrorHandler_); -}; - -/** - * Adds a listener to the player. - * - * @param {function(!PlayerState)} listener The player listener. - */ -Player.prototype.addPlayerListener = function(listener) { - this.playerListeners_.push(listener); -}; - -/** - * Removes a listener. - * - * @param {function(!Object)} listener The player listener. - */ -Player.prototype.removePlayerListener = function(listener) { - for (let i = 0; i < this.playerListeners_.length; i++) { - if (this.playerListeners_[i] === listener) { - this.playerListeners_.splice(i, 1); - break; - } - } -}; - -/** - * Gets the current PlayerState. - * - * @return {!PlayerState} - */ -Player.prototype.getPlayerState = function() { - return this.buildPlayerState_(); -}; - -/** - * Sends the current playback state to clients. - */ -Player.prototype.invalidate = function() { - const playbackState = this.buildPlayerState_(); - for (let i = 0; i < this.playerListeners_.length; i++) { - this.playerListeners_[i](playbackState); - } -}; - -/** - * Get the audio tracks. - * - * @return {!Array} An array with the track names}. - */ -Player.prototype.getAudioTracks = function() { - return this.windowMediaItemInfo_ !== DUMMY_MEDIA_ITEM_INFO ? - this.shakaPlayer_.getAudioLanguages() : - []; -}; - -/** - * Gets the video tracks. - * - * @return {!Array} An array with the video tracks. - */ -Player.prototype.getVideoTracks = function() { - return this.windowMediaItemInfo_ !== DUMMY_MEDIA_ITEM_INFO ? - this.shakaPlayer_.getVariantTracks() : - []; -}; - -/** - * Gets the playback state. - * - * @return {!PlaybackState} The playback state. - */ -Player.prototype.getPlaybackState = function() { - return this.playbackState_; -}; - -/** - * Gets the playback error if any. - * - * @return {?Object} The playback error. - */ -Player.prototype.getPlaybackError = function() { - return this.playbackError_; -}; - -/** - * Gets the duration in milliseconds or a negative value if unknown. - * - * @return {number} The duration in milliseconds. - */ -Player.prototype.getDurationMs = function() { - return this.windowMediaItemInfo_ ? - this.windowMediaItemInfo_.windowDurationUs / 1000 : -1; -}; - -/** - * Gets the current position in milliseconds or a negative value if not known. - * - * @return {number} The current position in milliseconds. - */ -Player.prototype.getCurrentPositionMs = function() { - if (!this.videoElement_.currentTime) { - return 0; - } - return (this.videoElement_.currentTime * 1000) - - (this.windowMediaItemInfo_.positionInFirstPeriodUs / 1000); -}; - -/** - * Gets the current window index. - * - * @return {number} The current window index. - */ -Player.prototype.getCurrentWindowIndex = function() { - if (this.playbackState_ === PlaybackState.IDLE) { - return this.queueUuidIndexMap_[this.uuidToPrepare_ || ''] || 0; - } - return Math.max(0, this.windowIndex_); -}; - -/** - * Gets the media item of the current window or null if the queue is empty. - * - * @return {?MediaItem} The media item of the current window. - */ -Player.prototype.getCurrentMediaItem = function() { - return this.windowIndex_ >= 0 ? this.queue_[this.windowIndex_] : null; -}; - -/** - * Gets the media item info of the current window index or null if not yet - * available. - * - * @return {?MediaItemInfo} The current media item info or undefined. - */ -Player.prototype.getCurrentMediaItemInfo = function () { - return this.windowMediaItemInfo_; -}; - -/** - * Gets the text tracks. - * - * @return {!TextTrackList} The text tracks. - */ -Player.prototype.getTextTracks = function() { - return this.videoElement_.textTracks; -}; - -/** - * Gets whether the player should play when ready. - * - * @return {boolean} True when it plays when ready. - */ -Player.prototype.getPlayWhenReady = function() { - return this.playWhenReady_; -}; - -/** - * Sets whether to play when ready. - * - * @param {boolean} playWhenReady Whether to play when ready. - * @return {boolean} Whether calling this method causes a change of the player - * state. - */ -Player.prototype.setPlayWhenReady = function(playWhenReady) { - if (this.playWhenReady_ === playWhenReady) { - return false; - } - this.playWhenReady_ = playWhenReady; - this.invalidate(); - if (this.playbackState_ === PlaybackState.IDLE || - this.playbackState_ === PlaybackState.ENDED) { - return true; - } - if (this.playWhenReady_) { - this.videoElement_.play(); - } else { - this.videoElement_.pause(); - } - return true; -}; - -/** - * Gets the repeat mode. - * - * @return {!RepeatMode} The repeat mode. - */ -Player.prototype.getRepeatMode = function() { - return this.repeatMode_; -}; - -/** - * Sets the repeat mode. Must be a value of the enum Player.RepeatMode. - * - * @param {!RepeatMode} mode The repeat mode. - * @return {boolean} Whether calling this method causes a change of the player - * state. - */ -Player.prototype.setRepeatMode = function(mode) { - if (this.repeatMode_ === mode) { - return false; - } - if (mode === Player.RepeatMode.OFF || - mode === Player.RepeatMode.ONE || - mode === Player.RepeatMode.ALL) { - this.repeatMode_ = mode; - } else { - throw new Error('illegal repeat mode: ' + mode); - } - this.invalidate(); - return true; -}; - -/** - * Enables or disables the shuffle mode. - * - * @param {boolean} enabled Whether the shuffle mode is enabled or not. - * @return {boolean} Whether calling this method causes a change of the player - * state. - */ -Player.prototype.setShuffleModeEnabled = function(enabled) { - if (this.shuffleModeEnabled_ === enabled) { - return false; - } - this.shuffleModeEnabled_ = enabled; - this.invalidate(); - return true; -}; - -/** - * Sets the track selection parameters. - * - * @param {!TrackSelectionParameters} trackSelectionParameters The parameters. - * @return {boolean} Whether calling this method causes a change of the player - * state. - */ -Player.prototype.setTrackSelectionParameters = function( - trackSelectionParameters) { - this.trackSelectionParameters_ = trackSelectionParameters; - /** @type {!PlayerConfiguration} */ - const configuration = /** @type {!PlayerConfiguration} */ ({}); - this.configurationFactory_.mapLanguageConfiguration( - trackSelectionParameters, configuration); - /** @type {!PlayerConfiguration} */ - const currentConfiguration = this.shakaPlayer_.getConfiguration(); - /** @type {boolean} */ - let isStateChange = false; - if (currentConfiguration.preferredAudioLanguage !== - configuration.preferredAudioLanguage) { - this.shakaPlayer_.selectAudioLanguage(configuration.preferredAudioLanguage); - isStateChange = true; - } - if (currentConfiguration.preferredTextLanguage !== - configuration.preferredTextLanguage) { - this.shakaPlayer_.selectTextLanguage(configuration.preferredTextLanguage); - isStateChange = true; - } - return isStateChange; -}; - -/** - * Gets the previous window index or a negative number if no item previous to - * the current item is available. - * - * @return {number} The previous window index or a negative number if the - * current item is the first item. - */ -Player.prototype.getPreviousWindowIndex = function() { - if (this.playbackType_ === PlaybackType.UNKNOWN) { - return INDEX_UNSET; - } - switch (this.repeatMode_) { - case RepeatMode.ONE: - return this.windowIndex_; - case RepeatMode.ALL: - if (this.shuffleModeEnabled_) { - const previousIndex = this.shuffleIndex_ > 0 ? - this.shuffleIndex_ - 1 : this.queue_.length - 1; - return this.shuffleOrder_[previousIndex]; - } else { - const previousIndex = this.windowIndex_ > 0 ? - this.windowIndex_ - 1 : this.queue_.length - 1; - return previousIndex; - } - break; - case RepeatMode.OFF: - if (this.shuffleModeEnabled_) { - const previousIndex = this.shuffleIndex_ - 1; - return previousIndex < 0 ? -1 : this.shuffleOrder_[previousIndex]; - } else { - const previousIndex = this.windowIndex_ - 1; - return previousIndex < 0 ? -1 : previousIndex; - } - break; - default: - throw new Error('illegal state of repeat mode: ' + this.repeatMode_); - } -}; - -/** - * Gets the next window index or a negative number if the current item is the - * last item. - * - * @return {number} The next window index or a negative number if the current - * item is the last item. - */ -Player.prototype.getNextWindowIndex = function() { - if (this.playbackType_ === PlaybackType.UNKNOWN) { - return INDEX_UNSET; - } - switch (this.repeatMode_) { - case RepeatMode.ONE: - return this.windowIndex_; - case RepeatMode.ALL: - if (this.shuffleModeEnabled_) { - const nextIndex = (this.shuffleIndex_ + 1) % this.queue_.length; - return this.shuffleOrder_[nextIndex]; - } else { - return (this.windowIndex_ + 1) % this.queue_.length; - } - break; - case RepeatMode.OFF: - if (this.shuffleModeEnabled_) { - const nextIndex = this.shuffleIndex_ + 1; - return nextIndex < this.shuffleOrder_.length ? - this.shuffleOrder_[nextIndex] : -1; - } else { - const nextIndex = this.windowIndex_ + 1; - return nextIndex < this.queue_.length ? nextIndex : -1; - } - break; - default: - throw new Error('illegal state of repeat mode: ' + this.repeatMode_); - } -}; - -/** - * Gets whether the current window is seekable. - * - * @return {boolean} True if seekable. - */ -Player.prototype.isCurrentWindowSeekable = function() { - return !!this.videoElement_.seekable; -}; - -/** - * Seeks to the positionMs of the media item with the given uuid. - * - * @param {string} uuid The uuid of the media item to seek to. - * @param {number|undefined} positionMs The position in milliseconds to seek to. - * @return {boolean} True if a seek operation has been processed, false - * otherwise. - */ -Player.prototype.seekToUuid = function(uuid, positionMs) { - if (this.playbackState_ === PlaybackState.IDLE) { - this.uuidToPrepare_ = uuid; - this.videoElement_.currentTime = - this.getPosition_(positionMs, INDEX_UNSET) / 1000; - this.invalidate(); - return true; - } - const windowIndex = this.queueUuidIndexMap_[uuid]; - if (windowIndex !== undefined) { - positionMs = this.getPosition_(positionMs, windowIndex); - this.discontinuityReason_ = DiscontinuityReason.SEEK; - this.seekToWindowInternal_(windowIndex, positionMs); - return true; - } - return false; -}; - -/** - * Seeks to the positionMs of the given window. - * - * The index must be a valid index of the current queue, else this method does - * nothing. - * - * @param {number} windowIndex The index of the window to seek to. - * @param {number|undefined} positionMs The position to seek to within the - * window. - */ -Player.prototype.seekToWindow = function(windowIndex, positionMs) { - if (windowIndex < 0 || windowIndex >= this.queue_.length) { - return; - } - this.seekToUuid(this.queue_[windowIndex].uuid, positionMs); -}; - -/** - * Gets the number of media items in the queue. - * - * @return {number} The size of the queue. - */ -Player.prototype.getQueueSize = function() { - return this.queue_.length; -}; - -/** - * Adds an array of items at the given index of the queue. - * - * Items are expected to have been validated with `validation#validateMediaItem` - * or `validation#validateMediaItems` before being passed to this method. - * - * @param {number} index The index where to insert the media item. - * @param {!Array} mediaItems The media items. - * @param {!Array|undefined} shuffleOrder The new shuffle order. - * @return {number} The number of added items. - */ -Player.prototype.addQueueItems = function(index, mediaItems, shuffleOrder) { - if (index < 0 || mediaItems.length === 0) { - return 0; - } - let addedItemCount = 0; - index = Math.min(this.queue_.length, index); - mediaItems.forEach((itemToAdd) => { - if (this.queueUuidIndexMap_[itemToAdd.uuid] === undefined) { - this.queue_.splice(index + addedItemCount, 0, itemToAdd); - this.queueUuidIndexMap_[itemToAdd.uuid] = index + addedItemCount; - addedItemCount++; - } - }); - if (addedItemCount === 0) { - return 0; - } - this.buildUuidIndexMap_(index + addedItemCount); - this.setShuffleOrder_(shuffleOrder); - if (this.queue_.length === addedItemCount) { - this.windowIndex_ = 0; - this.updateShuffleIndex_(); - } else if ( - index <= this.windowIndex_ && - this.playbackType_ !== PlaybackType.UNKNOWN) { - this.windowIndex_ += mediaItems.length; - this.updateShuffleIndex_(); - } - this.invalidate(); - return addedItemCount; -}; - -/** - * Removes the queue items with the given uuids. - * - * @param {!Array} uuids The uuids of the queue items to remove. - * @return {number} The number of items removed from the queue. - */ -Player.prototype.removeQueueItems = function(uuids) { - let currentWindowRemoved = false; - let lowestIndexRemoved = this.queue_.length - 1; - const initialQueueSize = this.queue_.length; - // Sort in descending order to start removing from the end. - uuids = uuids.sort(this.uuidComparator_); - uuids.forEach((uuid) => { - const indexToRemove = this.queueUuidIndexMap_[uuid]; - if (indexToRemove === undefined) { - return; - } - // Remove the item from the queue. - this.queue_.splice(indexToRemove, 1); - // Remove the corresponding media item info. - delete this.mediaItemInfoMap_[uuid]; - // Remove the mapping to the window index. - delete this.queueUuidIndexMap_[uuid]; - lowestIndexRemoved = Math.min(lowestIndexRemoved, indexToRemove); - currentWindowRemoved = - currentWindowRemoved || indexToRemove === this.windowIndex_; - // The window index needs to be decreased when the item which has been - // removed was before the current item, when the current item at the last - // position has been removed, or when the queue has been emptied. - if (indexToRemove < this.windowIndex_ || - (indexToRemove === this.windowIndex_ && - indexToRemove === this.queue_.length) || - this.queue_.length === 0) { - this.windowIndex_--; - } - // Adjust the shuffle order. - let shuffleIndexToRemove; - this.shuffleOrder_.forEach((windowIndex, index) => { - if (windowIndex > indexToRemove) { - // Decrease the index in the shuffle order. - this.shuffleOrder_[index]--; - } else if (windowIndex === indexToRemove) { - // Recall index for removal after traversing. - shuffleIndexToRemove = index; - } - }); - // Remove the shuffle order entry of the removed item. - this.shuffleOrder_.splice(shuffleIndexToRemove, 1); - }); - const removedItemsCount = initialQueueSize - this.queue_.length; - if (removedItemsCount === 0) { - return 0; - } - this.updateShuffleIndex_(); - this.buildUuidIndexMap_(lowestIndexRemoved); - if (currentWindowRemoved) { - if (this.queue_.length === 0) { - this.playbackState_ = this.playbackState_ === PlaybackState.IDLE ? - PlaybackState.IDLE : - PlaybackState.ENDED; - this.windowMediaItemInfo_ = DUMMY_MEDIA_ITEM_INFO; - this.windowPeriodIndex_ = 0; - this.videoElement_.currentTime = 0; - this.uuidToPrepare_ = null; - this.unregisterManifestResponseFilter_(); - this.unload_(/** reinitialiseMediaSource= */ true); - } else if (this.windowIndex_ >= 0) { - const windowIndexToPrepare = this.windowIndex_; - this.windowIndex_ = INDEX_UNSET; - this.seekToWindowInternal_(windowIndexToPrepare, undefined); - return removedItemsCount; - } - } - this.invalidate(); - return removedItemsCount; -}; - -/** - * Move the queue item with the given id to the given position. - * - * @param {string} uuid The uuid of the queue item to move. - * @param {number} to The position to move the item to. - * @param {!Array|undefined} shuffleOrder The new shuffle order. - * @return {boolean} Whether the item has been moved. - */ -Player.prototype.moveQueueItem = function(uuid, to, shuffleOrder) { - if (to < 0 || to >= this.queue_.length) { - return false; - } - const windowIndex = this.queueUuidIndexMap_[uuid]; - if (windowIndex === undefined) { - return false; - } - const itemMoved = this.moveInQueue_(windowIndex, to); - if (itemMoved) { - this.setShuffleOrder_(shuffleOrder); - this.invalidate(); - } - return itemMoved; -}; - -/** - * Prepares the player at the current window index and position. - * - * The playback state immediately transitions to `BUFFERING`. If the queue - * is empty the player transitions to `ENDED`. - */ -Player.prototype.prepare = function() { - if (this.queue_.length === 0) { - this.uuidToPrepare_ = null; - this.playbackState_ = PlaybackState.ENDED; - this.invalidate(); - return; - } - if (this.uuidToPrepare_) { - this.windowIndex_ = - this.queueUuidIndexMap_[this.uuidToPrepare_] || INDEX_UNSET; - this.uuidToPrepare_ = null; - } - this.windowIndex_ = clamp(this.windowIndex_, 0, this.queue_.length - 1); - this.prepare_(this.getCurrentPositionMs()); - this.invalidate(); -}; - -/** - * Stops the player. - * - * Calling this method causes the player to transition into `IDLE` state. - * If `reset` is `true` the player is reset to the initial state of right - * after construction. If `reset` is `false`, the media queue is preserved - * and calling `prepare()` results in resuming the player state to what it - * was before calling `#stop(false)`. - * - * @param {boolean} reset Whether the state should be reset. - * @return {!Promise} A promise which resolves after async unload - * tasks have finished. - */ -Player.prototype.stop = function(reset) { - this.playbackState_ = PlaybackState.IDLE; - this.playbackError_ = null; - this.discontinuityReason_ = null; - this.unregisterManifestResponseFilter_(); - this.uuidToPrepare_ = this.uuidToPrepare_ || (this.queue_[this.windowIndex_] ? - this.queue_[this.windowIndex_].uuid : - null); - if (reset) { - this.uuidToPrepare_ = null; - this.queue_ = []; - this.queueUuidIndexMap_ = {}; - this.uuidComparator_ = createUuidComparator(this.queueUuidIndexMap_); - this.windowIndex_ = INDEX_UNSET; - this.mediaItemInfoMap_ = {}; - this.windowMediaItemInfo_ = DUMMY_MEDIA_ITEM_INFO; - this.windowPeriodIndex_ = 0; - this.videoElement_.currentTime = 0; - this.shuffleOrder_ = []; - this.shuffleIndex_ = 0; - } - this.invalidate(); - return this.unload_(/** reinitialiseMediaSource= */ !reset); -}; - -/** - * Resets player and media element. - * - * @private - * @param {boolean} reinitialiseMediaSource Whether the media source should be - * reinitialized. - * @return {!Promise} A promise which resolves after async unload - * tasks have finished. - */ -Player.prototype.unload_ = function(reinitialiseMediaSource) { - const playbackTypeToUnload = this.playbackType_; - this.playbackType_ = PlaybackType.UNKNOWN; - switch (playbackTypeToUnload) { - case PlaybackType.VIDEO_ELEMENT: - this.videoElement_.removeAttribute('src'); - this.videoElement_.load(); - return Promise.resolve(); - case PlaybackType.SHAKA_PLAYER: - return new Promise((resolve, reject) => { - this.shakaPlayer_.unload(reinitialiseMediaSource) - .then(resolve) - .catch(resolve); - }); - default: - return Promise.resolve(); - } -}; - -/** - * Releases the current Shaka instance and create a new one. - * - * This function should only be called if the Shaka instance is out of order due - * to https://github.com/google/shaka-player/issues/1785. It assumes the current - * Shaka instance has fallen into a state in which promises returned by - * `shakaPlayer.load` and `shakaPlayer.unload` do not resolve nor are they - * rejected anymore. - * - * @private - */ -Player.prototype.replaceShaka_ = function() { - // Remove all listeners. - this.shakaPlayer_.removeEventListener( - ShakaEvent.STREAMING, this.playbackStateListener_); - this.shakaPlayer_.removeEventListener( - ShakaEvent.TRACKS_CHANGED, this.playbackStateListener_); - this.shakaPlayer_.removeEventListener( - ShakaEvent.ERROR, this.shakaErrorHandler_); - // Unregister response filter if any. - this.unregisterManifestResponseFilter_(); - // Unload the old instance. - this.shakaPlayer_.unload(false); - // Reset video element. - this.videoElement_.removeAttribute('src'); - this.videoElement_.load(); - // Create a new instance and add listeners. - this.shakaPlayer_ = new ShakaPlayer(this.videoElement_); - this.shakaPlayer_.addEventListener( - ShakaEvent.STREAMING, this.playbackStateListener_); - this.shakaPlayer_.addEventListener( - ShakaEvent.TRACKS_CHANGED, this.playbackStateListener_); - this.shakaPlayer_.addEventListener(ShakaEvent.ERROR, this.shakaErrorHandler_); -}; - -/** - * Moves a queue item within the queue. - * - * @private - * @param {number} from The initial position. - * @param {number} to The position to move the item to. - * @return {boolean} Whether the item has been moved. - */ -Player.prototype.moveInQueue_ = function(from, to) { - if (from < 0 || to < 0 - || from >= this.queue_.length || to >= this.queue_.length - || from === to) { - return false; - } - this.queue_.splice(to, 0, this.queue_.splice(from, 1)[0]); - this.buildUuidIndexMap_(Math.min(from, to)); - if (from === this.windowIndex_) { - this.windowIndex_ = to; - } else if (from > this.windowIndex_ && to <= this.windowIndex_) { - this.windowIndex_++; - } else if (from < this.windowIndex_ && to >= this.windowIndex_) { - this.windowIndex_--; - } - return true; -}; - -/** - * Shuffles the queue. - * - * @private - */ -Player.prototype.shuffle_ = function() { - this.shuffleOrder_ = this.queue_.map((item, index) => index); - googArray.shuffle(this.shuffleOrder_); - this.updateShuffleIndex_(); -}; - -/** - * Sets the new shuffle order. - * - * @private - * @param {!Array|undefined} shuffleOrder The new shuffle order. - */ -Player.prototype.setShuffleOrder_ = function(shuffleOrder) { - if (shuffleOrder && this.queue_.length === shuffleOrder.length) { - this.shuffleOrder_ = shuffleOrder; - this.updateShuffleIndex_(); - } else if (this.shuffleOrder_.length !== this.queue_.length) { - this.shuffle_(); - } -}; - -/** - * Updates the shuffle order to point to the current window index. - * - * @private - */ -Player.prototype.updateShuffleIndex_ = function() { - this.shuffleIndex_ = - this.shuffleOrder_.findIndex((idx) => idx === this.windowIndex_); -}; - -/** - * Builds the `queueUuidIndexMap` using the uuid of a media item as the key and - * the window index as the value of an entry. - * - * @private - * @param {number} startPosition The window index to start updating at. - */ -Player.prototype.buildUuidIndexMap_ = function(startPosition) { - for (let i = startPosition; i < this.queue_.length; i++) { - this.queueUuidIndexMap_[this.queue_[i].uuid] = i; - } -}; - -/** - * Gets the default position of the current window. - * - * @private - * @return {number} The default position of the current window. - */ -Player.prototype.getDefaultPosition_ = function() { - return this.windowMediaItemInfo_.defaultStartPositionUs; -}; - -/** - * Checks whether the given position is buffered. - * - * @private - * @param {number} positionMs The position to check. - * @return {boolean} true if the media data of the current position is buffered. - */ -Player.prototype.isBuffered_ = function(positionMs) { - const ranges = this.videoElement_.buffered; - for (let i = 0; i < ranges.length; i++) { - const start = ranges.start(i) * 1000; - const end = ranges.end(i) * 1000; - if (start <= positionMs && positionMs <= end) { - return true; - } - } - return false; -}; - -/** - * Seeks to the positionMs of the given window. - * - * To signal a user seek, callers are expected to set the discontinuity reason - * to `DiscontinuityReason.SEEK` before calling this method. If not set this - * method may set the `DiscontinuityReason.PERIOD_TRANSITION` in case the - * `windowIndex` changes. - * - * @private - * @param {number} windowIndex The non-negative index of the window to seek to. - * @param {number|undefined} positionMs The position to seek to within the - * window. If undefined it seeks to the default position of the window. - */ -Player.prototype.seekToWindowInternal_ = function(windowIndex, positionMs) { - const windowChanges = this.windowIndex_ !== windowIndex; - // Update window index and position in any case. - this.windowIndex_ = Math.max(0, windowIndex); - this.updateShuffleIndex_(); - const seekPositionMs = this.getPosition_(positionMs, windowIndex); - this.videoElement_.currentTime = seekPositionMs / 1000; - - // IDLE or ENDED with empty queue. - if (this.playbackState_ === PlaybackState.IDLE || this.queue_.length === 0) { - // Do nothing but report the change in window index and position. - this.invalidate(); - return; - } - - // Prepare for a seek to another window or when in ENDED state whilst the - // queue is not empty but prepare has not been called yet. - if (windowChanges || this.playbackType_ === PlaybackType.UNKNOWN) { - // Reset and prepare. - this.unregisterManifestResponseFilter_(); - this.discontinuityReason_ = - this.discontinuityReason_ || DiscontinuityReason.PERIOD_TRANSITION; - this.prepare_(seekPositionMs); - this.invalidate(); - return; - } - - // Sync playWhenReady with video element after ENDED state. - if (this.playbackState_ === PlaybackState.ENDED && this.playWhenReady_) { - this.videoElement_.play(); - return; - } - - // A seek within the current window when READY or BUFFERING. - this.playbackState_ = this.isBuffered_(seekPositionMs) ? - PlaybackState.READY : - PlaybackState.BUFFERING; - this.invalidate(); -}; - -/** - * Prepares the player at the current window index and the given - * `startPositionMs`. - * - * Calling this method resets the media item information, transitions to - * 'BUFFERING', prepares either the plain video element for progressive - * media, or the Shaka player for adaptive media. - * - * Media items are mapped by media type to a `PlaybackType`s in - * `exoplayer.cast.constants.SupportedMediaTypes`. Unsupported mime types will - * cause the player to transition to the `IDLE` state. - * - * Items in the queue are expected to have been validated with - * `validation#validateMediaItem` or `validation#validateMediaItems`. If this is - * not the case this method might throw an Assertion exception. - * - * @private - * @param {number} startPositionMs The position at which to start playback. - * @throws {!AssertionException} In case an unvalidated item can't be mapped to - * a supported playback type. - */ -Player.prototype.prepare_ = function(startPositionMs) { - const mediaItem = this.queue_[this.windowIndex_]; - const windowUuid = this.queue_[this.windowIndex_].uuid; - const mediaItemInfo = this.mediaItemInfoMap_[windowUuid]; - if (mediaItemInfo && !mediaItemInfo.isDynamic) { - // Do reuse if not dynamic. - this.windowMediaItemInfo_ = mediaItemInfo; - } else { - // Use the dummy info until manifest/data available. - this.windowMediaItemInfo_ = DUMMY_MEDIA_ITEM_INFO; - this.mediaItemInfoMap_[windowUuid] = DUMMY_MEDIA_ITEM_INFO; - } - this.windowPeriodIndex_ = 0; - this.playbackType_ = getPlaybackType(mediaItem.mimeType); - this.playbackState_ = PlaybackState.BUFFERING; - const uri = mediaItem.media.uri; - switch (this.playbackType_) { - case PlaybackType.VIDEO_ELEMENT: - this.videoElement_.currentTime = startPositionMs / 1000; - this.shakaPlayer_.unload(false) - .then(() => { - this.setMediaElementSrc(uri); - this.videoElement_.currentTime = startPositionMs / 1000; - }) - .catch((error) => { - // Let's still try. We actually don't need Shaka right now. - this.setMediaElementSrc(uri); - this.videoElement_.currentTime = startPositionMs / 1000; - console.error('Shaka error while unloading', error); - }); - break; - case PlaybackType.SHAKA_PLAYER: - this.shakaPlayer_.configure( - this.configurationFactory_.createConfiguration( - mediaItem, this.trackSelectionParameters_)); - this.shakaPlayer_.load(uri, startPositionMs / 1000).catch((error) => { - const shakaError = /** @type {!ShakaError} */ (error); - if (shakaError.severity !== ShakaError.Severity.RECOVERABLE && - shakaError.code !== ShakaError.Code.LOAD_INTERRUPTED) { - this.fatalShakaError_(shakaError, 'loading failed for uri: ' + uri); - this.invalidate(); - } else { - console.error('Recoverable Shaka error while loading', shakaError); - } - }); - break; - default: - fail('unknown playback type for mime type: ' + mediaItem.mimeType); - } -}; - -/** - * Sets the uri to the `src` attribute of the media element in a safe way. - * - * @param {string} uri The uri to set as the value of the `src` attribute. - */ -Player.prototype.setMediaElementSrc = function(uri) { - safedom.setVideoSrc( - asserts.assertIsHTMLVideoElement(this.videoElement_), uri); -}; - -/** - * Handles a fatal Shaka error by setting the playback error, transitioning to - * state `IDLE` and setting the playback type to `UNKNOWN`. Player needs to be - * reprepared after calling this method. - * - * @private - * @param {!ShakaError} shakaError The error. - * @param {string|undefined} customMessage A custom message. - */ -Player.prototype.fatalShakaError_ = function(shakaError, customMessage) { - this.playbackState_ = PlaybackState.IDLE; - this.playbackType_ = PlaybackType.UNKNOWN; - this.uuidToPrepare_ = this.queue_[this.windowIndex_] ? - this.queue_[this.windowIndex_].uuid : - null; - if (typeof shakaError.severity === 'undefined') { - // Not a Shaka error. We need to assume the worst case. - this.replaceShaka_(); - this.playbackError_ = /** @type {!PlayerError} */ ({ - message: ErrorMessages.UNKNOWN_FATAL_ERROR, - code: -1, - category: ErrorCategory.FATAL_SHAKA_ERROR, - }); - } else { - // A critical ShakaError. Can be recovered from by calling prepare. - this.playbackError_ = /** @type {!PlayerError} */ ({ - message: customMessage || shakaError.message || - ErrorMessages.SHAKA_UNKNOWN_ERROR, - code: shakaError.code, - category: shakaError.category, - }); - } - console.error('caught shaka load error', shakaError); -}; - -/** - * Gets the position to use. If `undefined` or `null` is passed as argument the - * default start position of the media item info of the given windowIndex is - * returned. - * - * @private - * @param {?number|undefined} positionMs The position in milliseconds, - * `undefined` or `null`. - * @param {number} windowIndex The window index for which to evaluate the - * position. - * @return {number} The position to use in milliseconds. - */ -Player.prototype.getPosition_ = function(positionMs, windowIndex) { - if (positionMs !== undefined) { - return Math.max(0, positionMs); - } - const windowUuid = assert(this.queue_[windowIndex]).uuid; - const mediaItemInfo = - this.mediaItemInfoMap_[windowUuid] || DUMMY_MEDIA_ITEM_INFO; - return mediaItemInfo.defaultStartPositionUs; -}; - -/** - * Refreshes the media item info of the current window. - * - * @private - */ -Player.prototype.updateWindowMediaItemInfo_ = function() { - this.windowMediaItemInfo_ = this.buildMediaItemInfo_(); - if (this.windowMediaItemInfo_) { - const mediaItem = this.queue_[this.windowIndex_]; - this.mediaItemInfoMap_[mediaItem.uuid] = this.windowMediaItemInfo_; - this.evaluateAndSetCurrentPeriod_(this.windowMediaItemInfo_.periods); - } -}; - -/** - * Evaluates the current period and stores it in a member variable. - * - * @private - * @param {!Array} periods The periods of the current mediaItem. - */ -Player.prototype.evaluateAndSetCurrentPeriod_ = function(periods) { - const positionUs = this.getCurrentPositionMs() * 1000; - let positionInWindowUs = 0; - periods.some((period, i) => { - positionInWindowUs += period.durationUs; - if (positionUs < positionInWindowUs) { - this.windowPeriodIndex_ = i; - return true; - } - return false; - }); -}; - -/** - * Registers a response filter which is notified when a manifest has been - * downloaded. - * - * @private - */ -Player.prototype.registerManifestResponseFilter_ = function() { - if (this.isManifestFilterRegistered_) { - return; - } - this.shakaPlayer_.getNetworkingEngine().registerResponseFilter( - this.manifestResponseFilter_); - this.isManifestFilterRegistered_ = true; -}; - -/** - * Unregisters the manifest response filter. - * - * @private - */ -Player.prototype.unregisterManifestResponseFilter_ = function() { - if (this.isManifestFilterRegistered_) { - this.shakaPlayer_.getNetworkingEngine().unregisterResponseFilter( - this.manifestResponseFilter_); - this.isManifestFilterRegistered_ = false; - } -}; - -/** - * Builds a MediaItemInfo from the media element. - * - * @private - * @return {!MediaItemInfo} A media item info. - */ -Player.prototype.buildMediaItemInfoFromElement_ = function() { - const durationUs = this.videoElement_.duration * 1000 * 1000; - return /** @type {!MediaItemInfo} */ ({ - isSeekable: !!this.videoElement_.seekable, - isDynamic: false, - positionInFirstPeriodUs: 0, - defaultStartPositionUs: 0, - windowDurationUs: durationUs, - periods: [{ - id: 0, - durationUs: durationUs, - }], - }); -}; - -/** - * Builds a MediaItemInfo from the manifest or null if no manifest is available. - * - * @private - * @return {!MediaItemInfo} - */ -Player.prototype.buildMediaItemInfo_ = function() { - const manifest = this.shakaPlayer_.getManifest(); - if (manifest === null) { - return DUMMY_MEDIA_ITEM_INFO; - } - const timeline = manifest.presentationTimeline; - const isDynamic = timeline.isLive(); - const windowStartUs = isDynamic ? - timeline.getSeekRangeStart() * 1000 * 1000 : - timeline.getSegmentAvailabilityStart() * 1000 * 1000; - const windowDurationUs = isDynamic ? - (timeline.getSeekRangeEnd() - timeline.getSeekRangeStart()) * 1000 * - 1000 : - timeline.getDuration() * 1000 * 1000; - const defaultStartPositionUs = isDynamic ? - timeline.getSeekRangeEnd() * 1000 * 1000 : - timeline.getSegmentAvailabilityStart() * 1000 * 1000; - - const periods = []; - let previousStartTimeUs = 0; - let positionInFirstPeriodUs = 0; - manifest.periods.forEach((period, index) => { - const startTimeUs = period.startTime * 1000 * 1000; - periods.push({ - id: Math.floor(startTimeUs), - }); - if (index > 0) { - // calculate duration of previous period - periods[index - 1].durationUs = startTimeUs - previousStartTimeUs; - if (previousStartTimeUs <= windowStartUs && windowStartUs < startTimeUs) { - positionInFirstPeriodUs = windowStartUs - previousStartTimeUs; - } - } - previousStartTimeUs = startTimeUs; - }); - // calculate duration of last period - if (periods.length) { - const lastPeriodDurationUs = - isDynamic ? Infinity : windowDurationUs - previousStartTimeUs; - periods.slice(-1)[0].durationUs = lastPeriodDurationUs; - if (previousStartTimeUs <= windowStartUs) { - positionInFirstPeriodUs = windowStartUs - previousStartTimeUs; - } - } - return /** @type {!MediaItemInfo} */ ({ - windowDurationUs: Math.floor(windowDurationUs), - defaultStartPositionUs: Math.floor(defaultStartPositionUs), - isSeekable: this.videoElement_ ? !!this.videoElement_.seekable : false, - positionInFirstPeriodUs: Math.floor(positionInFirstPeriodUs), - isDynamic: isDynamic, - periods: periods, - }); -}; - -/** - * Builds the player state message. - * - * @private - * @return {!PlayerState} The player state. - */ -Player.prototype.buildPlayerState_ = function() { - const playerState = { - playbackState: this.getPlaybackState(), - playbackParameters: { - speed: 1, - pitch: 1, - skipSilence: false, - }, - playbackPosition: this.buildPlaybackPosition_(), - playWhenReady: this.getPlayWhenReady(), - windowIndex: this.getCurrentWindowIndex(), - windowCount: this.queue_.length, - audioTracks: this.getAudioTracks() || [], - videoTracks: this.getVideoTracks(), - repeatMode: this.repeatMode_, - shuffleModeEnabled: this.shuffleModeEnabled_, - mediaQueue: this.queue_.slice(), - mediaItemsInfo: this.mediaItemInfoMap_, - shuffleOrder: this.shuffleOrder_, - sequenceNumber: -1, - }; - if (this.playbackError_) { - playerState.error = this.playbackError_; - this.playbackError_ = null; - } - return playerState; -}; - -/** - * Builds the playback position. Returns null if all properties of the playback - * position are empty. - * - * @private - * @return {?PlaybackPosition} The playback position. - */ -Player.prototype.buildPlaybackPosition_ = function() { - if ((this.playbackState_ === PlaybackState.IDLE && !this.uuidToPrepare_) || - this.playbackState_ === PlaybackState.ENDED && this.queue_.length === 0) { - this.discontinuityReason_ = null; - return null; - } - /** @type {!PlaybackPosition} */ - const playbackPosition = { - positionMs: this.getCurrentPositionMs(), - uuid: this.uuidToPrepare_ || this.queue_[this.windowIndex_].uuid, - periodId: this.windowMediaItemInfo_.periods[this.windowPeriodIndex_].id, - discontinuityReason: null, - }; - if (this.discontinuityReason_ !== null) { - playbackPosition.discontinuityReason = this.discontinuityReason_; - this.discontinuityReason_ = null; - } - return playbackPosition; -}; - -exports = Player; -exports.RepeatMode = RepeatMode; -exports.PlaybackState = PlaybackState; -exports.DiscontinuityReason = DiscontinuityReason; -exports.DUMMY_MEDIA_ITEM_INFO = DUMMY_MEDIA_ITEM_INFO; diff --git a/cast_receiver_app/src/timeout.js b/cast_receiver_app/src/timeout.js deleted file mode 100644 index e5df5ec2f4..0000000000 --- a/cast_receiver_app/src/timeout.js +++ /dev/null @@ -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} 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; diff --git a/cast_receiver_app/src/util.js b/cast_receiver_app/src/util.js deleted file mode 100644 index 75afd9e5d3..0000000000 --- a/cast_receiver_app/src/util.js +++ /dev/null @@ -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} 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, -}; diff --git a/cast_receiver_app/test/caf_bootstrap.js b/cast_receiver_app/test/caf_bootstrap.js deleted file mode 100644 index 721360e8a7..0000000000 --- a/cast_receiver_app/test/caf_bootstrap.js +++ /dev/null @@ -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', - }, - }, - }, -}; diff --git a/cast_receiver_app/test/configuration_factory_test.js b/cast_receiver_app/test/configuration_factory_test.js deleted file mode 100644 index af9254c59e..0000000000 --- a/cast_receiver_app/test/configuration_factory_test.js +++ /dev/null @@ -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); - } -}); diff --git a/cast_receiver_app/test/externs.js b/cast_receiver_app/test/externs.js deleted file mode 100644 index a90a367691..0000000000 --- a/cast_receiver_app/test/externs.js +++ /dev/null @@ -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} */ -Uuids.prototype.uuids; diff --git a/cast_receiver_app/test/message_dispatcher_test.js b/cast_receiver_app/test/message_dispatcher_test.js deleted file mode 100644 index 3e7daaf573..0000000000 --- a/cast_receiver_app/test/message_dispatcher_test.js +++ /dev/null @@ -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')); - } -}); diff --git a/cast_receiver_app/test/mocks.js b/cast_receiver_app/test/mocks.js deleted file mode 100644 index 244ac72829..0000000000 --- a/cast_receiver_app/test/mocks.js +++ /dev/null @@ -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; diff --git a/cast_receiver_app/test/playback_info_view_test.js b/cast_receiver_app/test/playback_info_view_test.js deleted file mode 100644 index 87cefe1884..0000000000 --- a/cast_receiver_app/test/playback_info_view_test.js +++ /dev/null @@ -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_); - }, - -}); diff --git a/cast_receiver_app/test/player_test.js b/cast_receiver_app/test/player_test.js deleted file mode 100644 index 96dfbf8614..0000000000 --- a/cast_receiver_app/test/player_test.js +++ /dev/null @@ -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_); - }, -}); diff --git a/cast_receiver_app/test/queue_test.js b/cast_receiver_app/test/queue_test.js deleted file mode 100644 index b46361fb2e..0000000000 --- a/cast_receiver_app/test/queue_test.js +++ /dev/null @@ -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); - }, -}); diff --git a/cast_receiver_app/test/receiver_test.js b/cast_receiver_app/test/receiver_test.js deleted file mode 100644 index 303a1caf64..0000000000 --- a/cast_receiver_app/test/receiver_test.js +++ /dev/null @@ -1,1027 +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 receiver. - */ - -goog.module('exoplayer.cast.test.receiver'); -goog.setTestOnly(); - -const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory'); -const MessageDispatcher = goog.require('exoplayer.cast.MessageDispatcher'); -const Player = goog.require('exoplayer.cast.Player'); -const Receiver = goog.require('exoplayer.cast.Receiver'); -const mocks = goog.require('exoplayer.cast.test.mocks'); -const testSuite = goog.require('goog.testing.testSuite'); -const util = goog.require('exoplayer.cast.test.util'); - -/** @type {?Player|undefined} */ -let player; -/** @type {!Array} */ -let queue = []; -let shakaFake; -let castContextMock; - -/** - * Sends a message to the receiver under test. - * - * @param {!Object} message The message to send as json. - */ -const sendMessage = function(message) { - mocks.state().customMessageListener({ - data: message, - senderId: 'sender0', - }); -}; - -/** - * Creates a valid media item with the suffix appended to each field. - * - * @param {string} suffix The suffix to append to the fields value. - * @return {!Object} The media item. - */ -const createMediaItem = function(suffix) { - return { - uuid: 'uuid' + suffix, - media: {uri: 'uri' + suffix}, - mimeType: 'application/dash+xml', - }; -}; - -let messageSequence = 0; - -/** - * Creates a message in the format sent bey the sender app. - * - * @param {string} method The name of the method. - * @param {?Object} args The arguments. - * @return {!Object} The message. - */ -const createMessage = function (method, args) { - return { - method: method, - args: args, - sequenceNumber: ++messageSequence, - }; -}; - -/** - * Asserts the `playerState` is in the same state as just after creation of the - * player. - * - * @param {!PlayerState} playerState The player state to assert. - * @param {string} playbackState The expected playback state. - */ -const assertInitialState = function(playerState, playbackState) { - assertEquals(playbackState, playerState.playbackState); - // Assert the state is in initial state. - assertArrayEquals([], queue); - assertEquals(0, playerState.windowCount); - assertEquals(0, playerState.windowIndex); - assertUndefined(playerState.playbackError); - assertNull(playerState.playbackPosition); - // Assert player properties. - assertEquals(0, player.getDurationMs()); - assertArrayEquals([], Object.entries(player.mediaItemInfoMap_)); - assertEquals(0, player.windowPeriodIndex_); - assertEquals(999, player.playbackType_); - assertEquals(0, player.getCurrentWindowIndex()); - assertEquals(Player.DUMMY_MEDIA_ITEM_INFO, player.windowMediaItemInfo_); -}; - - -testSuite({ - setUp() { - mocks.setUp(); - shakaFake = mocks.createShakaFake(); - castContextMock = mocks.createCastReceiverContextFake(); - player = new Player(shakaFake, new ConfigurationFactory()); - player.addPlayerListener((playerState) => { - queue = playerState.mediaQueue; - }); - const messageDispatcher = new MessageDispatcher( - 'urn:x-cast:com.google.exoplayer.cast', castContextMock); - new Receiver(player, castContextMock, messageDispatcher); - }, - - tearDown() { - queue = []; - }, - - /** Tests whether a status was sent to the sender on connect. */ - testNotifyClientConnected() { - assertUndefined(mocks.state().outputMessages['sender0']); - - sendMessage(createMessage('player.onClientConnected', {})); - const message = mocks.state().outputMessages['sender0'][0]; - assertEquals(messageSequence, message.sequenceNumber); - }, - - /** - * Tests whether a custom message listener has been registered after - * construction. - */ - testCustomMessageListener() { - assertTrue(goog.isFunction(mocks.state().customMessageListener)); - }, - - /** Tests set playWhenReady. */ - testSetPlayWhenReady() { - let playWhenReady; - player.addPlayerListener((playerState) => { - playWhenReady = playerState.playWhenReady; - }); - - sendMessage(createMessage( - 'player.setPlayWhenReady', - { playWhenReady: true } - )); - assertTrue(playWhenReady); - sendMessage(createMessage( - 'player.setPlayWhenReady', - { playWhenReady: false } - )); - assertFalse(playWhenReady); - }, - - /** Tests setting repeat modes. */ - testSetRepeatMode() { - let repeatMode; - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - player.addPlayerListener((playerState) => { - repeatMode = playerState.repeatMode; - }); - - sendMessage(createMessage( - 'player.setRepeatMode', - { repeatMode: Player.RepeatMode.ONE } - )); - assertEquals(Player.RepeatMode.ONE, repeatMode); - assertEquals(0, player.getNextWindowIndex()); - assertEquals(0, player.getPreviousWindowIndex()); - - sendMessage(createMessage( - 'player.setRepeatMode', - { repeatMode: Player.RepeatMode.ALL } - )); - assertEquals(Player.RepeatMode.ALL, repeatMode); - assertEquals(1, player.getNextWindowIndex()); - assertEquals(2, player.getPreviousWindowIndex()); - - sendMessage(createMessage( - 'player.setRepeatMode', - { repeatMode: Player.RepeatMode.OFF } - )); - assertEquals(Player.RepeatMode.OFF, repeatMode); - assertEquals(1, player.getNextWindowIndex()); - assertTrue(player.getPreviousWindowIndex() < 0); - }, - - /** Tests setting an invalid repeat mode value. */ - testSetRepeatMode_invalid_noStateChange() { - let repeatMode; - player.addPlayerListener((playerState) => { - repeatMode = playerState.repeatMode; - }); - - sendMessage(createMessage( - 'player.setRepeatMode', - { repeatMode: "UNKNOWN" } - )); - assertEquals(Player.RepeatMode.OFF, player.repeatMode_); - assertUndefined(repeatMode); - player.invalidate(); - assertEquals(Player.RepeatMode.OFF, repeatMode); - }, - - /** Tests enabling and disabling shuffle mode. */ - testSetShuffleModeEnabled() { - const enableMessage = createMessage('player.setShuffleModeEnabled', { - shuffleModeEnabled: true, - }); - const disableMessage = createMessage('player.setShuffleModeEnabled', { - shuffleModeEnabled: false, - }); - let shuffleModeEnabled; - player.addPlayerListener((state) => { - shuffleModeEnabled = state.shuffleModeEnabled; - }); - assertFalse(player.shuffleModeEnabled_); - sendMessage(enableMessage); - assertTrue(shuffleModeEnabled); - sendMessage(disableMessage); - assertFalse(shuffleModeEnabled); - }, - - /** Tests adding a single media item to the queue. */ - testAddMediaItem_single() { - const suffix = '0'; - const jsonMessage = createMessage('player.addItems', { - index: 0, - items: [ - createMediaItem(suffix), - ], - shuffleOrder: [0], - }); - - sendMessage(jsonMessage); - assertEquals(1, queue.length); - assertEquals('uuid0', queue[0].uuid); - assertEquals('uri0', queue[0].media.uri); - assertArrayEquals([0], player.shuffleOrder_); - }, - - /** Tests adding multiple media items to the queue. */ - testAddMediaItem_multiple() { - const shuffleOrder = [0, 2, 1]; - const jsonMessage = createMessage('player.addItems', { - index: 0, - items: [ - createMediaItem('0'), - createMediaItem('1'), - createMediaItem('2'), - ], - shuffleOrder: shuffleOrder, - }); - - sendMessage(jsonMessage); - assertArrayEquals(['uuid0', 'uuid1', 'uuid2'], queue.map((x) => x.uuid)); - assertArrayEquals(shuffleOrder, player.shuffleOrder_); - }, - - /** Tests adding a media item to end of the queue by omitting the index. */ - testAddMediaItem_noindex_addstoend() { - const shuffleOrder = [1, 3, 2, 0]; - const jsonMessage = createMessage('player.addItems', { - items: [createMediaItem('99')], - shuffleOrder: shuffleOrder, - }); - player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); - let queue = []; - player.addPlayerListener((playerState) => { - queue = playerState.mediaQueue; - }); - sendMessage(jsonMessage); - assertEquals(4, queue.length); - assertEquals('uuid99', queue[3].uuid); - assertArrayEquals(shuffleOrder, player.shuffleOrder_); - }, - - /** Tests adding items with a shuffle order of invalid length. */ - testAddMediaItems_invalidShuffleOrderLength() { - const shuffleOrder = [1, 3, 2]; - const jsonMessage = createMessage('player.addItems', { - items: [createMediaItem('99')], - shuffleOrder: shuffleOrder, - }); - player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); - let queue = []; - player.addPlayerListener((playerState) => { - queue = playerState.mediaQueue; - }); - sendMessage(jsonMessage); - assertEquals(4, queue.length); - assertEquals('uuid99', queue[3].uuid); - assertEquals(4, player.shuffleOrder_.length); - }, - - /** Tests inserting a media item to the queue. */ - testAddMediaItem_insert() { - const index = 1; - const shuffleOrder = [1, 0, 3, 2, 4]; - const firstInsertionMessage = createMessage('player.addItems', { - index, - items: [ - createMediaItem('99'), - createMediaItem('100'), - ], - shuffleOrder, - }); - const prepareMessage = createMessage('player.prepare', {}); - const secondInsertionMessage = createMessage('player.addItems', { - index, - items: [ - createMediaItem('199'), - createMediaItem('1100'), - ], - shuffleOrder, - }); - // fill with three items - player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); - player.seekToUuid('uuid99', 0); - - sendMessage(firstInsertionMessage); - // The window index does not change when IDLE. - assertEquals(1, player.getCurrentWindowIndex()); - assertEquals(5, queue.length); - assertArrayEquals(shuffleOrder, player.shuffleOrder_); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - - // Prepare sets the index by the uuid to which we seeked. - sendMessage(prepareMessage); - assertEquals(1, player.getCurrentWindowIndex()); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - // Add two items at the current window index. - sendMessage(secondInsertionMessage); - // Current window index is adjusted. - assertEquals(3, player.getCurrentWindowIndex()); - assertEquals(7, queue.length); - assertEquals('uuid199', queue[index].uuid); - assertEquals(7, player.shuffleOrder_.length); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests adding a media item with an index larger than the queue size. */ - testAddMediaItem_indexLargerThanQueueSize_addsToEnd() { - const index = 4; - const jsonMessage = createMessage('player.addItems', { - index: index, - items: [ - createMediaItem('99'), - createMediaItem('100'), - ], - shuffleOrder: [], - }); - player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); - - assertArrayEquals(['uuid0', 'uuid1', 'uuid2'], queue.map((x) => x.uuid)); - sendMessage(jsonMessage); - assertArrayEquals(['uuid0', 'uuid1', 'uuid2', 'uuid99', 'uuid100'], - queue.map((x) => x.uuid)); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests removing an item from the queue. */ - testRemoveMediaItem() { - const jsonMessage = - createMessage('player.removeItems', {uuids: ['uuid1', 'uuid0']}); - player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); - assertArrayEquals(['uuid0', 'uuid1', 'uuid2'], queue.map((x) => x.uuid)); - - sendMessage(jsonMessage); - assertArrayEquals(['uuid2'], queue.map((x) => x.uuid)); - assertArrayEquals([0], player.shuffleOrder_); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests removing the currently playing item from the queue. */ - async testRemoveMediaItem_currentItem() { - const jsonMessage = - createMessage('player.removeItems', {uuids: ['uuid1', 'uuid0']}); - player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); - player.seekToWindow(1, 0); - player.prepare(); - - await sendMessage(jsonMessage); - assertArrayEquals(['uuid2'], queue.map((x) => x.uuid)); - assertEquals(0, player.getCurrentWindowIndex()); - assertEquals(util.queue[2].media.uri, shakaFake.getMediaElement().src); - assertArrayEquals([0], player.shuffleOrder_); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests removing items which affect the current window index. */ - async testRemoveMediaItem_affectsWindowIndex() { - const jsonMessage = - createMessage('player.removeItems', {uuids: ['uuid1', 'uuid0']}); - const currentUri = util.queue[4].media.uri; - player.addQueueItems(0, util.queue.slice(0, 6), [3, 2, 1, 4, 0, 5]); - player.prepare(); - await player.seekToWindow(4, 2000); - assertEquals(currentUri, shakaFake.getMediaElement().src); - - sendMessage(jsonMessage); - assertEquals(4, queue.length); - assertEquals('uuid4', queue[player.getCurrentWindowIndex()].uuid); - assertEquals(2, player.getCurrentWindowIndex()); - assertEquals(currentUri, shakaFake.getMediaElement().src); - assertArrayEquals([1, 0, 2, 3], player.shuffleOrder_); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests removing the last item of the queue. */ - testRemoveMediaItem_firstItem_windowIndexIsCorrect() { - const jsonMessage = - createMessage('player.removeItems', {uuids: ['uuid0']}); - player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); - player.seekToWindow(1, 0); - - sendMessage(jsonMessage); - assertArrayEquals(['uuid1', 'uuid2'], queue.map((x) => x.uuid)); - assertEquals(0, player.getCurrentWindowIndex()); - assertArrayEquals([1, 0], player.shuffleOrder_); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests removing the last item of the queue. */ - testRemoveMediaItem_lastItem_windowIndexIsCorrect() { - const jsonMessage = - createMessage('player.removeItems', {uuids: ['uuid2']}); - player.addQueueItems(0, util.queue.slice(0, 3), [0, 2, 1]); - player.seekToWindow(2, 0); - player.prepare(); - - mocks.state().isSilent = true; - const states = []; - player.addPlayerListener((playerState) => { - states.push(playerState); - }); - sendMessage(jsonMessage); - assertArrayEquals(['uuid0', 'uuid1'], queue.map((x) => x.uuid)); - assertEquals(1, player.getCurrentWindowIndex()); - assertArrayEquals([0, 1], player.shuffleOrder_); - assertEquals(1, states.length); - assertEquals(Player.PlaybackState.BUFFERING, states[0].playbackState); - assertEquals( - Player.DiscontinuityReason.PERIOD_TRANSITION, - states[0].playbackPosition.discontinuityReason); - assertEquals( - Player.DUMMY_MEDIA_ITEM_INFO, states[0].mediaItemsInfo['uuid1']); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests removing items all items. */ - testRemoveMediaItem_removeAll() { - const jsonMessage = createMessage('player.removeItems', - {uuids: ['uuid1', 'uuid0', 'uuid2']}); - player.addQueueItems(0, util.queue.slice(0, 3)); - player.seekToWindow(2, 2000); - player.prepare(); - let playerState; - player.addPlayerListener((state) => { - playerState = state; - }); - - sendMessage(jsonMessage); - assertInitialState(playerState, 'ENDED'); - assertEquals(0, player.getCurrentWindowIndex()); - assertArrayEquals([], player.shuffleOrder_); - assertEquals(Player.PlaybackState.ENDED, player.getPlaybackState()); - util.assertUuidIndexMap(player.queueUuidIndexMap_, []); - }, - - /** Tests moving an item in the queue. */ - testMoveItem() { - let shuffleOrder = [0, 2, 1]; - const jsonMessage = createMessage('player.moveItem', { - uuid: 'uuid2', - index: 0, - shuffleOrder: shuffleOrder, - }); - player.addQueueItems(0, util.queue.slice(0, 3)); - - assertArrayEquals(['uuid0', 'uuid1', 'uuid2'], queue.map((x) => x.uuid)); - sendMessage(jsonMessage); - assertArrayEquals(['uuid2', 'uuid0', 'uuid1'], queue.map((x) => x.uuid)); - assertArrayEquals(shuffleOrder, player.shuffleOrder_); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests moving the currently playing item in the queue. */ - testMoveItem_currentWindowIndex() { - let shuffleOrder = [0, 2, 1]; - const jsonMessage = createMessage('player.moveItem', { - uuid: 'uuid2', - index: 0, - shuffleOrder: shuffleOrder, - }); - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - player.seekToUuid('uuid2', 0); - assertEquals(2, player.getCurrentWindowIndex()); - - assertArrayEquals(['uuid0', 'uuid1', 'uuid2'], queue.map((x) => x.uuid)); - sendMessage(jsonMessage); - assertArrayEquals(['uuid2', 'uuid0', 'uuid1'], queue.map((x) => x.uuid)); - assertEquals(0, player.getCurrentWindowIndex()); - assertArrayEquals(shuffleOrder, player.shuffleOrder_); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests moving an item from before to after the currently playing item. */ - testMoveItem_decreaseCurrentWindowIndex() { - const jsonMessage = createMessage('player.moveItem', { - uuid: 'uuid0', - index: 5, - shuffleOrder: [], - }); - player.addQueueItems(0, util.queue.slice(0, 6)); - player.prepare(); - player.seekToWindow(2, 0); - assertEquals(2, player.getCurrentWindowIndex()); - - assertArrayEquals(['uuid0', 'uuid1', 'uuid2', 'uuid3', 'uuid4', 'uuid5'], - queue.map((x) => x.uuid)); - sendMessage(jsonMessage); - assertArrayEquals(['uuid1', 'uuid2', 'uuid3', 'uuid4', 'uuid5', 'uuid0'], - queue.map((x) => x.uuid)); - assertEquals(1, player.getCurrentWindowIndex()); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests moving an item from after to before the currently playing item. */ - testMoveItem_increaseCurrentWindowIndex() { - const jsonMessage = createMessage('player.moveItem', { - uuid: 'uuid5', - index: 0, - shuffleOrder: [], - }); - player.addQueueItems(0, util.queue.slice(0, 6)); - player.prepare(); - player.seekToWindow(2, 0); - assertEquals(2, player.getCurrentWindowIndex()); - - assertArrayEquals(['uuid0', 'uuid1', 'uuid2', 'uuid3', 'uuid4', 'uuid5'], - queue.map((x) => x.uuid)); - sendMessage(jsonMessage); - assertArrayEquals(['uuid5', 'uuid0', 'uuid1', 'uuid2', 'uuid3', 'uuid4'], - queue.map((x) => x.uuid)); - assertEquals(3, player.getCurrentWindowIndex()); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests moving an item from after to the current window index. */ - testMoveItem_toCurrentWindowIndex_fromAfter() { - const jsonMessage = createMessage('player.moveItem', { - uuid: 'uuid5', - index: 2, - shuffleOrder: [], - }); - player.addQueueItems(0, util.queue.slice(0, 6)); - player.prepare(); - player.seekToWindow(2, 0); - assertEquals(2, player.getCurrentWindowIndex()); - - assertArrayEquals(['uuid0', 'uuid1', 'uuid2', 'uuid3', 'uuid4', 'uuid5'], - queue.map((x) => x.uuid)); - sendMessage(jsonMessage); - assertArrayEquals(['uuid0', 'uuid1', 'uuid5', 'uuid2', 'uuid3', 'uuid4'], - queue.map((x) => x.uuid)); - assertEquals(3, player.getCurrentWindowIndex()); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests moving an item from before to the current window index. */ - testMoveItem_toCurrentWindowIndex_fromBefore() { - const jsonMessage = createMessage('player.moveItem', { - uuid: 'uuid0', - index: 2, - shuffleOrder: [], - }); - player.addQueueItems(0, util.queue.slice(0, 6)); - player.prepare(); - player.seekToWindow(2, 0); - assertEquals(2, player.getCurrentWindowIndex()); - - assertArrayEquals(['uuid0', 'uuid1', 'uuid2', 'uuid3', 'uuid4', 'uuid5'], - queue.map((x) => x.uuid)); - sendMessage(jsonMessage); - assertArrayEquals(['uuid1', 'uuid2', 'uuid0', 'uuid3', 'uuid4', 'uuid5'], - queue.map((x) => x.uuid)); - assertEquals(1, player.getCurrentWindowIndex()); - util.assertUuidIndexMap(player.queueUuidIndexMap_, queue); - }, - - /** Tests seekTo. */ - testSeekTo() { - const jsonMessage = createMessage('player.seekTo', - { - 'uuid': 'uuid1', - 'positionMs': 2000 - }); - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - sendMessage(jsonMessage); - assertEquals(2000, player.getCurrentPositionMs()); - assertEquals(1, player.getCurrentWindowIndex()); - }, - - /** Tests seekTo to unknown uuid. */ - testSeekTo_unknownUuid() { - const jsonMessage = createMessage('player.seekTo', - { - 'uuid': 'unknown', - }); - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - player.seekToWindow(1, 2000); - assertEquals(2000, player.getCurrentPositionMs()); - assertEquals(1, player.getCurrentWindowIndex()); - - sendMessage(jsonMessage); - assertEquals(2000, player.getCurrentPositionMs()); - assertEquals(1, player.getCurrentWindowIndex()); - }, - - /** Tests seekTo without position. */ - testSeekTo_noPosition_defaultsToZero() { - const jsonMessage = createMessage('player.seekTo', - { - 'uuid': 'uuid1', - }); - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - sendMessage(jsonMessage); - assertEquals(0, player.getCurrentPositionMs()); - assertEquals(1, player.getCurrentWindowIndex()); - }, - - /** Tests seekTo to negative position. */ - testSeekTo_negativePosition_defaultsToZero() { - const jsonMessage = createMessage('player.seekTo', - { - 'uuid': 'uuid2', - 'positionMs': -1, - }); - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - player.seekToWindow(1, 2000); - assertEquals(2000, player.getCurrentPositionMs()); - assertEquals(1, player.getCurrentWindowIndex()); - - sendMessage(jsonMessage); - assertEquals(0, player.getCurrentPositionMs()); - assertEquals(2, player.getCurrentWindowIndex()); - }, - - /** Tests whether validation is turned on. */ - testMediaItemValidation_isOn() { - const index = 0; - const mediaItem = createMediaItem('99'); - delete mediaItem.uuid; - const jsonMessage = createMessage('player.addItems', { - index: index, - items: [mediaItem], - shuffleOrder: [], - }); - - sendMessage(jsonMessage); - assertEquals(0, queue.length); - }, - - /** Tests whether the state is sent to sender apps on state transition. */ - testPlayerStateIsSent_withCorrectSequenceNumber() { - assertUndefined(mocks.state().outputMessages['sender0']); - const playMessage = - createMessage('player.setPlayWhenReady', {playWhenReady: true}); - sendMessage(playMessage); - - const playerState = mocks.state().outputMessages['sender0'][0]; - assertTrue(playerState.playWhenReady); - assertEquals(playMessage.sequenceNumber, playerState.sequenceNumber); - }, - - /** Tests whether a connect of a sender app sends the current player state. */ - testSenderConnection() { - const onSenderConnected = mocks.state().onSenderConnected; - assertTrue(goog.isFunction(onSenderConnected)); - onSenderConnected({senderId: 'sender0'}); - - const playerState = mocks.state().outputMessages['sender0'][0]; - assertEquals(Player.RepeatMode.OFF, playerState.repeatMode); - assertEquals('IDLE', playerState.playbackState); - assertArrayEquals([], playerState.mediaQueue); - assertEquals(-1, playerState.sequenceNumber); - }, - - /** Tests whether a disconnect of a sender notifies the message dispatcher. */ - testSenderDisconnection_callsMessageDispatcher() { - mocks.setUp(); - let notifiedSenderId; - const myPlayer = new Player(mocks.createShakaFake()); - const myManagerFake = mocks.createCastReceiverContextFake(); - new Receiver(myPlayer, myManagerFake, { - registerActionHandler() {}, - notifySenderDisconnected(senderId) { - notifiedSenderId = senderId; - }, - }); - - const onSenderDisconnected = mocks.state().onSenderDisconnected; - assertTrue(goog.isFunction(onSenderDisconnected)); - onSenderDisconnected({senderId: 'sender0'}); - assertEquals('sender0', notifiedSenderId); - }, - - /** - * Tests whether the state right after creation of the player matches - * expectations. - */ - testInitialState() { - mocks.state().isSilent = true; - let playerState; - player.addPlayerListener((state) => { - playerState = state; - }); - assertEquals(0, player.getCurrentPositionMs()); - // Dump a player state to the listener. - player.invalidate(); - // Asserts the state just after creation. - assertInitialState(playerState, 'IDLE'); - }, - - /** Tests whether user properties can be changed when in IDLE state */ - testChangingUserPropertiesWhenIdle() { - mocks.state().isSilent = true; - const states = []; - let counter = 0; - player.addPlayerListener((state) => { - states.push(state); - }); - // Adding items when IDLE. - player.addQueueItems(0, util.queue.slice(0, 3)); - counter++; - assertEquals(counter, states.length); - assertEquals(Player.PlaybackState.IDLE, states[counter - 1].playbackState); - assertArrayEquals( - ['uuid0', 'uuid1', 'uuid2'], - states[counter - 1].mediaQueue.map((i) => i.uuid)); - - // Set playWhenReady when IDLE. - assertFalse(player.getPlayWhenReady()); - player.setPlayWhenReady(true); - counter++; - assertTrue(player.getPlayWhenReady()); - assertEquals(counter, states.length); - assertEquals(Player.PlaybackState.IDLE, states[counter - 1].playbackState); - - // Seeking when IDLE. - player.seekToUuid('uuid2', 1000); - counter++; - // Window index not set when idle. - assertEquals(2, player.getCurrentWindowIndex()); - assertEquals(1000, player.getCurrentPositionMs()); - assertEquals(counter, states.length); - assertEquals(Player.PlaybackState.IDLE, states[counter - 1].playbackState); - // But window index is set when prepared. - player.prepare(); - assertEquals(2, player.getCurrentWindowIndex()); - }, - - /** Tests the state after calling prepare. */ - testPrepare() { - mocks.state().isSilent = true; - const states = []; - let counter = 0; - player.addPlayerListener((state) => { - states.push(state); - }); - const prepareMessage = createMessage('player.prepare', {}); - - player.addQueueItems(0, util.queue.slice(0, 3)); - player.seekToWindow(1, 1000); - counter += 2; - - // Sends prepare message. - sendMessage(prepareMessage); - counter++; - assertEquals(counter, states.length); - assertEquals('uuid1', states[counter - 1].playbackPosition.uuid); - assertEquals( - Player.PlaybackState.BUFFERING, states[counter - 1].playbackState); - - // Fakes Shaka events. - mocks.state().isSilent = false; - mocks.notifyListeners('streaming'); - mocks.notifyListeners('loadeddata'); - counter += 2; - assertEquals(counter, states.length); - assertEquals(Player.PlaybackState.READY, states[counter - 1].playbackState); - }, - - /** Tests stopping the player with `reset=true`. */ - testStop_resetTrue() { - mocks.state().isSilent = true; - let playerState; - player.addPlayerListener((state) => { - playerState = state; - }); - const stopMessage = createMessage('player.stop', {reset: true}); - - player.setRepeatMode(Player.RepeatMode.ALL); - player.setShuffleModeEnabled(true); - player.setPlayWhenReady(true); - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - mocks.state().isSilent = false; - mocks.notifyListeners('loadeddata'); - assertArrayEquals(['uuid0', 'uuid1', 'uuid2'], queue.map((i) => i.uuid)); - assertEquals(0, playerState.windowIndex); - assertNotEquals(Player.DUMMY_MEDIA_ITEM_INFO, player.windowMediaItemInfo_); - assertEquals(1, player.playbackType_); - // Stop the player. - sendMessage(stopMessage); - // Asserts the state looks the same as just after creation. - assertInitialState(playerState, 'IDLE'); - assertNull(playerState.playbackPosition); - // Assert player properties are preserved. - assertTrue(playerState.shuffleModeEnabled); - assertTrue(playerState.playWhenReady); - assertEquals(Player.RepeatMode.ALL, playerState.repeatMode); - }, - - /** Tests stopping the player with `reset=false`. */ - testStop_resetFalse() { - mocks.state().isSilent = true; - let playerState; - player.addPlayerListener((state) => { - playerState = state; - }); - const stopMessage = createMessage('player.stop', {reset: false}); - - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - player.seekToUuid('uuid1', 1000); - mocks.state().isSilent = false; - mocks.notifyListeners('streaming'); - mocks.notifyListeners('trackschanged'); - mocks.notifyListeners('loadeddata'); - assertArrayEquals(['uuid0', 'uuid1', 'uuid2'], queue.map((i) => i.uuid)); - assertEquals(1, playerState.windowIndex); - assertNotEquals(Player.DUMMY_MEDIA_ITEM_INFO, player.windowMediaItemInfo_); - assertEquals(2, player.playbackType_); - // Stop the player. - sendMessage(stopMessage); - assertEquals('IDLE', playerState.playbackState); - assertUndefined(playerState.playbackError); - // Assert the timeline is preserved. - assertEquals(3, queue.length); - assertEquals(3, playerState.windowCount); - assertEquals(1, player.windowIndex_); - assertEquals(1, playerState.windowIndex); - // Assert the playback position is correct. - assertEquals(1000, playerState.playbackPosition.positionMs); - assertEquals('uuid1', playerState.playbackPosition.uuid); - assertEquals(0, playerState.playbackPosition.periodId); - assertNull(playerState.playbackPosition.discontinuityReason); - assertEquals(1000, player.getCurrentPositionMs()); - // Assert player properties are preserved. - assertEquals(20000, player.getDurationMs()); - assertEquals(2, Object.entries(player.mediaItemInfoMap_).length); - assertEquals(0, player.windowPeriodIndex_); - assertEquals(1, player.getCurrentWindowIndex()); - assertEquals(1, player.windowIndex_); - assertNotEquals(Player.DUMMY_MEDIA_ITEM_INFO, player.windowMediaItemInfo_); - assertEquals(999, player.playbackType_); - assertEquals('uuid1', player.uuidToPrepare_); - }, - - /** - * Tests the state after having removed the last item in the queue. This - * resolves to the same state like calling `stop(true)` except that the state - * is ENDED and the queue is naturally empty and hence the windowIndex is - * unset. - */ - testRemoveLastQueueItem() { - mocks.state().isSilent = true; - let playerState; - player.addPlayerListener((state) => { - playerState = state; - }); - const removeAllItemsMessage = createMessage( - 'player.removeItems', {uuids: ['uuid0', 'uuid1', 'uuid2']}); - - player.addQueueItems(0, util.queue.slice(0, 3)); - player.seekToWindow(0, 1000); - player.prepare(); - mocks.state().isSilent = false; - mocks.notifyListeners('loadeddata'); - // Remove all items. - sendMessage(removeAllItemsMessage); - // Assert the state after removal of all items. - assertInitialState(playerState, 'ENDED'); - }, - - /** Tests whether a player state is sent when no item is added. */ - testAddItem_noop() { - mocks.state().isSilent = true; - let playerStates = []; - player.addPlayerListener((state) => { - playerStates.push(state); - }); - const noOpMessage = createMessage('player.addItems', { - index: 0, - items: [ - util.queue[0], - ], - shuffleOrder: [0], - }); - player.addQueueItems(0, [util.queue[0]], []); - player.prepare(); - assertEquals(2, playerStates.length); - assertEquals(2, mocks.state().outputMessages['sender0'].length); - sendMessage(noOpMessage); - assertEquals(2, playerStates.length); - assertEquals(3, mocks.state().outputMessages['sender0'].length); - }, - - /** Tests whether a player state is sent when no item is removed. */ - testRemoveItem_noop() { - mocks.state().isSilent = true; - let playerStates = []; - player.addPlayerListener((state) => { - playerStates.push(state); - }); - const noOpMessage = - createMessage('player.removeItems', {uuids: ['uuid00']}); - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - assertEquals(2, playerStates.length); - assertEquals(2, mocks.state().outputMessages['sender0'].length); - sendMessage(noOpMessage); - assertEquals(2, playerStates.length); - assertEquals(3, mocks.state().outputMessages['sender0'].length); - }, - - /** Tests whether a player state is sent when item is not moved. */ - testMoveItem_noop() { - mocks.state().isSilent = true; - let playerStates = []; - player.addPlayerListener((state) => { - playerStates.push(state); - }); - const noOpMessage = createMessage('player.moveItem', { - uuid: 'uuid00', - index: 0, - shuffleOrder: [], - }); - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - assertEquals(2, playerStates.length); - assertEquals(2, mocks.state().outputMessages['sender0'].length); - sendMessage(noOpMessage); - assertEquals(2, playerStates.length); - assertEquals(3, mocks.state().outputMessages['sender0'].length); - }, - - /** Tests whether playback actions send a state when no-op */ - testNoOpPlaybackActionsSendPlayerState() { - mocks.state().isSilent = true; - let playerStates = []; - let parsedMessage; - player.addPlayerListener((state) => { - playerStates.push(state); - }); - player.addQueueItems(0, util.queue.slice(0, 3)); - player.prepare(); - - const outputMessages = mocks.state().outputMessages['sender0']; - const setupMessageCount = playerStates.length; - let totalMessageCount = setupMessageCount; - assertEquals(setupMessageCount, playerStates.length); - assertEquals(totalMessageCount, outputMessages.length); - - const firstNoOpMessage = createMessage('player.setPlayWhenReady', { - playWhenReady: false, - }); - let expectedSequenceNumber = firstNoOpMessage.sequenceNumber; - - sendMessage(firstNoOpMessage); - totalMessageCount++; - assertEquals(setupMessageCount, playerStates.length); - assertEquals(totalMessageCount, outputMessages.length); - parsedMessage = outputMessages[totalMessageCount - 1]; - assertEquals(expectedSequenceNumber++, parsedMessage.sequenceNumber); - - sendMessage(createMessage('player.setRepeatMode', { - repeatMode: 'OFF', - })); - totalMessageCount++; - assertEquals(setupMessageCount, playerStates.length); - assertEquals(totalMessageCount, outputMessages.length); - parsedMessage = outputMessages[totalMessageCount - 1]; - assertEquals(expectedSequenceNumber++, parsedMessage.sequenceNumber); - - sendMessage(createMessage('player.setShuffleModeEnabled', { - shuffleModeEnabled: false, - })); - totalMessageCount++; - assertEquals(setupMessageCount, playerStates.length); - assertEquals(totalMessageCount, outputMessages.length); - parsedMessage = outputMessages[totalMessageCount - 1]; - assertEquals(expectedSequenceNumber++, parsedMessage.sequenceNumber); - - sendMessage(createMessage('player.seekTo', { - uuid: 'not_existing', - positionMs: 0, - })); - totalMessageCount++; - assertEquals(setupMessageCount, playerStates.length); - assertEquals(totalMessageCount, outputMessages.length); - parsedMessage = outputMessages[totalMessageCount - 1]; - assertEquals(expectedSequenceNumber++, parsedMessage.sequenceNumber); - }, -}); diff --git a/cast_receiver_app/test/shaka_error_handling_test.js b/cast_receiver_app/test/shaka_error_handling_test.js deleted file mode 100644 index a7dafd3176..0000000000 --- a/cast_receiver_app/test/shaka_error_handling_test.js +++ /dev/null @@ -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_); - }, -}); diff --git a/cast_receiver_app/test/util.js b/cast_receiver_app/test/util.js deleted file mode 100644 index 22244675b7..0000000000 --- a/cast_receiver_app/test/util.js +++ /dev/null @@ -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} - */ -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} uuidIndexMap The uuid to index map. - * @param {!Array} 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; diff --git a/cast_receiver_app/test/validation_test.js b/cast_receiver_app/test/validation_test.js deleted file mode 100644 index 8e58185cfa..0000000000 --- a/cast_receiver_app/test/validation_test.js +++ /dev/null @@ -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')); - }, -}); diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java deleted file mode 100644 index bc38cbdb8a..0000000000 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java +++ /dev/null @@ -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 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); - } -} diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java index 9599da15cb..2d5a5f0ccf 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java @@ -98,6 +98,11 @@ import java.util.UUID; "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd", "Clear DASH: Tears", 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( new Sample( "https://html5demos.com/assets/dizzy.mp4", "Clear MP4: Dizzy", MIME_TYPE_VIDEO_MP4)); diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/ExoCastPlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/ExoCastPlayerManager.java deleted file mode 100644 index e8ad2c1a0d..0000000000 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/ExoCastPlayerManager.java +++ /dev/null @@ -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 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); - } - } - } -} diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java index 5ed434eed6..c17c0a62ab 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -34,14 +34,11 @@ import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.TextView; -import android.widget.Toast; import com.google.android.exoplayer2.C; 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.ui.PlayerControlView; 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.CastContext; 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. return; } - String applicationId = castContext.getCastOptions().getReceiverApplicationId(); - switch (applicationId) { - case CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID: - case DefaultCastOptionsProvider.APP_ID_DEFAULT_RECEIVER_WITH_DRM: - playerManager = - new DefaultReceiverPlayerManager( - /* listener= */ this, - localPlayerView, - castControlView, - /* context= */ this, - castContext); - break; - default: - throw new IllegalStateException("Illegal receiver app id: " + applicationId); - } + playerManager = + new PlayerManager( + /* listener= */ this, + localPlayerView, + castControlView, + /* context= */ this, + castContext); 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. private View buildSampleListView() { diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index c9a728b3ff..c92ebd7e94 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -15,53 +15,419 @@ */ 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 the players in the Cast demo app. */ -/* package */ interface PlayerManager { +/** Manages players and an internal media queue for the demo app. */ +/* package */ class PlayerManager implements EventListener, SessionAvailabilityListener { /** Listener for events. */ interface Listener { /** Called when the currently played item of the media queue changes. */ 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. */ - boolean dispatchKeyEvent(KeyEvent keyEvent); + private static final String USER_AGENT = "ExoCastDemoPlayer"; + private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY = + new DefaultHttpDataSourceFactory(USER_AGENT); - /** Appends the given {@link MediaItem} to the media queue. */ - void addItem(MediaItem mediaItem); + private final PlayerView localPlayerView; + private final PlayerControlView castControlView; + private final SimpleExoPlayer exoPlayer; + private final CastPlayer castPlayer; + private final ArrayList mediaQueue; + private final Listener listener; + private final ConcatenatingMediaSource concatenatingMediaSource; - /** Returns the number of items in the media queue. */ - int getMediaQueueSize(); - - /** Selects the item at the given position for playback. */ - void selectQueueItem(int position); + private int currentItemIndex; + private Player currentPlayer; /** - * Returns the position of the item currently being played, or {@link C#INDEX_UNSET} if no item is - * being played. + * 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}. */ - 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}. */ - MediaItem getItem(int position); + DefaultTrackSelector trackSelector = new DefaultTrackSelector(); + 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}. */ - boolean moveItem(MediaItem item, int to); + castPlayer = new CastPlayer(castContext); + castPlayer.addListener(this); + castPlayer.setSessionAvailabilityListener(this); + castControlView.setPlayer(castPlayer); - /** Removes the item at position {@code index}. */ - boolean removeItem(MediaItem item); + setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer); + } - /** Releases any acquired resources. */ - void release(); + // Queue manipulation methods. + + /** + * 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); + } } diff --git a/demos/cast/src/main/res/values/strings.xml b/demos/cast/src/main/res/values/strings.xml index 013b50a175..2f0acd4808 100644 --- a/demos/cast/src/main/res/values/strings.xml +++ b/demos/cast/src/main/res/values/strings.xml @@ -24,6 +24,4 @@ Failed to get Cast context. Try updating Google Play Services and restart the app. - Player error encountered. Select a queue item to reprepare. Check the logcat and receiver app\'s console for more info. - diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastSessionManager.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastSessionManager.java deleted file mode 100644 index 7c1f06e8d2..0000000000 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastSessionManager.java +++ /dev/null @@ -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. - * - *

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. - * - *

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); -} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java index 5aed1373e5..8948173f60 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java @@ -44,7 +44,7 @@ public final class DefaultCastOptionsProvider implements OptionsProvider { * do not require DRM, the default receiver app should be used (see {@link * #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]. public static final String APP_ID_DEFAULT_RECEIVER_WITH_DRM = "A12D4273"; diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastSessionManager.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastSessionManager.java deleted file mode 100644 index c08a9bc352..0000000000 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastSessionManager.java +++ /dev/null @@ -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 { - - @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); - } - } - } -} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastConstants.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastConstants.java deleted file mode 100644 index 36173bfc5d..0000000000 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastConstants.java +++ /dev/null @@ -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"; -} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastMessage.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastMessage.java deleted file mode 100644 index 1529e9f5ac..0000000000 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastMessage.java +++ /dev/null @@ -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 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 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 uuids; - - /** @param uuids See {@link #uuids}. */ - public RemoveItems(List 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); - } - } -} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastOptionsProvider.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastOptionsProvider.java deleted file mode 100644 index 56b5d3cc8c..0000000000 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastOptionsProvider.java +++ /dev/null @@ -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 getAdditionalSessionProviders(Context context) { - return null; - } -} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastPlayer.java deleted file mode 100644 index e24970ba0d..0000000000 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastPlayer.java +++ /dev/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. - * - *

The ExoCast communication protocol consists in exchanging serialized {@link ExoCastMessage - * ExoCastMessages} and {@link ReceiverAppStateUpdate receiver app state updates}. - * - *

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. - * - *

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 listeners; - private final ArrayList notificationsBatch; - private final ArrayDeque ongoingNotificationsTasks; - private final Timeline.Period scratchPeriod; - @Nullable private SessionAvailabilityListener sessionAvailabilityListener; - - // Player state. - - private final List mediaItems; - private final StateHolder currentTimeline; - private ShuffleOrder currentShuffleOrder; - - private final StateHolder playbackState; - private final StateHolder playWhenReady; - private final StateHolder repeatMode; - private final StateHolder shuffleModeEnabled; - private final StateHolder isLoading; - private final StateHolder playbackParameters; - private final StateHolder trackselectionParameters; - private final StateHolder currentTrackGroups; - private final StateHolder currentTrackSelections; - private final StateHolder<@NullableType Object> currentManifest; - private final StateHolder<@NullableType PeriodUid> currentPeriodUid; - private final StateHolder playbackPositionMs; - private final HashMap 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. - * - *

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. - * - *

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 itemsToAdd = new ArrayList<>(); - HashSet 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. - * - *

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. - * - *

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. - * - *

{@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 mediaItemsInformation) { - for (Map.Entry 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 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 { - - public T value; - public long sequence; - - public StateHolder(T initialValue) { - value = initialValue; - sequence = CastSessionManager.SEQUENCE_NUMBER_UNSET; - } - } - - private final class ListenerNotificationTask { - - private final Iterator 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); - } - } - } -} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastTimeline.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastTimeline.java deleted file mode 100644 index 115536ac4c..0000000000 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ExoCastTimeline.java +++ /dev/null @@ -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. - * - *

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 mediaItems, - Map 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 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 mediaItems; - private final Map mediaItemInfoMap; - private final ShuffleOrder shuffleOrder; - - // Precomputed for quick access. - private final Map uuidToIndex; - private final int[] accumulativePeriodCount; - - private ExoCastTimeline( - List mediaItems, - Map mediaItemInfoMap, - Map 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 mediaItems, - Map 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 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; - } - } -} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemInfo.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemInfo.java deleted file mode 100644 index cb5eff4f37..0000000000 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemInfo.java +++ /dev/null @@ -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}. - * - *

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 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 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; - } -} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemQueue.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemQueue.java deleted file mode 100644 index 184e347e1c..0000000000 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemQueue.java +++ /dev/null @@ -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. - * - *

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. - * - *

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(); -} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdate.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdate.java deleted file mode 100644 index c1b12428d4..0000000000 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdate.java +++ /dev/null @@ -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 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 shuffleOrder; - private Map 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 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 mediaItemsInformation) { - this.mediaItemsInformation = Collections.unmodifiableMap(mediaItemsInformation); - return this; - } - - /** See {@link ReceiverAppStateUpdate#shuffleOrder}. */ - public Builder setShuffleOrder(List 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 mediaItemInformation = new HashMap<>(); - JSONObject mediaItemsInfo = stateAsJson.getJSONObject(KEY_MEDIA_ITEMS_INFO); - for (Iterator 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 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 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 mediaItemsInformation; - /** Holds the indices of the media queue items in shuffle order. */ - @Nullable public final List shuffleOrder; - - /** Creates an instance with the given values. */ - private ReceiverAppStateUpdate( - long sequenceNumber, - @Nullable Boolean playWhenReady, - @Nullable Integer playbackState, - @Nullable List 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 mediaItemsInformation, - @Nullable List 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 toMediaItemArrayList(JSONArray mediaItemsAsJson) - throws JSONException { - ArrayList 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 jsonArrayToDrmSchemes(JSONArray drmSchemesAsJson) - throws JSONException { - ArrayList 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 requestHeaders = new HashMap<>(); - for (Iterator 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 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); - } -} diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastMessageTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastMessageTest.java deleted file mode 100644 index b900a78937..0000000000 --- a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastMessageTest.java +++ /dev/null @@ -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 headersMedia = new HashMap<>(); - headersMedia.put("header1", "value1"); - headersMedia.put("header2", "value2"); - UriBundle media = new UriBundle(Uri.parse("www.google.com"), headersMedia); - - HashMap headersWidevine = new HashMap<>(); - headersWidevine.put("widevine", "value"); - UriBundle widevingUriBundle = new UriBundle(Uri.parse("www.widevine.com"), headersWidevine); - - HashMap 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 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 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 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); - } - } -} diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastPlayerTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastPlayerTest.java deleted file mode 100644 index 58f78b090a..0000000000 --- a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastPlayerTest.java +++ /dev/null @@ -1,1018 +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_INDEX; -import static com.google.android.exoplayer2.ext.cast.ExoCastConstants.KEY_ITEMS; -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.common.truth.Truth.assertThat; -import static org.junit.Assert.fail; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Matchers.isNull; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.initMocks; - -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.testutil.FakeClock; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelectionParameters; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.UUID; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; - -/** Unit test for {@link ExoCastPlayer}. */ -@RunWith(AndroidJUnit4.class) -public class ExoCastPlayerTest { - - private static final long MOCK_SEQUENCE_NUMBER = 1; - private ExoCastPlayer player; - private MediaItem.Builder itemBuilder; - private CastSessionManager.StateListener receiverAppStateListener; - private FakeClock clock; - @Mock private CastSessionManager sessionManager; - @Mock private SessionAvailabilityListener sessionAvailabilityListener; - @Mock private Player.EventListener playerEventListener; - - @Before - public void setUp() { - initMocks(this); - clock = new FakeClock(/* initialTimeMs= */ 0); - player = - new ExoCastPlayer( - listener -> { - receiverAppStateListener = listener; - return sessionManager; - }, - clock); - player.addListener(playerEventListener); - itemBuilder = new MediaItem.Builder(); - } - - @Test - public void exoCastPlayer_startsAndStopsSessionManager() { - // The session manager should have been started when setting up, with the creation of - // ExoCastPlayer. - verify(sessionManager).start(); - verifyNoMoreInteractions(sessionManager); - player.release(); - verify(sessionManager).stopTrackingSession(); - verifyNoMoreInteractions(sessionManager); - } - - @Test - public void exoCastPlayer_propagatesSessionStatus() { - player.setSessionAvailabilityListener(sessionAvailabilityListener); - verify(sessionAvailabilityListener, never()).onCastSessionAvailable(); - receiverAppStateListener.onCastSessionAvailable(); - verify(sessionAvailabilityListener).onCastSessionAvailable(); - verifyNoMoreInteractions(sessionAvailabilityListener); - receiverAppStateListener.onCastSessionUnavailable(); - verify(sessionAvailabilityListener).onCastSessionUnavailable(); - verifyNoMoreInteractions(sessionAvailabilityListener); - } - - @Test - public void addItemsToQueue_producesExpectedMessages() throws JSONException { - MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); - MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); - MediaItem item3 = itemBuilder.setUuid(toUuid(3)).build(); - MediaItem item4 = itemBuilder.setUuid(toUuid(4)).build(); - MediaItem item5 = itemBuilder.setUuid(toUuid(5)).build(); - - player.addItemsToQueue(item1, item2); - assertMediaItemQueue(item1, item2); - - player.addItemsToQueue(1, item3, item4); - assertMediaItemQueue(item1, item3, item4, item2); - - player.addItemsToQueue(item5); - assertMediaItemQueue(item1, item3, item4, item2, item5); - - ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(ExoCastMessage.class); - verify(sessionManager, times(3)).send(messageCaptor.capture()); - assertMessageAddsItems( - /* message= */ messageCaptor.getAllValues().get(0), - /* index= */ C.INDEX_UNSET, - Arrays.asList(item1, item2)); - assertMessageAddsItems( - /* message= */ messageCaptor.getAllValues().get(1), - /* index= */ 1, - Arrays.asList(item3, item4)); - assertMessageAddsItems( - /* message= */ messageCaptor.getAllValues().get(2), - /* index= */ C.INDEX_UNSET, - Collections.singletonList(item5)); - } - - @Test - public void addItemsToQueue_masksRemoteUpdates() { - player.prepare(); - when(sessionManager.send(any(ExoCastMessage.class))).thenReturn(3L); - MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); - MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); - MediaItem item3 = itemBuilder.setUuid(toUuid(3)).build(); - MediaItem item4 = itemBuilder.setUuid(toUuid(4)).build(); - - player.addItemsToQueue(item1, item2); - assertMediaItemQueue(item1, item2); - - // Should be ignored due to a lower sequence number. - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 2) - .setItems(Arrays.asList(item3, item4)) - .build()); - - // Should override the current state. - assertMediaItemQueue(item1, item2); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 3) - .setItems(Arrays.asList(item3, item4)) - .build()); - - assertMediaItemQueue(item3, item4); - } - - @Test - public void addItemsToQueue_masksWindowIndexAsExpected() { - player.prepare(); - player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); - player.seekTo(/* windowIndex= */ 2, /* positionMs= */ 500); - - assertThat(player.getCurrentWindowIndex()).isEqualTo(2); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(2); - player.addItemsToQueue(/* optionalIndex= */ 0, itemBuilder.build()); - assertThat(player.getCurrentWindowIndex()).isEqualTo(3); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(3); - - player.addItemsToQueue(itemBuilder.build()); - assertThat(player.getCurrentWindowIndex()).isEqualTo(3); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(3); - } - - @Test - public void addItemsToQueue_doesNotAddDuplicateUuids() { - player.prepare(); - player.addItemsToQueue(itemBuilder.setUuid(toUuid(1)).build()); - assertThat(player.getQueueSize()).isEqualTo(1); - player.addItemsToQueue( - itemBuilder.setUuid(toUuid(1)).build(), itemBuilder.setUuid(toUuid(2)).build()); - assertThat(player.getQueueSize()).isEqualTo(2); - try { - player.addItemsToQueue( - itemBuilder.setUuid(toUuid(3)).build(), itemBuilder.setUuid(toUuid(3)).build()); - fail(); - } catch (IllegalArgumentException e) { - // Expected. - } - } - - @Test - public void moveItemInQueue_behavesAsExpected() throws JSONException { - MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); - MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); - MediaItem item3 = itemBuilder.setUuid(toUuid(3)).build(); - player.addItemsToQueue(item1, item2, item3); - assertMediaItemQueue(item1, item2, item3); - player.moveItemInQueue(/* index= */ 0, /* newIndex= */ 2); - assertMediaItemQueue(item2, item3, item1); - player.moveItemInQueue(/* index= */ 1, /* newIndex= */ 1); - assertMediaItemQueue(item2, item3, item1); - player.moveItemInQueue(/* index= */ 1, /* newIndex= */ 0); - assertMediaItemQueue(item3, item2, item1); - - ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(ExoCastMessage.class); - verify(sessionManager, times(4)).send(messageCaptor.capture()); - // First sent message is an "add" message. - assertMessageMovesItem( - /* message= */ messageCaptor.getAllValues().get(1), item1, /* index= */ 2); - assertMessageMovesItem( - /* message= */ messageCaptor.getAllValues().get(2), item3, /* index= */ 1); - assertMessageMovesItem( - /* message= */ messageCaptor.getAllValues().get(3), item3, /* index= */ 0); - } - - @Test - public void moveItemInQueue_moveBeforeToAfter_masksWindowIndexAsExpected() { - player.prepare(); - player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); - player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 500); - - assertThat(player.getCurrentWindowIndex()).isEqualTo(1); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(1); - player.moveItemInQueue(/* index= */ 0, /* newIndex= */ 1); - assertThat(player.getCurrentWindowIndex()).isEqualTo(0); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); - } - - @Test - public void moveItemInQueue_moveAfterToBefore_masksWindowIndexAsExpected() { - player.prepare(); - player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); - player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 500); - - assertThat(player.getCurrentWindowIndex()).isEqualTo(0); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); - player.moveItemInQueue(/* index= */ 1, /* newIndex= */ 0); - assertThat(player.getCurrentWindowIndex()).isEqualTo(1); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(1); - } - - @Test - public void moveItemInQueue_moveCurrent_masksWindowIndexAsExpected() { - player.prepare(); - player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); - player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 500); - - assertThat(player.getCurrentWindowIndex()).isEqualTo(0); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); - player.moveItemInQueue(/* index= */ 0, /* newIndex= */ 2); - assertThat(player.getCurrentWindowIndex()).isEqualTo(2); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(2); - } - - @Test - public void removeItemsFromQueue_masksMediaQueue() throws JSONException { - MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); - MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); - MediaItem item3 = itemBuilder.setUuid(toUuid(3)).build(); - MediaItem item4 = itemBuilder.setUuid(toUuid(4)).build(); - MediaItem item5 = itemBuilder.setUuid(toUuid(5)).build(); - player.addItemsToQueue(item1, item2, item3, item4, item5); - assertMediaItemQueue(item1, item2, item3, item4, item5); - - player.removeItemFromQueue(2); - assertMediaItemQueue(item1, item2, item4, item5); - - player.removeRangeFromQueue(1, 3); - assertMediaItemQueue(item1, item5); - - player.clearQueue(); - assertMediaItemQueue(); - - ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(ExoCastMessage.class); - verify(sessionManager, times(4)).send(messageCaptor.capture()); - // First sent message is an "add" message. - assertMessageRemovesItems( - messageCaptor.getAllValues().get(1), Collections.singletonList(item3)); - assertMessageRemovesItems(messageCaptor.getAllValues().get(2), Arrays.asList(item2, item4)); - assertMessageRemovesItems(messageCaptor.getAllValues().get(3), Arrays.asList(item1, item5)); - } - - @Test - public void removeRangeFromQueue_beforeCurrentItem_masksWindowIndexAsExpected() { - player.prepare(); - player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); - player.seekTo(/* windowIndex= */ 2, /* positionMs= */ 500); - - assertThat(player.getCurrentWindowIndex()).isEqualTo(2); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(2); - player.removeRangeFromQueue(/* indexFrom= */ 0, /* indexExclusiveTo= */ 2); - assertThat(player.getCurrentWindowIndex()).isEqualTo(0); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); - } - - @Test - public void removeRangeFromQueue_currentItem_masksWindowIndexAsExpected() { - player.prepare(); - player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); - player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 500); - - assertThat(player.getCurrentWindowIndex()).isEqualTo(1); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(1); - player.removeRangeFromQueue(/* indexFrom= */ 0, /* indexExclusiveTo= */ 2); - assertThat(player.getCurrentWindowIndex()).isEqualTo(0); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); - } - - @Test - public void removeRangeFromQueue_currentItemWhichIsLast_transitionsToEnded() { - player.prepare(); - player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); - player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 500); - - assertThat(player.getCurrentWindowIndex()).isEqualTo(1); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(1); - player.removeRangeFromQueue(/* indexFrom= */ 1, /* indexExclusiveTo= */ 3); - assertThat(player.getCurrentWindowIndex()).isEqualTo(0); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); - assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_ENDED); - } - - @Test - public void clearQueue_resetsPlaybackPosition() { - player.prepare(); - player.addItemsToQueue(itemBuilder.build(), itemBuilder.build(), itemBuilder.build()); - player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 500); - - assertThat(player.getCurrentWindowIndex()).isEqualTo(1); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(1); - player.clearQueue(); - assertThat(player.getCurrentWindowIndex()).isEqualTo(0); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); - assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_ENDED); - } - - @Test - public void prepare_emptyQueue_transitionsToEnded() { - player.prepare(); - assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_ENDED); - verify(playerEventListener).onPlayerStateChanged(/* playWhenReady=*/ false, Player.STATE_ENDED); - } - - @Test - public void prepare_withQueue_transitionsToBuffering() { - player.addItemsToQueue(itemBuilder.build()); - player.prepare(); - assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_BUFFERING); - verify(playerEventListener) - .onPlayerStateChanged(/* playWhenReady=*/ false, Player.STATE_BUFFERING); - ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Timeline.class); - verify(playerEventListener) - .onTimelineChanged( - argumentCaptor.capture(), - /* manifest= */ isNull(), - eq(Player.TIMELINE_CHANGE_REASON_PREPARED)); - assertThat(argumentCaptor.getValue().getWindowCount()).isEqualTo(1); - } - - @Test - public void stop_withoutReset_leavesCurrentTimeline() { - assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_IDLE); - player.addItemsToQueue(itemBuilder.setUuid(toUuid(1)).build()); - player.prepare(); - assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_BUFFERING); - player.seekTo(/* windowIndex= */ 0, /* positionMs= */ C.TIME_UNSET); - verify(playerEventListener) - .onPlayerStateChanged(/* playWhenReady =*/ false, Player.STATE_BUFFERING); - verify(playerEventListener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); - player.stop(/* reset= */ false); - verify(playerEventListener).onPlayerStateChanged(/* playWhenReady =*/ false, Player.STATE_IDLE); - - ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Timeline.class); - // Update for prepare. - verify(playerEventListener) - .onTimelineChanged( - argumentCaptor.capture(), - /* manifest= */ isNull(), - eq(Player.TIMELINE_CHANGE_REASON_PREPARED)); - assertThat(argumentCaptor.getValue().getWindowCount()).isEqualTo(1); - - // Update for stop. - verifyNoMoreInteractions(playerEventListener); - assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(1); - } - - @Test - public void stop_withReset_clearsQueue() { - player.prepare(); - player.addItemsToQueue(itemBuilder.setUuid(toUuid(1)).build()); - verify(playerEventListener) - .onTimelineChanged( - any(Timeline.class), isNull(), eq(Player.TIMELINE_CHANGE_REASON_DYNAMIC)); - player.seekTo(/* windowIndex= */ 0, /* positionMs= */ C.TIME_UNSET); - verify(playerEventListener) - .onPlayerStateChanged(/* playWhenReady =*/ false, Player.STATE_BUFFERING); - player.stop(/* reset= */ true); - verify(playerEventListener).onPlayerStateChanged(/* playWhenReady =*/ false, Player.STATE_IDLE); - - // Update for add. - ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Timeline.class); - verify(playerEventListener) - .onTimelineChanged( - argumentCaptor.capture(), - /* manifest= */ isNull(), - eq(Player.TIMELINE_CHANGE_REASON_DYNAMIC)); - assertThat(argumentCaptor.getValue().getWindowCount()).isEqualTo(1); - - // Update for stop. - verify(playerEventListener) - .onTimelineChanged( - argumentCaptor.capture(), - /* manifest= */ isNull(), - eq(Player.TIMELINE_CHANGE_REASON_RESET)); - assertThat(argumentCaptor.getValue().getWindowCount()).isEqualTo(0); - - assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); - } - - @Test - public void getCurrentTimeline_masksRemoteUpdates() { - player.prepare(); - MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); - MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); - assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); - player.addItemsToQueue(item1, item2); - - ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(Timeline.class); - verify(playerEventListener) - .onTimelineChanged( - messageCaptor.capture(), - /* manifest= */ isNull(), - eq(Player.TIMELINE_CHANGE_REASON_DYNAMIC)); - Timeline reportedTimeline = messageCaptor.getValue(); - assertThat(reportedTimeline).isSameInstanceAs(player.getCurrentTimeline()); - assertThat(reportedTimeline.getWindowCount()).isEqualTo(2); - assertThat(reportedTimeline.getWindow(/* windowIndex= */ 0, new Timeline.Window()).durationUs) - .isEqualTo(C.TIME_UNSET); - assertThat(reportedTimeline.getWindow(/* windowIndex= */ 1, new Timeline.Window()).durationUs) - .isEqualTo(C.TIME_UNSET); - } - - @Test - public void getCurrentTimeline_exposesReceiverState() { - MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); - MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1) - .setPlaybackState(Player.STATE_BUFFERING) - .setItems(Arrays.asList(item1, item2)) - .setShuffleOrder(Arrays.asList(1, 0)) - .build()); - ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(Timeline.class); - verify(playerEventListener) - .onTimelineChanged( - messageCaptor.capture(), - /* manifest= */ isNull(), - eq(Player.TIMELINE_CHANGE_REASON_DYNAMIC)); - Timeline reportedTimeline = messageCaptor.getValue(); - assertThat(reportedTimeline).isSameInstanceAs(player.getCurrentTimeline()); - assertThat(reportedTimeline.getWindowCount()).isEqualTo(2); - assertThat(reportedTimeline.getWindow(/* windowIndex= */ 0, new Timeline.Window()).durationUs) - .isEqualTo(C.TIME_UNSET); - assertThat(reportedTimeline.getWindow(/* windowIndex= */ 1, new Timeline.Window()).durationUs) - .isEqualTo(C.TIME_UNSET); - } - - @Test - public void timelineUpdateFromReceiver_matchesLocalState_doesNotCallEventLsitener() { - MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); - MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); - MediaItem item3 = itemBuilder.setUuid(toUuid(3)).build(); - MediaItem item4 = itemBuilder.setUuid(toUuid(4)).build(); - - MediaItemInfo.Period period1 = - new MediaItemInfo.Period("id1", /* durationUs= */ 1000000L, /* positionInWindowUs= */ 0); - MediaItemInfo.Period period2 = - new MediaItemInfo.Period( - "id2", /* durationUs= */ 1000000L, /* positionInWindowUs= */ 1000000L); - MediaItemInfo.Period period3 = - new MediaItemInfo.Period( - "id3", /* durationUs= */ 1000000L, /* positionInWindowUs= */ 2000000L); - HashMap mediaItemInfoMap1 = new HashMap<>(); - mediaItemInfoMap1.put( - toUuid(1), - new MediaItemInfo( - /* windowDurationUs= */ 3000L, - /* defaultStartPositionUs= */ 10, - /* periods= */ Arrays.asList(period1, period2, period3), - /* positionInFirstPeriodUs= */ 0, - /* isSeekable= */ true, - /* isDynamic= */ false)); - mediaItemInfoMap1.put( - toUuid(3), - new MediaItemInfo( - /* windowDurationUs= */ 2000L, - /* defaultStartPositionUs= */ 10, - /* periods= */ Arrays.asList(period1, period2), - /* positionInFirstPeriodUs= */ 500, - /* isSeekable= */ true, - /* isDynamic= */ false)); - - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(1) - .setPlaybackState(Player.STATE_BUFFERING) - .setItems(Arrays.asList(item1, item2, item3, item4)) - .setShuffleOrder(Arrays.asList(1, 0, 2, 3)) - .setMediaItemsInformation(mediaItemInfoMap1) - .build()); - verify(playerEventListener) - .onTimelineChanged( - any(), /* manifest= */ isNull(), eq(Player.TIMELINE_CHANGE_REASON_DYNAMIC)); - verify(playerEventListener) - .onPlayerStateChanged( - /* playWhenReady= */ false, /* playbackState= */ Player.STATE_BUFFERING); - - HashMap mediaItemInfoMap2 = new HashMap<>(mediaItemInfoMap1); - mediaItemInfoMap2.put( - toUuid(5), - new MediaItemInfo( - /* windowDurationUs= */ 5, - /* defaultStartPositionUs= */ 0, - /* periods= */ Arrays.asList(period1, period2), - /* positionInFirstPeriodUs= */ 500, - /* isSeekable= */ true, - /* isDynamic= */ false)); - - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(1).setMediaItemsInformation(mediaItemInfoMap2).build()); - verifyNoMoreInteractions(playerEventListener); - } - - @Test - public void getPeriodIndex_producesExpectedOutput() { - MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); - MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); - MediaItem item3 = itemBuilder.setUuid(toUuid(3)).build(); - MediaItem item4 = itemBuilder.setUuid(toUuid(4)).build(); - - MediaItemInfo.Period period1 = - new MediaItemInfo.Period("id1", /* durationUs= */ 1000000L, /* positionInWindowUs= */ 0); - MediaItemInfo.Period period2 = - new MediaItemInfo.Period( - "id2", /* durationUs= */ 1000000L, /* positionInWindowUs= */ 1000000L); - MediaItemInfo.Period period3 = - new MediaItemInfo.Period( - "id3", /* durationUs= */ 1000000L, /* positionInWindowUs= */ 2000000L); - HashMap mediaItemInfoMap = new HashMap<>(); - mediaItemInfoMap.put( - toUuid(1), - new MediaItemInfo( - /* windowDurationUs= */ 3000L, - /* defaultStartPositionUs= */ 10, - /* periods= */ Arrays.asList(period1, period2, period3), - /* positionInFirstPeriodUs= */ 0, - /* isSeekable= */ true, - /* isDynamic= */ false)); - mediaItemInfoMap.put( - toUuid(3), - new MediaItemInfo( - /* windowDurationUs= */ 2000L, - /* defaultStartPositionUs= */ 10, - /* periods= */ Arrays.asList(period1, period2), - /* positionInFirstPeriodUs= */ 500, - /* isSeekable= */ true, - /* isDynamic= */ false)); - - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1L) - .setPlaybackState(Player.STATE_BUFFERING) - .setItems(Arrays.asList(item1, item2, item3, item4)) - .setShuffleOrder(Arrays.asList(1, 0, 3, 2)) - .setMediaItemsInformation(mediaItemInfoMap) - .setPlaybackPosition( - /* currentPlayingItemUuid= */ item3.uuid, - /* currentPlayingPeriodId= */ "id2", - /* currentPlaybackPositionMs= */ 500L) - .build()); - - assertThat(player.getCurrentPeriodIndex()).isEqualTo(5); - player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 0L); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(3); - player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 1500L); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(1); - } - - @Test - public void exoCastPlayer_propagatesPlayerStateFromReceiver() { - ReceiverAppStateUpdate.Builder builder = - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1); - - // The first idle state update should be discarded, since it matches the current state. - receiverAppStateListener.onStateUpdateFromReceiverApp( - builder.setPlaybackState(Player.STATE_IDLE).build()); - receiverAppStateListener.onStateUpdateFromReceiverApp( - builder.setPlaybackState(Player.STATE_BUFFERING).build()); - receiverAppStateListener.onStateUpdateFromReceiverApp( - builder.setPlaybackState(Player.STATE_READY).build()); - receiverAppStateListener.onStateUpdateFromReceiverApp( - builder.setPlaybackState(Player.STATE_ENDED).build()); - ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(Integer.class); - verify(playerEventListener, times(3)) - .onPlayerStateChanged(/* playWhenReady= */ eq(false), messageCaptor.capture()); - List states = messageCaptor.getAllValues(); - assertThat(states).hasSize(3); - assertThat(states) - .isEqualTo(Arrays.asList(Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED)); - } - - @Test - public void setPlayWhenReady_changedLocally_notifiesListeners() { - player.setPlayWhenReady(false); - verify(playerEventListener, never()).onPlayerStateChanged(false, Player.STATE_IDLE); - player.setPlayWhenReady(true); - verify(playerEventListener).onPlayerStateChanged(true, Player.STATE_IDLE); - player.setPlayWhenReady(false); - verify(playerEventListener).onPlayerStateChanged(false, Player.STATE_IDLE); - } - - @Test - public void setPlayWhenReady_changedRemotely_notifiesListeners() { - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 0).setPlayWhenReady(true).build()); - verify(playerEventListener) - .onPlayerStateChanged(/* playWhenReady= */ true, /* playbackState= */ Player.STATE_IDLE); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 0).setPlayWhenReady(true).build()); - verifyNoMoreInteractions(playerEventListener); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 0).setPlayWhenReady(false).build()); - verify(playerEventListener) - .onPlayerStateChanged(/* playWhenReady= */ false, /* playbackState= */ Player.STATE_IDLE); - verifyNoMoreInteractions(playerEventListener); - } - - @Test - public void getPlayWhenReady_masksRemoteUpdates() { - when(sessionManager.send(any(ExoCastMessage.class))).thenReturn(3L); - player.setPlayWhenReady(true); - verify(playerEventListener) - .onPlayerStateChanged(/* playWhenReady= */ true, /* playbackState= */ Player.STATE_IDLE); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 2).setPlayWhenReady(false).build()); - verifyNoMoreInteractions(playerEventListener); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 3).setPlayWhenReady(true).build()); - verifyNoMoreInteractions(playerEventListener); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 3).setPlayWhenReady(false).build()); - verify(playerEventListener) - .onPlayerStateChanged(/* playWhenReady= */ false, /* playbackState= */ Player.STATE_IDLE); - } - - @Test - public void setRepeatMode_changedLocally_notifiesListeners() { - player.setRepeatMode(Player.REPEAT_MODE_OFF); - verifyNoMoreInteractions(playerEventListener); - player.setRepeatMode(Player.REPEAT_MODE_ONE); - verify(playerEventListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE); - player.setRepeatMode(Player.REPEAT_MODE_ONE); - verifyNoMoreInteractions(playerEventListener); - } - - @Test - public void setRepeatMode_changedRemotely_notifiesListeners() { - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 0) - .setRepeatMode(Player.REPEAT_MODE_ONE) - .build()); - verify(playerEventListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE); - assertThat(player.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE); - } - - @Test - public void getRepeatMode_masksRemoteUpdates() { - when(sessionManager.send(any(ExoCastMessage.class))).thenReturn(3L); - player.setRepeatMode(Player.REPEAT_MODE_ALL); - assertThat(player.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ALL); - verify(playerEventListener).onRepeatModeChanged(Player.REPEAT_MODE_ALL); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 2) - .setRepeatMode(Player.REPEAT_MODE_ONE) - .build()); - verifyNoMoreInteractions(playerEventListener); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 3) - .setRepeatMode(Player.REPEAT_MODE_ONE) - .build()); - verify(playerEventListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE); - } - - @Test - public void getPlaybackPosition_withStateChanges_producesExpectedOutput() { - UUID uuid = toUuid(1); - HashMap mediaItemInfoMap = new HashMap<>(); - - MediaItemInfo.Period period1 = new MediaItemInfo.Period("id1", 1000L, 0); - MediaItemInfo.Period period2 = new MediaItemInfo.Period("id2", 1000L, 0); - MediaItemInfo.Period period3 = new MediaItemInfo.Period("id3", 1000L, 0); - mediaItemInfoMap.put( - uuid, - new MediaItemInfo( - /* windowDurationUs= */ 1000L, - /* defaultStartPositionUs= */ 10, - /* periods= */ Arrays.asList(period1, period2, period3), - /* positionInFirstPeriodUs= */ 500, - /* isSeekable= */ true, - /* isDynamic= */ false)); - - when(sessionManager.send(any(ExoCastMessage.class))).thenReturn(1L); - player.addItemsToQueue(itemBuilder.setUuid(uuid).build()); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1) - .setPlaybackState(Player.STATE_BUFFERING) - .setMediaItemsInformation(mediaItemInfoMap) - .setPlaybackPosition(uuid, "id2", /* currentPlaybackPositionMs= */ 1000L) - .build()); - assertThat(player.getCurrentWindowIndex()).isEqualTo(0); - assertThat(player.getCurrentPosition()).isEqualTo(1000L); - clock.advanceTime(/* timeDiffMs= */ 1L); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1) - .setPlaybackState(Player.STATE_READY) - .build()); - // Play when ready is still false, so position should not change. - assertThat(player.getCurrentPosition()).isEqualTo(1000L); - player.setPlayWhenReady(true); - clock.advanceTime(1); - assertThat(player.getCurrentPosition()).isEqualTo(1001L); - clock.advanceTime(1); - assertThat(player.getCurrentPosition()).isEqualTo(1002L); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1) - .setPlaybackState(Player.STATE_BUFFERING) - .setMediaItemsInformation(mediaItemInfoMap) - .setPlaybackPosition(uuid, "id2", /* currentPlaybackPositionMs= */ 1010L) - .build()); - clock.advanceTime(1); - assertThat(player.getCurrentPosition()).isEqualTo(1010L); - clock.advanceTime(1); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1) - .setPlaybackState(Player.STATE_READY) - .setMediaItemsInformation(mediaItemInfoMap) - .setPlaybackPosition(uuid, "id2", /* currentPlaybackPositionMs= */ 1011L) - .build()); - clock.advanceTime(10); - assertThat(player.getCurrentPosition()).isEqualTo(1021L); - } - - @Test - public void getPlaybackPosition_withNonDefaultPlaybackSpeed_producesExpectedOutput() { - MediaItem item = itemBuilder.setUuid(toUuid(1)).build(); - MediaItemInfo info = - new MediaItemInfo( - /* windowDurationUs= */ 10000000, - /* defaultStartPositionUs= */ 3000000, - /* periods= */ Collections.singletonList( - new MediaItemInfo.Period( - /* id= */ "id", /* durationUs= */ 10000000, /* positionInWindowUs= */ 0)), - /* positionInFirstPeriodUs= */ 0, - /* isSeekable= */ true, - /* isDynamic= */ false); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1) - .setMediaItemsInformation(Collections.singletonMap(toUuid(1), info)) - .setShuffleOrder(Collections.singletonList(0)) - .setItems(Collections.singletonList(item)) - .setPlaybackPosition( - toUuid(1), /* currentPlayingPeriodId= */ "id", /* currentPlaybackPositionMs= */ 20L) - .setPlaybackState(Player.STATE_READY) - .setPlayWhenReady(true) - .build()); - assertThat(player.getCurrentPeriodIndex()).isEqualTo(0); - assertThat(player.getCurrentWindowIndex()).isEqualTo(0); - assertThat(player.getCurrentPosition()).isEqualTo(20); - clock.advanceTime(10); - assertThat(player.getCurrentPosition()).isEqualTo(30); - clock.advanceTime(10); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(1) - .setPlaybackPosition( - toUuid(1), /* currentPlayingPeriodId= */ "id", /* currentPlaybackPositionMs= */ 40L) - .setPlaybackParameters(new PlaybackParameters(2)) - .build()); - clock.advanceTime(10); - assertThat(player.getCurrentPosition()).isEqualTo(60); - } - - @Test - public void positionChanges_notifiesDiscontinuities() { - UUID uuid = toUuid(1); - HashMap mediaItemInfoMap = new HashMap<>(); - - MediaItemInfo.Period period1 = new MediaItemInfo.Period("id1", 1000L, 0); - MediaItemInfo.Period period2 = new MediaItemInfo.Period("id2", 1000L, 0); - MediaItemInfo.Period period3 = new MediaItemInfo.Period("id3", 1000L, 0); - mediaItemInfoMap.put( - uuid, - new MediaItemInfo( - /* windowDurationUs= */ 1000L, - /* defaultStartPositionUs= */ 10, - /* periods= */ Arrays.asList(period1, period2, period3), - /* positionInFirstPeriodUs= */ 500, - /* isSeekable= */ true, - /* isDynamic= */ false)); - - player.addItemsToQueue(itemBuilder.setUuid(uuid).build()); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 1) - .setPlaybackState(Player.STATE_BUFFERING) - .setMediaItemsInformation(mediaItemInfoMap) - .setPlaybackPosition(uuid, "id2", /* currentPlaybackPositionMs= */ 1000L) - .setDiscontinuityReason(Player.DISCONTINUITY_REASON_SEEK) - .build()); - verify(playerEventListener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); - player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 999); - verify(playerEventListener, times(2)).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); - } - - @Test - public void setShuffleModeEnabled_changedLocally_notifiesListeners() { - player.setShuffleModeEnabled(true); - verify(playerEventListener).onShuffleModeEnabledChanged(true); - player.setShuffleModeEnabled(true); - verifyNoMoreInteractions(playerEventListener); - } - - @Test - public void setShuffleModeEnabled_changedRemotely_notifiesListeners() { - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 0) - .setShuffleModeEnabled(true) - .build()); - verify(playerEventListener).onShuffleModeEnabledChanged(true); - assertThat(player.getShuffleModeEnabled()).isTrue(); - } - - @Test - public void getShuffleMode_masksRemoteUpdates() { - when(sessionManager.send(any(ExoCastMessage.class))).thenReturn(3L); - player.setShuffleModeEnabled(true); - assertThat(player.getShuffleModeEnabled()).isTrue(); - verify(playerEventListener).onShuffleModeEnabledChanged(true); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 2) - .setShuffleModeEnabled(false) - .build()); - verifyNoMoreInteractions(playerEventListener); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 3) - .setShuffleModeEnabled(false) - .build()); - verify(playerEventListener).onShuffleModeEnabledChanged(false); - assertThat(player.getShuffleModeEnabled()).isFalse(); - } - - @Test - public void seekTo_inIdle_doesNotChangePlaybackState() { - player.prepare(); - assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_ENDED); - player.addItemsToQueue(itemBuilder.build(), itemBuilder.build()); - assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_ENDED); - player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 0); - assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_BUFFERING); - player.stop(false); - assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_IDLE); - player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 0); - assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_IDLE); - } - - @Test - public void seekTo_withTwoItems_producesExpectedMessage() { - player.prepare(); - MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); - MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); - player.addItemsToQueue(item1, item2); - player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 1000); - ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(ExoCastMessage.class); - verify(sessionManager, times(3)).send(messageCaptor.capture()); - // Messages should be prepare, add and seek. - ExoCastMessage.SeekTo seekToMessage = - (ExoCastMessage.SeekTo) messageCaptor.getAllValues().get(2); - assertThat(seekToMessage.positionMs).isEqualTo(1000); - assertThat(seekToMessage.uuid).isEqualTo(toUuid(2)); - } - - @Test - public void seekTo_masksRemoteUpdates() { - player.prepare(); - when(sessionManager.send(any(ExoCastMessage.class))).thenReturn(3L); - MediaItem item1 = itemBuilder.setUuid(toUuid(1)).build(); - MediaItem item2 = itemBuilder.setUuid(toUuid(2)).build(); - player.addItemsToQueue(item1, item2); - player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 1000L); - verify(playerEventListener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); - verify(playerEventListener) - .onPlayerStateChanged( - /* playWhenReady= */ false, /* playbackState= */ Player.STATE_BUFFERING); - assertThat(player.getCurrentWindowIndex()).isEqualTo(1); - assertThat(player.getCurrentPosition()).isEqualTo(1000); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 2) - .setPlaybackPosition(toUuid(1), "id", 500L) - .build()); - assertThat(player.getCurrentWindowIndex()).isEqualTo(1); - assertThat(player.getCurrentPosition()).isEqualTo(1000); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 3) - .setPlaybackPosition(toUuid(1), "id", 500L) - .setDiscontinuityReason(Player.DISCONTINUITY_REASON_SEEK) - .build()); - verify(playerEventListener, times(2)).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); - assertThat(player.getCurrentWindowIndex()).isEqualTo(0); - assertThat(player.getCurrentPosition()).isEqualTo(500); - } - - @Test - public void setPlaybackParameters_producesExpectedMessage() { - PlaybackParameters playbackParameters = - new PlaybackParameters(/* speed= */ .5f, /* pitch= */ .25f, /* skipSilence= */ true); - player.setPlaybackParameters(playbackParameters); - ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(ExoCastMessage.class); - verify(sessionManager).send(messageCaptor.capture()); - ExoCastMessage.SetPlaybackParameters message = - (ExoCastMessage.SetPlaybackParameters) messageCaptor.getValue(); - assertThat(message.playbackParameters).isEqualTo(playbackParameters); - } - - @Test - public void getTrackSelectionParameters_doesNotOverrideUnexpectedFields() { - when(sessionManager.send(any(ExoCastMessage.class))).thenReturn(3L); - DefaultTrackSelector.Parameters parameters = - DefaultTrackSelector.Parameters.DEFAULT - .buildUpon() - .setPreferredAudioLanguage("spa") - .setMaxVideoSize(/* maxVideoWidth= */ 3, /* maxVideoHeight= */ 3) - .build(); - player.setTrackSelectionParameters(parameters); - TrackSelectionParameters returned = - TrackSelectionParameters.DEFAULT.buildUpon().setPreferredAudioLanguage("deu").build(); - receiverAppStateListener.onStateUpdateFromReceiverApp( - ReceiverAppStateUpdate.builder(/* sequenceNumber= */ 3) - .setTrackSelectionParameters(returned) - .build()); - DefaultTrackSelector.Parameters result = - (DefaultTrackSelector.Parameters) player.getTrackSelectionParameters(); - assertThat(result.preferredAudioLanguage).isEqualTo("deu"); - assertThat(result.maxVideoHeight).isEqualTo(3); - assertThat(result.maxVideoWidth).isEqualTo(3); - } - - @Test - public void testExoCast_getRendererType() { - assertThat(player.getRendererCount()).isEqualTo(4); - assertThat(player.getRendererType(/* index= */ 0)).isEqualTo(C.TRACK_TYPE_VIDEO); - assertThat(player.getRendererType(/* index= */ 1)).isEqualTo(C.TRACK_TYPE_AUDIO); - assertThat(player.getRendererType(/* index= */ 2)).isEqualTo(C.TRACK_TYPE_TEXT); - assertThat(player.getRendererType(/* index= */ 3)).isEqualTo(C.TRACK_TYPE_METADATA); - } - - private static UUID toUuid(long lowerBits) { - return new UUID(0, lowerBits); - } - - private void assertMediaItemQueue(MediaItem... mediaItemQueue) { - assertThat(player.getQueueSize()).isEqualTo(mediaItemQueue.length); - for (int i = 0; i < mediaItemQueue.length; i++) { - assertThat(player.getQueueItem(i).uuid).isEqualTo(mediaItemQueue[i].uuid); - } - } - - private static void assertMessageAddsItems( - ExoCastMessage message, int index, List mediaItems) throws JSONException { - assertThat(message.method).isEqualTo(ExoCastConstants.METHOD_ADD_ITEMS); - JSONObject args = - new JSONObject(message.toJsonString(MOCK_SEQUENCE_NUMBER)).getJSONObject(KEY_ARGS); - if (index != C.INDEX_UNSET) { - assertThat(args.getInt(KEY_INDEX)).isEqualTo(index); - } else { - assertThat(args.has(KEY_INDEX)).isFalse(); - } - JSONArray itemsAsJson = args.getJSONArray(KEY_ITEMS); - assertThat(ReceiverAppStateUpdate.toMediaItemArrayList(itemsAsJson)).isEqualTo(mediaItems); - } - - private static void assertMessageMovesItem(ExoCastMessage message, MediaItem item, int index) - throws JSONException { - assertThat(message.method).isEqualTo(ExoCastConstants.METHOD_MOVE_ITEM); - JSONObject args = - new JSONObject(message.toJsonString(MOCK_SEQUENCE_NUMBER)).getJSONObject(KEY_ARGS); - assertThat(args.getString(KEY_UUID)).isEqualTo(item.uuid.toString()); - assertThat(args.getInt(KEY_INDEX)).isEqualTo(index); - } - - private static void assertMessageRemovesItems(ExoCastMessage message, List items) - throws JSONException { - assertThat(message.method).isEqualTo(ExoCastConstants.METHOD_REMOVE_ITEMS); - JSONObject args = - new JSONObject(message.toJsonString(MOCK_SEQUENCE_NUMBER)).getJSONObject(KEY_ARGS); - JSONArray uuidsAsJson = args.getJSONArray(KEY_UUIDS); - for (int i = 0; i < uuidsAsJson.length(); i++) { - assertThat(uuidsAsJson.getString(i)).isEqualTo(items.get(i).uuid.toString()); - } - } -} diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastTimelineTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastTimelineTest.java deleted file mode 100644 index f6084339e4..0000000000 --- a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ExoCastTimelineTest.java +++ /dev/null @@ -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 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 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 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 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 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 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 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); - } -} diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdateTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdateTest.java deleted file mode 100644 index fbe936a016..0000000000 --- a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/ReceiverAppStateUpdateTest.java +++ /dev/null @@ -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 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); - } -}