/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.ads.InterstitialAdManager');
goog.require('goog.asserts');
goog.require('shaka.Player');
goog.require('shaka.ads.InterstitialAd');
goog.require('shaka.ads.InterstitialStaticAd');
goog.require('shaka.ads.Utils');
goog.require('shaka.log');
goog.require('shaka.media.PreloadManager');
goog.require('shaka.net.NetworkingEngine');
goog.require('shaka.net.NetworkingUtils');
goog.require('shaka.util.Dom');
goog.require('shaka.util.Error');
goog.require('shaka.util.EventManager');
goog.require('shaka.util.FakeEvent');
goog.require('shaka.util.IReleasable');
goog.require('shaka.util.Platform');
goog.require('shaka.util.PublicPromise');
goog.require('shaka.util.StringUtils');
goog.require('shaka.util.Timer');
goog.require('shaka.util.TXml');
/**
* A class responsible for Interstitial ad interactions.
*
* @implements {shaka.util.IReleasable}
*/
shaka.ads.InterstitialAdManager = class {
/**
* @param {HTMLElement} adContainer
* @param {shaka.Player} basePlayer
* @param {HTMLMediaElement} baseVideo
* @param {function(!shaka.util.FakeEvent)} onEvent
*/
constructor(adContainer, basePlayer, baseVideo, onEvent) {
/** @private {?shaka.extern.AdsConfiguration} */
this.config_ = null;
/** @private {HTMLElement} */
this.adContainer_ = adContainer;
/** @private {shaka.Player} */
this.basePlayer_ = basePlayer;
/** @private {HTMLMediaElement} */
this.baseVideo_ = baseVideo;
/** @private {?HTMLMediaElement} */
this.adVideo_ = null;
/** @private {boolean} */
this.usingBaseVideo_ = true;
/** @private {HTMLMediaElement} */
this.video_ = this.baseVideo_;
/** @private {function(!shaka.util.FakeEvent)} */
this.onEvent_ = onEvent;
/** @private {!Set.<string>} */
this.interstitialIds_ = new Set();
/** @private {!Set.<shaka.extern.AdInterstitial>} */
this.interstitials_ = new Set();
/**
* @private {!Map.<shaka.extern.AdInterstitial,
* Promise<?shaka.media.PreloadManager>>}
*/
this.preloadManagerInterstitials_ = new Map();
/** @private {shaka.Player} */
this.player_ = new shaka.Player();
this.updatePlayerConfig_();
/** @private {shaka.util.EventManager} */
this.eventManager_ = new shaka.util.EventManager();
/** @private {shaka.util.EventManager} */
this.adEventManager_ = new shaka.util.EventManager();
/** @private {boolean} */
this.playingAd_ = false;
/** @private {?number} */
this.lastTime_ = null;
/** @private {?shaka.extern.AdInterstitial} */
this.lastPlayedAd_ = null;
/** @private {?shaka.util.Timer} */
this.playoutLimitTimer_ = null;
this.eventManager_.listen(this.baseVideo_, 'timeupdate', () => {
if (this.playingAd_ || this.lastTime_ ||
this.basePlayer_.isRemotePlayback()) {
return;
}
this.lastTime_ = this.baseVideo_.currentTime;
const currentInterstitial = this.getCurrentInterstitial_(
/* needPreRoll= */ true);
if (currentInterstitial) {
this.setupAd_(currentInterstitial, /* sequenceLength= */ 1,
/* adPosition= */ 1, /* initialTime= */ Date.now());
}
});
const checkForInterstitials = () => {
if (this.playingAd_ || !this.lastTime_ ||
this.basePlayer_.isRemotePlayback()) {
return;
}
this.lastTime_ = this.baseVideo_.currentTime;
// Remove last played add when the new time is before to the ad time.
if (this.lastPlayedAd_ &&
!this.lastPlayedAd_.pre && !this.lastPlayedAd_.post &&
this.lastTime_ < this.lastPlayedAd_.startTime) {
this.lastPlayedAd_ = null;
}
const currentInterstitial = this.getCurrentInterstitial_();
if (currentInterstitial) {
this.setupAd_(currentInterstitial, /* sequenceLength= */ 1,
/* adPosition= */ 1, /* initialTime= */ Date.now());
}
};
this.eventManager_.listen(this.baseVideo_, 'ended', () => {
checkForInterstitials();
});
/** @private {shaka.util.Timer} */
this.timeUpdateTimer_ = new shaka.util.Timer(checkForInterstitials);
if ('requestVideoFrameCallback' in this.baseVideo_ &&
!shaka.util.Platform.isSmartTV()) {
const baseVideo = /** @type {!HTMLVideoElement} */ (this.baseVideo_);
const videoFrameCallback = () => {
checkForInterstitials();
baseVideo.requestVideoFrameCallback(videoFrameCallback);
};
baseVideo.requestVideoFrameCallback(videoFrameCallback);
} else {
this.timeUpdateTimer_.tickEvery(/* seconds= */ 0.025);
}
/** @private {shaka.util.Timer} */
this.pollTimer_ = new shaka.util.Timer(async () => {
if (this.interstitials_.size && this.lastTime_ != null) {
const currentLoadMode = this.basePlayer_.getLoadMode();
if (currentLoadMode == shaka.Player.LoadMode.DESTROYED ||
currentLoadMode == shaka.Player.LoadMode.NOT_LOADED) {
return;
}
let cuepointsChanged = false;
const interstitials = Array.from(this.interstitials_);
const seekRange = this.basePlayer_.seekRange();
for (const interstitial of interstitials) {
if (interstitial == this.lastPlayedAd_) {
continue;
}
const comparisonTime = interstitial.endTime || interstitial.startTime;
if ((seekRange.start - comparisonTime) >= 1) {
if (this.preloadManagerInterstitials_.has(interstitial)) {
const preloadManager =
// eslint-disable-next-line no-await-in-loop
await this.preloadManagerInterstitials_.get(interstitial);
if (preloadManager) {
preloadManager.destroy();
}
this.preloadManagerInterstitials_.delete(interstitial);
}
const interstitialId = JSON.stringify(interstitial);
if (this.interstitialIds_.has(interstitialId)) {
this.interstitialIds_.delete(interstitialId);
}
this.interstitials_.delete(interstitial);
if (!interstitial.overlay) {
cuepointsChanged = true;
}
} else {
const difference = interstitial.startTime - this.lastTime_;
if (difference > 0 && difference <= 10) {
if (!this.preloadManagerInterstitials_.has(interstitial) &&
this.isPreloadAllowed_(interstitial)) {
this.preloadManagerInterstitials_.set(
interstitial, this.player_.preload(
interstitial.uri,
/* startTime= */ null,
interstitial.mimeType || undefined));
}
}
}
}
if (cuepointsChanged) {
this.cuepointsChanged_();
}
}
}).tickEvery(/* seconds= */ 1);
}
/**
* Called by the AdManager to provide an updated configuration any time it
* changes.
*
* @param {shaka.extern.AdsConfiguration} config
*/
configure(config) {
this.config_ = config;
this.determineIfUsingBaseVideo_();
}
/**
* @private
*/
determineIfUsingBaseVideo_() {
if (!this.adContainer_ || !this.config_ || this.playingAd_) {
return;
}
let supportsMultipleMediaElements =
this.config_.supportsMultipleMediaElements;
const video = /** @type {HTMLVideoElement} */(this.baseVideo_);
if (video.webkitSupportsFullscreen && video.webkitDisplayingFullscreen) {
supportsMultipleMediaElements = false;
}
if (this.usingBaseVideo_ != supportsMultipleMediaElements) {
return;
}
this.usingBaseVideo_ = !supportsMultipleMediaElements;
if (this.usingBaseVideo_) {
this.video_ = this.baseVideo_;
if (this.adVideo_) {
if (this.adVideo_.parentElement) {
this.adContainer_.removeChild(this.adVideo_);
}
this.adVideo_ = null;
}
} else {
if (!this.adVideo_) {
this.adVideo_ = this.createMediaElement_();
}
this.video_ = this.adVideo_;
}
}
/**
* Resets the Interstitial manager and removes any continuous polling.
*/
stop() {
if (this.adEventManager_) {
this.adEventManager_.removeAll();
}
this.interstitialIds_.clear();
this.interstitials_.clear();
this.player_.destroyAllPreloads();
this.preloadManagerInterstitials_.clear();
this.player_.detach();
this.playingAd_ = false;
this.lastTime_ = null;
this.lastPlayedAd_ = null;
this.usingBaseVideo_ = true;
this.video_ = this.baseVideo_;
this.adVideo_ = null;
if (this.adContainer_) {
shaka.util.Dom.removeAllChildren(this.adContainer_);
}
if (this.playoutLimitTimer_) {
this.playoutLimitTimer_.stop();
this.playoutLimitTimer_ = null;
}
}
/** @override */
release() {
this.stop();
if (this.eventManager_) {
this.eventManager_.release();
}
if (this.adEventManager_) {
this.adEventManager_.release();
}
if (this.timeUpdateTimer_) {
this.timeUpdateTimer_.stop();
this.timeUpdateTimer_ = null;
}
if (this.pollTimer_) {
this.pollTimer_.stop();
this.pollTimer_ = null;
}
this.player_.destroy();
}
/**
* @param {shaka.extern.HLSInterstitial} hlsInterstitial
*/
async addMetadata(hlsInterstitial) {
this.updatePlayerConfig_();
const adInterstitials = await this.getInterstitialsInfo_(hlsInterstitial);
if (adInterstitials.length) {
this.addInterstitials(adInterstitials);
} else {
shaka.log.alwaysWarn('Unsupported HLS interstitial', hlsInterstitial);
}
}
/**
* @param {shaka.extern.TimelineRegionInfo} region
*/
addRegion(region) {
let alternativeMPDUri;
for (const node of region.eventNode.children) {
if (node.tagName == 'AlternativeMPD') {
const uri = node.attributes['uri'];
if (uri) {
alternativeMPDUri = uri;
break;
}
}
}
if (!alternativeMPDUri) {
shaka.log.alwaysWarn('Unsupported MPD alternate', region);
return;
}
const isReplace =
region.schemeIdUri == 'urn:mpeg:dash:event:alternativeMPD:replace:2025';
const isInsert =
region.schemeIdUri == 'urn:mpeg:dash:event:alternativeMPD:insert:2025';
if (!isReplace && !isInsert) {
shaka.log.warning('Unsupported MPD alternate', region);
return;
}
/** @type {!shaka.extern.AdInterstitial} */
const interstitial = {
id: region.id,
startTime: region.startTime,
endTime: region.endTime,
uri: alternativeMPDUri,
mimeType: null,
isSkippable: false,
skipOffset: null,
skipFor: null,
canJump: true,
resumeOffset: isInsert ? 0 : null,
playoutLimit: null,
once: false,
pre: false,
post: false,
timelineRange: isReplace && !isInsert,
loop: false,
overlay: null,
};
this.addInterstitials([interstitial]);
}
/**
* @param {shaka.extern.TimelineRegionInfo} region
*/
addOverlayRegion(region) {
const TXml = shaka.util.TXml;
goog.asserts.assert(region.eventNode, 'Need a region eventNode');
const overlayEvent = TXml.findChild(region.eventNode, 'OverlayEvent');
const uri = overlayEvent.attributes['uri'];
const mimeType = overlayEvent.attributes['mimeType'];
const loop = overlayEvent.attributes['loop'] == 'true';
if (!uri || !mimeType) {
shaka.log.warning('Unsupported OverlayEvent', region);
return;
}
/** @type {!shaka.extern.AdInterstitialOverlay} */
let overlay = {
viewport: {
x: 1920,
y: 1080,
},
topLeft: {
x: 0,
y: 0,
},
size: {
x: 1920,
y: 1080,
},
};
const viewport = TXml.findChild(overlayEvent, 'Viewport');
const topLeft = TXml.findChild(overlayEvent, 'TopLeft');
const size = TXml.findChild(overlayEvent, 'Size');
if (viewport && topLeft && size) {
const viewportX = TXml.parseAttr(viewport, 'x', TXml.parseInt);
if (viewportX == null) {
shaka.log.warning('Unsupported OverlayEvent', region);
return;
}
const viewportY = TXml.parseAttr(viewport, 'y', TXml.parseInt);
if (viewportY == null) {
shaka.log.warning('Unsupported OverlayEvent', region);
return;
}
const topLeftX = TXml.parseAttr(topLeft, 'x', TXml.parseInt);
if (topLeftX == null) {
shaka.log.warning('Unsupported OverlayEvent', region);
return;
}
const topLeftY = TXml.parseAttr(topLeft, 'y', TXml.parseInt);
if (topLeftY == null) {
shaka.log.warning('Unsupported OverlayEvent', region);
return;
}
const sizeX = TXml.parseAttr(size, 'x', TXml.parseInt);
if (sizeX == null) {
shaka.log.warning('Unsupported OverlayEvent', region);
return;
}
const sizeY = TXml.parseAttr(size, 'y', TXml.parseInt);
if (sizeY == null) {
shaka.log.warning('Unsupported OverlayEvent', region);
return;
}
overlay = {
viewport: {
x: viewportX,
y: viewportY,
},
topLeft: {
x: topLeftX,
y: topLeftY,
},
size: {
x: sizeX,
y: sizeY,
},
};
}
/** @type {!shaka.extern.AdInterstitial} */
const interstitial = {
id: region.id,
startTime: region.startTime,
endTime: region.endTime,
uri,
mimeType,
isSkippable: false,
skipOffset: null,
skipFor: null,
canJump: true,
resumeOffset: null,
playoutLimit: null,
once: false,
pre: false,
post: false,
timelineRange: true,
loop,
overlay,
};
this.addInterstitials([interstitial]);
}
/**
* @param {string} url
* @return {!Promise}
*/
async addAdUrlInterstitial(url) {
const NetworkingEngine = shaka.net.NetworkingEngine;
const context = {
type: NetworkingEngine.AdvancedRequestType.INTERSTITIAL_AD_URL,
};
const responseData = await this.makeAdRequest_(url, context);
const data = shaka.util.TXml.parseXml(responseData, 'VAST,vmap:VMAP');
if (!data) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.ADS,
shaka.util.Error.Code.VAST_INVALID_XML);
}
let interstitials = [];
if (data.tagName == 'VAST') {
interstitials = shaka.ads.Utils.parseVastToInterstitials(
data, this.lastTime_);
} else if (data.tagName == 'vmap:VMAP') {
for (const ad of shaka.ads.Utils.parseVMAP(data)) {
// eslint-disable-next-line no-await-in-loop
const vastResponseData = await this.makeAdRequest_(ad.uri, context);
const vast = shaka.util.TXml.parseXml(vastResponseData, 'VAST');
if (!vast) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.ADS,
shaka.util.Error.Code.VAST_INVALID_XML);
}
interstitials.push(...shaka.ads.Utils.parseVastToInterstitials(
vast, ad.time));
}
}
this.addInterstitials(interstitials);
}
/**
* @param {!Array.<shaka.extern.AdInterstitial>} interstitials
*/
async addInterstitials(interstitials) {
let cuepointsChanged = false;
for (const interstitial of interstitials) {
if (!interstitial.uri) {
shaka.log.alwaysWarn('Missing URL in interstitial', interstitial);
continue;
}
if (!interstitial.mimeType) {
try {
const netEngine = this.player_.getNetworkingEngine();
goog.asserts.assert(netEngine, 'Need networking engine');
// eslint-disable-next-line no-await-in-loop
interstitial.mimeType = await shaka.net.NetworkingUtils.getMimeType(
interstitial.uri, netEngine,
this.basePlayer_.getConfiguration().streaming.retryParameters);
} catch (error) {}
}
const interstitialId = interstitial.id || JSON.stringify(interstitial);
if (this.interstitialIds_.has(interstitialId)) {
continue;
}
if (interstitial.loop && !interstitial.overlay) {
shaka.log.alwaysWarn('Loop is only supported in overlay interstitials',
interstitial);
}
if (!interstitial.overlay) {
cuepointsChanged = true;
}
this.interstitialIds_.add(interstitialId);
this.interstitials_.add(interstitial);
let shouldPreload = false;
if (interstitial.pre && this.lastTime_ == null) {
shouldPreload = true;
} else if (interstitial.startTime == 0 && !interstitial.canJump) {
shouldPreload = true;
} else if (this.lastTime_ != null) {
const difference = interstitial.startTime - this.lastTime_;
if (difference > 0 && difference <= 10) {
shouldPreload = true;
}
}
if (shouldPreload) {
if (!this.preloadManagerInterstitials_.has(interstitial) &&
this.isPreloadAllowed_(interstitial)) {
this.preloadManagerInterstitials_.set(
interstitial, this.player_.preload(
interstitial.uri,
/* startTime= */ null,
interstitial.mimeType || undefined));
}
}
}
if (cuepointsChanged) {
this.cuepointsChanged_();
}
}
/**
* @return {!HTMLMediaElement}
* @private
*/
createMediaElement_() {
const video = /** @type {!HTMLMediaElement} */(
document.createElement(this.baseVideo_.tagName));
video.autoplay = true;
video.style.position = 'absolute';
video.style.top = '0';
video.style.left = '0';
video.style.width = '100%';
video.style.height = '100%';
video.style.backgroundColor = 'rgb(0, 0, 0)';
video.style.display = 'none';
video.setAttribute('playsinline', '');
return video;
}
/**
* @param {boolean=} needPreRoll
* @param {?number=} numberToSkip
* @return {?shaka.extern.AdInterstitial}
* @private
*/
getCurrentInterstitial_(needPreRoll = false, numberToSkip = null) {
let skipped = 0;
let currentInterstitial = null;
if (this.interstitials_.size && this.lastTime_ != null) {
const isEnded = this.baseVideo_.ended;
const interstitials = Array.from(this.interstitials_).sort((a, b) => {
return b.startTime - a.startTime;
});
const roundDecimals = (number) => {
return Math.round(number * 1000) / 1000;
};
let interstitialsToCheck = interstitials;
if (needPreRoll) {
interstitialsToCheck = interstitials.filter((i) => i.pre);
} else if (isEnded) {
interstitialsToCheck = interstitials.filter((i) => i.post);
} else {
interstitialsToCheck = interstitials.filter((i) => !i.pre && !i.post);
}
for (const interstitial of interstitialsToCheck) {
let isValid = false;
if (needPreRoll) {
isValid = interstitial.pre;
} else if (isEnded) {
isValid = interstitial.post;
} else if (!interstitial.pre && !interstitial.post) {
const difference =
this.lastTime_ - roundDecimals(interstitial.startTime);
if (difference > 0 &&
(difference <= 1 || !interstitial.canJump)) {
if (numberToSkip == null && this.lastPlayedAd_ &&
!this.lastPlayedAd_.pre && !this.lastPlayedAd_.post &&
this.lastPlayedAd_.startTime >= interstitial.startTime) {
isValid = false;
} else {
isValid = true;
}
}
}
if (isValid && (!this.lastPlayedAd_ ||
interstitial.startTime >= this.lastPlayedAd_.startTime)) {
if (skipped == (numberToSkip || 0)) {
currentInterstitial = interstitial;
} else if (currentInterstitial && !interstitial.canJump) {
const currentStartTime =
roundDecimals(currentInterstitial.startTime);
const newStartTime =
roundDecimals(interstitial.startTime);
if (newStartTime - currentStartTime > 0.001) {
currentInterstitial = interstitial;
skipped = 0;
}
}
skipped++;
}
}
}
return currentInterstitial;
}
/**
* @param {shaka.extern.AdInterstitial} interstitial
* @param {number} sequenceLength
* @param {number} adPosition
* @param {number} initialTime the clock time the ad started at
* @param {number=} oncePlayed
* @private
*/
setupAd_(interstitial, sequenceLength, adPosition, initialTime,
oncePlayed = 0) {
shaka.log.info('Starting interstitial',
interstitial.startTime, 'at', this.lastTime_);
this.lastPlayedAd_ = interstitial;
this.determineIfUsingBaseVideo_();
goog.asserts.assert(this.video_, 'Must have video');
if (!this.video_.parentElement && this.adContainer_) {
this.adContainer_.appendChild(this.video_);
}
if (adPosition == 1 && sequenceLength == 1) {
sequenceLength = Array.from(this.interstitials_).filter((i) => {
if (interstitial.pre) {
return i.pre == interstitial.pre;
} else if (interstitial.post) {
return i.post == interstitial.post;
}
return Math.abs(i.startTime - interstitial.startTime) < 0.001;
}).length;
}
if (interstitial.once) {
oncePlayed++;
this.interstitials_.delete(interstitial);
if (!interstitial.overlay) {
this.cuepointsChanged_();
}
}
if (interstitial.mimeType) {
if (interstitial.mimeType.startsWith('image/') ||
interstitial.mimeType === 'text/html') {
if (!interstitial.overlay) {
shaka.log.alwaysWarn('Unsupported interstitial', interstitial);
return;
}
this.setupStaticAd_(interstitial, sequenceLength, adPosition,
oncePlayed);
return;
}
}
if (this.usingBaseVideo_ && interstitial.overlay) {
shaka.log.alwaysWarn('Unsupported interstitial', interstitial);
return;
}
this.setupVideoAd_(interstitial, sequenceLength, adPosition, initialTime,
oncePlayed);
}
/**
* @param {shaka.extern.AdInterstitial} interstitial
* @param {number} sequenceLength
* @param {number} adPosition
* @param {number} oncePlayed
* @private
*/
setupStaticAd_(interstitial, sequenceLength, adPosition, oncePlayed) {
const overlay = interstitial.overlay;
goog.asserts.assert(overlay, 'Must have overlay');
const tagName = interstitial.mimeType == 'text/html' ? 'iframe' : 'img';
const htmlElement = /** @type {!(HTMLImageElement|HTMLIFrameElement)} */ (
document.createElement(tagName));
htmlElement.style.objectFit = 'contain';
htmlElement.style.position = 'absolute';
htmlElement.style.border = 'none';
const basicTask = () => {
if (this.playoutLimitTimer_) {
this.playoutLimitTimer_.stop();
this.playoutLimitTimer_ = null;
}
this.adContainer_.removeChild(htmlElement);
this.onEvent_(
new shaka.util.FakeEvent(shaka.ads.Utils.AD_STOPPED));
const nextCurrentInterstitial = this.getCurrentInterstitial_(
interstitial.pre, adPosition - oncePlayed);
if (nextCurrentInterstitial) {
this.adEventManager_.removeAll();
this.setupAd_(nextCurrentInterstitial, sequenceLength,
++adPosition, /* initialTime= */ Date.now(), oncePlayed);
} else {
this.playingAd_ = false;
}
};
const ad = new shaka.ads.InterstitialStaticAd(sequenceLength, adPosition,
interstitial.overlay == null);
this.onEvent_(
new shaka.util.FakeEvent(shaka.ads.Utils.AD_STARTED,
(new Map()).set('ad', ad)));
if (tagName == 'iframe') {
htmlElement.src = interstitial.uri;
} else {
htmlElement.src = interstitial.uri;
htmlElement.onerror = (e) => {
this.onEvent_(new shaka.util.FakeEvent(shaka.ads.Utils.AD_ERROR,
(new Map()).set('originalEvent', e)));
basicTask();
};
}
const viewport = overlay.viewport;
const topLeft = overlay.topLeft;
const size = overlay.size;
// Special case for VAST non-linear ads
if (viewport.x == 0 && viewport.y == 0) {
htmlElement.width = interstitial.overlay.size.x;
htmlElement.height = interstitial.overlay.size.y;
htmlElement.style.bottom = '10%';
htmlElement.style.left = '0';
htmlElement.style.right = '0';
htmlElement.style.width = '100%';
if (!interstitial.overlay.size.y && tagName == 'iframe') {
htmlElement.style.height = 'auto';
}
} else {
htmlElement.style.height = (size.y / viewport.y * 100) + '%';
htmlElement.style.left = (topLeft.x / viewport.x * 100) + '%';
htmlElement.style.top = (topLeft.y / viewport.y * 100) + '%';
htmlElement.style.width = (size.x / viewport.x * 100) + '%';
}
this.adContainer_.appendChild(htmlElement);
const startTime = Date.now();
if (this.playoutLimitTimer_) {
this.playoutLimitTimer_.stop();
}
this.playoutLimitTimer_ = new shaka.util.Timer(() => {
if (interstitial.playoutLimit &&
(Date.now() - startTime) / 1000 > interstitial.playoutLimit) {
this.onEvent_(
new shaka.util.FakeEvent(shaka.ads.Utils.AD_COMPLETE));
basicTask();
} else if (interstitial.endTime &&
this.baseVideo_.currentTime > interstitial.endTime) {
this.onEvent_(
new shaka.util.FakeEvent(shaka.ads.Utils.AD_COMPLETE));
basicTask();
} else if (this.baseVideo_.currentTime < interstitial.startTime) {
this.onEvent_(
new shaka.util.FakeEvent(shaka.ads.Utils.AD_SKIPPED));
basicTask();
}
});
if (interstitial.playoutLimit && !interstitial.endTime) {
this.playoutLimitTimer_.tickAfter(interstitial.playoutLimit);
} else if (interstitial.endTime) {
this.playoutLimitTimer_.tickEvery(/* seconds= */ 0.025);
}
}
/**
* @param {shaka.extern.AdInterstitial} interstitial
* @param {number} sequenceLength
* @param {number} adPosition
* @param {number} initialTime the clock time the ad started at
* @param {number} oncePlayed
* @private
*/
async setupVideoAd_(interstitial, sequenceLength, adPosition, initialTime,
oncePlayed) {
goog.asserts.assert(this.video_, 'Must have video');
const startTime = Date.now();
this.playingAd_ = true;
if (this.usingBaseVideo_ && adPosition == 1) {
this.onEvent_(new shaka.util.FakeEvent(
shaka.ads.Utils.AD_CONTENT_PAUSE_REQUESTED,
(new Map()).set('saveLivePosition', true)));
const detachBasePlayerPromise = new shaka.util.PublicPromise();
const checkState = async (e) => {
if (e['state'] == 'detach') {
if (shaka.util.Platform.isSmartTV()) {
await new Promise(
(resolve) => new shaka.util.Timer(resolve).tickAfter(0.1));
}
detachBasePlayerPromise.resolve();
this.adEventManager_.unlisten(
this.basePlayer_, 'onstatechange', checkState);
}
};
this.adEventManager_.listen(
this.basePlayer_, 'onstatechange', checkState);
await detachBasePlayerPromise;
}
if (!this.usingBaseVideo_) {
this.video_.style.display = '';
if (interstitial.overlay) {
this.video_.loop = interstitial.loop;
const viewport = interstitial.overlay.viewport;
const topLeft = interstitial.overlay.topLeft;
const size = interstitial.overlay.size;
this.video_.style.height = (size.y / viewport.y * 100) + '%';
this.video_.style.left = (topLeft.x / viewport.x * 100) + '%';
this.video_.style.top = (topLeft.y / viewport.y * 100) + '%';
this.video_.style.width = (size.x / viewport.x * 100) + '%';
} else {
this.baseVideo_.pause();
if (interstitial.resumeOffset != null &&
interstitial.resumeOffset != 0) {
this.baseVideo_.currentTime += interstitial.resumeOffset;
}
this.video_.loop = false;
this.video_.style.height = '100%';
this.video_.style.left = '0';
this.video_.style.top = '0';
this.video_.style.width = '100%';
}
}
let unloadingInterstitial = false;
const updateBaseVideoTime = () => {
if (!this.usingBaseVideo_ && !interstitial.overlay) {
if (interstitial.resumeOffset == null) {
if (interstitial.timelineRange && interstitial.endTime &&
interstitial.endTime != Infinity) {
if (this.baseVideo_.currentTime != interstitial.endTime) {
this.baseVideo_.currentTime = interstitial.endTime;
}
} else {
const now = Date.now();
this.baseVideo_.currentTime += (now - initialTime) / 1000;
initialTime = now;
}
}
}
};
const basicTask = async () => {
updateBaseVideoTime();
if (this.playoutLimitTimer_) {
this.playoutLimitTimer_.stop();
this.playoutLimitTimer_ = null;
}
// Optimization to avoid returning to main content when there is another
// interstitial below.
const nextCurrentInterstitial = this.getCurrentInterstitial_(
interstitial.pre, adPosition - oncePlayed);
if (!nextCurrentInterstitial || nextCurrentInterstitial.overlay) {
if (interstitial.post) {
this.lastTime_ = null;
this.lastPlayedAd_ = null;
}
if (this.usingBaseVideo_) {
await this.player_.detach();
} else {
await this.player_.unload();
}
if (this.usingBaseVideo_) {
let offset = interstitial.resumeOffset;
if (offset == null) {
if (interstitial.timelineRange && interstitial.endTime &&
interstitial.endTime != Infinity) {
offset = interstitial.endTime - (this.lastTime_ || 0);
} else {
offset = (Date.now() - initialTime) / 1000;
}
}
this.onEvent_(new shaka.util.FakeEvent(
shaka.ads.Utils.AD_CONTENT_RESUME_REQUESTED,
(new Map()).set('offset', offset)));
}
this.onEvent_(
new shaka.util.FakeEvent(shaka.ads.Utils.AD_STOPPED));
this.adEventManager_.removeAll();
this.playingAd_ = false;
if (!this.usingBaseVideo_) {
this.video_.style.display = 'none';
updateBaseVideoTime();
if (!this.baseVideo_.ended) {
this.baseVideo_.play();
}
} else {
this.cuepointsChanged_();
}
}
this.determineIfUsingBaseVideo_();
if (nextCurrentInterstitial) {
this.onEvent_(
new shaka.util.FakeEvent(shaka.ads.Utils.AD_STOPPED));
this.adEventManager_.removeAll();
this.setupAd_(nextCurrentInterstitial, sequenceLength,
++adPosition, initialTime, oncePlayed);
}
};
const error = async (e) => {
if (unloadingInterstitial) {
return;
}
unloadingInterstitial = true;
this.onEvent_(new shaka.util.FakeEvent(shaka.ads.Utils.AD_ERROR,
(new Map()).set('originalEvent', e)));
await basicTask();
};
const complete = async () => {
if (unloadingInterstitial) {
return;
}
unloadingInterstitial = true;
await basicTask();
this.onEvent_(
new shaka.util.FakeEvent(shaka.ads.Utils.AD_COMPLETE));
};
const onSkip = async () => {
if (unloadingInterstitial) {
return;
}
unloadingInterstitial = true;
this.onEvent_(new shaka.util.FakeEvent(shaka.ads.Utils.AD_SKIPPED));
await basicTask();
};
const ad = new shaka.ads.InterstitialAd(this.video_,
interstitial.isSkippable, interstitial.skipOffset,
interstitial.skipFor, onSkip, sequenceLength, adPosition,
!this.usingBaseVideo_, interstitial.overlay);
if (!this.usingBaseVideo_) {
ad.setMuted(this.baseVideo_.muted);
ad.setVolume(this.baseVideo_.volume);
}
this.onEvent_(
new shaka.util.FakeEvent(shaka.ads.Utils.AD_STARTED,
(new Map()).set('ad', ad)));
let prevCanSkipNow = ad.canSkipNow();
if (prevCanSkipNow) {
this.onEvent_(new shaka.util.FakeEvent(
shaka.ads.Utils.AD_SKIP_STATE_CHANGED));
}
this.adEventManager_.listenOnce(this.player_, 'error', error);
this.adEventManager_.listen(this.video_, 'timeupdate', () => {
const duration = this.video_.duration;
if (!duration) {
return;
}
const currentCanSkipNow = ad.canSkipNow();
if (prevCanSkipNow != currentCanSkipNow &&
ad.getRemainingTime() > 0 && ad.getDuration() > 0) {
this.onEvent_(
new shaka.util.FakeEvent(shaka.ads.Utils.AD_SKIP_STATE_CHANGED));
}
prevCanSkipNow = currentCanSkipNow;
});
this.adEventManager_.listenOnce(this.player_, 'firstquartile', () => {
updateBaseVideoTime();
this.onEvent_(
new shaka.util.FakeEvent(shaka.ads.Utils.AD_FIRST_QUARTILE));
});
this.adEventManager_.listenOnce(this.player_, 'midpoint', () => {
updateBaseVideoTime();
this.onEvent_(
new shaka.util.FakeEvent(shaka.ads.Utils.AD_MIDPOINT));
});
this.adEventManager_.listenOnce(this.player_, 'thirdquartile', () => {
updateBaseVideoTime();
this.onEvent_(
new shaka.util.FakeEvent(shaka.ads.Utils.AD_THIRD_QUARTILE));
});
this.adEventManager_.listenOnce(this.player_, 'complete', complete);
this.adEventManager_.listen(this.video_, 'play', () => {
this.onEvent_(
new shaka.util.FakeEvent(shaka.ads.Utils.AD_RESUMED));
});
this.adEventManager_.listen(this.video_, 'pause', () => {
// playRangeEnd in src= causes the ended event not to be fired when that
// position is reached, instead pause event is fired.
const currentConfig = this.player_.getConfiguration();
if (this.video_.currentTime >= currentConfig.playRangeEnd) {
complete();
return;
}
this.onEvent_(
new shaka.util.FakeEvent(shaka.ads.Utils.AD_PAUSED));
});
this.adEventManager_.listen(this.video_, 'volumechange', () => {
if (this.video_.muted) {
this.onEvent_(
new shaka.util.FakeEvent(shaka.ads.Utils.AD_MUTED));
} else {
this.onEvent_(
new shaka.util.FakeEvent(shaka.ads.Utils.AD_VOLUME_CHANGED));
}
});
try {
this.updatePlayerConfig_();
if (interstitial.startTime && interstitial.endTime &&
interstitial.endTime != Infinity &&
interstitial.startTime != interstitial.endTime) {
const duration = interstitial.endTime - interstitial.startTime;
if (duration > 0) {
this.player_.configure('playRangeEnd', duration);
}
}
if (interstitial.playoutLimit) {
this.playoutLimitTimer_ = new shaka.util.Timer(() => {
ad.skip();
}).tickAfter(interstitial.playoutLimit);
this.player_.configure('playRangeEnd', interstitial.playoutLimit);
}
await this.player_.attach(this.video_);
if (this.preloadManagerInterstitials_.has(interstitial)) {
const preloadManager =
await this.preloadManagerInterstitials_.get(interstitial);
this.preloadManagerInterstitials_.delete(interstitial);
if (preloadManager) {
await this.player_.load(preloadManager);
} else {
await this.player_.load(
interstitial.uri,
/* startTime= */ null,
interstitial.mimeType || undefined);
}
} else {
await this.player_.load(
interstitial.uri,
/* startTime= */ null,
interstitial.mimeType || undefined);
}
if (interstitial.playoutLimit) {
if (this.playoutLimitTimer_) {
this.playoutLimitTimer_.stop();
}
this.playoutLimitTimer_ = new shaka.util.Timer(() => {
ad.skip();
}).tickAfter(interstitial.playoutLimit);
}
const loadTime = (Date.now() - startTime) / 1000;
this.onEvent_(new shaka.util.FakeEvent(shaka.ads.Utils.ADS_LOADED,
(new Map()).set('loadTime', loadTime)));
if (this.usingBaseVideo_) {
this.baseVideo_.play();
}
} catch (e) {
if (!this.playingAd_) {
return;
}
error(e);
}
}
/**
* @param {shaka.extern.HLSInterstitial} hlsInterstitial
* @return {!Promise.<!Array.<shaka.extern.AdInterstitial>>}
* @private
*/
async getInterstitialsInfo_(hlsInterstitial) {
const interstitialsAd = [];
if (!hlsInterstitial) {
return interstitialsAd;
}
const assetUri = hlsInterstitial.values.find((v) => v.key == 'X-ASSET-URI');
const assetList =
hlsInterstitial.values.find((v) => v.key == 'X-ASSET-LIST');
if (!assetUri && !assetList) {
return interstitialsAd;
}
let id = null;
const hlsInterstitialId = hlsInterstitial.values.find((v) => v.key == 'ID');
if (hlsInterstitialId) {
id = /** @type {string} */(hlsInterstitialId.data);
}
const startTime = id == null ?
Math.floor(hlsInterstitial.startTime * 10) / 10:
hlsInterstitial.startTime;
let endTime = hlsInterstitial.endTime;
if (hlsInterstitial.endTime && hlsInterstitial.endTime != Infinity &&
typeof(hlsInterstitial.endTime) == 'number') {
endTime = id == null ?
Math.floor(hlsInterstitial.endTime * 10) / 10:
hlsInterstitial.endTime;
}
const restrict = hlsInterstitial.values.find((v) => v.key == 'X-RESTRICT');
let isSkippable = true;
let canJump = true;
if (restrict && restrict.data) {
const data = /** @type {string} */(restrict.data);
isSkippable = !data.includes('SKIP');
canJump = !data.includes('JUMP');
}
let skipOffset = isSkippable ? 0 : null;
const enableSkipAfter =
hlsInterstitial.values.find((v) => v.key == 'X-ENABLE-SKIP-AFTER');
if (enableSkipAfter) {
const enableSkipAfterString = /** @type {string} */(enableSkipAfter.data);
skipOffset = parseFloat(enableSkipAfterString);
if (isNaN(skipOffset)) {
skipOffset = isSkippable ? 0 : null;
}
}
let skipFor = null;
const enableSkipFor =
hlsInterstitial.values.find((v) => v.key == 'X-ENABLE-SKIP-FOR');
if (enableSkipFor) {
const enableSkipForString = /** @type {string} */(enableSkipFor.data);
skipFor = parseFloat(enableSkipForString);
if (isNaN(skipOffset)) {
skipFor = null;
}
}
let resumeOffset = null;
const resume =
hlsInterstitial.values.find((v) => v.key == 'X-RESUME-OFFSET');
if (resume) {
const resumeOffsetString = /** @type {string} */(resume.data);
resumeOffset = parseFloat(resumeOffsetString);
if (isNaN(resumeOffset)) {
resumeOffset = null;
}
}
let playoutLimit = null;
const playout =
hlsInterstitial.values.find((v) => v.key == 'X-PLAYOUT-LIMIT');
if (playout) {
const playoutLimitString = /** @type {string} */(playout.data);
playoutLimit = parseFloat(playoutLimitString);
if (isNaN(playoutLimit)) {
playoutLimit = null;
}
}
let once = false;
let pre = false;
let post = false;
const cue = hlsInterstitial.values.find((v) => v.key == 'CUE');
if (cue) {
const data = /** @type {string} */(cue.data);
once = data.includes('ONCE');
pre = data.includes('PRE');
post = data.includes('POST');
}
let timelineRange = false;
const timelineOccupies =
hlsInterstitial.values.find((v) => v.key == 'X-TIMELINE-OCCUPIES');
if (timelineOccupies) {
const data = /** @type {string} */(timelineOccupies.data);
timelineRange = data.includes('RANGE');
} else if (!resume && this.basePlayer_.isLive()) {
timelineRange = !pre && !post;
}
if (assetUri) {
const uri = /** @type {string} */(assetUri.data);
if (!uri) {
return interstitialsAd;
}
interstitialsAd.push({
id,
startTime,
endTime,
uri,
mimeType: null,
isSkippable,
skipOffset,
skipFor,
canJump,
resumeOffset,
playoutLimit,
once,
pre,
post,
timelineRange,
loop: false,
overlay: null,
});
} else if (assetList) {
const uri = /** @type {string} */(assetList.data);
if (!uri) {
return interstitialsAd;
}
try {
const NetworkingEngine = shaka.net.NetworkingEngine;
const context = {
type: NetworkingEngine.AdvancedRequestType.INTERSTITIAL_ASSET_LIST,
};
const responseData = await this.makeAdRequest_(uri, context);
const data = shaka.util.StringUtils.fromUTF8(responseData);
const dataAsJson =
/** @type {!shaka.ads.InterstitialAdManager.AssetsList} */ (
JSON.parse(data));
const skipControl = dataAsJson['SKIP-CONTROL'];
if (skipControl) {
const enableSkipAfterValue = skipControl['ENABLE-SKIP-AFTER'];
if ((typeof enableSkipAfterValue) == 'number') {
skipOffset = parseFloat(enableSkipAfterValue);
if (isNaN(enableSkipAfterValue)) {
skipOffset = isSkippable ? 0 : null;
}
}
const enableSkipForValue = skipControl['ENABLE-SKIP-FOR'];
if ((typeof enableSkipForValue) == 'number') {
skipFor = parseFloat(enableSkipForValue);
if (isNaN(enableSkipForValue)) {
skipFor = null;
}
}
}
for (let i = 0; i < dataAsJson['ASSETS'].length; i++) {
const asset = dataAsJson['ASSETS'][i];
if (asset['URI']) {
interstitialsAd.push({
id: id + '_asset_' + i,
startTime,
endTime,
uri: asset['URI'],
mimeType: null,
isSkippable,
skipOffset,
skipFor,
canJump,
resumeOffset,
playoutLimit,
once,
pre,
post,
timelineRange,
loop: false,
overlay: null,
});
}
}
} catch (e) {
// Ignore errors
}
}
return interstitialsAd;
}
/**
* @private
*/
cuepointsChanged_() {
/** @type {!Array.<!shaka.extern.AdCuePoint>} */
const cuePoints = [];
for (const interstitial of this.interstitials_) {
if (interstitial.overlay) {
continue;
}
/** @type {shaka.extern.AdCuePoint} */
const shakaCuePoint = {
start: interstitial.startTime,
end: null,
};
if (interstitial.pre) {
shakaCuePoint.start = 0;
shakaCuePoint.end = null;
} else if (interstitial.post) {
shakaCuePoint.start = -1;
shakaCuePoint.end = null;
} else if (interstitial.timelineRange) {
shakaCuePoint.end = interstitial.endTime;
}
const isValid = !cuePoints.find((c) => {
return shakaCuePoint.start == c.start && shakaCuePoint.end == c.end;
});
if (isValid) {
cuePoints.push(shakaCuePoint);
}
}
this.onEvent_(new shaka.util.FakeEvent(
shaka.ads.Utils.CUEPOINTS_CHANGED,
(new Map()).set('cuepoints', cuePoints)));
}
/**
* @private
*/
updatePlayerConfig_() {
goog.asserts.assert(this.player_, 'Must have player');
goog.asserts.assert(this.basePlayer_, 'Must have base player');
this.player_.configure(this.basePlayer_.getNonDefaultConfiguration());
this.player_.configure('ads.disableHLSInterstitial', true);
this.player_.configure('ads.disableDASHInterstitial', true);
const netEngine = this.player_.getNetworkingEngine();
goog.asserts.assert(netEngine, 'Need networking engine');
netEngine.clearAllRequestFilters();
netEngine.clearAllResponseFilters();
this.basePlayer_.getNetworkingEngine().copyFiltersInto(netEngine);
}
/**
* @param {string} url
* @param {shaka.extern.RequestContext=} context
* @return {!Promise.<BufferSource>}
* @private
*/
async makeAdRequest_(url, context) {
const type = shaka.net.NetworkingEngine.RequestType.ADS;
const request = shaka.net.NetworkingEngine.makeRequest(
[url],
shaka.net.NetworkingEngine.defaultRetryParameters());
const op = this.basePlayer_.getNetworkingEngine()
.request(type, request, context);
const response = await op.promise;
return response.data;
}
/**
* @param {!shaka.extern.AdInterstitial} interstitial
* @return {boolean}
* @private
*/
isPreloadAllowed_(interstitial) {
const interstitialMimeType = interstitial.mimeType;
if (!interstitialMimeType) {
return true;
}
return !interstitialMimeType.startsWith('image/') &&
interstitialMimeType !== 'text/html';
}
/**
* Only for testing
*
* @return {!Array.<shaka.extern.AdInterstitial>}
*/
getInterstitials() {
return Array.from(this.interstitials_);
}
};
/**
* @typedef {{
* ASSETS: !Array.<shaka.ads.InterstitialAdManager.Asset>,
* SKIP-CONTROL: ?shaka.ads.InterstitialAdManager.SkipControl
* }}
*
* @property {!Array.<shaka.ads.InterstitialAdManager.Asset>} ASSETS
* @property {shaka.ads.InterstitialAdManager.SkipControl} SKIP-CONTROL
*/
shaka.ads.InterstitialAdManager.AssetsList;
/**
* @typedef {{
* URI: string
* }}
*
* @property {string} URI
*/
shaka.ads.InterstitialAdManager.Asset;
/**
* @typedef {{
* ENABLE-SKIP-AFTER: number,
* ENABLE-SKIP-FOR: number
* }}
*
* @property {number} ENABLE-SKIP-AFTER
* @property {number} ENABLE-SKIP-FOR
*/
shaka.ads.InterstitialAdManager.SkipControl;