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.device.DeviceFactory');
  13. goog.require('shaka.device.IDevice');
  14. goog.require('shaka.log');
  15. goog.require('shaka.media.PreloadManager');
  16. goog.require('shaka.net.NetworkingEngine');
  17. goog.require('shaka.net.NetworkingUtils');
  18. goog.require('shaka.util.Dom');
  19. goog.require('shaka.util.Error');
  20. goog.require('shaka.util.EventManager');
  21. goog.require('shaka.util.FakeEvent');
  22. goog.require('shaka.util.IReleasable');
  23. goog.require('shaka.util.PublicPromise');
  24. goog.require('shaka.util.StringUtils');
  25. goog.require('shaka.util.Timer');
  26. goog.require('shaka.util.TXml');
  27. /**
  28. * A class responsible for Interstitial ad interactions.
  29. *
  30. * @implements {shaka.util.IReleasable}
  31. */
  32. shaka.ads.InterstitialAdManager = class {
  33. /**
  34. * @param {HTMLElement} adContainer
  35. * @param {shaka.Player} basePlayer
  36. * @param {HTMLMediaElement} baseVideo
  37. * @param {function(!shaka.util.FakeEvent)} onEvent
  38. */
  39. constructor(adContainer, basePlayer, baseVideo, onEvent) {
  40. /** @private {?shaka.extern.AdsConfiguration} */
  41. this.config_ = null;
  42. /** @private {HTMLElement} */
  43. this.adContainer_ = adContainer;
  44. /** @private {shaka.Player} */
  45. this.basePlayer_ = basePlayer;
  46. /** @private {HTMLMediaElement} */
  47. this.baseVideo_ = baseVideo;
  48. /** @private {?HTMLMediaElement} */
  49. this.adVideo_ = null;
  50. /** @private {boolean} */
  51. this.usingBaseVideo_ = true;
  52. /** @private {HTMLMediaElement} */
  53. this.video_ = this.baseVideo_;
  54. /** @private {function(!shaka.util.FakeEvent)} */
  55. this.onEvent_ = onEvent;
  56. /** @private {!Set<string>} */
  57. this.interstitialIds_ = new Set();
  58. /** @private {!Set<shaka.extern.AdInterstitial>} */
  59. this.interstitials_ = new Set();
  60. /**
  61. * @private {!Map<shaka.extern.AdInterstitial,
  62. * Promise<?shaka.media.PreloadManager>>}
  63. */
  64. this.preloadManagerInterstitials_ = new Map();
  65. /**
  66. * @private {!Map<shaka.extern.AdInterstitial, !Array<!HTMLLinkElement>>}
  67. */
  68. this.preloadOnDomElements_ = new Map();
  69. /** @private {shaka.Player} */
  70. this.player_ = new shaka.Player();
  71. this.updatePlayerConfig_();
  72. /** @private {shaka.util.EventManager} */
  73. this.eventManager_ = new shaka.util.EventManager();
  74. /** @private {shaka.util.EventManager} */
  75. this.adEventManager_ = new shaka.util.EventManager();
  76. /** @private {boolean} */
  77. this.playingAd_ = false;
  78. /** @private {?number} */
  79. this.lastTime_ = null;
  80. /** @private {?shaka.extern.AdInterstitial} */
  81. this.lastPlayedAd_ = null;
  82. /** @private {?shaka.util.Timer} */
  83. this.playoutLimitTimer_ = null;
  84. /** @private {?function()} */
  85. this.lastOnSkip_ = null;
  86. /** @private {boolean} */
  87. this.usingListeners_ = false;
  88. /** @private {number} */
  89. this.videoCallbackId_ = -1;
  90. // Note: checkForInterstitials_ and onTimeUpdate_ are defined here because
  91. // we use it on listener callback, and for unlisten is necessary use the
  92. // same callback.
  93. /** @private {function()} */
  94. this.checkForInterstitials_ = () => {
  95. if (this.playingAd_ || !this.lastTime_ ||
  96. this.basePlayer_.isRemotePlayback()) {
  97. return;
  98. }
  99. this.lastTime_ = this.baseVideo_.currentTime;
  100. const currentInterstitial = this.getCurrentInterstitial_();
  101. if (currentInterstitial) {
  102. this.setupAd_(currentInterstitial, /* sequenceLength= */ 1,
  103. /* adPosition= */ 1, /* initialTime= */ Date.now());
  104. }
  105. };
  106. /** @private {function()} */
  107. this.onTimeUpdate_ = () => {
  108. if (this.playingAd_ || this.lastTime_ ||
  109. this.basePlayer_.isRemotePlayback()) {
  110. return;
  111. }
  112. this.lastTime_ = this.baseVideo_.currentTime;
  113. const currentInterstitial = this.getCurrentInterstitial_(
  114. /* needPreRoll= */ true);
  115. if (currentInterstitial) {
  116. this.setupAd_(currentInterstitial, /* sequenceLength= */ 1,
  117. /* adPosition= */ 1, /* initialTime= */ Date.now());
  118. }
  119. };
  120. /** @private {function()} */
  121. this.onSeeked_ = () => {
  122. if (this.playingAd_ || !this.lastTime_ ||
  123. this.basePlayer_.isRemotePlayback()) {
  124. return;
  125. }
  126. const currentTime = this.baseVideo_.currentTime;
  127. // Remove last played ad when the new time is before the ad time.
  128. if (this.lastPlayedAd_ &&
  129. !this.lastPlayedAd_.pre && !this.lastPlayedAd_.post &&
  130. currentTime < (this.lastPlayedAd_.endTime ||
  131. this.lastPlayedAd_.startTime)) {
  132. this.lastPlayedAd_ = null;
  133. }
  134. };
  135. /** @private {shaka.util.Timer} */
  136. this.timeUpdateTimer_ = new shaka.util.Timer(this.checkForInterstitials_);
  137. /** @private {shaka.util.Timer} */
  138. this.pollTimer_ = new shaka.util.Timer(async () => {
  139. if (this.interstitials_.size && this.lastTime_ != null) {
  140. const currentLoadMode = this.basePlayer_.getLoadMode();
  141. if (currentLoadMode == shaka.Player.LoadMode.DESTROYED ||
  142. currentLoadMode == shaka.Player.LoadMode.NOT_LOADED) {
  143. return;
  144. }
  145. let cuepointsChanged = false;
  146. const interstitials = Array.from(this.interstitials_);
  147. const seekRange = this.basePlayer_.seekRange();
  148. for (const interstitial of interstitials) {
  149. if (interstitial == this.lastPlayedAd_) {
  150. continue;
  151. }
  152. const comparisonTime = interstitial.endTime || interstitial.startTime;
  153. if ((seekRange.start - comparisonTime) >= 1) {
  154. if (this.preloadManagerInterstitials_.has(interstitial)) {
  155. const preloadManager =
  156. // eslint-disable-next-line no-await-in-loop
  157. await this.preloadManagerInterstitials_.get(interstitial);
  158. if (preloadManager) {
  159. preloadManager.destroy();
  160. }
  161. this.preloadManagerInterstitials_.delete(interstitial);
  162. }
  163. this.removePreloadOnDomElements_(interstitial);
  164. const interstitialId = JSON.stringify(interstitial);
  165. if (this.interstitialIds_.has(interstitialId)) {
  166. this.interstitialIds_.delete(interstitialId);
  167. }
  168. this.interstitials_.delete(interstitial);
  169. this.removeEventListeners_();
  170. if (!interstitial.overlay) {
  171. cuepointsChanged = true;
  172. }
  173. } else {
  174. const difference = interstitial.startTime - this.lastTime_;
  175. if (difference > 0 && difference <= 10) {
  176. if (!this.preloadManagerInterstitials_.has(interstitial) &&
  177. this.isPreloadAllowed_(interstitial)) {
  178. this.preloadManagerInterstitials_.set(
  179. interstitial, this.player_.preload(
  180. interstitial.uri,
  181. /* startTime= */ null,
  182. interstitial.mimeType || undefined));
  183. }
  184. this.checkPreloadOnDomElements_(interstitial);
  185. }
  186. }
  187. }
  188. if (cuepointsChanged) {
  189. this.cuepointsChanged_();
  190. }
  191. }
  192. });
  193. this.configure(this.basePlayer_.getConfiguration().ads);
  194. }
  195. /**
  196. * Called by the AdManager to provide an updated configuration any time it
  197. * changes.
  198. *
  199. * @param {shaka.extern.AdsConfiguration} config
  200. */
  201. configure(config) {
  202. this.config_ = config;
  203. this.determineIfUsingBaseVideo_();
  204. }
  205. /**
  206. * @private
  207. */
  208. addEventListeners_() {
  209. if (this.usingListeners_ || !this.interstitials_.size) {
  210. return;
  211. }
  212. this.eventManager_.listen(
  213. this.baseVideo_, 'playing', this.onTimeUpdate_);
  214. this.eventManager_.listen(
  215. this.baseVideo_, 'timeupdate', this.onTimeUpdate_);
  216. this.eventManager_.listen(
  217. this.baseVideo_, 'seeked', this.onSeeked_);
  218. this.eventManager_.listen(
  219. this.baseVideo_, 'ended', this.checkForInterstitials_);
  220. if ('requestVideoFrameCallback' in this.baseVideo_ && !this.isSmartTV_()) {
  221. const baseVideo = /** @type {!HTMLVideoElement} */ (this.baseVideo_);
  222. const videoFrameCallback = (now, metadata) => {
  223. if (this.videoCallbackId_ == -1) {
  224. return;
  225. }
  226. this.checkForInterstitials_();
  227. // It is necessary to check this again because this callback can be
  228. // executed in another thread by the browser and we have to be sure
  229. // again here that we have not cancelled it in the middle of an
  230. // execution.
  231. if (this.videoCallbackId_ == -1) {
  232. return;
  233. }
  234. this.videoCallbackId_ =
  235. baseVideo.requestVideoFrameCallback(videoFrameCallback);
  236. };
  237. this.videoCallbackId_ =
  238. baseVideo.requestVideoFrameCallback(videoFrameCallback);
  239. } else {
  240. this.timeUpdateTimer_.tickEvery(/* seconds= */ 0.025);
  241. }
  242. if (this.pollTimer_) {
  243. this.pollTimer_.tickEvery(/* seconds= */ 1); ;
  244. }
  245. this.usingListeners_ = true;
  246. }
  247. /**
  248. * @private
  249. */
  250. removeEventListeners_() {
  251. if (!this.usingListeners_ || this.interstitials_.size) {
  252. return;
  253. }
  254. this.eventManager_.unlisten(
  255. this.baseVideo_, 'playing', this.onTimeUpdate_);
  256. this.eventManager_.unlisten(
  257. this.baseVideo_, 'timeupdate', this.onTimeUpdate_);
  258. this.eventManager_.unlisten(
  259. this.baseVideo_, 'seeked', this.onSeeked_);
  260. this.eventManager_.unlisten(
  261. this.baseVideo_, 'ended', this.checkForInterstitials_);
  262. if (this.videoCallbackId_ != -1) {
  263. const baseVideo = /** @type {!HTMLVideoElement} */ (this.baseVideo_);
  264. baseVideo.cancelVideoFrameCallback(this.videoCallbackId_);
  265. this.videoCallbackId_ = -1;
  266. }
  267. if (this.timeUpdateTimer_) {
  268. this.timeUpdateTimer_.stop();
  269. }
  270. if (this.pollTimer_) {
  271. this.pollTimer_.stop();
  272. }
  273. this.usingListeners_ = false;
  274. }
  275. /**
  276. * @private
  277. */
  278. determineIfUsingBaseVideo_() {
  279. if (!this.adContainer_ || !this.config_ || this.playingAd_) {
  280. this.usingBaseVideo_ = true;
  281. return;
  282. }
  283. let supportsMultipleMediaElements =
  284. this.config_.supportsMultipleMediaElements;
  285. const video = /** @type {HTMLVideoElement} */(this.baseVideo_);
  286. if (video.webkitSupportsFullscreen && video.webkitDisplayingFullscreen) {
  287. supportsMultipleMediaElements = false;
  288. }
  289. if (this.usingBaseVideo_ != supportsMultipleMediaElements) {
  290. return;
  291. }
  292. this.usingBaseVideo_ = !supportsMultipleMediaElements;
  293. if (this.usingBaseVideo_) {
  294. this.video_ = this.baseVideo_;
  295. if (this.adVideo_) {
  296. if (this.adVideo_.parentElement) {
  297. this.adContainer_.removeChild(this.adVideo_);
  298. }
  299. this.adVideo_ = null;
  300. }
  301. } else {
  302. if (!this.adVideo_) {
  303. this.adVideo_ = this.createMediaElement_();
  304. }
  305. this.video_ = this.adVideo_;
  306. }
  307. }
  308. /**
  309. * Resets the Interstitial manager and removes any continuous polling.
  310. */
  311. stop() {
  312. if (this.adEventManager_) {
  313. this.adEventManager_.removeAll();
  314. }
  315. this.interstitialIds_.clear();
  316. this.interstitials_.clear();
  317. this.player_.destroyAllPreloads();
  318. if (this.preloadManagerInterstitials_.size) {
  319. const values = Array.from(this.preloadManagerInterstitials_.values());
  320. for (const value of values) {
  321. if (value) {
  322. value.then((preloadManager) => {
  323. if (preloadManager) {
  324. preloadManager.destroy();
  325. }
  326. });
  327. }
  328. };
  329. }
  330. this.preloadManagerInterstitials_.clear();
  331. if (this.preloadOnDomElements_.size) {
  332. const interstitials = Array.from(this.preloadOnDomElements_.keys());
  333. for (const interstitial of interstitials) {
  334. this.removePreloadOnDomElements_(interstitial);
  335. }
  336. }
  337. this.preloadOnDomElements_.clear();
  338. this.player_.detach();
  339. this.playingAd_ = false;
  340. this.lastTime_ = null;
  341. this.lastPlayedAd_ = null;
  342. this.usingBaseVideo_ = true;
  343. this.video_ = this.baseVideo_;
  344. this.adVideo_ = null;
  345. this.removeBaseStyles_();
  346. this.removeEventListeners_();
  347. if (this.adContainer_) {
  348. shaka.util.Dom.removeAllChildren(this.adContainer_);
  349. }
  350. if (this.playoutLimitTimer_) {
  351. this.playoutLimitTimer_.stop();
  352. this.playoutLimitTimer_ = null;
  353. }
  354. }
  355. /** @override */
  356. release() {
  357. this.stop();
  358. if (this.eventManager_) {
  359. this.eventManager_.release();
  360. }
  361. if (this.adEventManager_) {
  362. this.adEventManager_.release();
  363. }
  364. if (this.timeUpdateTimer_) {
  365. this.timeUpdateTimer_.stop();
  366. this.timeUpdateTimer_ = null;
  367. }
  368. if (this.pollTimer_) {
  369. this.pollTimer_.stop();
  370. this.pollTimer_ = null;
  371. }
  372. this.player_.destroy();
  373. }
  374. /**
  375. * @return {shaka.Player}
  376. */
  377. getPlayer() {
  378. return this.player_;
  379. }
  380. /**
  381. * @param {shaka.extern.HLSInterstitial} hlsInterstitial
  382. */
  383. async addMetadata(hlsInterstitial) {
  384. this.updatePlayerConfig_();
  385. const adInterstitials = await this.getInterstitialsInfo_(hlsInterstitial);
  386. if (adInterstitials.length) {
  387. this.addInterstitials(adInterstitials);
  388. } else {
  389. shaka.log.alwaysWarn('Unsupported HLS interstitial', hlsInterstitial);
  390. }
  391. }
  392. /**
  393. * @param {shaka.extern.TimelineRegionInfo} region
  394. */
  395. addRegion(region) {
  396. const TXml = shaka.util.TXml;
  397. const isReplace =
  398. region.schemeIdUri == 'urn:mpeg:dash:event:alternativeMPD:replace:2025';
  399. const isInsert =
  400. region.schemeIdUri == 'urn:mpeg:dash:event:alternativeMPD:insert:2025';
  401. if (!isReplace && !isInsert) {
  402. shaka.log.warning('Unsupported alternative media presentation', region);
  403. return;
  404. }
  405. const startTime = region.startTime;
  406. let endTime = region.endTime;
  407. let playoutLimit = null;
  408. let resumeOffset = 0;
  409. let interstitialUri;
  410. for (const node of region.eventNode.children) {
  411. if (node.tagName == 'AlternativeMPD') {
  412. const uri = node.attributes['uri'];
  413. if (uri) {
  414. interstitialUri = uri;
  415. break;
  416. }
  417. } else if (node.tagName == 'InsertPresentation' ||
  418. node.tagName == 'ReplacePresentation') {
  419. const url = node.attributes['url'];
  420. if (url) {
  421. interstitialUri = url;
  422. const unscaledMaxDuration =
  423. TXml.parseAttr(node, 'maxDuration', TXml.parseInt);
  424. if (unscaledMaxDuration) {
  425. playoutLimit = unscaledMaxDuration / region.timescale;
  426. }
  427. const unscaledReturnOffset =
  428. TXml.parseAttr(node, 'returnOffset', TXml.parseInt);
  429. if (unscaledReturnOffset) {
  430. resumeOffset = unscaledReturnOffset / region.timescale;
  431. }
  432. if (isReplace && resumeOffset) {
  433. endTime = startTime + resumeOffset;
  434. }
  435. break;
  436. }
  437. }
  438. }
  439. if (!interstitialUri) {
  440. shaka.log.warning('Unsupported alternative media presentation', region);
  441. return;
  442. }
  443. /** @type {!shaka.extern.AdInterstitial} */
  444. const interstitial = {
  445. id: region.id,
  446. groupId: null,
  447. startTime,
  448. endTime,
  449. uri: interstitialUri,
  450. mimeType: null,
  451. isSkippable: false,
  452. skipOffset: null,
  453. skipFor: null,
  454. canJump: true,
  455. resumeOffset: isInsert ? resumeOffset : null,
  456. playoutLimit,
  457. once: false,
  458. pre: false,
  459. post: false,
  460. timelineRange: isReplace && !isInsert,
  461. loop: false,
  462. overlay: null,
  463. displayOnBackground: false,
  464. currentVideo: null,
  465. background: null,
  466. };
  467. this.addInterstitials([interstitial]);
  468. }
  469. /**
  470. * @param {shaka.extern.TimelineRegionInfo} region
  471. */
  472. addOverlayRegion(region) {
  473. const TXml = shaka.util.TXml;
  474. goog.asserts.assert(region.eventNode, 'Need a region eventNode');
  475. const overlayEvent = TXml.findChild(region.eventNode, 'OverlayEvent');
  476. const uri = overlayEvent.attributes['uri'];
  477. const mimeType = overlayEvent.attributes['mimeType'];
  478. const loop = overlayEvent.attributes['loop'] == 'true';
  479. const z = TXml.parseAttr(overlayEvent, 'z', TXml.parseInt);
  480. if (!uri || z == 0) {
  481. shaka.log.warning('Unsupported OverlayEvent', region);
  482. return;
  483. }
  484. let background = null;
  485. const backgroundElement = TXml.findChild(overlayEvent, 'Background');
  486. if (backgroundElement) {
  487. const backgroundUri = backgroundElement.attributes['uri'];
  488. if (backgroundUri) {
  489. background = `center / contain no-repeat url('${backgroundUri}')`;
  490. } else {
  491. background = TXml.getContents(backgroundElement);
  492. }
  493. }
  494. const viewport = {
  495. x: 1920,
  496. y: 1080,
  497. };
  498. const viewportElement = TXml.findChild(overlayEvent, 'Viewport');
  499. if (viewportElement) {
  500. const viewportX = TXml.parseAttr(viewportElement, 'x', TXml.parseInt);
  501. if (viewportX == null) {
  502. shaka.log.warning('Unsupported OverlayEvent', region);
  503. return;
  504. }
  505. const viewportY = TXml.parseAttr(viewportElement, 'y', TXml.parseInt);
  506. if (viewportY == null) {
  507. shaka.log.warning('Unsupported OverlayEvent', region);
  508. return;
  509. }
  510. viewport.x = viewportX;
  511. viewport.y = viewportY;
  512. }
  513. /** @type {!shaka.extern.AdPositionInfo} */
  514. const overlay = {
  515. viewport: {
  516. x: viewport.x,
  517. y: viewport.y,
  518. },
  519. topLeft: {
  520. x: 0,
  521. y: 0,
  522. },
  523. size: {
  524. x: viewport.x,
  525. y: viewport.y,
  526. },
  527. };
  528. const overlayElement = TXml.findChild(overlayEvent, 'Overlay');
  529. if (viewportElement && overlayElement) {
  530. const topLeft = TXml.findChild(overlayElement, 'TopLeft');
  531. const size = TXml.findChild(overlayElement, 'Size');
  532. if (topLeft && size) {
  533. const topLeftX = TXml.parseAttr(topLeft, 'x', TXml.parseInt);
  534. if (topLeftX == null) {
  535. shaka.log.warning('Unsupported OverlayEvent', region);
  536. return;
  537. }
  538. const topLeftY = TXml.parseAttr(topLeft, 'y', TXml.parseInt);
  539. if (topLeftY == null) {
  540. shaka.log.warning('Unsupported OverlayEvent', region);
  541. return;
  542. }
  543. const sizeX = TXml.parseAttr(size, 'x', TXml.parseInt);
  544. if (sizeX == null) {
  545. shaka.log.warning('Unsupported OverlayEvent', region);
  546. return;
  547. }
  548. const sizeY = TXml.parseAttr(size, 'y', TXml.parseInt);
  549. if (sizeY == null) {
  550. shaka.log.warning('Unsupported OverlayEvent', region);
  551. return;
  552. }
  553. overlay.topLeft.x = topLeftX;
  554. overlay.topLeft.y = topLeftY;
  555. overlay.size.x = sizeX;
  556. overlay.size.y = sizeY;
  557. }
  558. }
  559. let currentVideo = null;
  560. const squeezeElement = TXml.findChild(overlayEvent, 'Squeeze');
  561. if (viewportElement && squeezeElement) {
  562. const topLeft = TXml.findChild(squeezeElement, 'TopLeft');
  563. const size = TXml.findChild(squeezeElement, 'Size');
  564. if (topLeft && size) {
  565. const topLeftX = TXml.parseAttr(topLeft, 'x', TXml.parseInt);
  566. if (topLeftX == null) {
  567. shaka.log.warning('Unsupported OverlayEvent', region);
  568. return;
  569. }
  570. const topLeftY = TXml.parseAttr(topLeft, 'y', TXml.parseInt);
  571. if (topLeftY == null) {
  572. shaka.log.warning('Unsupported OverlayEvent', region);
  573. return;
  574. }
  575. const sizeX = TXml.parseAttr(size, 'x', TXml.parseInt);
  576. if (sizeX == null) {
  577. shaka.log.warning('Unsupported OverlayEvent', region);
  578. return;
  579. }
  580. const sizeY = TXml.parseAttr(size, 'y', TXml.parseInt);
  581. if (sizeY == null) {
  582. shaka.log.warning('Unsupported OverlayEvent', region);
  583. return;
  584. }
  585. currentVideo = {
  586. viewport: {
  587. x: viewport.x,
  588. y: viewport.y,
  589. },
  590. topLeft: {
  591. x: topLeftX,
  592. y: topLeftY,
  593. },
  594. size: {
  595. x: sizeX,
  596. y: sizeY,
  597. },
  598. };
  599. }
  600. }
  601. /** @type {!shaka.extern.AdInterstitial} */
  602. const interstitial = {
  603. id: region.id,
  604. groupId: null,
  605. startTime: region.startTime,
  606. endTime: region.endTime,
  607. uri,
  608. mimeType,
  609. isSkippable: false,
  610. skipOffset: null,
  611. skipFor: null,
  612. canJump: true,
  613. resumeOffset: null,
  614. playoutLimit: null,
  615. once: false,
  616. pre: false,
  617. post: false,
  618. timelineRange: true,
  619. loop,
  620. overlay,
  621. displayOnBackground: z == -1,
  622. currentVideo,
  623. background,
  624. };
  625. this.addInterstitials([interstitial]);
  626. }
  627. /**
  628. * @param {string} url
  629. * @return {!Promise}
  630. */
  631. async addAdUrlInterstitial(url) {
  632. const NetworkingEngine = shaka.net.NetworkingEngine;
  633. const context = {
  634. type: NetworkingEngine.AdvancedRequestType.INTERSTITIAL_AD_URL,
  635. };
  636. const responseData = await this.makeAdRequest_(url, context);
  637. const data = shaka.util.TXml.parseXml(responseData, 'VAST,vmap:VMAP');
  638. if (!data) {
  639. throw new shaka.util.Error(
  640. shaka.util.Error.Severity.CRITICAL,
  641. shaka.util.Error.Category.ADS,
  642. shaka.util.Error.Code.VAST_INVALID_XML);
  643. }
  644. let interstitials = [];
  645. if (data.tagName == 'VAST') {
  646. interstitials = shaka.ads.Utils.parseVastToInterstitials(
  647. data, this.lastTime_);
  648. } else if (data.tagName == 'vmap:VMAP') {
  649. for (const ad of shaka.ads.Utils.parseVMAP(data)) {
  650. // eslint-disable-next-line no-await-in-loop
  651. const vastResponseData = await this.makeAdRequest_(ad.uri, context);
  652. const vast = shaka.util.TXml.parseXml(vastResponseData, 'VAST');
  653. if (!vast) {
  654. throw new shaka.util.Error(
  655. shaka.util.Error.Severity.CRITICAL,
  656. shaka.util.Error.Category.ADS,
  657. shaka.util.Error.Code.VAST_INVALID_XML);
  658. }
  659. interstitials.push(...shaka.ads.Utils.parseVastToInterstitials(
  660. vast, ad.time));
  661. }
  662. }
  663. this.addInterstitials(interstitials);
  664. }
  665. /**
  666. * @param {!Array<shaka.extern.AdInterstitial>} interstitials
  667. */
  668. async addInterstitials(interstitials) {
  669. let cuepointsChanged = false;
  670. for (const interstitial of interstitials) {
  671. if (!interstitial.uri) {
  672. shaka.log.alwaysWarn('Missing URL in interstitial', interstitial);
  673. continue;
  674. }
  675. if (!interstitial.mimeType) {
  676. try {
  677. const netEngine = this.player_.getNetworkingEngine();
  678. goog.asserts.assert(netEngine, 'Need networking engine');
  679. // eslint-disable-next-line no-await-in-loop
  680. interstitial.mimeType = await shaka.net.NetworkingUtils.getMimeType(
  681. interstitial.uri, netEngine,
  682. this.basePlayer_.getConfiguration().streaming.retryParameters);
  683. } catch (error) {}
  684. }
  685. const interstitialId = interstitial.id || JSON.stringify(interstitial);
  686. if (this.interstitialIds_.has(interstitialId)) {
  687. continue;
  688. }
  689. if (interstitial.loop && !interstitial.overlay) {
  690. shaka.log.alwaysWarn('Loop is only supported in overlay interstitials',
  691. interstitial);
  692. }
  693. if (!interstitial.overlay) {
  694. cuepointsChanged = true;
  695. }
  696. this.interstitialIds_.add(interstitialId);
  697. this.interstitials_.add(interstitial);
  698. let shouldPreload = false;
  699. if (interstitial.pre && this.lastTime_ == null) {
  700. shouldPreload = true;
  701. } else if (interstitial.startTime == 0 && !interstitial.canJump) {
  702. shouldPreload = true;
  703. } else if (this.lastTime_ != null) {
  704. const difference = interstitial.startTime - this.lastTime_;
  705. if (difference > 0 && difference <= 10) {
  706. shouldPreload = true;
  707. }
  708. }
  709. if (shouldPreload) {
  710. if (!this.preloadManagerInterstitials_.has(interstitial) &&
  711. this.isPreloadAllowed_(interstitial)) {
  712. this.preloadManagerInterstitials_.set(
  713. interstitial, this.player_.preload(
  714. interstitial.uri,
  715. /* startTime= */ null,
  716. interstitial.mimeType || undefined));
  717. }
  718. this.checkPreloadOnDomElements_(interstitial);
  719. }
  720. }
  721. if (cuepointsChanged) {
  722. this.cuepointsChanged_();
  723. }
  724. this.addEventListeners_();
  725. }
  726. /**
  727. * @return {!HTMLMediaElement}
  728. * @private
  729. */
  730. createMediaElement_() {
  731. const video = /** @type {!HTMLMediaElement} */(
  732. document.createElement(this.baseVideo_.tagName));
  733. video.autoplay = true;
  734. video.style.position = 'absolute';
  735. video.style.top = '0';
  736. video.style.left = '0';
  737. video.style.width = '100%';
  738. video.style.height = '100%';
  739. video.style.display = 'none';
  740. video.setAttribute('playsinline', '');
  741. return video;
  742. }
  743. /**
  744. * @param {boolean=} needPreRoll
  745. * @param {?number=} numberToSkip
  746. * @return {?shaka.extern.AdInterstitial}
  747. * @private
  748. */
  749. getCurrentInterstitial_(needPreRoll = false, numberToSkip = null) {
  750. let skipped = 0;
  751. let currentInterstitial = null;
  752. if (this.interstitials_.size && this.lastTime_ != null) {
  753. const isEnded = this.baseVideo_.ended;
  754. const interstitials = Array.from(this.interstitials_).sort((a, b) => {
  755. return b.startTime - a.startTime;
  756. });
  757. const roundDecimals = (number) => {
  758. return Math.round(number * 1000) / 1000;
  759. };
  760. let interstitialsToCheck = interstitials;
  761. if (needPreRoll) {
  762. interstitialsToCheck = interstitials.filter((i) => i.pre);
  763. } else if (isEnded) {
  764. interstitialsToCheck = interstitials.filter((i) => i.post);
  765. } else {
  766. interstitialsToCheck = interstitials.filter((i) => !i.pre && !i.post);
  767. }
  768. for (const interstitial of interstitialsToCheck) {
  769. let isValid = false;
  770. if (needPreRoll) {
  771. isValid = interstitial.pre;
  772. } else if (isEnded) {
  773. isValid = interstitial.post;
  774. } else if (!interstitial.pre && !interstitial.post) {
  775. const difference =
  776. this.lastTime_ - roundDecimals(interstitial.startTime);
  777. let maxDifference = 1;
  778. if (this.config_.allowStartInMiddleOfInterstitial &&
  779. interstitial.endTime && interstitial.endTime != Infinity) {
  780. maxDifference = interstitial.endTime - interstitial.startTime;
  781. }
  782. if (difference > 0 &&
  783. (difference <= maxDifference || !interstitial.canJump)) {
  784. if (numberToSkip == null && this.lastPlayedAd_ &&
  785. !this.lastPlayedAd_.pre && !this.lastPlayedAd_.post &&
  786. this.lastPlayedAd_.startTime >= interstitial.startTime) {
  787. isValid = false;
  788. } else {
  789. isValid = true;
  790. }
  791. }
  792. }
  793. if (isValid && (!this.lastPlayedAd_ ||
  794. interstitial.startTime >= this.lastPlayedAd_.startTime)) {
  795. if (skipped == (numberToSkip || 0)) {
  796. currentInterstitial = interstitial;
  797. } else if (currentInterstitial && !interstitial.canJump) {
  798. const currentStartTime =
  799. roundDecimals(currentInterstitial.startTime);
  800. const newStartTime =
  801. roundDecimals(interstitial.startTime);
  802. if (newStartTime - currentStartTime > 0.001) {
  803. currentInterstitial = interstitial;
  804. skipped = 0;
  805. }
  806. }
  807. skipped++;
  808. }
  809. }
  810. }
  811. return currentInterstitial;
  812. }
  813. /**
  814. * @param {shaka.extern.AdInterstitial} interstitial
  815. * @param {number} sequenceLength
  816. * @param {number} adPosition
  817. * @param {number} initialTime the clock time the ad started at
  818. * @param {number=} oncePlayed
  819. * @private
  820. */
  821. setupAd_(interstitial, sequenceLength, adPosition, initialTime,
  822. oncePlayed = 0) {
  823. shaka.log.info('Starting interstitial',
  824. interstitial.startTime, 'at', this.lastTime_);
  825. this.lastPlayedAd_ = interstitial;
  826. this.determineIfUsingBaseVideo_();
  827. goog.asserts.assert(this.video_, 'Must have video');
  828. if (!this.usingBaseVideo_ && this.adContainer_ &&
  829. !this.video_.parentElement) {
  830. this.adContainer_.appendChild(this.video_);
  831. }
  832. if (adPosition == 1 && sequenceLength == 1) {
  833. sequenceLength = Array.from(this.interstitials_).filter((i) => {
  834. if (interstitial.pre) {
  835. return i.pre == interstitial.pre;
  836. } else if (interstitial.post) {
  837. return i.post == interstitial.post;
  838. }
  839. return Math.abs(i.startTime - interstitial.startTime) < 0.001;
  840. }).length;
  841. }
  842. if (interstitial.once) {
  843. oncePlayed++;
  844. this.interstitials_.delete(interstitial);
  845. this.removeEventListeners_();
  846. if (!interstitial.overlay) {
  847. this.cuepointsChanged_();
  848. }
  849. }
  850. if (interstitial.mimeType) {
  851. if (interstitial.mimeType.startsWith('image/') ||
  852. interstitial.mimeType === 'text/html') {
  853. if (!interstitial.overlay) {
  854. shaka.log.alwaysWarn('Unsupported interstitial', interstitial);
  855. return;
  856. }
  857. this.setupStaticAd_(interstitial, sequenceLength, adPosition,
  858. oncePlayed);
  859. return;
  860. }
  861. }
  862. if (this.usingBaseVideo_ && interstitial.overlay) {
  863. shaka.log.alwaysWarn('Unsupported interstitial', interstitial);
  864. return;
  865. }
  866. this.setupVideoAd_(interstitial, sequenceLength, adPosition, initialTime,
  867. oncePlayed);
  868. }
  869. /**
  870. * @param {shaka.extern.AdInterstitial} interstitial
  871. * @param {number} sequenceLength
  872. * @param {number} adPosition
  873. * @param {number} oncePlayed
  874. * @private
  875. */
  876. setupStaticAd_(interstitial, sequenceLength, adPosition, oncePlayed) {
  877. this.playingAd_ = true;
  878. const overlay = interstitial.overlay;
  879. goog.asserts.assert(overlay, 'Must have overlay');
  880. const tagName = interstitial.mimeType == 'text/html' ? 'iframe' : 'img';
  881. const htmlElement = /** @type {!(HTMLImageElement|HTMLIFrameElement)} */ (
  882. document.createElement(tagName));
  883. htmlElement.style.objectFit = 'contain';
  884. htmlElement.style.position = 'absolute';
  885. htmlElement.style.border = 'none';
  886. this.setBaseStyles_(interstitial);
  887. const basicTask = () => {
  888. if (this.playoutLimitTimer_) {
  889. this.playoutLimitTimer_.stop();
  890. this.playoutLimitTimer_ = null;
  891. }
  892. this.adContainer_.removeChild(htmlElement);
  893. this.removeBaseStyles_(interstitial);
  894. this.onEvent_(
  895. new shaka.util.FakeEvent(shaka.ads.Utils.AD_STOPPED));
  896. this.adEventManager_.removeAll();
  897. const nextCurrentInterstitial = this.getCurrentInterstitial_(
  898. interstitial.pre, adPosition - oncePlayed);
  899. if (nextCurrentInterstitial) {
  900. this.setupAd_(nextCurrentInterstitial, sequenceLength,
  901. ++adPosition, /* initialTime= */ Date.now(), oncePlayed);
  902. } else {
  903. this.playingAd_ = false;
  904. }
  905. };
  906. const ad = new shaka.ads.InterstitialStaticAd(
  907. interstitial, sequenceLength, adPosition);
  908. this.onEvent_(
  909. new shaka.util.FakeEvent(shaka.ads.Utils.AD_STARTED,
  910. (new Map()).set('ad', ad)));
  911. if (tagName == 'iframe') {
  912. htmlElement.src = interstitial.uri;
  913. } else {
  914. htmlElement.src = interstitial.uri;
  915. htmlElement.onerror = (e) => {
  916. this.onEvent_(new shaka.util.FakeEvent(shaka.ads.Utils.AD_ERROR,
  917. (new Map()).set('originalEvent', e)));
  918. basicTask();
  919. };
  920. }
  921. const viewport = overlay.viewport;
  922. const topLeft = overlay.topLeft;
  923. const size = overlay.size;
  924. // Special case for VAST non-linear ads
  925. if (viewport.x == 0 && viewport.y == 0) {
  926. htmlElement.width = interstitial.overlay.size.x;
  927. htmlElement.height = interstitial.overlay.size.y;
  928. htmlElement.style.bottom = '10%';
  929. htmlElement.style.left = '0';
  930. htmlElement.style.right = '0';
  931. htmlElement.style.width = '100%';
  932. if (!interstitial.overlay.size.y && tagName == 'iframe') {
  933. htmlElement.style.height = 'auto';
  934. }
  935. } else {
  936. htmlElement.style.height = (size.y / viewport.y * 100) + '%';
  937. htmlElement.style.left = (topLeft.x / viewport.x * 100) + '%';
  938. htmlElement.style.top = (topLeft.y / viewport.y * 100) + '%';
  939. htmlElement.style.width = (size.x / viewport.x * 100) + '%';
  940. }
  941. this.adContainer_.appendChild(htmlElement);
  942. const startTime = Date.now();
  943. if (this.playoutLimitTimer_) {
  944. this.playoutLimitTimer_.stop();
  945. }
  946. this.playoutLimitTimer_ = new shaka.util.Timer(() => {
  947. if (interstitial.playoutLimit &&
  948. (Date.now() - startTime) / 1000 > interstitial.playoutLimit) {
  949. this.onEvent_(
  950. new shaka.util.FakeEvent(shaka.ads.Utils.AD_COMPLETE));
  951. basicTask();
  952. } else if (interstitial.endTime &&
  953. this.baseVideo_.currentTime > interstitial.endTime) {
  954. this.onEvent_(
  955. new shaka.util.FakeEvent(shaka.ads.Utils.AD_COMPLETE));
  956. basicTask();
  957. } else if (this.baseVideo_.currentTime < interstitial.startTime) {
  958. this.onEvent_(
  959. new shaka.util.FakeEvent(shaka.ads.Utils.AD_SKIPPED));
  960. basicTask();
  961. }
  962. });
  963. if (interstitial.playoutLimit && !interstitial.endTime) {
  964. this.playoutLimitTimer_.tickAfter(interstitial.playoutLimit);
  965. } else if (interstitial.endTime) {
  966. this.playoutLimitTimer_.tickEvery(/* seconds= */ 0.025);
  967. }
  968. this.adEventManager_.listen(this.baseVideo_, 'seeked', () => {
  969. const currentTime = this.baseVideo_.currentTime;
  970. if (currentTime < interstitial.startTime ||
  971. (interstitial.endTime && currentTime > interstitial.endTime)) {
  972. if (this.playoutLimitTimer_) {
  973. this.playoutLimitTimer_.stop();
  974. }
  975. this.onEvent_(
  976. new shaka.util.FakeEvent(shaka.ads.Utils.AD_SKIPPED));
  977. basicTask();
  978. }
  979. });
  980. }
  981. /**
  982. * @param {shaka.extern.AdInterstitial} interstitial
  983. * @param {number} sequenceLength
  984. * @param {number} adPosition
  985. * @param {number} initialTime the clock time the ad started at
  986. * @param {number} oncePlayed
  987. * @private
  988. */
  989. async setupVideoAd_(interstitial, sequenceLength, adPosition, initialTime,
  990. oncePlayed) {
  991. goog.asserts.assert(this.video_, 'Must have video');
  992. const startTime = Date.now();
  993. this.playingAd_ = true;
  994. if (this.usingBaseVideo_ && adPosition == 1) {
  995. this.onEvent_(new shaka.util.FakeEvent(
  996. shaka.ads.Utils.AD_CONTENT_PAUSE_REQUESTED,
  997. (new Map()).set('saveLivePosition', true)));
  998. const detachBasePlayerPromise = new shaka.util.PublicPromise();
  999. const checkState = async (e) => {
  1000. if (e['state'] == 'detach') {
  1001. if (this.isSmartTV_()) {
  1002. await new Promise(
  1003. (resolve) => new shaka.util.Timer(resolve).tickAfter(0.1));
  1004. }
  1005. detachBasePlayerPromise.resolve();
  1006. this.adEventManager_.unlisten(
  1007. this.basePlayer_, 'onstatechange', checkState);
  1008. }
  1009. };
  1010. this.adEventManager_.listen(
  1011. this.basePlayer_, 'onstatechange', checkState);
  1012. await detachBasePlayerPromise;
  1013. }
  1014. this.setBaseStyles_(interstitial);
  1015. if (!this.usingBaseVideo_) {
  1016. this.video_.style.display = '';
  1017. if (interstitial.overlay) {
  1018. this.video_.loop = interstitial.loop;
  1019. const viewport = interstitial.overlay.viewport;
  1020. const topLeft = interstitial.overlay.topLeft;
  1021. const size = interstitial.overlay.size;
  1022. this.video_.style.height = (size.y / viewport.y * 100) + '%';
  1023. this.video_.style.left = (topLeft.x / viewport.x * 100) + '%';
  1024. this.video_.style.top = (topLeft.y / viewport.y * 100) + '%';
  1025. this.video_.style.width = (size.x / viewport.x * 100) + '%';
  1026. } else {
  1027. this.baseVideo_.pause();
  1028. if (interstitial.resumeOffset != null &&
  1029. interstitial.resumeOffset != 0) {
  1030. this.baseVideo_.currentTime += interstitial.resumeOffset;
  1031. }
  1032. this.video_.loop = false;
  1033. this.video_.style.height = '100%';
  1034. this.video_.style.left = '0';
  1035. this.video_.style.top = '0';
  1036. this.video_.style.width = '100%';
  1037. }
  1038. }
  1039. let unloadingInterstitial = false;
  1040. const updateBaseVideoTime = () => {
  1041. if (!this.usingBaseVideo_ && !interstitial.overlay) {
  1042. if (interstitial.resumeOffset == null) {
  1043. if (interstitial.timelineRange && interstitial.endTime &&
  1044. interstitial.endTime != Infinity) {
  1045. if (this.baseVideo_.currentTime != interstitial.endTime) {
  1046. this.baseVideo_.currentTime = interstitial.endTime;
  1047. }
  1048. } else {
  1049. const now = Date.now();
  1050. this.baseVideo_.currentTime += (now - initialTime) / 1000;
  1051. initialTime = now;
  1052. }
  1053. }
  1054. }
  1055. };
  1056. const basicTask = async (isSkip) => {
  1057. updateBaseVideoTime();
  1058. // Optimization to avoid returning to main content when there is another
  1059. // interstitial below.
  1060. let nextCurrentInterstitial = this.getCurrentInterstitial_(
  1061. interstitial.pre, adPosition - oncePlayed);
  1062. if (isSkip && interstitial.groupId) {
  1063. while (nextCurrentInterstitial &&
  1064. nextCurrentInterstitial.groupId == interstitial.groupId) {
  1065. adPosition++;
  1066. nextCurrentInterstitial = this.getCurrentInterstitial_(
  1067. interstitial.pre, adPosition - oncePlayed);
  1068. }
  1069. }
  1070. if (this.playoutLimitTimer_ && (!interstitial.groupId ||
  1071. (nextCurrentInterstitial &&
  1072. nextCurrentInterstitial.groupId != interstitial.groupId))) {
  1073. this.playoutLimitTimer_.stop();
  1074. this.playoutLimitTimer_ = null;
  1075. }
  1076. this.removeBaseStyles_(interstitial);
  1077. if (!nextCurrentInterstitial || nextCurrentInterstitial.overlay) {
  1078. if (interstitial.post) {
  1079. this.lastTime_ = null;
  1080. this.lastPlayedAd_ = null;
  1081. }
  1082. if (this.usingBaseVideo_) {
  1083. await this.player_.detach();
  1084. } else {
  1085. await this.player_.unload();
  1086. }
  1087. if (this.usingBaseVideo_) {
  1088. let offset = interstitial.resumeOffset;
  1089. if (offset == null) {
  1090. if (interstitial.timelineRange && interstitial.endTime &&
  1091. interstitial.endTime != Infinity) {
  1092. offset = interstitial.endTime - (this.lastTime_ || 0);
  1093. } else {
  1094. offset = (Date.now() - initialTime) / 1000;
  1095. }
  1096. }
  1097. this.onEvent_(new shaka.util.FakeEvent(
  1098. shaka.ads.Utils.AD_CONTENT_RESUME_REQUESTED,
  1099. (new Map()).set('offset', offset)));
  1100. }
  1101. this.onEvent_(
  1102. new shaka.util.FakeEvent(shaka.ads.Utils.AD_STOPPED));
  1103. this.adEventManager_.removeAll();
  1104. this.playingAd_ = false;
  1105. if (!this.usingBaseVideo_) {
  1106. this.video_.style.display = 'none';
  1107. updateBaseVideoTime();
  1108. if (!this.baseVideo_.ended) {
  1109. this.baseVideo_.play();
  1110. }
  1111. } else {
  1112. this.cuepointsChanged_();
  1113. }
  1114. }
  1115. this.determineIfUsingBaseVideo_();
  1116. if (nextCurrentInterstitial) {
  1117. this.onEvent_(
  1118. new shaka.util.FakeEvent(shaka.ads.Utils.AD_STOPPED));
  1119. this.adEventManager_.removeAll();
  1120. this.setupAd_(nextCurrentInterstitial, sequenceLength,
  1121. ++adPosition, initialTime, oncePlayed);
  1122. }
  1123. };
  1124. const error = async (e) => {
  1125. if (unloadingInterstitial) {
  1126. return;
  1127. }
  1128. unloadingInterstitial = true;
  1129. this.onEvent_(new shaka.util.FakeEvent(shaka.ads.Utils.AD_ERROR,
  1130. (new Map()).set('originalEvent', e)));
  1131. await basicTask(/* isSkip= */ false);
  1132. };
  1133. const complete = async () => {
  1134. if (unloadingInterstitial) {
  1135. return;
  1136. }
  1137. unloadingInterstitial = true;
  1138. await basicTask(/* isSkip= */ false);
  1139. this.onEvent_(
  1140. new shaka.util.FakeEvent(shaka.ads.Utils.AD_COMPLETE));
  1141. };
  1142. this.lastOnSkip_ = async () => {
  1143. if (unloadingInterstitial) {
  1144. return;
  1145. }
  1146. unloadingInterstitial = true;
  1147. this.onEvent_(new shaka.util.FakeEvent(shaka.ads.Utils.AD_SKIPPED));
  1148. await basicTask(/* isSkip= */ true);
  1149. };
  1150. const ad = new shaka.ads.InterstitialAd(this.video_,
  1151. interstitial, this.lastOnSkip_, sequenceLength, adPosition,
  1152. !this.usingBaseVideo_);
  1153. if (!this.usingBaseVideo_) {
  1154. ad.setMuted(this.baseVideo_.muted);
  1155. ad.setVolume(this.baseVideo_.volume);
  1156. }
  1157. this.onEvent_(
  1158. new shaka.util.FakeEvent(shaka.ads.Utils.AD_STARTED,
  1159. (new Map()).set('ad', ad)));
  1160. let prevCanSkipNow = ad.canSkipNow();
  1161. if (prevCanSkipNow) {
  1162. this.onEvent_(new shaka.util.FakeEvent(
  1163. shaka.ads.Utils.AD_SKIP_STATE_CHANGED));
  1164. }
  1165. this.adEventManager_.listenOnce(this.player_, 'error', error);
  1166. this.adEventManager_.listen(this.video_, 'timeupdate', () => {
  1167. const duration = this.video_.duration;
  1168. if (!duration) {
  1169. return;
  1170. }
  1171. const currentCanSkipNow = ad.canSkipNow();
  1172. if (prevCanSkipNow != currentCanSkipNow &&
  1173. ad.getRemainingTime() > 0 && ad.getDuration() > 0) {
  1174. this.onEvent_(
  1175. new shaka.util.FakeEvent(shaka.ads.Utils.AD_SKIP_STATE_CHANGED));
  1176. }
  1177. prevCanSkipNow = currentCanSkipNow;
  1178. });
  1179. this.adEventManager_.listenOnce(this.player_, 'firstquartile', () => {
  1180. updateBaseVideoTime();
  1181. this.onEvent_(
  1182. new shaka.util.FakeEvent(shaka.ads.Utils.AD_FIRST_QUARTILE));
  1183. });
  1184. this.adEventManager_.listenOnce(this.player_, 'midpoint', () => {
  1185. updateBaseVideoTime();
  1186. this.onEvent_(
  1187. new shaka.util.FakeEvent(shaka.ads.Utils.AD_MIDPOINT));
  1188. });
  1189. this.adEventManager_.listenOnce(this.player_, 'thirdquartile', () => {
  1190. updateBaseVideoTime();
  1191. this.onEvent_(
  1192. new shaka.util.FakeEvent(shaka.ads.Utils.AD_THIRD_QUARTILE));
  1193. });
  1194. this.adEventManager_.listenOnce(this.player_, 'complete', complete);
  1195. this.adEventManager_.listen(this.video_, 'play', () => {
  1196. this.onEvent_(
  1197. new shaka.util.FakeEvent(shaka.ads.Utils.AD_RESUMED));
  1198. });
  1199. this.adEventManager_.listen(this.video_, 'pause', () => {
  1200. // playRangeEnd in src= causes the ended event not to be fired when that
  1201. // position is reached, instead pause event is fired.
  1202. const currentConfig = this.player_.getConfiguration();
  1203. if (this.video_.currentTime >= currentConfig.playRangeEnd) {
  1204. complete();
  1205. return;
  1206. }
  1207. this.onEvent_(
  1208. new shaka.util.FakeEvent(shaka.ads.Utils.AD_PAUSED));
  1209. });
  1210. this.adEventManager_.listen(this.video_, 'volumechange', () => {
  1211. if (this.video_.muted) {
  1212. this.onEvent_(
  1213. new shaka.util.FakeEvent(shaka.ads.Utils.AD_MUTED));
  1214. } else {
  1215. this.onEvent_(
  1216. new shaka.util.FakeEvent(shaka.ads.Utils.AD_VOLUME_CHANGED));
  1217. }
  1218. });
  1219. try {
  1220. this.updatePlayerConfig_();
  1221. if (interstitial.startTime && interstitial.endTime &&
  1222. interstitial.endTime != Infinity &&
  1223. interstitial.startTime != interstitial.endTime) {
  1224. const duration = interstitial.endTime - interstitial.startTime;
  1225. if (duration > 0) {
  1226. this.player_.configure('playRangeEnd', duration);
  1227. }
  1228. }
  1229. if (interstitial.playoutLimit && !this.playoutLimitTimer_) {
  1230. this.playoutLimitTimer_ = new shaka.util.Timer(() => {
  1231. this.lastOnSkip_();
  1232. }).tickAfter(interstitial.playoutLimit);
  1233. this.player_.configure('playRangeEnd', interstitial.playoutLimit);
  1234. }
  1235. await this.player_.attach(this.video_);
  1236. let playerStartTime = null;
  1237. if (this.config_.allowStartInMiddleOfInterstitial &&
  1238. this.lastTime_ != null) {
  1239. const newPosition = this.lastTime_ - interstitial.startTime;
  1240. if (Math.abs(newPosition) > 0.25) {
  1241. playerStartTime = newPosition;
  1242. }
  1243. }
  1244. if (this.preloadManagerInterstitials_.has(interstitial)) {
  1245. const preloadManager =
  1246. await this.preloadManagerInterstitials_.get(interstitial);
  1247. this.preloadManagerInterstitials_.delete(interstitial);
  1248. if (preloadManager) {
  1249. await this.player_.load(preloadManager);
  1250. } else {
  1251. await this.player_.load(
  1252. interstitial.uri,
  1253. playerStartTime,
  1254. interstitial.mimeType || undefined);
  1255. }
  1256. } else {
  1257. await this.player_.load(
  1258. interstitial.uri,
  1259. playerStartTime,
  1260. interstitial.mimeType || undefined);
  1261. }
  1262. this.video_.play();
  1263. const loadTime = (Date.now() - startTime) / 1000;
  1264. this.onEvent_(new shaka.util.FakeEvent(shaka.ads.Utils.ADS_LOADED,
  1265. (new Map()).set('loadTime', loadTime)));
  1266. if (this.usingBaseVideo_) {
  1267. this.baseVideo_.play();
  1268. }
  1269. if (interstitial.overlay) {
  1270. const setPosition = () => {
  1271. const newPosition =
  1272. this.baseVideo_.currentTime - interstitial.startTime;
  1273. if (Math.abs(newPosition - this.video_.currentTime) > 0.1) {
  1274. this.video_.currentTime = newPosition;
  1275. }
  1276. };
  1277. this.adEventManager_.listenOnce(this.video_, 'playing', setPosition);
  1278. this.adEventManager_.listen(this.baseVideo_, 'seeking', setPosition);
  1279. this.adEventManager_.listen(this.baseVideo_, 'seeked', () => {
  1280. const currentTime = this.baseVideo_.currentTime;
  1281. if (currentTime < interstitial.startTime ||
  1282. (interstitial.endTime && currentTime > interstitial.endTime)) {
  1283. this.lastOnSkip_();
  1284. }
  1285. });
  1286. }
  1287. } catch (e) {
  1288. if (!this.playingAd_) {
  1289. return;
  1290. }
  1291. error(e);
  1292. }
  1293. }
  1294. /**
  1295. * @param {shaka.extern.AdInterstitial} interstitial
  1296. * @private
  1297. */
  1298. setBaseStyles_(interstitial) {
  1299. if (interstitial.displayOnBackground) {
  1300. this.baseVideo_.style.zIndex = '1';
  1301. }
  1302. if (interstitial.currentVideo != null) {
  1303. const currentVideo = interstitial.currentVideo;
  1304. this.baseVideo_.style.transformOrigin = 'top left';
  1305. let addTransition = true;
  1306. const transforms = [];
  1307. const translateX = currentVideo.topLeft.x / currentVideo.viewport.x * 100;
  1308. if (translateX > 0 && translateX <= 100) {
  1309. transforms.push(`translateX(${translateX}%)`);
  1310. // In the case of double box ads we do not want transitions.
  1311. addTransition = false;
  1312. }
  1313. const translateY = currentVideo.topLeft.y / currentVideo.viewport.y * 100;
  1314. if (translateY > 0 && translateY <= 100) {
  1315. transforms.push(`translateY(${translateY}%)`);
  1316. // In the case of double box ads we do not want transitions.
  1317. addTransition = false;
  1318. }
  1319. const scaleX = currentVideo.size.x / currentVideo.viewport.x;
  1320. if (scaleX < 1) {
  1321. transforms.push(`scaleX(${scaleX})`);
  1322. }
  1323. const scaleY = currentVideo.size.y / currentVideo.viewport.y;
  1324. if (scaleX < 1) {
  1325. transforms.push(`scaleY(${scaleY})`);
  1326. }
  1327. if (transforms.length) {
  1328. this.baseVideo_.style.transform = transforms.join(' ');
  1329. }
  1330. if (addTransition) {
  1331. this.baseVideo_.style.transition = 'transform 250ms';
  1332. }
  1333. }
  1334. if (this.adContainer_) {
  1335. this.adContainer_.style.pointerEvents = 'none';
  1336. if (interstitial.background) {
  1337. this.adContainer_.style.background = interstitial.background;
  1338. }
  1339. }
  1340. if (this.adVideo_) {
  1341. if (interstitial.overlay) {
  1342. this.adVideo_.style.background = '';
  1343. } else {
  1344. this.adVideo_.style.background = 'rgb(0, 0, 0)';
  1345. }
  1346. }
  1347. }
  1348. /**
  1349. * @param {?shaka.extern.AdInterstitial=} interstitial
  1350. * @private
  1351. */
  1352. removeBaseStyles_(interstitial) {
  1353. if (!interstitial || interstitial.displayOnBackground) {
  1354. this.baseVideo_.style.zIndex = '';
  1355. }
  1356. if (!interstitial || interstitial.currentVideo != null) {
  1357. this.baseVideo_.style.transformOrigin = '';
  1358. this.baseVideo_.style.transition = '';
  1359. this.baseVideo_.style.transform = '';
  1360. }
  1361. if (this.adContainer_) {
  1362. this.adContainer_.style.pointerEvents = '';
  1363. if (!interstitial || interstitial.background) {
  1364. this.adContainer_.style.background = '';
  1365. }
  1366. }
  1367. if (this.adVideo_) {
  1368. this.adVideo_.style.background = '';
  1369. }
  1370. }
  1371. /**
  1372. * @param {shaka.extern.HLSInterstitial} hlsInterstitial
  1373. * @return {!Promise<!Array<shaka.extern.AdInterstitial>>}
  1374. * @private
  1375. */
  1376. async getInterstitialsInfo_(hlsInterstitial) {
  1377. const interstitialsAd = [];
  1378. if (!hlsInterstitial) {
  1379. return interstitialsAd;
  1380. }
  1381. const assetUri = hlsInterstitial.values.find((v) => v.key == 'X-ASSET-URI');
  1382. const assetList =
  1383. hlsInterstitial.values.find((v) => v.key == 'X-ASSET-LIST');
  1384. if (!assetUri && !assetList) {
  1385. return interstitialsAd;
  1386. }
  1387. let id = null;
  1388. const hlsInterstitialId = hlsInterstitial.values.find((v) => v.key == 'ID');
  1389. if (hlsInterstitialId) {
  1390. id = /** @type {string} */(hlsInterstitialId.data);
  1391. }
  1392. const startTime = id == null ?
  1393. Math.floor(hlsInterstitial.startTime * 10) / 10:
  1394. hlsInterstitial.startTime;
  1395. let endTime = hlsInterstitial.endTime;
  1396. if (hlsInterstitial.endTime && hlsInterstitial.endTime != Infinity &&
  1397. typeof(hlsInterstitial.endTime) == 'number') {
  1398. endTime = id == null ?
  1399. Math.floor(hlsInterstitial.endTime * 10) / 10:
  1400. hlsInterstitial.endTime;
  1401. }
  1402. const restrict = hlsInterstitial.values.find((v) => v.key == 'X-RESTRICT');
  1403. let isSkippable = true;
  1404. let canJump = true;
  1405. if (restrict && restrict.data) {
  1406. const data = /** @type {string} */(restrict.data);
  1407. isSkippable = !data.includes('SKIP');
  1408. canJump = !data.includes('JUMP');
  1409. }
  1410. let skipOffset = isSkippable ? 0 : null;
  1411. const enableSkipAfter =
  1412. hlsInterstitial.values.find((v) => v.key == 'X-ENABLE-SKIP-AFTER');
  1413. if (enableSkipAfter) {
  1414. const enableSkipAfterString = /** @type {string} */(enableSkipAfter.data);
  1415. skipOffset = parseFloat(enableSkipAfterString);
  1416. if (isNaN(skipOffset)) {
  1417. skipOffset = isSkippable ? 0 : null;
  1418. }
  1419. }
  1420. let skipFor = null;
  1421. const enableSkipFor =
  1422. hlsInterstitial.values.find((v) => v.key == 'X-ENABLE-SKIP-FOR');
  1423. if (enableSkipFor) {
  1424. const enableSkipForString = /** @type {string} */(enableSkipFor.data);
  1425. skipFor = parseFloat(enableSkipForString);
  1426. if (isNaN(skipOffset)) {
  1427. skipFor = null;
  1428. }
  1429. }
  1430. let resumeOffset = null;
  1431. const resume =
  1432. hlsInterstitial.values.find((v) => v.key == 'X-RESUME-OFFSET');
  1433. if (resume) {
  1434. const resumeOffsetString = /** @type {string} */(resume.data);
  1435. resumeOffset = parseFloat(resumeOffsetString);
  1436. if (isNaN(resumeOffset)) {
  1437. resumeOffset = null;
  1438. }
  1439. }
  1440. let playoutLimit = null;
  1441. const playout =
  1442. hlsInterstitial.values.find((v) => v.key == 'X-PLAYOUT-LIMIT');
  1443. if (playout) {
  1444. const playoutLimitString = /** @type {string} */(playout.data);
  1445. playoutLimit = parseFloat(playoutLimitString);
  1446. if (isNaN(playoutLimit)) {
  1447. playoutLimit = null;
  1448. }
  1449. }
  1450. let once = false;
  1451. let pre = false;
  1452. let post = false;
  1453. const cue = hlsInterstitial.values.find((v) => v.key == 'CUE');
  1454. if (cue) {
  1455. const data = /** @type {string} */(cue.data);
  1456. once = data.includes('ONCE');
  1457. pre = data.includes('PRE');
  1458. post = data.includes('POST');
  1459. }
  1460. let timelineRange = false;
  1461. const timelineOccupies =
  1462. hlsInterstitial.values.find((v) => v.key == 'X-TIMELINE-OCCUPIES');
  1463. if (timelineOccupies) {
  1464. const data = /** @type {string} */(timelineOccupies.data);
  1465. timelineRange = data.includes('RANGE');
  1466. } else if (!resume && this.basePlayer_.isLive()) {
  1467. timelineRange = !pre && !post;
  1468. }
  1469. if (assetUri) {
  1470. const uri = /** @type {string} */(assetUri.data);
  1471. if (!uri) {
  1472. return interstitialsAd;
  1473. }
  1474. interstitialsAd.push({
  1475. id,
  1476. groupId: null,
  1477. startTime,
  1478. endTime,
  1479. uri,
  1480. mimeType: null,
  1481. isSkippable,
  1482. skipOffset,
  1483. skipFor,
  1484. canJump,
  1485. resumeOffset,
  1486. playoutLimit,
  1487. once,
  1488. pre,
  1489. post,
  1490. timelineRange,
  1491. loop: false,
  1492. overlay: null,
  1493. displayOnBackground: false,
  1494. currentVideo: null,
  1495. background: null,
  1496. });
  1497. } else if (assetList) {
  1498. const uri = /** @type {string} */(assetList.data);
  1499. if (!uri) {
  1500. return interstitialsAd;
  1501. }
  1502. try {
  1503. const NetworkingEngine = shaka.net.NetworkingEngine;
  1504. const context = {
  1505. type: NetworkingEngine.AdvancedRequestType.INTERSTITIAL_ASSET_LIST,
  1506. };
  1507. const responseData = await this.makeAdRequest_(uri, context);
  1508. const data = shaka.util.StringUtils.fromUTF8(responseData);
  1509. const dataAsJson =
  1510. /** @type {!shaka.ads.InterstitialAdManager.AssetsList} */ (
  1511. JSON.parse(data));
  1512. const skipControl = dataAsJson['SKIP-CONTROL'];
  1513. if (skipControl) {
  1514. const enableSkipAfterValue = skipControl['ENABLE-SKIP-AFTER'];
  1515. if ((typeof enableSkipAfterValue) == 'number') {
  1516. skipOffset = parseFloat(enableSkipAfterValue);
  1517. if (isNaN(enableSkipAfterValue)) {
  1518. skipOffset = isSkippable ? 0 : null;
  1519. }
  1520. }
  1521. const enableSkipForValue = skipControl['ENABLE-SKIP-FOR'];
  1522. if ((typeof enableSkipForValue) == 'number') {
  1523. skipFor = parseFloat(enableSkipForValue);
  1524. if (isNaN(enableSkipForValue)) {
  1525. skipFor = null;
  1526. }
  1527. }
  1528. }
  1529. for (let i = 0; i < dataAsJson['ASSETS'].length; i++) {
  1530. const asset = dataAsJson['ASSETS'][i];
  1531. if (asset['URI']) {
  1532. interstitialsAd.push({
  1533. id: id + '_shaka_asset_' + i,
  1534. groupId: id,
  1535. startTime,
  1536. endTime,
  1537. uri: asset['URI'],
  1538. mimeType: null,
  1539. isSkippable,
  1540. skipOffset,
  1541. skipFor,
  1542. canJump,
  1543. resumeOffset,
  1544. playoutLimit,
  1545. once,
  1546. pre,
  1547. post,
  1548. timelineRange,
  1549. loop: false,
  1550. overlay: null,
  1551. displayOnBackground: false,
  1552. currentVideo: null,
  1553. background: null,
  1554. });
  1555. }
  1556. }
  1557. } catch (e) {
  1558. // Ignore errors
  1559. }
  1560. }
  1561. return interstitialsAd;
  1562. }
  1563. /**
  1564. * @private
  1565. */
  1566. cuepointsChanged_() {
  1567. /** @type {!Array<!shaka.extern.AdCuePoint>} */
  1568. const cuePoints = [];
  1569. for (const interstitial of this.interstitials_) {
  1570. if (interstitial.overlay) {
  1571. continue;
  1572. }
  1573. /** @type {shaka.extern.AdCuePoint} */
  1574. const shakaCuePoint = {
  1575. start: interstitial.startTime,
  1576. end: null,
  1577. };
  1578. if (interstitial.pre) {
  1579. shakaCuePoint.start = 0;
  1580. shakaCuePoint.end = null;
  1581. } else if (interstitial.post) {
  1582. shakaCuePoint.start = -1;
  1583. shakaCuePoint.end = null;
  1584. } else if (interstitial.timelineRange) {
  1585. shakaCuePoint.end = interstitial.endTime;
  1586. }
  1587. const isValid = !cuePoints.find((c) => {
  1588. return shakaCuePoint.start == c.start && shakaCuePoint.end == c.end;
  1589. });
  1590. if (isValid) {
  1591. cuePoints.push(shakaCuePoint);
  1592. }
  1593. }
  1594. this.onEvent_(new shaka.util.FakeEvent(
  1595. shaka.ads.Utils.CUEPOINTS_CHANGED,
  1596. (new Map()).set('cuepoints', cuePoints)));
  1597. }
  1598. /**
  1599. * @private
  1600. */
  1601. updatePlayerConfig_() {
  1602. goog.asserts.assert(this.player_, 'Must have player');
  1603. goog.asserts.assert(this.basePlayer_, 'Must have base player');
  1604. this.player_.configure(this.basePlayer_.getNonDefaultConfiguration());
  1605. this.player_.configure('ads.disableHLSInterstitial', true);
  1606. this.player_.configure('ads.disableDASHInterstitial', true);
  1607. this.player_.configure('playRangeEnd', Infinity);
  1608. const netEngine = this.player_.getNetworkingEngine();
  1609. goog.asserts.assert(netEngine, 'Need networking engine');
  1610. this.basePlayer_.getNetworkingEngine().copyFiltersInto(netEngine);
  1611. }
  1612. /**
  1613. * @param {string} url
  1614. * @param {shaka.extern.RequestContext=} context
  1615. * @return {!Promise<BufferSource>}
  1616. * @private
  1617. */
  1618. async makeAdRequest_(url, context) {
  1619. const type = shaka.net.NetworkingEngine.RequestType.ADS;
  1620. const request = shaka.net.NetworkingEngine.makeRequest(
  1621. [url],
  1622. shaka.net.NetworkingEngine.defaultRetryParameters());
  1623. const op = this.basePlayer_.getNetworkingEngine()
  1624. .request(type, request, context);
  1625. const response = await op.promise;
  1626. return response.data;
  1627. }
  1628. /**
  1629. * @param {!shaka.extern.AdInterstitial} interstitial
  1630. * @return {boolean}
  1631. * @private
  1632. */
  1633. isPreloadAllowed_(interstitial) {
  1634. const interstitialMimeType = interstitial.mimeType;
  1635. if (!interstitialMimeType) {
  1636. return true;
  1637. }
  1638. return !interstitialMimeType.startsWith('image/') &&
  1639. interstitialMimeType !== 'text/html';
  1640. }
  1641. /**
  1642. * Only for testing
  1643. *
  1644. * @return {!Array<shaka.extern.AdInterstitial>}
  1645. */
  1646. getInterstitials() {
  1647. return Array.from(this.interstitials_);
  1648. }
  1649. /**
  1650. * @return {boolean}
  1651. * @private
  1652. */
  1653. isSmartTV_() {
  1654. const device = shaka.device.DeviceFactory.getDevice();
  1655. const deviceType = device.getDeviceType();
  1656. if (deviceType == shaka.device.IDevice.DeviceType.TV ||
  1657. deviceType == shaka.device.IDevice.DeviceType.CONSOLE ||
  1658. deviceType == shaka.device.IDevice.DeviceType.CAST) {
  1659. return true;
  1660. }
  1661. return false;
  1662. }
  1663. /**
  1664. * @param {!shaka.extern.AdInterstitial} interstitial
  1665. * @private
  1666. */
  1667. checkPreloadOnDomElements_(interstitial) {
  1668. if (this.preloadOnDomElements_.has(interstitial) ||
  1669. (this.config_ && !this.config_.allowPreloadOnDomElements)) {
  1670. return;
  1671. }
  1672. const createAndAddLink = (url) => {
  1673. const link = /** @type {HTMLLinkElement} */(
  1674. document.createElement('link'));
  1675. link.rel = 'preload';
  1676. link.href = url;
  1677. link.as = 'image';
  1678. document.head.appendChild(link);
  1679. return link;
  1680. };
  1681. const links = [];
  1682. if (interstitial.background) {
  1683. const urlRegExp = /url\(('|")?([^'"()]+)('|")\)?/;
  1684. const match = interstitial.background.match(urlRegExp);
  1685. if (match) {
  1686. links.push(createAndAddLink(match[2]));
  1687. }
  1688. }
  1689. if (interstitial.mimeType.startsWith('image/')) {
  1690. links.push(createAndAddLink(interstitial.uri));
  1691. }
  1692. this.preloadOnDomElements_.set(interstitial, links);
  1693. }
  1694. /**
  1695. * @param {!shaka.extern.AdInterstitial} interstitial
  1696. * @private
  1697. */
  1698. removePreloadOnDomElements_(interstitial) {
  1699. if (!this.preloadOnDomElements_.has(interstitial)) {
  1700. return;
  1701. }
  1702. const links = this.preloadOnDomElements_.get(interstitial);
  1703. for (const link of links) {
  1704. link.parentNode.removeChild(link);
  1705. }
  1706. this.preloadOnDomElements_.delete(interstitial);
  1707. }
  1708. };
  1709. /**
  1710. * @typedef {{
  1711. * ASSETS: !Array<shaka.ads.InterstitialAdManager.Asset>,
  1712. * SKIP-CONTROL: ?shaka.ads.InterstitialAdManager.SkipControl,
  1713. * }}
  1714. *
  1715. * @property {!Array<shaka.ads.InterstitialAdManager.Asset>} ASSETS
  1716. * @property {shaka.ads.InterstitialAdManager.SkipControl} SKIP-CONTROL
  1717. */
  1718. shaka.ads.InterstitialAdManager.AssetsList;
  1719. /**
  1720. * @typedef {{
  1721. * URI: string,
  1722. * }}
  1723. *
  1724. * @property {string} URI
  1725. */
  1726. shaka.ads.InterstitialAdManager.Asset;
  1727. /**
  1728. * @typedef {{
  1729. * ENABLE-SKIP-AFTER: number,
  1730. * ENABLE-SKIP-FOR: number,
  1731. * }}
  1732. *
  1733. * @property {number} ENABLE-SKIP-AFTER
  1734. * @property {number} ENABLE-SKIP-FOR
  1735. */
  1736. shaka.ads.InterstitialAdManager.SkipControl;