Skip to content

Commit 0c7d204

Browse files
authored
feat(Ads): Parse non-linear VAST ads (#7702)
1 parent 4744d1e commit 0c7d204

File tree

3 files changed

+232
-65
lines changed

3 files changed

+232
-65
lines changed

lib/ads/ad_utils.js

+142-56
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,6 @@ shaka.ads.Utils = class {
2626
/** @type {!Array.<shaka.extern.AdInterstitial>} */
2727
const interstitials = [];
2828

29-
let startTime = 0;
30-
if (currentTime != null) {
31-
startTime = currentTime;
32-
}
33-
3429
for (const ad of TXml.findChildren(vast, 'Ad')) {
3530
const inline = TXml.findChild(ad, 'InLine');
3631
if (!inline) {
@@ -42,65 +37,156 @@ shaka.ads.Utils = class {
4237
}
4338
for (const creative of TXml.findChildren(creatives, 'Creative')) {
4439
const linear = TXml.findChild(creative, 'Linear');
45-
if (!linear) {
46-
continue;
40+
if (linear) {
41+
shaka.ads.Utils.processLinearAd_(
42+
interstitials, currentTime, linear);
4743
}
48-
let skipOffset = null;
49-
if (linear.attributes['skipoffset']) {
50-
skipOffset = shaka.util.TextParser.parseTime(
51-
linear.attributes['skipoffset']);
52-
if (isNaN(skipOffset)) {
53-
skipOffset = null;
44+
const nonLinearAds = TXml.findChild(creative, 'NonLinearAds');
45+
if (nonLinearAds) {
46+
const nonLinears = TXml.findChildren(nonLinearAds, 'NonLinear');
47+
for (const nonLinear of nonLinears) {
48+
shaka.ads.Utils.processNonLinearAd_(
49+
interstitials, currentTime, nonLinear);
5450
}
5551
}
56-
const mediaFiles = TXml.findChild(linear, 'MediaFiles');
57-
if (!mediaFiles) {
58-
continue;
59-
}
60-
const medias = TXml.findChildren(mediaFiles, 'MediaFile');
61-
let checkMedias = medias;
62-
const streamingMedias = medias.filter((media) => {
63-
return media.attributes['delivery'] == 'streaming';
64-
});
65-
if (streamingMedias.length) {
66-
checkMedias = streamingMedias;
67-
}
68-
const sortedMedias = checkMedias.sort((a, b) => {
69-
const aHeight = parseInt(a.attributes['height'], 10) || 0;
70-
const bHeight = parseInt(b.attributes['height'], 10) || 0;
71-
return bHeight - aHeight;
72-
});
73-
for (const media of sortedMedias) {
74-
const adUrl = TXml.getTextContents(media);
75-
if (!adUrl) {
76-
continue;
77-
}
78-
interstitials.push({
79-
id: null,
80-
startTime: startTime,
81-
endTime: null,
82-
uri: adUrl,
83-
mimeType: media.attributes['type'] || null,
84-
isSkippable: skipOffset != null,
85-
skipOffset,
86-
skipFor: null,
87-
canJump: false,
88-
resumeOffset: 0,
89-
playoutLimit: null,
90-
once: true,
91-
pre: currentTime == null,
92-
post: currentTime == Infinity,
93-
timelineRange: false,
94-
loop: false,
95-
overlay: null,
96-
});
97-
break;
98-
}
9952
}
10053
}
10154
return interstitials;
10255
}
10356

57+
/**
58+
* @param {!Array.<shaka.extern.AdInterstitial>} interstitials
59+
* @param {?number} currentTime
60+
* @param {!shaka.extern.xml.Node} linear
61+
* @private
62+
*/
63+
static processLinearAd_(interstitials, currentTime, linear) {
64+
const TXml = shaka.util.TXml;
65+
let startTime = 0;
66+
if (currentTime != null) {
67+
startTime = currentTime;
68+
}
69+
let skipOffset = null;
70+
if (linear.attributes['skipoffset']) {
71+
skipOffset = shaka.util.TextParser.parseTime(
72+
linear.attributes['skipoffset']);
73+
if (isNaN(skipOffset)) {
74+
skipOffset = null;
75+
}
76+
}
77+
const mediaFiles = TXml.findChild(linear, 'MediaFiles');
78+
if (!mediaFiles) {
79+
return;
80+
}
81+
const medias = TXml.findChildren(mediaFiles, 'MediaFile');
82+
let checkMedias = medias;
83+
const streamingMedias = medias.filter((media) => {
84+
return media.attributes['delivery'] == 'streaming';
85+
});
86+
if (streamingMedias.length) {
87+
checkMedias = streamingMedias;
88+
}
89+
const sortedMedias = checkMedias.sort((a, b) => {
90+
const aHeight = parseInt(a.attributes['height'], 10) || 0;
91+
const bHeight = parseInt(b.attributes['height'], 10) || 0;
92+
return bHeight - aHeight;
93+
});
94+
for (const media of sortedMedias) {
95+
if (media.attributes['apiFramework']) {
96+
continue;
97+
}
98+
const adUrl = TXml.getContents(media);
99+
if (!adUrl) {
100+
continue;
101+
}
102+
interstitials.push({
103+
id: null,
104+
startTime: startTime,
105+
endTime: null,
106+
uri: adUrl,
107+
mimeType: media.attributes['type'] || null,
108+
isSkippable: skipOffset != null,
109+
skipOffset,
110+
skipFor: null,
111+
canJump: false,
112+
resumeOffset: 0,
113+
playoutLimit: null,
114+
once: true,
115+
pre: currentTime == null,
116+
post: currentTime == Infinity,
117+
timelineRange: false,
118+
loop: false,
119+
overlay: null,
120+
});
121+
break;
122+
}
123+
}
124+
125+
/**
126+
* @param {!Array.<shaka.extern.AdInterstitial>} interstitials
127+
* @param {?number} currentTime
128+
* @param {!shaka.extern.xml.Node} nonLinear
129+
* @private
130+
*/
131+
static processNonLinearAd_(interstitials, currentTime, nonLinear) {
132+
const TXml = shaka.util.TXml;
133+
const staticResource = TXml.findChild(nonLinear, 'StaticResource');
134+
if (!staticResource) {
135+
return;
136+
}
137+
const adUrl = TXml.getContents(staticResource);
138+
if (!adUrl) {
139+
return;
140+
}
141+
const width = TXml.parseAttr(nonLinear, 'width', TXml.parseInt);
142+
const height = TXml.parseAttr(nonLinear, 'height', TXml.parseInt);
143+
if (!width || !height) {
144+
return;
145+
}
146+
let playoutLimit = null;
147+
const minSuggestedDuration =
148+
nonLinear.attributes['minSuggestedDuration'];
149+
if (minSuggestedDuration) {
150+
playoutLimit = shaka.util.TextParser.parseTime(minSuggestedDuration);
151+
}
152+
let startTime = 0;
153+
if (currentTime != null) {
154+
startTime = currentTime;
155+
}
156+
interstitials.push({
157+
id: null,
158+
startTime: startTime,
159+
endTime: null,
160+
uri: adUrl,
161+
mimeType: staticResource.attributes['creativeType'] || null,
162+
isSkippable: false,
163+
skipOffset: null,
164+
skipFor: null,
165+
canJump: false,
166+
resumeOffset: 0,
167+
playoutLimit,
168+
once: true,
169+
pre: currentTime == null,
170+
post: currentTime == Infinity,
171+
timelineRange: false,
172+
loop: false,
173+
overlay: {
174+
viewport: {
175+
x: 0,
176+
y: 0,
177+
},
178+
topLeft: {
179+
x: 0,
180+
y: 0,
181+
},
182+
size: {
183+
x: width,
184+
y: height,
185+
},
186+
},
187+
});
188+
}
189+
104190
/**
105191
* @param {!shaka.extern.xml.Node} vmap
106192
* @return {!Array.<{time: ?number, uri: string}>}

lib/ads/interstitial_ad_manager.js

+25-9
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,8 @@ shaka.ads.InterstitialAdManager = class {
185185
} else {
186186
const difference = interstitial.startTime - this.lastTime_;
187187
if (difference > 0 && difference <= 10) {
188-
if (!this.preloadManagerInterstitials_.has(interstitial)) {
188+
if (!this.preloadManagerInterstitials_.has(interstitial) &&
189+
this.isPreloadAllowed_(interstitial)) {
189190
this.preloadManagerInterstitials_.set(
190191
interstitial, this.player_.preload(
191192
interstitial.uri,
@@ -527,7 +528,8 @@ shaka.ads.InterstitialAdManager = class {
527528
}
528529
}
529530
if (shouldPreload) {
530-
if (!this.preloadManagerInterstitials_.has(interstitial)) {
531+
if (!this.preloadManagerInterstitials_.has(interstitial) &&
532+
this.isPreloadAllowed_(interstitial)) {
531533
this.preloadManagerInterstitials_.set(
532534
interstitial, this.player_.preload(
533535
interstitial.uri,
@@ -790,13 +792,7 @@ shaka.ads.InterstitialAdManager = class {
790792
// interstitial below.
791793
const nextCurrentInterstitial = this.getCurrentInterstitial_(
792794
interstitial.pre, adPosition - oncePlayed);
793-
if (nextCurrentInterstitial) {
794-
this.onEvent_(
795-
new shaka.util.FakeEvent(shaka.ads.Utils.AD_STOPPED));
796-
this.adEventManager_.removeAll();
797-
this.setupAd_(nextCurrentInterstitial, sequenceLength,
798-
++adPosition, initialTime, oncePlayed);
799-
} else {
795+
if (!nextCurrentInterstitial || nextCurrentInterstitial.overlay) {
800796
if (interstitial.post) {
801797
this.lastTime_ = null;
802798
this.lastPlayedAd_ = null;
@@ -833,6 +829,12 @@ shaka.ads.InterstitialAdManager = class {
833829
this.cuepointsChanged_();
834830
}
835831
this.determineIfUsingBaseVideo_();
832+
} else {
833+
this.onEvent_(
834+
new shaka.util.FakeEvent(shaka.ads.Utils.AD_STOPPED));
835+
this.adEventManager_.removeAll();
836+
this.setupAd_(nextCurrentInterstitial, sequenceLength,
837+
++adPosition, initialTime, oncePlayed);
836838
}
837839
};
838840
const error = async (e) => {
@@ -1246,6 +1248,20 @@ shaka.ads.InterstitialAdManager = class {
12461248
return response.data;
12471249
}
12481250

1251+
/**
1252+
* @param {!shaka.extern.AdInterstitial} interstitial
1253+
* @return {boolean}
1254+
* @private
1255+
*/
1256+
isPreloadAllowed_(interstitial) {
1257+
const interstitialMimeType = interstitial.mimeType;
1258+
if (!interstitialMimeType) {
1259+
return true;
1260+
}
1261+
return !interstitialMimeType.startsWith('image/') &&
1262+
interstitialMimeType !== 'text/html';
1263+
}
1264+
12491265

12501266
/**
12511267
* Only for testing

test/ads/interstitial_ad_manager_unit.js

+65
Original file line numberDiff line numberDiff line change
@@ -1086,6 +1086,71 @@ describe('Interstitial Ad manager', () => {
10861086
jasmine.objectContaining(eventValue1));
10871087
});
10881088

1089+
it('supports non-linear ads', async () => {
1090+
const vast = [
1091+
'<?xml version="1.0" encoding="UTF-8"?>',
1092+
'<VAST version="3.0">',
1093+
'<Ad id="5925573263">',
1094+
'<InLine>',
1095+
'<Creatives>',
1096+
'<Creative id="138381721867" sequence="1">',
1097+
'<NonLinearAds>',
1098+
'<NonLinear width="535" height="80" minSuggestedDuration="00:00:05">',
1099+
'<StaticResource creativeType="image/png">',
1100+
'<![CDATA[test.png]]>',
1101+
'</StaticResource>',
1102+
'</NonLinear>',
1103+
'</NonLinearAds>',
1104+
'</Creative>',
1105+
'</Creatives>',
1106+
'</InLine>',
1107+
'</Ad>',
1108+
'</VAST>',
1109+
].join('');
1110+
1111+
networkingEngine.setResponseText('test:/vast', vast);
1112+
1113+
await interstitialAdManager.addAdUrlInterstitial('test:/vast');
1114+
1115+
expect(onEventSpy).not.toHaveBeenCalled();
1116+
1117+
const interstitials = interstitialAdManager.getInterstitials();
1118+
expect(interstitials.length).toBe(1);
1119+
const expectedInterstitial = {
1120+
id: null,
1121+
startTime: 0,
1122+
endTime: null,
1123+
uri: 'test.png',
1124+
mimeType: 'image/png',
1125+
isSkippable: false,
1126+
skipOffset: null,
1127+
skipFor: null,
1128+
canJump: false,
1129+
resumeOffset: 0,
1130+
playoutLimit: 5,
1131+
once: true,
1132+
pre: true,
1133+
post: false,
1134+
timelineRange: false,
1135+
loop: false,
1136+
overlay: {
1137+
viewport: {
1138+
x: 0,
1139+
y: 0,
1140+
},
1141+
topLeft: {
1142+
x: 0,
1143+
y: 0,
1144+
},
1145+
size: {
1146+
x: 535,
1147+
y: 80,
1148+
},
1149+
},
1150+
};
1151+
expect(interstitials[0]).toEqual(expectedInterstitial);
1152+
});
1153+
10891154
it('ignore empty', async () => {
10901155
const vast = [
10911156
'<?xml version="1.0" encoding="UTF-8"?>',

0 commit comments

Comments
 (0)