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