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