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