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): Add CUE PRE and POST support in Interstitials #6799

Merged
merged 4 commits into from
Jun 12, 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
63 changes: 52 additions & 11 deletions lib/ads/interstitial_ad_manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,9 @@ shaka.ads.InterstitialAdManager = class {
if (this.playingAd_) {
return;
}
const needPreRoll = this.lastTime_ == null;
this.lastTime_ = this.baseVideo_.currentTime;
const currentInterstitial = this.getCurrentIntestitial_();
const currentInterstitial = this.getCurrentInterstitial_(needPreRoll);
if (currentInterstitial) {
this.setupAd_(currentInterstitial, /* sequenceLength= */ 1,
/* adPosition= */ 1, /* initialTime= */ Date.now());
Expand Down Expand Up @@ -225,7 +226,9 @@ shaka.ads.InterstitialAdManager = class {
this.interstitialIds_.add(interstitialId);
this.interstitials_.add(interstitialAd);
let shouldPreload = false;
if (interstitialAd.startTime == 0 && !interstitialAd.canJump) {
if (interstitial.pre && this.lastTime_ == null) {
shouldPreload = true;
} else if (interstitialAd.startTime == 0 && !interstitialAd.canJump) {
shouldPreload = true;
} else if (this.lastTime_ != null) {
const difference = interstitial.startTime - this.lastTime_;
Expand Down Expand Up @@ -268,24 +271,35 @@ shaka.ads.InterstitialAdManager = class {


/**
* @param {boolean} needPreRoll
* @param {number=} numberToSkip
* @return {?shaka.ads.InterstitialAdManager.Interstitial}
* @private
*/
getCurrentIntestitial_(numberToSkip = 0) {
getCurrentInterstitial_(needPreRoll, numberToSkip = 0) {
let skipped = 0;
let currentInterstitial = null;
if (this.interstitials_.size && this.lastTime_ != null) {
const isEnded = this.baseVideo_.ended;
const interstitials = Array.from(this.interstitials_).sort((a, b) => {
return b.startTime - a.startTime;
});
const roundDecimals = (number) => {
return Math.round(number * 1000) / 1000;
};
for (const interstitial of interstitials) {
const difference =
this.lastTime_ - roundDecimals(interstitial.startTime);
if (difference > 0 && (difference <= 1 || !interstitial.canJump)) {
let isValid = false;
if (needPreRoll) {
isValid = interstitial.pre;
} else if (isEnded) {
isValid = interstitial.post;
} else if (!interstitial.pre && !interstitial.post) {
const difference =
this.lastTime_ - roundDecimals(interstitial.startTime);
isValid = difference > 0 &&
(difference <= 1 || !interstitial.canJump);
}
if (isValid) {
if (skipped == numberToSkip) {
currentInterstitial = interstitial;
break;
Expand Down Expand Up @@ -316,6 +330,11 @@ shaka.ads.InterstitialAdManager = class {

if (adPosition == 1 && sequenceLength == 1) {
sequenceLength = Array.from(this.interstitials_).filter((i) => {
if (interstitial.pre) {
return i.pre == interstitial.pre;
} else if (interstitial.post) {
return i.post == interstitial.post;
}
return Math.abs(i.startTime - interstitial.startTime) < 0.001;
}).length;
}
Expand Down Expand Up @@ -349,7 +368,8 @@ shaka.ads.InterstitialAdManager = class {
}
if (!this.usingBaseVideo_) {
this.baseVideo_.pause();
if (interstitial.resumeOffset != null) {
if (interstitial.resumeOffset != null &&
interstitial.resumeOffset != 0) {
this.baseVideo_.currentTime += interstitial.resumeOffset;
}
}
Expand Down Expand Up @@ -377,8 +397,8 @@ shaka.ads.InterstitialAdManager = class {
'Should be an number!');
// Optimization to avoid returning to main content when there is another
// interstitial below.
const nextCurrentInterstitial =
this.getCurrentIntestitial_(adPosition - oncePlayed);
const nextCurrentInterstitial = this.getCurrentInterstitial_(
interstitial.pre, adPosition - oncePlayed);
if (nextCurrentInterstitial) {
this.onEvent_(
new shaka.util.FakeEvent(shaka.ads.AdManager.AD_STOPPED));
Expand Down Expand Up @@ -406,7 +426,9 @@ shaka.ads.InterstitialAdManager = class {
this.playingAd_ = false;
if (!this.usingBaseVideo_) {
updateBaseVideoTime();
this.baseVideo_.play();
if (!this.baseVideo_.ended) {
this.baseVideo_.play();
}
}
}
};
Expand Down Expand Up @@ -557,10 +579,14 @@ shaka.ads.InterstitialAdManager = class {
}
}
let once = false;
let pre = false;
let post = false;
const cue = interstitial.values.find((v) => v.key == 'CUE');
if (cue) {
const data = /** @type {string} */(cue.data);
once = data.includes('ONCE');
pre = data.includes('PRE');
post = data.includes('POST');
}
if (assetUri) {
const uri = /** @type {string} */(assetUri.data);
Expand All @@ -576,6 +602,8 @@ shaka.ads.InterstitialAdManager = class {
resumeOffset,
playoutLimit,
once,
pre,
post,
});
} else if (assetList) {
const uri = /** @type {string} */(assetList.data);
Expand Down Expand Up @@ -604,6 +632,8 @@ shaka.ads.InterstitialAdManager = class {
resumeOffset,
playoutLimit,
once,
pre,
post,
});
}
}
Expand All @@ -627,6 +657,13 @@ shaka.ads.InterstitialAdManager = class {
start: interstitial.startTime,
end: null,
};
if (interstitial.pre) {
shakaCuePoint.start = 0;
shakaCuePoint.end = null;
} else if (interstitial.post) {
shakaCuePoint.start = -1;
shakaCuePoint.end = null;
}
const isValid = !cuePoints.find((c) => {
return shakaCuePoint.start == c.start && shakaCuePoint.end == c.end;
});
Expand Down Expand Up @@ -686,7 +723,9 @@ shaka.ads.InterstitialAdManager.Asset;
* canJump: boolean,
* resumeOffset: ?number,
* playoutLimit: ?number,
* once: boolean
* once: boolean,
* pre: boolean,
* post: boolean
* }}
*
* @property {number} startTime
Expand All @@ -697,5 +736,7 @@ shaka.ads.InterstitialAdManager.Asset;
* @property {?number} resumeOffset
* @property {?number} playoutLimit
* @property {boolean} once
* @property {boolean} pre
* @property {boolean} post
*/
shaka.ads.InterstitialAdManager.Interstitial;
11 changes: 10 additions & 1 deletion lib/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -776,15 +776,23 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
/** @private {HTMLMediaElement} */
this.preloadDueAdManagerVideo_ = null;

/** @private {boolean} */
this.preloadDueAdManagerVideoEnded_ = false;

/** @private {shaka.util.Timer} */
this.preloadDueAdManagerTimer_ = new shaka.util.Timer(async () => {
if (this.preloadDueAdManager_) {
goog.asserts.assert(this.preloadDueAdManagerVideo_, 'Must have video');
await this.attach(
this.preloadDueAdManagerVideo_, /* initializeMediaSource= */ true);
await this.load(this.preloadDueAdManager_);
this.preloadDueAdManagerVideo_.play();
if (!this.preloadDueAdManagerVideoEnded_) {
this.preloadDueAdManagerVideo_.play();
} else {
this.preloadDueAdManagerVideo_.pause();
}
this.preloadDueAdManager_ = null;
this.preloadDueAdManagerVideoEnded_ = false;
}
});

Expand All @@ -799,6 +807,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
this.preloadDueAdManagerTimer_.stop();
if (!this.preloadDueAdManager_) {
this.preloadDueAdManagerVideo_ = this.video_;
this.preloadDueAdManagerVideoEnded_ = this.video_.ended;
const saveLivePosition = /** @type {boolean} */(
e['saveLivePosition']) || false;
this.preloadDueAdManager_ = await this.detachAndSavePreload(
Expand Down
25 changes: 8 additions & 17 deletions test/ads_integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ describe('Ads', () => {
document.body.appendChild(video);
adContainer =
/** @type {!HTMLElement} */ (document.createElement('div'));
adContainer.style.position = 'relative';
adContainer.style.width = '600px';
adContainer.style.height = '400px';
document.body.appendChild(adContainer);
compiledShaka =
await shaka.test.Loader.loadShaka(getClientArg('uncompiled'));
Expand Down Expand Up @@ -192,18 +195,14 @@ describe('Ads', () => {
await video.play();
expect(player.isLive()).toBe(false);

// Wait for the video to start playback. If it takes longer than 10
// seconds, fail the test.
await waiter.waitForMovementOrFailOnTimeout(video, 10);

// Wait a maximum of 10 seconds before the ad starts playing.
await waiter.timeoutAfter(10)
.waitForEvent(adManager, shaka.ads.AdManager.AD_STARTED);
await waiter.timeoutAfter(20)
.waitForEvent(adManager, shaka.ads.AdManager.AD_STOPPED);

await shaka.test.Util.delay(1);
expect(video.currentTime).toBeLessThanOrEqual(6);
expect(video.currentTime).toBeLessThanOrEqual(3);

// Wait a maximum of 10 seconds before the ad starts playing.
await waiter.timeoutAfter(10)
Expand All @@ -212,7 +211,7 @@ describe('Ads', () => {
.waitForEvent(adManager, shaka.ads.AdManager.AD_STOPPED);

await shaka.test.Util.delay(1);
expect(video.currentTime).toBeGreaterThan(12);
expect(video.currentTime).toBeGreaterThan(8);

// Play for 10 seconds, but stop early if the video ends. If it takes
// longer than 30 seconds, fail the test.
Expand All @@ -230,18 +229,14 @@ describe('Ads', () => {
await video.play();
expect(player.isLive()).toBe(false);

// Wait for the video to start playback. If it takes longer than 10
// seconds, fail the test.
await waiter.waitForMovementOrFailOnTimeout(video, 10);

// Wait a maximum of 10 seconds before the ad starts playing.
await waiter.timeoutAfter(10)
.waitForEvent(adManager, shaka.ads.AdManager.AD_STARTED);
await waiter.timeoutAfter(20)
.waitForEvent(adManager, shaka.ads.AdManager.AD_STOPPED);

await shaka.test.Util.delay(1);
expect(video.currentTime).toBeLessThanOrEqual(6);
expect(video.currentTime).toBeLessThanOrEqual(3);

// Wait a maximum of 10 seconds before the ad starts playing.
await waiter.timeoutAfter(10)
Expand All @@ -250,7 +245,7 @@ describe('Ads', () => {
.waitForEvent(adManager, shaka.ads.AdManager.AD_STOPPED);

await shaka.test.Util.delay(1);
expect(video.currentTime).toBeGreaterThan(12);
expect(video.currentTime).toBeGreaterThan(8);

// Play for 10 seconds, but stop early if the video ends. If it takes
// longer than 30 seconds, fail the test.
Expand All @@ -266,18 +261,14 @@ describe('Ads', () => {
await video.play();
expect(player.isLive()).toBe(false);

// Wait for the video to start playback. If it takes longer than 10
// seconds, fail the test.
await waiter.waitForMovementOrFailOnTimeout(video, 10);

// Wait a maximum of 10 seconds before the ad starts playing.
await waiter.timeoutAfter(10)
.waitForEvent(adManager, shaka.ads.AdManager.AD_STARTED);
await waiter.timeoutAfter(20)
.waitForEvent(adManager, shaka.ads.AdManager.AD_STOPPED);

await shaka.test.Util.delay(1);
expect(video.currentTime).toBeLessThanOrEqual(5);
expect(video.currentTime).toBeLessThanOrEqual(3);

// Wait a maximum of 10 seconds before the ad starts playing.
await waiter.timeoutAfter(10)
Expand Down
4 changes: 2 additions & 2 deletions test/test/assets/hls-interstitial/main.m3u8
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ s4.mp4
#EXTINF:4.000,
s5.mp4
#EXT-X-ENDLIST
#EXT-X-DATERANGE:ID="0",CLASS="com.apple.hls.interstitial",CUE="ONCE",START-DATE="2000-01-01T00:00:02Z",X-ASSET-URI="ad.m3u8",X-RESTRICT="JUMP",X-RESUME-OFFSET=0.0
#EXT-X-DATERANGE:ID="1",CLASS="com.apple.hls.interstitial",CUE="ONCE",START-DATE="2000-01-01T00:00:08Z",X-ASSET-URI="ad.m3u8",X-RESTRICT="SKIP,JUMP",X-RESUME-OFFSET=6.0,X-PLAYOUT-LIMIT=15.0
#EXT-X-DATERANGE:ID="0",CLASS="com.apple.hls.interstitial",CUE="PRE",START-DATE="2000-01-01T00:00:02Z",X-ASSET-URI="ad.m3u8",X-RESTRICT="JUMP",X-RESUME-OFFSET=0.0
#EXT-X-DATERANGE:ID="1",CLASS="com.apple.hls.interstitial",CUE="ONCE",START-DATE="2000-01-01T00:00:04Z",X-ASSET-URI="ad.m3u8",X-RESTRICT="SKIP,JUMP",X-RESUME-OFFSET=6.0,X-PLAYOUT-LIMIT=15.0
Loading