Source: lib/util/cmcd_manager.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.util.CmcdManager');
  7. goog.require('goog.Uri');
  8. goog.require('shaka.log');
  9. goog.require('shaka.net.NetworkingEngine');
  10. goog.require('shaka.util.ArrayUtils');
  11. goog.requireType('shaka.media.SegmentReference');
  12. /**
  13. * @summary
  14. * A CmcdManager maintains CMCD state as well as a collection of utility
  15. * functions.
  16. */
  17. shaka.util.CmcdManager = class {
  18. /**
  19. * @param {shaka.util.CmcdManager.PlayerInterface} playerInterface
  20. * @param {shaka.extern.CmcdConfiguration} config
  21. */
  22. constructor(playerInterface, config) {
  23. /** @private {shaka.util.CmcdManager.PlayerInterface} */
  24. this.playerInterface_ = playerInterface;
  25. /** @private {?shaka.extern.CmcdConfiguration} */
  26. this.config_ = config;
  27. /**
  28. * Streaming format
  29. *
  30. * @private {(shaka.util.CmcdManager.StreamingFormat|undefined)}
  31. */
  32. this.sf_ = undefined;
  33. /**
  34. * @private {boolean}
  35. */
  36. this.playbackStarted_ = false;
  37. /**
  38. * @private {boolean}
  39. */
  40. this.buffering_ = true;
  41. /**
  42. * @private {boolean}
  43. */
  44. this.starved_ = false;
  45. /**
  46. * @private {boolean}
  47. */
  48. this.lowLatency_ = false;
  49. }
  50. /**
  51. * Called by the Player to provide an updated configuration any time it
  52. * changes.
  53. *
  54. * @param {shaka.extern.CmcdConfiguration} config
  55. */
  56. configure(config) {
  57. this.config_ = config;
  58. }
  59. /**
  60. * Resets the CmcdManager.
  61. */
  62. reset() {
  63. this.playbackStarted_ = false;
  64. this.buffering_ = true;
  65. this.starved_ = false;
  66. this.lowLatency_ = false;
  67. }
  68. /**
  69. * Set the buffering state
  70. *
  71. * @param {boolean} buffering
  72. */
  73. setBuffering(buffering) {
  74. if (!buffering && !this.playbackStarted_) {
  75. this.playbackStarted_ = true;
  76. }
  77. if (this.playbackStarted_ && buffering) {
  78. this.starved_ = true;
  79. }
  80. this.buffering_ = buffering;
  81. }
  82. /**
  83. * Set the low latency
  84. *
  85. * @param {boolean} lowLatency
  86. */
  87. setLowLatency(lowLatency) {
  88. this.lowLatency_ = lowLatency;
  89. const StreamingFormat = shaka.util.CmcdManager.StreamingFormat;
  90. if (this.lowLatency_) {
  91. if (this.sf_ == StreamingFormat.DASH) {
  92. this.sf_ = StreamingFormat.LOW_LATENCY_DASH;
  93. } else if (this.sf_ == StreamingFormat.HLS) {
  94. this.sf_ = StreamingFormat.LOW_LATENCY_HLS;
  95. }
  96. } else {
  97. if (this.sf_ == StreamingFormat.LOW_LATENCY_DASH) {
  98. this.sf_ = StreamingFormat.DASH;
  99. } else if (this.sf_ == StreamingFormat.LOW_LATENCY_HLS) {
  100. this.sf_ = StreamingFormat.HLS;
  101. }
  102. }
  103. }
  104. /**
  105. * Apply CMCD data to a request.
  106. *
  107. * @param {!shaka.net.NetworkingEngine.RequestType} type
  108. * The request type
  109. * @param {!shaka.extern.Request} request
  110. * The request to apply CMCD data to
  111. * @param {shaka.extern.RequestContext=} context
  112. * The request context
  113. */
  114. applyData(type, request, context = {}) {
  115. if (!this.config_.enabled) {
  116. return;
  117. }
  118. if (request.method === 'HEAD') {
  119. this.apply_(request);
  120. return;
  121. }
  122. const RequestType = shaka.net.NetworkingEngine.RequestType;
  123. const ObjectType = shaka.util.CmcdManager.ObjectType;
  124. switch (type) {
  125. case RequestType.MANIFEST:
  126. this.applyManifestData(request, context);
  127. break;
  128. case RequestType.SEGMENT:
  129. this.applySegmentData(request, context);
  130. break;
  131. case RequestType.LICENSE:
  132. case RequestType.SERVER_CERTIFICATE:
  133. case RequestType.KEY:
  134. this.apply_(request, {ot: ObjectType.KEY});
  135. break;
  136. case RequestType.TIMING:
  137. this.apply_(request, {ot: ObjectType.OTHER});
  138. break;
  139. }
  140. }
  141. /**
  142. * Apply CMCD data to a manifest request.
  143. *
  144. * @param {!shaka.extern.Request} request
  145. * The request to apply CMCD data to
  146. * @param {shaka.extern.RequestContext} context
  147. * The request context
  148. */
  149. applyManifestData(request, context) {
  150. try {
  151. if (!this.config_.enabled) {
  152. return;
  153. }
  154. if (context.type) {
  155. this.sf_ = this.getStreamFormat_(context.type);
  156. }
  157. this.apply_(request, {
  158. ot: shaka.util.CmcdManager.ObjectType.MANIFEST,
  159. su: !this.playbackStarted_,
  160. });
  161. } catch (error) {
  162. shaka.log.warnOnce('CMCD_MANIFEST_ERROR',
  163. 'Could not generate manifest CMCD data.', error);
  164. }
  165. }
  166. /**
  167. * Apply CMCD data to a segment request
  168. *
  169. * @param {!shaka.extern.Request} request
  170. * @param {shaka.extern.RequestContext} context
  171. * The request context
  172. */
  173. applySegmentData(request, context) {
  174. try {
  175. if (!this.config_.enabled) {
  176. return;
  177. }
  178. const segment = context.segment;
  179. let duration = 0;
  180. if (segment) {
  181. duration = segment.endTime - segment.startTime;
  182. }
  183. const data = {
  184. d: duration * 1000,
  185. st: this.getStreamType_(),
  186. };
  187. data.ot = this.getObjectType_(context);
  188. const ObjectType = shaka.util.CmcdManager.ObjectType;
  189. const isMedia = data.ot === ObjectType.VIDEO ||
  190. data.ot === ObjectType.AUDIO ||
  191. data.ot === ObjectType.MUXED ||
  192. data.ot === ObjectType.TIMED_TEXT;
  193. const stream = context.stream;
  194. if (stream) {
  195. const playbackRate = this.playerInterface_.getPlaybackRate();
  196. if (isMedia) {
  197. data.bl = this.getBufferLength_(stream.type);
  198. if (data.ot !== ObjectType.TIMED_TEXT) {
  199. const remainingBufferLength =
  200. this.getRemainingBufferLength_(stream.type);
  201. if (playbackRate) {
  202. data.dl = remainingBufferLength / Math.abs(playbackRate);
  203. } else {
  204. data.dl = remainingBufferLength;
  205. }
  206. }
  207. }
  208. if (stream.bandwidth) {
  209. data.br = stream.bandwidth / 1000;
  210. }
  211. if (stream.segmentIndex && segment) {
  212. const reverse = playbackRate < 0;
  213. const iterator = stream.segmentIndex.getIteratorForTime(
  214. segment.endTime, /* allowNonIndepedent= */ true, reverse);
  215. if (iterator) {
  216. const nextSegment = iterator.next().value;
  217. if (nextSegment && nextSegment != segment) {
  218. if (!shaka.util.ArrayUtils.equal(
  219. segment.getUris(), nextSegment.getUris())) {
  220. data.nor = this.urlToRelativePath_(
  221. nextSegment.getUris()[0], request.uris[0]);
  222. }
  223. if ((nextSegment.startByte || nextSegment.endByte) &&
  224. (segment.startByte != nextSegment.startByte ||
  225. segment.endByte != nextSegment.endByte)) {
  226. let range = nextSegment.startByte + '-';
  227. if (nextSegment.endByte) {
  228. range += nextSegment.endByte;
  229. }
  230. data.nrr = range;
  231. }
  232. }
  233. }
  234. const rtp = this.calculateRtp_(stream, segment);
  235. if (!isNaN(rtp)) {
  236. data.rtp = rtp;
  237. }
  238. }
  239. }
  240. if (isMedia && data.ot !== ObjectType.TIMED_TEXT) {
  241. data.tb = this.getTopBandwidth_(data.ot) / 1000;
  242. }
  243. this.apply_(request, data);
  244. } catch (error) {
  245. shaka.log.warnOnce('CMCD_SEGMENT_ERROR',
  246. 'Could not generate segment CMCD data.', error);
  247. }
  248. }
  249. /**
  250. * Apply CMCD data to a text request
  251. *
  252. * @param {!shaka.extern.Request} request
  253. */
  254. applyTextData(request) {
  255. try {
  256. if (!this.config_.enabled) {
  257. return;
  258. }
  259. this.apply_(request, {
  260. ot: shaka.util.CmcdManager.ObjectType.CAPTION,
  261. su: true,
  262. });
  263. } catch (error) {
  264. shaka.log.warnOnce('CMCD_TEXT_ERROR',
  265. 'Could not generate text CMCD data.', error);
  266. }
  267. }
  268. /**
  269. * Apply CMCD data to streams loaded via src=.
  270. *
  271. * @param {string} uri
  272. * @param {string} mimeType
  273. * @return {string}
  274. */
  275. appendSrcData(uri, mimeType) {
  276. try {
  277. if (!this.config_.enabled) {
  278. return uri;
  279. }
  280. const data = this.createData_();
  281. data.ot = this.getObjectTypeFromMimeType_(mimeType);
  282. data.su = true;
  283. const query = shaka.util.CmcdManager.toQuery(data);
  284. return shaka.util.CmcdManager.appendQueryToUri(uri, query);
  285. } catch (error) {
  286. shaka.log.warnOnce('CMCD_SRC_ERROR',
  287. 'Could not generate src CMCD data.', error);
  288. return uri;
  289. }
  290. }
  291. /**
  292. * Apply CMCD data to side car text track uri.
  293. *
  294. * @param {string} uri
  295. * @return {string}
  296. */
  297. appendTextTrackData(uri) {
  298. try {
  299. if (!this.config_.enabled) {
  300. return uri;
  301. }
  302. const data = this.createData_();
  303. data.ot = shaka.util.CmcdManager.ObjectType.CAPTION;
  304. data.su = true;
  305. const query = shaka.util.CmcdManager.toQuery(data);
  306. return shaka.util.CmcdManager.appendQueryToUri(uri, query);
  307. } catch (error) {
  308. shaka.log.warnOnce('CMCD_TEXT_TRACK_ERROR',
  309. 'Could not generate text track CMCD data.', error);
  310. return uri;
  311. }
  312. }
  313. /**
  314. * Create baseline CMCD data
  315. *
  316. * @return {CmcdData}
  317. * @private
  318. */
  319. createData_() {
  320. if (!this.config_.sessionId) {
  321. this.config_.sessionId = window.crypto.randomUUID();
  322. }
  323. return {
  324. v: shaka.util.CmcdManager.Version,
  325. sf: this.sf_,
  326. sid: this.config_.sessionId,
  327. cid: this.config_.contentId,
  328. mtp: this.playerInterface_.getBandwidthEstimate() / 1000,
  329. };
  330. }
  331. /**
  332. * Apply CMCD data to a request.
  333. *
  334. * @param {!shaka.extern.Request} request The request to apply CMCD data to
  335. * @param {!CmcdData} data The data object
  336. * @param {boolean} useHeaders Send data via request headers
  337. * @private
  338. */
  339. apply_(request, data = {}, useHeaders = this.config_.useHeaders) {
  340. if (!this.config_.enabled) {
  341. return;
  342. }
  343. // apply baseline data
  344. Object.assign(data, this.createData_());
  345. data.pr = this.playerInterface_.getPlaybackRate();
  346. const isVideo = data.ot === shaka.util.CmcdManager.ObjectType.VIDEO ||
  347. data.ot === shaka.util.CmcdManager.ObjectType.MUXED;
  348. if (this.starved_ && isVideo) {
  349. data.bs = true;
  350. data.su = true;
  351. this.starved_ = false;
  352. }
  353. if (data.su == null) {
  354. data.su = this.buffering_;
  355. }
  356. const output = this.filterKeys_(data);
  357. if (useHeaders) {
  358. const headers = shaka.util.CmcdManager.toHeaders(output);
  359. if (!Object.keys(headers).length) {
  360. return;
  361. }
  362. Object.assign(request.headers, headers);
  363. } else {
  364. const query = shaka.util.CmcdManager.toQuery(output);
  365. if (!query) {
  366. return;
  367. }
  368. request.uris = request.uris.map((uri) => {
  369. return shaka.util.CmcdManager.appendQueryToUri(uri, query);
  370. });
  371. }
  372. }
  373. /**
  374. * Filter the CMCD data object to include only the keys specified in the
  375. * configuration.
  376. *
  377. * @param {CmcdData} data
  378. * @return {CmcdData}
  379. * @private
  380. */
  381. filterKeys_(data) {
  382. const includeKeys = this.config_.includeKeys;
  383. if (!includeKeys.length) {
  384. return data;
  385. }
  386. return Object.keys(data).reduce((acc, key) => {
  387. if (includeKeys.includes(key)) {
  388. acc[key] = data[key];
  389. }
  390. return acc;
  391. }, {});
  392. }
  393. /**
  394. * The CMCD object type.
  395. *
  396. * @param {shaka.extern.RequestContext} context
  397. * The request context
  398. * @private
  399. */
  400. getObjectType_(context) {
  401. if (context.type ===
  402. shaka.net.NetworkingEngine.AdvancedRequestType.INIT_SEGMENT) {
  403. return shaka.util.CmcdManager.ObjectType.INIT;
  404. }
  405. const stream = context.stream;
  406. if (!stream) {
  407. return undefined;
  408. }
  409. const type = stream.type;
  410. if (type == 'video') {
  411. if (stream.codecs && stream.codecs.includes(',')) {
  412. return shaka.util.CmcdManager.ObjectType.MUXED;
  413. }
  414. return shaka.util.CmcdManager.ObjectType.VIDEO;
  415. }
  416. if (type == 'audio') {
  417. return shaka.util.CmcdManager.ObjectType.AUDIO;
  418. }
  419. if (type == 'text') {
  420. if (stream.mimeType === 'application/mp4') {
  421. return shaka.util.CmcdManager.ObjectType.TIMED_TEXT;
  422. }
  423. return shaka.util.CmcdManager.ObjectType.CAPTION;
  424. }
  425. return undefined;
  426. }
  427. /**
  428. * The CMCD object type from mimeType.
  429. *
  430. * @param {!string} mimeType
  431. * @return {(shaka.util.CmcdManager.ObjectType|undefined)}
  432. * @private
  433. */
  434. getObjectTypeFromMimeType_(mimeType) {
  435. switch (mimeType.toLowerCase()) {
  436. case 'audio/mp4':
  437. case 'audio/webm':
  438. case 'audio/ogg':
  439. case 'audio/mpeg':
  440. case 'audio/aac':
  441. case 'audio/flac':
  442. case 'audio/wav':
  443. return shaka.util.CmcdManager.ObjectType.AUDIO;
  444. case 'video/webm':
  445. case 'video/mp4':
  446. case 'video/mpeg':
  447. case 'video/mp2t':
  448. return shaka.util.CmcdManager.ObjectType.MUXED;
  449. case 'application/x-mpegurl':
  450. case 'application/vnd.apple.mpegurl':
  451. case 'application/dash+xml':
  452. case 'video/vnd.mpeg.dash.mpd':
  453. case 'application/vnd.ms-sstr+xml':
  454. return shaka.util.CmcdManager.ObjectType.MANIFEST;
  455. default:
  456. return undefined;
  457. }
  458. }
  459. /**
  460. * Get the buffer length for a media type in milliseconds
  461. *
  462. * @param {string} type
  463. * @return {number}
  464. * @private
  465. */
  466. getBufferLength_(type) {
  467. const ranges = this.playerInterface_.getBufferedInfo()[type];
  468. if (!ranges.length) {
  469. return NaN;
  470. }
  471. const start = this.playerInterface_.getCurrentTime();
  472. const range = ranges.find((r) => r.start <= start && r.end >= start);
  473. if (!range) {
  474. return NaN;
  475. }
  476. return (range.end - start) * 1000;
  477. }
  478. /**
  479. * Get the remaining buffer length for a media type in milliseconds
  480. *
  481. * @param {string} type
  482. * @return {number}
  483. * @private
  484. */
  485. getRemainingBufferLength_(type) {
  486. const ranges = this.playerInterface_.getBufferedInfo()[type];
  487. if (!ranges.length) {
  488. return 0;
  489. }
  490. const start = this.playerInterface_.getCurrentTime();
  491. const range = ranges.find((r) => r.start <= start && r.end >= start);
  492. if (!range) {
  493. return 0;
  494. }
  495. return (range.end - start) * 1000;
  496. }
  497. /**
  498. * Constructs a relative path from a URL
  499. *
  500. * @param {string} url
  501. * @param {string} base
  502. * @return {string}
  503. * @private
  504. */
  505. urlToRelativePath_(url, base) {
  506. const to = new URL(url);
  507. const from = new URL(base);
  508. if (to.origin !== from.origin) {
  509. return url;
  510. }
  511. const toPath = to.pathname.split('/').slice(1);
  512. const fromPath = from.pathname.split('/').slice(1, -1);
  513. // remove common parents
  514. while (toPath[0] === fromPath[0]) {
  515. toPath.shift();
  516. fromPath.shift();
  517. }
  518. // add back paths
  519. while (fromPath.length) {
  520. fromPath.shift();
  521. toPath.unshift('..');
  522. }
  523. return toPath.join('/');
  524. }
  525. /**
  526. * Calculate requested maximun throughput
  527. *
  528. * @param {shaka.extern.Stream} stream
  529. * @param {shaka.media.SegmentReference} segment
  530. * @return {number}
  531. * @private
  532. */
  533. calculateRtp_(stream, segment) {
  534. const playbackRate = this.playerInterface_.getPlaybackRate() || 1;
  535. const currentBufferLevel =
  536. this.getRemainingBufferLength_(stream.type) || 500;
  537. const bandwidth = stream.bandwidth;
  538. if (!bandwidth) {
  539. return NaN;
  540. }
  541. const segmentDuration = segment.endTime - segment.startTime;
  542. // Calculate file size in kilobits
  543. const segmentSize = bandwidth * segmentDuration / 1000;
  544. // Calculate time available to load file in seconds
  545. const timeToLoad = (currentBufferLevel / playbackRate) / 1000;
  546. // Calculate the exact bandwidth required
  547. const minBandwidth = segmentSize / timeToLoad;
  548. // Include a safety buffer
  549. return minBandwidth * this.config_.rtpSafetyFactor;
  550. }
  551. /**
  552. * Get the stream format
  553. *
  554. * @param {shaka.net.NetworkingEngine.AdvancedRequestType} type
  555. * The request's advanced type
  556. * @return {(shaka.util.CmcdManager.StreamingFormat|undefined)}
  557. * @private
  558. */
  559. getStreamFormat_(type) {
  560. const AdvancedRequestType = shaka.net.NetworkingEngine.AdvancedRequestType;
  561. switch (type) {
  562. case AdvancedRequestType.MPD:
  563. if (this.lowLatency_) {
  564. return shaka.util.CmcdManager.StreamingFormat.LOW_LATENCY_DASH;
  565. }
  566. return shaka.util.CmcdManager.StreamingFormat.DASH;
  567. case AdvancedRequestType.MASTER_PLAYLIST:
  568. case AdvancedRequestType.MEDIA_PLAYLIST:
  569. if (this.lowLatency_) {
  570. return shaka.util.CmcdManager.StreamingFormat.LOW_LATENCY_HLS;
  571. }
  572. return shaka.util.CmcdManager.StreamingFormat.HLS;
  573. case AdvancedRequestType.MSS:
  574. return shaka.util.CmcdManager.StreamingFormat.SMOOTH;
  575. }
  576. return undefined;
  577. }
  578. /**
  579. * Get the stream type
  580. *
  581. * @return {shaka.util.CmcdManager.StreamType}
  582. * @private
  583. */
  584. getStreamType_() {
  585. const isLive = this.playerInterface_.isLive();
  586. if (isLive) {
  587. return shaka.util.CmcdManager.StreamType.LIVE;
  588. } else {
  589. return shaka.util.CmcdManager.StreamType.VOD;
  590. }
  591. }
  592. /**
  593. * Get the highest bandwidth for a given type.
  594. *
  595. * @param {string} type
  596. * @return {number}
  597. * @private
  598. */
  599. getTopBandwidth_(type) {
  600. const variants = this.playerInterface_.getVariantTracks();
  601. if (!variants.length) {
  602. return NaN;
  603. }
  604. let top = variants[0];
  605. for (const variant of variants) {
  606. if (variant.type === 'variant' && variant.bandwidth > top.bandwidth) {
  607. top = variant;
  608. }
  609. }
  610. const ObjectType = shaka.util.CmcdManager.ObjectType;
  611. switch (type) {
  612. case ObjectType.VIDEO:
  613. return top.videoBandwidth || NaN;
  614. case ObjectType.AUDIO:
  615. return top.audioBandwidth || NaN;
  616. default:
  617. return top.bandwidth;
  618. }
  619. }
  620. /**
  621. * Serialize a CMCD data object according to the rules defined in the
  622. * section 3.2 of
  623. * [CTA-5004](https://cdn.cta.tech/cta/media/media/resources/standards/pdfs/cta-5004-final.pdf).
  624. *
  625. * @param {CmcdData} data The CMCD data object
  626. * @return {string}
  627. */
  628. static serialize(data) {
  629. const results = [];
  630. const isValid = (value) =>
  631. !Number.isNaN(value) && value != null && value !== '' && value !== false;
  632. const toRounded = (value) => Math.round(value);
  633. const toHundred = (value) => toRounded(value / 100) * 100;
  634. const toUrlSafe = (value) => encodeURIComponent(value);
  635. const formatters = {
  636. br: toRounded,
  637. d: toRounded,
  638. bl: toHundred,
  639. dl: toHundred,
  640. mtp: toHundred,
  641. nor: toUrlSafe,
  642. rtp: toHundred,
  643. tb: toRounded,
  644. };
  645. const keys = Object.keys(data || {}).sort();
  646. for (const key of keys) {
  647. let value = data[key];
  648. // ignore invalid values
  649. if (!isValid(value)) {
  650. continue;
  651. }
  652. // Version should only be reported if not equal to 1.
  653. if (key === 'v' && value === 1) {
  654. continue;
  655. }
  656. // Playback rate should only be sent if not equal to 1.
  657. if (key == 'pr' && value === 1) {
  658. continue;
  659. }
  660. // Certain values require special formatting
  661. const formatter = formatters[key];
  662. if (formatter) {
  663. value = formatter(value);
  664. }
  665. // Serialize the key/value pair
  666. const type = typeof value;
  667. let result;
  668. if (type === 'string' && key !== 'ot' && key !== 'sf' && key !== 'st') {
  669. result = `${key}=${JSON.stringify(value)}`;
  670. } else if (type === 'boolean') {
  671. result = key;
  672. } else if (type === 'symbol') {
  673. result = `${key}=${value.description}`;
  674. } else {
  675. result = `${key}=${value}`;
  676. }
  677. results.push(result);
  678. }
  679. return results.join(',');
  680. }
  681. /**
  682. * Convert a CMCD data object to request headers according to the rules
  683. * defined in the section 2.1 and 3.2 of
  684. * [CTA-5004](https://cdn.cta.tech/cta/media/media/resources/standards/pdfs/cta-5004-final.pdf).
  685. *
  686. * @param {CmcdData} data The CMCD data object
  687. * @return {!Object}
  688. */
  689. static toHeaders(data) {
  690. const keys = Object.keys(data);
  691. const headers = {};
  692. const headerNames = ['Object', 'Request', 'Session', 'Status'];
  693. const headerGroups = [{}, {}, {}, {}];
  694. const headerMap = {
  695. br: 0, d: 0, ot: 0, tb: 0,
  696. bl: 1, dl: 1, mtp: 1, nor: 1, nrr: 1, su: 1,
  697. cid: 2, pr: 2, sf: 2, sid: 2, st: 2, v: 2,
  698. bs: 3, rtp: 3,
  699. };
  700. for (const key of keys) {
  701. // Unmapped fields are mapped to the Request header
  702. const index = (headerMap[key] != null) ? headerMap[key] : 1;
  703. headerGroups[index][key] = data[key];
  704. }
  705. for (let i = 0; i < headerGroups.length; i++) {
  706. const value = shaka.util.CmcdManager.serialize(headerGroups[i]);
  707. if (value) {
  708. headers[`CMCD-${headerNames[i]}`] = value;
  709. }
  710. }
  711. return headers;
  712. }
  713. /**
  714. * Convert a CMCD data object to query args according to the rules
  715. * defined in the section 2.2 and 3.2 of
  716. * [CTA-5004](https://cdn.cta.tech/cta/media/media/resources/standards/pdfs/cta-5004-final.pdf).
  717. *
  718. * @param {CmcdData} data The CMCD data object
  719. * @return {string}
  720. */
  721. static toQuery(data) {
  722. return shaka.util.CmcdManager.serialize(data);
  723. }
  724. /**
  725. * Append query args to a uri.
  726. *
  727. * @param {string} uri
  728. * @param {string} query
  729. * @return {string}
  730. */
  731. static appendQueryToUri(uri, query) {
  732. if (!query) {
  733. return uri;
  734. }
  735. if (uri.includes('offline:')) {
  736. return uri;
  737. }
  738. const url = new goog.Uri(uri);
  739. url.getQueryData().set('CMCD', query);
  740. return url.toString();
  741. }
  742. };
  743. /**
  744. * @typedef {{
  745. * getBandwidthEstimate: function():number,
  746. * getBufferedInfo: function():shaka.extern.BufferedInfo,
  747. * getCurrentTime: function():number,
  748. * getPlaybackRate: function():number,
  749. * getVariantTracks: function():Array.<shaka.extern.Track>,
  750. * isLive: function():boolean
  751. * }}
  752. *
  753. * @property {function():number} getBandwidthEstimate
  754. * Get the estimated bandwidth in bits per second.
  755. * @property {function():shaka.extern.BufferedInfo} getBufferedInfo
  756. * Get information about what the player has buffered.
  757. * @property {function():number} getCurrentTime
  758. * Get the current time
  759. * @property {function():number} getPlaybackRate
  760. * Get the playback rate
  761. * @property {function():Array.<shaka.extern.Track>} getVariantTracks
  762. * Get the variant tracks
  763. * @property {function():boolean} isLive
  764. * Get if the player is playing live content.
  765. */
  766. shaka.util.CmcdManager.PlayerInterface;
  767. /**
  768. * @enum {string}
  769. */
  770. shaka.util.CmcdManager.ObjectType = {
  771. MANIFEST: 'm',
  772. AUDIO: 'a',
  773. VIDEO: 'v',
  774. MUXED: 'av',
  775. INIT: 'i',
  776. CAPTION: 'c',
  777. TIMED_TEXT: 'tt',
  778. KEY: 'k',
  779. OTHER: 'o',
  780. };
  781. /**
  782. * @enum {string}
  783. */
  784. shaka.util.CmcdManager.StreamType = {
  785. VOD: 'v',
  786. LIVE: 'l',
  787. };
  788. /**
  789. * @enum {string}
  790. * @export
  791. */
  792. shaka.util.CmcdManager.StreamingFormat = {
  793. DASH: 'd',
  794. LOW_LATENCY_DASH: 'ld',
  795. HLS: 'h',
  796. LOW_LATENCY_HLS: 'lh',
  797. SMOOTH: 's',
  798. OTHER: 'o',
  799. };
  800. /**
  801. * The CMCD spec version
  802. * @const {number}
  803. */
  804. shaka.util.CmcdManager.Version = 1;