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