Source: lib/util/stream_utils.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.util.StreamUtils');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.log');
  9. goog.require('shaka.text.TextEngine');
  10. goog.require('shaka.util.Functional');
  11. goog.require('shaka.util.LanguageUtils');
  12. goog.require('shaka.util.ManifestParserUtils');
  13. goog.require('shaka.util.MimeUtils');
  14. goog.require('shaka.util.MultiMap');
  15. goog.require('shaka.util.Platform');
  16. goog.requireType('shaka.media.DrmEngine');
  17. /**
  18. * @summary A set of utility functions for dealing with Streams and Manifests.
  19. */
  20. shaka.util.StreamUtils = class {
  21. /**
  22. * In case of multiple usable codecs, choose one based on lowest average
  23. * bandwidth and filter out the rest.
  24. * Also filters out variants that have too many audio channels.
  25. * @param {!shaka.extern.Manifest} manifest
  26. * @param {!Array.<string>} preferredVideoCodecs
  27. * @param {!Array.<string>} preferredAudioCodecs
  28. * @param {number} preferredAudioChannelCount
  29. * @param {!Array.<string>} preferredDecodingAttributes
  30. */
  31. static chooseCodecsAndFilterManifest(manifest, preferredVideoCodecs,
  32. preferredAudioCodecs, preferredAudioChannelCount,
  33. preferredDecodingAttributes) {
  34. const StreamUtils = shaka.util.StreamUtils;
  35. let variants = manifest.variants;
  36. // To start, choose the codecs based on configured preferences if available.
  37. if (preferredVideoCodecs.length || preferredAudioCodecs.length) {
  38. variants = StreamUtils.choosePreferredCodecs(variants,
  39. preferredVideoCodecs, preferredAudioCodecs);
  40. }
  41. // Consider a subset of variants based on audio channel
  42. // preferences.
  43. // For some content (#1013), surround-sound variants will use a different
  44. // codec than stereo variants, so it is important to choose codecs **after**
  45. // considering the audio channel config.
  46. variants = StreamUtils.filterVariantsByAudioChannelCount(
  47. variants, preferredAudioChannelCount);
  48. // Now organize variants into buckets by codecs.
  49. /** @type {!shaka.util.MultiMap.<shaka.extern.Variant>} */
  50. let variantsByCodecs = StreamUtils.getVariantsByCodecs_(variants);
  51. variantsByCodecs = StreamUtils.filterVariantsByDensity_(variantsByCodecs);
  52. const bestCodecs = StreamUtils.chooseCodecsByDecodingAttributes_(
  53. variantsByCodecs, preferredDecodingAttributes);
  54. // Filter out any variants that don't match, forcing AbrManager to choose
  55. // from a single video codec and a single audio codec possible.
  56. manifest.variants = manifest.variants.filter((variant) => {
  57. const codecs = StreamUtils.getVariantCodecs_(variant);
  58. if (codecs == bestCodecs) {
  59. return true;
  60. }
  61. shaka.log.debug('Dropping Variant (better codec available)', variant);
  62. return false;
  63. });
  64. }
  65. /**
  66. * Get variants by codecs.
  67. *
  68. * @param {!Array<shaka.extern.Variant>} variants
  69. * @return {!shaka.util.MultiMap.<shaka.extern.Variant>}
  70. * @private
  71. */
  72. static getVariantsByCodecs_(variants) {
  73. const variantsByCodecs = new shaka.util.MultiMap();
  74. for (const variant of variants) {
  75. const variantCodecs = shaka.util.StreamUtils.getVariantCodecs_(variant);
  76. variantsByCodecs.push(variantCodecs, variant);
  77. }
  78. return variantsByCodecs;
  79. }
  80. /**
  81. * Filters variants by density.
  82. * Get variants by codecs map with the max density where all codecs are
  83. * present.
  84. *
  85. * @param {!shaka.util.MultiMap.<shaka.extern.Variant>} variantsByCodecs
  86. * @return {!shaka.util.MultiMap.<shaka.extern.Variant>}
  87. * @private
  88. */
  89. static filterVariantsByDensity_(variantsByCodecs) {
  90. let maxDensity = 0;
  91. const codecGroupsByDensity = new Map();
  92. const countCodecs = variantsByCodecs.size();
  93. variantsByCodecs.forEach((codecs, variants) => {
  94. for (const variant of variants) {
  95. const video = variant.video;
  96. if (!video || !video.width || !video.height) {
  97. continue;
  98. }
  99. const density = video.width * video.height * (video.frameRate || 1);
  100. if (!codecGroupsByDensity.has(density)) {
  101. codecGroupsByDensity.set(density, new shaka.util.MultiMap());
  102. }
  103. /** @type {!shaka.util.MultiMap.<shaka.extern.Variant>} */
  104. const group = codecGroupsByDensity.get(density);
  105. group.push(codecs, variant);
  106. // We want to look at the groups in which all codecs are present.
  107. // Take the max density from those groups where all codecs are present.
  108. // Later, we will compare bandwidth numbers only within this group.
  109. // Effectively, only the bandwidth differences in the highest-res and
  110. // highest-framerate content will matter in choosing a codec.
  111. if (group.size() === countCodecs) {
  112. maxDensity = Math.max(maxDensity, density);
  113. }
  114. }
  115. });
  116. return maxDensity ? codecGroupsByDensity.get(maxDensity) : variantsByCodecs;
  117. }
  118. /**
  119. * Choose the codecs by configured preferred audio and video codecs.
  120. *
  121. * @param {!Array<shaka.extern.Variant>} variants
  122. * @param {!Array.<string>} preferredVideoCodecs
  123. * @param {!Array.<string>} preferredAudioCodecs
  124. * @return {!Array<shaka.extern.Variant>}
  125. */
  126. static choosePreferredCodecs(variants, preferredVideoCodecs,
  127. preferredAudioCodecs) {
  128. let subset = variants;
  129. for (const videoCodec of preferredVideoCodecs) {
  130. const filtered = subset.filter((variant) => {
  131. return variant.video && variant.video.codecs.startsWith(videoCodec);
  132. });
  133. if (filtered.length) {
  134. subset = filtered;
  135. break;
  136. }
  137. }
  138. for (const audioCodec of preferredAudioCodecs) {
  139. const filtered = subset.filter((variant) => {
  140. return variant.audio && variant.audio.codecs.startsWith(audioCodec);
  141. });
  142. if (filtered.length) {
  143. subset = filtered;
  144. break;
  145. }
  146. }
  147. return subset;
  148. }
  149. /**
  150. * Choose the codecs by configured preferred decoding attributes.
  151. *
  152. * @param {!shaka.util.MultiMap.<shaka.extern.Variant>} variantsByCodecs
  153. * @param {!Array.<string>} attributes
  154. * @return {string}
  155. * @private
  156. */
  157. static chooseCodecsByDecodingAttributes_(variantsByCodecs, attributes) {
  158. const StreamUtils = shaka.util.StreamUtils;
  159. for (const attribute of attributes) {
  160. if (attribute == StreamUtils.DecodingAttributes.SMOOTH ||
  161. attribute == StreamUtils.DecodingAttributes.POWER) {
  162. variantsByCodecs = StreamUtils.chooseCodecsByMediaCapabilitiesInfo_(
  163. variantsByCodecs, attribute);
  164. // If we only have one smooth or powerEfficient codecs, choose it as the
  165. // best codecs.
  166. if (variantsByCodecs.size() == 1) {
  167. return variantsByCodecs.keys()[0];
  168. }
  169. } else if (attribute == StreamUtils.DecodingAttributes.BANDWIDTH) {
  170. return StreamUtils.findCodecsByLowestBandwidth_(variantsByCodecs);
  171. }
  172. }
  173. // If there's no configured decoding preferences, or we have multiple codecs
  174. // that meets the configured decoding preferences, choose the one with
  175. // the lowest bandwidth.
  176. return StreamUtils.findCodecsByLowestBandwidth_(variantsByCodecs);
  177. }
  178. /**
  179. * Choose the best codecs by configured preferred MediaCapabilitiesInfo
  180. * attributes.
  181. *
  182. * @param {!shaka.util.MultiMap.<shaka.extern.Variant>} variantsByCodecs
  183. * @param {string} attribute
  184. * @return {!shaka.util.MultiMap.<shaka.extern.Variant>}
  185. * @private
  186. */
  187. static chooseCodecsByMediaCapabilitiesInfo_(variantsByCodecs, attribute) {
  188. let highestScore = 0;
  189. const bestVariantsByCodecs = new shaka.util.MultiMap();
  190. variantsByCodecs.forEach((codecs, variants) => {
  191. let sum = 0;
  192. let num = 0;
  193. for (const variant of variants) {
  194. if (variant.decodingInfos.length) {
  195. sum += variant.decodingInfos[0][attribute] ? 1 : 0;
  196. num++;
  197. }
  198. }
  199. const averageScore = sum / num;
  200. shaka.log.debug('codecs', codecs, 'avg', attribute, averageScore);
  201. if (averageScore > highestScore) {
  202. bestVariantsByCodecs.clear();
  203. bestVariantsByCodecs.push(codecs, variants);
  204. highestScore = averageScore;
  205. } else if (averageScore == highestScore) {
  206. bestVariantsByCodecs.push(codecs, variants);
  207. }
  208. });
  209. return bestVariantsByCodecs;
  210. }
  211. /**
  212. * Find the lowest-bandwidth (best) codecs.
  213. * Compute the average bandwidth for each group of variants.
  214. *
  215. * @param {!shaka.util.MultiMap.<shaka.extern.Variant>} variantsByCodecs
  216. * @return {string}
  217. * @private
  218. */
  219. static findCodecsByLowestBandwidth_(variantsByCodecs) {
  220. let bestCodecs = '';
  221. let lowestAverageBandwidth = Infinity;
  222. variantsByCodecs.forEach((codecs, variants) => {
  223. let sum = 0;
  224. let num = 0;
  225. for (const variant of variants) {
  226. sum += variant.bandwidth || 0;
  227. ++num;
  228. }
  229. const averageBandwidth = sum / num;
  230. shaka.log.debug('codecs', codecs, 'avg bandwidth', averageBandwidth);
  231. if (averageBandwidth < lowestAverageBandwidth) {
  232. bestCodecs = codecs;
  233. lowestAverageBandwidth = averageBandwidth;
  234. }
  235. });
  236. goog.asserts.assert(bestCodecs !== '', 'Should have chosen codecs!');
  237. goog.asserts.assert(!isNaN(lowestAverageBandwidth),
  238. 'Bandwidth should be a number!');
  239. return bestCodecs;
  240. }
  241. /**
  242. * Get a string representing all codecs used in a variant.
  243. *
  244. * @param {!shaka.extern.Variant} variant
  245. * @return {string}
  246. * @private
  247. */
  248. static getVariantCodecs_(variant) {
  249. // Only consider the base of the codec string. For example, these should
  250. // both be considered the same codec: avc1.42c01e, avc1.4d401f
  251. let baseVideoCodec = '';
  252. if (variant.video) {
  253. baseVideoCodec = shaka.util.MimeUtils.getCodecBase(variant.video.codecs);
  254. }
  255. let baseAudioCodec = '';
  256. if (variant.audio) {
  257. baseAudioCodec = shaka.util.MimeUtils.getCodecBase(variant.audio.codecs);
  258. }
  259. return baseVideoCodec + '-' + baseAudioCodec;
  260. }
  261. /**
  262. * Filter the variants in |manifest| to only include the variants that meet
  263. * the given restrictions.
  264. *
  265. * @param {!shaka.extern.Manifest} manifest
  266. * @param {shaka.extern.Restrictions} restrictions
  267. * @param {{width: number, height:number}} maxHwResolution
  268. */
  269. static filterByRestrictions(manifest, restrictions, maxHwResolution) {
  270. manifest.variants = manifest.variants.filter((variant) => {
  271. return shaka.util.StreamUtils.meetsRestrictions(
  272. variant, restrictions, maxHwResolution);
  273. });
  274. }
  275. /**
  276. * @param {shaka.extern.Variant} variant
  277. * @param {shaka.extern.Restrictions} restrictions
  278. * Configured restrictions from the user.
  279. * @param {{width: number, height: number}} maxHwRes
  280. * The maximum resolution the hardware can handle.
  281. * This is applied separately from user restrictions because the setting
  282. * should not be easily replaced by the user's configuration.
  283. * @return {boolean}
  284. */
  285. static meetsRestrictions(variant, restrictions, maxHwRes) {
  286. /** @type {function(number, number, number):boolean} */
  287. const inRange = (x, min, max) => {
  288. return x >= min && x <= max;
  289. };
  290. const video = variant.video;
  291. // |video.width| and |video.height| can be undefined, which breaks
  292. // the math, so make sure they are there first.
  293. if (video && video.width && video.height) {
  294. if (!inRange(video.width,
  295. restrictions.minWidth,
  296. Math.min(restrictions.maxWidth, maxHwRes.width))) {
  297. return false;
  298. }
  299. if (!inRange(video.height,
  300. restrictions.minHeight,
  301. Math.min(restrictions.maxHeight, maxHwRes.height))) {
  302. return false;
  303. }
  304. if (!inRange(video.width * video.height,
  305. restrictions.minPixels,
  306. restrictions.maxPixels)) {
  307. return false;
  308. }
  309. }
  310. // |variant.frameRate| can be undefined, which breaks
  311. // the math, so make sure they are there first.
  312. if (variant && variant.video && variant.video.frameRate) {
  313. if (!inRange(variant.video.frameRate,
  314. restrictions.minFrameRate,
  315. restrictions.maxFrameRate)) {
  316. return false;
  317. }
  318. }
  319. if (!inRange(variant.bandwidth,
  320. restrictions.minBandwidth,
  321. restrictions.maxBandwidth)) {
  322. return false;
  323. }
  324. return true;
  325. }
  326. /**
  327. * @param {!Array.<shaka.extern.Variant>} variants
  328. * @param {shaka.extern.Restrictions} restrictions
  329. * @param {{width: number, height: number}} maxHwRes
  330. * @return {boolean} Whether the tracks changed.
  331. */
  332. static applyRestrictions(variants, restrictions, maxHwRes) {
  333. let tracksChanged = false;
  334. for (const variant of variants) {
  335. const originalAllowed = variant.allowedByApplication;
  336. variant.allowedByApplication = shaka.util.StreamUtils.meetsRestrictions(
  337. variant, restrictions, maxHwRes);
  338. if (originalAllowed != variant.allowedByApplication) {
  339. tracksChanged = true;
  340. }
  341. }
  342. return tracksChanged;
  343. }
  344. /**
  345. * Alters the given Manifest to filter out any unplayable streams.
  346. *
  347. * @param {shaka.media.DrmEngine} drmEngine
  348. * @param {?shaka.extern.Variant} currentVariant
  349. * @param {shaka.extern.Manifest} manifest
  350. */
  351. static async filterManifest(
  352. drmEngine, currentVariant, manifest) {
  353. await shaka.util.StreamUtils.filterManifestByMediaCapabilities(manifest,
  354. manifest.offlineSessionIds.length > 0);
  355. shaka.util.StreamUtils.filterManifestByCurrentVariant(
  356. currentVariant, manifest);
  357. shaka.util.StreamUtils.filterTextStreams_(manifest);
  358. shaka.util.StreamUtils.filterImageStreams_(manifest);
  359. }
  360. /**
  361. * Alters the given Manifest to filter out any streams unsupported by the
  362. * platform via MediaCapabilities.decodingInfo() API.
  363. *
  364. * @param {shaka.extern.Manifest} manifest
  365. * @param {boolean} usePersistentLicenses
  366. */
  367. static async filterManifestByMediaCapabilities(
  368. manifest, usePersistentLicenses) {
  369. goog.asserts.assert(window.shakaMediaCapabilities,
  370. 'MediaCapabilities should be valid.');
  371. await shaka.util.StreamUtils.getDecodingInfosForVariants(
  372. manifest.variants, usePersistentLicenses, /* srcEquals= */ false);
  373. manifest.variants = manifest.variants.filter((variant) => {
  374. const video = variant.video;
  375. // See: https://github.com/google/shaka-player/issues/3380
  376. if (shaka.util.Platform.isXboxOne() && video &&
  377. ((video.width && video.width > 1920) ||
  378. (video.height && video.height > 1080)) &&
  379. video.codecs.includes('avc1.')) {
  380. shaka.log.debug('Dropping variant - not compatible with platform',
  381. shaka.util.StreamUtils.getVariantSummaryString_(variant));
  382. return false;
  383. }
  384. const supported = variant.decodingInfos.some((decodingInfo) => {
  385. return decodingInfo.supported;
  386. });
  387. // Filter out all unsupported variants.
  388. if (!supported) {
  389. shaka.log.debug('Dropping variant - not compatible with platform',
  390. shaka.util.StreamUtils.getVariantSummaryString_(variant));
  391. }
  392. return supported;
  393. });
  394. }
  395. /**
  396. * Get the decodingInfo results of the variants via MediaCapabilities.
  397. * This should be called after the DrmEngine is created and configured, and
  398. * before DrmEngine sets the mediaKeys.
  399. *
  400. * @param {!Array.<shaka.extern.Variant>} variants
  401. * @param {boolean} usePersistentLicenses
  402. * @param {boolean} srcEquals
  403. * @exportDoc
  404. */
  405. static async getDecodingInfosForVariants(variants, usePersistentLicenses,
  406. srcEquals) {
  407. const gotDecodingInfo = variants.some((variant) =>
  408. variant.decodingInfos.length);
  409. if (gotDecodingInfo) {
  410. shaka.log.debug('Already got the variants\' decodingInfo.');
  411. return;
  412. }
  413. const mediaCapabilities = window.shakaMediaCapabilities;
  414. const operations = [];
  415. const getVariantDecodingInfos = (async (variant, decodingConfig) => {
  416. try {
  417. const result = await mediaCapabilities.decodingInfo(decodingConfig);
  418. variant.decodingInfos.push(result);
  419. } catch (e) {
  420. shaka.log.info('MediaCapabilities.decodingInfo() failed.',
  421. JSON.stringify(decodingConfig), e);
  422. }
  423. });
  424. for (const variant of variants) {
  425. /** @type {!Array.<!MediaDecodingConfiguration>} */
  426. const decodingConfigs = shaka.util.StreamUtils.getDecodingConfigs_(
  427. variant, usePersistentLicenses, srcEquals);
  428. for (const config of decodingConfigs) {
  429. operations.push(getVariantDecodingInfos(variant, config));
  430. }
  431. }
  432. await Promise.all(operations);
  433. }
  434. /**
  435. * Generate a MediaDecodingConfiguration object to get the decodingInfo
  436. * results for each variant.
  437. * @param {!shaka.extern.Variant} variant
  438. * @param {boolean} usePersistentLicenses
  439. * @param {boolean} srcEquals
  440. * @return {!Array.<!MediaDecodingConfiguration>}
  441. * @private
  442. */
  443. static getDecodingConfigs_(variant, usePersistentLicenses, srcEquals) {
  444. const audio = variant.audio;
  445. const video = variant.video;
  446. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  447. /** @type {!MediaDecodingConfiguration} */
  448. const mediaDecodingConfig = {
  449. type: srcEquals ? 'file' : 'media-source',
  450. };
  451. if (video) {
  452. let videoCodecs = video.codecs;
  453. // For multiplexed streams with audio+video codecs, the config should have
  454. // AudioConfiguration and VideoConfiguration.
  455. if (video.codecs.includes(',')) {
  456. const allCodecs = video.codecs.split(',');
  457. videoCodecs = shaka.util.ManifestParserUtils.guessCodecs(
  458. ContentType.VIDEO, allCodecs);
  459. videoCodecs = shaka.util.StreamUtils.patchVp9(videoCodecs);
  460. const audioCodecs = shaka.util.ManifestParserUtils.guessCodecs(
  461. ContentType.AUDIO, allCodecs);
  462. const audioFullType = shaka.util.MimeUtils.getFullOrConvertedType(
  463. video.mimeType, audioCodecs, ContentType.AUDIO);
  464. mediaDecodingConfig.audio = {
  465. contentType: audioFullType,
  466. channels: 2,
  467. bitrate: variant.bandwidth || 1,
  468. samplerate: 1,
  469. spatialRendering: false,
  470. };
  471. }
  472. videoCodecs = shaka.util.StreamUtils.patchVp9(videoCodecs);
  473. const fullType = shaka.util.MimeUtils.getFullOrConvertedType(
  474. video.mimeType, videoCodecs, ContentType.VIDEO);
  475. // VideoConfiguration
  476. mediaDecodingConfig.video = {
  477. contentType: fullType,
  478. width: video.width || 1,
  479. height: video.height || 1,
  480. bitrate: video.bandwidth || variant.bandwidth || 1,
  481. // framerate must be greater than 0, otherwise the config is invalid.
  482. framerate: video.frameRate || 1,
  483. };
  484. if (video.hdr) {
  485. switch (video.hdr) {
  486. case 'SDR':
  487. mediaDecodingConfig.video.transferFunction = 'srgb';
  488. break;
  489. case 'PQ':
  490. mediaDecodingConfig.video.transferFunction = 'pq';
  491. break;
  492. case 'HLG':
  493. mediaDecodingConfig.video.transferFunction = 'hlg';
  494. break;
  495. }
  496. }
  497. }
  498. if (audio) {
  499. // Some Tizen devices seem to misreport AC-3 support, but correctly
  500. // report EC-3 support. So query EC-3 as a fallback for AC-3.
  501. // See https://github.com/google/shaka-player/issues/2989 for details.
  502. const codecs =
  503. (audio.codecs.toLowerCase() == 'ac-3' &&
  504. shaka.util.Platform.isTizen()) ? 'ec-3' : audio.codecs;
  505. const fullType = shaka.util.MimeUtils.getFullOrConvertedType(
  506. audio.mimeType, codecs, ContentType.AUDIO);
  507. // AudioConfiguration
  508. mediaDecodingConfig.audio = {
  509. contentType: fullType,
  510. channels: audio.channelsCount || 2,
  511. bitrate: audio.bandwidth || variant.bandwidth || 1,
  512. samplerate: audio.audioSamplingRate || 1,
  513. spatialRendering: audio.spatialAudio,
  514. };
  515. }
  516. const videoDrmInfos = variant.video ? variant.video.drmInfos : [];
  517. const audioDrmInfos = variant.audio ? variant.audio.drmInfos : [];
  518. const allDrmInfos = videoDrmInfos.concat(audioDrmInfos);
  519. // Return a list containing the mediaDecodingConfig for unencrypted variant.
  520. if (!allDrmInfos.length) {
  521. return [mediaDecodingConfig];
  522. }
  523. // A list of MediaDecodingConfiguration objects created for the variant.
  524. const configs = [];
  525. // Get all the drm info so that we can avoid using nested loops when we
  526. // just need the drm info.
  527. const drmInfoByKeySystems = new Map();
  528. for (const info of allDrmInfos) {
  529. if (!drmInfoByKeySystems.get(info.keySystem)) {
  530. drmInfoByKeySystems.set(info.keySystem, []);
  531. }
  532. drmInfoByKeySystems.get(info.keySystem).push(info);
  533. }
  534. const persistentState =
  535. usePersistentLicenses ? 'required' : 'optional';
  536. const sessionTypes =
  537. usePersistentLicenses ? ['persistent-license'] : ['temporary'];
  538. for (const keySystem of drmInfoByKeySystems.keys()) {
  539. // Create a copy of the mediaDecodingConfig.
  540. const config = /** @type {!MediaDecodingConfiguration} */
  541. (Object.assign({}, mediaDecodingConfig));
  542. const drmInfos = drmInfoByKeySystems.get(keySystem);
  543. /** @type {!MediaCapabilitiesKeySystemConfiguration} */
  544. const keySystemConfig = {
  545. keySystem: keySystem,
  546. initDataType: 'cenc',
  547. persistentState: persistentState,
  548. distinctiveIdentifier: 'optional',
  549. sessionTypes: sessionTypes,
  550. };
  551. for (const info of drmInfos) {
  552. if (info.initData && info.initData.length) {
  553. const initDataTypes = new Set();
  554. for (const initData of info.initData) {
  555. initDataTypes.add(initData.initDataType);
  556. }
  557. if (initDataTypes.size > 1) {
  558. shaka.log.v2('DrmInfo contains more than one initDataType,',
  559. 'and we use the initDataType of the first initData.',
  560. info);
  561. }
  562. keySystemConfig.initDataType = info.initData[0].initDataType;
  563. }
  564. if (info.distinctiveIdentifierRequired) {
  565. keySystemConfig.distinctiveIdentifier = 'required';
  566. }
  567. if (info.persistentStateRequired) {
  568. keySystemConfig.persistentState = 'required';
  569. }
  570. if (info.sessionType) {
  571. keySystemConfig.sessionTypes = [info.sessionType];
  572. }
  573. if (audio) {
  574. if (!keySystemConfig.audio) {
  575. // KeySystemTrackConfiguration
  576. keySystemConfig.audio = {
  577. robustness: info.audioRobustness,
  578. };
  579. } else {
  580. keySystemConfig.audio.robustness =
  581. keySystemConfig.audio.robustness || info.audioRobustness;
  582. }
  583. }
  584. if (video) {
  585. if (!keySystemConfig.video) {
  586. // KeySystemTrackConfiguration
  587. keySystemConfig.video = {
  588. robustness: info.videoRobustness,
  589. };
  590. } else {
  591. keySystemConfig.video.robustness =
  592. keySystemConfig.video.robustness || info.videoRobustness;
  593. }
  594. }
  595. }
  596. config.keySystemConfiguration = keySystemConfig;
  597. configs.push(config);
  598. }
  599. return configs;
  600. }
  601. /**
  602. * MediaCapabilities supports 'vp09...' codecs, but not 'vp9'. Translate vp9
  603. * codec strings into 'vp09...', to allow such content to play with
  604. * mediaCapabilities enabled.
  605. *
  606. * @param {string} codec
  607. * @return {string}
  608. */
  609. static patchVp9(codec) {
  610. if (codec == 'vp9') {
  611. return 'vp09.00.10.08';
  612. }
  613. return codec;
  614. }
  615. /**
  616. * Alters the given Manifest to filter out any streams uncompatible with the
  617. * current variant.
  618. *
  619. * @param {?shaka.extern.Variant} currentVariant
  620. * @param {shaka.extern.Manifest} manifest
  621. */
  622. static filterManifestByCurrentVariant(currentVariant, manifest) {
  623. const StreamUtils = shaka.util.StreamUtils;
  624. manifest.variants = manifest.variants.filter((variant) => {
  625. const audio = variant.audio;
  626. const video = variant.video;
  627. if (audio && currentVariant && currentVariant.audio) {
  628. if (!StreamUtils.areStreamsCompatible_(audio, currentVariant.audio)) {
  629. shaka.log.debug('Droping variant - not compatible with active audio',
  630. 'active audio',
  631. StreamUtils.getStreamSummaryString_(currentVariant.audio),
  632. 'variant.audio',
  633. StreamUtils.getStreamSummaryString_(audio));
  634. return false;
  635. }
  636. }
  637. if (video && currentVariant && currentVariant.video) {
  638. if (!StreamUtils.areStreamsCompatible_(video, currentVariant.video)) {
  639. shaka.log.debug('Droping variant - not compatible with active video',
  640. 'active video',
  641. StreamUtils.getStreamSummaryString_(currentVariant.video),
  642. 'variant.video',
  643. StreamUtils.getStreamSummaryString_(video));
  644. return false;
  645. }
  646. }
  647. return true;
  648. });
  649. }
  650. /**
  651. * Alters the given Manifest to filter out any unsupported text streams.
  652. *
  653. * @param {shaka.extern.Manifest} manifest
  654. * @private
  655. */
  656. static filterTextStreams_(manifest) {
  657. // Filter text streams.
  658. manifest.textStreams = manifest.textStreams.filter((stream) => {
  659. const fullMimeType = shaka.util.MimeUtils.getFullType(
  660. stream.mimeType, stream.codecs);
  661. const keep = shaka.text.TextEngine.isTypeSupported(fullMimeType);
  662. if (!keep) {
  663. shaka.log.debug('Dropping text stream. Is not supported by the ' +
  664. 'platform.', stream);
  665. }
  666. return keep;
  667. });
  668. }
  669. /**
  670. * Alters the given Manifest to filter out any unsupported image streams.
  671. *
  672. * @param {shaka.extern.Manifest} manifest
  673. * @private
  674. */
  675. static filterImageStreams_(manifest) {
  676. // Filter image streams.
  677. manifest.imageStreams = manifest.imageStreams.filter((stream) => {
  678. // TODO: re-examine this and avoid allow-listing the MIME types we can
  679. // accept.
  680. const validMimeTypes = [
  681. 'image/svg+xml',
  682. 'image/png',
  683. 'image/jpeg',
  684. ];
  685. const Platform = shaka.util.Platform;
  686. // Add webp support to popular platforms that support it.
  687. const webpSupport = Platform.isWebOS() ||
  688. Platform.isTizen() ||
  689. Platform.isChromecast();
  690. if (webpSupport) {
  691. validMimeTypes.push('image/webp');
  692. }
  693. // TODO: add support to image/webp and image/avif
  694. const keep = validMimeTypes.includes(stream.mimeType);
  695. if (!keep) {
  696. shaka.log.debug('Dropping image stream. Is not supported by the ' +
  697. 'platform.', stream);
  698. }
  699. return keep;
  700. });
  701. }
  702. /**
  703. * @param {shaka.extern.Stream} s0
  704. * @param {shaka.extern.Stream} s1
  705. * @return {boolean}
  706. * @private
  707. */
  708. static areStreamsCompatible_(s0, s1) {
  709. // Basic mime types and basic codecs need to match.
  710. // For example, we can't adapt between WebM and MP4,
  711. // nor can we adapt between mp4a.* to ec-3.
  712. // We can switch between text types on the fly,
  713. // so don't run this check on text.
  714. if (s0.mimeType != s1.mimeType) {
  715. return false;
  716. }
  717. if (s0.codecs.split('.')[0] != s1.codecs.split('.')[0]) {
  718. return false;
  719. }
  720. return true;
  721. }
  722. /**
  723. * @param {shaka.extern.Variant} variant
  724. * @return {shaka.extern.Track}
  725. */
  726. static variantToTrack(variant) {
  727. /** @type {?shaka.extern.Stream} */
  728. const audio = variant.audio;
  729. /** @type {?shaka.extern.Stream} */
  730. const video = variant.video;
  731. /** @type {?string} */
  732. const audioCodec = audio ? audio.codecs : null;
  733. /** @type {?string} */
  734. const videoCodec = video ? video.codecs : null;
  735. /** @type {!Array.<string>} */
  736. const codecs = [];
  737. if (videoCodec) {
  738. codecs.push(videoCodec);
  739. }
  740. if (audioCodec) {
  741. codecs.push(audioCodec);
  742. }
  743. /** @type {!Array.<string>} */
  744. const mimeTypes = [];
  745. if (video) {
  746. mimeTypes.push(video.mimeType);
  747. }
  748. if (audio) {
  749. mimeTypes.push(audio.mimeType);
  750. }
  751. /** @type {?string} */
  752. const mimeType = mimeTypes[0] || null;
  753. /** @type {!Array.<string>} */
  754. const kinds = [];
  755. if (audio) {
  756. kinds.push(audio.kind);
  757. }
  758. if (video) {
  759. kinds.push(video.kind);
  760. }
  761. /** @type {?string} */
  762. const kind = kinds[0] || null;
  763. /** @type {!Set.<string>} */
  764. const roles = new Set();
  765. if (audio) {
  766. for (const role of audio.roles) {
  767. roles.add(role);
  768. }
  769. }
  770. if (video) {
  771. for (const role of video.roles) {
  772. roles.add(role);
  773. }
  774. }
  775. /** @type {shaka.extern.Track} */
  776. const track = {
  777. id: variant.id,
  778. active: false,
  779. type: 'variant',
  780. bandwidth: variant.bandwidth,
  781. language: variant.language,
  782. label: null,
  783. kind: kind,
  784. width: null,
  785. height: null,
  786. frameRate: null,
  787. pixelAspectRatio: null,
  788. hdr: null,
  789. mimeType: mimeType,
  790. codecs: codecs.join(', '),
  791. audioCodec: audioCodec,
  792. videoCodec: videoCodec,
  793. primary: variant.primary,
  794. roles: Array.from(roles),
  795. audioRoles: null,
  796. forced: false,
  797. videoId: null,
  798. audioId: null,
  799. channelsCount: null,
  800. audioSamplingRate: null,
  801. spatialAudio: false,
  802. tilesLayout: null,
  803. audioBandwidth: null,
  804. videoBandwidth: null,
  805. originalVideoId: null,
  806. originalAudioId: null,
  807. originalTextId: null,
  808. originalImageId: null,
  809. };
  810. if (video) {
  811. track.videoId = video.id;
  812. track.originalVideoId = video.originalId;
  813. track.width = video.width || null;
  814. track.height = video.height || null;
  815. track.frameRate = video.frameRate || null;
  816. track.pixelAspectRatio = video.pixelAspectRatio || null;
  817. track.videoBandwidth = video.bandwidth || null;
  818. }
  819. if (audio) {
  820. track.audioId = audio.id;
  821. track.originalAudioId = audio.originalId;
  822. track.channelsCount = audio.channelsCount;
  823. track.audioSamplingRate = audio.audioSamplingRate;
  824. track.audioBandwidth = audio.bandwidth || null;
  825. track.label = audio.label;
  826. track.audioRoles = audio.roles;
  827. }
  828. return track;
  829. }
  830. /**
  831. * @param {shaka.extern.Stream} stream
  832. * @return {shaka.extern.Track}
  833. */
  834. static textStreamToTrack(stream) {
  835. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  836. /** @type {shaka.extern.Track} */
  837. const track = {
  838. id: stream.id,
  839. active: false,
  840. type: ContentType.TEXT,
  841. bandwidth: 0,
  842. language: stream.language,
  843. label: stream.label,
  844. kind: stream.kind || null,
  845. width: null,
  846. height: null,
  847. frameRate: null,
  848. pixelAspectRatio: null,
  849. hdr: null,
  850. mimeType: stream.mimeType,
  851. codecs: stream.codecs || null,
  852. audioCodec: null,
  853. videoCodec: null,
  854. primary: stream.primary,
  855. roles: stream.roles,
  856. audioRoles: null,
  857. forced: stream.forced,
  858. videoId: null,
  859. audioId: null,
  860. channelsCount: null,
  861. audioSamplingRate: null,
  862. spatialAudio: false,
  863. tilesLayout: null,
  864. audioBandwidth: null,
  865. videoBandwidth: null,
  866. originalVideoId: null,
  867. originalAudioId: null,
  868. originalTextId: stream.originalId,
  869. originalImageId: null,
  870. };
  871. return track;
  872. }
  873. /**
  874. * @param {shaka.extern.Stream} stream
  875. * @return {shaka.extern.Track}
  876. */
  877. static imageStreamToTrack(stream) {
  878. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  879. /** @type {shaka.extern.Track} */
  880. const track = {
  881. id: stream.id,
  882. active: false,
  883. type: ContentType.IMAGE,
  884. bandwidth: stream.bandwidth || 0,
  885. language: '',
  886. label: null,
  887. kind: null,
  888. width: stream.width || null,
  889. height: stream.height || null,
  890. frameRate: null,
  891. pixelAspectRatio: null,
  892. hdr: null,
  893. mimeType: stream.mimeType,
  894. codecs: null,
  895. audioCodec: null,
  896. videoCodec: null,
  897. primary: false,
  898. roles: [],
  899. audioRoles: null,
  900. forced: false,
  901. videoId: null,
  902. audioId: null,
  903. channelsCount: null,
  904. audioSamplingRate: null,
  905. spatialAudio: false,
  906. tilesLayout: stream.tilesLayout || null,
  907. audioBandwidth: null,
  908. videoBandwidth: null,
  909. originalVideoId: null,
  910. originalAudioId: null,
  911. originalTextId: null,
  912. originalImageId: stream.originalId,
  913. };
  914. return track;
  915. }
  916. /**
  917. * Generate and return an ID for this track, since the ID field is optional.
  918. *
  919. * @param {TextTrack|AudioTrack} html5Track
  920. * @return {number} The generated ID.
  921. */
  922. static html5TrackId(html5Track) {
  923. if (!html5Track['__shaka_id']) {
  924. html5Track['__shaka_id'] = shaka.util.StreamUtils.nextTrackId_++;
  925. }
  926. return html5Track['__shaka_id'];
  927. }
  928. /**
  929. * @param {TextTrack} textTrack
  930. * @return {shaka.extern.Track}
  931. */
  932. static html5TextTrackToTrack(textTrack) {
  933. const CLOSED_CAPTION_MIMETYPE =
  934. shaka.util.MimeUtils.CEA608_CLOSED_CAPTION_MIMETYPE;
  935. const StreamUtils = shaka.util.StreamUtils;
  936. /** @type {shaka.extern.Track} */
  937. const track = StreamUtils.html5TrackToGenericShakaTrack_(textTrack);
  938. track.active = textTrack.mode != 'disabled';
  939. track.type = 'text';
  940. track.originalTextId = textTrack.id;
  941. if (textTrack.kind == 'captions') {
  942. track.mimeType = CLOSED_CAPTION_MIMETYPE;
  943. }
  944. if (textTrack.kind) {
  945. track.roles = [textTrack.kind];
  946. }
  947. if (textTrack.kind == 'forced') {
  948. track.forced = true;
  949. }
  950. return track;
  951. }
  952. /**
  953. * @param {AudioTrack} audioTrack
  954. * @return {shaka.extern.Track}
  955. */
  956. static html5AudioTrackToTrack(audioTrack) {
  957. const StreamUtils = shaka.util.StreamUtils;
  958. /** @type {shaka.extern.Track} */
  959. const track = StreamUtils.html5TrackToGenericShakaTrack_(audioTrack);
  960. track.active = audioTrack.enabled;
  961. track.type = 'variant';
  962. track.originalAudioId = audioTrack.id;
  963. if (audioTrack.kind == 'main') {
  964. track.primary = true;
  965. }
  966. if (audioTrack.kind) {
  967. track.roles = [audioTrack.kind];
  968. track.audioRoles = [audioTrack.kind];
  969. track.label = audioTrack.label;
  970. }
  971. return track;
  972. }
  973. /**
  974. * Creates a Track object with non-type specific fields filled out. The
  975. * caller is responsible for completing the Track object with any
  976. * type-specific information (audio or text).
  977. *
  978. * @param {TextTrack|AudioTrack} html5Track
  979. * @return {shaka.extern.Track}
  980. * @private
  981. */
  982. static html5TrackToGenericShakaTrack_(html5Track) {
  983. /** @type {shaka.extern.Track} */
  984. const track = {
  985. id: shaka.util.StreamUtils.html5TrackId(html5Track),
  986. active: false,
  987. type: '',
  988. bandwidth: 0,
  989. language: shaka.util.LanguageUtils.normalize(html5Track.language),
  990. label: html5Track.label,
  991. kind: html5Track.kind,
  992. width: null,
  993. height: null,
  994. frameRate: null,
  995. pixelAspectRatio: null,
  996. hdr: null,
  997. mimeType: null,
  998. codecs: null,
  999. audioCodec: null,
  1000. videoCodec: null,
  1001. primary: false,
  1002. roles: [],
  1003. forced: false,
  1004. audioRoles: null,
  1005. videoId: null,
  1006. audioId: null,
  1007. channelsCount: null,
  1008. audioSamplingRate: null,
  1009. spatialAudio: false,
  1010. tilesLayout: null,
  1011. audioBandwidth: null,
  1012. videoBandwidth: null,
  1013. originalVideoId: null,
  1014. originalAudioId: null,
  1015. originalTextId: null,
  1016. originalImageId: null,
  1017. };
  1018. return track;
  1019. }
  1020. /**
  1021. * Determines if the given variant is playable.
  1022. * @param {!shaka.extern.Variant} variant
  1023. * @return {boolean}
  1024. */
  1025. static isPlayable(variant) {
  1026. return variant.allowedByApplication && variant.allowedByKeySystem;
  1027. }
  1028. /**
  1029. * Filters out unplayable variants.
  1030. * @param {!Array.<!shaka.extern.Variant>} variants
  1031. * @return {!Array.<!shaka.extern.Variant>}
  1032. */
  1033. static getPlayableVariants(variants) {
  1034. return variants.filter((variant) => {
  1035. return shaka.util.StreamUtils.isPlayable(variant);
  1036. });
  1037. }
  1038. /**
  1039. * Filters variants according to the given audio channel count config.
  1040. *
  1041. * @param {!Array.<shaka.extern.Variant>} variants
  1042. * @param {number} preferredAudioChannelCount
  1043. * @return {!Array.<!shaka.extern.Variant>}
  1044. */
  1045. static filterVariantsByAudioChannelCount(
  1046. variants, preferredAudioChannelCount) {
  1047. // Group variants by their audio channel counts.
  1048. const variantsWithChannelCounts =
  1049. variants.filter((v) => v.audio && v.audio.channelsCount);
  1050. /** @type {!Map.<number, !Array.<shaka.extern.Variant>>} */
  1051. const variantsByChannelCount = new Map();
  1052. for (const variant of variantsWithChannelCounts) {
  1053. const count = variant.audio.channelsCount;
  1054. goog.asserts.assert(count != null, 'Must have count after filtering!');
  1055. if (!variantsByChannelCount.has(count)) {
  1056. variantsByChannelCount.set(count, []);
  1057. }
  1058. variantsByChannelCount.get(count).push(variant);
  1059. }
  1060. /** @type {!Array.<number>} */
  1061. const channelCounts = Array.from(variantsByChannelCount.keys());
  1062. // If no variant has audio channel count info, return the original variants.
  1063. if (channelCounts.length == 0) {
  1064. return variants;
  1065. }
  1066. // Choose the variants with the largest number of audio channels less than
  1067. // or equal to the configured number of audio channels.
  1068. const countLessThanOrEqualtoConfig =
  1069. channelCounts.filter((count) => count <= preferredAudioChannelCount);
  1070. if (countLessThanOrEqualtoConfig.length) {
  1071. return variantsByChannelCount.get(
  1072. Math.max(...countLessThanOrEqualtoConfig));
  1073. }
  1074. // If all variants have more audio channels than the config, choose the
  1075. // variants with the fewest audio channels.
  1076. return variantsByChannelCount.get(Math.min(...channelCounts));
  1077. }
  1078. /**
  1079. * Chooses streams according to the given config.
  1080. *
  1081. * @param {!Array.<shaka.extern.Stream>} streams
  1082. * @param {string} preferredLanguage
  1083. * @param {string} preferredRole
  1084. * @param {boolean} preferredForced
  1085. * @return {!Array.<!shaka.extern.Stream>}
  1086. */
  1087. static filterStreamsByLanguageAndRole(
  1088. streams, preferredLanguage, preferredRole, preferredForced) {
  1089. const LanguageUtils = shaka.util.LanguageUtils;
  1090. /** @type {!Array.<!shaka.extern.Stream>} */
  1091. let chosen = streams;
  1092. // Start with the set of primary streams.
  1093. /** @type {!Array.<!shaka.extern.Stream>} */
  1094. const primary = streams.filter((stream) => {
  1095. return stream.primary;
  1096. });
  1097. if (primary.length) {
  1098. chosen = primary;
  1099. }
  1100. // Now reduce the set to one language. This covers both arbitrary language
  1101. // choice and the reduction of the "primary" stream set to one language.
  1102. const firstLanguage = chosen.length ? chosen[0].language : '';
  1103. chosen = chosen.filter((stream) => {
  1104. return stream.language == firstLanguage;
  1105. });
  1106. // Find the streams that best match our language preference. This will
  1107. // override previous selections.
  1108. if (preferredLanguage) {
  1109. const closestLocale = LanguageUtils.findClosestLocale(
  1110. LanguageUtils.normalize(preferredLanguage),
  1111. streams.map((stream) => stream.language));
  1112. // Only replace |chosen| if we found a locale that is close to our
  1113. // preference.
  1114. if (closestLocale) {
  1115. chosen = streams.filter((stream) => {
  1116. const locale = LanguageUtils.normalize(stream.language);
  1117. return locale == closestLocale;
  1118. });
  1119. }
  1120. }
  1121. // Filter by forced preference
  1122. chosen = chosen.filter((stream) => {
  1123. return stream.forced == preferredForced;
  1124. });
  1125. // Now refine the choice based on role preference.
  1126. if (preferredRole) {
  1127. const roleMatches = shaka.util.StreamUtils.filterTextStreamsByRole_(
  1128. chosen, preferredRole);
  1129. if (roleMatches.length) {
  1130. return roleMatches;
  1131. } else {
  1132. shaka.log.warning('No exact match for the text role could be found.');
  1133. }
  1134. } else {
  1135. // Prefer text streams with no roles, if they exist.
  1136. const noRoleMatches = chosen.filter((stream) => {
  1137. return stream.roles.length == 0;
  1138. });
  1139. if (noRoleMatches.length) {
  1140. return noRoleMatches;
  1141. }
  1142. }
  1143. // Either there was no role preference, or it could not be satisfied.
  1144. // Choose an arbitrary role, if there are any, and filter out any other
  1145. // roles. This ensures we never adapt between roles.
  1146. const allRoles = chosen.map((stream) => {
  1147. return stream.roles;
  1148. }).reduce(shaka.util.Functional.collapseArrays, []);
  1149. if (!allRoles.length) {
  1150. return chosen;
  1151. }
  1152. return shaka.util.StreamUtils.filterTextStreamsByRole_(chosen, allRoles[0]);
  1153. }
  1154. /**
  1155. * Filter text Streams by role.
  1156. *
  1157. * @param {!Array.<shaka.extern.Stream>} textStreams
  1158. * @param {string} preferredRole
  1159. * @return {!Array.<shaka.extern.Stream>}
  1160. * @private
  1161. */
  1162. static filterTextStreamsByRole_(textStreams, preferredRole) {
  1163. return textStreams.filter((stream) => {
  1164. return stream.roles.includes(preferredRole);
  1165. });
  1166. }
  1167. /**
  1168. * Checks if the given stream is an audio stream.
  1169. *
  1170. * @param {shaka.extern.Stream} stream
  1171. * @return {boolean}
  1172. */
  1173. static isAudio(stream) {
  1174. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  1175. return stream.type == ContentType.AUDIO;
  1176. }
  1177. /**
  1178. * Checks if the given stream is a video stream.
  1179. *
  1180. * @param {shaka.extern.Stream} stream
  1181. * @return {boolean}
  1182. */
  1183. static isVideo(stream) {
  1184. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  1185. return stream.type == ContentType.VIDEO;
  1186. }
  1187. /**
  1188. * Get all non-null streams in the variant as an array.
  1189. *
  1190. * @param {shaka.extern.Variant} variant
  1191. * @return {!Array.<shaka.extern.Stream>}
  1192. */
  1193. static getVariantStreams(variant) {
  1194. const streams = [];
  1195. if (variant.audio) {
  1196. streams.push(variant.audio);
  1197. }
  1198. if (variant.video) {
  1199. streams.push(variant.video);
  1200. }
  1201. return streams;
  1202. }
  1203. /**
  1204. * Returns a string of a variant, with the attribute values of its audio
  1205. * and/or video streams for log printing.
  1206. * @param {shaka.extern.Variant} variant
  1207. * @return {string}
  1208. * @private
  1209. */
  1210. static getVariantSummaryString_(variant) {
  1211. const summaries = [];
  1212. if (variant.audio) {
  1213. summaries.push(shaka.util.StreamUtils.getStreamSummaryString_(
  1214. variant.audio));
  1215. }
  1216. if (variant.video) {
  1217. summaries.push(shaka.util.StreamUtils.getStreamSummaryString_(
  1218. variant.video));
  1219. }
  1220. return summaries.join(', ');
  1221. }
  1222. /**
  1223. * Returns a string of an audio or video stream for log printing.
  1224. * @param {shaka.extern.Stream} stream
  1225. * @return {string}
  1226. * @private
  1227. */
  1228. static getStreamSummaryString_(stream) {
  1229. // Accepted parameters for Chromecast can be found (internally) at
  1230. // go/cast-mime-params
  1231. if (shaka.util.StreamUtils.isAudio(stream)) {
  1232. return 'type=audio' +
  1233. ' codecs=' + stream.codecs +
  1234. ' bandwidth='+ stream.bandwidth +
  1235. ' channelsCount=' + stream.channelsCount +
  1236. ' audioSamplingRate=' + stream.audioSamplingRate;
  1237. }
  1238. if (shaka.util.StreamUtils.isVideo(stream)) {
  1239. return 'type=video' +
  1240. ' codecs=' + stream.codecs +
  1241. ' bandwidth=' + stream.bandwidth +
  1242. ' frameRate=' + stream.frameRate +
  1243. ' width=' + stream.width +
  1244. ' height=' + stream.height;
  1245. }
  1246. return 'unexpected stream type';
  1247. }
  1248. };
  1249. /** @private {number} */
  1250. shaka.util.StreamUtils.nextTrackId_ = 0;
  1251. /**
  1252. * @enum {string}
  1253. */
  1254. shaka.util.StreamUtils.DecodingAttributes = {
  1255. SMOOTH: 'smooth',
  1256. POWER: 'powerEfficient',
  1257. BANDWIDTH: 'bandwidth',
  1258. };