feat(Cast): Support getCurrentAd while casting (#9306)

This commit is contained in:
Álvaro Velad Galván
2025-10-28 13:25:57 +01:00
committed by GitHub
parent a5c2c4f6ea
commit 02a2bb54d0
10 changed files with 182 additions and 109 deletions
-1
View File
@@ -3,7 +3,6 @@
+../../ui/ad_info.js
+../../ui/ad_statistics_button.js
+../../ui/audio_language_selection.js
+../../ui/basic_ad.js
+../../ui/content_title.js
+../../ui/externs/ui.js
+../../ui/externs/watermark.js
+52 -4
View File
@@ -8,6 +8,7 @@ goog.provide('shaka.cast.CastProxy');
goog.require('goog.asserts');
goog.require('shaka.Player');
goog.require('shaka.ads.AbstractAd');
goog.require('shaka.cast.CastSender');
goog.require('shaka.cast.CastUtils');
goog.require('shaka.device.DeviceFactory');
@@ -71,6 +72,9 @@ shaka.cast.CastProxy = class extends shaka.util.FakeEventTarget {
/** @private {Object} */
this.adManagerProxy_ = null;
/** @private {Object} */
this.currentAdProxy_ = null;
/** @private {shaka.util.FakeEventTarget} */
this.videoEventTarget_ = null;
@@ -156,6 +160,7 @@ shaka.cast.CastProxy = class extends shaka.util.FakeEventTarget {
this.videoProxy_ = null;
this.playerProxy_ = null;
this.adManagerProxy_ = null;
this.currentAdProxy_ = null;
// FakeEventTarget implements IReleasable
super.release();
@@ -494,8 +499,8 @@ shaka.cast.CastProxy = class extends shaka.util.FakeEventTarget {
}
/**
* Iterates over all of the methods of the player, including inherited methods
* from FakeEventTarget.
* Iterates over all of the methods of the ad manager, including inherited
* methods from FakeEventTarget.
* @param {function(string, function())} operation
* @private
*/
@@ -505,6 +510,16 @@ shaka.cast.CastProxy = class extends shaka.util.FakeEventTarget {
this.iterateOverClassMethods_(operation, adManager);
}
/**
* Iterates over all of the methods of the current ad.
* @param {function(string, function())} operation
* @private
*/
iterateOverCurrentAdMethods_(operation) {
const ad = /** @type {!Object} */ (new shaka.cast.BasicAd());
this.iterateOverClassMethods_(operation, ad);
}
/**
* Iterates over all of the methods of an Object, including inherited methods
* from FakeEventTarget.
@@ -948,11 +963,10 @@ shaka.cast.CastProxy = class extends shaka.util.FakeEventTarget {
/**
* @param {string} name
* @param {boolean} dontRecordCalls
* @return {?}
* @private
*/
adManagerProxyGet_(name, dontRecordCalls = false) {
adManagerProxyGet_(name) {
// If name is a shortened compiled name, get the original version
// from our map.
if (this.adManagerCompiledToExternNames_.has(name)) {
@@ -981,6 +995,10 @@ shaka.cast.CastProxy = class extends shaka.util.FakeEventTarget {
return value.bind(this.localAdManager_);
}
if (name == 'getCurrentAd') {
return this.currentAdProxy_;
}
return this.sender_.get('adManager', name);
}
@@ -997,6 +1015,20 @@ shaka.cast.CastProxy = class extends shaka.util.FakeEventTarget {
this.adManagerEventTarget_.dispatchEvent(event);
}
/**
* @param {string} name
* @return {?}
* @private
*/
currentAdProxyGet_(name) {
// This function should not be called if we are not casting.
if (!this.sender_.isCasting()) {
return null;
}
return this.sender_.get('currentAd', name);
}
/**
* @param {string} targetName
* @param {!shaka.util.FakeEvent} event
@@ -1018,6 +1050,20 @@ shaka.cast.CastProxy = class extends shaka.util.FakeEventTarget {
}
this.playerEventTarget_.dispatchEvent(event);
} else if (targetName == 'adManager') {
if (event.type == 'ad-started') {
this.currentAdProxy_ = {};
this.iterateOverCurrentAdMethods_((name, method) => {
goog.asserts.assert(this.currentAdProxy_,
'Must have current ad proxy!');
Object.defineProperty(this.currentAdProxy_, name, {
configurable: false,
enumerable: true,
get: () => this.currentAdProxyGet_(name),
});
});
} else if (event.type == 'ad-stopped') {
this.currentAdProxy_ = null;
}
this.adManagerEventTarget_.dispatchEvent(event);
}
}
@@ -1046,3 +1092,5 @@ shaka.cast.CastProxy = class extends shaka.util.FakeEventTarget {
return true;
}
};
shaka.cast.BasicAd = class extends shaka.ads.AbstractAd {};
+30 -6
View File
@@ -52,6 +52,9 @@ shaka.cast.CastReceiver = class extends shaka.util.FakeEventTarget {
/** @private {shaka.extern.IAdManager} */
this.adManager_ = player.getAdManager();
/** @private {?shaka.extern.IAd} */
this.currentAd_ = null;
/** @private {shaka.util.EventManager} */
this.eventManager_ = new shaka.util.EventManager();
@@ -60,6 +63,7 @@ shaka.cast.CastReceiver = class extends shaka.util.FakeEventTarget {
'video': video,
'player': player,
'adManager': this.adManager_,
'currentAd': this.currentAd_,
};
/** @private {?function(Object)} */
@@ -299,9 +303,16 @@ shaka.cast.CastReceiver = class extends shaka.util.FakeEventTarget {
if (this.adManager_) {
for (const name of shaka.cast.CastUtils.AdManagerEvents) {
this.eventManager_.listen(
this.adManager_, name,
(event) => this.proxyEvent_('adManager', event));
this.eventManager_.listen(this.adManager_, name, (event) => {
if (event.type == 'ad-started') {
this.currentAd_ = this.adManager_.getCurrentAd();
// Reset update message frequency values after new ad.
this.updateNumber_ = 0;
} else if (event.type == 'ad-stopped') {
this.currentAd_ = null;
}
this.proxyEvent_('adManager', event);
});
}
}
@@ -521,6 +532,15 @@ shaka.cast.CastReceiver = class extends shaka.util.FakeEventTarget {
}
});
if (this.currentAd_) {
shaka.cast.CastUtils.CurrentAdGetterMethods.forEach((frequency, name) => {
if (this.updateNumber_ % frequency == 0) {
update['currentAd'][name] =
/** @type {Object} */ (this.currentAd_)[name]();
}
});
}
// Volume attributes are tied to the system volume.
const manager = cast.receiver.CastReceiverManager.getInstance();
const systemVolume = manager.getSystemVolume();
@@ -695,7 +715,9 @@ shaka.cast.CastReceiver = class extends shaka.util.FakeEventTarget {
}
}
this.targets_[targetName][property] = value;
if (this.targets_[targetName]) {
this.targets_[targetName][property] = value;
}
break;
}
case 'call': {
@@ -703,8 +725,10 @@ shaka.cast.CastReceiver = class extends shaka.util.FakeEventTarget {
const methodName = message['methodName'];
const args = message['args'];
const target = this.targets_[targetName];
// eslint-disable-next-line prefer-spread
target[methodName].apply(target, args);
if (target) {
// eslint-disable-next-line prefer-spread
target[methodName].apply(target, args);
}
break;
}
case 'asyncCall': {
+7
View File
@@ -383,6 +383,13 @@ shaka.cast.CastSender = class {
if (CastUtils.LargeAdManagerGetterMethods.has(property)) {
return () => this.propertyGetter_(targetName, property);
}
} else if (targetName == 'currentAd') {
if (CastUtils.CurrentAdVoidMethods.includes(property)) {
return (...args) => this.remoteCall_(targetName, property, ...args);
}
if (CastUtils.CurrentAdGetterMethods.has(property)) {
return () => this.propertyGetter_(targetName, property);
}
}
return this.propertyGetter_(targetName, property);
+51
View File
@@ -529,6 +529,57 @@ shaka.cast.CastUtils.AdManagerEvents = [
];
/**
* Current ad getter methods that are proxied while casting.
* The key is the method, the value is the frequency of updates.
* Frequency 1 translates to every update; frequency 2 to every 2 updates, etc.
* @const {!Map<string, number>}
*/
shaka.cast.CastUtils.CurrentAdGetterMethods = new Map()
.set('needsSkipUI', 20)
.set('isClientRendering', 20)
.set('hasCustomClick', 20)
.set('isUsingAnotherMediaElement', 20)
.set('getDuration', 1)
.set('getMinSuggestedDuration', 1)
.set('getRemainingTime', 1)
.set('getTimeUntilSkippable', 1)
.set('isPaused', 1)
.set('isSkippable', 1)
.set('canSkipNow', 1)
.set('getVolume', 1)
.set('isMuted', 1)
.set('isLinear', 20)
.set('getSequenceLength', 20)
.set('getPositionInSequence', 20)
.set('getTitle', 20)
.set('getDescription', 20)
.set('getVastMediaBitrate', 20)
.set('getVastMediaHeight', 20)
.set('getVastMediaWidth', 20)
.set('getVastAdId', 20)
.set('getAdId', 20)
.set('getCreativeAdId', 20)
.set('getAdvertiserName', 20)
.set('getMediaUrl', 20)
.set('getTimeOffset', 20)
.set('getPodIndex', 20);
/**
* Current ad methods with no return value that are proxied while casting.
* @const {!Array<string>}
*/
shaka.cast.CastUtils.CurrentAdVoidMethods = [
'skip',
'play',
'pause',
'setVolume',
'setMuted',
'resize',
];
/**
* @typedef {{
* video: Object,
+28
View File
@@ -113,6 +113,34 @@ describe('CastUtils', () => {
.toEqual([]);
});
it('includes every Ad member', () => {
const ignoredMembers = [
'constructor', // JavaScript added field
'release', // Handled by AdManager
];
const castMembers = CastUtils.CurrentAdVoidMethods
.concat(Array.from(CastUtils.CurrentAdGetterMethods.keys()));
const allAdMembers =
// eslint-disable-next-line no-restricted-syntax
Object.getOwnPropertyNames(shaka.ads.AbstractAd.prototype);
expect(
ignoredMembers.filter((member) => !allAdMembers.includes(member)))
.toEqual([]);
const adMembers = allAdMembers.filter((name) => {
// Private members end with _.
return !ignoredMembers.includes(name) && !name.endsWith('_');
});
// To make debugging easier, don't check that they are equal; instead check
// that neither has any extra entries.
expect(castMembers.filter((name) => !adMembers.includes(name)))
.toEqual([]);
expect(adMembers.filter((name) => !castMembers.includes(name)))
.toEqual([]);
});
describe('serialize/deserialize', () => {
it('transfers infinite values and NaN', () => {
const orig = {
+8 -1
View File
@@ -14,6 +14,9 @@ shaka.test.FakeAdManager = class extends shaka.util.FakeEventTarget {
/** @private {shaka.ads.AdsStats} */
this.stats_ = new shaka.ads.AdsStats();
/** @private {?shaka.extern.IAd} */
this.currentAd_ = null;
}
/** @override */
@@ -102,12 +105,15 @@ shaka.test.FakeAdManager = class extends shaka.util.FakeEventTarget {
getInterstitialPlayer() {}
/** @override */
getCurrentAd() {}
getCurrentAd() {
return this.currentAd_;
}
/**
* @param {!shaka.test.FakeAd} ad
*/
startAd(ad) {
this.currentAd_ = ad;
const event = new shaka.util.FakeEvent(shaka.ads.Utils.AD_STARTED,
(new Map()).set('ad', ad));
@@ -116,6 +122,7 @@ shaka.test.FakeAdManager = class extends shaka.util.FakeEventTarget {
/** @public */
finishAd() {
this.currentAd_ = null;
const event = new shaka.util.FakeEvent(shaka.ads.Utils.AD_STOPPED);
this.dispatchEvent(event);
}
-67
View File
@@ -1,67 +0,0 @@
/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.ui.BasicAd');
goog.require('shaka.ads.AbstractAd');
shaka.ui.BasicAd = class extends shaka.ads.AbstractAd {
/**
* @param {HTMLMediaElement} video
* @param {?number} startTime
* @param {?number} endTime
*/
constructor(video, startTime, endTime) {
super(video);
/** @private {?number} */
this.startTime_ = startTime;
/** @private {?number} */
this.endTime_ = endTime;
/** @private {boolean} */
this.isLinear_ = this.startTime_ != null;
}
/**
* @override
*/
getDuration() {
if (this.endTime_ == null || this.startTime_ == null) {
return -1;
}
return this.endTime_ - this.startTime_;
}
/**
* @override
*/
getRemainingTime() {
if (this.endTime_ == null) {
return -1;
}
return this.endTime_ - this.video.currentTime;
}
/**
* @override
*/
isLinear() {
return this.isLinear_;
}
/**
* @override
*/
getTimeOffset() {
if (this.startTime_ == null) {
return 0;
}
return this.startTime_;
}
};
+3 -15
View File
@@ -15,7 +15,6 @@ goog.require('shaka.device.DeviceFactory');
goog.require('shaka.device.IDevice');
goog.require('shaka.log');
goog.require('shaka.ui.AdInfo');
goog.require('shaka.ui.BasicAd');
goog.require('shaka.ui.BigPlayButton');
goog.require('shaka.ui.ContextMenu');
goog.require('shaka.ui.HiddenFastForwardButton');
@@ -178,7 +177,7 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
this.queueManager_.setCustomPlayer(this.player_);
/** @private {?shaka.extern.IAd} */
this.ad_ = null;
this.ad_ = this.adManager_.getCurrentAd();
/** @private {!Array<!shaka.extern.AdCuePoint>} */
this.adCuePoints_ = [];
@@ -1523,19 +1522,8 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
});
this.eventManager_.listen(
this.adManager_, shaka.ads.Utils.AD_STARTED, (e) => {
this.ad_ = (/** @type {!Object} */ (e))['ad'];
if (!this.ad_) {
const currentTime = this.video_.currentTime;
const cuePoint = this.adCuePoints_.find((c) => {
return currentTime >= c.start &&
currentTime <= (c.end || Infinity);
});
const start = cuePoint ? cuePoint.start : null;
const end = cuePoint ? cuePoint.end : null;
// Note: We assume the ad uses the base video.
this.ad_ = new shaka.ui.BasicAd(this.video_, start, end);
}
this.adManager_, shaka.ads.Utils.AD_STARTED, () => {
this.ad_ = this.adManager_.getCurrentAd();
this.showAdUI();
this.onBufferingStateChange_();
});
+3 -15
View File
@@ -8,7 +8,6 @@
goog.provide('shaka.ui.Element');
goog.require('shaka.ads.Utils');
goog.require('shaka.ui.BasicAd');
goog.require('shaka.util.EventManager');
goog.requireType('shaka.Player');
goog.requireType('shaka.ui.Controls');
@@ -72,22 +71,11 @@ shaka.ui.Element = class {
* @protected {?shaka.extern.IAd}
* @exportInterface
*/
this.ad = controls.getAd();
this.ad = this.adManager.getCurrentAd(); ;
const AD_STARTED = shaka.ads.Utils.AD_STARTED;
this.eventManager.listen(this.adManager, AD_STARTED, (e) => {
this.ad = (/** @type {!Object} */ (e))['ad'];
if (!this.ad) {
const currentTime = this.video.currentTime;
const cuePoint = this.controls.getAdCuePoints().find((c) => {
return currentTime >= c.start &&
currentTime <= (c.end || Infinity);
});
const start = cuePoint ? cuePoint.start : null;
const end = cuePoint ? cuePoint.end : null;
// Note: We assume the ad uses the base video.
this.ad = new shaka.ui.BasicAd(this.video, start, end);
}
this.eventManager.listen(this.adManager, AD_STARTED, () => {
this.ad = this.adManager.getCurrentAd();
});
const AD_STOPPED = shaka.ads.Utils.AD_STOPPED;