Source: lib/ads/ad_utils.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.ads.Utils');
  7. goog.require('shaka.util.TextParser');
  8. goog.require('shaka.util.TXml');
  9. /**
  10. * A class responsible for ad utils.
  11. * @export
  12. */
  13. shaka.ads.Utils = class {
  14. /**
  15. * @param {!shaka.extern.xml.Node} vast
  16. * @param {?number} currentTime
  17. * @return {!Array<shaka.extern.AdInterstitial>}
  18. */
  19. static parseVastToInterstitials(vast, currentTime) {
  20. const TXml = shaka.util.TXml;
  21. /** @type {!Array<shaka.extern.AdInterstitial>} */
  22. const interstitials = [];
  23. for (const ad of TXml.findChildren(vast, 'Ad')) {
  24. const inline = TXml.findChild(ad, 'InLine');
  25. if (!inline) {
  26. continue;
  27. }
  28. const creatives = TXml.findChild(inline, 'Creatives');
  29. if (!creatives) {
  30. continue;
  31. }
  32. for (const creative of TXml.findChildren(creatives, 'Creative')) {
  33. const linear = TXml.findChild(creative, 'Linear');
  34. if (linear) {
  35. shaka.ads.Utils.processLinearAd_(
  36. interstitials, currentTime, linear);
  37. }
  38. const nonLinearAds = TXml.findChild(creative, 'NonLinearAds');
  39. if (nonLinearAds) {
  40. const nonLinears = TXml.findChildren(nonLinearAds, 'NonLinear');
  41. for (const nonLinear of nonLinears) {
  42. shaka.ads.Utils.processNonLinearAd_(
  43. interstitials, currentTime, nonLinear);
  44. }
  45. }
  46. }
  47. }
  48. return interstitials;
  49. }
  50. /**
  51. * @param {!Array<shaka.extern.AdInterstitial>} interstitials
  52. * @param {?number} currentTime
  53. * @param {!shaka.extern.xml.Node} linear
  54. * @private
  55. */
  56. static processLinearAd_(interstitials, currentTime, linear) {
  57. const TXml = shaka.util.TXml;
  58. let startTime = 0;
  59. if (currentTime != null) {
  60. startTime = currentTime;
  61. }
  62. let skipOffset = null;
  63. if (linear.attributes['skipoffset']) {
  64. skipOffset = shaka.util.TextParser.parseTime(
  65. linear.attributes['skipoffset']);
  66. if (isNaN(skipOffset)) {
  67. skipOffset = null;
  68. }
  69. }
  70. const mediaFiles = TXml.findChild(linear, 'MediaFiles');
  71. if (!mediaFiles) {
  72. return;
  73. }
  74. const medias = TXml.findChildren(mediaFiles, 'MediaFile');
  75. let checkMedias = medias;
  76. const streamingMedias = medias.filter((media) => {
  77. return media.attributes['delivery'] == 'streaming';
  78. });
  79. if (streamingMedias.length) {
  80. checkMedias = streamingMedias;
  81. }
  82. const sortedMedias = checkMedias.sort((a, b) => {
  83. const aHeight = parseInt(a.attributes['height'], 10) || 0;
  84. const bHeight = parseInt(b.attributes['height'], 10) || 0;
  85. return bHeight - aHeight;
  86. });
  87. for (const media of sortedMedias) {
  88. if (media.attributes['apiFramework']) {
  89. continue;
  90. }
  91. const adUrl = TXml.getContents(media);
  92. if (!adUrl) {
  93. continue;
  94. }
  95. interstitials.push({
  96. id: null,
  97. groupId: null,
  98. startTime: startTime,
  99. endTime: null,
  100. uri: adUrl,
  101. mimeType: media.attributes['type'] || null,
  102. isSkippable: skipOffset != null,
  103. skipOffset,
  104. skipFor: null,
  105. canJump: false,
  106. resumeOffset: 0,
  107. playoutLimit: null,
  108. once: true,
  109. pre: currentTime == null,
  110. post: currentTime == Infinity,
  111. timelineRange: false,
  112. loop: false,
  113. overlay: null,
  114. displayOnBackground: false,
  115. currentVideo: null,
  116. background: null,
  117. });
  118. break;
  119. }
  120. }
  121. /**
  122. * @param {!Array<shaka.extern.AdInterstitial>} interstitials
  123. * @param {?number} currentTime
  124. * @param {!shaka.extern.xml.Node} nonLinear
  125. * @private
  126. */
  127. static processNonLinearAd_(interstitials, currentTime, nonLinear) {
  128. const TXml = shaka.util.TXml;
  129. let mimeType = null;
  130. let resource = TXml.findChild(nonLinear, 'StaticResource');
  131. if (resource) {
  132. mimeType = resource.attributes['creativeType'];
  133. } else {
  134. resource = TXml.findChild(nonLinear, 'HTMLResource');
  135. if (!resource) {
  136. return;
  137. }
  138. mimeType = 'text/html';
  139. }
  140. let adUrl = TXml.getContents(resource);
  141. if (!adUrl) {
  142. return;
  143. }
  144. if (mimeType === 'text/html') {
  145. adUrl = 'data:text/html;charset=UTF-8,' + encodeURIComponent(adUrl);
  146. }
  147. const width = TXml.parseAttr(nonLinear, 'width', TXml.parseInt) ||
  148. TXml.parseAttr(nonLinear, 'expandedWidth', TXml.parseInt);
  149. const height = TXml.parseAttr(nonLinear, 'height', TXml.parseInt) ||
  150. TXml.parseAttr(nonLinear, 'expandedHeight', TXml.parseInt);
  151. if (!width && !height) {
  152. return;
  153. }
  154. let playoutLimit = null;
  155. const minSuggestedDuration =
  156. nonLinear.attributes['minSuggestedDuration'];
  157. if (minSuggestedDuration) {
  158. playoutLimit = shaka.util.TextParser.parseTime(minSuggestedDuration);
  159. }
  160. let startTime = 0;
  161. if (currentTime != null) {
  162. startTime = currentTime;
  163. }
  164. interstitials.push({
  165. id: null,
  166. groupId: null,
  167. startTime: startTime,
  168. endTime: null,
  169. uri: adUrl,
  170. mimeType,
  171. isSkippable: false,
  172. skipOffset: null,
  173. skipFor: null,
  174. canJump: false,
  175. resumeOffset: 0,
  176. playoutLimit,
  177. once: true,
  178. pre: currentTime == null,
  179. post: currentTime == Infinity,
  180. timelineRange: false,
  181. loop: false,
  182. overlay: {
  183. viewport: {
  184. x: 0,
  185. y: 0,
  186. },
  187. topLeft: {
  188. x: 0,
  189. y: 0,
  190. },
  191. size: {
  192. x: width || 0,
  193. y: height || 0,
  194. },
  195. },
  196. displayOnBackground: false,
  197. currentVideo: null,
  198. background: null,
  199. });
  200. }
  201. /**
  202. * @param {!shaka.extern.xml.Node} vmap
  203. * @return {!Array<{time: ?number, uri: string}>}
  204. */
  205. static parseVMAP(vmap) {
  206. const TXml = shaka.util.TXml;
  207. /** @type {!Array<{time: ?number, uri: string}>} */
  208. const ads = [];
  209. for (const adBreak of TXml.findChildren(vmap, 'vmap:AdBreak')) {
  210. const timeOffset = adBreak.attributes['timeOffset'];
  211. if (!timeOffset) {
  212. continue;
  213. }
  214. let time = null;
  215. if (timeOffset == 'start') {
  216. time = 0;
  217. } else if (timeOffset == 'end') {
  218. time = Infinity;
  219. } else {
  220. time = shaka.util.TextParser.parseTime(timeOffset);
  221. }
  222. const adSource = TXml.findChild(adBreak, 'vmap:AdSource');
  223. if (!adSource) {
  224. continue;
  225. }
  226. const adTagURI = TXml.findChild(adSource, 'vmap:AdTagURI');
  227. if (!adTagURI) {
  228. continue;
  229. }
  230. const uri = TXml.getTextContents(adTagURI);
  231. if (!uri) {
  232. continue;
  233. }
  234. ads.push({
  235. time,
  236. uri,
  237. });
  238. }
  239. return ads;
  240. }
  241. };
  242. /**
  243. * The event name for when a sequence of ads has been loaded.
  244. *
  245. * @const {string}
  246. * @export
  247. */
  248. shaka.ads.Utils.ADS_LOADED = 'ads-loaded';
  249. /**
  250. * The event name for when an ad has started playing.
  251. *
  252. * @const {string}
  253. * @export
  254. */
  255. shaka.ads.Utils.AD_STARTED = 'ad-started';
  256. /**
  257. * The event name for when an ad playhead crosses first quartile.
  258. *
  259. * @const {string}
  260. * @export
  261. */
  262. shaka.ads.Utils.AD_FIRST_QUARTILE = 'ad-first-quartile';
  263. /**
  264. * The event name for when an ad playhead crosses midpoint.
  265. *
  266. * @const {string}
  267. * @export
  268. */
  269. shaka.ads.Utils.AD_MIDPOINT = 'ad-midpoint';
  270. /**
  271. * The event name for when an ad playhead crosses third quartile.
  272. *
  273. * @const {string}
  274. * @export
  275. */
  276. shaka.ads.Utils.AD_THIRD_QUARTILE = 'ad-third-quartile';
  277. /**
  278. * The event name for when an ad has completed playing.
  279. *
  280. * @const {string}
  281. * @export
  282. */
  283. shaka.ads.Utils.AD_COMPLETE = 'ad-complete';
  284. /**
  285. * The event name for when an ad has finished playing
  286. * (played all the way through, was skipped, or was unable to proceed
  287. * due to an error).
  288. *
  289. * @const {string}
  290. * @export
  291. */
  292. shaka.ads.Utils.AD_STOPPED = 'ad-stopped';
  293. /**
  294. * The event name for when an ad is skipped by the user..
  295. *
  296. * @const {string}
  297. * @export
  298. */
  299. shaka.ads.Utils.AD_SKIPPED = 'ad-skipped';
  300. /**
  301. * The event name for when the ad volume has changed.
  302. *
  303. * @const {string}
  304. * @export
  305. */
  306. shaka.ads.Utils.AD_VOLUME_CHANGED = 'ad-volume-changed';
  307. /**
  308. * The event name for when the ad was muted.
  309. *
  310. * @const {string}
  311. * @export
  312. */
  313. shaka.ads.Utils.AD_MUTED = 'ad-muted';
  314. /**
  315. * The event name for when the ad was paused.
  316. *
  317. * @const {string}
  318. * @export
  319. */
  320. shaka.ads.Utils.AD_PAUSED = 'ad-paused';
  321. /**
  322. * The event name for when the ad was resumed after a pause.
  323. *
  324. * @const {string}
  325. * @export
  326. */
  327. shaka.ads.Utils.AD_RESUMED = 'ad-resumed';
  328. /**
  329. * The event name for when the ad's skip status changes
  330. * (usually it becomes skippable when it wasn't before).
  331. *
  332. * @const {string}
  333. * @export
  334. */
  335. shaka.ads.Utils.AD_SKIP_STATE_CHANGED = 'ad-skip-state-changed';
  336. /**
  337. * The event name for when the ad's cue points (start/end markers)
  338. * have changed.
  339. *
  340. * @const {string}
  341. * @export
  342. */
  343. shaka.ads.Utils.CUEPOINTS_CHANGED = 'ad-cue-points-changed';
  344. /**
  345. * The event name for when the native IMA ad manager object has
  346. * loaded and become available.
  347. *
  348. * @const {string}
  349. * @export
  350. */
  351. shaka.ads.Utils.IMA_AD_MANAGER_LOADED = 'ima-ad-manager-loaded';
  352. /**
  353. * The event name for when the native IMA stream manager object has
  354. * loaded and become available.
  355. *
  356. * @const {string}
  357. * @export
  358. */
  359. shaka.ads.Utils.IMA_STREAM_MANAGER_LOADED = 'ima-stream-manager-loaded';
  360. /**
  361. * The event name for when the ad was clicked.
  362. *
  363. * @const {string}
  364. * @export
  365. */
  366. shaka.ads.Utils.AD_CLICKED = 'ad-clicked';
  367. /**
  368. * The event name for when there is an update to the current ad's progress.
  369. *
  370. * @const {string}
  371. * @export
  372. */
  373. shaka.ads.Utils.AD_PROGRESS = 'ad-progress';
  374. /**
  375. * The event name for when the ad is buffering.
  376. *
  377. * @const {string}
  378. * @export
  379. */
  380. shaka.ads.Utils.AD_BUFFERING = 'ad-buffering';
  381. /**
  382. * The event name for when the ad's URL was hit.
  383. *
  384. * @const {string}
  385. * @export
  386. */
  387. shaka.ads.Utils.AD_IMPRESSION = 'ad-impression';
  388. /**
  389. * The event name for when the ad's duration changed.
  390. *
  391. * @const {string}
  392. * @export
  393. */
  394. shaka.ads.Utils.AD_DURATION_CHANGED = 'ad-duration-changed';
  395. /**
  396. * The event name for when the ad was closed by the user.
  397. *
  398. * @const {string}
  399. * @export
  400. */
  401. shaka.ads.Utils.AD_CLOSED = 'ad-closed';
  402. /**
  403. * The event name for when the ad data becomes available.
  404. *
  405. * @const {string}
  406. * @export
  407. */
  408. shaka.ads.Utils.AD_LOADED = 'ad-loaded';
  409. /**
  410. * The event name for when all the ads were completed.
  411. *
  412. * @const {string}
  413. * @export
  414. */
  415. shaka.ads.Utils.ALL_ADS_COMPLETED = 'all-ads-completed';
  416. /**
  417. * The event name for when the ad changes from or to linear.
  418. *
  419. * @const {string}
  420. * @export
  421. */
  422. shaka.ads.Utils.AD_LINEAR_CHANGED = 'ad-linear-changed';
  423. /**
  424. * The event name for when the ad's metadata becomes available.
  425. *
  426. * @const {string}
  427. * @export
  428. */
  429. shaka.ads.Utils.AD_METADATA = 'ad-metadata';
  430. /**
  431. * The event name for when the ad display encountered a recoverable
  432. * error.
  433. *
  434. * @const {string}
  435. * @export
  436. */
  437. shaka.ads.Utils.AD_RECOVERABLE_ERROR = 'ad-recoverable-error';
  438. /**
  439. * The event name for when the ad manager dispatch errors.
  440. *
  441. * @const {string}
  442. * @export
  443. */
  444. shaka.ads.Utils.AD_ERROR = 'ad-error';
  445. /**
  446. * The event name for when the client side SDK signalled its readiness
  447. * to play a VPAID ad or an ad rule.
  448. *
  449. * @const {string}
  450. * @export
  451. */
  452. shaka.ads.Utils.AD_BREAK_READY = 'ad-break-ready';
  453. /**
  454. * The event name for when the interaction callback for the ad was
  455. * triggered.
  456. *
  457. * @const {string}
  458. * @export
  459. */
  460. shaka.ads.Utils.AD_INTERACTION = 'ad-interaction';
  461. /**
  462. * The name of the event for when an ad requires the main content to be paused.
  463. * Fired when the platform does not support multiple media elements.
  464. *
  465. * @const {string}
  466. * @export
  467. */
  468. shaka.ads.Utils.AD_CONTENT_PAUSE_REQUESTED = 'ad-content-pause-requested';
  469. /**
  470. * The name of the event for when an ad requires the main content to be resumed.
  471. * Fired when the platform does not support multiple media elements.
  472. *
  473. * @const {string}
  474. * @export
  475. */
  476. shaka.ads.Utils.AD_CONTENT_RESUME_REQUESTED = 'ad-content-resume-requested';
  477. /**
  478. * The name of the event for when an ad requires the video of the main content
  479. * to be attached.
  480. *
  481. * @const {string}
  482. * @export
  483. */
  484. shaka.ads.Utils.AD_CONTENT_ATTACH_REQUESTED = 'ad-content-attach-requested';