feat(UI)!: Improve Media Session management (#9483)

Fixes https://github.com/shaka-project/shaka-player/issues/9478
This commit is contained in:
Álvaro Velad Galván
2025-12-10 11:56:03 +01:00
committed by GitHub
parent b1e276cfb8
commit ff72abc4f5
9 changed files with 660 additions and 387 deletions
+1
View File
@@ -26,6 +26,7 @@
+../../ui/language_utils.js
+../../ui/localization.js
+../../ui/loop_button.js
+../../ui/media_session.js
+../../ui/mute_button.js
+../../ui/overflow_menu.js
+../../ui/pip_button.js
+3 -44
View File
@@ -58,6 +58,9 @@ class ShakaReceiverApp {
this.receiver_ = new shaka.cast.CastReceiver(
this.video_, this.player_,
(appData) => this.appDataCallback_(appData));
ui.getControls().setCastReceiver(this.receiver_);
this.receiver_.addEventListener(
'caststatuschanged', () => this.checkIdle_());
@@ -69,50 +72,6 @@ class ShakaReceiverApp {
this.video_.removeAttribute('poster');
});
// Setup content image and title
this.player_.addEventListener('metadata', (event) => {
const payload = event['payload'];
if (!payload) {
return;
}
let title;
if (payload['key'] == 'TIT2' && payload['data']) {
title = payload['data'];
}
let imageUrl;
if (payload['key'] == 'APIC' && payload['mimeType'] == '-->') {
imageUrl = payload['data'];
}
if (title) {
this.receiver_.setContentTitle(title);
}
if (imageUrl) {
this.receiver_.setContentImage(imageUrl);
}
});
this.player_.addEventListener('sessiondata', (event) => {
const id = event['id'];
switch (id) {
case 'com.apple.hls.title': {
const title = event['value'];
if (title) {
this.receiver_.setContentTitle(title);
}
break;
}
case 'com.apple.hls.poster': {
let imageUrl = event['value'];
if (imageUrl) {
imageUrl = imageUrl.replace('{w}', '512')
.replace('{h}', '512')
.replace('{f}', 'jpeg');
this.receiver_.setContentImage(imageUrl);
}
break;
}
}
});
this.startIdleTimer_();
}
+3 -3
View File
@@ -1274,7 +1274,7 @@ shakaDemo.Main = class {
this.player_.unload();
const queueManager = this.player_.getQueueManager();
const queueManager = this.controls_.getQueueManager();
queueManager.removeAllItems();
// The currently-selected asset changed, so update asset cards.
@@ -1412,7 +1412,7 @@ shakaDemo.Main = class {
ui.configure(uiConfig);
}
const queueManager = this.player_.getQueueManager();
const queueManager = this.controls_.getQueueManager();
await queueManager.removeAllItems();
if (asset.hasAds()) {
@@ -1492,7 +1492,7 @@ shakaDemo.Main = class {
* @param {ShakaDemoAssetInfo} asset
*/
async addToQueue(asset) {
const queueManager = this.player_.getQueueManager();
const queueManager = this.controls_.getQueueManager();
const queueItem = await this.getQueueItem_(asset);
queueManager.insertItems([queueItem]);
}
+62 -330
View File
@@ -21,6 +21,7 @@ goog.require('shaka.ui.HiddenFastForwardButton');
goog.require('shaka.ui.HiddenRewindButton');
goog.require('shaka.ui.Locales');
goog.require('shaka.ui.Localization');
goog.require('shaka.ui.MediaSession');
goog.require('shaka.ui.SeekBar');
goog.require('shaka.ui.SkipAdButton');
goog.require('shaka.ui.Utils');
@@ -31,9 +32,9 @@ goog.require('shaka.util.FakeEvent');
goog.require('shaka.util.FakeEventTarget');
goog.require('shaka.util.IDestroyable');
goog.require('shaka.util.Timer');
goog.require('shaka.util.TXml');
goog.requireType('shaka.Player');
goog.requireType('shaka.cast.CastReceiver');
/**
@@ -147,6 +148,9 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
video, player, this.config_.castReceiverAppId,
this.config_.castAndroidReceiverCompatible);
/** @private {?shaka.cast.CastReceiver} */
this.castReceiver_ = null;
/** @private {boolean} */
this.castAllowed_ = true;
@@ -301,7 +305,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
// Configure and create the layout of the controls
this.configure(this.config_);
this.addEventListeners_();
this.setupMediaSession_();
/**
* The pressed keys set is used to record which keys are currently pressed
@@ -365,6 +368,9 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
this.togglePiP();
}
});
/** @private {shaka.ui.MediaSession} */
this.mediaSession_ = new shaka.ui.MediaSession(this);
}
/**
@@ -450,7 +456,10 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
this.localization_ = null;
this.pressedKeys_.clear();
this.removeMediaSession_();
if (this.mediaSession_) {
this.mediaSession_.release();
this.mediaSession_ = null;
}
// FakeEventTarget implements IReleasable
super.release();
@@ -574,6 +583,10 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
this.eventManager_.listen(element, 'touchend', touchCb);
}
}
if (this.mediaSession_) {
this.mediaSession_.configure(this.config_);
}
}
/**
@@ -621,6 +634,14 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
}
}
/**
* @param {!shaka.cast.CastReceiver} receiver
* @export
*/
setCastReceiver(receiver) {
this.castReceiver_ = receiver;
}
/**
* @export
* @return {?shaka.extern.IAd}
@@ -653,6 +674,14 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
return this.castProxy_;
}
/**
* @export
* @return {?shaka.cast.CastReceiver}
*/
getCastReceiver() {
return this.castReceiver_;
}
/**
* @return {shaka.ui.Localization}
* @export
@@ -709,6 +738,22 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
return this.adManager_;
}
/**
* @return {shaka.extern.IQueueManager}
* @export
*/
getQueueManager() {
return this.queueManager_;
}
/**
* @return {shaka.ui.MediaSession}
* @export
*/
getMediaSession() {
return this.mediaSession_;
}
/**
* @return {!HTMLElement}
* @export
@@ -1573,333 +1618,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
}
/**
* @private
*/
setupMediaSession_() {
if (!this.config_.setupMediaSession || !navigator.mediaSession) {
return;
}
const addMediaSessionHandler = (type, callback) => {
try {
navigator.mediaSession.setActionHandler(type, callback);
} catch (error) {
shaka.log.debug(
`The "${type}" media session action is not supported.`);
}
};
const updatePositionState = () => {
if (this.ad_ && this.ad_.isLinear()) {
clearPositionState();
return;
}
const seekRange = this.player_.seekRange();
let duration = seekRange.end - seekRange.start;
const position = parseFloat(
(this.video_.currentTime - seekRange.start).toFixed(2));
if (this.player_.isLive() && Math.abs(duration - position) < 1) {
// Positive infinity indicates media without a defined end, such as a
// live stream.
duration = Infinity;
}
try {
navigator.mediaSession.setPositionState({
duration: Math.max(0, duration),
playbackRate: this.video_.playbackRate,
position: Math.max(0, position),
});
} catch (error) {
shaka.log.v2(
'setPositionState in media session is not supported.');
}
};
const clearPositionState = () => {
try {
navigator.mediaSession.setPositionState();
} catch (error) {
shaka.log.v2(
'setPositionState in media session is not supported.');
}
};
const commonHandler = (details) => {
const keyboardSeekDistance = this.config_.keyboardSeekDistance;
switch (details.action) {
case 'pause':
this.playPausePresentation();
break;
case 'play':
this.playPausePresentation();
break;
case 'seekbackward':
if (details.seekOffset && !isFinite(details.seekOffset)) {
break;
}
if (!this.ad_ || !this.ad_.isLinear()) {
this.seek_(this.seekBar_.getValue() -
(details.seekOffset || keyboardSeekDistance));
}
break;
case 'seekforward':
if (details.seekOffset && !isFinite(details.seekOffset)) {
break;
}
if (!this.ad_ || !this.ad_.isLinear()) {
this.seek_(this.seekBar_.getValue() +
(details.seekOffset || keyboardSeekDistance));
}
break;
case 'seekto':
if (details.seekTime && !isFinite(details.seekTime)) {
break;
}
if (!this.ad_ || !this.ad_.isLinear()) {
this.seek_(this.player_.seekRange().start + details.seekTime);
}
break;
case 'stop':
this.player_.unload();
break;
case 'enterpictureinpicture':
if (!this.ad_ || !this.ad_.isLinear()) {
this.togglePiP();
}
break;
case 'nexttrack':
this.queueManager_.playItem(
this.queueManager_.getCurrentItemIndex() + 1);
break;
case 'previoustrack':
this.queueManager_.playItem(
this.queueManager_.getCurrentItemIndex() - 1);
break;
case 'skipad':
if (this.ad_) {
this.ad_.skip();
}
break;
}
};
addMediaSessionHandler('pause', commonHandler);
addMediaSessionHandler('play', commonHandler);
addMediaSessionHandler('seekbackward', commonHandler);
addMediaSessionHandler('seekforward', commonHandler);
addMediaSessionHandler('seekto', commonHandler);
addMediaSessionHandler('stop', commonHandler);
if ('documentPictureInPicture' in window ||
document.pictureInPictureEnabled) {
addMediaSessionHandler('enterpictureinpicture', commonHandler);
}
// eslint-disable-next-line no-restricted-syntax
const supportsChapterInfo = 'chapterInfo' in MediaMetadata.prototype;
const getMediaMetadata = () => {
const metadata = {
title: '',
artist: '',
album: '',
artwork: [],
};
if (supportsChapterInfo) {
metadata.chapterInfo = [];
}
if (navigator.mediaSession.metadata) {
metadata.title = navigator.mediaSession.metadata.title;
metadata.artist = navigator.mediaSession.metadata.artist;
metadata.album = navigator.mediaSession.metadata.album;
metadata.artwork = navigator.mediaSession.metadata.artwork;
if (supportsChapterInfo) {
metadata.chapterInfo = navigator.mediaSession.metadata.chapterInfo;
}
}
return metadata;
};
const setupTitle = (title) => {
const metadata = getMediaMetadata();
metadata.title = title;
navigator.mediaSession.metadata = new MediaMetadata(metadata);
};
const setupPoster = (imageUrl) => {
const video = /** @type {HTMLVideoElement} */ (this.localVideo_);
if (imageUrl != video.poster) {
video.poster = imageUrl;
}
const metadata = getMediaMetadata();
metadata.artwork = [{src: imageUrl}];
navigator.mediaSession.metadata = new MediaMetadata(metadata);
};
const setupChapters = () => {
if (!supportsChapterInfo) {
return;
}
const chapterInfo = [];
for (const chapter of this.chapters_) {
chapterInfo.push({
title: chapter.title,
startTime: chapter.startTime,
artwork: [],
});
}
const metadata = getMediaMetadata();
metadata.chapterInfo = chapterInfo;
navigator.mediaSession.metadata = new MediaMetadata(metadata);
};
const playerLoaded = () => {
if (this.player_.isLive() || this.player_.seekRange().start != 0) {
updatePositionState();
this.eventManager_.listen(
this.video_, 'timeupdate', updatePositionState);
} else {
clearPositionState();
}
};
const playerUnloading = () => {
this.eventManager_.unlisten(
this.video_, 'timeupdate', updatePositionState);
navigator.mediaSession.metadata = new MediaMetadata({});
};
if (this.player_.isFullyLoaded()) {
playerLoaded();
}
this.eventManager_.listen(this.player_, 'loaded', playerLoaded);
this.eventManager_.listen(this.player_, 'unloading', playerUnloading);
this.eventManager_.listen(this.player_, 'trackschanged', setupChapters);
this.eventManager_.listen(this.player_, 'metadata', (event) => {
const payload = event['payload'];
if (!payload) {
return;
}
let title;
if (payload['key'] == 'TIT2' && payload['data']) {
title = payload['data'];
}
let imageUrl;
if (payload['key'] == 'APIC' && payload['mimeType'] == '-->') {
imageUrl = payload['data'];
}
if (title) {
setupTitle(title);
}
if (imageUrl) {
setupPoster(imageUrl);
}
});
this.eventManager_.listen(this.player_, 'sessiondata', (event) => {
const id = event['id'];
switch (id) {
case 'com.apple.hls.title': {
const title = event['value'];
if (title) {
setupTitle(title);
}
break;
}
case 'com.apple.hls.poster': {
let imageUrl = event['value'];
if (imageUrl) {
imageUrl = imageUrl.replace('{w}', '512')
.replace('{h}', '512')
.replace('{f}', 'jpeg');
setupPoster(imageUrl);
}
break;
}
}
});
this.eventManager_.listen(this.player_, 'programinformation', (event) => {
if (!event['detail']) {
return;
}
const TXml = shaka.util.TXml;
/** @type {!shaka.extern.xml.Node} */
const detail = /** @type {!shaka.extern.xml.Node} */(event['detail']);
const titleNode = TXml.findChild(detail, 'Title');
if (titleNode) {
const title = TXml.getContents(titleNode);
if (title) {
setupTitle(title);
}
}
});
const checkQueueItems = () => {
const itemsLength = this.queueManager_.getItems().length;
const currentIndex = this.queueManager_.getCurrentItemIndex();
if (itemsLength <= 1 || currentIndex == -1) {
addMediaSessionHandler('previoustrack', null);
addMediaSessionHandler('nexttrack', null);
return;
}
if (currentIndex > 0) {
addMediaSessionHandler('previoustrack', commonHandler);
} else {
addMediaSessionHandler('previoustrack', null);
}
if ((currentIndex + 1) < itemsLength) {
addMediaSessionHandler('nexttrack', commonHandler);
} else {
addMediaSessionHandler('nexttrack', null);
}
};
this.eventManager_.listen(
this.queueManager_, 'currentitemchanged', checkQueueItems);
this.eventManager_.listen(
this.queueManager_, 'itemsinserted', checkQueueItems);
this.eventManager_.listen(
this.queueManager_, 'itemsremoved', checkQueueItems);
this.eventManager_.listen(this.player_, 'loading', checkQueueItems);
const checkSkipAd = () => {
if (!this.ad_ || !this.ad_.isSkippable() || !this.ad_.canSkipNow()) {
addMediaSessionHandler('skipad', null);
} else {
addMediaSessionHandler('skipad', commonHandler);
}
};
this.eventManager_.listen(
this.adManager_, shaka.ads.Utils.AD_STARTED, checkSkipAd);
this.eventManager_.listen(
this.adManager_, shaka.ads.Utils.AD_SKIP_STATE_CHANGED, checkSkipAd);
this.eventManager_.listen(
this.adManager_, shaka.ads.Utils.AD_STOPPED, checkSkipAd);
}
/**
* @private
*/
removeMediaSession_() {
if (!this.config_.setupMediaSession || !navigator.mediaSession) {
return;
}
try {
navigator.mediaSession.setPositionState();
} catch (error) {}
const disableMediaSessionHandler = (type) => {
try {
navigator.mediaSession.setActionHandler(type, null);
} catch (error) {}
};
disableMediaSessionHandler('pause');
disableMediaSessionHandler('play');
disableMediaSessionHandler('seekbackward');
disableMediaSessionHandler('seekforward');
disableMediaSessionHandler('seekto');
disableMediaSessionHandler('stop');
disableMediaSessionHandler('enterpictureinpicture');
}
/**
* When a mobile device is rotated to landscape layout, and the video is
* loaded, make the demo app go into fullscreen.
@@ -2538,6 +2256,20 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
this.updateTimeAndSeekRange_();
}
/**
* @param {number} increment
*/
seekIncrement(increment) {
this.seek_(this.seekBar_.getValue() + increment);
}
/**
* @param {number} value
*/
seekTo(value) {
this.seek_(value);
}
/**
* Called when the seek range or current time need to be updated.
* @private
+46 -7
View File
@@ -173,6 +173,49 @@ shaka.extern.UIQualityMarks;
*/
shaka.extern.UIShortcuts;
/**
* @typedef {{
* enabled: boolean,
* handleMetadata: boolean,
* handleActions: boolean,
* handlePosition: boolean,
* supportedActions: !Array<string>,
* }}
*
* @property {boolean} enabled
* If true, MediaSession controls will be managed by the UI.
* <br>
* Defaults to <code>true</code>.
* @property {boolean} handleMetadata
* Setup MediaSession metadata from the following sources:
* <br>
* - ID3 with the `TIT2` tag for title
* <br>
* - ID3 with the `APIC` tag for image
* <br>
* - HLS with the `#EXT-X-SESSION-DATA` tag with the ID
* `com.apple.hls.title` for title
* <br>
* - HLS with the `#EXT-X-SESSION-DATA` tag with the ID
* `com.apple.hls.poster` for image
* <br>
* - DASH with `ProgramInformation` element and child `Title` field for title.
* <br>
* Defaults to <code>true</code>.
* @property {boolean} handleActions
* If true, MediaSession actions supported will be managed by the UI.
* <br>
* Defaults to <code>true</code>.
* @property {boolean} handlePosition
* If true, MediaSession position will be managed by the UI.
* <br>
* Defaults to <code>true</code>.
* @property {!Array<string>} supportedActions
* List of supported MediaSession actions.
* @exportDoc
*/
shaka.extern.UIMediaSession;
/**
* @description
* The UI's configuration options.
@@ -217,7 +260,6 @@ shaka.extern.UIShortcuts;
* refreshTickInSeconds: number,
* displayInVrMode: boolean,
* defaultVrProjectionMode: string,
* setupMediaSession: boolean,
* preferVideoFullScreenInVisionOS: boolean,
* showAudioCodec: boolean,
* showVideoCodec: boolean,
@@ -231,6 +273,7 @@ shaka.extern.UIShortcuts;
* enableVrDeviceMotion: boolean,
* showUIAlwaysOnAudioOnly: boolean,
* preferIntlDisplayNames: boolean,
* mediaSession: shaka.extern.UIMediaSession,
* }}
*
* @property {!Array<string>} controlPanelElements
@@ -435,12 +478,6 @@ shaka.extern.UIShortcuts;
* <code>'halfequirectangular'</code> or <code>'cubemap'</code>.
* <br>
* Defaults to <code>'equirectangular'</code>.
* @property {boolean} setupMediaSession
* If true, MediaSession controls will be managed by the UI. It will also use
* the ID3 APIC and TIT2 as image and title in Media Session, and ID3 APIC
* will also be used to change video poster.
* <br>
* Defaults to <code>true</code>.
* @property {boolean} preferVideoFullScreenInVisionOS
* If true, we will use the fullscreen API of the video element itself if it
* is available in Vision OS. This is useful to be able to access 3D
@@ -500,6 +537,8 @@ shaka.extern.UIShortcuts;
* is available.
* <br>
* Defaults to <code>true</code>.
* @property {shaka.extern.UIMediaSession} mediaSession
* Media Session config.
* @exportDoc
*/
shaka.extern.UIConfiguration;
+520
View File
@@ -0,0 +1,520 @@
/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.ui.MediaSession');
goog.require('shaka.log');
goog.require('shaka.ads.Utils');
goog.require('shaka.util.EventManager');
goog.require('shaka.util.IReleasable');
goog.require('shaka.util.TXml');
goog.requireType('shaka.Player');
goog.requireType('shaka.ui.Controls');
/**
* @export
* @implements {shaka.util.IReleasable}
*/
shaka.ui.MediaSession = class {
/**
* @param {!shaka.ui.Controls} controls
*/
constructor(controls) {
/** @private {!shaka.ui.Controls} */
this.controls_ = controls;
/** @private {shaka.Player} */
this.player_ = this.controls_.getPlayer();
/** @private {HTMLMediaElement} */
this.video_ = this.controls_.getVideo();
/** @private {shaka.extern.IAdManager} */
this.adManager_ = this.controls_.getAdManager();
/** @private {shaka.extern.IQueueManager} */
this.queueManager_ = this.controls_.getQueueManager();
/** @private {!shaka.extern.UIConfiguration} */
this.config_ = this.controls_.getConfig();
/** @private {shaka.util.EventManager} */
this.loadEventManager_ = new shaka.util.EventManager();
/** @private {shaka.util.EventManager} */
this.eventManager_ = new shaka.util.EventManager();
/** @private {boolean} */
this.supported_ = !!navigator.mediaSession;
/** @private {boolean} */
this.enabled_ = false;
/** @private {boolean} */
this.supportsChapterInfo_ =
// eslint-disable-next-line no-restricted-syntax
this.supported_ && 'chapterInfo' in MediaMetadata.prototype;
/** @private {!Set<string>} */
this.actionsHandled_ = new Set();
if (this.player_.isFullyLoaded()) {
this.init_();
}
this.loadEventManager_.listen(this.player_, 'loading', () => {
this.init_();
});
this.loadEventManager_.listen(this.player_, 'unloading', () => {
this.stop_();
});
}
/**
* @param {!shaka.extern.UIConfiguration} config
* @export
*/
configure(config) {
this.stop_();
this.config_ = config;
this.init_();
}
/**
* @override
* @export
*/
release() {
if (this.loadEventManager_) {
this.loadEventManager_.release();
this.loadEventManager_ = null;
}
if (this.eventManager_) {
this.eventManager_.release();
this.eventManager_ = null;
}
this.stop_();
}
/**
* @private
*/
stop_() {
if (!this.enabled_) {
return;
}
this.enabled_ = false;
if (this.eventManager_) {
this.eventManager_.removeAll();
}
if (this.config_.mediaSession.handleMetadata) {
navigator.mediaSession.metadata = new MediaMetadata({});
}
if (this.config_.mediaSession.handlePosition) {
this.clearPositionState_();
}
for (const actionName of Array.from(this.actionsHandled_)) {
this.addMediaSessionHandler(actionName);
}
this.actionsHandled_.clear();
}
/**
* @private
*/
init_() {
if (!this.supported_ || this.enabled_ ||
!this.config_.mediaSession.enabled) {
return;
}
this.enabled_ = true;
this.setupMediaSessionActions_();
this.setupMediaSessionMetadata_();
this.setupMediaSessionPosition_();
}
/**
* @return {!{title: string, artist: string, album: string,
* artwork: Object, chapterInfo: ?Object}}
*/
getMediaMetadata() {
const metadata = {
title: '',
artist: '',
album: '',
artwork: [],
};
if (this.supportsChapterInfo_) {
metadata.chapterInfo = [];
}
if (this.supported_ && navigator.mediaSession.metadata) {
metadata.title = navigator.mediaSession.metadata.title;
metadata.artist = navigator.mediaSession.metadata.artist;
metadata.album = navigator.mediaSession.metadata.album;
metadata.artwork = navigator.mediaSession.metadata.artwork;
if (this.supportsChapterInfo_) {
metadata.chapterInfo = navigator.mediaSession.metadata.chapterInfo;
}
}
return metadata;
}
/**
* @param {string} title
* @export
*/
setupTitle(title) {
const castReceiver = this.controls_.getCastReceiver();
if (castReceiver) {
castReceiver.setContentTitle(title);
}
if (this.supported_) {
const metadata = this.getMediaMetadata();
metadata.title = title;
navigator.mediaSession.metadata = new MediaMetadata(metadata);
}
}
/**
* @param {string} imageUrl
* @export
*/
setupPoster(imageUrl) {
const video = /** @type {HTMLVideoElement} */ (this.video_);
if (imageUrl != video.poster) {
video.poster = imageUrl;
}
const castReceiver = this.controls_.getCastReceiver();
if (castReceiver) {
castReceiver.setContentImage(imageUrl);
}
if (this.supported_) {
const metadata = this.getMediaMetadata();
metadata.artwork = [{src: imageUrl}];
navigator.mediaSession.metadata = new MediaMetadata(metadata);
}
}
/**
* @param {!Array<!shaka.extern.Chapter>} chapters
* @export
*/
setupChapters(chapters) {
if (!this.supportsChapterInfo_) {
return;
}
const chapterInfo = [];
for (const chapter of chapters) {
chapterInfo.push({
title: chapter.title,
startTime: chapter.startTime,
artwork: [],
});
}
const metadata = this.getMediaMetadata();
metadata.chapterInfo = chapterInfo;
navigator.mediaSession.metadata = new MediaMetadata(metadata);
}
/**
* @param {string} type
* @param {?Function=} callback
* @export
*/
addMediaSessionHandler(type, callback = null) {
if (!this.supported_) {
return;
}
try {
if (callback) {
if (!this.config_.mediaSession.supportedActions.includes(type)) {
return;
}
this.actionsHandled_.add(type);
} else {
if (!this.actionsHandled_.has(type)) {
return;
}
this.actionsHandled_.delete(type);
}
navigator.mediaSession.setActionHandler(type, callback);
} catch (error) {
shaka.log.debug(
`The "${type}" media session action is not supported.`);
}
}
/**
* @param {!{action: string, seekOffset: ?number,
* seekTime: ?number}} details
* @export
*/
commonActionHandler(details) {
const ad = this.controls_.getAd();
const keyboardSeekDistance = this.config_.keyboardSeekDistance;
switch (details.action) {
case 'pause':
this.controls_.playPausePresentation();
break;
case 'play':
this.controls_.playPausePresentation();
break;
case 'seekbackward':
if (details.seekOffset && !isFinite(details.seekOffset)) {
break;
}
if (!ad || !ad.isLinear()) {
this.controls_.seekIncrement(
-(details.seekOffset || keyboardSeekDistance));
}
break;
case 'seekforward':
if (details.seekOffset && !isFinite(details.seekOffset)) {
break;
}
if (!ad || !ad.isLinear()) {
this.controls_.seekIncrement(
details.seekOffset || keyboardSeekDistance);
}
break;
case 'seekto':
if (details.seekTime && !isFinite(details.seekTime)) {
break;
}
if (!ad || !ad.isLinear()) {
this.controls_.seekTo(
this.player_.seekRange().start + details.seekTime);
}
break;
case 'stop':
this.player_.unload();
break;
case 'enterpictureinpicture':
if (!ad || !ad.isLinear()) {
this.controls_.togglePiP();
}
break;
case 'nexttrack':
this.queueManager_.playItem(
this.queueManager_.getCurrentItemIndex() + 1);
break;
case 'previoustrack':
this.queueManager_.playItem(
this.queueManager_.getCurrentItemIndex() - 1);
break;
case 'skipad':
if (ad) {
ad.skip();
}
break;
}
};
/**
* @private
*/
clearPositionState_() {
try {
navigator.mediaSession.setPositionState();
} catch (error) {
shaka.log.v2(
'setPositionState in media session is not supported.');
}
}
/**
* @private
*/
setupMediaSessionMetadata_() {
if (!this.config_.mediaSession.handleMetadata) {
return;
}
this.eventManager_.listen(this.player_, 'trackschanged', () => {
this.setupChapters(this.controls_.getChapters());
});
this.eventManager_.listen(this.player_, 'metadata', (event) => {
const payload = event['payload'];
if (!payload) {
return;
}
let title;
if (payload['key'] == 'TIT2' && payload['data']) {
title = payload['data'];
}
let imageUrl;
if (payload['key'] == 'APIC' && payload['mimeType'] == '-->') {
imageUrl = payload['data'];
}
if (title) {
this.setupTitle(title);
}
if (imageUrl) {
this.setupPoster(imageUrl);
}
});
this.eventManager_.listen(this.player_, 'sessiondata', (event) => {
const id = event['id'];
switch (id) {
case 'com.apple.hls.title': {
const title = event['value'];
if (title) {
this.setupTitle(title);
}
break;
}
case 'com.apple.hls.poster': {
let imageUrl = event['value'];
if (imageUrl) {
imageUrl = imageUrl.replace('{w}', '512')
.replace('{h}', '512')
.replace('{f}', 'jpeg');
this.setupPoster(imageUrl);
}
break;
}
}
});
this.eventManager_.listen(this.player_, 'programinformation', (event) => {
if (!event['detail']) {
return;
}
const TXml = shaka.util.TXml;
/** @type {!shaka.extern.xml.Node} */
const detail =
/** @type {!shaka.extern.xml.Node} */(event['detail']);
const titleNode = TXml.findChild(detail, 'Title');
if (titleNode) {
const title = TXml.getContents(titleNode);
if (title) {
this.setupTitle(title);
}
}
});
this.eventManager_.listen(this.player_, 'unloading', () => {
navigator.mediaSession.metadata = new MediaMetadata({});
});
}
/**
* @private
*/
setupMediaSessionPosition_() {
if (!this.config_.mediaSession.handlePosition) {
return;
}
const updatePositionState = () => {
const ad = this.controls_.getAd();
if (ad && ad.isLinear()) {
this.clearPositionState_();
return;
}
const seekRange = this.player_.seekRange();
let duration = seekRange.end - seekRange.start;
const position = parseFloat(
(this.video_.currentTime - seekRange.start).toFixed(2));
if (this.player_.isLive() && Math.abs(duration - position) < 1) {
// Positive infinity indicates media without a defined end, such as a
// live stream.
duration = Infinity;
}
try {
navigator.mediaSession.setPositionState({
duration: Math.max(0, duration),
playbackRate: this.video_.playbackRate,
position: Math.max(0, position),
});
} catch (error) {
shaka.log.v2(
'setPositionState in media session is not supported.');
}
};
const playerLoaded = () => {
if (this.player_.isLive() || this.player_.seekRange().start != 0) {
updatePositionState();
this.eventManager_.listen(
this.video_, 'timeupdate', updatePositionState);
} else {
this.clearPositionState_();
}
};
if (this.player_.isFullyLoaded()) {
playerLoaded();
}
this.eventManager_.listen(
this.player_, 'loaded', playerLoaded);
this.eventManager_.listen(this.player_, 'unloading', () => {
this.eventManager_.unlisten(
this.video_, 'timeupdate', updatePositionState);
});
}
/** @private */
setupMediaSessionActions_() {
if (!this.config_.mediaSession.handleActions) {
return;
}
const actionHandler = (details) => {
this.commonActionHandler(details);
};
this.addMediaSessionHandler('pause', actionHandler);
this.addMediaSessionHandler('play', actionHandler);
this.addMediaSessionHandler('seekbackward', actionHandler);
this.addMediaSessionHandler('seekforward', actionHandler);
this.addMediaSessionHandler('seekto', actionHandler);
this.addMediaSessionHandler('stop', actionHandler);
this.addMediaSessionHandler('enterpictureinpicture', actionHandler);
const checkQueueItems = () => {
const itemsLength = this.queueManager_.getItems().length;
const currentIndex = this.queueManager_.getCurrentItemIndex();
if (itemsLength <= 1 || currentIndex == -1) {
this.addMediaSessionHandler('previoustrack', null);
this.addMediaSessionHandler('nexttrack', null);
return;
}
if (currentIndex > 0) {
this.addMediaSessionHandler('previoustrack', actionHandler);
} else {
this.addMediaSessionHandler('previoustrack', null);
}
if ((currentIndex + 1) < itemsLength) {
this.addMediaSessionHandler('nexttrack', actionHandler);
} else {
this.addMediaSessionHandler('nexttrack', null);
}
};
this.eventManager_.listen(
this.queueManager_, 'currentitemchanged', checkQueueItems);
this.eventManager_.listen(
this.queueManager_, 'itemsinserted', checkQueueItems);
this.eventManager_.listen(
this.queueManager_, 'itemsremoved', checkQueueItems);
this.eventManager_.listen(
this.player_, 'loading', checkQueueItems);
checkQueueItems();
const checkSkipAd = () => {
const ad = this.controls_.getAd();
if (!ad || !ad.isSkippable() || !ad.canSkipNow()) {
this.addMediaSessionHandler('skipad', null);
} else {
this.addMediaSessionHandler('skipad', actionHandler);
}
};
this.eventManager_.listen(
this.adManager_, shaka.ads.Utils.AD_STARTED, checkSkipAd);
this.eventManager_.listen(
this.adManager_, shaka.ads.Utils.AD_SKIP_STATE_CHANGED, checkSkipAd);
this.eventManager_.listen(
this.adManager_, shaka.ads.Utils.AD_STOPPED, checkSkipAd);
checkSkipAd();
}
};
+1 -1
View File
@@ -30,7 +30,7 @@ shaka.ui.SkipNextButton = class extends shaka.ui.Element {
constructor(parent, controls) {
super(parent, controls);
this.queueManager_ = this.player.getQueueManager();
this.queueManager_ = this.controls.getQueueManager();
if (!this.queueManager_) {
return;
+1 -1
View File
@@ -30,7 +30,7 @@ shaka.ui.SkipPreviousButton = class extends shaka.ui.Element {
constructor(parent, controls) {
super(parent, controls);
this.queueManager_ = this.player.getQueueManager();
this.queueManager_ = this.controls.getQueueManager();
if (!this.queueManager_) {
return;
+23 -1
View File
@@ -286,6 +286,22 @@ shaka.ui.Overlay = class {
}
controlPanelElements.push('fullscreen');
const mediaSessionActions = [
'pause',
'play',
'seekbackward',
'seekforward',
'seekto',
'stop',
'skipad',
'previoustrack',
'nexttrack',
];
if ('documentPictureInPicture' in window ||
document.pictureInPictureEnabled) {
mediaSessionActions.push('enterpictureinpicture');
}
const config = {
controlPanelElements,
topControlPanelElements: [
@@ -392,7 +408,6 @@ shaka.ui.Overlay = class {
refreshTickInSeconds: 0.125,
displayInVrMode: false,
defaultVrProjectionMode: 'equirectangular',
setupMediaSession: true,
preferVideoFullScreenInVisionOS: true,
showAudioCodec: true,
showVideoCodec: true,
@@ -423,6 +438,13 @@ shaka.ui.Overlay = class {
enableVrDeviceMotion: true,
showUIAlwaysOnAudioOnly: true,
preferIntlDisplayNames: true,
mediaSession: {
enabled: true,
handleMetadata: true,
handleActions: true,
handlePosition: true,
supportedActions: mediaSessionActions,
},
};
// On mobile, by default, hide the volume slide and the small play/pause