/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.util.CmcdManager');
goog.require('goog.asserts');
goog.require('cml.cmcd.CMCD_EVENT_KEYS');
goog.require('cml.cmcd.CMCD_HEADERS');
goog.require('cml.cmcd.CMCD_QUERY');
goog.require('cml.cmcd.CMCD_REQUEST_KEYS');
goog.require('cml.cmcd.CMCD_REQUEST_MODE');
goog.require('cml.cmcd.CMCD_RESPONSE_KEYS');
goog.require('cml.cmcd.CMCD_V1');
goog.require('cml.cmcd.CMCD_V1_KEYS');
goog.require('cml.cmcd.CMCD_V2');
goog.require('cml.cmcd.CmcdEventType');
goog.require('cml.cmcd.CmcdObjectType');
goog.require('cml.cmcd.CmcdPlayerState');
goog.require('cml.cmcd.CmcdReporter');
goog.require('cml.cmcd.CmcdStreamType');
goog.require('cml.cmcd.CmcdStreamingFormat');
goog.require('cml.cmcd.appendCmcdQuery');
goog.require('cml.cmcd.isCmcdCustomKey');
goog.require('shaka.log');
goog.require('shaka.net.NetworkingEngine');
goog.require('shaka.util.ArrayUtils');
goog.require('shaka.util.EventManager');
goog.require('shaka.util.StringUtils');
goog.requireType('cml.cmcd.Cmcd');
goog.requireType('cml.cmcd.CmcdEncodeOptions');
goog.requireType('cml.cmcd.CmcdReporterConfig');
goog.requireType('shaka.media.SegmentReference');
goog.requireType('shaka.Player');
/**
* @summary
* Thin shaka adapter around `cml.cmcd.CmcdReporter`. The vendored
* reporter owns CMCD state, encoding, key filtering, sequence numbers,
* and event-mode dispatch. The adapter translates shaka's player /
* `<video>` events into reporter calls, and shaka's request/response
* shapes into CML's HttpRequest/HttpResponse shapes.
*
* Adapter responsibilities:
* 1. Lifecycle: construct/start/stop CmcdReporter from configure() /
* onLoad() / reset()
* 2. Player wiring: <video> + Player events → reporter.update.
* State-change events (PLAY_STATE/BITRATE_CHANGE/BACKGROUNDED_MODE)
* auto-fire from update() since v2.4.0; adapter only invokes
* recordEvent() for non-state-change events (MUTE/UNMUTE/ERROR/etc.).
* 3. Request mode: applyRequestData → reporter.createRequestReport
* 4. Response mode: applyResponseData → reporter.recordResponseReceived
* 5. Event mode: requester callback → NetworkingEngine.request
* 6. Config translation: shaka.extern.CmcdConfiguration → CmcdReporterConfig
* 7. Public-API back-compat: re-exports for StreamingFormat / EventType /
* PlayerState
*/
shaka.util.CmcdManager = class {
/**
* @param {shaka.Player} player
* @param {shaka.extern.CmcdConfiguration} config
*/
constructor(player, config) {
/** @private {?shaka.Player} */
this.player_ = player;
/** @private {?shaka.extern.CmcdConfiguration} */
this.config_ = config;
/**
* CMCD parameters signaled by the manifest, or null.
*
* @private {?shaka.extern.ClientDataReporting}
*/
this.manifestParams_ = null;
/**
* Session id generated when neither the app nor the manifest supplies
* one. Kept across reporter rebuilds; cleared by reset() so each
* playback session gets a fresh id.
*
* @private {?string}
*/
this.generatedSessionId_ = null;
/**
* The configuration actually in effect: app config merged with manifest
* parameters.
*
* @private {!shaka.util.CmcdManager.EffectiveConfig}
*/
this.effective_ = this.resolveEffectiveConfig_();
/** @private {HTMLMediaElement} */
this.video_ = null;
/** @private {?cml.cmcd.CmcdReporter} */
this.reporter_ = null;
/** @private {?string} */
this.lastPlayerState_ = null;
/**
* Streaming format set once at manifest-load time. CTA-5004 / CTA-5004-B
* define `sf` as a stable manifest-type indicator (`'d'`/`'h'`/`'s'`/
* `'o'`); the value does not reflect the low-latency state.
*
* @private {(cml.cmcd.CmcdStreamingFormat|undefined)}
*/
this.sf_ = undefined;
/**
* Per-request `ts` values for TTFB/TTLB derivation in
* `applyResponseData`. CmcdReporter cannot observe request-send time;
* the adapter measures it.
*
* @private {!Map<!shaka.extern.Request, number>}
*/
this.requestTimestampMap_ = new Map();
/** @private {shaka.util.EventManager} */
this.eventManager_ = new shaka.util.EventManager();
/** @private {number} */
this.startTimeOfLoad_ = 0;
}
/**
* Set the media element and start the reporter (if enabled).
*
* @param {HTMLMediaElement} mediaElement The video element
*/
setMediaElement(mediaElement) {
this.video_ = mediaElement;
this.maybeStartReporter_();
}
/**
* Update the application configuration. If the effective configuration
* changes materially, the reporter is torn down and recreated.
*
* @param {shaka.extern.CmcdConfiguration} config
*/
configure(config) {
this.config_ = config;
this.applyEffectiveConfig_();
}
/**
* Apply CMCD parameters signaled by the manifest (DASH
* ServiceDescription/ClientDataReporting, ISO/IEC 23009-1:2026 Annex K).
* Pass null when the manifest carries none. Honored only while
* `applyParametersFromManifest` is enabled in the app configuration.
*
* @param {?shaka.extern.ClientDataReporting} params
*/
setManifestParameters(params) {
this.manifestParams_ = params;
this.applyEffectiveConfig_();
}
/**
* Reset the manager at the end of a playback session. Stops the reporter
* and clears all session-scoped state, including manifest parameters and
* the generated session id. The video element reference is preserved
* (shaka keeps it attached across unload()/load(); only detach() releases
* it), so onLoad() can re-arm the reporter for the next session.
*/
reset() {
this.stopReporter_();
this.manifestParams_ = null;
this.generatedSessionId_ = null;
this.lastPlayerState_ = null;
this.sf_ = undefined;
this.startTimeOfLoad_ = 0;
this.effective_ = this.resolveEffectiveConfig_();
}
/**
* Recompute the effective configuration and rebuild the reporter when a
* material field changed.
*
* @private
*/
applyEffectiveConfig_() {
const previous = this.effective_;
this.effective_ = this.resolveEffectiveConfig_();
if (this.reporter_ &&
shaka.util.CmcdManager.isMaterialChange_(previous, this.effective_)) {
this.stopReporter_();
}
this.maybeStartReporter_();
}
/**
* @return {!shaka.util.CmcdManager.EffectiveConfig}
* @private
*/
resolveEffectiveConfig_() {
return shaka.util.CmcdManager.resolveEffectiveConfig(
/** @type {!shaka.extern.CmcdConfiguration} */ (this.config_),
this.manifestParams_, this.generatedSessionId_);
}
/**
* Stop the reporter and drop request-scoped state. Session-scoped state
* that a rebuild must carry over (manifest parameters, the generated
* session id, the learned `sf`, the last player state and the start time
* of load) is kept, so that a rebuild triggered mid-load does not lose
* the pending `msd` measurement.
*
* @private
*/
stopReporter_() {
if (this.reporter_) {
this.reporter_.stop(true);
this.reporter_ = null;
}
this.requestTimestampMap_.clear();
this.eventManager_.removeAll();
}
/**
* Re-arm the reporter for a new playback session. The Player calls
* this at the start of every `load()`: each `load()` after the first
* is preceded by an unload that stops the reporter via `reset()`, and
* neither `setMediaElement()` (the element stays attached) nor
* `configure()` runs again on that path, so without this hook CMCD
* would stay silent for every load after the first
* (https://github.com/shaka-project/shaka-player/issues/10414).
*
* No-ops when a reporter is already running, when CMCD is disabled,
* or when no media element is attached.
*/
onLoad() {
this.maybeStartReporter_();
}
/**
* Forwarded from Player buffering observer; translates to a
* REBUFFERING / PLAYING player-state transition.
*
* @param {boolean} buffering
*/
setBuffering(buffering) {
this.setPlayerState_(buffering ?
cml.cmcd.CmcdPlayerState.REBUFFERING :
cml.cmcd.CmcdPlayerState.PLAYING);
}
/**
* No-op shim retained for Player call-site back-compat. Phase 1
* dropped the non-spec `'ld'`/`'lh'` StreamingFormat values; CTA-5004
* does not define LL-specific values, so low-latency content emits
* `sf=d` (DASH) or `sf=h` (HLS).
*
* @param {boolean} lowLatency
*/
setLowLatency(lowLatency) {}
/**
* Set start time of load; trigger autoplay-driven start if applicable.
*
* @param {number} startTimeOfLoad
*/
setStartTimeOfLoad(startTimeOfLoad) {
if (!this.reporter_) {
return;
}
this.startTimeOfLoad_ = startTimeOfLoad;
if (this.video_ && this.video_.autoplay) {
const playResult = this.video_.play();
if (playResult) {
playResult.catch(() => {
this.startTimeOfLoad_ = 0;
});
}
}
}
/**
* Apply CMCD data to a request via the reporter.
*
* @param {!shaka.net.NetworkingEngine.RequestType} type
* @param {!shaka.extern.Request} request
* @param {shaka.extern.RequestContext=} context
*/
applyRequestData(type, request, context = {}) {
const RequestType = shaka.net.NetworkingEngine.RequestType;
const ObjectType = cml.cmcd.CmcdObjectType;
// Learn the streaming format from every manifest request, even before a
// reporter exists (the manifest may be what enables CMCD) and even when
// includeInRequests excludes manifests from decoration: `sf` / `st` are
// session-scoped state the reporter needs for the requests it does
// decorate, not data carried by the manifest request itself.
if (type == RequestType.MANIFEST && context.type) {
const sf = this.getStreamFormat_(context.type);
if (sf && sf !== this.sf_) {
this.sf_ = sf;
if (this.reporter_) {
this.reporter_.update(/** @type {!cml.cmcd.Cmcd} */ (
{sf: sf, st: this.getStreamType_()}));
}
}
}
if (!this.reporter_) {
return;
}
// Track request initiation time for TTFB/TTLB derivation when the
// response comes back. Response reporting is not gated on
// includeInRequests, so the timestamp is recorded before that gate.
if (type == RequestType.SEGMENT) {
if (this.requestTimestampMap_.has(request)) {
this.requestTimestampMap_.delete(request);
}
this.requestTimestampMap_.set(request, Date.now());
}
if (!this.shouldApplyToRequest_(type, request, context)) {
return;
}
if (request.method === 'HEAD') {
this.applyToRequest_(request, {});
return;
}
switch (type) {
case RequestType.MANIFEST:
this.applyToRequest_(request, /** @type {!cml.cmcd.Cmcd} */ ({
ot: ObjectType.MANIFEST,
su: this.lastPlayerState_ === null,
}));
break;
case RequestType.SEGMENT: {
const data = this.getDataForSegment_(context, request.uris[0]);
this.applyToRequest_(request,
/** @type {!cml.cmcd.Cmcd} */ (data));
break;
}
case RequestType.LICENSE:
case RequestType.SERVER_CERTIFICATE:
case RequestType.KEY:
this.applyToRequest_(request, /** @type {!cml.cmcd.Cmcd} */ (
{ot: ObjectType.KEY}));
break;
case RequestType.TIMING:
this.applyToRequest_(request, /** @type {!cml.cmcd.Cmcd} */ (
{ot: ObjectType.OTHER}));
break;
case RequestType.CONTENT_STEERING:
case RequestType.EVENT_CALLBACK:
this.applyToRequest_(request, /** @type {!cml.cmcd.Cmcd} */ (
{ot: ObjectType.OTHER}));
break;
}
}
/**
* Apply CMCD data to a response.
*
* @param {!shaka.net.NetworkingEngine.RequestType} type
* @param {!shaka.extern.Response} response
* @param {shaka.extern.RequestContext=} context
*/
applyResponseData(type, response, context = {}) {
if (!this.reporter_) {
return;
}
const RequestType = shaka.net.NetworkingEngine.RequestType;
if (type !== RequestType.SEGMENT) {
return;
}
// Always clean up the per-request timestamp entry to prevent unbounded
// map growth when response-mode reporting is disabled (the default when
// eventTargets is empty). Mirrors the upstream fix in shaka-project#9193.
const startTs = this.requestTimestampMap_.get(response.originalRequest);
this.requestTimestampMap_.delete(response.originalRequest);
if (!this.responseModeEnabled_()) {
return;
}
try {
const url = this.removeCmcdQueryFromUri_(
response.originalUri || response.uri);
/** @type {!cml.cmcd.Cmcd} */
const data = /** @type {!cml.cmcd.Cmcd} */ ({url: url});
if (response.status != null) {
data.rc = response.status;
}
if (response.originalRequest &&
response.originalRequest.timeToFirstByte != null) {
data.ttfb = response.originalRequest.timeToFirstByte;
}
if (response.timeMs != null) {
data.ttlb = response.timeMs;
}
if (startTs != null) {
data.ts = startTs;
}
if (response.headers && response.headers['CMSD-Static']) {
data.cmsds = btoa(response.headers['CMSD-Static']);
}
if (response.headers && response.headers['CMSD-Dynamic']) {
data.cmsdd = btoa(response.headers['CMSD-Dynamic']);
}
// Synthesize a CML-shaped HttpResponse: CmcdReporter reads
// `request.url` and `status`. We pass `data` as the override so
// CML's auto-derivation from `resourceTiming` is bypassed
// (shaka measures these via `requestTimestampMap_`).
this.reporter_.recordResponseReceived({
request: {url: url},
status: response.status || 0,
}, data);
} catch (error) {
shaka.log.warnOnce(
'CMCD_RESPONSE_ERROR',
'Could not apply CMCD data to response.', error);
}
}
/**
* Apply CMCD data to a sidecar text request.
*
* @param {!shaka.extern.Request} request
*/
applyTextData(request) {
if (!this.reporter_ || !this.isTokenAllowed_('segment')) {
return;
}
this.applyToRequest_(request, /** @type {!cml.cmcd.Cmcd} */ ({
ot: cml.cmcd.CmcdObjectType.CAPTION,
su: true,
}));
}
/**
* Apply CMCD data to streams loaded via `<video src=...>`. Forces
* query-mode encoding regardless of configured `useHeaders`, since
* direct media loads cannot carry custom request headers.
*
* @param {string} uri
* @param {string} mimeType
* @return {string}
*/
appendSrcData(uri, mimeType) {
if (!this.reporter_ || !this.isTokenAllowed_('segment')) {
return uri;
}
try {
const data = this.buildExternalUriData_();
data.ot = this.getObjectTypeFromMimeType_(mimeType);
data.su = true;
return cml.cmcd.appendCmcdQuery(
uri, data, this.encodeOptionsForUri_(uri));
} catch (error) {
shaka.log.warnOnce('CMCD_SRC_ERROR',
'Could not generate src CMCD data.', error);
return uri;
}
}
/**
* Apply CMCD data to a sidecar text track URI.
*
* @param {string} uri
* @return {string}
*/
appendTextTrackData(uri) {
if (!this.reporter_ || !this.isTokenAllowed_('segment')) {
return uri;
}
try {
const data = this.buildExternalUriData_();
data.ot = cml.cmcd.CmcdObjectType.CAPTION;
data.su = true;
return cml.cmcd.appendCmcdQuery(
uri, data, this.encodeOptionsForUri_(uri));
} catch (error) {
shaka.log.warnOnce('CMCD_TEXT_TRACK_ERROR',
'Could not generate text track CMCD data.', error);
return uri;
}
}
/**
* Construct and start the reporter, and wire up its event listeners, if
* the effective configuration is enabled and a video element is
* available. No-ops if the reporter is already running or either
* precondition fails.
*
* @private
*/
maybeStartReporter_() {
if (this.reporter_ || !this.video_ || !this.effective_.enabled) {
return;
}
if (!this.effective_.sessionId) {
this.generatedSessionId_ = window.crypto.randomUUID();
this.effective_ = this.resolveEffectiveConfig_();
}
const reporterConfig = this.toReporterConfig_(this.effective_);
this.reporter_ = new cml.cmcd.CmcdReporter(
reporterConfig, this.makeRequester_());
this.reporter_.start();
// A rebuild happens after the manifest request that teaches sf and
// after playback may have started, so re-teach what the previous
// reporter had learned.
if (this.sf_) {
this.reporter_.update(/** @type {!cml.cmcd.Cmcd} */ (
{sf: this.sf_, st: this.getStreamType_()}));
}
if (this.lastPlayerState_) {
this.reporter_.update(/** @type {!cml.cmcd.Cmcd} */ (
{sta: this.lastPlayerState_}));
}
this.setupEventListeners_();
}
/**
* Translate the effective configuration into
* `cml.cmcd.CmcdReporterConfig`. Field renames:
* - `useHeaders` → `transmissionMode` enum
* - `version: 1|2` → `CMCD_V1` / `CMCD_V2` constants
* - per-target `includeKeys` → `enabledKeys`
*
* Empty `includeKeys` is expanded to all valid keys for the version,
* since CML's reporter early-returns when `enabledKeys` is empty.
*
* @param {!shaka.util.CmcdManager.EffectiveConfig} cfg
* @return {!cml.cmcd.CmcdReporterConfig}
* @private
*/
toReporterConfig_(cfg) {
const allKeysForVersion = shaka.util.CmcdManager.allKeysForVersion_;
const includeKeys = (cfg.includeKeys && cfg.includeKeys.length) ?
cfg.includeKeys : allKeysForVersion(cfg.version);
/** @type {!cml.cmcd.CmcdReporterConfig} */
const reporterConfig = /** @type {!cml.cmcd.CmcdReporterConfig} */ ({
sid: cfg.sessionId,
cid: cfg.contentId,
transmissionMode: cfg.useHeaders ?
cml.cmcd.CMCD_HEADERS : cml.cmcd.CMCD_QUERY,
enabledKeys: includeKeys,
version: cfg.version === cml.cmcd.CMCD_V2 ?
cml.cmcd.CMCD_V2 : cml.cmcd.CMCD_V1,
});
const targets = cfg.eventTargets;
if (targets && targets.length) {
reporterConfig.eventTargets = targets
.filter((t) => t.enabled !== false && t.url &&
t.events && t.events.length)
.map((t) => {
const tIncludeKeys = (t.includeKeys && t.includeKeys.length) ?
t.includeKeys :
allKeysForVersion(t.version || cfg.version);
return {
url: t.url,
events: t.events,
interval: t.interval,
batchSize: t.batchSize,
enabledKeys: tIncludeKeys,
version: cml.cmcd.CMCD_V2,
};
});
}
return reporterConfig;
}
/**
* Build the `requester` callback CmcdReporter uses for event-mode
* dispatch. Routes through NetworkingEngine so the request inherits
* auth / retry / filters.
*
* @return {function(!Object): !Promise<{status: number}>}
* @private
*/
makeRequester_() {
return async (cmcdReq) => {
const RequestType = shaka.net.NetworkingEngine.RequestType;
const retryParams =
shaka.net.NetworkingEngine.defaultRetryParameters();
const url = /** @type {string} */ (cmcdReq['url']);
const shakaReq =
shaka.net.NetworkingEngine.makeRequest([url], retryParams);
shakaReq.method =
/** @type {string} */ (cmcdReq['method']) || 'POST';
shakaReq.headers = /** @type {!Object<string, string>} */ (
cmcdReq['headers']) || {};
shakaReq.body = shaka.util.StringUtils.toUTF8(
/** @type {string} */ (cmcdReq['body']));
try {
const networkingEngine = this.player_.getNetworkingEngine();
goog.asserts.assert(networkingEngine, 'Must have net engine');
await networkingEngine.request(
RequestType.CMCD, shakaReq).promise;
return {status: 200};
} catch (err) {
return {
status: /** @type {number} */ (
(err && err['status']) ? err['status'] : 0),
};
}
};
}
/**
* Player-state deduplication: skip the update + recordEvent pair if
* the new state is identical to the last-emitted one. Without this,
* `<video>` events can fire repeatedly on stable states (e.g.,
* `playing` after every minor stall) and spam the reporter.
*
* @param {!cml.cmcd.CmcdPlayerState} state
* @private
*/
setPlayerState_(state) {
if (!this.reporter_ || this.lastPlayerState_ === state) {
return;
}
this.lastPlayerState_ = state;
// CmcdReporter.update() auto-fires PLAY_STATE since v2.4.0
// when sta changes vs. lastEmitted.
this.reporter_.update(/** @type {!cml.cmcd.Cmcd} */ ({sta: state}));
}
/**
* Push a CMCD-applied request through `createRequestReport`. Mutates
* `request` in place to preserve shaka's existing applyRequestData
* contract.
*
* @param {!shaka.extern.Request} request
* @param {!cml.cmcd.Cmcd} data
* @private
*/
applyToRequest_(request, data) {
try {
/** @type {!Object<string, *>} */
const cmlReq = {
url: request.uris[0],
headers: Object.assign({}, request.headers || {}),
customData: {},
};
const report = /** @type {!Object<string, *>} */ (
this.reporter_.createRequestReport(cmlReq, data));
// Headers mode: copy CMCD-* headers back onto the request.
const reportHeaders = /** @type {!Object<string, string>} */ (
report['headers'] || {});
const inputHeaderCount =
Object.keys(cmlReq.headers).length;
const newHeaderCount = Object.keys(reportHeaders).length;
if (newHeaderCount > inputHeaderCount) {
if (!request.headers) {
request.headers = {};
}
for (const key of Object.keys(reportHeaders)) {
if (!(key in cmlReq.headers)) {
request.headers[key] = reportHeaders[key];
}
}
}
// Query mode: report.url has CMCD appended. Apply to all URIs in
// the retry list (alternates carry identical CMCD).
const reportUrl = /** @type {string} */ (report['url']);
if (reportUrl && reportUrl !== cmlReq.url) {
if (request.uris.length === 1) {
request.uris = [reportUrl];
} else {
let cmcdParam = null;
try {
const u = new URL(reportUrl);
cmcdParam = u.searchParams.get('CMCD');
} catch (e) {
// First URI not parsable; leave list untouched.
}
if (cmcdParam != null) {
const param = cmcdParam;
request.uris = request.uris.map((uri) => {
if (uri.includes('offline:')) {
return uri;
}
try {
const uu = new URL(uri);
uu.searchParams.set('CMCD', param);
return uu.toString();
} catch (e) {
return uri;
}
});
}
}
}
} catch (error) {
shaka.log.warnOnce('CMCD_REQUEST_ERROR',
'Could not apply CMCD data to request.', error);
}
}
/**
* Whether CMCD data should be attached to this request, per the effective
* includeInRequests list (ISO/IEC 23009-1:2026 Table I.4 tokens).
*
* @param {!shaka.net.NetworkingEngine.RequestType} type
* @param {!shaka.extern.Request} request
* @param {!shaka.extern.RequestContext} context
* @return {boolean}
* @private
*/
shouldApplyToRequest_(type, request, context) {
const token = shaka.util.CmcdManager.requestTypeToken_(type, context);
if (!token || !this.isTokenAllowed_(token)) {
return false;
}
return this.passesManifestFilters_(type, request, context);
}
/**
* Apply the ClientDataReporting serviceLocations and adaptationSets
* filters (ISO/IEC 23009-1:2026 Table K.7). Both filters intersect. A
* request whose service location cannot be resolved is excluded on
* purpose: header-mode CMCD toward a CDN that did not ask for it can
* trigger CORS preflight failures. Steering requests are not CDN requests
* and bypass the service location filter (dash.js does the same). The
* adaptationSets filter applies only to requests that carry a stream.
*
* @param {!shaka.net.NetworkingEngine.RequestType} type
* @param {!shaka.extern.Request} request
* @param {!shaka.extern.RequestContext} context
* @return {boolean}
* @private
*/
passesManifestFilters_(type, request, context) {
const RequestType = shaka.net.NetworkingEngine.RequestType;
const effective = this.effective_;
if (effective.serviceLocations &&
type != RequestType.CONTENT_STEERING) {
const location = shaka.util.CmcdManager.resolveServiceLocation_(
request.uris[0], effective.serviceLocationBaseUris);
if (!location) {
shaka.log.warnOnce('CMCD_UNKNOWN_SERVICE_LOCATION',
'CMCD: the manifest restricts reporting to service locations ' +
effective.serviceLocations.join(' ') + ' but the location of ' +
'some requests cannot be resolved; they are not decorated.');
return false;
}
if (!effective.serviceLocations.includes(location)) {
return false;
}
}
if (effective.adaptationSets && context.stream) {
// For DASH, Stream.groupId is the AdaptationSet id, with a
// '_preselection_<id>' suffix on Preselection copies.
const groupId = context.stream.groupId;
if (!groupId) {
shaka.log.warnOnce('CMCD_NO_GROUP_ID',
'CMCD: the manifest restricts reporting to adaptation sets ' +
effective.adaptationSets.join(' ') + ' but some streams have ' +
'no group id; they are not decorated. For DASH this happens ' +
'when dash.enableAudioGroups is false.');
return false;
}
const matches = effective.adaptationSets.some((id) =>
groupId == id || groupId.startsWith(id + '_preselection_'));
if (!matches) {
return false;
}
}
return true;
}
/**
* An empty list means different things depending on where it came from. A
* manifest-supplied list is authoritative even when empty: it named only
* request types this player never makes, so nothing is decorated. An empty
* application list is just the unconfigured default, and falls back to the
* legacy request set.
*
* @param {string} token A Table I.4 request type token, or one of the
* internal tokens 'license' / 'timing'.
* @return {boolean}
* @private
*/
isTokenAllowed_(token) {
const list = this.effective_.includeInRequests;
let allowed = list;
if (!list.length) {
if (this.effective_.fromManifest) {
return false;
}
allowed = shaka.util.CmcdManager.LEGACY_REQUEST_TOKENS_;
}
if (allowed.includes('*')) {
return true;
}
if (token == 'init' && allowed.includes('segment')) {
// 'segment' covers initialization segments too (dash.js parity, and
// CTA-5004 expects CMCD on init files).
return true;
}
return allowed.includes(token);
}
/**
* Whether any configured `eventTarget` subscribes to the `'rr'`
* (response-received) event. Saves recording response-received calls
* into the reporter when no target consumes them.
*
* @return {boolean}
* @private
*/
responseModeEnabled_() {
const targets = this.effective_.eventTargets;
if (!targets) {
return false;
}
return targets.some((target) =>
target.enabled !== false &&
target.events &&
target.events.includes(cml.cmcd.CmcdEventType.RESPONSE_RECEIVED));
}
/**
* Persistent CMCD baseline used by `appendSrcData` /
* `appendTextTrackData`. These bypass the reporter (browsers can't
* observe headers for direct media loads, so we always force query
* mode regardless of configured `useHeaders`); `sn` is omitted since
* the reporter's per-target counters don't apply.
*
* @return {!cml.cmcd.Cmcd}
* @private
*/
buildExternalUriData_() {
return /** @type {!cml.cmcd.Cmcd} */ ({
v: this.effective_.version,
sid: this.effective_.sessionId,
cid: this.effective_.contentId,
sf: this.sf_,
mtp: this.player_.getBandwidthEstimate() / 1000,
});
}
/**
* Encode options for direct `cml.cmcd.appendCmcdQuery` calls
* (`appendSrcData`, `appendTextTrackData`). `baseUrl` lets CML
* relativize `nor` URLs root-relative; `version` filters keys per
* spec; `reportingMode = REQUEST` pins the request-mode key filter.
*
* @param {string} uri
* @return {!cml.cmcd.CmcdEncodeOptions}
* @private
*/
encodeOptionsForUri_(uri) {
/** @type {!cml.cmcd.CmcdEncodeOptions} */
const options = /** @type {!cml.cmcd.CmcdEncodeOptions} */ ({});
if (uri && !uri.includes('offline:')) {
try {
options.baseUrl = new URL(uri).origin;
} catch (e) {
// Unparsable URI; CML's `nor` formatter passes values through
// unchanged when baseUrl is absent.
}
}
if (this.effective_.version != null) {
options.version = this.effective_.version;
}
options.reportingMode = cml.cmcd.CMCD_REQUEST_MODE;
return options;
}
/**
* Wire `<video>` and Player events to reporter calls per spec
* § "Player state ↔ reporter state mapping".
*
* @private
*/
setupEventListeners_() {
const PlayerState = cml.cmcd.CmcdPlayerState;
const EventType = cml.cmcd.CmcdEventType;
this.eventManager_.listen(this.video_, 'pause', () => {
this.setPlayerState_(PlayerState.PAUSED);
});
this.eventManager_.listen(this.video_, 'playing', () => {
this.setPlayerState_(PlayerState.PLAYING);
// Compute and emit Measured Start Delay (msd) on the first 'playing'
// event after setStartTimeOfLoad() has been called. CmcdReporter
// sends msd once per target on the first request; zeroing
// startTimeOfLoad_ here prevents re-emission on subsequent 'playing'
// events within the same session.
if (this.reporter_ && this.startTimeOfLoad_) {
this.reporter_.update(/** @type {!cml.cmcd.Cmcd} */ (
{msd: Date.now() - this.startTimeOfLoad_}));
this.startTimeOfLoad_ = 0;
}
});
this.eventManager_.listen(this.video_, 'seeking', () => {
this.setPlayerState_(PlayerState.SEEKING);
});
this.eventManager_.listen(this.video_, 'ended', () => {
this.setPlayerState_(PlayerState.ENDED);
});
this.eventManager_.listen(this.video_, 'volumechange', () => {
if (!this.reporter_ || !this.video_) {
return;
}
const muted = this.video_.muted;
// Mute state isn't a persistent CMCD wire key (CTA-5004-B uses
// m/um events, not a `muted` data field). Emit the event only.
this.reporter_.recordEvent(muted ? EventType.MUTE : EventType.UNMUTE);
});
this.eventManager_.listen(this.player_, 'buffering', (e) => {
const buffering = /** @type {!Object} */ (e)['buffering'];
this.setPlayerState_(buffering ?
PlayerState.REBUFFERING : PlayerState.PLAYING);
});
this.eventManager_.listen(this.player_, 'adaptation', () => {
if (!this.reporter_ || !this.player_) {
return;
}
const variants = this.player_.getVariantTracks();
const active = variants.find((t) => t.active);
if (active && active.bandwidth) {
// CmcdReporter.update() auto-fires BITRATE_CHANGE since v2.4.0
// when br changes vs. lastEmitted.
this.reporter_.update(/** @type {!cml.cmcd.Cmcd} */ (
{br: [active.bandwidth / 1000]}));
}
});
this.eventManager_.listen(this.player_, 'error', () => {
this.setPlayerState_(PlayerState.FATAL_ERROR);
if (this.reporter_) {
this.reporter_.recordEvent(EventType.ERROR);
}
});
this.eventManager_.listen(document, 'fullscreenchange', () => {
if (!this.reporter_) {
return;
}
const isFullscreen = !!document.fullscreenElement;
this.reporter_.recordEvent(isFullscreen ?
EventType.PLAYER_EXPAND : EventType.PLAYER_COLLAPSE);
});
const video = /** @type {HTMLVideoElement} */ (this.video_);
if (video.webkitPresentationMode || video.webkitSupportsFullscreen) {
this.eventManager_.listen(video, 'webkitpresentationmodechanged', () => {
if (!this.reporter_) {
return;
}
if (video.webkitPresentationMode) {
this.reporter_.recordEvent(
video.webkitPresentationMode !== 'inline' ?
EventType.PLAYER_EXPAND : EventType.PLAYER_COLLAPSE);
} else if (video.webkitSupportsFullscreen) {
this.reporter_.recordEvent(
video.webkitDisplayingFullscreen ?
EventType.PLAYER_EXPAND : EventType.PLAYER_COLLAPSE);
}
});
}
this.eventManager_.listen(this.video_, 'enterpictureinpicture', () => {
if (this.reporter_) {
this.reporter_.recordEvent(EventType.PLAYER_EXPAND);
}
});
this.eventManager_.listen(this.video_, 'leavepictureinpicture', () => {
if (this.reporter_) {
this.reporter_.recordEvent(EventType.PLAYER_COLLAPSE);
}
});
if ('documentPictureInPicture' in window) {
this.eventManager_.listen(window.documentPictureInPicture, 'enter',
(e) => {
if (!this.reporter_) {
return;
}
this.reporter_.recordEvent(EventType.PLAYER_EXPAND);
const event = /** @type {DocumentPictureInPictureEvent} */ (e);
const pipWindow = event.window;
this.eventManager_.listenOnce(pipWindow, 'pagehide', () => {
if (this.reporter_) {
this.reporter_.recordEvent(EventType.PLAYER_COLLAPSE);
}
});
});
}
this.eventManager_.listen(document, 'visibilitychange', () => {
if (!this.reporter_) {
return;
}
const backgrounded = document.hidden;
// CmcdReporter.update() auto-fires BACKGROUNDED_MODE since v2.4.0
// when bg changes vs. lastEmitted.
this.reporter_.update(/** @type {!cml.cmcd.Cmcd} */ (
{bg: backgrounded}));
});
this.eventManager_.listen(this.player_, 'complete', () => {
this.setPlayerState_(PlayerState.ENDED);
});
}
/**
* Strip the CMCD query parameter from a URL.
*
* @param {string} uri
* @return {string}
* @private
*/
removeCmcdQueryFromUri_(uri) {
if (!uri.includes('CMCD=')) {
return uri;
}
try {
const url = new URL(uri);
url.searchParams.delete('CMCD');
return url.toString();
} catch (error) {
shaka.log.error('Failed to parse URI for CMCD removal:', uri, error);
return uri;
}
}
/**
* Map a shaka request context to a CMCD object type.
*
* @param {shaka.extern.RequestContext} context
* @return {(cml.cmcd.CmcdObjectType|undefined)}
* @private
*/
getObjectType_(context) {
if (context.type ===
shaka.net.NetworkingEngine.AdvancedRequestType.INIT_SEGMENT) {
return cml.cmcd.CmcdObjectType.INIT;
}
const stream = context.stream;
if (!stream) {
return undefined;
}
const type = stream.type;
if (type == 'video') {
if (stream.codecs && stream.codecs.includes(',')) {
return cml.cmcd.CmcdObjectType.MUXED;
}
return cml.cmcd.CmcdObjectType.VIDEO;
}
if (type == 'audio') {
return cml.cmcd.CmcdObjectType.AUDIO;
}
if (type == 'text') {
if (stream.mimeType === 'application/mp4') {
return cml.cmcd.CmcdObjectType.TIMED_TEXT;
}
return cml.cmcd.CmcdObjectType.CAPTION;
}
return undefined;
}
/**
* Map a mimeType to a CMCD object type. Used by `appendSrcData`.
*
* @param {string} mimeType
* @return {(cml.cmcd.CmcdObjectType|undefined)}
* @private
*/
getObjectTypeFromMimeType_(mimeType) {
switch (mimeType.toLowerCase()) {
case 'audio/mp4':
case 'audio/webm':
case 'audio/ogg':
case 'audio/mpeg':
case 'audio/aac':
case 'audio/flac':
case 'audio/wav':
return cml.cmcd.CmcdObjectType.AUDIO;
case 'video/webm':
case 'video/mp4':
case 'video/mpeg':
case 'video/mp2t':
return cml.cmcd.CmcdObjectType.MUXED;
case 'application/x-mpegurl':
case 'application/vnd.apple.mpegurl':
case 'audio/x-mpegurl':
case 'application/dash+xml':
case 'video/vnd.mpeg.dash.mpd':
case 'application/vnd.ms-sstr+xml':
return cml.cmcd.CmcdObjectType.MANIFEST;
default:
return undefined;
}
}
/**
* Buffer length in milliseconds for a media type.
*
* @param {string} type
* @return {number}
* @private
*/
getBufferLength_(type) {
const ranges = this.player_.getBufferedInfo()[type];
if (!ranges.length) {
return NaN;
}
const start = this.getCurrentTime_();
const range = ranges.find((r) => r.start <= start && r.end >= start);
if (!range) {
return NaN;
}
return (range.end - start) * 1000;
}
/**
* Remaining buffer length in milliseconds.
*
* @param {string} type
* @return {number}
* @private
*/
getRemainingBufferLength_(type) {
const ranges = this.player_.getBufferedInfo()[type];
if (!ranges.length) {
return 0;
}
const start = this.getCurrentTime_();
const range = ranges.find((r) => r.start <= start && r.end >= start);
if (!range) {
return 0;
}
return (range.end - start) * 1000;
}
/**
* Computed `rtp` (requested maximum throughput) for a segment.
*
* @param {shaka.extern.Stream} stream
* @param {shaka.media.SegmentReference} segment
* @return {number}
* @private
*/
calculateRtp_(stream, segment) {
const playbackRate = this.player_.getPlaybackRate() || 1;
const currentBufferLevel =
this.getRemainingBufferLength_(stream.type) || 500;
const bandwidth = stream.bandwidth;
if (!bandwidth) {
return NaN;
}
const segmentDuration = segment.endTime - segment.startTime;
const segmentSize = bandwidth * segmentDuration / 1000;
const timeToLoad = (currentBufferLevel / playbackRate) / 1000;
const minBandwidth = segmentSize / timeToLoad;
return minBandwidth * this.effective_.rtpSafetyFactor;
}
/**
* Resolve `sf` from the manifest-parser advanced-request type.
*
* @param {shaka.net.NetworkingEngine.AdvancedRequestType} type
* @return {(cml.cmcd.CmcdStreamingFormat|undefined)}
* @private
*/
getStreamFormat_(type) {
const AdvancedRequestType = shaka.net.NetworkingEngine.AdvancedRequestType;
switch (type) {
case AdvancedRequestType.MPD:
return cml.cmcd.CmcdStreamingFormat.DASH;
case AdvancedRequestType.MASTER_PLAYLIST:
case AdvancedRequestType.MEDIA_PLAYLIST:
return cml.cmcd.CmcdStreamingFormat.HLS;
}
return undefined;
}
/**
* @return {cml.cmcd.CmcdStreamType}
* @private
*/
getStreamType_() {
return this.player_.isLive() ?
cml.cmcd.CmcdStreamType.LIVE :
cml.cmcd.CmcdStreamType.VOD;
}
/**
* Top-bandwidth value across variants for a given object type.
*
* @param {(cml.cmcd.CmcdObjectType|undefined)} type
* @return {number}
* @private
*/
getTopBandwidth_(type) {
const variants = this.player_.getVariantTracks();
if (!variants.length) {
return NaN;
}
let top = variants[0];
for (const variant of variants) {
if (variant.type === 'variant' && variant.bandwidth > top.bandwidth) {
top = variant;
}
}
const ObjectType = cml.cmcd.CmcdObjectType;
switch (type) {
case ObjectType.VIDEO:
return top.videoBandwidth || NaN;
case ObjectType.AUDIO:
return top.audioBandwidth || NaN;
default:
return top.bandwidth;
}
}
/**
* Build the per-segment CMCD payload from shaka's request context.
*
* @param {shaka.extern.RequestContext} context
* @param {?string} requestUri
* @return {!Object<string, *>}
* @private
*/
getDataForSegment_(context, requestUri) {
const segment = context.segment;
let duration = 0;
if (segment) {
duration = segment.endTime - segment.startTime;
}
/** @type {!Object<string, *>} */
const data = {
d: duration * 1000,
st: this.getStreamType_(),
};
data['ot'] = this.getObjectType_(context);
const ObjectType = cml.cmcd.CmcdObjectType;
const isMedia = data['ot'] === ObjectType.VIDEO ||
data['ot'] === ObjectType.AUDIO ||
data['ot'] === ObjectType.MUXED ||
data['ot'] === ObjectType.TIMED_TEXT;
const stream = context.stream;
if (stream) {
const playbackRate = this.player_.getPlaybackRate();
if (isMedia) {
const bl = this.getBufferLength_(stream.type);
if (Number.isFinite(bl)) {
data['bl'] = bl;
}
if (data['ot'] !== ObjectType.TIMED_TEXT) {
const remainingBufferLength =
this.getRemainingBufferLength_(stream.type);
if (playbackRate) {
data['dl'] = remainingBufferLength / Math.abs(playbackRate);
} else {
data['dl'] = remainingBufferLength;
}
}
}
if (stream.bandwidth) {
data['br'] = stream.bandwidth / 1000;
}
if (stream.segmentIndex && segment) {
const reverse = playbackRate < 0;
const iterator = stream.segmentIndex.getIteratorForTime(
segment.endTime, /* allowNonIndependent= */ true, reverse);
if (iterator) {
const nextSegment = iterator.next().value;
if (nextSegment && nextSegment != segment) {
if (requestUri && !shaka.util.ArrayUtils.equal(
segment.getUris(), nextSegment.getUris())) {
data['nor'] = nextSegment.getUris()[0];
}
if ((nextSegment.startByte || nextSegment.endByte) &&
(segment.startByte != nextSegment.startByte ||
segment.endByte != nextSegment.endByte)) {
let range = nextSegment.startByte + '-';
if (nextSegment.endByte) {
range += nextSegment.endByte;
}
data['nrr'] = range;
}
}
}
const rtp = this.calculateRtp_(stream, segment);
if (Number.isFinite(rtp)) {
data['rtp'] = rtp;
}
}
}
if (isMedia && data['ot'] !== ObjectType.TIMED_TEXT) {
const tb = this.getTopBandwidth_(
/** @type {(cml.cmcd.CmcdObjectType|undefined)} */ (
data['ot'])) / 1000;
if (Number.isFinite(tb)) {
data['tb'] = tb;
}
}
if (this.player_.isLive()) {
const liveLatency = this.player_.getLiveLatency();
if (liveLatency) {
data['ltc'] = liveLatency;
}
}
// Set the startup flag: true when the player has not yet transitioned
// to a playing state (lastPlayerState_ is null = initial buffering) or
// is actively rebuffering. Mirrors pre-refactor: `data.su = buffering_`.
data['su'] = this.lastPlayerState_ === null ||
this.lastPlayerState_ === cml.cmcd.CmcdPlayerState.REBUFFERING;
return data;
}
/**
* @return {number}
* @private
*/
getCurrentTime_() {
return this.video_ ? this.video_.currentTime : 0;
}
};
/**
* The configuration actually in effect after merging the application
* configuration with manifest-signaled parameters.
*
* @typedef {{
* enabled: boolean,
* version: number,
* useHeaders: boolean,
* sessionId: string,
* contentId: string,
* rtpSafetyFactor: number,
* includeKeys: !Array<string>,
* includeInRequests: !Array<string>,
* eventTargets: ?Array<shaka.extern.CmcdTarget>,
* serviceLocations: ?Array<string>,
* adaptationSets: ?Array<string>,
* serviceLocationBaseUris: !Array<shaka.extern.ServiceLocationBaseUri>,
* fromManifest: boolean
* }}
*/
shaka.util.CmcdManager.EffectiveConfig;
/**
* Merge the application configuration with manifest-signaled CMCD
* parameters (ISO/IEC 23009-1:2026 Annex K). Manifest values win for the
* fields the manifest can express (version, mode, keys, includeInRequests,
* contentID, sessionID) and the presence of parameters enables reporting.
* Attributes the manifest omitted already carry the spec defaults, except
* keys / ids, which fall back to the application values. Everything else
* comes from the application. Does not mutate its arguments or any manager
* state.
*
* @param {!shaka.extern.CmcdConfiguration} config
* @param {?shaka.extern.ClientDataReporting} manifestParams
* @param {?string} generatedSessionId
* @return {!shaka.util.CmcdManager.EffectiveConfig}
*/
shaka.util.CmcdManager.resolveEffectiveConfig = (
config, manifestParams, generatedSessionId) => {
const applyManifest = config.applyParametersFromManifest !== false;
const reporting = (applyManifest && manifestParams &&
manifestParams.cmcdParameters) ? manifestParams : null;
const mpd = reporting ? reporting.cmcdParameters : null;
const version = mpd ? mpd.version : config.version;
let includeKeys = config.includeKeys || [];
if (mpd && mpd.keys) {
const filtered = shaka.util.CmcdManager.filterSupportedKeys_(
mpd.keys, version);
if (filtered.length) {
includeKeys = filtered;
} else {
// Keeping the empty list would expand to every key for the version in
// toReporterConfig_, which inverts Table K.8: keys the player does not
// support are ignored, they do not select everything.
shaka.log.warnOnce('CMCD_NO_SUPPORTED_MANIFEST_KEYS',
'CMCD: the manifest lists no key this player supports at version ' +
version + '; using the application keys instead.');
}
}
return {
enabled: !!mpd || config.enabled,
version: version,
useHeaders: mpd ? mpd.mode == 'header' : config.useHeaders,
sessionId: (mpd && mpd.sessionId) || config.sessionId ||
generatedSessionId || '',
contentId: (mpd && mpd.contentId) || config.contentId || '',
rtpSafetyFactor: config.rtpSafetyFactor,
includeKeys: includeKeys,
includeInRequests: mpd ?
mpd.includeInRequests : (config.includeInRequests || []),
eventTargets: config.eventTargets,
serviceLocations: reporting ? reporting.serviceLocations : null,
adaptationSets: reporting ? reporting.adaptationSets : null,
serviceLocationBaseUris: reporting ?
reporting.serviceLocationBaseUris : [],
fromManifest: !!mpd,
};
};
/**
* Keep only the manifest keys this player can emit for the given version:
* keys CML knows for that version plus custom keys (hyphenated prefix).
* Unsupported keys are ignored per Table K.8.
*
* @param {!Array<string>} keys
* @param {number} version
* @return {!Array<string>}
* @private
*/
shaka.util.CmcdManager.filterSupportedKeys_ = (keys, version) => {
const supported = new Set(
shaka.util.CmcdManager.allKeysForVersion_(version));
const kept = keys.filter((key) =>
supported.has(key) || cml.cmcd.isCmcdCustomKey(key));
if (kept.length != keys.length) {
shaka.log.info('CMCD: ignoring manifest keys this player does not ' +
'support:', keys.filter((key) => !kept.includes(key)));
}
return kept;
};
/**
* @param {(number|undefined)} version
* @return {!Array<string>}
* @private
*/
shaka.util.CmcdManager.allKeysForVersion_ = (version) => {
if (version === cml.cmcd.CMCD_V2) {
return Array.from(new Set([].concat(
cml.cmcd.CMCD_REQUEST_KEYS,
cml.cmcd.CMCD_RESPONSE_KEYS,
cml.cmcd.CMCD_EVENT_KEYS)));
}
return [].concat(cml.cmcd.CMCD_V1_KEYS);
};
/**
* Whether two effective configurations differ in a field the reporter
* itself consumes, which is the only reason to tear it down and build a
* new one. `includeInRequests`, `serviceLocations` and `adaptationSets`
* are evaluated by this adapter on each request, so they are not compared:
* rebuilding for them would clear the request timestamps and reset CML's
* counters for nothing. `eventTargets` is compared by reference, as
* before.
*
* @param {!shaka.util.CmcdManager.EffectiveConfig} a
* @param {!shaka.util.CmcdManager.EffectiveConfig} b
* @return {boolean}
* @private
*/
shaka.util.CmcdManager.isMaterialChange_ = (a, b) => {
const ArrayUtils = shaka.util.ArrayUtils;
return a.enabled != b.enabled ||
a.version != b.version ||
a.useHeaders != b.useHeaders ||
a.sessionId != b.sessionId ||
a.contentId != b.contentId ||
a.eventTargets != b.eventTargets ||
!ArrayUtils.equal(a.includeKeys, b.includeKeys);
};
/**
* Map a shaka request to its ISO/IEC 23009-1:2026 Table I.4 request type
* token. License, key, certificate and timing requests have no DASH token;
* they get the internal tokens 'license' and 'timing', which only the legacy
* default set and '*' match.
*
* @param {!shaka.net.NetworkingEngine.RequestType} type
* @param {!shaka.extern.RequestContext} context
* @return {?string} null when shaka never decorates this request type.
* @private
*/
shaka.util.CmcdManager.requestTypeToken_ = (type, context) => {
const RequestType = shaka.net.NetworkingEngine.RequestType;
const AdvancedRequestType = shaka.net.NetworkingEngine.AdvancedRequestType;
switch (type) {
case RequestType.SEGMENT:
return context.type == AdvancedRequestType.INIT_SEGMENT ?
'init' : 'segment';
case RequestType.MANIFEST:
switch (context.type) {
case AdvancedRequestType.MPD_PATCH:
return 'mpdpatch';
case AdvancedRequestType.XLINK:
return 'xlink';
case AdvancedRequestType.LINKED_MPD:
return 'mpdlink';
default:
return 'mpd';
}
case RequestType.CONTENT_STEERING:
return 'steering';
case RequestType.EVENT_CALLBACK:
return 'callback';
case RequestType.LICENSE:
case RequestType.SERVER_CERTIFICATE:
case RequestType.KEY:
return 'license';
case RequestType.TIMING:
return 'timing';
default:
return null;
}
};
/**
* Resolve the service location of a URI by longest-prefix match over the
* manifest's BaseURL / Location declarations.
*
* @param {string} uri
* @param {!Array<shaka.extern.ServiceLocationBaseUri>} baseUris
* @return {?string}
* @private
*/
shaka.util.CmcdManager.resolveServiceLocation_ = (uri, baseUris) => {
/** @type {?shaka.extern.ServiceLocationBaseUri} */
let best = null;
for (const entry of baseUris) {
if (uri.startsWith(entry.uri) &&
(!best || entry.uri.length > best.uri.length)) {
best = entry;
}
}
return best ? best.serviceLocation : null;
};
/**
* The request types shaka decorated before includeInRequests existed. Used
* when the application left its list empty, so existing deployments see no
* change. Steering and callback requests are deliberately absent.
*
* @const {!Array<string>}
* @private
*/
shaka.util.CmcdManager.LEGACY_REQUEST_TOKENS_ = [
'segment', 'init', 'mpd', 'mpdpatch', 'xlink', 'mpdlink', 'license',
'timing',
];
/**
* Public re-export of CMCD streaming-format values. The literal object
* form is required for Closure tooling: clutz (TypeScript-defs gen)
* and `generateExterns.js` reject `@export`ed `@enum`s whose RHS is a
* MemberExpression / alias. The 4 values match
* `cml.cmcd.CmcdStreamingFormat` exactly; a unit test asserts value
* identity.
*
* @enum {string}
* @export
*/
shaka.util.CmcdManager.StreamingFormat = {
DASH: 'd',
HLS: 'h',
SMOOTH: 's',
OTHER: 'o',
};
/**
* Public re-export of CMCD v2 event types. Literal form per the
* Closure-tooling constraint above. Values match `cml.cmcd.CmcdEventType`
* exactly; unit-test asserts identity.
*
* @enum {string}
* @export
*/
shaka.util.CmcdManager.EventType = {
BITRATE_CHANGE: 'bc',
PLAY_STATE: 'ps',
PLAYBACK_RATE: 'pr',
ERROR: 'e',
TIME_INTERVAL: 't',
CONTENT_ID: 'c',
BACKGROUNDED_MODE: 'b',
MUTE: 'm',
UNMUTE: 'um',
PLAYER_EXPAND: 'pe',
PLAYER_COLLAPSE: 'pc',
RESPONSE_RECEIVED: 'rr',
AD_START: 'as',
AD_END: 'ae',
AD_BREAK_START: 'abs',
AD_BREAK_END: 'abe',
SKIP: 'sk',
CUSTOM_EVENT: 'ce',
};
/**
* Public re-export of CMCD v2 player states. Literal form per the
* Closure-tooling constraint above. Values match `cml.cmcd.CmcdPlayerState`
* exactly; unit-test asserts identity.
*
* @enum {string}
* @export
*/
shaka.util.CmcdManager.PlayerState = {
STARTING: 's',
PLAYING: 'p',
SEEKING: 'k',
REBUFFERING: 'r',
PAUSED: 'a',
WAITING: 'w',
ENDED: 'e',
FATAL_ERROR: 'f',
QUIT: 'q',
PRELOADING: 'd',
};