Source: lib/ads/interstitial_ad_manager.js

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