mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-15 16:06:41 +03:00
8d1b5e6b07
Closes #3832
436 lines
14 KiB
JavaScript
436 lines
14 KiB
JavaScript
/*! @license
|
|
* Shaka Player
|
|
* Copyright 2016 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
/**
|
|
* @fileoverview
|
|
* @suppress {missingRequire} TODO(b/152540451): this shouldn't be needed
|
|
*/
|
|
|
|
goog.provide('shaka.ads.ClientSideAdManager');
|
|
|
|
goog.require('goog.asserts');
|
|
goog.require('shaka.ads.ClientSideAd');
|
|
goog.require('shaka.util.IReleasable');
|
|
|
|
|
|
/**
|
|
* A class responsible for client-side ad interactions.
|
|
* @implements {shaka.util.IReleasable}
|
|
*/
|
|
shaka.ads.ClientSideAdManager = class {
|
|
/**
|
|
* @param {HTMLElement} adContainer
|
|
* @param {HTMLMediaElement} video
|
|
* @param {string} locale
|
|
* @param {function(!shaka.util.FakeEvent)} onEvent
|
|
*/
|
|
constructor(adContainer, video, locale, onEvent) {
|
|
/** @private {HTMLElement} */
|
|
this.adContainer_ = adContainer;
|
|
|
|
/** @private {HTMLMediaElement} */
|
|
this.video_ = video;
|
|
|
|
/** @private {ResizeObserver} */
|
|
this.resizeObserver_ = null;
|
|
|
|
/** @private {number} */
|
|
this.requestAdsStartTime_ = NaN;
|
|
|
|
/** @private {function(!shaka.util.FakeEvent)} */
|
|
this.onEvent_ = onEvent;
|
|
|
|
/** @private {shaka.ads.ClientSideAd} */
|
|
this.ad_ = null;
|
|
|
|
/** @private {shaka.util.EventManager} */
|
|
this.eventManager_ = new shaka.util.EventManager();
|
|
|
|
google.ima.settings.setLocale(locale);
|
|
|
|
const adDisplayContainer = new google.ima.AdDisplayContainer(
|
|
this.adContainer_,
|
|
this.video_);
|
|
|
|
// TODO: IMA: Must be done as the result of a user action on mobile
|
|
adDisplayContainer.initialize();
|
|
|
|
// IMA: This instance should be re-used for the entire lifecycle of
|
|
// the page.
|
|
this.adsLoader_ = new google.ima.AdsLoader(adDisplayContainer);
|
|
|
|
this.adsLoader_.getSettings().setPlayerType('shaka-player');
|
|
this.adsLoader_.getSettings().setPlayerVersion(shaka.Player.version);
|
|
|
|
/** @private {google.ima.AdsManager} */
|
|
this.imaAdsManager_ = null;
|
|
|
|
this.eventManager_.listenOnce(this.adsLoader_,
|
|
google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, (e) => {
|
|
this.onAdsManagerLoaded_(
|
|
/** @type {!google.ima.AdsManagerLoadedEvent} */ (e));
|
|
});
|
|
|
|
this.eventManager_.listen(this.adsLoader_,
|
|
google.ima.AdErrorEvent.Type.AD_ERROR, (e) => {
|
|
this.onAdError_( /** @type {!google.ima.AdErrorEvent} */ (e));
|
|
});
|
|
|
|
// Notify the SDK when the video has ended, so it can play post-roll ads.
|
|
this.video_.onended = () => {
|
|
this.adsLoader_.contentComplete();
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {!google.ima.AdsRequest} imaRequest
|
|
*/
|
|
requestAds(imaRequest) {
|
|
goog.asserts.assert(
|
|
imaRequest.adTagUrl || imaRequest.adsResponse,
|
|
'The ad tag needs to be set up before requesting ads, ' +
|
|
'or adsResponse must be filled.');
|
|
this.requestAdsStartTime_ = Date.now() / 1000;
|
|
this.adsLoader_.requestAds(imaRequest);
|
|
}
|
|
|
|
/**
|
|
* Stop all currently playing ads.
|
|
*/
|
|
stop() {
|
|
// this.imaAdsManager_ might not be set yet... if, for example, an ad
|
|
// blocker prevented the ads from ever loading.
|
|
if (this.imaAdsManager_) {
|
|
this.imaAdsManager_.stop();
|
|
}
|
|
if (this.adContainer_) {
|
|
shaka.util.Dom.removeAllChildren(this.adContainer_);
|
|
}
|
|
}
|
|
|
|
/** @override */
|
|
release() {
|
|
this.stop();
|
|
if (this.resizeObserver_) {
|
|
this.resizeObserver_.disconnect();
|
|
}
|
|
if (this.eventManager_) {
|
|
this.eventManager_.release();
|
|
}
|
|
if (this.imaAdsManager_) {
|
|
this.imaAdsManager_.destroy();
|
|
}
|
|
this.adsLoader_.destroy();
|
|
}
|
|
|
|
/**
|
|
* @param {!google.ima.AdErrorEvent} e
|
|
* @private
|
|
*/
|
|
onAdError_(e) {
|
|
shaka.log.warning(
|
|
'There was an ad error from the IMA SDK: ' + e.getError());
|
|
shaka.log.warning('Resuming playback.');
|
|
this.onAdComplete_(/* adEvent= */ null);
|
|
// Remove ad breaks from the timeline
|
|
this.onEvent_(
|
|
new shaka.util.FakeEvent(shaka.ads.AdManager.CUEPOINTS_CHANGED,
|
|
(new Map()).set('cuepoints', [])));
|
|
}
|
|
|
|
|
|
/**
|
|
* @param {!google.ima.AdsManagerLoadedEvent} e
|
|
* @private
|
|
*/
|
|
onAdsManagerLoaded_(e) {
|
|
goog.asserts.assert(this.video_ != null, 'Video should not be null!');
|
|
|
|
const now = Date.now() / 1000;
|
|
const loadTime = now - this.requestAdsStartTime_;
|
|
this.onEvent_(new shaka.util.FakeEvent(shaka.ads.AdManager.ADS_LOADED,
|
|
(new Map()).set('loadTime', loadTime)));
|
|
|
|
this.imaAdsManager_ = e.getAdsManager(this.video_);
|
|
|
|
this.onEvent_(new shaka.util.FakeEvent(
|
|
shaka.ads.AdManager.IMA_AD_MANAGER_LOADED,
|
|
(new Map()).set('imaAdManager', this.imaAdsManager_)));
|
|
|
|
const cuePointStarts = this.imaAdsManager_.getCuePoints();
|
|
if (cuePointStarts.length) {
|
|
/** @type {!Array.<!shaka.extern.AdCuePoint>} */
|
|
const cuePoints = [];
|
|
for (const start of cuePointStarts) {
|
|
/** @type {shaka.extern.AdCuePoint} */
|
|
const shakaCuePoint = {
|
|
start: start,
|
|
end: null,
|
|
};
|
|
cuePoints.push(shakaCuePoint);
|
|
}
|
|
|
|
this.onEvent_(new shaka.util.FakeEvent(
|
|
shaka.ads.AdManager.CUEPOINTS_CHANGED,
|
|
(new Map()).set('cuepoints', cuePoints)));
|
|
}
|
|
|
|
this.addImaEventListeners_();
|
|
|
|
try {
|
|
const viewMode = this.isFullScreenEnabled_() ?
|
|
google.ima.ViewMode.FULLSCREEN : google.ima.ViewMode.NORMAL;
|
|
|
|
this.imaAdsManager_.init(this.video_.offsetWidth,
|
|
this.video_.offsetHeight, viewMode);
|
|
|
|
// Wait on the 'loadeddata' event rather than the 'loadedmetadata' event
|
|
// because 'loadedmetadata' is sometimes called before the video resizes
|
|
// on some platforms (e.g. Safari).
|
|
this.eventManager_.listen(this.video_, 'loadeddata', () => {
|
|
const viewMode = this.isFullScreenEnabled_() ?
|
|
google.ima.ViewMode.FULLSCREEN : google.ima.ViewMode.NORMAL;
|
|
this.imaAdsManager_.resize(this.video_.offsetWidth,
|
|
this.video_.offsetHeight, viewMode);
|
|
});
|
|
|
|
if ('ResizeObserver' in window) {
|
|
this.resizeObserver_ = new ResizeObserver(() => {
|
|
const viewMode = this.isFullScreenEnabled_() ?
|
|
google.ima.ViewMode.FULLSCREEN : google.ima.ViewMode.NORMAL;
|
|
this.imaAdsManager_.resize(this.video_.offsetWidth,
|
|
this.video_.offsetHeight, viewMode);
|
|
});
|
|
this.resizeObserver_.observe(this.video_);
|
|
}
|
|
|
|
// Single video and overlay ads will start at this time
|
|
// TODO (ismena): Need a better inderstanding of what this does.
|
|
// The docs say it's called to 'start playing the ads,' but I haven't
|
|
// seen the ads actually play until requestAds() is called.
|
|
this.imaAdsManager_.start();
|
|
} catch (adError) {
|
|
// If there was a problem with the VAST response,
|
|
// we we won't be getting an ad. Hide ad UI if we showed it already
|
|
// and get back to the presentation.
|
|
this.onAdComplete_(/* adEvent= */ null);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @return {boolean}
|
|
* @private
|
|
*/
|
|
isFullScreenEnabled_() {
|
|
if (document.fullscreenEnabled) {
|
|
return !!document.fullscreenElement;
|
|
} else {
|
|
const video = /** @type {HTMLVideoElement} */(this.video_);
|
|
if (video.webkitSupportsFullscreen) {
|
|
return video.webkitDisplayingFullscreen;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
addImaEventListeners_() {
|
|
/**
|
|
* @param {!Event} e
|
|
* @param {string} type
|
|
*/
|
|
const convertEventAndSend = (e, type) => {
|
|
const data = (new Map()).set('originalEvent', e);
|
|
this.onEvent_(new shaka.util.FakeEvent(type, data));
|
|
};
|
|
|
|
this.eventManager_.listen(this.imaAdsManager_,
|
|
google.ima.AdErrorEvent.Type.AD_ERROR, (error) => {
|
|
this.onAdError_(/** @type {!google.ima.AdErrorEvent} */ (error));
|
|
});
|
|
|
|
this.eventManager_.listen(this.imaAdsManager_,
|
|
google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED, (e) => {
|
|
this.onAdStart_(/** @type {!google.ima.AdEvent} */ (e));
|
|
});
|
|
|
|
this.eventManager_.listen(this.imaAdsManager_,
|
|
google.ima.AdEvent.Type.STARTED, (e) => {
|
|
this.onAdStart_(/** @type {!google.ima.AdEvent} */ (e));
|
|
});
|
|
|
|
this.eventManager_.listen(this.imaAdsManager_,
|
|
google.ima.AdEvent.Type.FIRST_QUARTILE, (e) => {
|
|
convertEventAndSend(e, shaka.ads.AdManager.AD_FIRST_QUARTILE);
|
|
});
|
|
|
|
this.eventManager_.listen(this.imaAdsManager_,
|
|
google.ima.AdEvent.Type.MIDPOINT, (e) => {
|
|
convertEventAndSend(e, shaka.ads.AdManager.AD_MIDPOINT);
|
|
});
|
|
|
|
this.eventManager_.listen(this.imaAdsManager_,
|
|
google.ima.AdEvent.Type.THIRD_QUARTILE, (e) => {
|
|
convertEventAndSend(e, shaka.ads.AdManager.AD_THIRD_QUARTILE);
|
|
});
|
|
|
|
this.eventManager_.listen(this.imaAdsManager_,
|
|
google.ima.AdEvent.Type.COMPLETE, (e) => {
|
|
convertEventAndSend(e, shaka.ads.AdManager.AD_COMPLETE);
|
|
});
|
|
|
|
this.eventManager_.listen(this.imaAdsManager_,
|
|
google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED, (e) => {
|
|
this.onAdComplete_(/** @type {!google.ima.AdEvent} */ (e));
|
|
});
|
|
|
|
this.eventManager_.listen(this.imaAdsManager_,
|
|
google.ima.AdEvent.Type.ALL_ADS_COMPLETED, (e) => {
|
|
this.onAdComplete_(/** @type {!google.ima.AdEvent} */ (e));
|
|
});
|
|
|
|
this.eventManager_.listen(this.imaAdsManager_,
|
|
google.ima.AdEvent.Type.SKIPPED, (e) => {
|
|
convertEventAndSend(e, shaka.ads.AdManager.AD_SKIPPED);
|
|
});
|
|
|
|
this.eventManager_.listen(this.imaAdsManager_,
|
|
google.ima.AdEvent.Type.VOLUME_CHANGED, (e) => {
|
|
convertEventAndSend(e, shaka.ads.AdManager.AD_VOLUME_CHANGED);
|
|
});
|
|
|
|
this.eventManager_.listen(this.imaAdsManager_,
|
|
google.ima.AdEvent.Type.VOLUME_MUTED, (e) => {
|
|
convertEventAndSend(e, shaka.ads.AdManager.AD_MUTED);
|
|
});
|
|
|
|
this.eventManager_.listen(this.imaAdsManager_,
|
|
google.ima.AdEvent.Type.PAUSED, (e) => {
|
|
goog.asserts.assert(this.ad_ != null, 'Ad should not be null!');
|
|
this.ad_.setPaused(true);
|
|
convertEventAndSend(e, shaka.ads.AdManager.AD_PAUSED);
|
|
});
|
|
|
|
this.eventManager_.listen(this.imaAdsManager_,
|
|
google.ima.AdEvent.Type.RESUMED, (e) => {
|
|
goog.asserts.assert(this.ad_ != null, 'Ad should not be null!');
|
|
this.ad_.setPaused(false);
|
|
convertEventAndSend(e, shaka.ads.AdManager.AD_RESUMED);
|
|
});
|
|
|
|
this.eventManager_.listen(this.imaAdsManager_,
|
|
google.ima.AdEvent.Type.SKIPPABLE_STATE_CHANGED, (e) => {
|
|
goog.asserts.assert(this.ad_ != null, 'Ad should not be null!');
|
|
convertEventAndSend(e, shaka.ads.AdManager.AD_SKIP_STATE_CHANGED);
|
|
});
|
|
|
|
this.eventManager_.listen(this.imaAdsManager_,
|
|
google.ima.AdEvent.Type.CLICK, (e) => {
|
|
convertEventAndSend(e, shaka.ads.AdManager.AD_CLICKED);
|
|
});
|
|
|
|
this.eventManager_.listen(this.imaAdsManager_,
|
|
google.ima.AdEvent.Type.AD_PROGRESS, (e) => {
|
|
convertEventAndSend(e, shaka.ads.AdManager.AD_PROGRESS);
|
|
});
|
|
|
|
this.eventManager_.listen(this.imaAdsManager_,
|
|
google.ima.AdEvent.Type.AD_BUFFERING, (e) => {
|
|
convertEventAndSend(e, shaka.ads.AdManager.AD_BUFFERING);
|
|
});
|
|
|
|
this.eventManager_.listen(this.imaAdsManager_,
|
|
google.ima.AdEvent.Type.IMPRESSION, (e) => {
|
|
convertEventAndSend(e, shaka.ads.AdManager.AD_IMPRESSION);
|
|
});
|
|
|
|
this.eventManager_.listen(this.imaAdsManager_,
|
|
google.ima.AdEvent.Type.DURATION_CHANGE, (e) => {
|
|
convertEventAndSend(e, shaka.ads.AdManager.AD_DURATION_CHANGED);
|
|
});
|
|
|
|
this.eventManager_.listen(this.imaAdsManager_,
|
|
google.ima.AdEvent.Type.USER_CLOSE, (e) => {
|
|
convertEventAndSend(e, shaka.ads.AdManager.AD_CLOSED);
|
|
});
|
|
|
|
this.eventManager_.listen(this.imaAdsManager_,
|
|
google.ima.AdEvent.Type.LOADED, (e) => {
|
|
convertEventAndSend(e, shaka.ads.AdManager.AD_LOADED);
|
|
});
|
|
|
|
this.eventManager_.listen(this.imaAdsManager_,
|
|
google.ima.AdEvent.Type.ALL_ADS_COMPLETED, (e) => {
|
|
convertEventAndSend(e, shaka.ads.AdManager.ALL_ADS_COMPLETED);
|
|
});
|
|
|
|
this.eventManager_.listen(this.imaAdsManager_,
|
|
google.ima.AdEvent.Type.LINEAR_CHANGED, (e) => {
|
|
convertEventAndSend(e, shaka.ads.AdManager.AD_LINEAR_CHANGED);
|
|
});
|
|
|
|
this.eventManager_.listen(this.imaAdsManager_,
|
|
google.ima.AdEvent.Type.AD_METADATA, (e) => {
|
|
convertEventAndSend(e, shaka.ads.AdManager.AD_METADATA);
|
|
});
|
|
|
|
this.eventManager_.listen(this.imaAdsManager_,
|
|
google.ima.AdEvent.Type.LOG, (e) => {
|
|
convertEventAndSend(e, shaka.ads.AdManager.AD_RECOVERABLE_ERROR);
|
|
});
|
|
|
|
this.eventManager_.listen(this.imaAdsManager_,
|
|
google.ima.AdEvent.Type.AD_BREAK_READY, (e) => {
|
|
convertEventAndSend(e, shaka.ads.AdManager.AD_BREAK_READY);
|
|
});
|
|
|
|
this.eventManager_.listen(this.imaAdsManager_,
|
|
google.ima.AdEvent.Type.INTERACTION, (e) => {
|
|
convertEventAndSend(e, shaka.ads.AdManager.AD_INTERACTION);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {!google.ima.AdEvent} e
|
|
* @private
|
|
*/
|
|
onAdStart_(e) {
|
|
goog.asserts.assert(this.imaAdsManager_,
|
|
'Should have an ads manager at this point!');
|
|
|
|
const imaAd = e.getAd();
|
|
this.ad_ = new shaka.ads.ClientSideAd(imaAd,
|
|
this.imaAdsManager_, this.video_);
|
|
const data = new Map()
|
|
.set('ad', this.ad_)
|
|
.set('sdkAdObject', imaAd)
|
|
.set('originalEvent', e);
|
|
this.onEvent_(new shaka.util.FakeEvent(
|
|
shaka.ads.AdManager.AD_STARTED, data));
|
|
if (this.ad_.isLinear()) {
|
|
this.adContainer_.setAttribute('ad-active', 'true');
|
|
this.video_.pause();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {?google.ima.AdEvent} e
|
|
* @private
|
|
*/
|
|
onAdComplete_(e) {
|
|
this.onEvent_(new shaka.util.FakeEvent(shaka.ads.AdManager.AD_STOPPED,
|
|
(new Map()).set('originalEvent', e)));
|
|
if (this.ad_.isLinear()) {
|
|
this.adContainer_.removeAttribute('ad-active');
|
|
this.video_.play();
|
|
}
|
|
}
|
|
};
|