Skip to content

Commit

Permalink
feat(Ads): Parse non-linear VAST ads (#7702)
Browse files Browse the repository at this point in the history
  • Loading branch information
avelad authored Dec 3, 2024
1 parent 4744d1e commit 0c7d204
Show file tree
Hide file tree
Showing 3 changed files with 232 additions and 65 deletions.
198 changes: 142 additions & 56 deletions lib/ads/ad_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,6 @@ shaka.ads.Utils = class {
/** @type {!Array.<shaka.extern.AdInterstitial>} */
const interstitials = [];

let startTime = 0;
if (currentTime != null) {
startTime = currentTime;
}

for (const ad of TXml.findChildren(vast, 'Ad')) {
const inline = TXml.findChild(ad, 'InLine');
if (!inline) {
Expand All @@ -42,65 +37,156 @@ shaka.ads.Utils = class {
}
for (const creative of TXml.findChildren(creatives, 'Creative')) {
const linear = TXml.findChild(creative, 'Linear');
if (!linear) {
continue;
if (linear) {
shaka.ads.Utils.processLinearAd_(
interstitials, currentTime, linear);
}
let skipOffset = null;
if (linear.attributes['skipoffset']) {
skipOffset = shaka.util.TextParser.parseTime(
linear.attributes['skipoffset']);
if (isNaN(skipOffset)) {
skipOffset = null;
const nonLinearAds = TXml.findChild(creative, 'NonLinearAds');
if (nonLinearAds) {
const nonLinears = TXml.findChildren(nonLinearAds, 'NonLinear');
for (const nonLinear of nonLinears) {
shaka.ads.Utils.processNonLinearAd_(
interstitials, currentTime, nonLinear);
}
}
const mediaFiles = TXml.findChild(linear, 'MediaFiles');
if (!mediaFiles) {
continue;
}
const medias = TXml.findChildren(mediaFiles, 'MediaFile');
let checkMedias = medias;
const streamingMedias = medias.filter((media) => {
return media.attributes['delivery'] == 'streaming';
});
if (streamingMedias.length) {
checkMedias = streamingMedias;
}
const sortedMedias = checkMedias.sort((a, b) => {
const aHeight = parseInt(a.attributes['height'], 10) || 0;
const bHeight = parseInt(b.attributes['height'], 10) || 0;
return bHeight - aHeight;
});
for (const media of sortedMedias) {
const adUrl = TXml.getTextContents(media);
if (!adUrl) {
continue;
}
interstitials.push({
id: null,
startTime: startTime,
endTime: null,
uri: adUrl,
mimeType: media.attributes['type'] || null,
isSkippable: skipOffset != null,
skipOffset,
skipFor: null,
canJump: false,
resumeOffset: 0,
playoutLimit: null,
once: true,
pre: currentTime == null,
post: currentTime == Infinity,
timelineRange: false,
loop: false,
overlay: null,
});
break;
}
}
}
return interstitials;
}

/**
* @param {!Array.<shaka.extern.AdInterstitial>} interstitials
* @param {?number} currentTime
* @param {!shaka.extern.xml.Node} linear
* @private
*/
static processLinearAd_(interstitials, currentTime, linear) {
const TXml = shaka.util.TXml;
let startTime = 0;
if (currentTime != null) {
startTime = currentTime;
}
let skipOffset = null;
if (linear.attributes['skipoffset']) {
skipOffset = shaka.util.TextParser.parseTime(
linear.attributes['skipoffset']);
if (isNaN(skipOffset)) {
skipOffset = null;
}
}
const mediaFiles = TXml.findChild(linear, 'MediaFiles');
if (!mediaFiles) {
return;
}
const medias = TXml.findChildren(mediaFiles, 'MediaFile');
let checkMedias = medias;
const streamingMedias = medias.filter((media) => {
return media.attributes['delivery'] == 'streaming';
});
if (streamingMedias.length) {
checkMedias = streamingMedias;
}
const sortedMedias = checkMedias.sort((a, b) => {
const aHeight = parseInt(a.attributes['height'], 10) || 0;
const bHeight = parseInt(b.attributes['height'], 10) || 0;
return bHeight - aHeight;
});
for (const media of sortedMedias) {
if (media.attributes['apiFramework']) {
continue;
}
const adUrl = TXml.getContents(media);
if (!adUrl) {
continue;
}
interstitials.push({
id: null,
startTime: startTime,
endTime: null,
uri: adUrl,
mimeType: media.attributes['type'] || null,
isSkippable: skipOffset != null,
skipOffset,
skipFor: null,
canJump: false,
resumeOffset: 0,
playoutLimit: null,
once: true,
pre: currentTime == null,
post: currentTime == Infinity,
timelineRange: false,
loop: false,
overlay: null,
});
break;
}
}

/**
* @param {!Array.<shaka.extern.AdInterstitial>} interstitials
* @param {?number} currentTime
* @param {!shaka.extern.xml.Node} nonLinear
* @private
*/
static processNonLinearAd_(interstitials, currentTime, nonLinear) {
const TXml = shaka.util.TXml;
const staticResource = TXml.findChild(nonLinear, 'StaticResource');
if (!staticResource) {
return;
}
const adUrl = TXml.getContents(staticResource);
if (!adUrl) {
return;
}
const width = TXml.parseAttr(nonLinear, 'width', TXml.parseInt);
const height = TXml.parseAttr(nonLinear, 'height', TXml.parseInt);
if (!width || !height) {
return;
}
let playoutLimit = null;
const minSuggestedDuration =
nonLinear.attributes['minSuggestedDuration'];
if (minSuggestedDuration) {
playoutLimit = shaka.util.TextParser.parseTime(minSuggestedDuration);
}
let startTime = 0;
if (currentTime != null) {
startTime = currentTime;
}
interstitials.push({
id: null,
startTime: startTime,
endTime: null,
uri: adUrl,
mimeType: staticResource.attributes['creativeType'] || null,
isSkippable: false,
skipOffset: null,
skipFor: null,
canJump: false,
resumeOffset: 0,
playoutLimit,
once: true,
pre: currentTime == null,
post: currentTime == Infinity,
timelineRange: false,
loop: false,
overlay: {
viewport: {
x: 0,
y: 0,
},
topLeft: {
x: 0,
y: 0,
},
size: {
x: width,
y: height,
},
},
});
}

/**
* @param {!shaka.extern.xml.Node} vmap
* @return {!Array.<{time: ?number, uri: string}>}
Expand Down
34 changes: 25 additions & 9 deletions lib/ads/interstitial_ad_manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,8 @@ shaka.ads.InterstitialAdManager = class {
} else {
const difference = interstitial.startTime - this.lastTime_;
if (difference > 0 && difference <= 10) {
if (!this.preloadManagerInterstitials_.has(interstitial)) {
if (!this.preloadManagerInterstitials_.has(interstitial) &&
this.isPreloadAllowed_(interstitial)) {
this.preloadManagerInterstitials_.set(
interstitial, this.player_.preload(
interstitial.uri,
Expand Down Expand Up @@ -527,7 +528,8 @@ shaka.ads.InterstitialAdManager = class {
}
}
if (shouldPreload) {
if (!this.preloadManagerInterstitials_.has(interstitial)) {
if (!this.preloadManagerInterstitials_.has(interstitial) &&
this.isPreloadAllowed_(interstitial)) {
this.preloadManagerInterstitials_.set(
interstitial, this.player_.preload(
interstitial.uri,
Expand Down Expand Up @@ -790,13 +792,7 @@ shaka.ads.InterstitialAdManager = class {
// interstitial below.
const nextCurrentInterstitial = this.getCurrentInterstitial_(
interstitial.pre, adPosition - oncePlayed);
if (nextCurrentInterstitial) {
this.onEvent_(
new shaka.util.FakeEvent(shaka.ads.Utils.AD_STOPPED));
this.adEventManager_.removeAll();
this.setupAd_(nextCurrentInterstitial, sequenceLength,
++adPosition, initialTime, oncePlayed);
} else {
if (!nextCurrentInterstitial || nextCurrentInterstitial.overlay) {
if (interstitial.post) {
this.lastTime_ = null;
this.lastPlayedAd_ = null;
Expand Down Expand Up @@ -833,6 +829,12 @@ shaka.ads.InterstitialAdManager = class {
this.cuepointsChanged_();
}
this.determineIfUsingBaseVideo_();
} else {
this.onEvent_(
new shaka.util.FakeEvent(shaka.ads.Utils.AD_STOPPED));
this.adEventManager_.removeAll();
this.setupAd_(nextCurrentInterstitial, sequenceLength,
++adPosition, initialTime, oncePlayed);
}
};
const error = async (e) => {
Expand Down Expand Up @@ -1246,6 +1248,20 @@ shaka.ads.InterstitialAdManager = class {
return response.data;
}

/**
* @param {!shaka.extern.AdInterstitial} interstitial
* @return {boolean}
* @private
*/
isPreloadAllowed_(interstitial) {
const interstitialMimeType = interstitial.mimeType;
if (!interstitialMimeType) {
return true;
}
return !interstitialMimeType.startsWith('image/') &&
interstitialMimeType !== 'text/html';
}


/**
* Only for testing
Expand Down
65 changes: 65 additions & 0 deletions test/ads/interstitial_ad_manager_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -1086,6 +1086,71 @@ describe('Interstitial Ad manager', () => {
jasmine.objectContaining(eventValue1));
});

it('supports non-linear ads', async () => {
const vast = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<VAST version="3.0">',
'<Ad id="5925573263">',
'<InLine>',
'<Creatives>',
'<Creative id="138381721867" sequence="1">',
'<NonLinearAds>',
'<NonLinear width="535" height="80" minSuggestedDuration="00:00:05">',
'<StaticResource creativeType="image/png">',
'<![CDATA[test.png]]>',
'</StaticResource>',
'</NonLinear>',
'</NonLinearAds>',
'</Creative>',
'</Creatives>',
'</InLine>',
'</Ad>',
'</VAST>',
].join('');

networkingEngine.setResponseText('test:/vast', vast);

await interstitialAdManager.addAdUrlInterstitial('test:/vast');

expect(onEventSpy).not.toHaveBeenCalled();

const interstitials = interstitialAdManager.getInterstitials();
expect(interstitials.length).toBe(1);
const expectedInterstitial = {
id: null,
startTime: 0,
endTime: null,
uri: 'test.png',
mimeType: 'image/png',
isSkippable: false,
skipOffset: null,
skipFor: null,
canJump: false,
resumeOffset: 0,
playoutLimit: 5,
once: true,
pre: true,
post: false,
timelineRange: false,
loop: false,
overlay: {
viewport: {
x: 0,
y: 0,
},
topLeft: {
x: 0,
y: 0,
},
size: {
x: 535,
y: 80,
},
},
};
expect(interstitials[0]).toEqual(expectedInterstitial);
});

it('ignore empty', async () => {
const vast = [
'<?xml version="1.0" encoding="UTF-8"?>',
Expand Down

0 comments on commit 0c7d204

Please sign in to comment.