/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.ads.SvtaInterstitialTracker');
goog.require('shaka.ads.InterstitialAdSession');
goog.require('shaka.ads.InterstitialTracker');
goog.require('shaka.ads.SvtaInterstitialParser');
goog.require('shaka.ads.Utils');
goog.require('shaka.log');
goog.require('shaka.util.IReleasable');
goog.require('shaka.util.StringUtils');
goog.requireType('shaka.net.NetworkingEngine');
/**
* Measures interstitials described by SVTA2053 version 2 creative signaling.
* Holds the state shared by the slots of a pod and by remote fields, which
* lives as long as the asset; each interstitial gets its own tracker.
*
* @implements {shaka.util.IReleasable}
*/
shaka.ads.SvtaInterstitialTracker = class {
constructor() {
/**
* @private {!WeakMap<shaka.extern.AdCreativeSignaling.Pod,
* shaka.ads.SvtaInterstitialTracker.PodState>}
*/
this.podStates_ = new WeakMap();
/**
* Tracking events already sent for embedded slots. Watching an embedded
* slot again (e.g. after seeking back) is not a new ad view.
* @private {!WeakMap<shaka.extern.AdCreativeSignaling.Slot,
* !Set<shaka.ads.InterstitialTracker.Event>>}
*/
this.slotFired_ = new WeakMap();
/** @private {!WeakMap<Object, !Promise>} */
this.remoteFields_ = new WeakMap();
/** @private {!Set<shaka.net.NetworkingEngine.PendingRequest>} */
this.requests_ = new Set();
/** @private {number} */
this.generation_ = 0;
}
/**
* Forgets the state of the current asset. Pending responses are discarded,
* even if the network abort arrives late.
*/
reset() {
this.generation_++;
this.podStates_ = new WeakMap();
this.slotFired_ = new WeakMap();
this.remoteFields_ = new WeakMap();
for (const request of this.requests_) {
request.abort();
}
this.requests_.clear();
}
/** @override */
release() {
this.reset();
}
/**
* @param {!shaka.extern.AdInterstitial} interstitial
* @param {!shaka.ads.InterstitialAdSession} session
* @param {function(string)} sendBeacon
* @param {function(string):!shaka.net.NetworkingEngine.PendingRequest} fetch
* @return {?shaka.ads.InterstitialAdSession.Tracker}
*/
create(interstitial, session, sendBeacon, fetch) {
if (!interstitial.adCreativeSignaling) {
return null;
}
/** @type {!shaka.extern.AdCreativeSignaling.Slot} */
const slot = interstitial.adCreativeSignaling;
const Tracker = shaka.ads.SvtaInterstitialTracker;
const pod = interstitial.pod || null;
let podTracker = null;
let podState = null;
if (pod) {
/** @type {!shaka.extern.AdCreativeSignaling.Pod} */
const nonNullPod = pod;
podState = this.podStates_.getOrInsertComputed(pod,
() => ({fired: new Set(), started: false, ended: false}));
podTracker = new shaka.ads.InterstitialTracker(
() => Tracker.getTracking_(nonNullPod), sendBeacon, podState.fired);
}
// A new playback of an external asset is a new ad view.
const fired = interstitial.playbackMode == shaka.ads.Utils.EMBEDDED ?
this.slotFired_.getOrInsertComputed(slot, () => new Set()) :
new Set();
return new Tracker.SlotTracker(
new shaka.ads.InterstitialTracker(
() => Tracker.getTracking_(slot), sendBeacon, fired),
pod, podTracker, podState, interstitial.podOffset || 0,
() => session.getPosition(),
() => Promise.all([pod, slot].filter((item) => item != null).map(
(item) => this.resolveContainer_(item, session, fetch))));
}
/**
* @param {!(shaka.extern.AdCreativeSignaling.Slot|
* shaka.extern.AdCreativeSignaling.Pod)} container
* @return {!Array<shaka.extern.AdCreativeSignaling.TrackingEvent>}
* @private
*/
static getTracking_(container) {
// Remote tracking replaces the inline one, so wait until it is resolved.
return container.$remote?.tracking ? [] : container.tracking || [];
}
/**
* Resolves each remote container once, when its media is needed. Responses
* replace the field rather than appending to its inline fallback.
*
* @param {!(shaka.extern.AdCreativeSignaling.Pod|
* shaka.extern.AdCreativeSignaling.Slot)} container
* @param {!shaka.ads.InterstitialAdSession} session
* @param {function(string):!shaka.net.NetworkingEngine.PendingRequest} fetch
* @return {!Promise}
* @private
*/
resolveContainer_(container, session, fetch) {
return this.remoteFields_.getOrInsertComputed(container, async () => {
// Remote pod slots are resolved by the parser before scheduling, and
// verifications are not used, so only tracking is needed here.
const remote = container.$remote;
if (!remote || !remote.tracking) {
return;
}
const generation = this.generation_;
const request = fetch(session.expandUri(remote.tracking));
this.requests_.add(request);
try {
const response = await request.promise;
const envelope = shaka.ads.SvtaInterstitialParser.parseEnvelope(
JSON.parse(shaka.util.StringUtils.fromUTF8(response.data)),
'trackingEvent');
if (generation == this.generation_ && envelope) {
// The envelope type guarantees the payload holds tracking events.
container.tracking = /** @type {?} */ (envelope.payload);
}
} catch (error) {
shaka.log.warning('Failed to resolve SVTA remote tracking', error);
} finally {
this.requests_.delete(request);
if (generation == this.generation_) {
delete remote.tracking;
}
}
});
}
};
/**
* Tracks a slot and, at its boundaries, the pod that contains it.
*
* @implements {shaka.ads.InterstitialAdSession.Tracker}
*/
shaka.ads.SvtaInterstitialTracker.SlotTracker = class {
/**
* @param {!shaka.ads.InterstitialTracker} slotTracker
* @param {?shaka.extern.AdCreativeSignaling.Pod} pod
* @param {?shaka.ads.InterstitialTracker} podTracker
* @param {?shaka.ads.SvtaInterstitialTracker.PodState} podState
* @param {number} podOffset The slot start relative to its pod.
* @param {function():number} getPosition The slot playhead position.
* @param {function():!Promise} resolve
*/
constructor(slotTracker, pod, podTracker, podState, podOffset, getPosition,
resolve) {
/** @private {!shaka.ads.InterstitialTracker} */
this.slot_ = slotTracker;
/** @private {?shaka.extern.AdCreativeSignaling.Pod} */
this.pod_ = pod;
/** @private {?shaka.ads.InterstitialTracker} */
this.podTracker_ = podTracker;
/** @private {?shaka.ads.SvtaInterstitialTracker.PodState} */
this.podState_ = podState;
/** @private {number} */
this.podOffset_ = podOffset;
/** @private {function():number} */
this.getPosition_ = getPosition;
/** @private {function():!Promise} */
this.resolve_ = resolve;
}
/** @override */
release() {
this.slot_.release();
if (this.podTracker_) {
this.podTracker_.release();
}
}
/** @override */
resolve() {
return this.resolve_();
}
/** @override */
onEvent(type) {
const name = shaka.ads.InterstitialTracker.getName(type);
const pod = this.pod_;
const podTracker = this.podTracker_;
const podState = this.podState_;
if (!name || !pod || !podTracker || !podState) {
this.slot_.onEvent(type);
return;
}
const podPosition = this.podOffset_ + this.getPosition_();
const isStart =
name == 'start' || name == 'impression' || name == 'loaded';
if (isStart && (podPosition < 0.1 || podState.started)) {
podState.started = true;
podTracker.fire('podStart');
}
this.slot_.onEvent(type);
// Only actions, and the start of the first slot, apply to the pod.
if ((isStart && podPosition < 0.1) ||
shaka.ads.InterstitialTracker.isRepeated(name)) {
podTracker.fire(name);
}
// Pod end follows slot completion and all other slot tracking.
if (name == 'complete' && podPosition >= pod.duration && !podState.ended) {
podState.ended = true;
podTracker.fire('complete');
podTracker.fire('podEnd');
}
}
/** @override */
onProgress(previous, position) {
this.slot_.onProgress(previous, position);
const pod = this.pod_;
const podTracker = this.podTracker_;
if (!pod || !podTracker) {
return;
}
const podPrevious = this.podOffset_ + previous;
const podPosition = this.podOffset_ + position;
podTracker.onProgress(podPrevious, podPosition);
const names = ['firstQuartile', 'midpoint', 'thirdQuartile'];
for (let i = 0; i < names.length; i++) {
const threshold = pod.duration * (i + 1) / 4;
if (podPrevious <= threshold && podPosition >= threshold) {
podTracker.fire(names[i]);
}
}
}
};
/**
* @typedef {{
* fired: !Set<shaka.extern.AdCreativeSignaling.TrackingEvent>,
* started: boolean,
* ended: boolean,
* }}
*/
shaka.ads.SvtaInterstitialTracker.PodState;