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