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