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