Source: lib/ads/media_tailor_ad_manager.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. /**
  7. * @fileoverview
  8. * @suppress {missingRequire} TODO(b/152540451): this shouldn't be needed
  9. */
  10. goog.provide('shaka.ads.MediaTailorAdManager');
  11. goog.require('goog.asserts');
  12. goog.require('shaka.ads.MediaTailorAd');
  13. goog.require('shaka.log');
  14. goog.require('shaka.net.NetworkingEngine');
  15. goog.require('goog.Uri');
  16. goog.require('shaka.util.EventManager');
  17. goog.require('shaka.util.Error');
  18. goog.require('shaka.util.FakeEvent');
  19. goog.require('shaka.util.IReleasable');
  20. goog.require('shaka.util.PublicPromise');
  21. goog.require('shaka.util.StringUtils');
  22. /**
  23. * A class responsible for MediaTailor ad interactions.
  24. *
  25. * @implements {shaka.util.IReleasable}
  26. */
  27. shaka.ads.MediaTailorAdManager = class {
  28. /**
  29. * @param {HTMLElement} adContainer
  30. * @param {shaka.net.NetworkingEngine} networkingEngine
  31. * @param {HTMLMediaElement} video
  32. * @param {function(!shaka.util.FakeEvent)} onEvent
  33. */
  34. constructor(adContainer, networkingEngine, video, onEvent) {
  35. /** @private {HTMLElement} */
  36. this.adContainer_ = adContainer;
  37. /** @private {shaka.net.NetworkingEngine} */
  38. this.networkingEngine_ = networkingEngine;
  39. /** @private {HTMLMediaElement} */
  40. this.video_ = video;
  41. /** @private {?shaka.util.PublicPromise.<string>} */
  42. this.streamPromise_ = null;
  43. /** @private {number} */
  44. this.streamRequestStartTime_ = NaN;
  45. /** @private {function(!shaka.util.FakeEvent)} */
  46. this.onEvent_ = onEvent;
  47. /** @private {boolean} */
  48. this.isLive_ = false;
  49. /**
  50. * Time to seek to after an ad if that ad was played as the result of
  51. * snapback.
  52. * @private {?number}
  53. */
  54. this.snapForwardTime_ = null;
  55. /** @private {!Array.<!mediaTailor.AdBreak>} */
  56. this.adBreaks_ = [];
  57. /** @private {!Array.<string>} */
  58. this.playedAds_ = [];
  59. /** @private {?shaka.ads.MediaTailorAd} */
  60. this.ad_ = null;
  61. /** @private {?mediaTailor.Ad} */
  62. this.mediaTailorAd_ = null;
  63. /** @private {?mediaTailor.AdBreak} */
  64. this.mediaTailorAdBreak_ = null;
  65. /** @private {!Map.<string,!Array.<mediaTailorExternalResource.App>>} */
  66. this.staticResources_ = new Map();
  67. /** @private {!Array.<{target: EventTarget, type: string,
  68. * listener: shaka.util.EventManager.ListenerType}>}
  69. */
  70. this.adListeners_ = [];
  71. /** @private {!Array.<string>} */
  72. this.eventsSent = [];
  73. /** @private {string} */
  74. this.trackingUrl_ = '';
  75. /** @private {boolean} */
  76. this.firstTrackingRequest_ = true;
  77. /** @private {string} */
  78. this.backupUrl_ = '';
  79. /** @private {!Array.<!shaka.extern.AdCuePoint>} */
  80. this.currentCuePoints_ = [];
  81. /** @private {shaka.util.EventManager} */
  82. this.eventManager_ = new shaka.util.EventManager();
  83. }
  84. /**
  85. * @param {string} url
  86. * @param {Object} adsParams
  87. * @param {string} backupUrl
  88. * @return {!Promise.<string>}
  89. */
  90. streamRequest(url, adsParams, backupUrl) {
  91. if (this.streamPromise_) {
  92. return Promise.reject(new shaka.util.Error(
  93. shaka.util.Error.Severity.RECOVERABLE,
  94. shaka.util.Error.Category.ADS,
  95. shaka.util.Error.Code.CURRENT_DAI_REQUEST_NOT_FINISHED));
  96. }
  97. this.streamPromise_ = new shaka.util.PublicPromise();
  98. this.requestSessionInfo_(url, adsParams);
  99. this.backupUrl_ = backupUrl || '';
  100. this.streamRequestStartTime_ = Date.now() / 1000;
  101. return this.streamPromise_;
  102. }
  103. /**
  104. * @param {string} url
  105. */
  106. addTrackingUrl(url) {
  107. this.trackingUrl_ = url;
  108. this.onEvent_(new shaka.util.FakeEvent(shaka.ads.AdManager.ADS_LOADED,
  109. (new Map()).set('loadTime', 0)));
  110. }
  111. /**
  112. * Resets the MediaTailor manager and removes any continuous polling.
  113. */
  114. stop() {
  115. for (const listener of this.adListeners_) {
  116. this.eventManager_.unlisten(
  117. listener.target, listener.type, listener.listener);
  118. }
  119. this.onEnded_();
  120. this.adListeners_ = [];
  121. this.eventsSent = [];
  122. this.trackingUrl_ = '';
  123. this.firstTrackingRequest_ = true;
  124. this.backupUrl_ = '';
  125. this.snapForwardTime_ = null;
  126. this.adBreaks_ = [];
  127. this.playedAds_ = [];
  128. this.staticResources_.clear();
  129. }
  130. /** @override */
  131. release() {
  132. this.stop();
  133. if (this.eventManager_) {
  134. this.eventManager_.release();
  135. }
  136. }
  137. /**
  138. * Fired when the manifest is updated
  139. *
  140. * @param {boolean} isLive
  141. */
  142. onManifestUpdated(isLive) {
  143. this.isLive_ = isLive;
  144. if (this.trackingUrl_ != '') {
  145. this.requestTrackingInfo_(
  146. this.trackingUrl_, this.firstTrackingRequest_);
  147. this.firstTrackingRequest_ = false;
  148. }
  149. }
  150. /**
  151. * @return {!Array.<!shaka.extern.AdCuePoint>}
  152. */
  153. getCuePoints() {
  154. const cuePoints = [];
  155. for (const adbreak of this.adBreaks_) {
  156. for (const ad of adbreak.ads) {
  157. /** @type {!shaka.extern.AdCuePoint} */
  158. const cuePoint = {
  159. start: ad.startTimeInSeconds,
  160. end: ad.startTimeInSeconds + ad.durationInSeconds,
  161. };
  162. cuePoints.push(cuePoint);
  163. }
  164. }
  165. return cuePoints;
  166. }
  167. /**
  168. * @param {string} url
  169. * @param {Object} adsParams
  170. * @private
  171. */
  172. async requestSessionInfo_(url, adsParams) {
  173. const type = shaka.net.NetworkingEngine.RequestType.ADS;
  174. const request = shaka.net.NetworkingEngine.makeRequest(
  175. [url],
  176. shaka.net.NetworkingEngine.defaultRetryParameters());
  177. request.method = 'POST';
  178. if (adsParams) {
  179. const body = JSON.stringify(adsParams);
  180. request.body = shaka.util.StringUtils.toUTF8(body);
  181. }
  182. const op = this.networkingEngine_.request(type, request);
  183. try {
  184. const response = await op.promise;
  185. const data = shaka.util.StringUtils.fromUTF8(response.data);
  186. const dataAsJson =
  187. /** @type {!mediaTailor.SessionResponse} */ (JSON.parse(data));
  188. if (dataAsJson.manifestUrl && dataAsJson.trackingUrl) {
  189. const baseUri = new goog.Uri(url);
  190. const relativeTrackingUri = new goog.Uri(dataAsJson.trackingUrl);
  191. this.trackingUrl_ = baseUri.resolve(relativeTrackingUri).toString();
  192. const now = Date.now() / 1000;
  193. const loadTime = now - this.streamRequestStartTime_;
  194. this.onEvent_(new shaka.util.FakeEvent(shaka.ads.AdManager.ADS_LOADED,
  195. (new Map()).set('loadTime', loadTime)));
  196. const relativeManifestUri = new goog.Uri(dataAsJson.manifestUrl);
  197. this.streamPromise_.resolve(
  198. baseUri.resolve(relativeManifestUri).toString());
  199. this.streamPromise_ = null;
  200. } else {
  201. throw new Error('Insufficient data from MediaTailor.');
  202. }
  203. } catch (e) {
  204. if (!this.backupUrl_.length) {
  205. this.streamPromise_.reject('MediaTailor request returned an error ' +
  206. 'and there was no backup asset uri provided.');
  207. this.streamPromise_ = null;
  208. return;
  209. }
  210. shaka.log.warning('MediaTailor request returned an error. ' +
  211. 'Falling back to the backup asset uri.');
  212. this.streamPromise_.resolve(this.backupUrl_);
  213. this.streamPromise_ = null;
  214. }
  215. }
  216. /**
  217. * @param {string} trackingUrl
  218. * @param {boolean} firstRequest
  219. * @private
  220. */
  221. async requestTrackingInfo_(trackingUrl, firstRequest) {
  222. const type = shaka.net.NetworkingEngine.RequestType.ADS;
  223. const request = shaka.net.NetworkingEngine.makeRequest(
  224. [trackingUrl],
  225. shaka.net.NetworkingEngine.defaultRetryParameters());
  226. const op = this.networkingEngine_.request(type, request);
  227. try {
  228. const response = await op.promise;
  229. let cuepoints = [];
  230. const data = shaka.util.StringUtils.fromUTF8(response.data);
  231. const dataAsJson =
  232. /** @type {!mediaTailor.TrackingResponse} */ (JSON.parse(data));
  233. if (dataAsJson.avails.length > 0) {
  234. if (JSON.stringify(this.adBreaks_) !=
  235. JSON.stringify(dataAsJson.avails)) {
  236. this.adBreaks_ = dataAsJson.avails;
  237. for (const adbreak of this.adBreaks_) {
  238. for (const nonLinearAd of adbreak.nonLinearAdsList) {
  239. for (const nonLinearAdResource of nonLinearAd.nonLinearAdList) {
  240. this.requestStaticResource_(nonLinearAdResource);
  241. }
  242. }
  243. }
  244. cuepoints = this.getCuePoints();
  245. this.onEvent_(new shaka.util.FakeEvent(
  246. shaka.ads.AdManager.CUEPOINTS_CHANGED,
  247. (new Map()).set('cuepoints', cuepoints)));
  248. }
  249. } else {
  250. if (this.adBreaks_.length) {
  251. this.onEvent_(new shaka.util.FakeEvent(
  252. shaka.ads.AdManager.CUEPOINTS_CHANGED,
  253. (new Map()).set('cuepoints', cuepoints)));
  254. }
  255. this.onEnded_();
  256. this.adBreaks_ = [];
  257. }
  258. if (firstRequest && (this.isLive_ || cuepoints.length > 0)) {
  259. this.setupAdBreakListeners_();
  260. }
  261. } catch (e) {}
  262. }
  263. /**
  264. * @param {mediaTailor.NonLinearAd} nonLinearAd
  265. * @private
  266. */
  267. async requestStaticResource_(nonLinearAd) {
  268. if (!nonLinearAd.staticResource) {
  269. return;
  270. }
  271. const cacheKey = this.getCacheKeyForNonLinear_(nonLinearAd);
  272. const staticResource = this.staticResources_.get(cacheKey);
  273. if (staticResource) {
  274. return;
  275. }
  276. const type = shaka.net.NetworkingEngine.RequestType.ADS;
  277. const request = shaka.net.NetworkingEngine.makeRequest(
  278. [nonLinearAd.staticResource],
  279. shaka.net.NetworkingEngine.defaultRetryParameters());
  280. const op = this.networkingEngine_.request(type, request);
  281. try {
  282. this.staticResources_.set(cacheKey, []);
  283. const response = await op.promise;
  284. const data = shaka.util.StringUtils.fromUTF8(response.data);
  285. const dataAsJson =
  286. /** @type {!mediaTailorExternalResource.Response} */ (JSON.parse(data));
  287. const apps = dataAsJson.apps;
  288. this.staticResources_.set(cacheKey, apps);
  289. } catch (e) {
  290. this.staticResources_.delete(cacheKey);
  291. }
  292. }
  293. /**
  294. * @param {mediaTailor.NonLinearAd} nonLinearAd
  295. * @return {string}
  296. * @private
  297. */
  298. getCacheKeyForNonLinear_(nonLinearAd) {
  299. return [
  300. nonLinearAd.adId,
  301. nonLinearAd.adParameters,
  302. nonLinearAd.adSystem,
  303. nonLinearAd.adTitle,
  304. nonLinearAd.creativeAdId,
  305. nonLinearAd.creativeId,
  306. nonLinearAd.creativeSequence,
  307. nonLinearAd.height,
  308. nonLinearAd.width,
  309. nonLinearAd.staticResource,
  310. ].join('');
  311. }
  312. /**
  313. * Setup Ad Break listeners
  314. *
  315. * @private
  316. */
  317. setupAdBreakListeners_() {
  318. this.onTimeupdate_();
  319. if (!this.isLive_) {
  320. this.checkForSnapback_();
  321. this.eventManager_.listen(this.video_, 'seeked', () => {
  322. this.checkForSnapback_();
  323. });
  324. this.eventManager_.listen(this.video_, 'ended', () => {
  325. this.onEnded_();
  326. });
  327. }
  328. this.eventManager_.listen(this.video_, 'timeupdate', () => {
  329. this.onTimeupdate_();
  330. });
  331. }
  332. /**
  333. * If a seek jumped over the ad break, return to the start of the
  334. * ad break, then complete the seek after the ad played through.
  335. *
  336. * @private
  337. */
  338. checkForSnapback_() {
  339. const currentTime = this.video_.currentTime;
  340. if (currentTime == 0 || this.snapForwardTime_ != null) {
  341. return;
  342. }
  343. let previousAdBreak;
  344. let previousAd;
  345. for (const adbreak of this.adBreaks_) {
  346. for (const ad of adbreak.ads) {
  347. if (!previousAd) {
  348. if (ad.startTimeInSeconds < currentTime) {
  349. previousAd = ad;
  350. previousAdBreak = adbreak;
  351. }
  352. } else if (ad.startTimeInSeconds < currentTime &&
  353. ad.startTimeInSeconds >
  354. (previousAd.startTimeInSeconds + previousAd.durationInSeconds)) {
  355. previousAd = ad;
  356. previousAdBreak = adbreak;
  357. break;
  358. }
  359. }
  360. }
  361. // The cue point gets marked as 'played' as soon as the playhead hits it
  362. // (at the start of an ad), so when we come back to this method as a result
  363. // of seeking back to the user-selected time, the 'played' flag will be set.
  364. if (previousAdBreak && previousAd &&
  365. !this.playedAds_.includes(previousAd.adId)) {
  366. shaka.log.info('Seeking back to the start of the ad break at ' +
  367. previousAdBreak.startTimeInSeconds +
  368. ' and will return to ' + currentTime);
  369. this.snapForwardTime_ = currentTime;
  370. this.video_.currentTime = previousAdBreak.startTimeInSeconds;
  371. }
  372. }
  373. /**
  374. * @private
  375. */
  376. onAdBreakEnded_() {
  377. const currentTime = this.video_.currentTime;
  378. // If the ad break was a result of snapping back (a user seeked over
  379. // an ad break and was returned to it), seek forward to the point,
  380. // originally chosen by the user.
  381. if (this.snapForwardTime_ && this.snapForwardTime_ > currentTime) {
  382. this.video_.currentTime = this.snapForwardTime_;
  383. }
  384. this.snapForwardTime_ = null;
  385. }
  386. /**
  387. * @private
  388. */
  389. onTimeupdate_() {
  390. if (!this.video_.duration) {
  391. // Can't play yet. Ignore.
  392. return;
  393. }
  394. if (!this.ad_ && !this.adBreaks_.length) {
  395. // No ads
  396. return;
  397. }
  398. const currentTime = this.video_.currentTime;
  399. let previousAd = false;
  400. if (this.ad_) {
  401. previousAd = true;
  402. goog.asserts.assert(this.mediaTailorAd_, 'Ad should be defined');
  403. this.sendInProgressEvents_(currentTime, this.mediaTailorAd_);
  404. const remainingTime = this.ad_.getRemainingTime();
  405. const duration = this.ad_.getDuration();
  406. if (this.ad_.canSkipNow() && remainingTime > 0 && duration > 0) {
  407. this.sendTrackingEvent_(
  408. shaka.ads.MediaTailorAdManager.SKIP_STATE_CHANGED_);
  409. }
  410. if (duration > 0 && (remainingTime <= 0 || remainingTime > duration)) {
  411. this.onEnded_();
  412. }
  413. }
  414. if (!this.ad_ || !this.ad_.isLinear()) {
  415. this.checkLinearAds_(currentTime);
  416. if (!this.ad_) {
  417. this.checkNonLinearAds_(currentTime);
  418. }
  419. if (previousAd && !this.ad_) {
  420. this.onAdBreakEnded_();
  421. }
  422. }
  423. }
  424. /**
  425. * @param {number} currentTime
  426. * @param {mediaTailor.Ad} ad
  427. * @private
  428. */
  429. sendInProgressEvents_(currentTime, ad) {
  430. const MediaTailorAdManager = shaka.ads.MediaTailorAdManager;
  431. const firstQuartileTime = ad.startTimeInSeconds +
  432. 0.25 * ad.durationInSeconds;
  433. const midpointTime = ad.startTimeInSeconds +
  434. 0.5 * ad.durationInSeconds;
  435. const thirdQuartileTime = ad.startTimeInSeconds +
  436. 0.75 * ad.durationInSeconds;
  437. if (currentTime >= firstQuartileTime &&
  438. !this.eventsSent.includes(MediaTailorAdManager.FIRSTQUARTILE_)) {
  439. this.eventsSent.push(MediaTailorAdManager.FIRSTQUARTILE_);
  440. this.sendTrackingEvent_(MediaTailorAdManager.FIRSTQUARTILE_);
  441. } else if (currentTime >= midpointTime &&
  442. !this.eventsSent.includes(MediaTailorAdManager.MIDPOINT_)) {
  443. this.eventsSent.push(MediaTailorAdManager.MIDPOINT_);
  444. this.sendTrackingEvent_(MediaTailorAdManager.MIDPOINT_);
  445. } else if (currentTime >= thirdQuartileTime &&
  446. !this.eventsSent.includes(MediaTailorAdManager.THIRDQUARTILE_)) {
  447. this.eventsSent.push(MediaTailorAdManager.THIRDQUARTILE_);
  448. this.sendTrackingEvent_(MediaTailorAdManager.THIRDQUARTILE_);
  449. }
  450. }
  451. /**
  452. * @param {number} currentTime
  453. * @private
  454. */
  455. checkLinearAds_(currentTime) {
  456. const MediaTailorAdManager = shaka.ads.MediaTailorAdManager;
  457. for (const adbreak of this.adBreaks_) {
  458. if (this.ad_ && this.ad_.isLinear()) {
  459. break;
  460. }
  461. for (let i = 0; i < adbreak.ads.length; i++) {
  462. const ad = adbreak.ads[i];
  463. const startTime = ad.startTimeInSeconds;
  464. const endTime = ad.startTimeInSeconds + ad.durationInSeconds;
  465. if (startTime <= currentTime && endTime > currentTime) {
  466. if (this.playedAds_.includes(ad.adId)) {
  467. if (this.video_.ended) {
  468. continue;
  469. }
  470. this.video_.currentTime = endTime;
  471. return;
  472. }
  473. this.onEnded_();
  474. this.mediaTailorAdBreak_ = adbreak;
  475. this.ad_ = new shaka.ads.MediaTailorAd(
  476. ad,
  477. /* adPosition= */ i + 1,
  478. /* totalAds= */ adbreak.ads.length,
  479. /* isLinear= */ true,
  480. this.video_);
  481. this.mediaTailorAd_ = ad;
  482. if (i === 0) {
  483. this.sendTrackingEvent_(MediaTailorAdManager.BREAKSTART_);
  484. }
  485. this.setupCurrentAdListeners_();
  486. break;
  487. }
  488. }
  489. }
  490. }
  491. /**
  492. * @param {number} currentTime
  493. * @private
  494. */
  495. checkNonLinearAds_(currentTime) {
  496. const MediaTailorAdManager = shaka.ads.MediaTailorAdManager;
  497. for (const adbreak of this.adBreaks_) {
  498. if (this.ad_) {
  499. break;
  500. }
  501. for (let i = 0; i < adbreak.nonLinearAdsList.length; i++) {
  502. const ad = adbreak.nonLinearAdsList[i];
  503. if (!ad.nonLinearAdList.length) {
  504. continue;
  505. }
  506. const startTime = adbreak.startTimeInSeconds;
  507. const cacheKey = this.getCacheKeyForNonLinear_(ad.nonLinearAdList[0]);
  508. const staticResource = this.staticResources_.get(cacheKey);
  509. if (startTime <= currentTime &&
  510. staticResource && staticResource.length) {
  511. this.onEnded_();
  512. this.displayNonLinearAd_(staticResource);
  513. this.mediaTailorAdBreak_ = adbreak;
  514. this.ad_ = new shaka.ads.MediaTailorAd(
  515. ad,
  516. /* adPosition= */ i + 1,
  517. /* totalAds= */ adbreak.ads.length,
  518. /* isLinear= */ false,
  519. this.video_);
  520. this.mediaTailorAd_ = ad;
  521. if (i === 0) {
  522. this.sendTrackingEvent_(MediaTailorAdManager.BREAKSTART_);
  523. }
  524. this.setupCurrentAdListeners_();
  525. break;
  526. }
  527. }
  528. }
  529. }
  530. /**
  531. * @param {!Array.<mediaTailorExternalResource.App>} apps
  532. * @private
  533. */
  534. displayNonLinearAd_(apps) {
  535. for (const app of apps) {
  536. if (!app.data.source.length) {
  537. continue;
  538. }
  539. const imageElement = /** @type {!HTMLImageElement} */ (
  540. document.createElement('img'));
  541. imageElement.src = app.data.source[0].url;
  542. imageElement.style.top = (app.placeholder.top || 0) + '%';
  543. imageElement.style.height = (100 - (app.placeholder.top || 0)) + '%';
  544. imageElement.style.left = (app.placeholder.left || 0) + '%';
  545. imageElement.style.maxWidth = (100 - (app.placeholder.left || 0)) + '%';
  546. imageElement.style.objectFit = 'contain';
  547. imageElement.style.position = 'absolute';
  548. this.adContainer_.appendChild(imageElement);
  549. }
  550. }
  551. /**
  552. * @private
  553. */
  554. onEnded_() {
  555. if (this.ad_) {
  556. // Remove all child nodes
  557. while (this.adContainer_.lastChild) {
  558. this.adContainer_.removeChild(this.adContainer_.firstChild);
  559. }
  560. if (!this.isLive_) {
  561. this.playedAds_.push(this.mediaTailorAd_.adId);
  562. }
  563. this.removeCurrentAdListeners_(this.ad_.isSkipped());
  564. const position = this.ad_.getPositionInSequence();
  565. const totalAdsInBreak = this.ad_.getSequenceLength();
  566. if (position === totalAdsInBreak) {
  567. this.sendTrackingEvent_(shaka.ads.MediaTailorAdManager.BREAKEND_);
  568. }
  569. this.ad_ = null;
  570. this.mediaTailorAd_ = null;
  571. this.mediaTailorAdBreak_ = null;
  572. }
  573. }
  574. /**
  575. * @private
  576. */
  577. setupCurrentAdListeners_() {
  578. const MediaTailorAdManager = shaka.ads.MediaTailorAdManager;
  579. let needFirstEvents = false;
  580. if (!this.video_.paused) {
  581. this.sendTrackingEvent_(MediaTailorAdManager.IMPRESSION_);
  582. this.sendTrackingEvent_(MediaTailorAdManager.START_);
  583. } else {
  584. needFirstEvents = true;
  585. }
  586. this.adListeners_.push({
  587. target: this.video_,
  588. type: 'volumechange',
  589. listener: () => {
  590. if (this.video_.muted) {
  591. this.sendTrackingEvent_(MediaTailorAdManager.MUTE_);
  592. }
  593. },
  594. });
  595. this.adListeners_.push({
  596. target: this.video_,
  597. type: 'volumechange',
  598. listener: () => {
  599. if (!this.video_.muted) {
  600. this.sendTrackingEvent_(MediaTailorAdManager.UNMUTE_);
  601. }
  602. },
  603. });
  604. this.adListeners_.push({
  605. target: this.video_,
  606. type: 'play',
  607. listener: () => {
  608. if (needFirstEvents) {
  609. this.sendTrackingEvent_(MediaTailorAdManager.IMPRESSION_);
  610. this.sendTrackingEvent_(MediaTailorAdManager.START_);
  611. needFirstEvents = false;
  612. } else {
  613. this.sendTrackingEvent_(MediaTailorAdManager.RESUME_);
  614. }
  615. },
  616. });
  617. this.adListeners_.push({
  618. target: this.video_,
  619. type: 'pause',
  620. listener: () => {
  621. this.sendTrackingEvent_(MediaTailorAdManager.PAUSE_);
  622. },
  623. });
  624. for (const listener of this.adListeners_) {
  625. this.eventManager_.listen(
  626. listener.target, listener.type, listener.listener);
  627. }
  628. }
  629. /**
  630. * @param {boolean=} skipped
  631. * @private
  632. */
  633. removeCurrentAdListeners_(skipped = false) {
  634. if (skipped) {
  635. this.sendTrackingEvent_(shaka.ads.MediaTailorAdManager.SKIPPED_);
  636. } else {
  637. this.sendTrackingEvent_(shaka.ads.MediaTailorAdManager.COMPLETE_);
  638. }
  639. for (const listener of this.adListeners_) {
  640. this.eventManager_.unlisten(
  641. listener.target, listener.type, listener.listener);
  642. }
  643. this.adListeners_ = [];
  644. this.eventsSent = [];
  645. }
  646. /**
  647. * @param {string} eventType
  648. * @private
  649. */
  650. sendTrackingEvent_(eventType) {
  651. let trackingEvent = this.mediaTailorAd_.trackingEvents.find(
  652. (event) => event.eventType == eventType);
  653. if (!trackingEvent) {
  654. trackingEvent = this.mediaTailorAdBreak_.adBreakTrackingEvents.find(
  655. (event) => event.eventType == eventType);
  656. }
  657. if (trackingEvent) {
  658. const type = shaka.net.NetworkingEngine.RequestType.ADS;
  659. for (const beaconUrl of trackingEvent.beaconUrls) {
  660. if (!beaconUrl || beaconUrl == '') {
  661. continue;
  662. }
  663. const request = shaka.net.NetworkingEngine.makeRequest(
  664. [beaconUrl],
  665. shaka.net.NetworkingEngine.defaultRetryParameters());
  666. request.method = 'POST';
  667. this.networkingEngine_.request(type, request);
  668. }
  669. }
  670. switch (eventType) {
  671. case shaka.ads.MediaTailorAdManager.IMPRESSION_:
  672. this.onEvent_(
  673. new shaka.util.FakeEvent(shaka.ads.AdManager.AD_IMPRESSION));
  674. break;
  675. case shaka.ads.MediaTailorAdManager.START_:
  676. this.onEvent_(
  677. new shaka.util.FakeEvent(shaka.ads.AdManager.AD_STARTED,
  678. (new Map()).set('ad', this.ad_)));
  679. break;
  680. case shaka.ads.MediaTailorAdManager.MUTE_:
  681. this.onEvent_(
  682. new shaka.util.FakeEvent(shaka.ads.AdManager.AD_MUTED));
  683. break;
  684. case shaka.ads.MediaTailorAdManager.UNMUTE_:
  685. this.onEvent_(
  686. new shaka.util.FakeEvent(shaka.ads.AdManager.AD_VOLUME_CHANGED));
  687. break;
  688. case shaka.ads.MediaTailorAdManager.RESUME_:
  689. this.onEvent_(
  690. new shaka.util.FakeEvent(shaka.ads.AdManager.AD_RESUMED));
  691. break;
  692. case shaka.ads.MediaTailorAdManager.PAUSE_:
  693. this.onEvent_(
  694. new shaka.util.FakeEvent(shaka.ads.AdManager.AD_PAUSED));
  695. break;
  696. case shaka.ads.MediaTailorAdManager.FIRSTQUARTILE_:
  697. this.onEvent_(
  698. new shaka.util.FakeEvent(shaka.ads.AdManager.AD_FIRST_QUARTILE));
  699. break;
  700. case shaka.ads.MediaTailorAdManager.MIDPOINT_:
  701. this.onEvent_(
  702. new shaka.util.FakeEvent(shaka.ads.AdManager.AD_MIDPOINT));
  703. break;
  704. case shaka.ads.MediaTailorAdManager.THIRDQUARTILE_:
  705. this.onEvent_(
  706. new shaka.util.FakeEvent(shaka.ads.AdManager.AD_THIRD_QUARTILE));
  707. break;
  708. case shaka.ads.MediaTailorAdManager.COMPLETE_:
  709. this.onEvent_(
  710. new shaka.util.FakeEvent(shaka.ads.AdManager.AD_COMPLETE));
  711. this.onEvent_(
  712. new shaka.util.FakeEvent(shaka.ads.AdManager.AD_STOPPED));
  713. break;
  714. case shaka.ads.MediaTailorAdManager.SKIPPED_:
  715. this.onEvent_(
  716. new shaka.util.FakeEvent(shaka.ads.AdManager.AD_SKIPPED));
  717. this.onEvent_(
  718. new shaka.util.FakeEvent(shaka.ads.AdManager.AD_STOPPED));
  719. break;
  720. case shaka.ads.MediaTailorAdManager.BREAKSTART_:
  721. this.adContainer_.setAttribute('ad-active', 'true');
  722. break;
  723. case shaka.ads.MediaTailorAdManager.BREAKEND_:
  724. this.adContainer_.removeAttribute('ad-active');
  725. break;
  726. case shaka.ads.MediaTailorAdManager.SKIP_STATE_CHANGED_:
  727. this.onEvent_(
  728. new shaka.util.FakeEvent(
  729. shaka.ads.AdManager.AD_SKIP_STATE_CHANGED));
  730. break;
  731. }
  732. }
  733. };
  734. /**
  735. * @const {string}
  736. * @private
  737. */
  738. shaka.ads.MediaTailorAdManager.IMPRESSION_ = 'impression';
  739. /**
  740. * @const {string}
  741. * @private
  742. */
  743. shaka.ads.MediaTailorAdManager.START_ = 'start';
  744. /**
  745. * @const {string}
  746. * @private
  747. */
  748. shaka.ads.MediaTailorAdManager.MUTE_ = 'mute';
  749. /**
  750. * @const {string}
  751. * @private
  752. */
  753. shaka.ads.MediaTailorAdManager.UNMUTE_ = 'unmute';
  754. /**
  755. * @const {string}
  756. * @private
  757. */
  758. shaka.ads.MediaTailorAdManager.RESUME_ = 'resume';
  759. /**
  760. * @const {string}
  761. * @private
  762. */
  763. shaka.ads.MediaTailorAdManager.PAUSE_ = 'pause';
  764. /**
  765. * @const {string}
  766. * @private
  767. */
  768. shaka.ads.MediaTailorAdManager.FIRSTQUARTILE_ = 'firstQuartile';
  769. /**
  770. * @const {string}
  771. * @private
  772. */
  773. shaka.ads.MediaTailorAdManager.MIDPOINT_ = 'midpoint';
  774. /**
  775. * @const {string}
  776. * @private
  777. */
  778. shaka.ads.MediaTailorAdManager.THIRDQUARTILE_ = 'thirdQuartile';
  779. /**
  780. * @const {string}
  781. * @private
  782. */
  783. shaka.ads.MediaTailorAdManager.COMPLETE_ = 'complete';
  784. /**
  785. * @const {string}
  786. * @private
  787. */
  788. shaka.ads.MediaTailorAdManager.SKIPPED_ = 'skip';
  789. /**
  790. * @const {string}
  791. * @private
  792. */
  793. shaka.ads.MediaTailorAdManager.BREAKSTART_ = 'breakStart';
  794. /**
  795. * @const {string}
  796. * @private
  797. */
  798. shaka.ads.MediaTailorAdManager.BREAKEND_ = 'breakEnd';
  799. /**
  800. * @const {string}
  801. * @private
  802. */
  803. shaka.ads.MediaTailorAdManager.SKIP_STATE_CHANGED_ = 'skipStateChanged';