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