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