diff --git a/docs/design/chromecast.md b/docs/design/chromecast.md
index 8a49411520..24b38acb31 100644
--- a/docs/design/chromecast.md
+++ b/docs/design/chromecast.md
@@ -26,11 +26,13 @@ the `Player` and `HTMLMediaElement` objects. Receiver apps only have to worry
about their UI, while the `CastReceiver` takes care of playback and
communication.
+To enable an Android (TV) receiver apps set `androidReceiverCompatible` to true.
+
#### `CastProxy` API sketch
```js
-new shaka.cast.CastProxy(video, player, receiverAppId)
+new shaka.cast.CastProxy(video, player, receiverAppId, androidReceiverCompatible)
// Also destroys the underlying local Player object
shaka.cast.CastProxy.prototype.destroy() => Promise
diff --git a/docs/tutorials/ui.md b/docs/tutorials/ui.md
index 0e7fb4b9c0..aa44570961 100644
--- a/docs/tutorials/ui.md
+++ b/docs/tutorials/ui.md
@@ -130,6 +130,23 @@ Next, let's add a listener to the 'caststatuschanged' event in myapp.js:
}
```
+
+#### Enabling Android Receiver Apps
+
+If you'd like to take advantage of Android Receiver App support,
+you will need to provide a boolean flag to enable support for
+casting to an Android receiver app.
+
+```html
+
+
+
+
` element
@@ -200,6 +217,15 @@ const controls = ui.getControls();
// your API calls will be routed to the remote playback session.
const player = controls.getPlayer();
const video = controls.getVideo();
+
+// Programatically configure the Chromecast Receiver App Id and Android
+// Receiver Compatability.
+ui.configure({
+ // Set the castReceiverAppId
+ 'castReceiverAppId': 'BBED8D28',
+ // Enable casting to native Android Apps (e.g. Android TV Apps)
+ 'castAndroidReceiverCompatible': true,
+});
```
diff --git a/externs/chromecast.js b/externs/chromecast.js
index ed2ef74c28..382165769a 100644
--- a/externs/chromecast.js
+++ b/externs/chromecast.js
@@ -270,6 +270,13 @@ chrome.cast.Session = class {
chrome.cast.SessionRequest = class {
- /** @param {string} appId */
- constructor(appId) {}
+ /**
+ * @param {string} appId
+ * @param {Array.} capabilities
+ * @param {?number} timeout
+ * @param {boolean} androidReceiverCompatible
+ * @param {Object} credentialsData
+ */
+ constructor(appId, capabilities, timeout, androidReceiverCompatible,
+ credentialsData) {}
};
diff --git a/lib/cast/cast_proxy.js b/lib/cast/cast_proxy.js
index 26c203ca4d..e7f1a6afd2 100644
--- a/lib/cast/cast_proxy.js
+++ b/lib/cast/cast_proxy.js
@@ -43,8 +43,11 @@ shaka.cast.CastProxy = class extends shaka.util.FakeEventTarget {
* @param {string} receiverAppId The ID of the cast receiver application.
* If blank, casting will not be available, but the proxy will still
* function otherwise.
+ * @param {boolean} androidReceiverCompatible Indicates if the app is
+ * compatible with an Android Receiver.
*/
- constructor(video, player, receiverAppId) {
+ constructor(video, player, receiverAppId,
+ androidReceiverCompatible = false) {
super();
/** @private {HTMLMediaElement} */
@@ -71,6 +74,9 @@ shaka.cast.CastProxy = class extends shaka.util.FakeEventTarget {
/** @private {string} */
this.receiverAppId_ = receiverAppId;
+ /** @private {boolean} */
+ this.androidReceiverCompatible_ = androidReceiverCompatible;
+
/** @private {!Map} */
this.compiledToExternNames_ = new Map();
@@ -81,7 +87,8 @@ shaka.cast.CastProxy = class extends shaka.util.FakeEventTarget {
() => this.onFirstCastStateUpdate_(),
(targetName, event) => this.onRemoteEvent_(targetName, event),
() => this.onResumeLocal_(),
- () => this.getInitState_());
+ () => this.getInitState_(),
+ androidReceiverCompatible);
this.init_();
@@ -225,15 +232,18 @@ shaka.cast.CastProxy = class extends shaka.util.FakeEventTarget {
/**
* @param {string} newAppId
+ * @param {boolean=} newCastAndroidReceiver
* @export
*/
- async changeReceiverId(newAppId) {
- if (newAppId == this.receiverAppId_) {
+ async changeReceiverId(newAppId, newCastAndroidReceiver = false) {
+ if (newAppId == this.receiverAppId_ &&
+ newCastAndroidReceiver == this.androidReceiverCompatible_) {
// Nothing to change
return;
}
this.receiverAppId_ = newAppId;
+ this.androidReceiverCompatible_ = newCastAndroidReceiver;
// Destroy the old sender
this.sender_.forceDisconnect();
@@ -248,7 +258,8 @@ shaka.cast.CastProxy = class extends shaka.util.FakeEventTarget {
() => this.onFirstCastStateUpdate_(),
(targetName, event) => this.onRemoteEvent_(targetName, event),
() => this.onResumeLocal_(),
- () => this.getInitState_());
+ () => this.getInitState_(),
+ newCastAndroidReceiver);
this.sender_.init();
}
diff --git a/lib/cast/cast_sender.js b/lib/cast/cast_sender.js
index 9e3bf90835..95e9aec6c5 100644
--- a/lib/cast/cast_sender.js
+++ b/lib/cast/cast_sender.js
@@ -32,12 +32,18 @@ shaka.cast.CastSender = class {
* should resume playback. Called before the cached remote state is wiped.
* @param {function()} onInitStateRequired A callback to get local player's.
* state. Invoked when casting is initiated from Chrome's cast button.
+ * @param {boolean} androidReceiverCompatible Indicates if the app is
+ * compatible with an Android Receiver.
*/
constructor(receiverAppId, onStatusChanged, onFirstCastStateUpdate,
- onRemoteEvent, onResumeLocal, onInitStateRequired) {
+ onRemoteEvent, onResumeLocal, onInitStateRequired,
+ androidReceiverCompatible) {
/** @private {string} */
this.receiverAppId_ = receiverAppId;
+ /** @private {boolean} */
+ this.androidReceiverCompatible_ = androidReceiverCompatible;
+
/** @private {shaka.util.Timer} */
this.statusChangeTimer_ = new shaka.util.Timer(onStatusChanged);
@@ -211,7 +217,11 @@ shaka.cast.CastSender = class {
// Use static versions of the API callbacks, since the ChromeCast API is
// static. If we used local versions, we might end up retaining references
// to destroyed players here.
- const sessionRequest = new chrome.cast.SessionRequest(this.receiverAppId_);
+ const sessionRequest = new chrome.cast.SessionRequest(this.receiverAppId_,
+ /* capabilities= */ [],
+ /* timeout= */ null,
+ this.androidReceiverCompatible_,
+ /* credentialsData= */null);
const apiConfig = new chrome.cast.ApiConfig(sessionRequest,
(session) => CastSender.onExistingSessionJoined_(session),
(availability) => CastSender.onReceiverStatusChanged_(availability),
diff --git a/test/cast/cast_proxy_unit.js b/test/cast/cast_proxy_unit.js
index 4055d362b2..3aaad92909 100644
--- a/test/cast/cast_proxy_unit.js
+++ b/test/cast/cast_proxy_unit.js
@@ -11,6 +11,7 @@ describe('CastProxy', () => {
const originalCastSender = shaka.cast.CastSender;
const fakeAppId = 'fake app ID';
+ const fakeAndroidReceiverCompatible = false;
let mockPlayer;
let mockSender;
@@ -31,7 +32,8 @@ describe('CastProxy', () => {
mockPlayer = createMockPlayer();
mockSender = null;
- proxy = new CastProxy(mockVideo, mockPlayer, fakeAppId);
+ proxy = new CastProxy(mockVideo, mockPlayer, fakeAppId,
+ fakeAndroidReceiverCompatible);
});
afterEach(async () => {
diff --git a/test/cast/cast_sender_unit.js b/test/cast/cast_sender_unit.js
index 2602fc54d4..4caa776539 100644
--- a/test/cast/cast_sender_unit.js
+++ b/test/cast/cast_sender_unit.js
@@ -13,6 +13,7 @@ describe('CastSender', () => {
const originalStatusDelay = shaka.cast.CastSender.STATUS_DELAY;
const fakeAppId = 'asdf';
+ const fakeAndroidReceiverCompatible = false;
const fakeInitState = {
manifest: null,
player: null,
@@ -52,7 +53,8 @@ describe('CastSender', () => {
sender = new CastSender(
fakeAppId, Util.spyFunc(onStatusChanged),
Util.spyFunc(onFirstCastStateUpdate), Util.spyFunc(onRemoteEvent),
- Util.spyFunc(onResumeLocal), Util.spyFunc(onInitStateRequired));
+ Util.spyFunc(onResumeLocal), Util.spyFunc(onInitStateRequired),
+ fakeAndroidReceiverCompatible);
});
afterEach(async () => {
@@ -87,7 +89,12 @@ describe('CastSender', () => {
expect(sender.apiReady()).toBe(true);
expect(sender.hasReceivers()).toBe(false);
expect(onStatusChanged).toHaveBeenCalled();
- expect(mockCastApi.SessionRequest).toHaveBeenCalledWith(fakeAppId);
+ expect(mockCastApi.SessionRequest).toHaveBeenCalledWith(
+ fakeAppId,
+ /* capabilities= */ [],
+ /* timeout= */ null,
+ fakeAndroidReceiverCompatible,
+ /* credentialsData= */ null);
expect(mockCastApi.initialize).toHaveBeenCalled();
});
@@ -97,7 +104,12 @@ describe('CastSender', () => {
expect(sender.apiReady()).toBe(true);
expect(sender.hasReceivers()).toBe(false);
expect(onStatusChanged).toHaveBeenCalled();
- expect(mockCastApi.SessionRequest).toHaveBeenCalledWith(fakeAppId);
+ expect(mockCastApi.SessionRequest).toHaveBeenCalledWith(
+ fakeAppId,
+ /* capabilities= */ [],
+ /* timeout= */ null,
+ fakeAndroidReceiverCompatible,
+ /* credentialsData= */ null);
expect(mockCastApi.initialize).toHaveBeenCalled();
});
});
@@ -122,7 +134,8 @@ describe('CastSender', () => {
sender = new CastSender(
fakeAppId, Util.spyFunc(onStatusChanged),
Util.spyFunc(onFirstCastStateUpdate), Util.spyFunc(onRemoteEvent),
- Util.spyFunc(onResumeLocal), Util.spyFunc(onInitStateRequired));
+ Util.spyFunc(onResumeLocal), Util.spyFunc(onInitStateRequired),
+ /* androidReceiverCompatible= */ false);
sender.init();
// You get an initial call to onStatusChanged when it initializes.
expect(onStatusChanged).toHaveBeenCalledTimes(3);
@@ -256,7 +269,8 @@ describe('CastSender', () => {
sender = new CastSender(
fakeAppId, Util.spyFunc(onStatusChanged),
Util.spyFunc(onFirstCastStateUpdate), Util.spyFunc(onRemoteEvent),
- Util.spyFunc(onResumeLocal), Util.spyFunc(onInitStateRequired));
+ Util.spyFunc(onResumeLocal), Util.spyFunc(onInitStateRequired),
+ /* androidReceiverCompatible= */ false);
sender.init();
// The sender should automatically rejoin the session, without needing
@@ -288,7 +302,8 @@ describe('CastSender', () => {
sender = new CastSender(
fakeAppId, Util.spyFunc(onStatusChanged),
Util.spyFunc(onFirstCastStateUpdate), Util.spyFunc(onRemoteEvent),
- Util.spyFunc(onResumeLocal), Util.spyFunc(onInitStateRequired));
+ Util.spyFunc(onResumeLocal), Util.spyFunc(onInitStateRequired),
+ /* androidReceiverCompatible= */ false);
sender.init();
expect(sender.isCasting()).toBe(false);
diff --git a/ui/controls.js b/ui/controls.js
index 2a2def7a6e..9008a0398d 100644
--- a/ui/controls.js
+++ b/ui/controls.js
@@ -53,7 +53,8 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
/** @private {shaka.cast.CastProxy} */
this.castProxy_ = new shaka.cast.CastProxy(
- video, player, this.config_.castReceiverAppId);
+ video, player, this.config_.castReceiverAppId,
+ this.config_.castAndroidReceiverCompatible);
/** @private {boolean} */
this.castAllowed_ = true;
@@ -315,7 +316,8 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
configure(config) {
this.config_ = config;
- this.castProxy_.changeReceiverId(config.castReceiverAppId);
+ this.castProxy_.changeReceiverId(config.castReceiverAppId,
+ config.castAndroidReceiverCompatible);
// Deconstruct the old layout if applicable
if (this.seekBar_) {
diff --git a/ui/externs/ui.js b/ui/externs/ui.js
index 5e8dd8e65d..a78473d576 100644
--- a/ui/externs/ui.js
+++ b/ui/externs/ui.js
@@ -72,6 +72,7 @@ shaka.extern.UIVolumeBarColors;
* addBigPlayButton: boolean,
* customContextMenu: boolean,
* castReceiverAppId: string,
+ * castAndroidReceiverCompatible: boolean,
* clearBufferOnQualityChange: boolean,
* showUnbufferedStart: boolean,
* seekBarColors: shaka.extern.UISeekBarColors,
@@ -109,6 +110,8 @@ shaka.extern.UIVolumeBarColors;
* Whether or not a custom context menu replaces the default.
* @property {string} castReceiverAppId
* Receiver app id to use for the Chromecast support.
+ * @property {boolean} castAndroidReceiverCompatible
+ * Indicates if the app is compatible with an Android Cast Receiver.
* @property {boolean} clearBufferOnQualityChange
* Only applicable if the resolution selection is part of the UI.
* Whether buffer should be cleared when changing resolution
diff --git a/ui/ui.js b/ui/ui.js
index 76b3b0419a..42224e5d3d 100644
--- a/ui/ui.js
+++ b/ui/ui.js
@@ -218,6 +218,7 @@ shaka.ui.Overlay = class {
addBigPlayButton: false,
customContextMenu: false,
castReceiverAppId: '',
+ castAndroidReceiverCompatible: false,
clearBufferOnQualityChange: true,
showUnbufferedStart: false,
seekBarColors: {
@@ -388,19 +389,28 @@ shaka.ui.Overlay = class {
// Get and configure cast app id.
let castAppId = '';
+ // Get and configure cast Android Receiver Compatibility
+ let castAndroidReceiverCompatible = false;
+
// Cast receiver id can be specified on either container or video.
// It should not be provided on both. If it was, we will use the last
// one we saw.
if (container['dataset'] &&
container['dataset']['shakaPlayerCastReceiverId']) {
castAppId = container['dataset']['shakaPlayerCastReceiverId'];
+ castAndroidReceiverCompatible =
+ container['dataset']['shakaPlayerCastAndroidReceiverCompatible'] ===
+ 'true';
} else if (video['dataset'] &&
video['dataset']['shakaPlayerCastReceiverId']) {
castAppId = video['dataset']['shakaPlayerCastReceiverId'];
+ castAndroidReceiverCompatible =
+ video['dataset']['shakaPlayerCastAndroidReceiverCompatible'] === 'true';
}
if (castAppId.length) {
- ui.configure({castReceiverAppId: castAppId});
+ ui.configure({castReceiverAppId: castAppId,
+ castAndroidReceiverCompatible: castAndroidReceiverCompatible});
}
if (shaka.util.Dom.asHTMLMediaElement(video).controls) {