Skip to content

Commit

Permalink
feat(UI): Add remote button with RemotePlayback API (#5650)
Browse files Browse the repository at this point in the history
  • Loading branch information
avelad authored Sep 14, 2023
1 parent bd17c2b commit 1ef5ae0
Show file tree
Hide file tree
Showing 6 changed files with 279 additions and 2 deletions.
1 change: 1 addition & 0 deletions build/types/ui
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
+../../ui/playback_rate_selection.js
+../../ui/presentation_time.js
+../../ui/range_element.js
+../../ui/remote_button.js
+../../ui/resolution_selection.js
+../../ui/rewind_button.js
+../../ui/seek_bar.js
Expand Down
4 changes: 4 additions & 0 deletions docs/tutorials/ui-customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ The following elements can be added to the UI bar using this configuration value
supports AirPlay.
* cast: adds a button that opens a Chromecast dialog. The button is visible only if there is
at least one Chromecast device on the same network available for casting.
* remote: adds a button that opens a Remote Playback dialog. The button is visible only if the
browser supports Remote Playback API.
* quality: adds a button that controls enabling/disabling of abr and video resolution selection.
* language: adds a button that controls audio language selection.
* playback_rate: adds a button that controls the playback rate selection.
Expand All @@ -91,6 +93,8 @@ The following buttons can be added to the overflow menu:
* playback_rate: adds a button that controls the playback rate selection.
* airplay: adds a button that opens a AirPlay dialog. The button is visible only if the browser
supports AirPlay.
* remote: adds a button that opens a Remote Playback dialog. The button is visible only if the
browser supports Remote Playback API.
* Statistics: adds a button that displays statistics of the video.
<!-- TODO: If we add more buttons that can be put in the order this way, list them here. -->

Expand Down
68 changes: 68 additions & 0 deletions externs/media_remote.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

/**
* @fileoverview Externs for HTMLMediaElement which were missing in the
* Closure compiler.
*
* @externs
*/


/**
* @constructor
* @implements {EventTarget}
*/
function RemotePlayback() {}


/**
* Represents the RemotePlayback connection's state.
* @type {string}
*/
RemotePlayback.prototype.state;


/**
* The watchAvailability() method of the RemotePlayback interface watches
* the list of available remote playback devices and returns a Promise that
* resolves with the callbackId of a remote playback device.
*
* @param {!function(boolean)} callback
* @return {!Promise}
*/
RemotePlayback.prototype.watchAvailability = function(callback) {};


/**
* The cancelWatchAvailability() method of the RemotePlayback interface
* cancels the request to watch for one or all available devices.
*
* @param {number} id
* @return {!Promise}
*/
RemotePlayback.prototype.cancelWatchAvailability = function(id) {};


/**
* The prompt() method of the RemotePlayback interface prompts the user
* to select an available remote playback device and give permission
* for the current media to be played using that device.
*
* If the user gives permission, the state will be set to connecting and
* the user agent will connect to the device to initiate playback.
*
* If the user chooses to instead disconnect from the device, the state will
* be set to disconnected and user agent will disconnect from this device.
*
* @return {!Promise}
*/
RemotePlayback.prototype.prompt = function() {};


/** @type {RemotePlayback} */
HTMLMediaElement.prototype.remote;

1 change: 1 addition & 0 deletions shaka-player.uncompiled.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ goog.require('shaka.ui.Overlay');
goog.require('shaka.ui.PipButton');
goog.require('shaka.ui.PlaybackRateSelection');
goog.require('shaka.ui.PresentationTimeTracker');
goog.require('shaka.ui.RemoteButton');
goog.require('shaka.ui.ResolutionSelection');
goog.require('shaka.ui.RewindButton');
goog.require('shaka.ui.SkipAdButton');
Expand Down
201 changes: 201 additions & 0 deletions ui/remote_button.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/


goog.provide('shaka.ui.RemoteButton');

goog.require('shaka.Player');
goog.require('shaka.ui.Controls');
goog.require('shaka.ui.Element');
goog.require('shaka.ui.Enums');
goog.require('shaka.ui.Locales');
goog.require('shaka.ui.Localization');
goog.require('shaka.ui.OverflowMenu');
goog.require('shaka.ui.Utils');
goog.require('shaka.util.Dom');
goog.require('shaka.util.Platform');
goog.requireType('shaka.ui.Controls');


/**
* @extends {shaka.ui.Element}
* @final
* @export
*/
shaka.ui.RemoteButton = class extends shaka.ui.Element {
/**
* @param {!HTMLElement} parent
* @param {!shaka.ui.Controls} controls
*/
constructor(parent, controls) {
super(parent, controls);

/** @private {!HTMLButtonElement} */
this.remoteButton_ = shaka.util.Dom.createButton();
this.remoteButton_.classList.add('shaka-remote-button');
this.remoteButton_.classList.add('shaka-tooltip');
this.remoteButton_.ariaPressed = 'false';

/** @private {!HTMLElement} */
this.remoteIcon_ = shaka.util.Dom.createHTMLElement('i');
this.remoteIcon_.classList.add('material-icons-round');
let icon = shaka.ui.Enums.MaterialDesignIcons.CAST;
const safariVersion = shaka.util.Platform.safariVersion();
if (safariVersion && safariVersion >= 13) {
icon = shaka.ui.Enums.MaterialDesignIcons.AIRPLAY;
}
this.remoteIcon_.textContent = icon;
this.remoteButton_.appendChild(this.remoteIcon_);

const label = shaka.util.Dom.createHTMLElement('label');
label.classList.add('shaka-overflow-button-label');
label.classList.add('shaka-overflow-menu-only');
this.remoteNameSpan_ = shaka.util.Dom.createHTMLElement('span');
label.appendChild(this.remoteNameSpan_);

this.remoteCurrentSelectionSpan_ =
shaka.util.Dom.createHTMLElement('span');
this.remoteCurrentSelectionSpan_.classList.add(
'shaka-current-selection-span');
label.appendChild(this.remoteCurrentSelectionSpan_);
this.remoteButton_.appendChild(label);
this.parent.appendChild(this.remoteButton_);

/** @private {number} */
this.callbackId_ = -1;

// Setup strings in the correct language
this.updateLocalizedStrings_();

shaka.ui.Utils.setDisplay(this.remoteButton_, false);

if (!this.video.remote || this.video.disableRemotePlayback) {
this.remoteButton_.classList.add('shaka-hidden');
} else {
this.eventManager.listen(
this.localization, shaka.ui.Localization.LOCALE_UPDATED, () => {
this.updateLocalizedStrings_();
});

this.eventManager.listen(
this.localization, shaka.ui.Localization.LOCALE_CHANGED, () => {
this.updateLocalizedStrings_();
});

this.eventManager.listen(this.controls, 'caststatuschanged', () => {
this.updateRemoteState_();
});

this.eventManager.listen(this.remoteButton_, 'click', () => {
this.video.remote.prompt();
});

this.eventManager.listen(this.video.remote, 'connect', () => {
this.updateRemoteState_();
});

this.eventManager.listen(this.video.remote, 'connecting', () => {
this.updateRemoteState_();
});

this.eventManager.listen(this.video.remote, 'disconnect', () => {
this.updateRemoteState_();
});

this.eventManager.listen(this.player, 'loaded', () => {
this.updateRemoteState_();
});

this.updateRemoteState_();
}
}

/** @override */
release() {
if (this.video.remote && this.callbackId_ != -1) {
this.video.remote.cancelWatchAvailability(this.callbackId_);
}

super.release();
}

/**
* @private
*/
async updateRemoteState_() {
if (this.controls.getCastProxy().canCast() &&
this.controls.isCastAllowed()) {
shaka.ui.Utils.setDisplay(this.remoteButton_, false);
if (this.callbackId_ != -1) {
this.video.remote.cancelWatchAvailability(this.callbackId_);
this.callbackId_ = -1;
}
} else if (this.video.remote.state == 'disconnected') {
const handleAvailabilityChange = (availability) => {
if (this.player) {
const loadMode = this.player.getLoadMode();
const srcMode = loadMode == shaka.Player.LoadMode.SRC_EQUALS;
shaka.ui.Utils.setDisplay(
this.remoteButton_, srcMode && availability);
} else {
shaka.ui.Utils.setDisplay(this.remoteButton_, false);
}
};
try {
if (this.callbackId_ != -1) {
await this.video.remote.cancelWatchAvailability(this.callbackId_);
this.callbackId_ = -1;
}
} catch (e) {
// Ignore this error.
}
try {
const id = await this.video.remote.watchAvailability(
handleAvailabilityChange);
this.callbackId_ = id;
} catch (e) {
handleAvailabilityChange(/* availability= */ true);
}
} else if (this.callbackId_ != -1) {
// If remote device is connecting or connected, we should stop
// watching remote device availability to save power.
await this.video.remote.cancelWatchAvailability(this.callbackId_);
this.callbackId_ = -1;
}
}

/**
* @private
*/
updateLocalizedStrings_() {
const LocIds = shaka.ui.Locales.Ids;
let text = this.localization.resolve(LocIds.CAST);
const safariVersion = shaka.util.Platform.safariVersion();
if (safariVersion && safariVersion >= 13) {
text = this.localization.resolve(LocIds.AIRPLAY);
}
this.remoteButton_.ariaLabel = text;
this.remoteNameSpan_.textContent = text;
}
};


/**
* @implements {shaka.extern.IUIElement.Factory}
* @final
*/
shaka.ui.RemoteButton.Factory = class {
/** @override */
create(rootElement, controls) {
return new shaka.ui.RemoteButton(rootElement, controls);
}
};

shaka.ui.OverflowMenu.registerElement(
'remote', new shaka.ui.RemoteButton.Factory());

shaka.ui.Controls.registerElement(
'remote', new shaka.ui.RemoteButton.Factory());
6 changes: 4 additions & 2 deletions ui/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,8 +247,10 @@ shaka.ui.Overlay = class {
fullScreenElement: this.videoContainer_,
};

// Check AirPlay support
if (window.WebKitPlaybackTargetAvailabilityEvent) {
// eslint-disable-next-line no-restricted-syntax
if ('remote' in HTMLMediaElement.prototype) {
config.overflowMenuButtons.push('remote');
} else if (window.WebKitPlaybackTargetAvailabilityEvent) {
config.overflowMenuButtons.push('airplay');
}

Expand Down

0 comments on commit 1ef5ae0

Please sign in to comment.