diff --git a/build/types/ui b/build/types/ui index 78c69f394..b2cd62c6e 100644 --- a/build/types/ui +++ b/build/types/ui @@ -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 diff --git a/lib/cast/cast_proxy.js b/lib/cast/cast_proxy.js index 4b341ffb4..31732dd04 100644 --- a/lib/cast/cast_proxy.js +++ b/lib/cast/cast_proxy.js @@ -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 {}; diff --git a/lib/cast/cast_receiver.js b/lib/cast/cast_receiver.js index deca2edfd..ca69c1891 100644 --- a/lib/cast/cast_receiver.js +++ b/lib/cast/cast_receiver.js @@ -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': { diff --git a/lib/cast/cast_sender.js b/lib/cast/cast_sender.js index f885d0c02..bb79ac2d4 100644 --- a/lib/cast/cast_sender.js +++ b/lib/cast/cast_sender.js @@ -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); diff --git a/lib/cast/cast_utils.js b/lib/cast/cast_utils.js index 734a516b3..114cedf5e 100644 --- a/lib/cast/cast_utils.js +++ b/lib/cast/cast_utils.js @@ -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} + */ +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} + */ +shaka.cast.CastUtils.CurrentAdVoidMethods = [ + 'skip', + 'play', + 'pause', + 'setVolume', + 'setMuted', + 'resize', +]; + + /** * @typedef {{ * video: Object, diff --git a/test/cast/cast_utils_unit.js b/test/cast/cast_utils_unit.js index 194c3018f..0212f600b 100644 --- a/test/cast/cast_utils_unit.js +++ b/test/cast/cast_utils_unit.js @@ -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 = { diff --git a/test/test/util/fake_ad_manager.js b/test/test/util/fake_ad_manager.js index e8369d789..18c54d462 100644 --- a/test/test/util/fake_ad_manager.js +++ b/test/test/util/fake_ad_manager.js @@ -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); } diff --git a/ui/basic_ad.js b/ui/basic_ad.js deleted file mode 100644 index 231ec50d5..000000000 --- a/ui/basic_ad.js +++ /dev/null @@ -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_; - } -}; diff --git a/ui/controls.js b/ui/controls.js index f7de5c535..797841ff6 100644 --- a/ui/controls.js +++ b/ui/controls.js @@ -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} */ 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_(); }); diff --git a/ui/element.js b/ui/element.js index 94bf7779e..5ed6f3a83 100644 --- a/ui/element.js +++ b/ui/element.js @@ -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;