Commit 0ddd3c2b by aquilescanta Committed by Oliver Woodman

Implement DecryptableSampleQueueReader.isReady

PiperOrigin-RevId: 254746146
parent 99054325
Showing with 4895 additions and 0 deletions
# 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"],
)
# 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 ...
```
# 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,
)
/*
* 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);
}
}
<!DOCTYPE html>
<!--
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
-->
<html>
<head>
<link rel="stylesheet" href="app_desktop_styles.css"/>
</head>
<body>
<ul id="log"></ul>
<section id="exo_demo_view">
<video id="video"></video>
<div id="exo_playback_info">
<div id="exo_time_bar">
<div id="exo_duration">
<div id="exo_elapsed_time"></div>
</div>
<span id="exo_elapsed_time_label" class="exo_text_label"></span>
<span id="exo_duration_label" class="exo_text_label"></span>
</div>
</div>
<div class="ribbon">
for debugging<br/>purpose only
</div>
</section>
<section id="exo_controls">
<ul class="exo_controls">
<li id="button_prepare" data-method="prepare" class="button">prepare</li>
<li id="button_previous" class="button" data-method="previous">prev</li>
<li data-method="rewind" class="button">rewind</li>
<li id="button_play" class="large button"
data-method="pwr_1">play</li>
<li id="button_pause" class="large button" data-method="pwr_0">pause</li>
<li data-method="fastforward" class="button">ffwd</li>
<li id="button_next" data-method="next" class="button">next</li>
<li id="button_stop" data-method="stop" class="button">stop</li>
</ul>
</section>
<section id="media-actions">
</section>
<script src="app_desktop.js"></script>
</body>
</html>
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.module('exoplayer.cast.debug');
const ConfigurationFactory = goog.require('exoplayer.cast.ConfigurationFactory');
const PlaybackInfoView = goog.require('exoplayer.cast.PlaybackInfoView');
const Player = goog.require('exoplayer.cast.Player');
const PlayerControls = goog.require('exoplayer.cast.PlayerControls');
const ShakaPlayer = goog.require('shaka.Player');
const SimpleTextDisplayer = goog.require('shaka.text.SimpleTextDisplayer');
const installAll = goog.require('shaka.polyfill.installAll');
const util = goog.require('exoplayer.cast.util');
/** @type {!Array<!MediaItem>} */
let queue = [];
/** @type {number} */
let uuidCounter = 1;
// install all polyfills for the Shaka player
installAll();
/**
* Listens for player state changes and logs the state to the console.
*
* @param {!PlayerState} playerState The player state.
*/
const playerListener = function(playerState) {
util.log(['playerState: ', playerState.playbackPosition, playerState]);
queue = playerState.mediaQueue;
highlightCurrentItem(
playerState.playbackPosition && playerState.playbackPosition.uuid ?
playerState.playbackPosition.uuid :
'');
if (playerState.playWhenReady && playerState.playbackState === 'READY') {
document.body.classList.add('playing');
} else {
document.body.classList.remove('playing');
}
if (playerState.playbackState === 'IDLE' && queue.length === 0) {
// Stop has been called or player not yet prepared.
resetSampleList();
}
};
/**
* Highlights the currently playing item in the samples list.
*
* @param {string} uuid
*/
const highlightCurrentItem = function(uuid) {
const actions = /** @type {!NodeList<!HTMLElement>} */ (
document.querySelectorAll('#media-actions .action'));
for (let action of actions) {
if (action.dataset['uuid'] === uuid) {
action.classList.add('prepared');
} else {
action.classList.remove('prepared');
}
}
};
/**
* Makes sure all items reflect being removed from the timeline.
*/
const resetSampleList = function() {
const actions = /** @type {!NodeList<!HTMLElement>} */ (
document.querySelectorAll('#media-actions .action'));
for (let action of actions) {
action.classList.remove('prepared');
delete action.dataset['uuid'];
}
};
/**
* If the arguments provide a valid media item it is added to the player.
*
* @param {!MediaItem} item The media item.
* @return {string} The uuid which has been created for the item before adding.
*/
const addQueueItem = function(item) {
if (!(item.media && item.media.uri && item.mimeType)) {
throw Error('insufficient arguments to add a queue item');
}
item.uuid = 'uuid-' + uuidCounter++;
player.addQueueItems(queue.length, [item], /* playbackOrder= */ undefined);
return item.uuid;
};
/**
* An event listener which listens for actions.
*
* @param {!Event} ev The DOM event.
*/
const handleAction = (ev) => {
let target = ev.target;
while (target !== document.body && !target.dataset['action']) {
target = target.parentNode;
}
if (!target || !target.dataset['action']) {
return;
}
switch (target.dataset['action']) {
case 'player.addItems':
if (target.dataset['uuid']) {
player.removeQueueItems([target.dataset['uuid']]);
delete target.dataset['uuid'];
} else {
const uuid = addQueueItem(/** @type {!MediaItem} */
(JSON.parse(target.dataset['item'])));
target.dataset['uuid'] = uuid;
}
break;
}
};
/**
* Appends samples to the list of media item actions.
*
* @param {!Array<!MediaItem>} mediaItems The samples to add.
*/
const appendSamples = function(mediaItems) {
const samplesList = document.getElementById('media-actions');
mediaItems.forEach((item) => {
const div = /** @type {!HTMLElement} */ (document.createElement('div'));
div.classList.add('action', 'button');
div.dataset['action'] = 'player.addItems';
div.dataset['item'] = JSON.stringify(item);
div.appendChild(document.createTextNode(item.title));
const marker = document.createElement('span');
marker.classList.add('queue-marker');
div.appendChild(marker);
samplesList.appendChild(div);
});
};
/** @type {!HTMLMediaElement} */
const mediaElement =
/** @type {!HTMLMediaElement} */ (document.getElementById('video'));
// Workaround for https://github.com/google/shaka-player/issues/1819
// TODO(bachinger) Remove line when better fix available.
new SimpleTextDisplayer(mediaElement);
/** @type {!ShakaPlayer} */
const shakaPlayer = new ShakaPlayer(mediaElement);
/** @type {!Player} */
const player = new Player(shakaPlayer, new ConfigurationFactory());
new PlayerControls(player, 'exo_controls');
new PlaybackInfoView(player, 'exo_playback_info');
// register listeners
document.body.addEventListener('click', handleAction);
player.addPlayerListener(playerListener);
// expose the player for debugging purposes.
window['player'] = player;
exports.appendSamples = appendSamples;
/*
* 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;
/*
* 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',
},
],
},
]);
/*
* 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',
},
],
},
]);
/*
* 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%;
}
<!DOCTYPE html>
<!--
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
-->
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="app_styles.css"/>
<script type="text/javascript"
src="//www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js">
</script>
</head>
<body>
<section id="exo_player_view">
<video id="exo_video"></video>
<div id="exo_playback_info">
<div id="exo_time_bar">
<div id="exo_duration">
<div id="exo_elapsed_time"></div>
</div>
<span id="exo_elapsed_time_label" class="exo_text_label"></span>
<span id="exo_duration_label" class="exo_text_label"></span>
</div>
</div>
</section>
<script src="app.js"></script>
</body>
</html>
/*
* 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;
}
/*
* 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;
}
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.module('exoplayer.cast.MessageDispatcher');
const validation = goog.require('exoplayer.cast.validation');
/**
* A callback function which is called by an action handler to indicate when
* processing has completed.
*
* @typedef {function(?PlayerState): undefined}
*/
const Callback = undefined;
/**
* Handles an action sent by a sender app.
*
* @typedef {function(!Object<string,?>, number, string, !Callback): undefined}
*/
const ActionHandler = undefined;
/**
* Dispatches messages of a cast message bus to registered action handlers.
*
* <p>The dispatcher listens to events of a CastMessageBus for the namespace
* passed to the constructor. The <code>data</code> property of the event is
* parsed as a json document and delegated to a handler registered for the given
* method.
*/
class MessageDispatcher {
/**
* @param {string} namespace The message namespace.
* @param {!cast.framework.CastReceiverContext} castReceiverContext The cast
* receiver manager.
*/
constructor(namespace, castReceiverContext) {
/** @private @const {string} */
this.namespace_ = namespace;
/** @private @const {!cast.framework.CastReceiverContext} */
this.castReceiverContext_ = castReceiverContext;
/** @private @const {!Array<!Message>} */
this.messageQueue_ = [];
/** @private @const {!Object} */
this.actions_ = {};
/** @private @const {!Object<string, number>} */
this.senderSequences_ = {};
/** @private @const {function(string, *)} */
this.jsonStringifyReplacer_ = (key, value) => {
if (value === Infinity || value === null) {
return undefined;
}
return value;
};
this.castReceiverContext_.addCustomMessageListener(
this.namespace_, this.onMessage.bind(this));
}
/**
* Registers a handler of a given action.
*
* @param {string} method The method name for which to register the handler.
* @param {!Array<!Array<string>>} argDefs The name and type of each argument
* or an empty array if the method has no arguments.
* @param {!ActionHandler} handler A function to process the action.
*/
registerActionHandler(method, argDefs, handler) {
this.actions_[method] = {
method,
argDefs,
handler,
};
}
/**
* Unregisters the handler of the given action.
*
* @param {string} action The action to unregister.
*/
unregisterActionHandler(action) {
delete this.actions_[action];
}
/**
* Callback to receive messages sent by sender apps.
*
* @param {!cast.framework.system.Event} event The event received from the
* sender app.
*/
onMessage(event) {
console.log('message arrived from sender', this.namespace_, event);
const message = /** @type {!ExoCastMessage} */ (event.data);
const action = this.actions_[message.method];
if (action) {
const args = message.args;
for (let i = 0; i < action.argDefs.length; i++) {
if (!validation.validateProperty(
args, action.argDefs[i][0], action.argDefs[i][1])) {
console.warn('invalid method call', message);
return;
}
}
this.messageQueue_.push({
senderId: event.senderId,
message: message,
handler: action.handler
});
if (this.messageQueue_.length === 1) {
this.executeNext();
} else {
// Do nothing. An action is executing asynchronously and will call
// executeNext when finished.
}
} else {
console.warn('handler of method not found', message);
}
}
/**
* Executes the next message in the queue.
*/
executeNext() {
if (this.messageQueue_.length === 0) {
return;
}
const head = this.messageQueue_[0];
const message = head.message;
const senderSequence = message.sequenceNumber;
this.senderSequences_[head.senderId] = senderSequence;
try {
head.handler(message.args, senderSequence, head.senderId, (response) => {
if (response) {
this.send(head.senderId, response);
}
this.shiftPendingMessage_(head);
});
} catch (e) {
this.shiftPendingMessage_(head);
console.error('error while executing method : ' + message.method, e);
}
}
/**
* Broadcasts the sender state to all sender apps registered for the
* given message namespace.
*
* @param {!PlayerState} playerState The player state to be sent.
*/
broadcast(playerState) {
this.castReceiverContext_.getSenders().forEach((sender) => {
this.send(sender.id, playerState);
});
delete playerState.sequenceNumber;
}
/**
* Sends the PlayerState to the given sender.
*
* @param {string} senderId The id of the sender.
* @param {!PlayerState} playerState The message to send.
*/
send(senderId, playerState) {
playerState.sequenceNumber = this.senderSequences_[senderId] || -1;
this.castReceiverContext_.sendCustomMessage(
this.namespace_, senderId,
// TODO(bachinger) Find a better solution.
JSON.parse(JSON.stringify(playerState, this.jsonStringifyReplacer_)));
}
/**
* Notifies the message dispatcher that a given sender has disconnected from
* the receiver.
*
* @param {string} senderId The id of the sender.
*/
notifySenderDisconnected(senderId) {
delete this.senderSequences_[senderId];
}
/**
* Shifts the pending message and executes the next if any.
*
* @private
* @param {!Message} pendingMessage The pending message.
*/
shiftPendingMessage_(pendingMessage) {
if (pendingMessage === this.messageQueue_[0]) {
this.messageQueue_.shift();
this.executeNext();
}
}
}
/**
* An item in the message queue.
*
* @record
*/
function Message() {}
/**
* The sender id.
*
* @type {string}
*/
Message.prototype.senderId;
/**
* The ExoCastMessage sent by the sender app.
*
* @type {!ExoCastMessage}
*/
Message.prototype.message;
/**
* The handler function handling the message.
*
* @type {!ActionHandler}
*/
Message.prototype.handler;
exports = MessageDispatcher;
/**
* 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;
/**
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @fileoverview A validator for messages received from sender apps.
*/
goog.module('exoplayer.cast.validation');
const {getPlaybackType, PlaybackType, RepeatMode} = goog.require('exoplayer.cast.constants');
/**
* Media item fields.
*
* @enum {string}
*/
const MediaItemField = {
UUID: 'uuid',
MEDIA: 'media',
MIME_TYPE: 'mimeType',
DRM_SCHEMES: 'drmSchemes',
TITLE: 'title',
DESCRIPTION: 'description',
START_POSITION_US: 'startPositionUs',
END_POSITION_US: 'endPositionUs',
};
/**
* DrmScheme fields.
*
* @enum {string}
*/
const DrmSchemeField = {
UUID: 'uuid',
LICENSE_SERVER_URI: 'licenseServer',
};
/**
* UriBundle fields.
*
* @enum {string}
*/
const UriBundleField = {
URI: 'uri',
REQUEST_HEADERS: 'requestHeaders',
};
/**
* Validates an array of media items.
*
* @param {!Array<!MediaItem>} mediaItems An array of media items.
* @return {boolean} true if all media items are valid, otherwise false is
* returned.
*/
const validateMediaItems = function (mediaItems) {
for (let i = 0; i < mediaItems.length; i++) {
if (!validateMediaItem(mediaItems[i])) {
return false;
}
}
return true;
};
/**
* Validates a queue item sent to the receiver by a sender app.
*
* @param {!MediaItem} mediaItem The media item.
* @return {boolean} true if the media item is valid, false otherwise.
*/
const validateMediaItem = function (mediaItem) {
// validate minimal properties
if (!validateProperty(mediaItem, MediaItemField.UUID, 'string')) {
console.log('missing mandatory uuid', mediaItem.uuid);
return false;
}
if (!validateProperty(mediaItem.media, UriBundleField.URI, 'string')) {
console.log('missing mandatory', mediaItem.media ? 'uri' : 'media');
return false;
}
const mimeType = mediaItem.mimeType;
if (!mimeType || getPlaybackType(mimeType) === PlaybackType.UNKNOWN) {
console.log('unsupported mime type:', mimeType);
return false;
}
// validate optional properties
if (goog.isArray(mediaItem.drmSchemes)) {
for (let i = 0; i < mediaItem.drmSchemes.length; i++) {
let drmScheme = mediaItem.drmSchemes[i];
if (!validateProperty(drmScheme, DrmSchemeField.UUID, 'string') ||
!validateProperty(
drmScheme.licenseServer, UriBundleField.URI, 'string')) {
console.log('invalid drm scheme', drmScheme);
return false;
}
}
}
if (!validateProperty(mediaItem, MediaItemField.START_POSITION_US, '?number')
|| !validateProperty(mediaItem, MediaItemField.END_POSITION_US, '?number')
|| !validateProperty(mediaItem, MediaItemField.TITLE, '?string')
|| !validateProperty(mediaItem, MediaItemField.DESCRIPTION, '?string')) {
console.log('invalid type of one of startPositionUs, endPositionUs, title'
+ ' or description', mediaItem);
return false;
}
return true;
};
/**
* Validates the existence and type of a property.
*
* <p>Supported types: number, string, boolean, Array.
* <p>Prefix the type with a ? to indicate that the property is optional.
*
* @param {?Object|?MediaItem|?UriBundle} obj The object to validate.
* @param {string} propertyName The name of the property.
* @param {string} type The type of the property.
* @return {boolean} True if valid, false otherwise.
*/
const validateProperty = function (obj, propertyName, type) {
if (typeof obj === 'undefined' || obj === null) {
return false;
}
const isOptional = type.startsWith('?');
const value = obj[propertyName];
if (isOptional && typeof value === 'undefined') {
return true;
}
type = isOptional ? type.substring(1) : type;
switch (type) {
case 'string':
return typeof value === 'string' || value instanceof String;
case 'number':
return typeof value === 'number' && isFinite(value);
case 'Array':
return typeof value !== 'undefined' && typeof value === 'object'
&& value.constructor === Array;
case 'boolean':
return typeof value === 'boolean';
case 'RepeatMode':
return value === RepeatMode.OFF || value === RepeatMode.ONE ||
value === RepeatMode.ALL;
default:
console.warn('Unsupported type when validating an object property. ' +
'Supported types are string, number, boolean and Array.', type);
return false;
}
};
exports.validateMediaItem = validateMediaItem;
exports.validateMediaItems = validateMediaItems;
exports.validateProperty = validateProperty;
#!/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."
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Externs for messages sent by a sender app in JSON format.
*
* Fields defined here are prevented from being renamed by the js compiler.
*
* @externs
*/
/**
* An uri bundle with an uri and request parameters.
*
* @record
*/
class UriBundle {
constructor() {
/**
* The URI.
*
* @type {string}
*/
this.uri;
/**
* The request headers.
*
* @type {?Object<string,string>}
*/
this.requestHeaders;
}
}
/**
* @record
*/
class DrmScheme {
constructor() {
/**
* The DRM UUID.
*
* @type {string}
*/
this.uuid;
/**
* The license URI.
*
* @type {?UriBundle}
*/
this.licenseServer;
}
}
/**
* @record
*/
class MediaItem {
constructor() {
/**
* The uuid of the item.
*
* @type {string}
*/
this.uuid;
/**
* The mime type.
*
* @type {string}
*/
this.mimeType;
/**
* The media uri bundle.
*
* @type {!UriBundle}
*/
this.media;
/**
* The DRM schemes.
*
* @type {!Array<!DrmScheme>}
*/
this.drmSchemes;
/**
* The position to start playback from.
*
* @type {number}
*/
this.startPositionUs;
/**
* The position at which to end playback.
*
* @type {number}
*/
this.endPositionUs;
/**
* The title of the media item.
*
* @type {string}
*/
this.title;
/**
* The description of the media item.
*
* @type {string}
*/
this.description;
}
}
/**
* Constraint parameters for track selection.
*
* @record
*/
class TrackSelectionParameters {
constructor() {
/**
* The preferred audio language.
*
* @type {string|undefined}
*/
this.preferredAudioLanguage;
/**
* The preferred text language.
*
* @type {string|undefined}
*/
this.preferredTextLanguage;
/**
* List of selection flags that are disabled for text track selections.
*
* @type {!Array<string>}
*/
this.disabledTextTrackSelectionFlags;
/**
* Whether a text track with undetermined language should be selected if no
* track with `preferredTextLanguage` is available, or if
* `preferredTextLanguage` is unset.
*
* @type {boolean}
*/
this.selectUndeterminedTextLanguage;
}
}
/**
* The PlaybackPosition defined by the position, the uuid of the media item and
* the period id.
*
* @record
*/
class PlaybackPosition {
constructor() {
/**
* The current playback position in milliseconds.
*
* @type {number}
*/
this.positionMs;
/**
* The uuid of the media item.
*
* @type {string}
*/
this.uuid;
/**
* The id of the currently playing period.
*
* @type {string}
*/
this.periodId;
/**
* The reason of a position discontinuity if any.
*
* @type {?string}
*/
this.discontinuityReason;
}
}
/**
* The playback parameters.
*
* @record
*/
class PlaybackParameters {
constructor() {
/**
* The playback speed.
*
* @type {number}
*/
this.speed;
/**
* The playback pitch.
*
* @type {number}
*/
this.pitch;
/**
* Whether silence is skipped.
*
* @type {boolean}
*/
this.skipSilence;
}
}
/**
* The player state.
*
* @record
*/
class PlayerState {
constructor() {
/**
* The playback state.
*
* @type {string}
*/
this.playbackState;
/**
* The playback parameters.
*
* @type {!PlaybackParameters}
*/
this.playbackParameters;
/**
* Playback starts when ready if true.
*
* @type {boolean}
*/
this.playWhenReady;
/**
* The current position within the media.
*
* @type {?PlaybackPosition}
*/
this.playbackPosition;
/**
* The current window index.
*
* @type {number}
*/
this.windowIndex;
/**
* The number of windows.
*
* @type {number}
*/
this.windowCount;
/**
* The audio tracks.
*
* @type {!Array<string>}
*/
this.audioTracks;
/**
* The video tracks in case of adaptive media.
*
* @type {!Array<!Object<string,*>>}
*/
this.videoTracks;
/**
* The repeat mode.
*
* @type {string}
*/
this.repeatMode;
/**
* Whether the shuffle mode is enabled.
*
* @type {boolean}
*/
this.shuffleModeEnabled;
/**
* The playback order to use when shuffle mode is enabled.
*
* @type {!Array<number>}
*/
this.shuffleOrder;
/**
* The queue of media items.
*
* @type {!Array<!MediaItem>}
*/
this.mediaQueue;
/**
* The media item info of the queue items if available.
*
* @type {!Object<string, !MediaItemInfo>}
*/
this.mediaItemsInfo;
/**
* The sequence number of the sender.
*
* @type {number}
*/
this.sequenceNumber;
/**
* The player error.
*
* @type {?PlayerError}
*/
this.error;
}
}
/**
* The error description.
*
* @record
*/
class PlayerError {
constructor() {
/**
* The error message.
*
* @type {string}
*/
this.message;
/**
* The error code.
*
* @type {number}
*/
this.code;
/**
* The error category.
*
* @type {number}
*/
this.category;
}
}
/**
* A period.
*
* @record
*/
class Period {
constructor() {
/**
* The id of the period. Must be unique within a media item.
*
* @type {string}
*/
this.id;
/**
* The duration of the period in microseconds.
*
* @type {number}
*/
this.durationUs;
}
}
/**
* Holds dynamic information for a MediaItem.
*
* <p>Holds information related to preparation for a specific {@link MediaItem}.
* Unprepared items are associated with an {@link #EMPTY} info object until
* prepared.
*
* @record
*/
class MediaItemInfo {
constructor() {
/**
* The duration of the window in microseconds.
*
* @type {number}
*/
this.windowDurationUs;
/**
* The default start position relative to the start of the window in
* microseconds.
*
* @type {number}
*/
this.defaultStartPositionUs;
/**
* The periods conforming the media item.
*
* @type {!Array<!Period>}
*/
this.periods;
/**
* The position of the window in the first period in microseconds.
*
* @type {number}
*/
this.positionInFirstPeriodUs;
/**
* Whether it is possible to seek within the window.
*
* @type {boolean}
*/
this.isSeekable;
/**
* Whether the window may change when the timeline is updated.
*
* @type {boolean}
*/
this.isDynamic;
}
}
/**
* The message envelope send by a sender app.
*
* @record
*/
class ExoCastMessage {
constructor() {
/**
* The clients message sequenec number.
*
* @type {number}
*/
this.sequenceNumber;
/**
* The name of the method.
*
* @type {string}
*/
this.method;
/**
* The arguments of the method.
*
* @type {!Object<string,*>}
*/
this.args;
}
};
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Externs of the Shaka configuration.
*
* @externs
*/
/**
* The drm configuration for the Shaka player.
*
* @record
*/
class DrmConfiguration {
constructor() {
/**
* A map of license servers with the UUID of the drm system as the key and the
* license uri as the value.
*
* @type {!Object<string, string>}
*/
this.servers;
}
}
/**
* The configuration of the Shaka player.
*
* @record
*/
class PlayerConfiguration {
constructor() {
/**
* The preferred audio language.
*
* @type {string}
*/
this.preferredAudioLanguage;
/**
* The preferred text language.
*
* @type {string}
*/
this.preferredTextLanguage;
/**
* The drm configuration.
*
* @type {?DrmConfiguration}
*/
this.drm;
}
}
/*
* 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;
/**
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.module('exoplayer.cast.constants');
/**
* The underyling player.
*
* @enum {number}
*/
const PlaybackType = {
VIDEO_ELEMENT: 1,
SHAKA_PLAYER: 2,
UNKNOWN: 999,
};
/**
* Supported mime types and their playback mode.
*
* @type {!Object<string, !PlaybackType>}
*/
const SUPPORTED_MIME_TYPES = Object.freeze({
'application/dash+xml': PlaybackType.SHAKA_PLAYER,
'application/vnd.apple.mpegurl': PlaybackType.SHAKA_PLAYER,
'application/vnd.ms-sstr+xml': PlaybackType.SHAKA_PLAYER,
'application/x-mpegURL': PlaybackType.SHAKA_PLAYER,
});
/**
* Returns the playback type required for a given mime type, or
* PlaybackType.UNKNOWN if the mime type is not recognized.
*
* @param {string} mimeType The mime type.
* @return {!PlaybackType} The required playback type, or PlaybackType.UNKNOWN
* if the mime type is not recognized.
*/
const getPlaybackType = function(mimeType) {
if (mimeType.startsWith('video/') || mimeType.startsWith('audio/')) {
return PlaybackType.VIDEO_ELEMENT;
} else {
return SUPPORTED_MIME_TYPES[mimeType] || PlaybackType.UNKNOWN;
}
};
/**
* Error messages.
*
* @enum {string}
*/
const ErrorMessages = {
SHAKA_LOAD_ERROR: 'Error while loading media with Shaka.',
SHAKA_UNKNOWN_ERROR: 'Shaka error event captured.',
MEDIA_ELEMENT_UNKNOWN_ERROR: 'Media element error event captured.',
UNKNOWN_FATAL_ERROR: 'Fatal playback error. Shaka instance replaced.',
UNKNOWN_ERROR: 'Unknown error',
};
/**
* ExoPlayer's repeat modes.
*
* @enum {string}
*/
const RepeatMode = {
OFF: 'OFF',
ONE: 'ONE',
ALL: 'ALL',
};
/**
* Error categories. Error categories coming from Shaka are defined in [Shaka
* source
* code](https://shaka-player-demo.appspot.com/docs/api/shaka.util.Error.html).
*
* @enum {number}
*/
const ErrorCategory = {
MEDIA_ELEMENT: 0,
FATAL_SHAKA_ERROR: 1000,
};
/**
* An error object to be used if no media error is assigned to the `error`
* field of the media element when an error event is fired
*
* @type {!PlayerError}
*/
const UNKNOWN_ERROR = /** @type {!PlayerError} */ (Object.freeze({
message: ErrorMessages.UNKNOWN_ERROR,
code: 0,
category: 0,
}));
/**
* UUID for the Widevine DRM scheme.
*
* @type {string}
*/
const WIDEVINE_UUID = 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed';
/**
* UUID for the PlayReady DRM scheme.
*
* @type {string}
*/
const PLAYREADY_UUID = '9a04f079-9840-4286-ab92-e65be0885f95';
/** @type {!Object<string, string>} */
const drmSystems = {};
drmSystems[WIDEVINE_UUID] = 'com.widevine.alpha';
drmSystems[PLAYREADY_UUID] = 'com.microsoft.playready';
/**
* The uuids of the supported DRM systems.
*
* @type {!Object<string, string>}
*/
const DRM_SYSTEMS = Object.freeze(drmSystems);
exports.PlaybackType = PlaybackType;
exports.ErrorMessages = ErrorMessages;
exports.ErrorCategory = ErrorCategory;
exports.RepeatMode = RepeatMode;
exports.getPlaybackType = getPlaybackType;
exports.WIDEVINE_UUID = WIDEVINE_UUID;
exports.PLAYREADY_UUID = PLAYREADY_UUID;
exports.DRM_SYSTEMS = DRM_SYSTEMS;
exports.UNKNOWN_ERROR = UNKNOWN_ERROR;
/*
* 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;
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.module('exoplayer.cast.Timeout');
/**
* A timeout which can be cancelled.
*/
class Timeout {
constructor() {
/** @private {?number} */
this.timeout_ = null;
}
/**
* Returns a promise which resolves when the duration of time defined by
* delayMs has elapsed and cancel() has not been called earlier.
*
* If the timeout is already set, the former timeout is cancelled and a new
* one is started.
*
* @param {number} delayMs The delay after which to resolve or a non-positive
* value if it should never resolve.
* @return {!Promise<undefined>} Resolves after the given delayMs or never
* for a non-positive delay.
*/
postDelayed(delayMs) {
this.cancel();
return new Promise((resolve, reject) => {
if (delayMs <= 0) {
return;
}
this.timeout_ = setTimeout(() => {
if (this.timeout_) {
this.timeout_ = null;
resolve();
}
}, delayMs);
});
}
/** Cancels the timeout. */
cancel() {
if (this.timeout_) {
clearTimeout(this.timeout_);
this.timeout_ = null;
}
}
/** @return {boolean} true if the timeout is currently ongoing. */
isOngoing() {
return this.timeout_ !== null;
}
}
exports = Timeout;
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.module('exoplayer.cast.util');
/**
* Indicates whether the logging is turned on.
*/
const enableLogging = true;
/**
* Logs to the console if logging enabled.
*
* @param {!Array<*>} statements The log statements to be logged.
*/
const log = function(statements) {
if (enableLogging) {
console.log.apply(console, statements);
}
};
/**
* A comparator function for uuids.
*
* @typedef {function(string,string):number}
*/
let UuidComparator;
/**
* Creates a comparator function which sorts uuids in descending order by the
* corresponding index of the given map.
*
* @param {!Object<string, number>} uuidIndexMap The map with uuids as the key
* and the window index as the value.
* @return {!UuidComparator} The comparator for sorting.
*/
const createUuidComparator = function(uuidIndexMap) {
return (a, b) => {
const indexA = uuidIndexMap[a] || -1;
const indexB = uuidIndexMap[b] || -1;
return indexB - indexA;
};
};
exports = {
log,
createUuidComparator,
UuidComparator,
};
/*
* 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',
},
},
},
};
/*
* 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);
}
});
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Externs for unit tests to avoid renaming of properties.
*
* These externs are only required when building with bazel because the
* closure_js_test compiles tests as well.
*
* @externs
*/
/** @record */
function ValidationObject() {}
/** @type {*} */
ValidationObject.prototype.field;
/** @record */
function Uuids() {}
/** @type {!Array<string>} */
Uuids.prototype.uuids;
/**
* 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'));
}
});
/**
* 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;
/**
* 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_);
},
});
/**
* 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);
},
});
/**
* 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_);
},
});
/**
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @fileoverview Description of this file.
*/
goog.module('exoplayer.cast.test.util');
goog.setTestOnly();
/**
* The queue of sample media items
*
* @type {!Array<!MediaItem>}
*/
const queue = [
{
uuid: 'uuid0',
media: {
uri: 'http://example.com',
},
mimeType: 'video/*',
},
{
uuid: 'uuid1',
media: {
uri: 'http://example1.com',
},
mimeType: 'application/dash+xml',
},
{
uuid: 'uuid2',
media: {
uri: 'http://example2.com',
},
mimeType: 'video/*',
},
{
uuid: 'uuid3',
media: {
uri: 'http://example3.com',
},
mimeType: 'application/dash+xml',
},
{
uuid: 'uuid4',
media: {
uri: 'http://example4.com',
},
mimeType: 'video/*',
},
{
uuid: 'uuid5',
media: {
uri: 'http://example5.com',
},
mimeType: 'application/dash+xml',
},
];
/**
* Asserts whether the map of uuids is complete and points to the correct
* indices.
*
* @param {!Object<string, number>} uuidIndexMap The uuid to index map.
* @param {!Array<!MediaItem>} queue The media item queue.
*/
const assertUuidIndexMap = (uuidIndexMap, queue) => {
assertEquals(queue.length, Object.entries(uuidIndexMap).length);
queue.forEach((mediaItem, index) => {
assertEquals(uuidIndexMap[mediaItem.uuid], index);
});
};
exports.queue = queue;
exports.assertUuidIndexMap = assertUuidIndexMap;
/**
* 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'));
},
});
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.cast;
/** Handles communication with the receiver app using a cast session. */
public interface CastSessionManager {
/** Factory for {@link CastSessionManager} instances. */
interface Factory {
/**
* Creates a {@link CastSessionManager} instance with the given listener.
*
* @param listener The listener to notify on receiver app and session state updates.
* @return The created instance.
*/
CastSessionManager create(StateListener listener);
}
/**
* Extends {@link SessionAvailabilityListener} by adding receiver app state notifications.
*
* <p>Receiver app state notifications contain a sequence number that matches the sequence number
* of the last {@link ExoCastMessage} sent (using {@link #send(ExoCastMessage)}) by this session
* manager and processed by the receiver app. Sequence numbers are non-negative numbers.
*/
interface StateListener extends SessionAvailabilityListener {
/**
* Called when a status update is received from the Cast Receiver app.
*
* @param stateUpdate A {@link ReceiverAppStateUpdate} containing the fields included in the
* message.
*/
void onStateUpdateFromReceiverApp(ReceiverAppStateUpdate stateUpdate);
}
/**
* Special constant representing an unset sequence number. It is guaranteed to be a negative
* value.
*/
long SEQUENCE_NUMBER_UNSET = Long.MIN_VALUE;
/**
* Connects the session manager to the cast message bus and starts listening for session
* availability changes. Also announces that this sender app is connected to the message bus.
*/
void start();
/** Stops tracking the state of the cast session and closes any existing session. */
void stopTrackingSession();
/**
* Same as {@link #stopTrackingSession()}, but also stops the receiver app if a session is
* currently available.
*/
void stopTrackingSessionAndCasting();
/** Whether a cast session is available. */
boolean isCastSessionAvailable();
/**
* Sends an {@link ExoCastMessage} to the receiver app.
*
* <p>A sequence number is assigned to every sent message. Message senders may mask the local
* state until a status update from the receiver app (see {@link StateListener}) is received with
* a greater or equal sequence number.
*
* @param message The message to send.
* @return The sequence number assigned to the message.
*/
long send(ExoCastMessage message);
}
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.cast;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.gms.cast.Cast;
import com.google.android.gms.cast.CastDevice;
import com.google.android.gms.cast.framework.CastContext;
import com.google.android.gms.cast.framework.CastSession;
import com.google.android.gms.cast.framework.SessionManager;
import com.google.android.gms.cast.framework.SessionManagerListener;
import java.io.IOException;
import org.json.JSONException;
/** Implements {@link CastSessionManager} by using JSON message passing. */
public class DefaultCastSessionManager implements CastSessionManager {
private static final String TAG = "DefaultCastSessionManager";
private static final String EXOPLAYER_CAST_NAMESPACE = "urn:x-cast:com.google.exoplayer.cast";
private final SessionManager sessionManager;
private final CastSessionListener castSessionListener;
private final StateListener stateListener;
private final Cast.MessageReceivedCallback messageReceivedCallback;
private boolean started;
private long sequenceNumber;
private long expectedInitialStateUpdateSequence;
@Nullable private CastSession currentSession;
/**
* @param context The Cast context from which the cast session is obtained.
* @param stateListener The listener to notify of state changes.
*/
public DefaultCastSessionManager(CastContext context, StateListener stateListener) {
this.stateListener = stateListener;
sessionManager = context.getSessionManager();
currentSession = sessionManager.getCurrentCastSession();
castSessionListener = new CastSessionListener();
messageReceivedCallback = new CastMessageCallback();
expectedInitialStateUpdateSequence = SEQUENCE_NUMBER_UNSET;
}
@Override
public void start() {
started = true;
sessionManager.addSessionManagerListener(castSessionListener, CastSession.class);
currentSession = sessionManager.getCurrentCastSession();
if (currentSession != null) {
setMessageCallbackOnSession();
}
}
@Override
public void stopTrackingSession() {
stop(/* stopCasting= */ false);
}
@Override
public void stopTrackingSessionAndCasting() {
stop(/* stopCasting= */ true);
}
@Override
public boolean isCastSessionAvailable() {
return currentSession != null && expectedInitialStateUpdateSequence == SEQUENCE_NUMBER_UNSET;
}
@Override
public long send(ExoCastMessage message) {
if (currentSession != null) {
currentSession.sendMessage(EXOPLAYER_CAST_NAMESPACE, message.toJsonString(sequenceNumber));
} else {
Log.w(TAG, "Tried to send a message with no established session. Method: " + message.method);
}
return sequenceNumber++;
}
private void stop(boolean stopCasting) {
sessionManager.removeSessionManagerListener(castSessionListener, CastSession.class);
if (currentSession != null) {
sessionManager.endCurrentSession(stopCasting);
}
currentSession = null;
started = false;
}
private void setCastSession(@Nullable CastSession session) {
Assertions.checkState(started);
boolean hadSession = currentSession != null;
currentSession = session;
if (!hadSession && session != null) {
setMessageCallbackOnSession();
} else if (hadSession && session == null) {
stateListener.onCastSessionUnavailable();
}
}
private void setMessageCallbackOnSession() {
try {
Assertions.checkNotNull(currentSession)
.setMessageReceivedCallbacks(EXOPLAYER_CAST_NAMESPACE, messageReceivedCallback);
expectedInitialStateUpdateSequence = send(new ExoCastMessage.OnClientConnected());
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
/** Listens for Cast session state changes. */
private class CastSessionListener implements SessionManagerListener<CastSession> {
@Override
public void onSessionStarting(CastSession castSession) {}
@Override
public void onSessionStarted(CastSession castSession, String sessionId) {
setCastSession(castSession);
}
@Override
public void onSessionStartFailed(CastSession castSession, int error) {}
@Override
public void onSessionEnding(CastSession castSession) {}
@Override
public void onSessionEnded(CastSession castSession, int error) {
setCastSession(null);
}
@Override
public void onSessionResuming(CastSession castSession, String sessionId) {}
@Override
public void onSessionResumed(CastSession castSession, boolean wasSuspended) {
setCastSession(castSession);
}
@Override
public void onSessionResumeFailed(CastSession castSession, int error) {}
@Override
public void onSessionSuspended(CastSession castSession, int reason) {
setCastSession(null);
}
}
private class CastMessageCallback implements Cast.MessageReceivedCallback {
@Override
public void onMessageReceived(CastDevice castDevice, String namespace, String message) {
if (!EXOPLAYER_CAST_NAMESPACE.equals(namespace)) {
// Non-matching namespace. Ignore.
Log.e(TAG, String.format("Unrecognized namespace: '%s'.", namespace));
return;
}
try {
ReceiverAppStateUpdate receivedUpdate = ReceiverAppStateUpdate.fromJsonMessage(message);
if (expectedInitialStateUpdateSequence == SEQUENCE_NUMBER_UNSET
|| receivedUpdate.sequenceNumber >= expectedInitialStateUpdateSequence) {
stateListener.onStateUpdateFromReceiverApp(receivedUpdate);
if (expectedInitialStateUpdateSequence != SEQUENCE_NUMBER_UNSET) {
expectedInitialStateUpdateSequence = SEQUENCE_NUMBER_UNSET;
stateListener.onCastSessionAvailable();
}
}
} catch (JSONException e) {
Log.e(TAG, "Error while parsing state update from receiver: ", e);
}
}
}
}
/*
* 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";
}
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.cast;
import android.content.Context;
import androidx.annotation.Nullable;
import com.google.android.gms.cast.framework.CastOptions;
import com.google.android.gms.cast.framework.OptionsProvider;
import com.google.android.gms.cast.framework.SessionProvider;
import java.util.List;
/** Cast options provider to target ExoPlayer's custom receiver app. */
public final class ExoCastOptionsProvider implements OptionsProvider {
public static final String RECEIVER_ID = "365DCC88";
@Override
public CastOptions getCastOptions(Context context) {
return new CastOptions.Builder().setReceiverApplicationId(RECEIVER_ID).build();
}
@Override
@Nullable
public List<SessionProvider> getAdditionalSessionProviders(Context context) {
return null;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment