/** @license
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.ads.AdManager');
goog.provide('shaka.ads.CuePoint');
goog.require('shaka.Player');
goog.require('shaka.ads.AdsStats');
goog.require('shaka.ads.ClientSideAdManager');
goog.require('shaka.ads.ServerSideAdManager');
goog.require('shaka.log');
goog.require('shaka.util.FakeEventTarget');
/**
* @event shaka.ads.AdManager.ADS_LOADED
* @description Fired when an ad has started playing.
* @property {string} type
* 'ads-loaded'
* @property {number} loadTime
* The time it takes to load ads.
* @exportDoc
*/
/**
* @event shaka.ads.AdManager.AdStartedEvent
* @description Fired when an ad has started playing.
* @property {string} type
* 'ad-started'
* @property {!shaka.extern.IAd} ad
* The ad that has started playing.
* @exportDoc
*/
/**
* @event shaka.ads.AdManager.AdCompleteEvent
* @description Fired when an ad has played through.
* @property {string} type
* 'ad-complete'
* @exportDoc
*/
/**
* @event shaka.ads.AdManager.AdSkippedEvent
* @description Fired when an ad has been skipped.
* @property {string} type
* 'ad-skipped'
* @exportDoc
*/
/**
* @event shaka.ads.AdManager.AdFirstQuartileEvent
* @description Fired when an ad has played through the first 1/4.
* @property {string} type
* 'ad-first-quartile'
* @exportDoc
*/
/**
* @event shaka.ads.AdManager.AdMidpointEvent
* @description Fired when an ad has played through its midpoint.
* @property {string} type
* 'ad-midpoint'
* @exportDoc
*/
/**
* @event shaka.ads.AdManager.AdThirdQuartileEvent
* @description Fired when an ad has played through the third quartile.
* @property {string} type
* 'ad-third-quartile'
* @exportDoc
*/
/**
* @event shaka.ads.AdManager.AdStoppedEvent
* @description Fired when an ad has stopped playing, was skipped,
* or was unable to proceed due to an error.
* @property {string} type
* 'ad-stopped'
* @exportDoc
*/
/**
* @event shaka.ads.AdManager.AdVolumeChangedEvent
* @description Fired when an ad's volume changed.
* @property {string} type
* 'ad-volume-changed'
* @exportDoc
*/
/**
* @event shaka.ads.AdManager.AdMutedEvent
* @description Fired when an ad was muted.
* @property {string} type
* 'ad-muted'
* @exportDoc
*/
/**
* @event shaka.ads.AdManager.AdPausedEvent
* @description Fired when an ad was paused.
* @property {string} type
* 'ad-paused'
* @exportDoc
*/
/**
* @event shaka.ads.AdManager.AdResumedEvent
* @description Fired when an ad was resumed after a pause.
* @property {string} type
* 'ad-resumed'
* @exportDoc
*/
/**
* @event shaka.ads.AdManager.AdSkipStateChangedEvent
* @description Fired when an ad's skip state changes (for example, when
* it becomes possible to skip the ad).
* @property {string} type
* 'ad-skip-state-changed'
* @exportDoc
*/
/**
* @event shaka.ads.AdManager.AdResumedEvent
* @description Fired when the ad cue points change, signalling ad breaks
* change.
* @property {string} type
* 'ad-cue-points-changed'
* @exportDoc
*/
/**
* A class responsible for ad-related interactions.
* @implements {shaka.extern.IAdManager}
* @export
*/
shaka.ads.AdManager = class extends shaka.util.FakeEventTarget {
constructor() {
super();
/** @private {shaka.ads.ClientSideAdManager} */
this.csAdManager_ = null;
/** @private {shaka.ads.ServerSideAdManager} */
this.ssAdManager_ = null;
/** @private {shaka.ads.AdsStats} */
this.stats_ = new shaka.ads.AdsStats();
/** @private {string} locale */
this.locale_ = navigator.language;
}
/**
* @override
* @export
*/
setLocale(locale) {
this.locale_ = locale;
}
/**
* @override
* @export
*/
initClientSide(adContainer, video) {
// Check that Client Side IMA SDK has been included
// NOTE: (window['google'] && google.ima) check for any
// IMA SDK, including SDK for Server Side ads.
// The 3rd check insures we have the right SDK:
// {google.ima.AdsLoader} is an object that's part of CS IMA SDK
// but not SS SDK.
if (!window['google'] || !google.ima || !google.ima.AdsLoader) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.ADS,
shaka.util.Error.Code.CS_IMA_SDK_MISSING);
}
this.csAdManager_ = new shaka.ads.ClientSideAdManager(
adContainer, video, this.locale_,
(e) => {
const event = /** @type {!shaka.util.FakeEvent} */ (e);
if (event && event.type) {
switch (event.type) {
case shaka.ads.AdManager.ADS_LOADED: {
const loadTime = (/** @type {!Object} */ (e))['loadTime'];
this.stats_.addLoadTime(loadTime);
break;
}
case shaka.ads.AdManager.AD_STARTED:
this.stats_.incrementStarted();
break;
case shaka.ads.AdManager.AD_COMPLETE:
this.stats_.incrementPlayedCompletely();
break;
case shaka.ads.AdManager.AD_SKIPPED:
this.stats_.incrementSkipped();
break;
}
}
this.dispatchEvent(event);
});
}
/**
* @override
* @export
*/
onAssetUnload() {
if (this.csAdManager_) {
this.csAdManager_.stop();
}
this.dispatchEvent(
new shaka.util.FakeEvent(shaka.ads.AdManager.AD_STOPPED));
// TODO:
// For SS DAI streams, if a different asset gets unloaded as
// part of the process
// of loading a DAI asset, stream manager state gets reset and we
// don't get any ad events.
// We need to figure out if it makes sense to stop the SS
// manager on unload, and, if it does, find
// a way to do it safely.
// if (this.ssAdManager_) {
// this.ssAdManager_.stop();
// }
this.stats_ = new shaka.ads.AdsStats();
}
/**
* @override
* @export
*/
requestClientSideAds(imaRequest) {
if (!this.csAdManager_) {
throw new shaka.util.Error(
shaka.util.Error.Severity.RECOVERABLE,
shaka.util.Error.Category.ADS,
shaka.util.Error.Code.CS_AD_MANAGER_NOT_INITIALIZED);
}
this.csAdManager_.requestAds(imaRequest);
}
/**
* @override
* @export
*/
initServerSide(adContainer, video) {
// Check that Client Side IMA SDK has been included
// NOTE: (window['google'] && google.ima) check for any
// IMA SDK, including SDK for Server Side ads.
// The 3rd check insures we have the right SDK:
// {google.ima.dai} is an object that's part of DAI IMA SDK
// but not SS SDK.
if (!window['google'] || !google.ima || !google.ima.dai) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.ADS,
shaka.util.Error.Code.SS_IMA_SDK_MISSING);
}
this.ssAdManager_ = new shaka.ads.ServerSideAdManager(
adContainer, video, this.locale_,
(e) => {
const event = /** @type {!shaka.util.FakeEvent} */ (e);
if (event && event.type) {
switch (event.type) {
case shaka.ads.AdManager.ADS_LOADED: {
const loadTime = (/** @type {!Object} */ (e))['loadTime'];
this.stats_.addLoadTime(loadTime);
break;
}
case shaka.ads.AdManager.AD_STARTED:
this.stats_.incrementStarted();
break;
case shaka.ads.AdManager.AD_COMPLETE:
this.stats_.incrementPlayedCompletely();
break;
case shaka.ads.AdManager.AD_SKIPPED:
this.stats_.incrementSkipped();
break;
}
}
this.dispatchEvent(event);
});
// Set the name and version of the player in IMA for tracking.
this.replaceServerSideAdTagParameters(
{
'mpt': 'shaka',
'mpv': shaka.Player.version,
});
}
/**
* @param {!google.ima.dai.api.StreamRequest} imaRequest
* @param {string=} backupUrl
* @return {!Promise.<string>}
* @override
* @export
*/
requestServerSideStream(imaRequest, backupUrl = '') {
if (!this.ssAdManager_) {
throw new shaka.util.Error(
shaka.util.Error.Severity.RECOVERABLE,
shaka.util.Error.Category.ADS,
shaka.util.Error.Code.SS_AD_MANAGER_NOT_INITIALIZED);
}
return this.ssAdManager_.streamRequest(imaRequest, backupUrl);
}
/**
* @override
* @export
*/
replaceServerSideAdTagParameters(adTagParameters) {
if (!this.ssAdManager_) {
throw new shaka.util.Error(
shaka.util.Error.Severity.RECOVERABLE,
shaka.util.Error.Category.ADS,
shaka.util.Error.Code.SS_AD_MANAGER_NOT_INITIALIZED);
}
this.ssAdManager_.replaceAdTagParameters(adTagParameters);
}
/**
* @return {shaka.extern.AdsStats}
* @override
* @export
*/
getStats() {
return this.stats_.getBlob();
}
/**
* @override
* @export
*/
onDashTimedMetadata(region) {
if (this.ssAdManager_ && region.schemeIdUri == 'urn:google:dai:2018') {
const type = region.schemeIdUri;
const data = region.eventElement ?
region.eventElement.getAttribute('messageData') : null;
const timestamp = region.startTime;
this.ssAdManager_.onTimedMetadata(type, data, timestamp);
}
}
/**
* @override
* @export
*/
onHlsTimedMetadata(metadata, timestampOffset) {
for (const sample of metadata) {
if (this.ssAdManager_ && sample['data'] && sample['cueTime']) {
const timestamp = sample['cueTime'] + timestampOffset;
this.ssAdManager_.onTimedMetadata('ID3', sample['data'], timestamp);
}
}
}
/**
* @override
* @export
*/
onCueMetadataChange(value) {
if (this.ssAdManager_) {
this.ssAdManager_.onCueMetadataChange(value);
} else {
shaka.log.warning('The method was called without initializing server ' +
'side logic and will not take effect');
}
}
};
shaka.ads.CuePoint = class {
/**
* @param {number} start
* @param {?number=} end
*/
constructor(start, end = null) {
/** @public {number} */
this.start = start;
/** @public {?number} */
this.end = end;
}
};
/**
* The event name for when a sequence of ads has been loaded.
*
* @const {string}
* @export
*/
shaka.ads.AdManager.ADS_LOADED = 'ads-loaded';
/**
* The event name for when an ad has started playing.
*
* @const {string}
* @export
*/
shaka.ads.AdManager.AD_STARTED = 'ad-started';
/**
* The event name for when an ad playhead crosses first quartile.
*
* @const {string}
* @export
*/
shaka.ads.AdManager.AD_FIRST_QUARTILE = 'ad-first-quartile';
/**
* The event name for when an ad playhead crosses midpoint.
*
* @const {string}
* @export
*/
shaka.ads.AdManager.AD_MIDPOINT = 'ad-midpoint';
/**
* The event name for when an ad playhead crosses third quartile.
*
* @const {string}
* @export
*/
shaka.ads.AdManager.AD_THIRD_QUARTILE = 'ad-third-quartile';
/**
* The event name for when an ad has completed playing.
*
* @const {string}
* @export
*/
shaka.ads.AdManager.AD_COMPLETE = 'ad-complete';
/**
* The event name for when an ad has finished playing
* (played all the way through, was skipped, or was unable to proceed
* due to an error).
*
* @const {string}
* @export
*/
shaka.ads.AdManager.AD_STOPPED = 'ad-stopped';
/**
* The event name for when an ad is skipped by the user..
*
* @const {string}
* @export
*/
shaka.ads.AdManager.AD_SKIPPED = 'ad-skipped';
/**
* The event name for when the ad volume has changed.
*
* @const {string}
* @export
*/
shaka.ads.AdManager.AD_VOLUME_CHANGED = 'ad-volume-changed';
/**
* The event name for when the ad was muted.
*
* @const {string}
* @export
*/
shaka.ads.AdManager.AD_MUTED = 'ad-muted';
/**
* The event name for when the ad was paused.
*
* @const {string}
* @export
*/
shaka.ads.AdManager.AD_PAUSED = 'ad-paused';
/**
* The event name for when the ad was resumed after a pause.
*
* @const {string}
* @export
*/
shaka.ads.AdManager.AD_RESUMED = 'ad-resumed';
/**
* The event name for when the ad's skip status changes
* (usually it becomes skippable when it wasn't before).
*
* @const {string}
* @export
*/
shaka.ads.AdManager.AD_SKIP_STATE_CHANGED = 'ad-skip-state-changed';
/**
* The event name for when the ad's cue points (start/end markers)
* have changed.
*
* @const {string}
* @export
*/
shaka.ads.AdManager.CUEPOINTS_CHANGED = 'ad-cue-points-changed';
/**
* Set this is a default ad manager for the player.
* Apps can also set their own ad manager, if they'd like.
*/
shaka.Player.setAdManagerFactory(() => new shaka.ads.AdManager());