Source: lib/ads/interstitial_ad_manager.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.ads.InterstitialAdManager');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.Player');
  9. goog.require('shaka.ads.InterstitialAd');
  10. goog.require('shaka.ads.InterstitialStaticAd');
  11. goog.require('shaka.ads.Utils');
  12. goog.require('shaka.log');
  13. goog.require('shaka.media.PreloadManager');
  14. goog.require('shaka.net.NetworkingEngine');
  15. goog.require('shaka.net.NetworkingUtils');
  16. goog.require('shaka.util.Dom');
  17. goog.require('shaka.util.Error');
  18. goog.require('shaka.util.EventManager');
  19. goog.require('shaka.util.FakeEvent');
  20. goog.require('shaka.util.IReleasable');
  21. goog.require('shaka.util.Platform');
  22. goog.require('shaka.util.PublicPromise');
  23. goog.require('shaka.util.StringUtils');
  24. goog.require('shaka.util.Timer');
  25. goog.require('shaka.util.TXml');
  26. /**
  27. * A class responsible for Interstitial ad interactions.
  28. *
  29. * @implements {shaka.util.IReleasable}
  30. */
  31. shaka.ads.InterstitialAdManager = class {
  32. /**
  33. * @param {HTMLElement} adContainer
  34. * @param {shaka.Player} basePlayer
  35. * @param {HTMLMediaElement} baseVideo
  36. * @param {function(!shaka.util.FakeEvent)} onEvent
  37. */
  38. constructor(adContainer, basePlayer, baseVideo, onEvent) {
  39. /** @private {?shaka.extern.AdsConfiguration} */
  40. this.config_ = null;
  41. /** @private {HTMLElement} */
  42. this.adContainer_ = adContainer;
  43. /** @private {shaka.Player} */
  44. this.basePlayer_ = basePlayer;
  45. /** @private {HTMLMediaElement} */
  46. this.baseVideo_ = baseVideo;
  47. /** @private {?HTMLMediaElement} */
  48. this.adVideo_ = null;
  49. /** @private {boolean} */
  50. this.usingBaseVideo_ = true;
  51. /** @private {HTMLMediaElement} */
  52. this.video_ = this.baseVideo_;
  53. /** @private {function(!shaka.util.FakeEvent)} */
  54. this.onEvent_ = onEvent;
  55. /** @private {!Set<string>} */
  56. this.interstitialIds_ = new Set();
  57. /** @private {!Set<shaka.extern.AdInterstitial>} */
  58. this.interstitials_ = new Set();
  59. /**
  60. * @private {!Map<shaka.extern.AdInterstitial,
  61. * Promise<?shaka.media.PreloadManager>>}
  62. */
  63. this.preloadManagerInterstitials_ = new Map();
  64. /** @private {shaka.Player} */
  65. this.player_ = new shaka.Player();
  66. this.updatePlayerConfig_();
  67. /** @private {shaka.util.EventManager} */
  68. this.eventManager_ = new shaka.util.EventManager();
  69. /** @private {shaka.util.EventManager} */
  70. this.adEventManager_ = new shaka.util.EventManager();
  71. /** @private {boolean} */
  72. this.playingAd_ = false;
  73. /** @private {?number} */
  74. this.lastTime_ = null;
  75. /** @private {?shaka.extern.AdInterstitial} */
  76. this.lastPlayedAd_ = null;
  77. /** @private {?shaka.util.Timer} */
  78. this.playoutLimitTimer_ = null;
  79. /** @private {?function()} */
  80. this.lastOnSkip_ = null;
  81. this.eventManager_.listen(this.baseVideo_, 'timeupdate', () => {
  82. if (this.playingAd_ || this.lastTime_ ||
  83. this.basePlayer_.isRemotePlayback()) {
  84. return;
  85. }
  86. this.lastTime_ = this.baseVideo_.currentTime;
  87. const currentInterstitial = this.getCurrentInterstitial_(
  88. /* needPreRoll= */ true);
  89. if (currentInterstitial) {
  90. this.setupAd_(currentInterstitial, /* sequenceLength= */ 1,
  91. /* adPosition= */ 1, /* initialTime= */ Date.now());
  92. }
  93. });
  94. const checkForInterstitials = () => {
  95. if (this.playingAd_ || !this.lastTime_ ||
  96. this.basePlayer_.isRemotePlayback()) {
  97. return;
  98. }
  99. this.lastTime_ = this.baseVideo_.currentTime;
  100. // Remove last played add when the new time is before to the ad time.
  101. if (this.lastPlayedAd_ &&
  102. !this.lastPlayedAd_.pre && !this.lastPlayedAd_.post &&
  103. this.lastTime_ < this.lastPlayedAd_.startTime) {
  104. this.lastPlayedAd_ = null;
  105. }
  106. const currentInterstitial = this.getCurrentInterstitial_();
  107. if (currentInterstitial) {
  108. this.setupAd_(currentInterstitial, /* sequenceLength= */ 1,
  109. /* adPosition= */ 1, /* initialTime= */ Date.now());
  110. }
  111. };
  112. this.eventManager_.listen(this.baseVideo_, 'ended', () => {
  113. checkForInterstitials();
  114. });
  115. /** @private {shaka.util.Timer} */
  116. this.timeUpdateTimer_ = new shaka.util.Timer(checkForInterstitials);
  117. if ('requestVideoFrameCallback' in this.baseVideo_ &&
  118. !shaka.util.Platform.isSmartTV()) {
  119. const baseVideo = /** @type {!HTMLVideoElement} */ (this.baseVideo_);
  120. const videoFrameCallback = () => {
  121. checkForInterstitials();
  122. baseVideo.requestVideoFrameCallback(videoFrameCallback);
  123. };
  124. baseVideo.requestVideoFrameCallback(videoFrameCallback);
  125. } else {
  126. this.timeUpdateTimer_.tickEvery(/* seconds= */ 0.025);
  127. }
  128. /** @private {shaka.util.Timer} */
  129. this.pollTimer_ = new shaka.util.Timer(async () => {
  130. if (this.interstitials_.size && this.lastTime_ != null) {
  131. const currentLoadMode = this.basePlayer_.getLoadMode();
  132. if (currentLoadMode == shaka.Player.LoadMode.DESTROYED ||
  133. currentLoadMode == shaka.Player.LoadMode.NOT_LOADED) {
  134. return;
  135. }
  136. let cuepointsChanged = false;
  137. const interstitials = Array.from(this.interstitials_);
  138. const seekRange = this.basePlayer_.seekRange();
  139. for (const interstitial of interstitials) {
  140. if (interstitial == this.lastPlayedAd_) {
  141. continue;
  142. }
  143. const comparisonTime = interstitial.endTime || interstitial.startTime;
  144. if ((seekRange.start - comparisonTime) >= 1) {
  145. if (this.preloadManagerInterstitials_.has(interstitial)) {
  146. const preloadManager =
  147. // eslint-disable-next-line no-await-in-loop
  148. await this.preloadManagerInterstitials_.get(interstitial);
  149. if (preloadManager) {
  150. preloadManager.destroy();
  151. }
  152. this.preloadManagerInterstitials_.delete(interstitial);
  153. }
  154. const interstitialId = JSON.stringify(interstitial);
  155. if (this.interstitialIds_.has(interstitialId)) {
  156. this.interstitialIds_.delete(interstitialId);
  157. }
  158. this.interstitials_.delete(interstitial);
  159. if (!interstitial.overlay) {
  160. cuepointsChanged = true;
  161. }
  162. } else {
  163. const difference = interstitial.startTime - this.lastTime_;
  164. if (difference > 0 && difference <= 10) {
  165. if (!this.preloadManagerInterstitials_.has(interstitial) &&
  166. this.isPreloadAllowed_(interstitial)) {
  167. this.preloadManagerInterstitials_.set(
  168. interstitial, this.player_.preload(
  169. interstitial.uri,
  170. /* startTime= */ null,
  171. interstitial.mimeType || undefined));
  172. }
  173. }
  174. }
  175. }
  176. if (cuepointsChanged) {
  177. this.cuepointsChanged_();
  178. }
  179. }
  180. }).tickEvery(/* seconds= */ 1);
  181. }
  182. /**
  183. * Called by the AdManager to provide an updated configuration any time it
  184. * changes.
  185. *
  186. * @param {shaka.extern.AdsConfiguration} config
  187. */
  188. configure(config) {
  189. this.config_ = config;
  190. this.determineIfUsingBaseVideo_();
  191. }
  192. /**
  193. * @private
  194. */
  195. determineIfUsingBaseVideo_() {
  196. if (!this.adContainer_ || !this.config_ || this.playingAd_) {
  197. return;
  198. }
  199. let supportsMultipleMediaElements =
  200. this.config_.supportsMultipleMediaElements;
  201. const video = /** @type {HTMLVideoElement} */(this.baseVideo_);
  202. if (video.webkitSupportsFullscreen && video.webkitDisplayingFullscreen) {
  203. supportsMultipleMediaElements = false;
  204. }
  205. if (this.usingBaseVideo_ != supportsMultipleMediaElements) {
  206. return;
  207. }
  208. this.usingBaseVideo_ = !supportsMultipleMediaElements;
  209. if (this.usingBaseVideo_) {
  210. this.video_ = this.baseVideo_;
  211. if (this.adVideo_) {
  212. if (this.adVideo_.parentElement) {
  213. this.adContainer_.removeChild(this.adVideo_);
  214. }
  215. this.adVideo_ = null;
  216. }
  217. } else {
  218. if (!this.adVideo_) {
  219. this.adVideo_ = this.createMediaElement_();
  220. }
  221. this.video_ = this.adVideo_;
  222. }
  223. }
  224. /**
  225. * Resets the Interstitial manager and removes any continuous polling.
  226. */
  227. stop() {
  228. if (this.adEventManager_) {
  229. this.adEventManager_.removeAll();
  230. }
  231. this.interstitialIds_.clear();
  232. this.interstitials_.clear();
  233. this.player_.destroyAllPreloads();
  234. this.preloadManagerInterstitials_.clear();
  235. this.player_.detach();
  236. this.playingAd_ = false;
  237. this.lastTime_ = null;
  238. this.lastPlayedAd_ = null;
  239. this.usingBaseVideo_ = true;
  240. this.video_ = this.baseVideo_;
  241. this.adVideo_ = null;
  242. if (this.adContainer_) {
  243. shaka.util.Dom.removeAllChildren(this.adContainer_);
  244. }
  245. if (this.playoutLimitTimer_) {
  246. this.playoutLimitTimer_.stop();
  247. this.playoutLimitTimer_ = null;
  248. }
  249. }
  250. /** @override */
  251. release() {
  252. this.stop();
  253. if (this.eventManager_) {
  254. this.eventManager_.release();
  255. }
  256. if (this.adEventManager_) {
  257. this.adEventManager_.release();
  258. }
  259. if (this.timeUpdateTimer_) {
  260. this.timeUpdateTimer_.stop();
  261. this.timeUpdateTimer_ = null;
  262. }
  263. if (this.pollTimer_) {
  264. this.pollTimer_.stop();
  265. this.pollTimer_ = null;
  266. }
  267. this.player_.destroy();
  268. }
  269. /**
  270. * @return {shaka.Player}
  271. */
  272. getPlayer() {
  273. return this.player_;
  274. }
  275. /**
  276. * @param {shaka.extern.HLSInterstitial} hlsInterstitial
  277. */
  278. async addMetadata(hlsInterstitial) {
  279. this.updatePlayerConfig_();
  280. const adInterstitials = await this.getInterstitialsInfo_(hlsInterstitial);
  281. if (adInterstitials.length) {
  282. this.addInterstitials(adInterstitials);
  283. } else {
  284. shaka.log.alwaysWarn('Unsupported HLS interstitial', hlsInterstitial);
  285. }
  286. }
  287. /**
  288. * @param {shaka.extern.TimelineRegionInfo} region
  289. */
  290. addRegion(region) {
  291. let alternativeMPDUri;
  292. for (const node of region.eventNode.children) {
  293. if (node.tagName == 'AlternativeMPD') {
  294. const uri = node.attributes['uri'];
  295. if (uri) {
  296. alternativeMPDUri = uri;
  297. break;
  298. }
  299. }
  300. }
  301. if (!alternativeMPDUri) {
  302. shaka.log.alwaysWarn('Unsupported MPD alternate', region);
  303. return;
  304. }
  305. const isReplace =
  306. region.schemeIdUri == 'urn:mpeg:dash:event:alternativeMPD:replace:2025';
  307. const isInsert =
  308. region.schemeIdUri == 'urn:mpeg:dash:event:alternativeMPD:insert:2025';
  309. if (!isReplace && !isInsert) {
  310. shaka.log.warning('Unsupported MPD alternate', region);
  311. return;
  312. }
  313. /** @type {!shaka.extern.AdInterstitial} */
  314. const interstitial = {
  315. id: region.id,
  316. groupId: null,
  317. startTime: region.startTime,
  318. endTime: region.endTime,
  319. uri: alternativeMPDUri,
  320. mimeType: null,
  321. isSkippable: false,
  322. skipOffset: null,
  323. skipFor: null,
  324. canJump: true,
  325. resumeOffset: isInsert ? 0 : null,
  326. playoutLimit: null,
  327. once: false,
  328. pre: false,
  329. post: false,
  330. timelineRange: isReplace && !isInsert,
  331. loop: false,
  332. overlay: null,
  333. };
  334. this.addInterstitials([interstitial]);
  335. }
  336. /**
  337. * @param {shaka.extern.TimelineRegionInfo} region
  338. */
  339. addOverlayRegion(region) {
  340. const TXml = shaka.util.TXml;
  341. goog.asserts.assert(region.eventNode, 'Need a region eventNode');
  342. const overlayEvent = TXml.findChild(region.eventNode, 'OverlayEvent');
  343. const uri = overlayEvent.attributes['uri'];
  344. const mimeType = overlayEvent.attributes['mimeType'];
  345. const loop = overlayEvent.attributes['loop'] == 'true';
  346. if (!uri || !mimeType) {
  347. shaka.log.warning('Unsupported OverlayEvent', region);
  348. return;
  349. }
  350. /** @type {!shaka.extern.AdInterstitialOverlay} */
  351. let overlay = {
  352. viewport: {
  353. x: 1920,
  354. y: 1080,
  355. },
  356. topLeft: {
  357. x: 0,
  358. y: 0,
  359. },
  360. size: {
  361. x: 1920,
  362. y: 1080,
  363. },
  364. };
  365. const viewport = TXml.findChild(overlayEvent, 'Viewport');
  366. const topLeft = TXml.findChild(overlayEvent, 'TopLeft');
  367. const size = TXml.findChild(overlayEvent, 'Size');
  368. if (viewport && topLeft && size) {
  369. const viewportX = TXml.parseAttr(viewport, 'x', TXml.parseInt);
  370. if (viewportX == null) {
  371. shaka.log.warning('Unsupported OverlayEvent', region);
  372. return;
  373. }
  374. const viewportY = TXml.parseAttr(viewport, 'y', TXml.parseInt);
  375. if (viewportY == null) {
  376. shaka.log.warning('Unsupported OverlayEvent', region);
  377. return;
  378. }
  379. const topLeftX = TXml.parseAttr(topLeft, 'x', TXml.parseInt);
  380. if (topLeftX == null) {
  381. shaka.log.warning('Unsupported OverlayEvent', region);
  382. return;
  383. }
  384. const topLeftY = TXml.parseAttr(topLeft, 'y', TXml.parseInt);
  385. if (topLeftY == null) {
  386. shaka.log.warning('Unsupported OverlayEvent', region);
  387. return;
  388. }
  389. const sizeX = TXml.parseAttr(size, 'x', TXml.parseInt);
  390. if (sizeX == null) {
  391. shaka.log.warning('Unsupported OverlayEvent', region);
  392. return;
  393. }
  394. const sizeY = TXml.parseAttr(size, 'y', TXml.parseInt);
  395. if (sizeY == null) {
  396. shaka.log.warning('Unsupported OverlayEvent', region);
  397. return;
  398. }
  399. overlay = {
  400. viewport: {
  401. x: viewportX,
  402. y: viewportY,
  403. },
  404. topLeft: {
  405. x: topLeftX,
  406. y: topLeftY,
  407. },
  408. size: {
  409. x: sizeX,
  410. y: sizeY,
  411. },
  412. };
  413. }
  414. /** @type {!shaka.extern.AdInterstitial} */
  415. const interstitial = {
  416. id: region.id,
  417. groupId: null,
  418. startTime: region.startTime,
  419. endTime: region.endTime,
  420. uri,
  421. mimeType,
  422. isSkippable: false,
  423. skipOffset: null,
  424. skipFor: null,
  425. canJump: true,
  426. resumeOffset: null,
  427. playoutLimit: null,
  428. once: false,
  429. pre: false,
  430. post: false,
  431. timelineRange: true,
  432. loop,
  433. overlay,
  434. };
  435. this.addInterstitials([interstitial]);
  436. }
  437. /**
  438. * @param {string} url
  439. * @return {!Promise}
  440. */
  441. async addAdUrlInterstitial(url) {
  442. const NetworkingEngine = shaka.net.NetworkingEngine;
  443. const context = {
  444. type: NetworkingEngine.AdvancedRequestType.INTERSTITIAL_AD_URL,
  445. };
  446. const responseData = await this.makeAdRequest_(url, context);
  447. const data = shaka.util.TXml.parseXml(responseData, 'VAST,vmap:VMAP');
  448. if (!data) {
  449. throw new shaka.util.Error(
  450. shaka.util.Error.Severity.CRITICAL,
  451. shaka.util.Error.Category.ADS,
  452. shaka.util.Error.Code.VAST_INVALID_XML);
  453. }
  454. let interstitials = [];
  455. if (data.tagName == 'VAST') {
  456. interstitials = shaka.ads.Utils.parseVastToInterstitials(
  457. data, this.lastTime_);
  458. } else if (data.tagName == 'vmap:VMAP') {
  459. for (const ad of shaka.ads.Utils.parseVMAP(data)) {
  460. // eslint-disable-next-line no-await-in-loop
  461. const vastResponseData = await this.makeAdRequest_(ad.uri, context);
  462. const vast = shaka.util.TXml.parseXml(vastResponseData, 'VAST');
  463. if (!vast) {
  464. throw new shaka.util.Error(
  465. shaka.util.Error.Severity.CRITICAL,
  466. shaka.util.Error.Category.ADS,
  467. shaka.util.Error.Code.VAST_INVALID_XML);
  468. }
  469. interstitials.push(...shaka.ads.Utils.parseVastToInterstitials(
  470. vast, ad.time));
  471. }
  472. }
  473. this.addInterstitials(interstitials);
  474. }
  475. /**
  476. * @param {!Array<shaka.extern.AdInterstitial>} interstitials
  477. */
  478. async addInterstitials(interstitials) {
  479. let cuepointsChanged = false;
  480. for (const interstitial of interstitials) {
  481. if (!interstitial.uri) {
  482. shaka.log.alwaysWarn('Missing URL in interstitial', interstitial);
  483. continue;
  484. }
  485. if (!interstitial.mimeType) {
  486. try {
  487. const netEngine = this.player_.getNetworkingEngine();
  488. goog.asserts.assert(netEngine, 'Need networking engine');
  489. // eslint-disable-next-line no-await-in-loop
  490. interstitial.mimeType = await shaka.net.NetworkingUtils.getMimeType(
  491. interstitial.uri, netEngine,
  492. this.basePlayer_.getConfiguration().streaming.retryParameters);
  493. } catch (error) {}
  494. }
  495. const interstitialId = interstitial.id || JSON.stringify(interstitial);
  496. if (this.interstitialIds_.has(interstitialId)) {
  497. continue;
  498. }
  499. if (interstitial.loop && !interstitial.overlay) {
  500. shaka.log.alwaysWarn('Loop is only supported in overlay interstitials',
  501. interstitial);
  502. }
  503. if (!interstitial.overlay) {
  504. cuepointsChanged = true;
  505. }
  506. this.interstitialIds_.add(interstitialId);
  507. this.interstitials_.add(interstitial);
  508. let shouldPreload = false;
  509. if (interstitial.pre && this.lastTime_ == null) {
  510. shouldPreload = true;
  511. } else if (interstitial.startTime == 0 && !interstitial.canJump) {
  512. shouldPreload = true;
  513. } else if (this.lastTime_ != null) {
  514. const difference = interstitial.startTime - this.lastTime_;
  515. if (difference > 0 && difference <= 10) {
  516. shouldPreload = true;
  517. }
  518. }
  519. if (shouldPreload) {
  520. if (!this.preloadManagerInterstitials_.has(interstitial) &&
  521. this.isPreloadAllowed_(interstitial)) {
  522. this.preloadManagerInterstitials_.set(
  523. interstitial, this.player_.preload(
  524. interstitial.uri,
  525. /* startTime= */ null,
  526. interstitial.mimeType || undefined));
  527. }
  528. }
  529. }
  530. if (cuepointsChanged) {
  531. this.cuepointsChanged_();
  532. }
  533. }
  534. /**
  535. * @return {!HTMLMediaElement}
  536. * @private
  537. */
  538. createMediaElement_() {
  539. const video = /** @type {!HTMLMediaElement} */(
  540. document.createElement(this.baseVideo_.tagName));
  541. video.autoplay = true;
  542. video.style.position = 'absolute';
  543. video.style.top = '0';
  544. video.style.left = '0';
  545. video.style.width = '100%';
  546. video.style.height = '100%';
  547. video.style.display = 'none';
  548. video.setAttribute('playsinline', '');
  549. return video;
  550. }
  551. /**
  552. * @param {boolean=} needPreRoll
  553. * @param {?number=} numberToSkip
  554. * @return {?shaka.extern.AdInterstitial}
  555. * @private
  556. */
  557. getCurrentInterstitial_(needPreRoll = false, numberToSkip = null) {
  558. let skipped = 0;
  559. let currentInterstitial = null;
  560. if (this.interstitials_.size && this.lastTime_ != null) {
  561. const isEnded = this.baseVideo_.ended;
  562. const interstitials = Array.from(this.interstitials_).sort((a, b) => {
  563. return b.startTime - a.startTime;
  564. });
  565. const roundDecimals = (number) => {
  566. return Math.round(number * 1000) / 1000;
  567. };
  568. let interstitialsToCheck = interstitials;
  569. if (needPreRoll) {
  570. interstitialsToCheck = interstitials.filter((i) => i.pre);
  571. } else if (isEnded) {
  572. interstitialsToCheck = interstitials.filter((i) => i.post);
  573. } else {
  574. interstitialsToCheck = interstitials.filter((i) => !i.pre && !i.post);
  575. }
  576. for (const interstitial of interstitialsToCheck) {
  577. let isValid = false;
  578. if (needPreRoll) {
  579. isValid = interstitial.pre;
  580. } else if (isEnded) {
  581. isValid = interstitial.post;
  582. } else if (!interstitial.pre && !interstitial.post) {
  583. const difference =
  584. this.lastTime_ - roundDecimals(interstitial.startTime);
  585. if (difference > 0 &&
  586. (difference <= 1 || !interstitial.canJump)) {
  587. if (numberToSkip == null && this.lastPlayedAd_ &&
  588. !this.lastPlayedAd_.pre && !this.lastPlayedAd_.post &&
  589. this.lastPlayedAd_.startTime >= interstitial.startTime) {
  590. isValid = false;
  591. } else {
  592. isValid = true;
  593. }
  594. }
  595. }
  596. if (isValid && (!this.lastPlayedAd_ ||
  597. interstitial.startTime >= this.lastPlayedAd_.startTime)) {
  598. if (skipped == (numberToSkip || 0)) {
  599. currentInterstitial = interstitial;
  600. } else if (currentInterstitial && !interstitial.canJump) {
  601. const currentStartTime =
  602. roundDecimals(currentInterstitial.startTime);
  603. const newStartTime =
  604. roundDecimals(interstitial.startTime);
  605. if (newStartTime - currentStartTime > 0.001) {
  606. currentInterstitial = interstitial;
  607. skipped = 0;
  608. }
  609. }
  610. skipped++;
  611. }
  612. }
  613. }
  614. return currentInterstitial;
  615. }
  616. /**
  617. * @param {shaka.extern.AdInterstitial} interstitial
  618. * @param {number} sequenceLength
  619. * @param {number} adPosition
  620. * @param {number} initialTime the clock time the ad started at
  621. * @param {number=} oncePlayed
  622. * @private
  623. */
  624. setupAd_(interstitial, sequenceLength, adPosition, initialTime,
  625. oncePlayed = 0) {
  626. shaka.log.info('Starting interstitial',
  627. interstitial.startTime, 'at', this.lastTime_);
  628. this.lastPlayedAd_ = interstitial;
  629. this.determineIfUsingBaseVideo_();
  630. goog.asserts.assert(this.video_, 'Must have video');
  631. if (!this.video_.parentElement && this.adContainer_) {
  632. this.adContainer_.appendChild(this.video_);
  633. }
  634. if (adPosition == 1 && sequenceLength == 1) {
  635. sequenceLength = Array.from(this.interstitials_).filter((i) => {
  636. if (interstitial.pre) {
  637. return i.pre == interstitial.pre;
  638. } else if (interstitial.post) {
  639. return i.post == interstitial.post;
  640. }
  641. return Math.abs(i.startTime - interstitial.startTime) < 0.001;
  642. }).length;
  643. }
  644. if (interstitial.once) {
  645. oncePlayed++;
  646. this.interstitials_.delete(interstitial);
  647. if (!interstitial.overlay) {
  648. this.cuepointsChanged_();
  649. }
  650. }
  651. if (interstitial.mimeType) {
  652. if (interstitial.mimeType.startsWith('image/') ||
  653. interstitial.mimeType === 'text/html') {
  654. if (!interstitial.overlay) {
  655. shaka.log.alwaysWarn('Unsupported interstitial', interstitial);
  656. return;
  657. }
  658. this.setupStaticAd_(interstitial, sequenceLength, adPosition,
  659. oncePlayed);
  660. return;
  661. }
  662. }
  663. if (this.usingBaseVideo_ && interstitial.overlay) {
  664. shaka.log.alwaysWarn('Unsupported interstitial', interstitial);
  665. return;
  666. }
  667. this.setupVideoAd_(interstitial, sequenceLength, adPosition, initialTime,
  668. oncePlayed);
  669. }
  670. /**
  671. * @param {shaka.extern.AdInterstitial} interstitial
  672. * @param {number} sequenceLength
  673. * @param {number} adPosition
  674. * @param {number} oncePlayed
  675. * @private
  676. */
  677. setupStaticAd_(interstitial, sequenceLength, adPosition, oncePlayed) {
  678. const overlay = interstitial.overlay;
  679. goog.asserts.assert(overlay, 'Must have overlay');
  680. const tagName = interstitial.mimeType == 'text/html' ? 'iframe' : 'img';
  681. const htmlElement = /** @type {!(HTMLImageElement|HTMLIFrameElement)} */ (
  682. document.createElement(tagName));
  683. htmlElement.style.objectFit = 'contain';
  684. htmlElement.style.position = 'absolute';
  685. htmlElement.style.border = 'none';
  686. const basicTask = () => {
  687. if (this.playoutLimitTimer_) {
  688. this.playoutLimitTimer_.stop();
  689. this.playoutLimitTimer_ = null;
  690. }
  691. this.adContainer_.removeChild(htmlElement);
  692. this.onEvent_(
  693. new shaka.util.FakeEvent(shaka.ads.Utils.AD_STOPPED));
  694. const nextCurrentInterstitial = this.getCurrentInterstitial_(
  695. interstitial.pre, adPosition - oncePlayed);
  696. if (nextCurrentInterstitial) {
  697. this.adEventManager_.removeAll();
  698. this.setupAd_(nextCurrentInterstitial, sequenceLength,
  699. ++adPosition, /* initialTime= */ Date.now(), oncePlayed);
  700. } else {
  701. this.playingAd_ = false;
  702. }
  703. };
  704. const ad = new shaka.ads.InterstitialStaticAd(
  705. interstitial, sequenceLength, adPosition);
  706. this.onEvent_(
  707. new shaka.util.FakeEvent(shaka.ads.Utils.AD_STARTED,
  708. (new Map()).set('ad', ad)));
  709. if (tagName == 'iframe') {
  710. htmlElement.src = interstitial.uri;
  711. } else {
  712. htmlElement.src = interstitial.uri;
  713. htmlElement.onerror = (e) => {
  714. this.onEvent_(new shaka.util.FakeEvent(shaka.ads.Utils.AD_ERROR,
  715. (new Map()).set('originalEvent', e)));
  716. basicTask();
  717. };
  718. }
  719. const viewport = overlay.viewport;
  720. const topLeft = overlay.topLeft;
  721. const size = overlay.size;
  722. // Special case for VAST non-linear ads
  723. if (viewport.x == 0 && viewport.y == 0) {
  724. htmlElement.width = interstitial.overlay.size.x;
  725. htmlElement.height = interstitial.overlay.size.y;
  726. htmlElement.style.bottom = '10%';
  727. htmlElement.style.left = '0';
  728. htmlElement.style.right = '0';
  729. htmlElement.style.width = '100%';
  730. if (!interstitial.overlay.size.y && tagName == 'iframe') {
  731. htmlElement.style.height = 'auto';
  732. }
  733. } else {
  734. htmlElement.style.height = (size.y / viewport.y * 100) + '%';
  735. htmlElement.style.left = (topLeft.x / viewport.x * 100) + '%';
  736. htmlElement.style.top = (topLeft.y / viewport.y * 100) + '%';
  737. htmlElement.style.width = (size.x / viewport.x * 100) + '%';
  738. }
  739. this.adContainer_.appendChild(htmlElement);
  740. const startTime = Date.now();
  741. if (this.playoutLimitTimer_) {
  742. this.playoutLimitTimer_.stop();
  743. }
  744. this.playoutLimitTimer_ = new shaka.util.Timer(() => {
  745. if (interstitial.playoutLimit &&
  746. (Date.now() - startTime) / 1000 > interstitial.playoutLimit) {
  747. this.onEvent_(
  748. new shaka.util.FakeEvent(shaka.ads.Utils.AD_COMPLETE));
  749. basicTask();
  750. } else if (interstitial.endTime &&
  751. this.baseVideo_.currentTime > interstitial.endTime) {
  752. this.onEvent_(
  753. new shaka.util.FakeEvent(shaka.ads.Utils.AD_COMPLETE));
  754. basicTask();
  755. } else if (this.baseVideo_.currentTime < interstitial.startTime) {
  756. this.onEvent_(
  757. new shaka.util.FakeEvent(shaka.ads.Utils.AD_SKIPPED));
  758. basicTask();
  759. }
  760. });
  761. if (interstitial.playoutLimit && !interstitial.endTime) {
  762. this.playoutLimitTimer_.tickAfter(interstitial.playoutLimit);
  763. } else if (interstitial.endTime) {
  764. this.playoutLimitTimer_.tickEvery(/* seconds= */ 0.025);
  765. }
  766. }
  767. /**
  768. * @param {shaka.extern.AdInterstitial} interstitial
  769. * @param {number} sequenceLength
  770. * @param {number} adPosition
  771. * @param {number} initialTime the clock time the ad started at
  772. * @param {number} oncePlayed
  773. * @private
  774. */
  775. async setupVideoAd_(interstitial, sequenceLength, adPosition, initialTime,
  776. oncePlayed) {
  777. goog.asserts.assert(this.video_, 'Must have video');
  778. const startTime = Date.now();
  779. this.playingAd_ = true;
  780. if (this.usingBaseVideo_ && adPosition == 1) {
  781. this.onEvent_(new shaka.util.FakeEvent(
  782. shaka.ads.Utils.AD_CONTENT_PAUSE_REQUESTED,
  783. (new Map()).set('saveLivePosition', true)));
  784. const detachBasePlayerPromise = new shaka.util.PublicPromise();
  785. const checkState = async (e) => {
  786. if (e['state'] == 'detach') {
  787. if (shaka.util.Platform.isSmartTV()) {
  788. await new Promise(
  789. (resolve) => new shaka.util.Timer(resolve).tickAfter(0.1));
  790. }
  791. detachBasePlayerPromise.resolve();
  792. this.adEventManager_.unlisten(
  793. this.basePlayer_, 'onstatechange', checkState);
  794. }
  795. };
  796. this.adEventManager_.listen(
  797. this.basePlayer_, 'onstatechange', checkState);
  798. await detachBasePlayerPromise;
  799. }
  800. if (!this.usingBaseVideo_) {
  801. this.video_.style.display = '';
  802. if (interstitial.overlay) {
  803. this.video_.loop = interstitial.loop;
  804. const viewport = interstitial.overlay.viewport;
  805. const topLeft = interstitial.overlay.topLeft;
  806. const size = interstitial.overlay.size;
  807. this.video_.style.height = (size.y / viewport.y * 100) + '%';
  808. this.video_.style.left = (topLeft.x / viewport.x * 100) + '%';
  809. this.video_.style.top = (topLeft.y / viewport.y * 100) + '%';
  810. this.video_.style.width = (size.x / viewport.x * 100) + '%';
  811. } else {
  812. this.baseVideo_.pause();
  813. if (interstitial.resumeOffset != null &&
  814. interstitial.resumeOffset != 0) {
  815. this.baseVideo_.currentTime += interstitial.resumeOffset;
  816. }
  817. this.video_.loop = false;
  818. this.video_.style.height = '100%';
  819. this.video_.style.left = '0';
  820. this.video_.style.top = '0';
  821. this.video_.style.width = '100%';
  822. }
  823. }
  824. let unloadingInterstitial = false;
  825. const updateBaseVideoTime = () => {
  826. if (!this.usingBaseVideo_ && !interstitial.overlay) {
  827. if (interstitial.resumeOffset == null) {
  828. if (interstitial.timelineRange && interstitial.endTime &&
  829. interstitial.endTime != Infinity) {
  830. if (this.baseVideo_.currentTime != interstitial.endTime) {
  831. this.baseVideo_.currentTime = interstitial.endTime;
  832. }
  833. } else {
  834. const now = Date.now();
  835. this.baseVideo_.currentTime += (now - initialTime) / 1000;
  836. initialTime = now;
  837. }
  838. }
  839. }
  840. };
  841. const basicTask = async (isSkip) => {
  842. updateBaseVideoTime();
  843. // Optimization to avoid returning to main content when there is another
  844. // interstitial below.
  845. let nextCurrentInterstitial = this.getCurrentInterstitial_(
  846. interstitial.pre, adPosition - oncePlayed);
  847. if (isSkip && interstitial.groupId) {
  848. while (nextCurrentInterstitial &&
  849. nextCurrentInterstitial.groupId == interstitial.groupId) {
  850. adPosition++;
  851. nextCurrentInterstitial = this.getCurrentInterstitial_(
  852. interstitial.pre, adPosition - oncePlayed);
  853. }
  854. }
  855. if (this.playoutLimitTimer_ && (!interstitial.groupId ||
  856. (nextCurrentInterstitial &&
  857. nextCurrentInterstitial.groupId != interstitial.groupId))) {
  858. this.playoutLimitTimer_.stop();
  859. this.playoutLimitTimer_ = null;
  860. }
  861. if (!nextCurrentInterstitial || nextCurrentInterstitial.overlay) {
  862. if (interstitial.post) {
  863. this.lastTime_ = null;
  864. this.lastPlayedAd_ = null;
  865. }
  866. if (this.usingBaseVideo_) {
  867. await this.player_.detach();
  868. } else {
  869. await this.player_.unload();
  870. }
  871. if (this.usingBaseVideo_) {
  872. let offset = interstitial.resumeOffset;
  873. if (offset == null) {
  874. if (interstitial.timelineRange && interstitial.endTime &&
  875. interstitial.endTime != Infinity) {
  876. offset = interstitial.endTime - (this.lastTime_ || 0);
  877. } else {
  878. offset = (Date.now() - initialTime) / 1000;
  879. }
  880. }
  881. this.onEvent_(new shaka.util.FakeEvent(
  882. shaka.ads.Utils.AD_CONTENT_RESUME_REQUESTED,
  883. (new Map()).set('offset', offset)));
  884. }
  885. this.onEvent_(
  886. new shaka.util.FakeEvent(shaka.ads.Utils.AD_STOPPED));
  887. this.adEventManager_.removeAll();
  888. this.playingAd_ = false;
  889. if (!this.usingBaseVideo_) {
  890. this.video_.style.display = 'none';
  891. updateBaseVideoTime();
  892. if (!this.baseVideo_.ended) {
  893. this.baseVideo_.play();
  894. }
  895. } else {
  896. this.cuepointsChanged_();
  897. }
  898. }
  899. this.determineIfUsingBaseVideo_();
  900. if (nextCurrentInterstitial) {
  901. this.onEvent_(
  902. new shaka.util.FakeEvent(shaka.ads.Utils.AD_STOPPED));
  903. this.adEventManager_.removeAll();
  904. this.setupAd_(nextCurrentInterstitial, sequenceLength,
  905. ++adPosition, initialTime, oncePlayed);
  906. }
  907. };
  908. const error = async (e) => {
  909. if (unloadingInterstitial) {
  910. return;
  911. }
  912. unloadingInterstitial = true;
  913. this.onEvent_(new shaka.util.FakeEvent(shaka.ads.Utils.AD_ERROR,
  914. (new Map()).set('originalEvent', e)));
  915. await basicTask(/* isSkip= */ false);
  916. };
  917. const complete = async () => {
  918. if (unloadingInterstitial) {
  919. return;
  920. }
  921. unloadingInterstitial = true;
  922. await basicTask(/* isSkip= */ false);
  923. this.onEvent_(
  924. new shaka.util.FakeEvent(shaka.ads.Utils.AD_COMPLETE));
  925. };
  926. this.lastOnSkip_ = async () => {
  927. if (unloadingInterstitial) {
  928. return;
  929. }
  930. unloadingInterstitial = true;
  931. this.onEvent_(new shaka.util.FakeEvent(shaka.ads.Utils.AD_SKIPPED));
  932. await basicTask(/* isSkip= */ true);
  933. };
  934. const ad = new shaka.ads.InterstitialAd(this.video_,
  935. interstitial, this.lastOnSkip_, sequenceLength, adPosition,
  936. !this.usingBaseVideo_);
  937. if (!this.usingBaseVideo_) {
  938. ad.setMuted(this.baseVideo_.muted);
  939. ad.setVolume(this.baseVideo_.volume);
  940. }
  941. this.onEvent_(
  942. new shaka.util.FakeEvent(shaka.ads.Utils.AD_STARTED,
  943. (new Map()).set('ad', ad)));
  944. let prevCanSkipNow = ad.canSkipNow();
  945. if (prevCanSkipNow) {
  946. this.onEvent_(new shaka.util.FakeEvent(
  947. shaka.ads.Utils.AD_SKIP_STATE_CHANGED));
  948. }
  949. this.adEventManager_.listenOnce(this.player_, 'error', error);
  950. this.adEventManager_.listen(this.video_, 'timeupdate', () => {
  951. const duration = this.video_.duration;
  952. if (!duration) {
  953. return;
  954. }
  955. const currentCanSkipNow = ad.canSkipNow();
  956. if (prevCanSkipNow != currentCanSkipNow &&
  957. ad.getRemainingTime() > 0 && ad.getDuration() > 0) {
  958. this.onEvent_(
  959. new shaka.util.FakeEvent(shaka.ads.Utils.AD_SKIP_STATE_CHANGED));
  960. }
  961. prevCanSkipNow = currentCanSkipNow;
  962. });
  963. this.adEventManager_.listenOnce(this.player_, 'firstquartile', () => {
  964. updateBaseVideoTime();
  965. this.onEvent_(
  966. new shaka.util.FakeEvent(shaka.ads.Utils.AD_FIRST_QUARTILE));
  967. });
  968. this.adEventManager_.listenOnce(this.player_, 'midpoint', () => {
  969. updateBaseVideoTime();
  970. this.onEvent_(
  971. new shaka.util.FakeEvent(shaka.ads.Utils.AD_MIDPOINT));
  972. });
  973. this.adEventManager_.listenOnce(this.player_, 'thirdquartile', () => {
  974. updateBaseVideoTime();
  975. this.onEvent_(
  976. new shaka.util.FakeEvent(shaka.ads.Utils.AD_THIRD_QUARTILE));
  977. });
  978. this.adEventManager_.listenOnce(this.player_, 'complete', complete);
  979. this.adEventManager_.listen(this.video_, 'play', () => {
  980. this.onEvent_(
  981. new shaka.util.FakeEvent(shaka.ads.Utils.AD_RESUMED));
  982. });
  983. this.adEventManager_.listen(this.video_, 'pause', () => {
  984. // playRangeEnd in src= causes the ended event not to be fired when that
  985. // position is reached, instead pause event is fired.
  986. const currentConfig = this.player_.getConfiguration();
  987. if (this.video_.currentTime >= currentConfig.playRangeEnd) {
  988. complete();
  989. return;
  990. }
  991. this.onEvent_(
  992. new shaka.util.FakeEvent(shaka.ads.Utils.AD_PAUSED));
  993. });
  994. this.adEventManager_.listen(this.video_, 'volumechange', () => {
  995. if (this.video_.muted) {
  996. this.onEvent_(
  997. new shaka.util.FakeEvent(shaka.ads.Utils.AD_MUTED));
  998. } else {
  999. this.onEvent_(
  1000. new shaka.util.FakeEvent(shaka.ads.Utils.AD_VOLUME_CHANGED));
  1001. }
  1002. });
  1003. try {
  1004. this.updatePlayerConfig_();
  1005. if (interstitial.startTime && interstitial.endTime &&
  1006. interstitial.endTime != Infinity &&
  1007. interstitial.startTime != interstitial.endTime) {
  1008. const duration = interstitial.endTime - interstitial.startTime;
  1009. if (duration > 0) {
  1010. this.player_.configure('playRangeEnd', duration);
  1011. }
  1012. }
  1013. if (interstitial.playoutLimit && !this.playoutLimitTimer_) {
  1014. this.playoutLimitTimer_ = new shaka.util.Timer(() => {
  1015. this.lastOnSkip_();
  1016. }).tickAfter(interstitial.playoutLimit);
  1017. this.player_.configure('playRangeEnd', interstitial.playoutLimit);
  1018. }
  1019. await this.player_.attach(this.video_);
  1020. if (this.preloadManagerInterstitials_.has(interstitial)) {
  1021. const preloadManager =
  1022. await this.preloadManagerInterstitials_.get(interstitial);
  1023. this.preloadManagerInterstitials_.delete(interstitial);
  1024. if (preloadManager) {
  1025. await this.player_.load(preloadManager);
  1026. } else {
  1027. await this.player_.load(
  1028. interstitial.uri,
  1029. /* startTime= */ null,
  1030. interstitial.mimeType || undefined);
  1031. }
  1032. } else {
  1033. await this.player_.load(
  1034. interstitial.uri,
  1035. /* startTime= */ null,
  1036. interstitial.mimeType || undefined);
  1037. }
  1038. const loadTime = (Date.now() - startTime) / 1000;
  1039. this.onEvent_(new shaka.util.FakeEvent(shaka.ads.Utils.ADS_LOADED,
  1040. (new Map()).set('loadTime', loadTime)));
  1041. if (this.usingBaseVideo_) {
  1042. this.baseVideo_.play();
  1043. }
  1044. } catch (e) {
  1045. if (!this.playingAd_) {
  1046. return;
  1047. }
  1048. error(e);
  1049. }
  1050. }
  1051. /**
  1052. * @param {shaka.extern.HLSInterstitial} hlsInterstitial
  1053. * @return {!Promise<!Array<shaka.extern.AdInterstitial>>}
  1054. * @private
  1055. */
  1056. async getInterstitialsInfo_(hlsInterstitial) {
  1057. const interstitialsAd = [];
  1058. if (!hlsInterstitial) {
  1059. return interstitialsAd;
  1060. }
  1061. const assetUri = hlsInterstitial.values.find((v) => v.key == 'X-ASSET-URI');
  1062. const assetList =
  1063. hlsInterstitial.values.find((v) => v.key == 'X-ASSET-LIST');
  1064. if (!assetUri && !assetList) {
  1065. return interstitialsAd;
  1066. }
  1067. let id = null;
  1068. const hlsInterstitialId = hlsInterstitial.values.find((v) => v.key == 'ID');
  1069. if (hlsInterstitialId) {
  1070. id = /** @type {string} */(hlsInterstitialId.data);
  1071. }
  1072. const startTime = id == null ?
  1073. Math.floor(hlsInterstitial.startTime * 10) / 10:
  1074. hlsInterstitial.startTime;
  1075. let endTime = hlsInterstitial.endTime;
  1076. if (hlsInterstitial.endTime && hlsInterstitial.endTime != Infinity &&
  1077. typeof(hlsInterstitial.endTime) == 'number') {
  1078. endTime = id == null ?
  1079. Math.floor(hlsInterstitial.endTime * 10) / 10:
  1080. hlsInterstitial.endTime;
  1081. }
  1082. const restrict = hlsInterstitial.values.find((v) => v.key == 'X-RESTRICT');
  1083. let isSkippable = true;
  1084. let canJump = true;
  1085. if (restrict && restrict.data) {
  1086. const data = /** @type {string} */(restrict.data);
  1087. isSkippable = !data.includes('SKIP');
  1088. canJump = !data.includes('JUMP');
  1089. }
  1090. let skipOffset = isSkippable ? 0 : null;
  1091. const enableSkipAfter =
  1092. hlsInterstitial.values.find((v) => v.key == 'X-ENABLE-SKIP-AFTER');
  1093. if (enableSkipAfter) {
  1094. const enableSkipAfterString = /** @type {string} */(enableSkipAfter.data);
  1095. skipOffset = parseFloat(enableSkipAfterString);
  1096. if (isNaN(skipOffset)) {
  1097. skipOffset = isSkippable ? 0 : null;
  1098. }
  1099. }
  1100. let skipFor = null;
  1101. const enableSkipFor =
  1102. hlsInterstitial.values.find((v) => v.key == 'X-ENABLE-SKIP-FOR');
  1103. if (enableSkipFor) {
  1104. const enableSkipForString = /** @type {string} */(enableSkipFor.data);
  1105. skipFor = parseFloat(enableSkipForString);
  1106. if (isNaN(skipOffset)) {
  1107. skipFor = null;
  1108. }
  1109. }
  1110. let resumeOffset = null;
  1111. const resume =
  1112. hlsInterstitial.values.find((v) => v.key == 'X-RESUME-OFFSET');
  1113. if (resume) {
  1114. const resumeOffsetString = /** @type {string} */(resume.data);
  1115. resumeOffset = parseFloat(resumeOffsetString);
  1116. if (isNaN(resumeOffset)) {
  1117. resumeOffset = null;
  1118. }
  1119. }
  1120. let playoutLimit = null;
  1121. const playout =
  1122. hlsInterstitial.values.find((v) => v.key == 'X-PLAYOUT-LIMIT');
  1123. if (playout) {
  1124. const playoutLimitString = /** @type {string} */(playout.data);
  1125. playoutLimit = parseFloat(playoutLimitString);
  1126. if (isNaN(playoutLimit)) {
  1127. playoutLimit = null;
  1128. }
  1129. }
  1130. let once = false;
  1131. let pre = false;
  1132. let post = false;
  1133. const cue = hlsInterstitial.values.find((v) => v.key == 'CUE');
  1134. if (cue) {
  1135. const data = /** @type {string} */(cue.data);
  1136. once = data.includes('ONCE');
  1137. pre = data.includes('PRE');
  1138. post = data.includes('POST');
  1139. }
  1140. let timelineRange = false;
  1141. const timelineOccupies =
  1142. hlsInterstitial.values.find((v) => v.key == 'X-TIMELINE-OCCUPIES');
  1143. if (timelineOccupies) {
  1144. const data = /** @type {string} */(timelineOccupies.data);
  1145. timelineRange = data.includes('RANGE');
  1146. } else if (!resume && this.basePlayer_.isLive()) {
  1147. timelineRange = !pre && !post;
  1148. }
  1149. if (assetUri) {
  1150. const uri = /** @type {string} */(assetUri.data);
  1151. if (!uri) {
  1152. return interstitialsAd;
  1153. }
  1154. interstitialsAd.push({
  1155. id,
  1156. groupId: null,
  1157. startTime,
  1158. endTime,
  1159. uri,
  1160. mimeType: null,
  1161. isSkippable,
  1162. skipOffset,
  1163. skipFor,
  1164. canJump,
  1165. resumeOffset,
  1166. playoutLimit,
  1167. once,
  1168. pre,
  1169. post,
  1170. timelineRange,
  1171. loop: false,
  1172. overlay: null,
  1173. });
  1174. } else if (assetList) {
  1175. const uri = /** @type {string} */(assetList.data);
  1176. if (!uri) {
  1177. return interstitialsAd;
  1178. }
  1179. try {
  1180. const NetworkingEngine = shaka.net.NetworkingEngine;
  1181. const context = {
  1182. type: NetworkingEngine.AdvancedRequestType.INTERSTITIAL_ASSET_LIST,
  1183. };
  1184. const responseData = await this.makeAdRequest_(uri, context);
  1185. const data = shaka.util.StringUtils.fromUTF8(responseData);
  1186. const dataAsJson =
  1187. /** @type {!shaka.ads.InterstitialAdManager.AssetsList} */ (
  1188. JSON.parse(data));
  1189. const skipControl = dataAsJson['SKIP-CONTROL'];
  1190. if (skipControl) {
  1191. const enableSkipAfterValue = skipControl['ENABLE-SKIP-AFTER'];
  1192. if ((typeof enableSkipAfterValue) == 'number') {
  1193. skipOffset = parseFloat(enableSkipAfterValue);
  1194. if (isNaN(enableSkipAfterValue)) {
  1195. skipOffset = isSkippable ? 0 : null;
  1196. }
  1197. }
  1198. const enableSkipForValue = skipControl['ENABLE-SKIP-FOR'];
  1199. if ((typeof enableSkipForValue) == 'number') {
  1200. skipFor = parseFloat(enableSkipForValue);
  1201. if (isNaN(enableSkipForValue)) {
  1202. skipFor = null;
  1203. }
  1204. }
  1205. }
  1206. for (let i = 0; i < dataAsJson['ASSETS'].length; i++) {
  1207. const asset = dataAsJson['ASSETS'][i];
  1208. if (asset['URI']) {
  1209. interstitialsAd.push({
  1210. id: id + '_shaka_asset_' + i,
  1211. groupId: id,
  1212. startTime,
  1213. endTime,
  1214. uri: asset['URI'],
  1215. mimeType: null,
  1216. isSkippable,
  1217. skipOffset,
  1218. skipFor,
  1219. canJump,
  1220. resumeOffset,
  1221. playoutLimit,
  1222. once,
  1223. pre,
  1224. post,
  1225. timelineRange,
  1226. loop: false,
  1227. overlay: null,
  1228. });
  1229. }
  1230. }
  1231. } catch (e) {
  1232. // Ignore errors
  1233. }
  1234. }
  1235. return interstitialsAd;
  1236. }
  1237. /**
  1238. * @private
  1239. */
  1240. cuepointsChanged_() {
  1241. /** @type {!Array<!shaka.extern.AdCuePoint>} */
  1242. const cuePoints = [];
  1243. for (const interstitial of this.interstitials_) {
  1244. if (interstitial.overlay) {
  1245. continue;
  1246. }
  1247. /** @type {shaka.extern.AdCuePoint} */
  1248. const shakaCuePoint = {
  1249. start: interstitial.startTime,
  1250. end: null,
  1251. };
  1252. if (interstitial.pre) {
  1253. shakaCuePoint.start = 0;
  1254. shakaCuePoint.end = null;
  1255. } else if (interstitial.post) {
  1256. shakaCuePoint.start = -1;
  1257. shakaCuePoint.end = null;
  1258. } else if (interstitial.timelineRange) {
  1259. shakaCuePoint.end = interstitial.endTime;
  1260. }
  1261. const isValid = !cuePoints.find((c) => {
  1262. return shakaCuePoint.start == c.start && shakaCuePoint.end == c.end;
  1263. });
  1264. if (isValid) {
  1265. cuePoints.push(shakaCuePoint);
  1266. }
  1267. }
  1268. this.onEvent_(new shaka.util.FakeEvent(
  1269. shaka.ads.Utils.CUEPOINTS_CHANGED,
  1270. (new Map()).set('cuepoints', cuePoints)));
  1271. }
  1272. /**
  1273. * @private
  1274. */
  1275. updatePlayerConfig_() {
  1276. goog.asserts.assert(this.player_, 'Must have player');
  1277. goog.asserts.assert(this.basePlayer_, 'Must have base player');
  1278. this.player_.configure(this.basePlayer_.getNonDefaultConfiguration());
  1279. this.player_.configure('ads.disableHLSInterstitial', true);
  1280. this.player_.configure('ads.disableDASHInterstitial', true);
  1281. this.player_.configure('playRangeEnd', Infinity);
  1282. const netEngine = this.player_.getNetworkingEngine();
  1283. goog.asserts.assert(netEngine, 'Need networking engine');
  1284. this.basePlayer_.getNetworkingEngine().copyFiltersInto(netEngine);
  1285. }
  1286. /**
  1287. * @param {string} url
  1288. * @param {shaka.extern.RequestContext=} context
  1289. * @return {!Promise<BufferSource>}
  1290. * @private
  1291. */
  1292. async makeAdRequest_(url, context) {
  1293. const type = shaka.net.NetworkingEngine.RequestType.ADS;
  1294. const request = shaka.net.NetworkingEngine.makeRequest(
  1295. [url],
  1296. shaka.net.NetworkingEngine.defaultRetryParameters());
  1297. const op = this.basePlayer_.getNetworkingEngine()
  1298. .request(type, request, context);
  1299. const response = await op.promise;
  1300. return response.data;
  1301. }
  1302. /**
  1303. * @param {!shaka.extern.AdInterstitial} interstitial
  1304. * @return {boolean}
  1305. * @private
  1306. */
  1307. isPreloadAllowed_(interstitial) {
  1308. const interstitialMimeType = interstitial.mimeType;
  1309. if (!interstitialMimeType) {
  1310. return true;
  1311. }
  1312. return !interstitialMimeType.startsWith('image/') &&
  1313. interstitialMimeType !== 'text/html';
  1314. }
  1315. /**
  1316. * Only for testing
  1317. *
  1318. * @return {!Array<shaka.extern.AdInterstitial>}
  1319. */
  1320. getInterstitials() {
  1321. return Array.from(this.interstitials_);
  1322. }
  1323. };
  1324. /**
  1325. * @typedef {{
  1326. * ASSETS: !Array<shaka.ads.InterstitialAdManager.Asset>,
  1327. * SKIP-CONTROL: ?shaka.ads.InterstitialAdManager.SkipControl
  1328. * }}
  1329. *
  1330. * @property {!Array<shaka.ads.InterstitialAdManager.Asset>} ASSETS
  1331. * @property {shaka.ads.InterstitialAdManager.SkipControl} SKIP-CONTROL
  1332. */
  1333. shaka.ads.InterstitialAdManager.AssetsList;
  1334. /**
  1335. * @typedef {{
  1336. * URI: string
  1337. * }}
  1338. *
  1339. * @property {string} URI
  1340. */
  1341. shaka.ads.InterstitialAdManager.Asset;
  1342. /**
  1343. * @typedef {{
  1344. * ENABLE-SKIP-AFTER: number,
  1345. * ENABLE-SKIP-FOR: number
  1346. * }}
  1347. *
  1348. * @property {number} ENABLE-SKIP-AFTER
  1349. * @property {number} ENABLE-SKIP-FOR
  1350. */
  1351. shaka.ads.InterstitialAdManager.SkipControl;