From 70257ffefe609dfa0bb8888f91c2ce1d52c78f9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Velad=20Galv=C3=A1n?= Date: Mon, 25 Nov 2024 12:30:33 +0100 Subject: [PATCH] feat(Ads): Add support for overlay interstitials (or non-linear ads) (#7657) --- build/conformance.textproto | 1 + docs/tutorials/ad_monetization.md | 50 ++++++++++++++++++++++++++++++ externs/shaka/ads.js | 33 +++++++++++++++++++- lib/ads/ad_utils.js | 2 ++ lib/ads/interstitial_ad.js | 8 +++-- lib/ads/interstitial_ad_manager.js | 41 ++++++++++++++++++++---- 6 files changed, 126 insertions(+), 9 deletions(-) diff --git a/build/conformance.textproto b/build/conformance.textproto index 9c4d33ef5..2ba63839e 100644 --- a/build/conformance.textproto +++ b/build/conformance.textproto @@ -223,6 +223,7 @@ requirement: { "use \"left\" and \"top\" instead." # The built-in set and map polyfills trigger this ban somehow. whitelist_regexp: "synthetic:es6/" + whitelist_regexp: "lib/ads/interstitial_ad_manager.js" } # Disallow fdescribe. diff --git a/docs/tutorials/ad_monetization.md b/docs/tutorials/ad_monetization.md index 76032145b..b81d5c872 100644 --- a/docs/tutorials/ad_monetization.md +++ b/docs/tutorials/ad_monetization.md @@ -110,6 +110,8 @@ adManager.addCustomInterstitial({ pre: false, post: false, timelineRange: false, + loop: false, + overlay: null, }); ``` @@ -144,11 +146,59 @@ player.addEventListener('timelineregionadded', (e) => { pre: false, post: false, timelineRange: player.isLive(), // If true, the ad will appear as a range on the timeline. + loop: false, + overlay: null }); }); ``` +##### Custom Overlay Interstitials + +Example: + +```js +const adManager = player.getAdManager(); +const video = document.getElementById('video'); +const ui = video['ui']; +// If you're using a non-UI build, this is the div you'll need to create +// for your layout. The ad manager will clear this div, when it unloads, so +// don't pass in a div that contains non-ad elements. +const container = video.ui.getControls().getClientSideAdContainer(); +adManager.initInterstitial(container, player, video); +adManager.addCustomInterstitial({ + id: null, + startTime: 10, + endTime: null, + uri: 'YOUR_URL', + isSkippable: true, + skipOffset: 10, + canJump: false, + resumeOffset: null, + playoutLimit: null, + once: true, + pre: false, + post: false, + timelineRange: false, + loop: false, + overlay: { // Show interstitial in upper right quadrant + viewport: { + x: 1920, // Pixels + y: 1080, // Pixels + }, + topLeft: { + x: 960, // Pixels + y: 0, // Pixels + }, + size: { + x: 960, // Pixels + y: 540, // Pixels + }, + }, +}); +``` + + ##### VAST/VMAP (playback without tracking) Example: diff --git a/externs/shaka/ads.js b/externs/shaka/ads.js index bb99c148c..646371439 100644 --- a/externs/shaka/ads.js +++ b/externs/shaka/ads.js @@ -72,7 +72,9 @@ shaka.extern.AdCuePoint; * once: boolean, * pre: boolean, * post: boolean, - * timelineRange: boolean + * timelineRange: boolean, + * loop: boolean, + * overlay: ?shaka.extern.AdInterstitialOverlay * }} * * @description @@ -116,10 +118,39 @@ shaka.extern.AdCuePoint; * @property {boolean} timelineRange * Indicates whether the interstitial should be presented in a timeline UI * as a single point or as a range. + * @property {boolean} loop + * Indicates that the interstitials should play in loop. + * Only applies if the interstitials is an overlay. + * Only supported when using multiple video elements for interstitials. + * @property {?shaka.extern.AdInterstitialOverlay} overlay + * Indicates the characteristics of the overlay + * Only supported when using multiple video elements for interstitials. + * @exportDoc */ shaka.extern.AdInterstitial; +/** + * @typedef {{ + * viewport: {x: number, y: number}, + * topLeft: {x: number, y: number}, + * size: {x: number, y: number} + * }} + * + * @description + * Contains the ad interstitial overlay info. + * + * @property {{x: number, y: number}} viewport + * The viewport in pixels. + * @property {{x: number, y: number}} topLeft + * The topLeft in pixels. + * @property {{x: number, y: number}} size + * The size in pixels. + * @exportDoc + */ +shaka.extern.AdInterstitialOverlay; + + /** * An object that's responsible for all the ad-related logic * in the player. diff --git a/lib/ads/ad_utils.js b/lib/ads/ad_utils.js index 27d0e431a..aa1284711 100644 --- a/lib/ads/ad_utils.js +++ b/lib/ads/ad_utils.js @@ -90,6 +90,8 @@ shaka.ads.Utils = class { pre: currentTime == null, post: currentTime == Infinity, timelineRange: false, + loop: false, + overlay: null, }); break; } diff --git a/lib/ads/interstitial_ad.js b/lib/ads/interstitial_ad.js index ed212334f..92f06fabb 100755 --- a/lib/ads/interstitial_ad.js +++ b/lib/ads/interstitial_ad.js @@ -21,9 +21,10 @@ shaka.ads.InterstitialAd = class { * @param {number} sequenceLength * @param {number} adPosition * @param {boolean} isUsingAnotherMediaElement + * @param {?shaka.extern.AdInterstitialOverlay} overlay */ constructor(video, isSkippable, skipOffset, skipFor, onSkip, - sequenceLength, adPosition, isUsingAnotherMediaElement) { + sequenceLength, adPosition, isUsingAnotherMediaElement, overlay) { /** @private {HTMLMediaElement} */ this.video_ = video; @@ -47,6 +48,9 @@ shaka.ads.InterstitialAd = class { /** @private {boolean} */ this.isUsingAnotherMediaElement_ = isUsingAnotherMediaElement; + + /** @private {?shaka.extern.AdInterstitialOverlay} */ + this.overlay_ = overlay; } /** @@ -201,7 +205,7 @@ shaka.ads.InterstitialAd = class { * @export */ isLinear() { - return true; + return this.overlay_ == null; } /** diff --git a/lib/ads/interstitial_ad_manager.js b/lib/ads/interstitial_ad_manager.js index c021f485b..344daf714 100644 --- a/lib/ads/interstitial_ad_manager.js +++ b/lib/ads/interstitial_ad_manager.js @@ -253,6 +253,13 @@ shaka.ads.InterstitialAdManager = class { this.playingAd_ = false; this.lastTime_ = null; this.lastPlayedAd_ = null; + if (!this.usingBaseVideo_) { + this.video_.loop = false; + this.video_.style.height = ''; + this.video_.style.left = ''; + this.video_.style.top = ''; + this.video_.style.width = ''; + } } /** @override */ @@ -336,6 +343,8 @@ shaka.ads.InterstitialAdManager = class { pre: false, post: false, timelineRange: isReplace && !isInsert, + loop: false, + overlay: null, }; this.addInterstitials([interstitial]); } @@ -548,10 +557,26 @@ shaka.ads.InterstitialAdManager = class { await detachBasePlayerPromise; } if (!this.usingBaseVideo_) { - this.baseVideo_.pause(); - if (interstitial.resumeOffset != null && - interstitial.resumeOffset != 0) { - this.baseVideo_.currentTime += interstitial.resumeOffset; + if (interstitial.overlay) { + this.video_.loop = interstitial.loop; + const viewport = interstitial.overlay.viewport; + const topLeft = interstitial.overlay.topLeft; + const size = interstitial.overlay.size; + this.video_.style.height = (size.y / viewport.y * 100) + '%'; + this.video_.style.left = (topLeft.x / viewport.x * 100) + '%'; + this.video_.style.top = (topLeft.y / viewport.y * 100) + '%'; + this.video_.style.width = (size.x / viewport.x * 100) + '%'; + } else { + this.baseVideo_.pause(); + if (interstitial.resumeOffset != null && + interstitial.resumeOffset != 0) { + this.baseVideo_.currentTime += interstitial.resumeOffset; + } + this.video_.loop = false; + this.video_.style.height = ''; + this.video_.style.left = ''; + this.video_.style.top = ''; + this.video_.style.width = ''; } } @@ -560,7 +585,7 @@ shaka.ads.InterstitialAdManager = class { let playoutLimitTimer = null; const updateBaseVideoTime = () => { - if (!this.usingBaseVideo_) { + if (!this.usingBaseVideo_ && !interstitial.overlay) { if (interstitial.resumeOffset == null) { if (interstitial.timelineRange && interstitial.endTime && interstitial.endTime != Infinity) { @@ -658,7 +683,7 @@ shaka.ads.InterstitialAdManager = class { const ad = new shaka.ads.InterstitialAd(this.video_, interstitial.isSkippable, interstitial.skipOffset, interstitial.skipFor, onSkip, sequenceLength, adPosition, - !this.usingBaseVideo_); + !this.usingBaseVideo_, interstitial.overlay); if (!this.usingBaseVideo_) { ad.setMuted(this.baseVideo_.muted); ad.setVolume(this.baseVideo_.volume); @@ -897,6 +922,8 @@ shaka.ads.InterstitialAdManager = class { pre, post, timelineRange, + loop: false, + overlay: null, }); } else if (assetList) { const uri = /** @type {string} */(assetList.data); @@ -947,6 +974,8 @@ shaka.ads.InterstitialAdManager = class { pre, post, timelineRange, + loop: false, + overlay: null, }); } }