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