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.media.Capabilities');
  10. goog.require('shaka.text.TextEngine');
  11. goog.require('shaka.util.Functional');
  12. goog.require('shaka.util.LanguageUtils');
  13. goog.require('shaka.util.ManifestParserUtils');
  14. goog.require('shaka.util.MimeUtils');
  15. goog.require('shaka.util.MultiMap');
  16. goog.require('shaka.util.ObjectUtils');
  17. goog.require('shaka.util.Platform');
  18. goog.requireType('shaka.media.DrmEngine');
  19. /**
  20. * @summary A set of utility functions for dealing with Streams and Manifests.
  21. * @export
  22. */
  23. shaka.util.StreamUtils = class {
  24. /**
  25. * In case of multiple usable codecs, choose one based on lowest average
  26. * bandwidth and filter out the rest.
  27. * Also filters out variants that have too many audio channels.
  28. * @param {!shaka.extern.Manifest} manifest
  29. * @param {!Array.<string>} preferredVideoCodecs
  30. * @param {!Array.<string>} preferredAudioCodecs
  31. * @param {!Array.<string>} preferredDecodingAttributes
  32. */
  33. static chooseCodecsAndFilterManifest(manifest, preferredVideoCodecs,
  34. preferredAudioCodecs, preferredDecodingAttributes) {
  35. const StreamUtils = shaka.util.StreamUtils;
  36. const MimeUtils = shaka.util.MimeUtils;
  37. let variants = manifest.variants;
  38. // To start, choose the codecs based on configured preferences if available.
  39. if (preferredVideoCodecs.length || preferredAudioCodecs.length) {
  40. variants = StreamUtils.choosePreferredCodecs(variants,
  41. preferredVideoCodecs, preferredAudioCodecs);
  42. }
  43. if (preferredDecodingAttributes.length) {
  44. // group variants by resolution and choose preferred variants only
  45. /** @type {!shaka.util.MultiMap.<shaka.extern.Variant>} */
  46. const variantsByResolutionMap = new shaka.util.MultiMap();
  47. for (const variant of variants) {
  48. variantsByResolutionMap
  49. .push(String(variant.video.width || 0), variant);
  50. }
  51. const bestVariants = [];
  52. variantsByResolutionMap.forEach((width, variantsByResolution) => {
  53. let highestMatch = 0;
  54. let matchingVariants = [];
  55. for (const variant of variantsByResolution) {
  56. const matchCount = preferredDecodingAttributes.filter(
  57. (attribute) => variant.decodingInfos[0][attribute],
  58. ).length;
  59. if (matchCount > highestMatch) {
  60. highestMatch = matchCount;
  61. matchingVariants = [variant];
  62. } else if (matchCount == highestMatch) {
  63. matchingVariants.push(variant);
  64. }
  65. }
  66. bestVariants.push(...matchingVariants);
  67. });
  68. variants = bestVariants;
  69. }
  70. const audioStreamsSet = new Set();
  71. const videoStreamsSet = new Set();
  72. for (const variant of variants) {
  73. if (variant.audio) {
  74. audioStreamsSet.add(variant.audio);
  75. }
  76. if (variant.video) {
  77. videoStreamsSet.add(variant.video);
  78. }
  79. }
  80. const audioStreams = Array.from(audioStreamsSet).sort((v1, v2) => {
  81. return v1.bandwidth - v2.bandwidth;
  82. });
  83. const validAudioIds = [];
  84. const validAudioStreamsMap = new Map();
  85. const getAudioId = (stream) => {
  86. return stream.language + (stream.channelsCount || 0) +
  87. (stream.audioSamplingRate || 0) + stream.roles.join(',') +
  88. stream.label + stream.groupId + stream.fastSwitching;
  89. };
  90. for (const stream of audioStreams) {
  91. const groupId = getAudioId(stream);
  92. const validAudioStreams = validAudioStreamsMap.get(groupId) || [];
  93. if (!validAudioStreams.length) {
  94. validAudioStreams.push(stream);
  95. validAudioIds.push(stream.id);
  96. } else {
  97. const previousStream = validAudioStreams[validAudioStreams.length - 1];
  98. const previousCodec =
  99. MimeUtils.getNormalizedCodec(previousStream.codecs);
  100. const currentCodec =
  101. MimeUtils.getNormalizedCodec(stream.codecs);
  102. if (previousCodec == currentCodec) {
  103. if (!stream.bandwidth || !previousStream.bandwidth ||
  104. stream.bandwidth > previousStream.bandwidth) {
  105. validAudioStreams.push(stream);
  106. validAudioIds.push(stream.id);
  107. }
  108. }
  109. }
  110. validAudioStreamsMap.set(groupId, validAudioStreams);
  111. }
  112. const videoStreams = Array.from(videoStreamsSet)
  113. .sort((v1, v2) => {
  114. if (!v1.bandwidth || !v2.bandwidth) {
  115. return v1.width - v2.width;
  116. }
  117. return v1.bandwidth - v2.bandwidth;
  118. });
  119. const isChangeTypeSupported =
  120. shaka.media.Capabilities.isChangeTypeSupported();
  121. const validVideoIds = [];
  122. const validVideoStreamsMap = new Map();
  123. const getVideoGroupId = (stream) => {
  124. return Math.round(stream.frameRate || 0) + (stream.hdr || '') +
  125. stream.fastSwitching;
  126. };
  127. for (const stream of videoStreams) {
  128. const groupId = getVideoGroupId(stream);
  129. const validVideoStreams = validVideoStreamsMap.get(groupId) || [];
  130. if (!validVideoStreams.length) {
  131. validVideoStreams.push(stream);
  132. validVideoIds.push(stream.id);
  133. } else {
  134. const previousStream = validVideoStreams[validVideoStreams.length - 1];
  135. if (!isChangeTypeSupported) {
  136. const previousCodec =
  137. MimeUtils.getNormalizedCodec(previousStream.codecs);
  138. const currentCodec =
  139. MimeUtils.getNormalizedCodec(stream.codecs);
  140. if (previousCodec !== currentCodec) {
  141. continue;
  142. }
  143. }
  144. if (stream.width > previousStream.width ||
  145. stream.height > previousStream.height) {
  146. validVideoStreams.push(stream);
  147. validVideoIds.push(stream.id);
  148. } else if (stream.width == previousStream.width &&
  149. stream.height == previousStream.height) {
  150. const previousCodec =
  151. MimeUtils.getNormalizedCodec(previousStream.codecs);
  152. const currentCodec =
  153. MimeUtils.getNormalizedCodec(stream.codecs);
  154. if (previousCodec == currentCodec) {
  155. if (!stream.bandwidth || !previousStream.bandwidth ||
  156. stream.bandwidth > previousStream.bandwidth) {
  157. validVideoStreams.push(stream);
  158. validVideoIds.push(stream.id);
  159. }
  160. }
  161. }
  162. }
  163. validVideoStreamsMap.set(groupId, validVideoStreams);
  164. }
  165. // Filter out any variants that don't match, forcing AbrManager to choose
  166. // from a single video codec and a single audio codec possible.
  167. manifest.variants = manifest.variants.filter((variant) => {
  168. const audio = variant.audio;
  169. const video = variant.video;
  170. if (audio) {
  171. if (!validAudioIds.includes(audio.id)) {
  172. shaka.log.debug('Dropping Variant (better codec available)', variant);
  173. return false;
  174. }
  175. }
  176. if (video) {
  177. if (!validVideoIds.includes(video.id)) {
  178. shaka.log.debug('Dropping Variant (better codec available)', variant);
  179. return false;
  180. }
  181. }
  182. return true;
  183. });
  184. }
  185. /**
  186. * Choose the codecs by configured preferred audio and video codecs.
  187. *
  188. * @param {!Array<shaka.extern.Variant>} variants
  189. * @param {!Array.<string>} preferredVideoCodecs
  190. * @param {!Array.<string>} preferredAudioCodecs
  191. * @return {!Array<shaka.extern.Variant>}
  192. */
  193. static choosePreferredCodecs(variants, preferredVideoCodecs,
  194. preferredAudioCodecs) {
  195. let subset = variants;
  196. for (const videoCodec of preferredVideoCodecs) {
  197. const filtered = subset.filter((variant) => {
  198. return variant.video && variant.video.codecs.startsWith(videoCodec);
  199. });
  200. if (filtered.length) {
  201. subset = filtered;
  202. break;
  203. }
  204. }
  205. for (const audioCodec of preferredAudioCodecs) {
  206. const filtered = subset.filter((variant) => {
  207. return variant.audio && variant.audio.codecs.startsWith(audioCodec);
  208. });
  209. if (filtered.length) {
  210. subset = filtered;
  211. break;
  212. }
  213. }
  214. return subset;
  215. }
  216. /**
  217. * Filter the variants in |manifest| to only include the variants that meet
  218. * the given restrictions.
  219. *
  220. * @param {!shaka.extern.Manifest} manifest
  221. * @param {shaka.extern.Restrictions} restrictions
  222. * @param {shaka.extern.Resolution} maxHwResolution
  223. */
  224. static filterByRestrictions(manifest, restrictions, maxHwResolution) {
  225. manifest.variants = manifest.variants.filter((variant) => {
  226. return shaka.util.StreamUtils.meetsRestrictions(
  227. variant, restrictions, maxHwResolution);
  228. });
  229. }
  230. /**
  231. * @param {shaka.extern.Variant} variant
  232. * @param {shaka.extern.Restrictions} restrictions
  233. * Configured restrictions from the user.
  234. * @param {shaka.extern.Resolution} maxHwRes
  235. * The maximum resolution the hardware can handle.
  236. * This is applied separately from user restrictions because the setting
  237. * should not be easily replaced by the user's configuration.
  238. * @return {boolean}
  239. * @export
  240. */
  241. static meetsRestrictions(variant, restrictions, maxHwRes) {
  242. /** @type {function(number, number, number):boolean} */
  243. const inRange = (x, min, max) => {
  244. return x >= min && x <= max;
  245. };
  246. const video = variant.video;
  247. // |video.width| and |video.height| can be undefined, which breaks
  248. // the math, so make sure they are there first.
  249. if (video && video.width && video.height) {
  250. let videoWidth = video.width;
  251. let videoHeight = video.height;
  252. if (videoHeight > videoWidth) {
  253. // Vertical video.
  254. [videoWidth, videoHeight] = [videoHeight, videoWidth];
  255. }
  256. if (!inRange(videoWidth,
  257. restrictions.minWidth,
  258. Math.min(restrictions.maxWidth, maxHwRes.width))) {
  259. return false;
  260. }
  261. if (!inRange(videoHeight,
  262. restrictions.minHeight,
  263. Math.min(restrictions.maxHeight, maxHwRes.height))) {
  264. return false;
  265. }
  266. if (!inRange(video.width * video.height,
  267. restrictions.minPixels,
  268. restrictions.maxPixels)) {
  269. return false;
  270. }
  271. }
  272. // |variant.video.frameRate| can be undefined, which breaks
  273. // the math, so make sure they are there first.
  274. if (variant && variant.video && variant.video.frameRate) {
  275. if (!inRange(variant.video.frameRate,
  276. restrictions.minFrameRate,
  277. restrictions.maxFrameRate)) {
  278. return false;
  279. }
  280. }
  281. // |variant.audio.channelsCount| can be undefined, which breaks
  282. // the math, so make sure they are there first.
  283. if (variant && variant.audio && variant.audio.channelsCount) {
  284. if (!inRange(variant.audio.channelsCount,
  285. restrictions.minChannelsCount,
  286. restrictions.maxChannelsCount)) {
  287. return false;
  288. }
  289. }
  290. if (!inRange(variant.bandwidth,
  291. restrictions.minBandwidth,
  292. restrictions.maxBandwidth)) {
  293. return false;
  294. }
  295. return true;
  296. }
  297. /**
  298. * @param {!Array.<shaka.extern.Variant>} variants
  299. * @param {shaka.extern.Restrictions} restrictions
  300. * @param {shaka.extern.Resolution} maxHwRes
  301. * @return {boolean} Whether the tracks changed.
  302. */
  303. static applyRestrictions(variants, restrictions, maxHwRes) {
  304. let tracksChanged = false;
  305. for (const variant of variants) {
  306. const originalAllowed = variant.allowedByApplication;
  307. variant.allowedByApplication = shaka.util.StreamUtils.meetsRestrictions(
  308. variant, restrictions, maxHwRes);
  309. if (originalAllowed != variant.allowedByApplication) {
  310. tracksChanged = true;
  311. }
  312. }
  313. return tracksChanged;
  314. }
  315. /**
  316. * Alters the given Manifest to filter out any unplayable streams.
  317. *
  318. * @param {shaka.media.DrmEngine} drmEngine
  319. * @param {shaka.extern.Manifest} manifest
  320. * @param {!Array<string>=} preferredKeySystems
  321. */
  322. static async filterManifest(drmEngine, manifest, preferredKeySystems = []) {
  323. await shaka.util.StreamUtils.filterManifestByMediaCapabilities(
  324. drmEngine, manifest, manifest.offlineSessionIds.length > 0,
  325. preferredKeySystems);
  326. shaka.util.StreamUtils.filterTextStreams_(manifest);
  327. await shaka.util.StreamUtils.filterImageStreams_(manifest);
  328. }
  329. /**
  330. * Alters the given Manifest to filter out any streams unsupported by the
  331. * platform via MediaCapabilities.decodingInfo() API.
  332. *
  333. * @param {shaka.media.DrmEngine} drmEngine
  334. * @param {shaka.extern.Manifest} manifest
  335. * @param {boolean} usePersistentLicenses
  336. * @param {!Array<string>} preferredKeySystems
  337. */
  338. static async filterManifestByMediaCapabilities(
  339. drmEngine, manifest, usePersistentLicenses, preferredKeySystems) {
  340. goog.asserts.assert(navigator.mediaCapabilities,
  341. 'MediaCapabilities should be valid.');
  342. if (shaka.util.Platform.isXboxOne()) {
  343. shaka.util.StreamUtils.overrideDolbyVisionCodecs(manifest.variants);
  344. }
  345. await shaka.util.StreamUtils.getDecodingInfosForVariants(
  346. manifest.variants, usePersistentLicenses, /* srcEquals= */ false,
  347. preferredKeySystems);
  348. let keySystem = null;
  349. if (drmEngine) {
  350. const drmInfo = drmEngine.getDrmInfo();
  351. if (drmInfo) {
  352. keySystem = drmInfo.keySystem;
  353. }
  354. }
  355. const StreamUtils = shaka.util.StreamUtils;
  356. manifest.variants = manifest.variants.filter((variant) => {
  357. const supported = StreamUtils.checkVariantSupported_(variant, keySystem);
  358. // Filter out all unsupported variants.
  359. if (!supported) {
  360. shaka.log.debug('Dropping variant - not compatible with platform',
  361. StreamUtils.getVariantSummaryString_(variant));
  362. }
  363. return supported;
  364. });
  365. }
  366. /**
  367. * Maps Dolby Vision codecs to H.264 and H.265 equivalents as a workaround
  368. * to make Dolby Vision playback work on some platforms.
  369. *
  370. * Mapping is done according to the relevant Dolby documentation found here:
  371. * https://professionalsupport.dolby.com/s/article/How-to-signal-Dolby-Vision-in-MPEG-DASH?language=en_US
  372. * @param {!Array<!shaka.extern.Variant>} variants
  373. */
  374. static overrideDolbyVisionCodecs(variants) {
  375. /** @type {!Object<string, string>} */
  376. const codecMap = {
  377. 'dvav': 'avc3',
  378. 'dva1': 'avc1',
  379. 'dvhe': 'hev1',
  380. 'dvh1': 'hvc1',
  381. };
  382. /** @type {!Set<!shaka.extern.Stream>} */
  383. const streams = new Set();
  384. for (const variant of variants) {
  385. if (variant.video) {
  386. streams.add(variant.video);
  387. }
  388. }
  389. for (const video of streams) {
  390. for (const dvCodec of Object.keys(codecMap)) {
  391. if (video.codecs.includes(dvCodec)) {
  392. video.codecs = video.codecs.replace(dvCodec, codecMap[dvCodec]);
  393. break;
  394. }
  395. }
  396. }
  397. }
  398. /**
  399. * @param {!shaka.extern.Variant} variant
  400. * @param {?string} keySystem
  401. * @return {boolean}
  402. * @private
  403. */
  404. static checkVariantSupported_(variant, keySystem) {
  405. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  406. const Capabilities = shaka.media.Capabilities;
  407. const ManifestParserUtils = shaka.util.ManifestParserUtils;
  408. const MimeUtils = shaka.util.MimeUtils;
  409. const StreamUtils = shaka.util.StreamUtils;
  410. const isXboxOne = shaka.util.Platform.isXboxOne();
  411. const isFirefoxAndroid = shaka.util.Platform.isFirefox() &&
  412. shaka.util.Platform.isAndroid();
  413. // See: https://github.com/shaka-project/shaka-player/issues/3860
  414. const video = variant.video;
  415. const videoWidth = (video && video.width) || 0;
  416. const videoHeight = (video && video.height) || 0;
  417. // See: https://github.com/shaka-project/shaka-player/issues/3380
  418. // Note: it makes sense to drop early
  419. if (isXboxOne && video && (videoWidth > 1920 || videoHeight > 1080) &&
  420. (video.codecs.includes('avc1.') || video.codecs.includes('avc3.'))) {
  421. return false;
  422. }
  423. if (video) {
  424. let videoCodecs = StreamUtils.getCorrectVideoCodecs(video.codecs);
  425. // For multiplexed streams. Here we must check the audio of the
  426. // stream to see if it is compatible.
  427. if (video.codecs.includes(',')) {
  428. const allCodecs = video.codecs.split(',');
  429. videoCodecs = ManifestParserUtils.guessCodecs(
  430. ContentType.VIDEO, allCodecs);
  431. videoCodecs = StreamUtils.getCorrectVideoCodecs(videoCodecs);
  432. let audioCodecs = ManifestParserUtils.guessCodecs(
  433. ContentType.AUDIO, allCodecs);
  434. audioCodecs = StreamUtils.getCorrectAudioCodecs(
  435. audioCodecs, video.mimeType);
  436. const audioFullType = MimeUtils.getFullOrConvertedType(
  437. video.mimeType, audioCodecs, ContentType.AUDIO);
  438. if (!Capabilities.isTypeSupported(audioFullType)) {
  439. return false;
  440. }
  441. // Update the codec string with the (possibly) converted codecs.
  442. videoCodecs = [videoCodecs, audioCodecs].join(',');
  443. }
  444. const fullType = MimeUtils.getFullOrConvertedType(
  445. video.mimeType, videoCodecs, ContentType.VIDEO);
  446. if (!Capabilities.isTypeSupported(fullType)) {
  447. return false;
  448. }
  449. // Update the codec string with the (possibly) converted codecs.
  450. video.codecs = videoCodecs;
  451. }
  452. const audio = variant.audio;
  453. // See: https://github.com/shaka-project/shaka-player/issues/6111
  454. // It seems that Firefox Android reports that it supports
  455. // Opus + Widevine, but it is not actually supported.
  456. // It makes sense to drop early.
  457. if (isFirefoxAndroid && audio && audio.encrypted &&
  458. audio.codecs.toLowerCase().includes('opus')) {
  459. return false;
  460. }
  461. if (audio) {
  462. const codecs = StreamUtils.getCorrectAudioCodecs(
  463. audio.codecs, audio.mimeType);
  464. const fullType = MimeUtils.getFullOrConvertedType(
  465. audio.mimeType, codecs, ContentType.AUDIO);
  466. if (!Capabilities.isTypeSupported(fullType)) {
  467. return false;
  468. }
  469. // Update the codec string with the (possibly) converted codecs.
  470. audio.codecs = codecs;
  471. }
  472. return variant.decodingInfos.some((decodingInfo) => {
  473. if (!decodingInfo.supported) {
  474. return false;
  475. }
  476. if (keySystem) {
  477. const keySystemAccess = decodingInfo.keySystemAccess;
  478. if (keySystemAccess) {
  479. if (keySystemAccess.keySystem != keySystem) {
  480. return false;
  481. }
  482. }
  483. }
  484. return true;
  485. });
  486. }
  487. /**
  488. * Constructs a string out of an object, similar to the JSON.stringify method.
  489. * Unlike that method, this guarantees that the order of the keys is
  490. * alphabetical, so it can be used as a way to reliably compare two objects.
  491. *
  492. * @param {!Object} obj
  493. * @return {string}
  494. * @private
  495. */
  496. static alphabeticalKeyOrderStringify_(obj) {
  497. const keys = [];
  498. for (const key in obj) {
  499. keys.push(key);
  500. }
  501. // Alphabetically sort the keys, so they will be in a reliable order.
  502. keys.sort();
  503. const terms = [];
  504. for (const key of keys) {
  505. const escapedKey = JSON.stringify(key);
  506. const value = obj[key];
  507. if (value instanceof Object) {
  508. const stringifiedValue =
  509. shaka.util.StreamUtils.alphabeticalKeyOrderStringify_(value);
  510. terms.push(escapedKey + ':' + stringifiedValue);
  511. } else {
  512. const escapedValue = JSON.stringify(value);
  513. terms.push(escapedKey + ':' + escapedValue);
  514. }
  515. }
  516. return '{' + terms.join(',') + '}';
  517. }
  518. /**
  519. * Queries mediaCapabilities for the decoding info for that decoding config,
  520. * and assigns it to the given variant.
  521. * If that query has been done before, instead return a cached result.
  522. * @param {!shaka.extern.Variant} variant
  523. * @param {!Array.<!MediaDecodingConfiguration>} decodingConfigs
  524. * @private
  525. */
  526. static async getDecodingInfosForVariant_(variant, decodingConfigs) {
  527. /**
  528. * @param {?MediaCapabilitiesDecodingInfo} a
  529. * @param {!MediaCapabilitiesDecodingInfo} b
  530. * @return {!MediaCapabilitiesDecodingInfo}
  531. */
  532. const merge = (a, b) => {
  533. if (!a) {
  534. return b;
  535. } else {
  536. const res = shaka.util.ObjectUtils.shallowCloneObject(a);
  537. res.supported = a.supported && b.supported;
  538. res.powerEfficient = a.powerEfficient && b.powerEfficient;
  539. res.smooth = a.smooth && b.smooth;
  540. if (b.keySystemAccess && !res.keySystemAccess) {
  541. res.keySystemAccess = b.keySystemAccess;
  542. }
  543. return res;
  544. }
  545. };
  546. const StreamUtils = shaka.util.StreamUtils;
  547. /** @type {?MediaCapabilitiesDecodingInfo} */
  548. let finalResult = null;
  549. const promises = [];
  550. for (const decodingConfig of decodingConfigs) {
  551. const cacheKey =
  552. StreamUtils.alphabeticalKeyOrderStringify_(decodingConfig);
  553. const cache = StreamUtils.decodingConfigCache_;
  554. if (cache[cacheKey]) {
  555. shaka.log.v2('Using cached results of mediaCapabilities.decodingInfo',
  556. 'for key', cacheKey);
  557. finalResult = merge(finalResult, cache[cacheKey]);
  558. } else {
  559. // Do a final pass-over of the decoding config: if a given stream has
  560. // multiple codecs, that suggests that it switches between those codecs
  561. // at points of the go-through.
  562. // mediaCapabilities by itself will report "not supported" when you
  563. // put in multiple different codecs, so each has to be checked
  564. // individually. So check each and take the worst result, to determine
  565. // overall variant compatibility.
  566. promises.push(StreamUtils
  567. .checkEachDecodingConfigCombination_(decodingConfig).then((res) => {
  568. /** @type {?MediaCapabilitiesDecodingInfo} */
  569. let acc = null;
  570. for (const result of (res || [])) {
  571. acc = merge(acc, result);
  572. }
  573. if (acc) {
  574. cache[cacheKey] = acc;
  575. finalResult = merge(finalResult, acc);
  576. }
  577. }));
  578. }
  579. }
  580. await Promise.all(promises);
  581. if (finalResult) {
  582. variant.decodingInfos.push(finalResult);
  583. }
  584. }
  585. /**
  586. * @param {!MediaDecodingConfiguration} decodingConfig
  587. * @return {!Promise.<?Array.<!MediaCapabilitiesDecodingInfo>>}
  588. * @private
  589. */
  590. static checkEachDecodingConfigCombination_(decodingConfig) {
  591. let videoCodecs = [''];
  592. if (decodingConfig.video) {
  593. videoCodecs = shaka.util.MimeUtils.getCodecs(
  594. decodingConfig.video.contentType).split(',');
  595. }
  596. let audioCodecs = [''];
  597. if (decodingConfig.audio) {
  598. audioCodecs = shaka.util.MimeUtils.getCodecs(
  599. decodingConfig.audio.contentType).split(',');
  600. }
  601. const promises = [];
  602. for (const videoCodec of videoCodecs) {
  603. for (const audioCodec of audioCodecs) {
  604. const copy = shaka.util.ObjectUtils.cloneObject(decodingConfig);
  605. if (decodingConfig.video) {
  606. const mimeType = shaka.util.MimeUtils.getBasicType(
  607. copy.video.contentType);
  608. copy.video.contentType = shaka.util.MimeUtils.getFullType(
  609. mimeType, videoCodec);
  610. }
  611. if (decodingConfig.audio) {
  612. const mimeType = shaka.util.MimeUtils.getBasicType(
  613. copy.audio.contentType);
  614. copy.audio.contentType = shaka.util.MimeUtils.getFullType(
  615. mimeType, audioCodec);
  616. }
  617. promises.push(new Promise((resolve, reject) => {
  618. navigator.mediaCapabilities.decodingInfo(copy).then((res) => {
  619. resolve(res);
  620. }).catch(reject);
  621. }));
  622. }
  623. }
  624. return Promise.all(promises).catch((e) => {
  625. shaka.log.info('MediaCapabilities.decodingInfo() failed.',
  626. JSON.stringify(decodingConfig), e);
  627. return null;
  628. });
  629. }
  630. /**
  631. * Get the decodingInfo results of the variants via MediaCapabilities.
  632. * This should be called after the DrmEngine is created and configured, and
  633. * before DrmEngine sets the mediaKeys.
  634. *
  635. * @param {!Array.<shaka.extern.Variant>} variants
  636. * @param {boolean} usePersistentLicenses
  637. * @param {boolean} srcEquals
  638. * @param {!Array<string>} preferredKeySystems
  639. * @exportDoc
  640. */
  641. static async getDecodingInfosForVariants(variants, usePersistentLicenses,
  642. srcEquals, preferredKeySystems) {
  643. const gotDecodingInfo = variants.some((variant) =>
  644. variant.decodingInfos.length);
  645. if (gotDecodingInfo) {
  646. shaka.log.debug('Already got the variants\' decodingInfo.');
  647. return;
  648. }
  649. // Try to get preferred key systems first to avoid unneeded calls to CDM.
  650. for (const preferredKeySystem of preferredKeySystems) {
  651. let keySystemSatisfied = false;
  652. for (const variant of variants) {
  653. /** @type {!Array.<!Array.<!MediaDecodingConfiguration>>} */
  654. const decodingConfigs = shaka.util.StreamUtils.getDecodingConfigs_(
  655. variant, usePersistentLicenses, srcEquals)
  656. .filter((configs) => {
  657. // All configs in a batch will have the same keySystem.
  658. const config = configs[0];
  659. const keySystem = config.keySystemConfiguration &&
  660. config.keySystemConfiguration.keySystem;
  661. return keySystem === preferredKeySystem;
  662. });
  663. // The reason we are performing this await in a loop rather than
  664. // batching into a `promise.all` is performance related.
  665. // https://github.com/shaka-project/shaka-player/pull/4708#discussion_r1022581178
  666. for (const configs of decodingConfigs) {
  667. // eslint-disable-next-line no-await-in-loop
  668. await shaka.util.StreamUtils.getDecodingInfosForVariant_(
  669. variant, configs);
  670. }
  671. if (variant.decodingInfos.length) {
  672. keySystemSatisfied = true;
  673. }
  674. } // for (const variant of variants)
  675. if (keySystemSatisfied) {
  676. // Return if any preferred key system is already satisfied.
  677. return;
  678. }
  679. } // for (const preferredKeySystem of preferredKeySystems)
  680. for (const variant of variants) {
  681. /** @type {!Array.<!Array.<!MediaDecodingConfiguration>>} */
  682. const decodingConfigs = shaka.util.StreamUtils.getDecodingConfigs_(
  683. variant, usePersistentLicenses, srcEquals)
  684. .filter((configs) => {
  685. // All configs in a batch will have the same keySystem.
  686. const config = configs[0];
  687. const keySystem = config.keySystemConfiguration &&
  688. config.keySystemConfiguration.keySystem;
  689. // Avoid checking preferred systems twice.
  690. return !keySystem || !preferredKeySystems.includes(keySystem);
  691. });
  692. // The reason we are performing this await in a loop rather than
  693. // batching into a `promise.all` is performance related.
  694. // https://github.com/shaka-project/shaka-player/pull/4708#discussion_r1022581178
  695. for (const configs of decodingConfigs) {
  696. // eslint-disable-next-line no-await-in-loop
  697. await shaka.util.StreamUtils.getDecodingInfosForVariant_(
  698. variant, configs);
  699. }
  700. }
  701. }
  702. /**
  703. * Generate a batch of MediaDecodingConfiguration objects to get the
  704. * decodingInfo results for each variant.
  705. * Each batch shares the same DRM information, and represents the various
  706. * fullMimeType combinations of the streams.
  707. * @param {!shaka.extern.Variant} variant
  708. * @param {boolean} usePersistentLicenses
  709. * @param {boolean} srcEquals
  710. * @return {!Array.<!Array.<!MediaDecodingConfiguration>>}
  711. * @private
  712. */
  713. static getDecodingConfigs_(variant, usePersistentLicenses, srcEquals) {
  714. const audio = variant.audio;
  715. const video = variant.video;
  716. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  717. const ManifestParserUtils = shaka.util.ManifestParserUtils;
  718. const MimeUtils = shaka.util.MimeUtils;
  719. const StreamUtils = shaka.util.StreamUtils;
  720. const videoConfigs = [];
  721. const audioConfigs = [];
  722. if (video) {
  723. for (const fullMimeType of video.fullMimeTypes) {
  724. let videoCodecs = MimeUtils.getCodecs(fullMimeType);
  725. // For multiplexed streams with audio+video codecs, the config should
  726. // have AudioConfiguration and VideoConfiguration.
  727. // We ignore the multiplexed audio when there is normal audio also.
  728. if (videoCodecs.includes(',') && !audio) {
  729. const allCodecs = videoCodecs.split(',');
  730. const baseMimeType = MimeUtils.getBasicType(fullMimeType);
  731. videoCodecs = ManifestParserUtils.guessCodecs(
  732. ContentType.VIDEO, allCodecs);
  733. let audioCodecs = ManifestParserUtils.guessCodecs(
  734. ContentType.AUDIO, allCodecs);
  735. audioCodecs = StreamUtils.getCorrectAudioCodecs(
  736. audioCodecs, baseMimeType);
  737. const audioFullType = MimeUtils.getFullOrConvertedType(
  738. baseMimeType, audioCodecs, ContentType.AUDIO);
  739. audioConfigs.push({
  740. contentType: audioFullType,
  741. channels: 2,
  742. bitrate: variant.bandwidth || 1,
  743. samplerate: 1,
  744. spatialRendering: false,
  745. });
  746. }
  747. videoCodecs = StreamUtils.getCorrectVideoCodecs(videoCodecs);
  748. const fullType = MimeUtils.getFullOrConvertedType(
  749. MimeUtils.getBasicType(fullMimeType), videoCodecs,
  750. ContentType.VIDEO);
  751. // VideoConfiguration
  752. const videoConfig = {
  753. contentType: fullType,
  754. // NOTE: Some decoders strictly check the width and height fields and
  755. // won't decode smaller than 64x64. So if we don't have this info (as
  756. // is the case in some of our simpler tests), assume a 64x64
  757. // resolution to fill in this required field for MediaCapabilities.
  758. //
  759. // This became an issue specifically on Firefox on M1 Macs.
  760. width: video.width || 64,
  761. height: video.height || 64,
  762. bitrate: video.bandwidth || variant.bandwidth || 1,
  763. // framerate must be greater than 0, otherwise the config is invalid.
  764. framerate: video.frameRate || 1,
  765. };
  766. if (video.hdr) {
  767. switch (video.hdr) {
  768. case 'SDR':
  769. videoConfig.transferFunction = 'srgb';
  770. break;
  771. case 'PQ':
  772. videoConfig.transferFunction = 'pq';
  773. break;
  774. case 'HLG':
  775. videoConfig.transferFunction = 'hlg';
  776. break;
  777. }
  778. }
  779. if (video.colorGamut) {
  780. videoConfig.colorGamut = video.colorGamut;
  781. }
  782. videoConfigs.push(videoConfig);
  783. }
  784. }
  785. if (audio) {
  786. for (const fullMimeType of audio.fullMimeTypes) {
  787. const baseMimeType = MimeUtils.getBasicType(fullMimeType);
  788. const codecs = StreamUtils.getCorrectAudioCodecs(
  789. MimeUtils.getCodecs(fullMimeType), baseMimeType);
  790. const fullType = MimeUtils.getFullOrConvertedType(
  791. baseMimeType, codecs, ContentType.AUDIO);
  792. // AudioConfiguration
  793. audioConfigs.push({
  794. contentType: fullType,
  795. channels: audio.channelsCount || 2,
  796. bitrate: audio.bandwidth || variant.bandwidth || 1,
  797. samplerate: audio.audioSamplingRate || 1,
  798. spatialRendering: audio.spatialAudio,
  799. });
  800. }
  801. }
  802. // Generate each combination of video and audio config as a separate
  803. // MediaDecodingConfiguration, inside the main "batch".
  804. /** @type {!Array.<!MediaDecodingConfiguration>} */
  805. const mediaDecodingConfigBatch = [];
  806. if (videoConfigs.length == 0) {
  807. videoConfigs.push(null);
  808. }
  809. if (audioConfigs.length == 0) {
  810. audioConfigs.push(null);
  811. }
  812. for (const videoConfig of videoConfigs) {
  813. for (const audioConfig of audioConfigs) {
  814. /** @type {!MediaDecodingConfiguration} */
  815. const mediaDecodingConfig = {
  816. type: srcEquals ? 'file' : 'media-source',
  817. };
  818. if (videoConfig) {
  819. mediaDecodingConfig.video = videoConfig;
  820. }
  821. if (audioConfig) {
  822. mediaDecodingConfig.audio = audioConfig;
  823. }
  824. mediaDecodingConfigBatch.push(mediaDecodingConfig);
  825. }
  826. }
  827. const videoDrmInfos = variant.video ? variant.video.drmInfos : [];
  828. const audioDrmInfos = variant.audio ? variant.audio.drmInfos : [];
  829. const allDrmInfos = videoDrmInfos.concat(audioDrmInfos);
  830. // Return a list containing the mediaDecodingConfig for unencrypted variant.
  831. if (!allDrmInfos.length) {
  832. return [mediaDecodingConfigBatch];
  833. }
  834. // A list of MediaDecodingConfiguration objects created for the variant.
  835. const configs = [];
  836. // Get all the drm info so that we can avoid using nested loops when we
  837. // just need the drm info.
  838. const drmInfoByKeySystems = new Map();
  839. for (const info of allDrmInfos) {
  840. if (!drmInfoByKeySystems.get(info.keySystem)) {
  841. drmInfoByKeySystems.set(info.keySystem, []);
  842. }
  843. drmInfoByKeySystems.get(info.keySystem).push(info);
  844. }
  845. const persistentState =
  846. usePersistentLicenses ? 'required' : 'optional';
  847. const sessionTypes =
  848. usePersistentLicenses ? ['persistent-license'] : ['temporary'];
  849. for (const keySystem of drmInfoByKeySystems.keys()) {
  850. const modifiedMediaDecodingConfigBatch = [];
  851. for (const base of mediaDecodingConfigBatch) {
  852. // Create a copy of the mediaDecodingConfig.
  853. const config = /** @type {!MediaDecodingConfiguration} */
  854. (Object.assign({}, base));
  855. const drmInfos = drmInfoByKeySystems.get(keySystem);
  856. /** @type {!MediaCapabilitiesKeySystemConfiguration} */
  857. const keySystemConfig = {
  858. keySystem: keySystem,
  859. initDataType: 'cenc',
  860. persistentState: persistentState,
  861. distinctiveIdentifier: 'optional',
  862. sessionTypes: sessionTypes,
  863. };
  864. for (const info of drmInfos) {
  865. if (info.initData && info.initData.length) {
  866. const initDataTypes = new Set();
  867. for (const initData of info.initData) {
  868. initDataTypes.add(initData.initDataType);
  869. }
  870. if (initDataTypes.size > 1) {
  871. shaka.log.v2('DrmInfo contains more than one initDataType,',
  872. 'and we use the initDataType of the first initData.',
  873. info);
  874. }
  875. keySystemConfig.initDataType = info.initData[0].initDataType;
  876. }
  877. if (info.distinctiveIdentifierRequired) {
  878. keySystemConfig.distinctiveIdentifier = 'required';
  879. }
  880. if (info.persistentStateRequired) {
  881. keySystemConfig.persistentState = 'required';
  882. }
  883. if (info.sessionType) {
  884. keySystemConfig.sessionTypes = [info.sessionType];
  885. }
  886. if (audio) {
  887. if (!keySystemConfig.audio) {
  888. // KeySystemTrackConfiguration
  889. keySystemConfig.audio = {
  890. robustness: info.audioRobustness,
  891. };
  892. if (info.encryptionScheme) {
  893. keySystemConfig.audio.encryptionScheme = info.encryptionScheme;
  894. }
  895. } else {
  896. if (info.encryptionScheme) {
  897. keySystemConfig.audio.encryptionScheme =
  898. keySystemConfig.audio.encryptionScheme ||
  899. info.encryptionScheme;
  900. }
  901. keySystemConfig.audio.robustness =
  902. keySystemConfig.audio.robustness ||
  903. info.audioRobustness;
  904. }
  905. // See: https://github.com/shaka-project/shaka-player/issues/4659
  906. if (keySystemConfig.audio.robustness == '') {
  907. delete keySystemConfig.audio.robustness;
  908. }
  909. }
  910. if (video) {
  911. if (!keySystemConfig.video) {
  912. // KeySystemTrackConfiguration
  913. keySystemConfig.video = {
  914. robustness: info.videoRobustness,
  915. };
  916. if (info.encryptionScheme) {
  917. keySystemConfig.video.encryptionScheme = info.encryptionScheme;
  918. }
  919. } else {
  920. if (info.encryptionScheme) {
  921. keySystemConfig.video.encryptionScheme =
  922. keySystemConfig.video.encryptionScheme ||
  923. info.encryptionScheme;
  924. }
  925. keySystemConfig.video.robustness =
  926. keySystemConfig.video.robustness ||
  927. info.videoRobustness;
  928. }
  929. // See: https://github.com/shaka-project/shaka-player/issues/4659
  930. if (keySystemConfig.video.robustness == '') {
  931. delete keySystemConfig.video.robustness;
  932. }
  933. }
  934. }
  935. config.keySystemConfiguration = keySystemConfig;
  936. modifiedMediaDecodingConfigBatch.push(config);
  937. }
  938. configs.push(modifiedMediaDecodingConfigBatch);
  939. }
  940. return configs;
  941. }
  942. /**
  943. * Generates the correct audio codec for MediaDecodingConfiguration and
  944. * for MediaSource.isTypeSupported.
  945. * @param {string} codecs
  946. * @param {string} mimeType
  947. * @return {string}
  948. */
  949. static getCorrectAudioCodecs(codecs, mimeType) {
  950. // According to RFC 6381 section 3.3, 'fLaC' is actually the correct
  951. // codec string. We still need to map it to 'flac', as some browsers
  952. // currently don't support 'fLaC', while 'flac' is supported by most
  953. // major browsers.
  954. // See https://bugs.chromium.org/p/chromium/issues/detail?id=1422728
  955. if (codecs.toLowerCase() == 'flac') {
  956. if (!shaka.util.Platform.isSafari()) {
  957. return 'flac';
  958. } else {
  959. return 'fLaC';
  960. }
  961. }
  962. // The same is true for 'Opus'.
  963. if (codecs.toLowerCase() === 'opus') {
  964. if (!shaka.util.Platform.isSafari()) {
  965. return 'opus';
  966. } else {
  967. if (shaka.util.MimeUtils.getContainerType(mimeType) == 'mp4') {
  968. return 'Opus';
  969. } else {
  970. return 'opus';
  971. }
  972. }
  973. }
  974. return codecs;
  975. }
  976. /**
  977. * Generates the correct video codec for MediaDecodingConfiguration and
  978. * for MediaSource.isTypeSupported.
  979. * @param {string} codec
  980. * @return {string}
  981. */
  982. static getCorrectVideoCodecs(codec) {
  983. if (codec.includes('avc1')) {
  984. // Convert avc1 codec string from RFC-4281 to RFC-6381 for
  985. // MediaSource.isTypeSupported
  986. // Example, convert avc1.66.30 to avc1.42001e (0x42 == 66 and 0x1e == 30)
  987. const avcdata = codec.split('.');
  988. if (avcdata.length == 3) {
  989. let result = avcdata.shift() + '.';
  990. result += parseInt(avcdata.shift(), 10).toString(16);
  991. result +=
  992. ('000' + parseInt(avcdata.shift(), 10).toString(16)).slice(-4);
  993. return result;
  994. }
  995. } else if (codec == 'vp9') {
  996. // MediaCapabilities supports 'vp09...' codecs, but not 'vp9'. Translate
  997. // vp9 codec strings into 'vp09...', to allow such content to play with
  998. // mediaCapabilities enabled.
  999. // This means profile 0, level 4.1, 8-bit color. This supports 1080p @
  1000. // 60Hz. See https://en.wikipedia.org/wiki/VP9#Levels
  1001. //
  1002. // If we don't have more detailed codec info, assume this profile and
  1003. // level because it's high enough to likely accommodate the parameters we
  1004. // do have, such as width and height. If an implementation is checking
  1005. // the profile and level very strictly, we want older VP9 content to
  1006. // still work to some degree. But we don't want to set a level so high
  1007. // that it is rejected by a hardware decoder that can't handle the
  1008. // maximum requirements of the level.
  1009. //
  1010. // This became an issue specifically on Firefox on M1 Macs.
  1011. return 'vp09.00.41.08';
  1012. }
  1013. return codec;
  1014. }
  1015. /**
  1016. * Alters the given Manifest to filter out any streams uncompatible with the
  1017. * current variant.
  1018. *
  1019. * @param {?shaka.extern.Variant} currentVariant
  1020. * @param {shaka.extern.Manifest} manifest
  1021. */
  1022. static filterManifestByCurrentVariant(currentVariant, manifest) {
  1023. const StreamUtils = shaka.util.StreamUtils;
  1024. manifest.variants = manifest.variants.filter((variant) => {
  1025. const audio = variant.audio;
  1026. const video = variant.video;
  1027. if (audio && currentVariant && currentVariant.audio) {
  1028. if (!StreamUtils.areStreamsCompatible_(audio, currentVariant.audio)) {
  1029. shaka.log.debug('Dropping variant - not compatible with active audio',
  1030. 'active audio',
  1031. StreamUtils.getStreamSummaryString_(currentVariant.audio),
  1032. 'variant.audio',
  1033. StreamUtils.getStreamSummaryString_(audio));
  1034. return false;
  1035. }
  1036. }
  1037. if (video && currentVariant && currentVariant.video) {
  1038. if (!StreamUtils.areStreamsCompatible_(video, currentVariant.video)) {
  1039. shaka.log.debug('Dropping variant - not compatible with active video',
  1040. 'active video',
  1041. StreamUtils.getStreamSummaryString_(currentVariant.video),
  1042. 'variant.video',
  1043. StreamUtils.getStreamSummaryString_(video));
  1044. return false;
  1045. }
  1046. }
  1047. return true;
  1048. });
  1049. }
  1050. /**
  1051. * Alters the given Manifest to filter out any unsupported text streams.
  1052. *
  1053. * @param {shaka.extern.Manifest} manifest
  1054. * @private
  1055. */
  1056. static filterTextStreams_(manifest) {
  1057. // Filter text streams.
  1058. manifest.textStreams = manifest.textStreams.filter((stream) => {
  1059. const fullMimeType = shaka.util.MimeUtils.getFullType(
  1060. stream.mimeType, stream.codecs);
  1061. const keep = shaka.text.TextEngine.isTypeSupported(fullMimeType);
  1062. if (!keep) {
  1063. shaka.log.debug('Dropping text stream. Is not supported by the ' +
  1064. 'platform.', stream);
  1065. }
  1066. return keep;
  1067. });
  1068. }
  1069. /**
  1070. * Alters the given Manifest to filter out any unsupported image streams.
  1071. *
  1072. * @param {shaka.extern.Manifest} manifest
  1073. * @private
  1074. */
  1075. static async filterImageStreams_(manifest) {
  1076. const imageStreams = [];
  1077. for (const stream of manifest.imageStreams) {
  1078. let mimeType = stream.mimeType;
  1079. if (mimeType == 'application/mp4' && stream.codecs == 'mjpg') {
  1080. mimeType = 'image/jpg';
  1081. }
  1082. if (!shaka.util.StreamUtils.supportedImageMimeTypes_.has(mimeType)) {
  1083. const minImage = shaka.util.StreamUtils.minImage_.get(mimeType);
  1084. if (minImage) {
  1085. // eslint-disable-next-line no-await-in-loop
  1086. const res = await shaka.util.StreamUtils.isImageSupported_(minImage);
  1087. shaka.util.StreamUtils.supportedImageMimeTypes_.set(mimeType, res);
  1088. } else {
  1089. shaka.util.StreamUtils.supportedImageMimeTypes_.set(mimeType, false);
  1090. }
  1091. }
  1092. const keep =
  1093. shaka.util.StreamUtils.supportedImageMimeTypes_.get(mimeType);
  1094. if (!keep) {
  1095. shaka.log.debug('Dropping image stream. Is not supported by the ' +
  1096. 'platform.', stream);
  1097. } else {
  1098. imageStreams.push(stream);
  1099. }
  1100. }
  1101. manifest.imageStreams = imageStreams;
  1102. }
  1103. /**
  1104. * @param {string} minImage
  1105. * @return {!Promise.<boolean>}
  1106. * @private
  1107. */
  1108. static isImageSupported_(minImage) {
  1109. return new Promise((resolve) => {
  1110. const imageElement = /** @type {HTMLImageElement} */(new Image());
  1111. imageElement.src = minImage;
  1112. if ('decode' in imageElement) {
  1113. imageElement.decode().then(() => {
  1114. resolve(true);
  1115. }).catch(() => {
  1116. resolve(false);
  1117. });
  1118. } else {
  1119. imageElement.onload = imageElement.onerror = () => {
  1120. resolve(imageElement.height === 2);
  1121. };
  1122. }
  1123. });
  1124. }
  1125. /**
  1126. * @param {shaka.extern.Stream} s0
  1127. * @param {shaka.extern.Stream} s1
  1128. * @return {boolean}
  1129. * @private
  1130. */
  1131. static areStreamsCompatible_(s0, s1) {
  1132. // Basic mime types and basic codecs need to match.
  1133. // For example, we can't adapt between WebM and MP4,
  1134. // nor can we adapt between mp4a.* to ec-3.
  1135. // We can switch between text types on the fly,
  1136. // so don't run this check on text.
  1137. if (s0.mimeType != s1.mimeType) {
  1138. return false;
  1139. }
  1140. if (s0.codecs.split('.')[0] != s1.codecs.split('.')[0]) {
  1141. return false;
  1142. }
  1143. return true;
  1144. }
  1145. /**
  1146. * @param {shaka.extern.Variant} variant
  1147. * @return {shaka.extern.Track}
  1148. */
  1149. static variantToTrack(variant) {
  1150. /** @type {?shaka.extern.Stream} */
  1151. const audio = variant.audio;
  1152. /** @type {?shaka.extern.Stream} */
  1153. const video = variant.video;
  1154. /** @type {?string} */
  1155. const audioMimeType = audio ? audio.mimeType : null;
  1156. /** @type {?string} */
  1157. const videoMimeType = video ? video.mimeType : null;
  1158. /** @type {?string} */
  1159. const audioCodec = audio ? audio.codecs : null;
  1160. /** @type {?string} */
  1161. const videoCodec = video ? video.codecs : null;
  1162. /** @type {!Array.<string>} */
  1163. const codecs = [];
  1164. if (videoCodec) {
  1165. codecs.push(videoCodec);
  1166. }
  1167. if (audioCodec) {
  1168. codecs.push(audioCodec);
  1169. }
  1170. /** @type {!Array.<string>} */
  1171. const mimeTypes = [];
  1172. if (video) {
  1173. mimeTypes.push(video.mimeType);
  1174. }
  1175. if (audio) {
  1176. mimeTypes.push(audio.mimeType);
  1177. }
  1178. /** @type {?string} */
  1179. const mimeType = mimeTypes[0] || null;
  1180. /** @type {!Array.<string>} */
  1181. const kinds = [];
  1182. if (audio) {
  1183. kinds.push(audio.kind);
  1184. }
  1185. if (video) {
  1186. kinds.push(video.kind);
  1187. }
  1188. /** @type {?string} */
  1189. const kind = kinds[0] || null;
  1190. /** @type {!Set.<string>} */
  1191. const roles = new Set();
  1192. if (audio) {
  1193. for (const role of audio.roles) {
  1194. roles.add(role);
  1195. }
  1196. }
  1197. if (video) {
  1198. for (const role of video.roles) {
  1199. roles.add(role);
  1200. }
  1201. }
  1202. /** @type {shaka.extern.Track} */
  1203. const track = {
  1204. id: variant.id,
  1205. active: false,
  1206. type: 'variant',
  1207. bandwidth: variant.bandwidth,
  1208. language: variant.language,
  1209. label: null,
  1210. kind: kind,
  1211. width: null,
  1212. height: null,
  1213. frameRate: null,
  1214. pixelAspectRatio: null,
  1215. hdr: null,
  1216. colorGamut: null,
  1217. videoLayout: null,
  1218. mimeType: mimeType,
  1219. audioMimeType: audioMimeType,
  1220. videoMimeType: videoMimeType,
  1221. codecs: codecs.join(', '),
  1222. audioCodec: audioCodec,
  1223. videoCodec: videoCodec,
  1224. primary: variant.primary,
  1225. roles: Array.from(roles),
  1226. audioRoles: null,
  1227. forced: false,
  1228. videoId: null,
  1229. audioId: null,
  1230. channelsCount: null,
  1231. audioSamplingRate: null,
  1232. spatialAudio: false,
  1233. tilesLayout: null,
  1234. audioBandwidth: null,
  1235. videoBandwidth: null,
  1236. originalVideoId: null,
  1237. originalAudioId: null,
  1238. originalTextId: null,
  1239. originalImageId: null,
  1240. accessibilityPurpose: null,
  1241. originalLanguage: null,
  1242. };
  1243. if (video) {
  1244. track.videoId = video.id;
  1245. track.originalVideoId = video.originalId;
  1246. track.width = video.width || null;
  1247. track.height = video.height || null;
  1248. track.frameRate = video.frameRate || null;
  1249. track.pixelAspectRatio = video.pixelAspectRatio || null;
  1250. track.videoBandwidth = video.bandwidth || null;
  1251. track.hdr = video.hdr || null;
  1252. track.colorGamut = video.colorGamut || null;
  1253. track.videoLayout = video.videoLayout || null;
  1254. if (videoCodec.includes(',')) {
  1255. track.channelsCount = video.channelsCount;
  1256. track.audioSamplingRate = video.audioSamplingRate;
  1257. track.spatialAudio = video.spatialAudio;
  1258. track.originalLanguage = video.originalLanguage;
  1259. }
  1260. }
  1261. if (audio) {
  1262. track.audioId = audio.id;
  1263. track.originalAudioId = audio.originalId;
  1264. track.channelsCount = audio.channelsCount;
  1265. track.audioSamplingRate = audio.audioSamplingRate;
  1266. track.audioBandwidth = audio.bandwidth || null;
  1267. track.spatialAudio = audio.spatialAudio;
  1268. track.label = audio.label;
  1269. track.audioRoles = audio.roles;
  1270. track.accessibilityPurpose = audio.accessibilityPurpose;
  1271. track.originalLanguage = audio.originalLanguage;
  1272. }
  1273. return track;
  1274. }
  1275. /**
  1276. * @param {shaka.extern.Stream} stream
  1277. * @return {shaka.extern.Track}
  1278. */
  1279. static textStreamToTrack(stream) {
  1280. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  1281. /** @type {shaka.extern.Track} */
  1282. const track = {
  1283. id: stream.id,
  1284. active: false,
  1285. type: ContentType.TEXT,
  1286. bandwidth: 0,
  1287. language: stream.language,
  1288. label: stream.label,
  1289. kind: stream.kind || null,
  1290. width: null,
  1291. height: null,
  1292. frameRate: null,
  1293. pixelAspectRatio: null,
  1294. hdr: null,
  1295. colorGamut: null,
  1296. videoLayout: null,
  1297. mimeType: stream.mimeType,
  1298. audioMimeType: null,
  1299. videoMimeType: null,
  1300. codecs: stream.codecs || null,
  1301. audioCodec: null,
  1302. videoCodec: null,
  1303. primary: stream.primary,
  1304. roles: stream.roles,
  1305. audioRoles: null,
  1306. forced: stream.forced,
  1307. videoId: null,
  1308. audioId: null,
  1309. channelsCount: null,
  1310. audioSamplingRate: null,
  1311. spatialAudio: false,
  1312. tilesLayout: null,
  1313. audioBandwidth: null,
  1314. videoBandwidth: null,
  1315. originalVideoId: null,
  1316. originalAudioId: null,
  1317. originalTextId: stream.originalId,
  1318. originalImageId: null,
  1319. accessibilityPurpose: stream.accessibilityPurpose,
  1320. originalLanguage: stream.originalLanguage,
  1321. };
  1322. return track;
  1323. }
  1324. /**
  1325. * @param {shaka.extern.Stream} stream
  1326. * @return {shaka.extern.Track}
  1327. */
  1328. static imageStreamToTrack(stream) {
  1329. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  1330. let width = stream.width || null;
  1331. let height = stream.height || null;
  1332. // The stream width and height represent the size of the entire thumbnail
  1333. // sheet, so divide by the layout.
  1334. let reference = null;
  1335. // Note: segmentIndex is built by default for HLS, but not for DASH, but
  1336. // in DASH this information comes at the stream level and not at the
  1337. // segment level.
  1338. if (stream.segmentIndex) {
  1339. reference = stream.segmentIndex.get(0);
  1340. }
  1341. let layout = stream.tilesLayout;
  1342. if (reference) {
  1343. layout = reference.getTilesLayout() || layout;
  1344. }
  1345. if (layout && width != null) {
  1346. width /= Number(layout.split('x')[0]);
  1347. }
  1348. if (layout && height != null) {
  1349. height /= Number(layout.split('x')[1]);
  1350. }
  1351. // TODO: What happens if there are multiple grids, with different
  1352. // layout sizes, inside this image stream?
  1353. /** @type {shaka.extern.Track} */
  1354. const track = {
  1355. id: stream.id,
  1356. active: false,
  1357. type: ContentType.IMAGE,
  1358. bandwidth: stream.bandwidth || 0,
  1359. language: '',
  1360. label: null,
  1361. kind: null,
  1362. width,
  1363. height,
  1364. frameRate: null,
  1365. pixelAspectRatio: null,
  1366. hdr: null,
  1367. colorGamut: null,
  1368. videoLayout: null,
  1369. mimeType: stream.mimeType,
  1370. audioMimeType: null,
  1371. videoMimeType: null,
  1372. codecs: stream.codecs || null,
  1373. audioCodec: null,
  1374. videoCodec: null,
  1375. primary: false,
  1376. roles: [],
  1377. audioRoles: null,
  1378. forced: false,
  1379. videoId: null,
  1380. audioId: null,
  1381. channelsCount: null,
  1382. audioSamplingRate: null,
  1383. spatialAudio: false,
  1384. tilesLayout: layout || null,
  1385. audioBandwidth: null,
  1386. videoBandwidth: null,
  1387. originalVideoId: null,
  1388. originalAudioId: null,
  1389. originalTextId: null,
  1390. originalImageId: stream.originalId,
  1391. accessibilityPurpose: null,
  1392. originalLanguage: null,
  1393. };
  1394. return track;
  1395. }
  1396. /**
  1397. * Generate and return an ID for this track, since the ID field is optional.
  1398. *
  1399. * @param {TextTrack|AudioTrack} html5Track
  1400. * @return {number} The generated ID.
  1401. */
  1402. static html5TrackId(html5Track) {
  1403. if (!html5Track['__shaka_id']) {
  1404. html5Track['__shaka_id'] = shaka.util.StreamUtils.nextTrackId_++;
  1405. }
  1406. return html5Track['__shaka_id'];
  1407. }
  1408. /**
  1409. * @param {TextTrack} textTrack
  1410. * @return {shaka.extern.Track}
  1411. */
  1412. static html5TextTrackToTrack(textTrack) {
  1413. const StreamUtils = shaka.util.StreamUtils;
  1414. /** @type {shaka.extern.Track} */
  1415. const track = StreamUtils.html5TrackToGenericShakaTrack_(textTrack);
  1416. track.active = textTrack.mode != 'disabled';
  1417. track.type = 'text';
  1418. track.originalTextId = textTrack.id;
  1419. if (textTrack.kind == 'captions') {
  1420. // See: https://github.com/shaka-project/shaka-player/issues/6233
  1421. track.mimeType = 'unknown';
  1422. }
  1423. if (textTrack.kind == 'subtitles') {
  1424. track.mimeType = 'text/vtt';
  1425. }
  1426. if (textTrack.kind) {
  1427. track.roles = [textTrack.kind];
  1428. }
  1429. if (textTrack.kind == 'forced') {
  1430. track.forced = true;
  1431. }
  1432. return track;
  1433. }
  1434. /**
  1435. * @param {AudioTrack} audioTrack
  1436. * @return {shaka.extern.Track}
  1437. */
  1438. static html5AudioTrackToTrack(audioTrack) {
  1439. const StreamUtils = shaka.util.StreamUtils;
  1440. /** @type {shaka.extern.Track} */
  1441. const track = StreamUtils.html5TrackToGenericShakaTrack_(audioTrack);
  1442. track.active = audioTrack.enabled;
  1443. track.type = 'variant';
  1444. track.originalAudioId = audioTrack.id;
  1445. if (audioTrack.kind == 'main') {
  1446. track.primary = true;
  1447. }
  1448. if (audioTrack.kind) {
  1449. track.roles = [audioTrack.kind];
  1450. track.audioRoles = [audioTrack.kind];
  1451. track.label = audioTrack.label;
  1452. }
  1453. return track;
  1454. }
  1455. /**
  1456. * Creates a Track object with non-type specific fields filled out. The
  1457. * caller is responsible for completing the Track object with any
  1458. * type-specific information (audio or text).
  1459. *
  1460. * @param {TextTrack|AudioTrack} html5Track
  1461. * @return {shaka.extern.Track}
  1462. * @private
  1463. */
  1464. static html5TrackToGenericShakaTrack_(html5Track) {
  1465. const language = html5Track.language;
  1466. /** @type {shaka.extern.Track} */
  1467. const track = {
  1468. id: shaka.util.StreamUtils.html5TrackId(html5Track),
  1469. active: false,
  1470. type: '',
  1471. bandwidth: 0,
  1472. language: shaka.util.LanguageUtils.normalize(language || 'und'),
  1473. label: html5Track.label,
  1474. kind: html5Track.kind,
  1475. width: null,
  1476. height: null,
  1477. frameRate: null,
  1478. pixelAspectRatio: null,
  1479. hdr: null,
  1480. colorGamut: null,
  1481. videoLayout: null,
  1482. mimeType: null,
  1483. audioMimeType: null,
  1484. videoMimeType: null,
  1485. codecs: null,
  1486. audioCodec: null,
  1487. videoCodec: null,
  1488. primary: false,
  1489. roles: [],
  1490. forced: false,
  1491. audioRoles: null,
  1492. videoId: null,
  1493. audioId: null,
  1494. channelsCount: null,
  1495. audioSamplingRate: null,
  1496. spatialAudio: false,
  1497. tilesLayout: null,
  1498. audioBandwidth: null,
  1499. videoBandwidth: null,
  1500. originalVideoId: null,
  1501. originalAudioId: null,
  1502. originalTextId: null,
  1503. originalImageId: null,
  1504. accessibilityPurpose: null,
  1505. originalLanguage: language,
  1506. };
  1507. return track;
  1508. }
  1509. /**
  1510. * Determines if the given variant is playable.
  1511. * @param {!shaka.extern.Variant} variant
  1512. * @return {boolean}
  1513. */
  1514. static isPlayable(variant) {
  1515. return variant.allowedByApplication &&
  1516. variant.allowedByKeySystem &&
  1517. variant.disabledUntilTime == 0;
  1518. }
  1519. /**
  1520. * Filters out unplayable variants.
  1521. * @param {!Array.<!shaka.extern.Variant>} variants
  1522. * @return {!Array.<!shaka.extern.Variant>}
  1523. */
  1524. static getPlayableVariants(variants) {
  1525. return variants.filter((variant) => {
  1526. return shaka.util.StreamUtils.isPlayable(variant);
  1527. });
  1528. }
  1529. /**
  1530. * Chooses streams according to the given config.
  1531. * Works both for Stream and Track types due to their similarities.
  1532. *
  1533. * @param {!Array<!shaka.extern.Stream>|!Array<!shaka.extern.Track>} streams
  1534. * @param {string} preferredLanguage
  1535. * @param {string} preferredRole
  1536. * @param {boolean} preferredForced
  1537. * @return {!Array<!shaka.extern.Stream>|!Array<!shaka.extern.Track>}
  1538. */
  1539. static filterStreamsByLanguageAndRole(
  1540. streams, preferredLanguage, preferredRole, preferredForced) {
  1541. const LanguageUtils = shaka.util.LanguageUtils;
  1542. /** @type {!Array<!shaka.extern.Stream>|!Array<!shaka.extern.Track>} */
  1543. let chosen = streams;
  1544. // Start with the set of primary streams.
  1545. /** @type {!Array<!shaka.extern.Stream>|!Array<!shaka.extern.Track>} */
  1546. const primary = streams.filter((stream) => {
  1547. return stream.primary;
  1548. });
  1549. if (primary.length) {
  1550. chosen = primary;
  1551. }
  1552. // Now reduce the set to one language. This covers both arbitrary language
  1553. // choice and the reduction of the "primary" stream set to one language.
  1554. const firstLanguage = chosen.length ? chosen[0].language : '';
  1555. chosen = chosen.filter((stream) => {
  1556. return stream.language == firstLanguage;
  1557. });
  1558. // Find the streams that best match our language preference. This will
  1559. // override previous selections.
  1560. if (preferredLanguage) {
  1561. const closestLocale = LanguageUtils.findClosestLocale(
  1562. LanguageUtils.normalize(preferredLanguage),
  1563. streams.map((stream) => stream.language));
  1564. // Only replace |chosen| if we found a locale that is close to our
  1565. // preference.
  1566. if (closestLocale) {
  1567. chosen = streams.filter((stream) => {
  1568. const locale = LanguageUtils.normalize(stream.language);
  1569. return locale == closestLocale;
  1570. });
  1571. }
  1572. }
  1573. // Filter by forced preference
  1574. chosen = chosen.filter((stream) => {
  1575. return stream.forced == preferredForced;
  1576. });
  1577. // Now refine the choice based on role preference.
  1578. if (preferredRole) {
  1579. const roleMatches = shaka.util.StreamUtils.filterStreamsByRole_(
  1580. chosen, preferredRole);
  1581. if (roleMatches.length) {
  1582. return roleMatches;
  1583. } else {
  1584. shaka.log.warning('No exact match for the text role could be found.');
  1585. }
  1586. } else {
  1587. // Prefer text streams with no roles, if they exist.
  1588. const noRoleMatches = chosen.filter((stream) => {
  1589. return stream.roles.length == 0;
  1590. });
  1591. if (noRoleMatches.length) {
  1592. return noRoleMatches;
  1593. }
  1594. }
  1595. // Either there was no role preference, or it could not be satisfied.
  1596. // Choose an arbitrary role, if there are any, and filter out any other
  1597. // roles. This ensures we never adapt between roles.
  1598. const allRoles = chosen.map((stream) => {
  1599. return stream.roles;
  1600. }).reduce(shaka.util.Functional.collapseArrays, []);
  1601. if (!allRoles.length) {
  1602. return chosen;
  1603. }
  1604. return shaka.util.StreamUtils.filterStreamsByRole_(chosen, allRoles[0]);
  1605. }
  1606. /**
  1607. * Filter Streams by role.
  1608. * Works both for Stream and Track types due to their similarities.
  1609. *
  1610. * @param {!Array<!shaka.extern.Stream>|!Array<!shaka.extern.Track>} streams
  1611. * @param {string} preferredRole
  1612. * @return {!Array<!shaka.extern.Stream>|!Array<!shaka.extern.Track>}
  1613. * @private
  1614. */
  1615. static filterStreamsByRole_(streams, preferredRole) {
  1616. return streams.filter((stream) => {
  1617. return stream.roles.includes(preferredRole);
  1618. });
  1619. }
  1620. /**
  1621. * Checks if the given stream is an audio stream.
  1622. *
  1623. * @param {shaka.extern.Stream} stream
  1624. * @return {boolean}
  1625. */
  1626. static isAudio(stream) {
  1627. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  1628. return stream.type == ContentType.AUDIO;
  1629. }
  1630. /**
  1631. * Checks if the given stream is a video stream.
  1632. *
  1633. * @param {shaka.extern.Stream} stream
  1634. * @return {boolean}
  1635. */
  1636. static isVideo(stream) {
  1637. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  1638. return stream.type == ContentType.VIDEO;
  1639. }
  1640. /**
  1641. * Get all non-null streams in the variant as an array.
  1642. *
  1643. * @param {shaka.extern.Variant} variant
  1644. * @return {!Array.<shaka.extern.Stream>}
  1645. */
  1646. static getVariantStreams(variant) {
  1647. const streams = [];
  1648. if (variant.audio) {
  1649. streams.push(variant.audio);
  1650. }
  1651. if (variant.video) {
  1652. streams.push(variant.video);
  1653. }
  1654. return streams;
  1655. }
  1656. /**
  1657. * Indicates if some of the variant's streams are fastSwitching.
  1658. *
  1659. * @param {shaka.extern.Variant} variant
  1660. * @return {boolean}
  1661. */
  1662. static isFastSwitching(variant) {
  1663. if (variant.audio && variant.audio.fastSwitching) {
  1664. return true;
  1665. }
  1666. if (variant.video && variant.video.fastSwitching) {
  1667. return true;
  1668. }
  1669. return false;
  1670. }
  1671. /**
  1672. * Returns a string of a variant, with the attribute values of its audio
  1673. * and/or video streams for log printing.
  1674. * @param {shaka.extern.Variant} variant
  1675. * @return {string}
  1676. * @private
  1677. */
  1678. static getVariantSummaryString_(variant) {
  1679. const summaries = [];
  1680. if (variant.audio) {
  1681. summaries.push(shaka.util.StreamUtils.getStreamSummaryString_(
  1682. variant.audio));
  1683. }
  1684. if (variant.video) {
  1685. summaries.push(shaka.util.StreamUtils.getStreamSummaryString_(
  1686. variant.video));
  1687. }
  1688. return summaries.join(', ');
  1689. }
  1690. /**
  1691. * Returns a string of an audio or video stream for log printing.
  1692. * @param {shaka.extern.Stream} stream
  1693. * @return {string}
  1694. * @private
  1695. */
  1696. static getStreamSummaryString_(stream) {
  1697. // Accepted parameters for Chromecast can be found (internally) at
  1698. // go/cast-mime-params
  1699. if (shaka.util.StreamUtils.isAudio(stream)) {
  1700. return 'type=audio' +
  1701. ' codecs=' + stream.codecs +
  1702. ' bandwidth='+ stream.bandwidth +
  1703. ' channelsCount=' + stream.channelsCount +
  1704. ' audioSamplingRate=' + stream.audioSamplingRate;
  1705. }
  1706. if (shaka.util.StreamUtils.isVideo(stream)) {
  1707. return 'type=video' +
  1708. ' codecs=' + stream.codecs +
  1709. ' bandwidth=' + stream.bandwidth +
  1710. ' frameRate=' + stream.frameRate +
  1711. ' width=' + stream.width +
  1712. ' height=' + stream.height;
  1713. }
  1714. return 'unexpected stream type';
  1715. }
  1716. /**
  1717. * Clears underlying decoding config cache.
  1718. */
  1719. static clearDecodingConfigCache() {
  1720. shaka.util.StreamUtils.decodingConfigCache_ = {};
  1721. }
  1722. };
  1723. /**
  1724. * A cache of results from mediaCapabilities.decodingInfo, indexed by the
  1725. * (stringified) decodingConfig.
  1726. *
  1727. * @type {Object.<(!string), (!MediaCapabilitiesDecodingInfo)>}
  1728. * @private
  1729. */
  1730. shaka.util.StreamUtils.decodingConfigCache_ = {};
  1731. /** @private {number} */
  1732. shaka.util.StreamUtils.nextTrackId_ = 0;
  1733. /**
  1734. * @enum {string}
  1735. */
  1736. shaka.util.StreamUtils.DecodingAttributes = {
  1737. SMOOTH: 'smooth',
  1738. POWER: 'powerEfficient',
  1739. };
  1740. /**
  1741. * @private {!Map.<string, boolean>}
  1742. */
  1743. shaka.util.StreamUtils.supportedImageMimeTypes_ = new Map()
  1744. .set('image/svg+xml', true)
  1745. .set('image/png', true)
  1746. .set('image/jpeg', true)
  1747. .set('image/jpg', true);
  1748. /**
  1749. * @const {string}
  1750. * @private
  1751. */
  1752. shaka.util.StreamUtils.minWebPImage_ = 'data:image/webp;base64,UklGRjoAAABXRU' +
  1753. 'JQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwY' +
  1754. 'AAA';
  1755. /**
  1756. * @const {string}
  1757. * @private
  1758. */
  1759. shaka.util.StreamUtils.minAvifImage_ = 'data:image/avif;base64,AAAAIGZ0eXBhdm' +
  1760. 'lmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljd' +
  1761. 'AAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEA' +
  1762. 'AAABAAABGgAAAB0AAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAA' +
  1763. 'AamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAIAAAACAAAAEHBpeGkAAAAAAwgICAAAAA' +
  1764. 'xhdjFDgQ0MAAAAABNjb2xybmNseAACAAIAAYAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAA' +
  1765. 'CVtZGF0EgAKCBgANogQEAwgMg8f8D///8WfhwB8+ErK42A=';
  1766. /**
  1767. * @const {!Map.<string, string>}
  1768. * @private
  1769. */
  1770. shaka.util.StreamUtils.minImage_ = new Map()
  1771. .set('image/webp', shaka.util.StreamUtils.minWebPImage_)
  1772. .set('image/avif', shaka.util.StreamUtils.minAvifImage_);