UNPKG

shaka-player

Version:
397 lines (373 loc) • 13.6 kB
/*! @license * Shaka Player * Copyright 2016 Google LLC * SPDX-License-Identifier: Apache-2.0 */ goog.provide('shaka.ads.SvtaInterstitialParser'); goog.require('shaka.ads.Utils'); goog.require('shaka.log'); goog.require('shaka.net.NetworkingEngine'); goog.require('shaka.util.StringUtils'); goog.require('shaka.util.TXml'); goog.require('shaka.util.Uint8ArrayUtils'); /** Parses SVTA2053 version 2 creative signaling, without loading resources. */ shaka.ads.SvtaInterstitialParser = class { /** * @param {shaka.extern.HLSMetadata} metadata * @return {boolean} */ static isValidMetadata(metadata) { return metadata.values.some((value) => value.key == shaka.ads.SvtaInterstitialParser.KEY); } /** * @param {shaka.extern.TimelineRegionInfo} region * @return {boolean} */ static isValidRegion(region) { return !!region.eventNode && (region.schemeIdUri == 'urn:svta:advertising-wg:ad-creative-signaling' || region.schemeIdUri == 'urn:svta:advertising-wg:ad-id-signaling'); } /** * @param {shaka.extern.HLSMetadata} metadata * @param {?shaka.net.NetworkingEngine=} networkingEngine Used to resolve * remote pod slots. Without it, pods with remote slots are ignored. * @return {!shaka.ads.SvtaInterstitialParser.Result} */ static parseMetadata(metadata, networkingEngine = null) { const id = metadata.values.find((value) => value.key == 'ID'); return shaka.ads.SvtaInterstitialParser.parseInterstitials( shaka.ads.SvtaInterstitialParser.decodeMetadata(metadata), metadata.startTime, id ? String(id.data) : 'hls_' + metadata.startTime, networkingEngine); } /** * @param {shaka.extern.HLSMetadata} metadata * @return {*} The decoded signaling, or null if there is none. */ static decodeMetadata(metadata) { const frame = metadata.values.find((value) => value.key == shaka.ads.SvtaInterstitialParser.KEY); try { return JSON.parse(shaka.util.StringUtils.fromUTF8( shaka.util.Uint8ArrayUtils.fromBase64( /** @type {string} */ (frame.data)))); } catch (error) { shaka.log.warning('Invalid SVTA HLS signaling', error); return null; } } /** * @param {shaka.extern.TimelineRegionInfo} region * @param {?shaka.net.NetworkingEngine=} networkingEngine Used to resolve * remote pod slots. Without it, pods with remote slots are ignored. * @return {!shaka.ads.SvtaInterstitialParser.Result} */ static parseRegion(region, networkingEngine = null) { const Parser = shaka.ads.SvtaInterstitialParser; if (!region.eventNode || !Parser.isValidRegion(region)) { return {interstitials: [], deferred: []}; } try { const envelope = JSON.parse( shaka.util.TXml.getTextContents(region.eventNode) || 'null'); return Parser.parseInterstitials(envelope, region.startTime, region.id || 'dash_' + region.startTime, networkingEngine); } catch (error) { shaka.log.warning('Invalid SVTA DASH signaling', error); return {interstitials: [], deferred: []}; } } /** * Validates an envelope before any of its payload is used. The version is * not checked, so earlier signaling without version, envelope type, slot * type or identifiers is accepted. * * @param {*} data * @param {string=} expectedType * @return {?shaka.extern.AdCreativeSignaling.CarriageEnvelope} */ static parseEnvelope(data, expectedType = '') { const Parser = shaka.ads.SvtaInterstitialParser; if (!data || typeof data != 'object' || !Array.isArray(data['payload']) || !data['payload'].length) { return null; } // Without a type, the payload is what the caller expects (slots for // carriage, the requested field for remote responses). const type = data['type'] || expectedType || 'slot'; if (expectedType && type != expectedType) { return null; } const features = data['features'] || {}; for (const key in features) { if (features[key] && key != 'remoteFields') { shaka.log.warning('Unsupported SVTA feature', key); return null; } } const remoteFields = !!features['remoteFields']; const validators = { 'slot': (value) => Parser.isSlot_(value, remoteFields), 'pod': (value) => Parser.isPod_(value, remoteFields), 'trackingEvent': (value) => Parser.isTrackingEvent_(value), }; const validate = validators[type]; if (!validate || !data['payload'].every(validate)) { return null; } return /** @type {!shaka.extern.AdCreativeSignaling.CarriageEnvelope} */ ( data); } /** * @param {*} data * @param {number} anchor * @param {string} id * @param {?shaka.net.NetworkingEngine=} networkingEngine Used to resolve * remote pod slots. Without it, pods with remote slots are ignored. * @return {!shaka.ads.SvtaInterstitialParser.Result} */ static parseInterstitials(data, anchor, id, networkingEngine = null) { const Parser = shaka.ads.SvtaInterstitialParser; /** @type {!shaka.ads.SvtaInterstitialParser.Result} */ const result = {interstitials: [], deferred: []}; const envelope = Parser.parseEnvelope(data); if (!envelope || !Number.isFinite(anchor)) { return result; } const interstitials = result.interstitials; const type = envelope.type || 'slot'; if (type == 'slot') { const slots = /** @type {!Array<shaka.extern.AdCreativeSignaling.Slot>} */ (envelope.payload); for (let i = 0; i < slots.length; i++) { interstitials.push(Parser.makeInterstitial_(slots[i], null, anchor, id + '_slot_' + i, id, slots.length, i + 1)); } } else if (type == 'pod') { const pods = /** @type {!Array<shaka.extern.AdCreativeSignaling.Pod>} */ (envelope.payload); for (let i = 0; i < pods.length; i++) { const pod = pods[i]; const podId = id + '_pod_' + i; const start = anchor + (pod.start || 0); if (pod.$remote && pod.$remote.slots) { // A pod with remote slots is not playable until they are resolved, // which the manager schedules like any other deferred resource. if (networkingEngine) { result.deferred.push(Parser.createDeferredPod_( pod, start, podId, networkingEngine)); } } else { for (let j = 0; j < pod.slots.length; j++) { interstitials.push(Parser.makeInterstitial_(pod.slots[j], pod, start, podId + '_slot_' + j, podId, pod.slots.length, j + 1)); } } } } return result; } /** * @param {!shaka.extern.AdCreativeSignaling.Pod} pod * @param {number} startTime * @param {string} id * @param {!shaka.net.NetworkingEngine} networkingEngine * @return {!shaka.extern.DeferredInterstitial} * @private */ static createDeferredPod_(pod, startTime, id, networkingEngine) { const Parser = shaka.ads.SvtaInterstitialParser; const uri = /** @type {string} */ (pod.$remote.slots); return shaka.ads.Utils.createDeferredInterstitial({ id, startTime, endTime: startTime + pod.duration, pre: false, post: false, canJump: true, timelineRange: true, playoutLimit: null, }, networkingEngine, () => uri, (data) => { const envelope = Parser.parseEnvelope(data, 'slot'); if (!envelope) { throw new Error('Invalid SVTA remote slots'); } // Remote slots replace the inline ones. const slots = /** @type {!Array<!shaka.extern.AdCreativeSignaling.Slot>} */ ( envelope.payload); const $remote = {tracking: pod.$remote.tracking}; const resolved = /** @type {!shaka.extern.AdCreativeSignaling.Pod} */ ( Object.assign({}, pod, {slots, $remote})); return slots.map((slot, i) => Parser.makeInterstitial_(slot, resolved, startTime, id + '_slot_' + i, id, slots.length, i + 1)); }); } /** * Attaches validated slot metadata to media identified by another format, * if the signaling describes a single slot. SVTA does not supply the URI or * change stitching decisions. * * @param {!shaka.extern.AdInterstitial} interstitial * @param {*} data * @param {?shaka.extern.AdCreativeSignaling.Pod=} pod */ static decorateInterstitial(interstitial, data, pod = null) { const Parser = shaka.ads.SvtaInterstitialParser; const envelope = Parser.parseEnvelope(data, 'slot'); if (!envelope || envelope.payload.length != 1) { return; } const slot = /** @type {!shaka.extern.AdCreativeSignaling.Slot} */ (envelope.payload[0]); interstitial.adCreativeSignaling = slot; interstitial.pod = pod; interstitial.podOffset = slot.start; interstitial.clickThroughUrl = slot.clickThrough || null; if (slot.skipOffset != null && interstitial.isSkippable) { interstitial.skipOffset = slot.skipOffset; } } /** * @param {!shaka.extern.AdCreativeSignaling.Slot} slot * @param {?shaka.extern.AdCreativeSignaling.Pod} pod * @param {number} anchor * @param {string} id * @param {string} groupId * @param {number} podSize * @param {number} podPosition * @return {!shaka.extern.AdInterstitial} * @private */ static makeInterstitial_(slot, pod, anchor, id, groupId, podSize, podPosition) { const startTime = anchor + slot.start; return shaka.ads.Utils.createInterstitial({ id, groupId, startTime, endTime: startTime + slot.duration, playbackMode: shaka.ads.Utils.EMBEDDED, isSkippable: slot.skipOffset != null, skipOffset: slot.skipOffset ?? null, timelineRange: true, clickThroughUrl: slot.clickThrough || null, adCreativeSignaling: slot, pod, podOffset: slot.start, podSize, podPosition, }); } /** * @param {?Object} value * @param {boolean} remoteFields * @return {boolean} * @private */ static isSlot_(value, remoteFields) { const Parser = shaka.ads.SvtaInterstitialParser; // Only what playback and measurement use is validated. Verifications // are not used. const valid = !!value && (value['type'] == null || value['type'] == 'linear') && Parser.isTime_(value['start']) && value['duration'] > 0 && Parser.isTime_(value['duration']) && Parser.isArray_(value['identifiers'], (id) => !!id) && (value['skipOffset'] == null || Parser.isTime_(value['skipOffset'])) && (value['clickThrough'] == null || typeof value['clickThrough'] == 'string') && Parser.isArray_(value['tracking'], Parser.isTrackingEvent_) && Parser.isRemote_(value['$remote'], remoteFields); if (valid) { // Defaults to an empty array, so users of the slot need no checks. value['identifiers'] = value['identifiers'] || []; } return valid; } /** * @param {?Object} value * @param {boolean} remoteFields * @return {boolean} * @private */ static isPod_(value, remoteFields) { const Parser = shaka.ads.SvtaInterstitialParser; const valid = !!value && value['duration'] > 0 && Parser.isTime_(value['duration']) && (value['start'] == null || Parser.isTime_(value['start'])) && Parser.isArray_(value['slots'], (slot) => Parser.isSlot_(slot, remoteFields)) && Parser.isArray_(value['tracking'], Parser.isTrackingEvent_) && Parser.isRemote_(value['$remote'], remoteFields); if (valid) { // Defaults to an empty array, so users of the pod need no checks. // Remote slots, if any, are resolved later. value['slots'] = value['slots'] || []; } return valid; } /** * @param {?Object} value * @return {boolean} * @private */ static isTrackingEvent_(value) { return !!value && typeof value['type'] == 'string' && shaka.ads.SvtaInterstitialParser.isArray_(value['urls'], (url) => typeof url == 'string') && (value['offset'] == null || shaka.ads.SvtaInterstitialParser.isTime_(value['offset'])); } /** * Remote fields are URLs, only allowed when the envelope declares them. * * @param {?Object} value * @param {boolean} enabled * @return {boolean} * @private */ static isRemote_(value, enabled) { return value == null || (enabled && typeof value == 'object' && Object.keys(value).every((key) => typeof value[key] == 'string')); } /** * @param {*} value * @return {boolean} * @private */ static isTime_(value) { return typeof value == 'number' && Number.isFinite(value) && value >= 0; } /** * @param {*} value * @param {function(?Object):boolean} validate * @return {boolean} * @private */ static isArray_(value, validate) { return value == null || (Array.isArray(value) && value.every(validate)); } }; /** * The HLS attribute, and asset list field, that carries the signaling. * * @const {string} * @noinline */ shaka.ads.SvtaInterstitialParser.KEY = 'X-AD-CREATIVE-SIGNALING'; /** * @typedef {{ * interstitials: !Array<shaka.extern.AdInterstitial>, * deferred: !Array<!shaka.extern.DeferredInterstitial>, * }} * * @property {!Array<shaka.extern.AdInterstitial>} interstitials * Interstitials ready to be scheduled. * @property {!Array<!shaka.extern.DeferredInterstitial>} deferred * Pods whose slots must be resolved before they can be scheduled. */ shaka.ads.SvtaInterstitialParser.Result;