Source: lib/ads/client_side_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.ClientSideAdManager');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.Player');
  9. goog.require('shaka.ads.ClientSideAd');
  10. goog.require('shaka.ads.Utils');
  11. goog.require('shaka.log');
  12. goog.require('shaka.util.Dom');
  13. goog.require('shaka.util.EventManager');
  14. goog.require('shaka.util.FakeEvent');
  15. goog.require('shaka.util.IReleasable');
  16. /**
  17. * A class responsible for client-side ad interactions.
  18. * @implements {shaka.util.IReleasable}
  19. */
  20. shaka.ads.ClientSideAdManager = class {
  21. /**
  22. * @param {HTMLElement} adContainer
  23. * @param {HTMLMediaElement} video
  24. * @param {string} locale
  25. * @param {?google.ima.AdsRenderingSettings} adsRenderingSettings
  26. * @param {function(!shaka.util.FakeEvent)} onEvent
  27. */
  28. constructor(adContainer, video, locale, adsRenderingSettings, onEvent) {
  29. /** @private {HTMLElement} */
  30. this.adContainer_ = adContainer;
  31. /** @private {HTMLMediaElement} */
  32. this.video_ = video;
  33. /** @private {boolean} */
  34. this.videoPlayed_ = false;
  35. /** @private {?shaka.extern.AdsConfiguration} */
  36. this.config_ = null;
  37. /** @private {ResizeObserver} */
  38. this.resizeObserver_ = null;
  39. /** @private {number} */
  40. this.requestAdsStartTime_ = NaN;
  41. /** @private {function(!shaka.util.FakeEvent)} */
  42. this.onEvent_ = onEvent;
  43. /** @private {shaka.ads.ClientSideAd} */
  44. this.ad_ = null;
  45. /** @private {shaka.util.EventManager} */
  46. this.eventManager_ = new shaka.util.EventManager();
  47. google.ima.settings.setLocale(locale);
  48. google.ima.settings.setDisableCustomPlaybackForIOS10Plus(true);
  49. /** @private {!google.ima.AdDisplayContainer} */
  50. this.adDisplayContainer_ = new google.ima.AdDisplayContainer(
  51. this.adContainer_,
  52. this.video_);
  53. // TODO: IMA: Must be done as the result of a user action on mobile
  54. this.adDisplayContainer_.initialize();
  55. // IMA: This instance should be re-used for the entire lifecycle of
  56. // the page.
  57. this.adsLoader_ = new google.ima.AdsLoader(this.adDisplayContainer_);
  58. this.adsLoader_.getSettings().setPlayerType('shaka-player');
  59. this.adsLoader_.getSettings().setPlayerVersion(shaka.Player.version);
  60. /** @private {google.ima.AdsManager} */
  61. this.imaAdsManager_ = null;
  62. /** @private {!google.ima.AdsRenderingSettings} */
  63. this.adsRenderingSettings_ =
  64. adsRenderingSettings || new google.ima.AdsRenderingSettings();
  65. this.eventManager_.listen(this.adsLoader_,
  66. google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, (e) => {
  67. this.onAdsManagerLoaded_(
  68. /** @type {!google.ima.AdsManagerLoadedEvent} */ (e));
  69. });
  70. this.eventManager_.listen(this.adsLoader_,
  71. google.ima.AdErrorEvent.Type.AD_ERROR, (e) => {
  72. this.onAdError_( /** @type {!google.ima.AdErrorEvent} */ (e));
  73. });
  74. // Notify the SDK when the video has ended, so it can play post-roll ads.
  75. this.eventManager_.listen(this.video_, 'ended', () => {
  76. this.adsLoader_.contentComplete();
  77. });
  78. this.eventManager_.listenOnce(this.video_, 'play', () => {
  79. this.videoPlayed_ = true;
  80. });
  81. }
  82. /**
  83. * Called by the AdManager to provide an updated configuration any time it
  84. * changes.
  85. *
  86. * @param {shaka.extern.AdsConfiguration} config
  87. */
  88. configure(config) {
  89. this.config_ = config;
  90. }
  91. /**
  92. * @param {!google.ima.AdsRequest} imaRequest
  93. */
  94. requestAds(imaRequest) {
  95. goog.asserts.assert(
  96. imaRequest.adTagUrl || imaRequest.adsResponse,
  97. 'The ad tag needs to be set up before requesting ads, ' +
  98. 'or adsResponse must be filled.');
  99. // Destroy the current AdsManager, in case the tag you requested previously
  100. // contains post-rolls (don't play those now).
  101. if (this.imaAdsManager_) {
  102. this.imaAdsManager_.destroy();
  103. }
  104. // Your AdsLoader will be set up on page-load. You should re-use the same
  105. // AdsLoader for every request.
  106. if (this.adsLoader_) {
  107. // Reset the IMA SDK.
  108. this.adsLoader_.contentComplete();
  109. }
  110. this.requestAdsStartTime_ = Date.now() / 1000;
  111. this.adsLoader_.requestAds(imaRequest);
  112. }
  113. /**
  114. * @param {!google.ima.AdsRenderingSettings} adsRenderingSettings
  115. */
  116. updateAdsRenderingSettings(adsRenderingSettings) {
  117. this.adsRenderingSettings_ = adsRenderingSettings;
  118. if (this.imaAdsManager_) {
  119. this.imaAdsManager_.updateAdsRenderingSettings(
  120. this.adsRenderingSettings_);
  121. }
  122. }
  123. /**
  124. * Stop all currently playing ads.
  125. */
  126. stop() {
  127. // this.imaAdsManager_ might not be set yet... if, for example, an ad
  128. // blocker prevented the ads from ever loading.
  129. if (this.imaAdsManager_) {
  130. this.imaAdsManager_.stop();
  131. }
  132. if (this.adContainer_) {
  133. shaka.util.Dom.removeAllChildren(this.adContainer_);
  134. }
  135. }
  136. /** @override */
  137. release() {
  138. this.stop();
  139. if (this.resizeObserver_) {
  140. this.resizeObserver_.disconnect();
  141. }
  142. if (this.eventManager_) {
  143. this.eventManager_.release();
  144. }
  145. if (this.imaAdsManager_) {
  146. this.imaAdsManager_.destroy();
  147. }
  148. this.adsLoader_.destroy();
  149. this.adDisplayContainer_.destroy();
  150. }
  151. /**
  152. * @param {!google.ima.AdErrorEvent} e
  153. * @private
  154. */
  155. onAdError_(e) {
  156. shaka.log.warning(
  157. 'There was an ad error from the IMA SDK: ' + e.getError());
  158. shaka.log.warning('Resuming playback.');
  159. const data = (new Map()).set('originalEvent', e);
  160. this.onEvent_(new shaka.util.FakeEvent(shaka.ads.Utils.AD_ERROR, data));
  161. this.onAdComplete_(/* adEvent= */ null);
  162. // Remove ad breaks from the timeline
  163. this.onEvent_(
  164. new shaka.util.FakeEvent(shaka.ads.Utils.CUEPOINTS_CHANGED,
  165. (new Map()).set('cuepoints', [])));
  166. }
  167. /**
  168. * @param {!google.ima.AdsManagerLoadedEvent} e
  169. * @private
  170. */
  171. onAdsManagerLoaded_(e) {
  172. goog.asserts.assert(this.video_ != null, 'Video should not be null!');
  173. const now = Date.now() / 1000;
  174. const loadTime = now - this.requestAdsStartTime_;
  175. this.onEvent_(new shaka.util.FakeEvent(shaka.ads.Utils.ADS_LOADED,
  176. (new Map()).set('loadTime', loadTime)));
  177. if (!this.config_.customPlayheadTracker) {
  178. this.imaAdsManager_ = e.getAdsManager(this.video_,
  179. this.adsRenderingSettings_);
  180. } else {
  181. const videoPlayHead = {
  182. currentTime: this.video_.currentTime,
  183. };
  184. this.imaAdsManager_ = e.getAdsManager(videoPlayHead,
  185. this.adsRenderingSettings_);
  186. if (this.video_.muted) {
  187. this.imaAdsManager_.setVolume(0);
  188. } else {
  189. this.imaAdsManager_.setVolume(this.video_.volume);
  190. }
  191. this.eventManager_.listen(this.video_, 'timeupdate', () => {
  192. if (!this.video_.duration) {
  193. return;
  194. }
  195. videoPlayHead.currentTime = this.video_.currentTime;
  196. });
  197. this.eventManager_.listen(this.video_, 'volumechange', () => {
  198. if (!this.ad_) {
  199. return;
  200. }
  201. this.ad_.setVolume(this.video_.volume);
  202. if (this.video_.muted) {
  203. this.ad_.setMuted(true);
  204. }
  205. });
  206. }
  207. this.onEvent_(new shaka.util.FakeEvent(
  208. shaka.ads.Utils.IMA_AD_MANAGER_LOADED,
  209. (new Map()).set('imaAdManager', this.imaAdsManager_)));
  210. const cuePointStarts = this.imaAdsManager_.getCuePoints();
  211. if (cuePointStarts.length) {
  212. /** @type {!Array<!shaka.extern.AdCuePoint>} */
  213. const cuePoints = [];
  214. for (const start of cuePointStarts) {
  215. /** @type {shaka.extern.AdCuePoint} */
  216. const shakaCuePoint = {
  217. start: start,
  218. end: null,
  219. };
  220. cuePoints.push(shakaCuePoint);
  221. }
  222. this.onEvent_(new shaka.util.FakeEvent(
  223. shaka.ads.Utils.CUEPOINTS_CHANGED,
  224. (new Map()).set('cuepoints', cuePoints)));
  225. }
  226. this.addImaEventListeners_();
  227. try {
  228. this.imaAdsManager_.init(
  229. this.video_.offsetWidth, this.video_.offsetHeight);
  230. // Wait on the 'loadeddata' event rather than the 'loadedmetadata' event
  231. // because 'loadedmetadata' is sometimes called before the video resizes
  232. // on some platforms (e.g. Safari).
  233. this.eventManager_.listen(this.video_, 'loadeddata', () => {
  234. this.imaAdsManager_.resize(
  235. this.video_.offsetWidth, this.video_.offsetHeight);
  236. });
  237. if ('ResizeObserver' in window) {
  238. this.resizeObserver_ = new ResizeObserver(() => {
  239. this.imaAdsManager_.resize(
  240. this.video_.offsetWidth, this.video_.offsetHeight);
  241. });
  242. this.resizeObserver_.observe(this.video_);
  243. } else {
  244. this.eventManager_.listen(document, 'fullscreenchange', () => {
  245. this.imaAdsManager_.resize(
  246. this.video_.offsetWidth, this.video_.offsetHeight);
  247. });
  248. }
  249. // Single video and overlay ads will start at this time
  250. // TODO (ismena): Need a better understanding of what this does.
  251. // The docs say it's called to 'start playing the ads,' but I haven't
  252. // seen the ads actually play until requestAds() is called.
  253. // Note: We listen for a play event to avoid autoplay issues that might
  254. // crash IMA.
  255. if (this.videoPlayed_ || this.config_.skipPlayDetection) {
  256. this.imaAdsManager_.start();
  257. } else {
  258. this.eventManager_.listenOnce(this.video_, 'play', () => {
  259. this.videoPlayed_ = true;
  260. this.imaAdsManager_.start();
  261. });
  262. }
  263. } catch (adError) {
  264. // If there was a problem with the VAST response,
  265. // we we won't be getting an ad. Hide ad UI if we showed it already
  266. // and get back to the presentation.
  267. this.onAdComplete_(/* adEvent= */ null);
  268. }
  269. }
  270. /**
  271. * @private
  272. */
  273. addImaEventListeners_() {
  274. /**
  275. * @param {!Event} e
  276. * @param {string} type
  277. */
  278. const convertEventAndSend = (e, type) => {
  279. const data = (new Map()).set('originalEvent', e);
  280. this.onEvent_(new shaka.util.FakeEvent(type, data));
  281. };
  282. this.eventManager_.listen(this.imaAdsManager_,
  283. google.ima.AdErrorEvent.Type.AD_ERROR, (error) => {
  284. this.onAdError_(/** @type {!google.ima.AdErrorEvent} */ (error));
  285. });
  286. this.eventManager_.listen(this.imaAdsManager_,
  287. google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED, (e) => {
  288. this.onAdStart_(/** @type {!google.ima.AdEvent} */ (e));
  289. });
  290. this.eventManager_.listen(this.imaAdsManager_,
  291. google.ima.AdEvent.Type.STARTED, (e) => {
  292. this.onAdStart_(/** @type {!google.ima.AdEvent} */ (e));
  293. });
  294. this.eventManager_.listen(this.imaAdsManager_,
  295. google.ima.AdEvent.Type.FIRST_QUARTILE, (e) => {
  296. convertEventAndSend(e, shaka.ads.Utils.AD_FIRST_QUARTILE);
  297. });
  298. this.eventManager_.listen(this.imaAdsManager_,
  299. google.ima.AdEvent.Type.MIDPOINT, (e) => {
  300. convertEventAndSend(e, shaka.ads.Utils.AD_MIDPOINT);
  301. });
  302. this.eventManager_.listen(this.imaAdsManager_,
  303. google.ima.AdEvent.Type.THIRD_QUARTILE, (e) => {
  304. convertEventAndSend(e, shaka.ads.Utils.AD_THIRD_QUARTILE);
  305. });
  306. this.eventManager_.listen(this.imaAdsManager_,
  307. google.ima.AdEvent.Type.COMPLETE, (e) => {
  308. convertEventAndSend(e, shaka.ads.Utils.AD_COMPLETE);
  309. });
  310. this.eventManager_.listen(this.imaAdsManager_,
  311. google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED, (e) => {
  312. this.onAdComplete_(/** @type {!google.ima.AdEvent} */ (e));
  313. });
  314. this.eventManager_.listen(this.imaAdsManager_,
  315. google.ima.AdEvent.Type.ALL_ADS_COMPLETED, (e) => {
  316. this.onAdComplete_(/** @type {!google.ima.AdEvent} */ (e));
  317. });
  318. this.eventManager_.listen(this.imaAdsManager_,
  319. google.ima.AdEvent.Type.SKIPPED, (e) => {
  320. convertEventAndSend(e, shaka.ads.Utils.AD_SKIPPED);
  321. });
  322. this.eventManager_.listen(this.imaAdsManager_,
  323. google.ima.AdEvent.Type.VOLUME_CHANGED, (e) => {
  324. convertEventAndSend(e, shaka.ads.Utils.AD_VOLUME_CHANGED);
  325. });
  326. this.eventManager_.listen(this.imaAdsManager_,
  327. google.ima.AdEvent.Type.VOLUME_MUTED, (e) => {
  328. convertEventAndSend(e, shaka.ads.Utils.AD_MUTED);
  329. });
  330. this.eventManager_.listen(this.imaAdsManager_,
  331. google.ima.AdEvent.Type.PAUSED, (e) => {
  332. if (this.ad_) {
  333. this.ad_.setPaused(true);
  334. convertEventAndSend(e, shaka.ads.Utils.AD_PAUSED);
  335. }
  336. });
  337. this.eventManager_.listen(this.imaAdsManager_,
  338. google.ima.AdEvent.Type.RESUMED, (e) => {
  339. if (this.ad_) {
  340. this.ad_.setPaused(false);
  341. convertEventAndSend(e, shaka.ads.Utils.AD_RESUMED);
  342. }
  343. });
  344. this.eventManager_.listen(this.imaAdsManager_,
  345. google.ima.AdEvent.Type.SKIPPABLE_STATE_CHANGED, (e) => {
  346. if (this.ad_) {
  347. convertEventAndSend(e, shaka.ads.Utils.AD_SKIP_STATE_CHANGED);
  348. }
  349. });
  350. this.eventManager_.listen(this.imaAdsManager_,
  351. google.ima.AdEvent.Type.CLICK, (e) => {
  352. convertEventAndSend(e, shaka.ads.Utils.AD_CLICKED);
  353. });
  354. this.eventManager_.listen(this.imaAdsManager_,
  355. google.ima.AdEvent.Type.AD_PROGRESS, (e) => {
  356. convertEventAndSend(e, shaka.ads.Utils.AD_PROGRESS);
  357. });
  358. this.eventManager_.listen(this.imaAdsManager_,
  359. google.ima.AdEvent.Type.AD_BUFFERING, (e) => {
  360. convertEventAndSend(e, shaka.ads.Utils.AD_BUFFERING);
  361. });
  362. this.eventManager_.listen(this.imaAdsManager_,
  363. google.ima.AdEvent.Type.IMPRESSION, (e) => {
  364. convertEventAndSend(e, shaka.ads.Utils.AD_IMPRESSION);
  365. });
  366. this.eventManager_.listen(this.imaAdsManager_,
  367. google.ima.AdEvent.Type.DURATION_CHANGE, (e) => {
  368. convertEventAndSend(e, shaka.ads.Utils.AD_DURATION_CHANGED);
  369. });
  370. this.eventManager_.listen(this.imaAdsManager_,
  371. google.ima.AdEvent.Type.USER_CLOSE, (e) => {
  372. convertEventAndSend(e, shaka.ads.Utils.AD_CLOSED);
  373. });
  374. this.eventManager_.listen(this.imaAdsManager_,
  375. google.ima.AdEvent.Type.LOADED, (e) => {
  376. convertEventAndSend(e, shaka.ads.Utils.AD_LOADED);
  377. });
  378. this.eventManager_.listen(this.imaAdsManager_,
  379. google.ima.AdEvent.Type.ALL_ADS_COMPLETED, (e) => {
  380. convertEventAndSend(e, shaka.ads.Utils.ALL_ADS_COMPLETED);
  381. });
  382. this.eventManager_.listen(this.imaAdsManager_,
  383. google.ima.AdEvent.Type.LINEAR_CHANGED, (e) => {
  384. convertEventAndSend(e, shaka.ads.Utils.AD_LINEAR_CHANGED);
  385. });
  386. this.eventManager_.listen(this.imaAdsManager_,
  387. google.ima.AdEvent.Type.AD_METADATA, (e) => {
  388. convertEventAndSend(e, shaka.ads.Utils.AD_METADATA);
  389. });
  390. this.eventManager_.listen(this.imaAdsManager_,
  391. google.ima.AdEvent.Type.LOG, (e) => {
  392. convertEventAndSend(e, shaka.ads.Utils.AD_RECOVERABLE_ERROR);
  393. });
  394. this.eventManager_.listen(this.imaAdsManager_,
  395. google.ima.AdEvent.Type.AD_BREAK_READY, (e) => {
  396. convertEventAndSend(e, shaka.ads.Utils.AD_BREAK_READY);
  397. });
  398. this.eventManager_.listen(this.imaAdsManager_,
  399. google.ima.AdEvent.Type.INTERACTION, (e) => {
  400. convertEventAndSend(e, shaka.ads.Utils.AD_INTERACTION);
  401. });
  402. }
  403. /**
  404. * @param {!google.ima.AdEvent} e
  405. * @private
  406. */
  407. onAdStart_(e) {
  408. goog.asserts.assert(this.imaAdsManager_,
  409. 'Should have an ads manager at this point!');
  410. const imaAd = e.getAd();
  411. if (!imaAd) {
  412. // Sometimes the IMA SDK will fire a CONTENT_PAUSE_REQUESTED or STARTED
  413. // event with no associated ad object.
  414. // We can't really play an ad in that situation, so just ignore the event.
  415. shaka.log.alwaysWarn(
  416. 'The IMA SDK fired a ' + e.type + ' event with no associated ad. ' +
  417. 'Unable to play ad!');
  418. return;
  419. }
  420. this.ad_ = new shaka.ads.ClientSideAd(imaAd,
  421. this.imaAdsManager_, this.video_);
  422. if (e.type == google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED &&
  423. !this.config_.supportsMultipleMediaElements ) {
  424. this.onEvent_(new shaka.util.FakeEvent(
  425. shaka.ads.Utils.AD_CONTENT_PAUSE_REQUESTED));
  426. }
  427. const data = new Map()
  428. .set('ad', this.ad_)
  429. .set('sdkAdObject', imaAd)
  430. .set('originalEvent', e);
  431. this.onEvent_(new shaka.util.FakeEvent(
  432. shaka.ads.Utils.AD_STARTED, data));
  433. if (this.ad_.isLinear()) {
  434. this.adContainer_.setAttribute('ad-active', 'true');
  435. if (!this.config_.customPlayheadTracker) {
  436. this.video_.pause();
  437. }
  438. if (this.video_.muted) {
  439. this.ad_.setInitialMuted(this.video_.volume);
  440. } else {
  441. this.ad_.setVolume(this.video_.volume);
  442. }
  443. }
  444. }
  445. /**
  446. * @param {?google.ima.AdEvent} e
  447. * @private
  448. */
  449. onAdComplete_(e) {
  450. if (e && e.type == google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED &&
  451. !this.config_.supportsMultipleMediaElements) {
  452. this.onEvent_(new shaka.util.FakeEvent(
  453. shaka.ads.Utils.AD_CONTENT_RESUME_REQUESTED));
  454. }
  455. this.onEvent_(new shaka.util.FakeEvent(shaka.ads.Utils.AD_STOPPED,
  456. (new Map()).set('originalEvent', e)));
  457. if (this.ad_ && this.ad_.isLinear()) {
  458. this.adContainer_.removeAttribute('ad-active');
  459. if (!this.config_.customPlayheadTracker && !this.video_.ended) {
  460. this.video_.play();
  461. }
  462. }
  463. }
  464. };