Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Ads): New HLS interstitial DATERANGE attributes for Skip Button #7467

Merged
merged 2 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, if you don't skip fast enough, you lose the chance? That seems backwards to how most skippable ads work, in my experience.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is a new feature that is only activated for HLS interstitials, and it is configurable. If you configure X-ENABLE-SKIP-FOR with a number high or equal to the duration of the ad or omit this parameter, you have the old behavior.

}
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 @@ -267,13 +267,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 @@ -603,7 +606,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 @@ -613,7 +616,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 @@ -624,12 +628,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 @@ -727,6 +732,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 @@ -777,7 +802,8 @@ shaka.ads.InterstitialAdManager = class {
endTime: hlsInterstitial.endTime,
uri,
isSkippable,
skipOffset: isSkippable ? 0 : null,
skipOffset,
skipFor,
canJump,
resumeOffset,
playoutLimit,
Expand All @@ -801,6 +827,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 @@ -809,7 +852,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 @@ -898,10 +942,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 @@ -914,3 +960,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
Loading