Skip to content

Commit

Permalink
feat(Ads): New HLS interstitial DATERANGE attributes for Skip Button (#…
Browse files Browse the repository at this point in the history
…7467)

Close: #7466
  • Loading branch information
avelad authored Oct 23, 2024
1 parent ba36958 commit 3107de3
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 22 deletions.
4 changes: 4 additions & 0 deletions externs/shaka/ads.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ shaka.extern.AdCuePoint;
* uri: string,
* isSkippable: boolean,
* skipOffset: ?number,
* skipFor: ?number,
* canJump: boolean,
* resumeOffset: ?number,
* playoutLimit: ?number,
Expand All @@ -91,6 +92,9 @@ shaka.extern.AdCuePoint;
* @property {?number} skipOffset
* Time value that identifies when skip controls are made available to the
* end user.
* @property {?number} skipFor
* The amount of time in seconds a skip button should be displayed for.
* Note that this value should be >= 0.
* @property {boolean} canJump
* Indicate if the interstitial is jumpable.
* @property {?number} resumeOffset
Expand Down
1 change: 1 addition & 0 deletions lib/ads/ad_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ shaka.ads.Utils = class {
uri: adUrl,
isSkippable: skipOffset != null,
skipOffset,
skipFor: null,
canJump: false,
resumeOffset: 0,
playoutLimit: null,
Expand Down
15 changes: 12 additions & 3 deletions lib/ads/interstitial_ad.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ shaka.ads.InterstitialAd = class {
* @param {HTMLMediaElement} video
* @param {boolean} isSkippable
* @param {?number} skipOffset
* @param {?number} skipFor
* @param {function()} onSkip
* @param {number} sequenceLength
* @param {number} adPosition
*/
constructor(video, isSkippable, skipOffset, onSkip,
constructor(video, isSkippable, skipOffset, skipFor, onSkip,
sequenceLength, adPosition) {
/** @private {HTMLMediaElement} */
this.video_ = video;
Expand All @@ -29,7 +30,10 @@ shaka.ads.InterstitialAd = class {
this.isSkippable_ = isSkippable;

/** @private {?number} */
this.skipOffset_ = skipOffset;
this.skipOffset_ = isSkippable ? skipOffset || 0 : skipOffset;

/** @private {?number} */
this.skipFor_ = skipFor;

/** @private {function()} */
this.onSkip_ = onSkip;
Expand Down Expand Up @@ -94,6 +98,11 @@ shaka.ads.InterstitialAd = class {
* @export
*/
isSkippable() {
if (this.isSkippable_ && this.skipFor_ != null) {
const position = this.getDuration() - this.getRemainingTime();
const maxTime = this.skipOffset_ + this.skipFor_;
return position < maxTime;
}
return this.isSkippable_;
}

Expand All @@ -102,7 +111,7 @@ shaka.ads.InterstitialAd = class {
* @export
*/
getTimeUntilSkippable() {
if (this.isSkippable_) {
if (this.isSkippable()) {
const canSkipIn =
this.getRemainingTime() + this.skipOffset_ - this.getDuration();
return Math.max(canSkipIn, 0);
Expand Down
74 changes: 66 additions & 8 deletions lib/ads/interstitial_ad_manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -274,13 +274,16 @@ shaka.ads.InterstitialAdManager = class {
shaka.log.warning('Unsupported MPD alternate', region);
return;
}

/** @type {!shaka.extern.AdInterstitial} */
const interstitial = {
id: null,
startTime: region.startTime,
endTime: isReplace ? region.endTime : null,
uri: alternativeMPDUri,
isSkippable: false,
skipOffset: null,
skipFor: null,
canJump: true,
resumeOffset: isInsert ? 0 : null,
playoutLimit: null,
Expand Down Expand Up @@ -610,7 +613,7 @@ shaka.ads.InterstitialAdManager = class {

const ad = new shaka.ads.InterstitialAd(this.video_,
interstitial.isSkippable, interstitial.skipOffset,
onSkip, sequenceLength, adPosition);
interstitial.skipFor, onSkip, sequenceLength, adPosition);
if (!this.usingBaseVideo_) {
ad.setMuted(this.baseVideo_.muted);
ad.setVolume(this.baseVideo_.volume);
Expand All @@ -620,7 +623,8 @@ shaka.ads.InterstitialAdManager = class {
new shaka.util.FakeEvent(shaka.ads.Utils.AD_STARTED,
(new Map()).set('ad', ad)));

if (ad.canSkipNow()) {
let prevCanSkipNow = ad.canSkipNow();
if (prevCanSkipNow) {
this.onEvent_(new shaka.util.FakeEvent(
shaka.ads.Utils.AD_SKIP_STATE_CHANGED));
}
Expand All @@ -631,12 +635,13 @@ shaka.ads.InterstitialAdManager = class {
if (!duration) {
return;
}
if (interstitial.isSkippable && interstitial.skipOffset &&
ad.canSkipNow() && ad.getRemainingTime() > 0 &&
ad.getDuration() > 0) {
const currentCanSkipNow = ad.canSkipNow();
if (prevCanSkipNow != currentCanSkipNow &&
ad.getRemainingTime() > 0 && ad.getDuration() > 0) {
this.onEvent_(
new shaka.util.FakeEvent(shaka.ads.Utils.AD_SKIP_STATE_CHANGED));
}
prevCanSkipNow = currentCanSkipNow;
const currentPercent = 100 * this.video_.currentTime / duration;
if (currentPercent >= 25 && !eventsSent.has('firstquartile')) {
updateBaseVideoTime();
Expand Down Expand Up @@ -743,6 +748,26 @@ shaka.ads.InterstitialAdManager = class {
isSkippable = !data.includes('SKIP');
canJump = !data.includes('JUMP');
}
let skipOffset = isSkippable ? 0 : null;
const enableSkipAfter =
hlsInterstitial.values.find((v) => v.key == 'X-ENABLE-SKIP-AFTER');
if (enableSkipAfter) {
const enableSkipAfterString = /** @type {string} */(enableSkipAfter.data);
skipOffset = parseFloat(enableSkipAfterString);
if (isNaN(skipOffset)) {
skipOffset = isSkippable ? 0 : null;
}
}
let skipFor = null;
const enableSkipFor =
hlsInterstitial.values.find((v) => v.key == 'X-ENABLE-SKIP-FOR');
if (enableSkipFor) {
const enableSkipForString = /** @type {string} */(enableSkipFor.data);
skipFor = parseFloat(enableSkipForString);
if (isNaN(skipOffset)) {
skipFor = null;
}
}
let resumeOffset = null;
const resume =
hlsInterstitial.values.find((v) => v.key == 'X-RESUME-OFFSET');
Expand Down Expand Up @@ -793,7 +818,8 @@ shaka.ads.InterstitialAdManager = class {
endTime: hlsInterstitial.endTime,
uri,
isSkippable,
skipOffset: isSkippable ? 0 : null,
skipOffset,
skipFor,
canJump,
resumeOffset,
playoutLimit,
Expand All @@ -817,6 +843,23 @@ shaka.ads.InterstitialAdManager = class {
const dataAsJson =
/** @type {!shaka.ads.InterstitialAdManager.AssetsList} */ (
JSON.parse(data));
const skipControl = dataAsJson['SKIP-CONTROL'];
if (skipControl) {
const enableSkipAfterValue = skipControl['ENABLE-SKIP-AFTER'];
if (enableSkipAfterValue instanceof Number) {
skipOffset = parseFloat(enableSkipAfterValue);
if (isNaN(enableSkipAfterValue)) {
skipOffset = isSkippable ? 0 : null;
}
}
const enableSkipForValue = skipControl['X-ENABLE-SKIP-FOR'];
if (enableSkipForValue instanceof Number) {
skipFor = parseFloat(enableSkipForValue);
if (isNaN(enableSkipForValue)) {
skipFor = null;
}
}
}
for (const asset of dataAsJson['ASSETS']) {
if (asset['URI']) {
interstitialsAd.push({
Expand All @@ -825,7 +868,8 @@ shaka.ads.InterstitialAdManager = class {
endTime: hlsInterstitial.endTime,
uri: asset['URI'],
isSkippable,
skipOffset: isSkippable ? 0 : null,
skipOffset,
skipFor,
canJump,
resumeOffset,
playoutLimit,
Expand Down Expand Up @@ -914,10 +958,12 @@ shaka.ads.InterstitialAdManager = class {

/**
* @typedef {{
* ASSETS: !Array.<shaka.ads.InterstitialAdManager.Asset>
* ASSETS: !Array.<shaka.ads.InterstitialAdManager.Asset>,
* SKIP-CONTROL: ?shaka.ads.InterstitialAdManager.SkipControl
* }}
*
* @property {!Array.<shaka.ads.InterstitialAdManager.Asset>} ASSETS
* @property {shaka.ads.InterstitialAdManager.SkipControl} SKIP-CONTROL
*/
shaka.ads.InterstitialAdManager.AssetsList;

Expand All @@ -930,3 +976,15 @@ shaka.ads.InterstitialAdManager.AssetsList;
* @property {string} URI
*/
shaka.ads.InterstitialAdManager.Asset;


/**
* @typedef {{
* ENABLE-SKIP-AFTER: number,
* ENABLE-SKIP-FOR: number
* }}
*
* @property {number} ENABLE-SKIP-AFTER
* @property {number} ENABLE-SKIP-FOR
*/
shaka.ads.InterstitialAdManager.SkipControl;
27 changes: 16 additions & 11 deletions ui/skip_ad_button.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,17 +142,20 @@ shaka.ui.SkipAdButton = class extends shaka.ui.Element {
onTimerTick_() {
goog.asserts.assert(this.ad != null,
'this.ad should exist at this point');

const secondsLeft = Math.round(this.ad.getTimeUntilSkippable());
if (secondsLeft > 0) {
this.counter_.textContent = secondsLeft;
if (this.ad.isSkippable()) {
const secondsLeft = Math.round(this.ad.getTimeUntilSkippable());
if (secondsLeft > 0) {
this.counter_.textContent = secondsLeft;
} else {
// The ad should now be skippable. OnSkipStateChanged() is
// listening for a SKIP_STATE_CHANGED event and will take care
// of the button. Here we just stop the timer and hide the counter.
// NOTE: onSkipStateChanged_() also hides the counter.
this.timer_.stop();
shaka.ui.Utils.setDisplay(this.counter_, false);
}
} else {
// The ad should now be skippable. OnSkipStateChanged() is
// listening for a SKIP_STATE_CHANGED event and will take care
// of the button. Here we just stop the timer and hide the counter.
// NOTE: onSkipStateChanged_() also hides the counter.
this.timer_.stop();
shaka.ui.Utils.setDisplay(this.counter_, false);
this.reset_();
}
}

Expand All @@ -161,7 +164,9 @@ shaka.ui.SkipAdButton = class extends shaka.ui.Element {
*/
onSkipStateChanged_() {
// Double-check that the ad is now skippable
if (this.ad.canSkipNow()) {
if (!this.ad.isSkippable()) {
this.reset_();
} else if (this.ad.canSkipNow()) {
this.button_.disabled = false;
this.timer_.stop();
shaka.ui.Utils.setDisplay(this.counter_, false);
Expand Down

0 comments on commit 3107de3

Please sign in to comment.