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