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