Source: lib/util/stream_utils.js

  1. /**
  2. * @license
  3. * Copyright 2016 Google Inc.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. goog.provide('shaka.util.StreamUtils');
  18. goog.require('goog.asserts');
  19. goog.require('shaka.log');
  20. goog.require('shaka.media.DrmEngine');
  21. goog.require('shaka.media.MediaSourceEngine');
  22. goog.require('shaka.text.TextEngine');
  23. goog.require('shaka.util.ArrayUtils');
  24. goog.require('shaka.util.Functional');
  25. goog.require('shaka.util.LanguageUtils');
  26. goog.require('shaka.util.ManifestParserUtils');
  27. goog.require('shaka.util.MimeUtils');
  28. /**
  29. * @namespace shaka.util.StreamUtils
  30. * @summary A set of utility functions for dealing with Streams and Manifests.
  31. */
  32. /**
  33. * @param {shakaExtern.Variant} variant
  34. * @param {shakaExtern.Restrictions} restrictions
  35. * Configured restrictions from the user.
  36. * @param {{width: number, height: number}} maxHwRes
  37. * The maximum resolution the hardware can handle.
  38. * This is applied separately from user restrictions because the setting
  39. * should not be easily replaced by the user's configuration.
  40. * @return {boolean}
  41. */
  42. shaka.util.StreamUtils.meetsRestrictions = function(
  43. variant, restrictions, maxHwRes) {
  44. let video = variant.video;
  45. if (video) {
  46. if (video.width < restrictions.minWidth ||
  47. video.width > restrictions.maxWidth || video.width > maxHwRes.width ||
  48. video.height < restrictions.minHeight ||
  49. video.height > restrictions.maxHeight ||
  50. video.height > maxHwRes.height ||
  51. (video.width * video.height) < restrictions.minPixels ||
  52. (video.width * video.height) > restrictions.maxPixels) {
  53. return false;
  54. }
  55. }
  56. if (variant.bandwidth < restrictions.minBandwidth ||
  57. variant.bandwidth > restrictions.maxBandwidth) {
  58. return false;
  59. }
  60. return true;
  61. };
  62. /**
  63. * @param {shakaExtern.Period} period
  64. * @param {shakaExtern.Restrictions} restrictions
  65. * @param {{width: number, height: number}} maxHwRes
  66. * @return {boolean} Whether the tracks changed.
  67. */
  68. shaka.util.StreamUtils.applyRestrictions =
  69. function(period, restrictions, maxHwRes) {
  70. let tracksChanged = false;
  71. period.variants.forEach(function(variant) {
  72. let originalAllowed = variant.allowedByApplication;
  73. variant.allowedByApplication = shaka.util.StreamUtils.meetsRestrictions(
  74. variant, restrictions, maxHwRes);
  75. if (originalAllowed != variant.allowedByApplication) {
  76. tracksChanged = true;
  77. }
  78. });
  79. return tracksChanged;
  80. };
  81. /**
  82. * Alters the given Period to filter out any unplayable streams.
  83. *
  84. * @param {shaka.media.DrmEngine} drmEngine
  85. * @param {?shakaExtern.Stream} activeAudio
  86. * @param {?shakaExtern.Stream} activeVideo
  87. * @param {shakaExtern.Period} period
  88. */
  89. shaka.util.StreamUtils.filterNewPeriod = function(
  90. drmEngine, activeAudio, activeVideo, period) {
  91. const StreamUtils = shaka.util.StreamUtils;
  92. if (activeAudio) {
  93. goog.asserts.assert(
  94. shaka.util.StreamUtils.isAudio(activeAudio),
  95. 'Audio streams must have the audio type.');
  96. }
  97. if (activeVideo) {
  98. goog.asserts.assert(
  99. shaka.util.StreamUtils.isVideo(activeVideo),
  100. 'Video streams must have the video type.');
  101. }
  102. // Filter variants.
  103. period.variants = period.variants.filter(function(variant) {
  104. let keep = StreamUtils.isVariantCompatible_(
  105. variant,
  106. drmEngine,
  107. activeAudio,
  108. activeVideo);
  109. if (!keep) {
  110. shaka.log.debug('Dropping Variant (not compatible with key system, ' +
  111. 'platform, or active Variant)', variant);
  112. }
  113. return keep;
  114. });
  115. // Filter text streams.
  116. period.textStreams = period.textStreams.filter(function(stream) {
  117. let fullMimeType = shaka.util.MimeUtils.getFullType(
  118. stream.mimeType, stream.codecs);
  119. let keep = shaka.text.TextEngine.isTypeSupported(fullMimeType);
  120. if (!keep) {
  121. shaka.log.debug('Dropping text stream. Is not supported by the ' +
  122. 'platform.', stream);
  123. }
  124. return keep;
  125. });
  126. };
  127. /**
  128. * Checks if a stream is compatible with the key system, platform,
  129. * and active stream.
  130. * This does not check if the stream is supported by the chosen key system.
  131. *
  132. * @param {?shakaExtern.Stream} stream A non-text stream to check.
  133. * @param {shaka.media.DrmEngine} drmEngine
  134. * @param {?shakaExtern.Stream} activeStream
  135. * @return {boolean}
  136. * @private
  137. */
  138. shaka.util.StreamUtils.isStreamCompatible_ =
  139. function(stream, drmEngine, activeStream) {
  140. if (!stream) return true;
  141. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  142. goog.asserts.assert(stream.type != ContentType.TEXT,
  143. 'Should not be called on a text stream!');
  144. let drmSupportedMimeTypes = null;
  145. if (drmEngine && drmEngine.initialized()) {
  146. drmSupportedMimeTypes = drmEngine.getSupportedTypes();
  147. }
  148. // Check if the stream can be played by the platform.
  149. let fullMimeType = shaka.util.MimeUtils.getFullType(
  150. stream.mimeType, stream.codecs);
  151. if (!shaka.media.MediaSourceEngine.isStreamSupported(stream)) {
  152. return false;
  153. }
  154. // Check if the stream can be handled by the key system.
  155. // There's no need to check that the stream is supported by the
  156. // chosen key system since the caller has already verified that.
  157. if (drmSupportedMimeTypes && stream.encrypted &&
  158. drmSupportedMimeTypes.indexOf(fullMimeType) < 0) {
  159. return false;
  160. }
  161. // Lastly, check if the active stream can switch to the stream.
  162. // Basic mime types and basic codecs need to match.
  163. // For example, we can't adapt between WebM and MP4,
  164. // nor can we adapt between mp4a.* to ec-3.
  165. // We can switch between text types on the fly,
  166. // so don't run this check on text.
  167. if (activeStream) {
  168. if (stream.mimeType != activeStream.mimeType ||
  169. stream.codecs.split('.')[0] != activeStream.codecs.split('.')[0]) {
  170. return false;
  171. }
  172. }
  173. return true;
  174. };
  175. /**
  176. * Checks if a variant is compatible with the key system, platform,
  177. * and active stream.
  178. *
  179. * @param {!shakaExtern.Variant} variant
  180. * @param {shaka.media.DrmEngine} drmEngine
  181. * @param {?shakaExtern.Stream} activeAudio
  182. * @param {?shakaExtern.Stream} activeVideo
  183. * @return {boolean}
  184. * @private
  185. */
  186. shaka.util.StreamUtils.isVariantCompatible_ =
  187. function(variant, drmEngine, activeAudio, activeVideo) {
  188. if (drmEngine && drmEngine.initialized()) {
  189. if (!drmEngine.isSupportedByKeySystem(variant)) return false;
  190. }
  191. let isStreamCompatible = shaka.util.StreamUtils.isStreamCompatible_;
  192. return isStreamCompatible(variant.audio, drmEngine, activeAudio) &&
  193. isStreamCompatible(variant.video, drmEngine, activeVideo);
  194. };
  195. /**
  196. * @param {shakaExtern.Variant} variant
  197. * @return {shakaExtern.Track}
  198. */
  199. shaka.util.StreamUtils.variantToTrack = function(variant) {
  200. /** @type {?shakaExtern.Stream} */
  201. let audio = variant.audio;
  202. /** @type {?shakaExtern.Stream} */
  203. let video = variant.video;
  204. /** @type {?string} */
  205. let audioCodec = audio ? audio.codecs : null;
  206. /** @type {?string} */
  207. let videoCodec = video ? video.codecs : null;
  208. /** @type {!Array.<string>} */
  209. let codecs = [];
  210. if (videoCodec) codecs.push(videoCodec);
  211. if (audioCodec) codecs.push(audioCodec);
  212. /** @type {!Array.<string>} */
  213. let mimeTypes = [];
  214. if (video) mimeTypes.push(video.mimeType);
  215. if (audio) mimeTypes.push(audio.mimeType);
  216. /** @type {?string} */
  217. let mimeType = mimeTypes[0] || null;
  218. /** @type {!Array.<string>} */
  219. let kinds = [];
  220. if (audio) kinds.push(audio.kind);
  221. if (video) kinds.push(video.kind);
  222. /** @type {?string} */
  223. let kind = kinds[0] || null;
  224. /** @type {!Array.<string>} */
  225. let roles = [];
  226. if (audio) roles.push.apply(roles, audio.roles);
  227. if (video) roles.push.apply(roles, video.roles);
  228. roles = shaka.util.ArrayUtils.removeDuplicates(roles);
  229. /** @type {shakaExtern.Track} */
  230. let track = {
  231. id: variant.id,
  232. active: false,
  233. type: 'variant',
  234. bandwidth: variant.bandwidth,
  235. language: variant.language,
  236. label: null,
  237. kind: kind,
  238. width: null,
  239. height: null,
  240. frameRate: null,
  241. mimeType: mimeType,
  242. codecs: codecs.join(', '),
  243. audioCodec: audioCodec,
  244. videoCodec: videoCodec,
  245. primary: variant.primary,
  246. roles: roles,
  247. videoId: null,
  248. audioId: null,
  249. channelsCount: null,
  250. audioBandwidth: null,
  251. videoBandwidth: null
  252. };
  253. if (video) {
  254. track.videoId = video.id;
  255. track.width = video.width || null;
  256. track.height = video.height || null;
  257. track.frameRate = video.frameRate || null;
  258. track.videoBandwidth = video.bandwidth || null;
  259. }
  260. if (audio) {
  261. track.audioId = audio.id;
  262. track.channelsCount = audio.channelsCount;
  263. track.audioBandwidth = audio.bandwidth || null;
  264. track.label = audio.label;
  265. }
  266. return track;
  267. };
  268. /**
  269. * @param {shakaExtern.Stream} stream
  270. * @return {shakaExtern.Track}
  271. */
  272. shaka.util.StreamUtils.textStreamToTrack = function(stream) {
  273. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  274. /** @type {shakaExtern.Track} */
  275. let track = {
  276. id: stream.id,
  277. active: false,
  278. type: ContentType.TEXT,
  279. bandwidth: 0,
  280. language: stream.language,
  281. label: stream.label,
  282. kind: stream.kind || null,
  283. width: null,
  284. height: null,
  285. frameRate: null,
  286. mimeType: stream.mimeType,
  287. codecs: stream.codecs || null,
  288. audioCodec: null,
  289. videoCodec: null,
  290. primary: stream.primary,
  291. roles: stream.roles,
  292. videoId: null,
  293. audioId: null,
  294. channelsCount: null,
  295. audioBandwidth: null,
  296. videoBandwidth: null
  297. };
  298. return track;
  299. };
  300. /**
  301. * Gets track representations of all playable variants and all text streams.
  302. *
  303. * @param {shakaExtern.Period} period
  304. * @return {!Array.<shakaExtern.Track>}
  305. */
  306. shaka.util.StreamUtils.getTracks = function(period) {
  307. const StreamUtils = shaka.util.StreamUtils;
  308. let tracks = [];
  309. let variants = StreamUtils.getPlayableVariants(period.variants);
  310. let textStreams = period.textStreams;
  311. variants.forEach(function(variant) {
  312. tracks.push(StreamUtils.variantToTrack(variant));
  313. });
  314. textStreams.forEach(function(stream) {
  315. tracks.push(StreamUtils.textStreamToTrack(stream));
  316. });
  317. return tracks;
  318. };
  319. /**
  320. * Gets an array of Track objects for the given Period.
  321. *
  322. * @param {shakaExtern.Period} period
  323. * @param {?number} activeAudioId
  324. * @param {?number} activeVideoId
  325. * @return {!Array.<shakaExtern.Track>}
  326. */
  327. shaka.util.StreamUtils.getVariantTracks =
  328. function(period, activeAudioId, activeVideoId) {
  329. const StreamUtils = shaka.util.StreamUtils;
  330. let variants = StreamUtils.getPlayableVariants(period.variants);
  331. return variants.map(function(variant) {
  332. let track = StreamUtils.variantToTrack(variant);
  333. if (variant.video && variant.audio) {
  334. track.active = activeVideoId == variant.video.id &&
  335. activeAudioId == variant.audio.id;
  336. } else if (variant.video) {
  337. track.active = activeVideoId == variant.video.id;
  338. } else if (variant.audio) {
  339. track.active = activeAudioId == variant.audio.id;
  340. }
  341. return track;
  342. });
  343. };
  344. /**
  345. * Gets an array of text Track objects for the given Period.
  346. *
  347. * @param {shakaExtern.Period} period
  348. * @param {?number} activeStreamId
  349. * @return {!Array.<shakaExtern.Track>}
  350. */
  351. shaka.util.StreamUtils.getTextTracks = function(period, activeStreamId) {
  352. return period.textStreams.map(function(stream) {
  353. let track = shaka.util.StreamUtils.textStreamToTrack(stream);
  354. track.active = activeStreamId == stream.id;
  355. return track;
  356. });
  357. };
  358. /**
  359. * Finds the Variant for the given track.
  360. *
  361. * @param {shakaExtern.Period} period
  362. * @param {shakaExtern.Track} track
  363. * @return {?shakaExtern.Variant}
  364. */
  365. shaka.util.StreamUtils.findVariantForTrack = function(period, track) {
  366. for (let i = 0; i < period.variants.length; i++) {
  367. if (period.variants[i].id == track.id) {
  368. return period.variants[i];
  369. }
  370. }
  371. return null;
  372. };
  373. /**
  374. * Finds the text stream for the given track.
  375. *
  376. * @param {shakaExtern.Period} period
  377. * @param {shakaExtern.Track} track
  378. * @return {?shakaExtern.Stream}
  379. */
  380. shaka.util.StreamUtils.findTextStreamForTrack = function(period, track) {
  381. for (let i = 0; i < period.textStreams.length; i++) {
  382. if (period.textStreams[i].id == track.id) {
  383. return period.textStreams[i];
  384. }
  385. }
  386. return null;
  387. };
  388. /**
  389. * Determines if the given variant is playable.
  390. * @param {!shakaExtern.Variant} variant
  391. * @return {boolean}
  392. */
  393. shaka.util.StreamUtils.isPlayable = function(variant) {
  394. return variant.allowedByApplication && variant.allowedByKeySystem;
  395. };
  396. /**
  397. * Filters out unplayable variants.
  398. * @param {!Array.<!shakaExtern.Variant>} variants
  399. * @return {!Array.<!shakaExtern.Variant>}
  400. */
  401. shaka.util.StreamUtils.getPlayableVariants = function(variants) {
  402. return variants.filter(function(variant) {
  403. return shaka.util.StreamUtils.isPlayable(variant);
  404. });
  405. };
  406. /**
  407. * Chooses variants according to the given config.
  408. *
  409. * @param {!Array.<shakaExtern.Variant>} variants
  410. * @param {string} preferredLanguage
  411. * @param {string} preferredRole
  412. * @param {number} preferredAudioChannelCount
  413. * @param {!Object=} languageMatches
  414. * @return {!Array.<!shakaExtern.Variant>}
  415. */
  416. shaka.util.StreamUtils.filterVariantsByConfig = function(
  417. variants, preferredLanguage, preferredRole, preferredAudioChannelCount,
  418. languageMatches) {
  419. let chosen = shaka.util.StreamUtils.filterVariantsByLanguageAndRole(
  420. variants, preferredLanguage, preferredRole, languageMatches);
  421. return shaka.util.StreamUtils.filterVariantsByAudioChannelCount(chosen,
  422. preferredAudioChannelCount);
  423. };
  424. /**
  425. * Chooses variants according to the given config.
  426. *
  427. * @param {!Array.<shakaExtern.Variant>} variants
  428. * @param {string} preferredLanguage
  429. * @param {string} preferredRole
  430. * @param {!Object=} languageMatches
  431. * @return {!Array.<!shakaExtern.Variant>}
  432. */
  433. shaka.util.StreamUtils.filterVariantsByLanguageAndRole = function(
  434. variants, preferredLanguage, preferredRole, languageMatches) {
  435. const LanguageUtils = shaka.util.LanguageUtils;
  436. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  437. /** @type {!Array.<!shakaExtern.Variant>} */
  438. let playable = shaka.util.StreamUtils.getPlayableVariants(variants);
  439. /** @type {!Array.<!shakaExtern.Variant>} */
  440. let chosen = playable;
  441. // Start with the set of primary variants.
  442. /** @type {!Array.<!shakaExtern.Variant>} */
  443. let primary = playable.filter(function(variant) {
  444. return variant.primary;
  445. });
  446. if (primary.length) {
  447. chosen = primary;
  448. }
  449. // Now reduce the set to one language. This covers both arbitrary language
  450. // choices and the reduction of the "primary" variant set to one language.
  451. let firstLanguage = chosen.length ? chosen[0].language : '';
  452. chosen = chosen.filter(function(variant) {
  453. return variant.language == firstLanguage;
  454. });
  455. // Now search for matches based on language preference. If any language match
  456. // is found, it overrides the selection above. Favor exact matches, then base
  457. // matches, finally different subtags. Execute in reverse order so the later
  458. // steps override the previous ones.
  459. if (preferredLanguage) {
  460. let pref = LanguageUtils.normalize(preferredLanguage);
  461. [LanguageUtils.MatchType.OTHER_SUB_LANGUAGE_OKAY,
  462. LanguageUtils.MatchType.BASE_LANGUAGE_OKAY,
  463. LanguageUtils.MatchType.EXACT]
  464. .forEach(function(matchType) {
  465. let betterLangMatchFound = false;
  466. playable.forEach(function(variant) {
  467. pref = LanguageUtils.normalize(pref);
  468. let lang = LanguageUtils.normalize(variant.language);
  469. if (LanguageUtils.match(matchType, pref, lang)) {
  470. if (betterLangMatchFound) {
  471. chosen.push(variant);
  472. } else {
  473. chosen = [variant];
  474. betterLangMatchFound = true;
  475. }
  476. if (languageMatches) {
  477. languageMatches[ContentType.AUDIO] = true;
  478. }
  479. }
  480. }); // forEach(variant)
  481. }); // forEach(matchType)
  482. } // if (preferredLanguage)
  483. // Now refine the choice based on role preference.
  484. if (preferredRole) {
  485. let roleMatches = shaka.util.StreamUtils.filterVariantsByRole_(
  486. chosen, preferredRole);
  487. if (roleMatches.length) {
  488. return roleMatches;
  489. } else {
  490. shaka.log.warning('No exact match for the variant role could be found.');
  491. }
  492. }
  493. // Either there was no role preference, or it could not be satisfied.
  494. // Choose an arbitrary role, if there are any, and filter out any other roles.
  495. // This ensures we never adapt between roles.
  496. let allRoles = chosen.map(function(variant) {
  497. let audioRoles = variant.audio ? variant.audio.roles : [];
  498. let videoRoles = variant.video ? variant.video.roles : [];
  499. return audioRoles.concat(videoRoles);
  500. }).reduce(shaka.util.Functional.collapseArrays, []);
  501. if (!allRoles.length) {
  502. return chosen;
  503. }
  504. return shaka.util.StreamUtils.filterVariantsByRole_(chosen, allRoles[0]);
  505. };
  506. /**
  507. * Filters variants according to the given audio channel count config.
  508. *
  509. * @param {!Array.<shakaExtern.Variant>} variants
  510. * @param {number} preferredAudioChannelCount
  511. * @return {!Array.<!shakaExtern.Variant>}
  512. */
  513. shaka.util.StreamUtils.filterVariantsByAudioChannelCount = function(
  514. variants, preferredAudioChannelCount) {
  515. // Group variants by their audio channel counts.
  516. let variantsByChannelCount = variants
  517. .filter((v) => v.audio && v.audio.channelsCount)
  518. .reduce((map, variant) => {
  519. let count = variant.audio.channelsCount;
  520. if (map[count]) {
  521. map[count].push(variant);
  522. } else {
  523. map[count] = [variant];
  524. }
  525. return map;
  526. }, {});
  527. let channelCounts = Object.keys(variantsByChannelCount);
  528. // If no variant has audio channel count info, return the original variants.
  529. if (channelCounts.length == 0) {
  530. return variants;
  531. }
  532. // Choose the variants with the largest number of audio channels less than or
  533. // equal to the configured number of audio channels.
  534. let countLessThanOrEqualtoConfig =
  535. channelCounts.filter((count) => count <= preferredAudioChannelCount);
  536. if (countLessThanOrEqualtoConfig.length) {
  537. return variantsByChannelCount[Math.max.apply(null,
  538. countLessThanOrEqualtoConfig)];
  539. }
  540. // If all variants have more audio channels than the config, choose the
  541. // variants with the fewest audio channels.
  542. return variantsByChannelCount[Math.min.apply(null, channelCounts)];
  543. };
  544. /**
  545. * Chooses streams according to the given config.
  546. *
  547. * @param {!Array.<shakaExtern.Stream>} streams
  548. * @param {string} preferredLanguage
  549. * @param {string} preferredRole
  550. * @param {!Object=} languageMatches
  551. * @return {!Array.<!shakaExtern.Stream>}
  552. */
  553. shaka.util.StreamUtils.filterStreamsByLanguageAndRole = function(
  554. streams, preferredLanguage, preferredRole, languageMatches) {
  555. const LanguageUtils = shaka.util.LanguageUtils;
  556. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  557. /** @type {!Array.<!shakaExtern.Stream>} */
  558. let chosen = streams;
  559. // Start with the set of primary streams.
  560. /** @type {!Array.<!shakaExtern.Stream>} */
  561. let primary = streams.filter(function(stream) {
  562. return stream.primary;
  563. });
  564. if (primary.length) {
  565. chosen = primary;
  566. }
  567. // Now reduce the set to one language. This covers both arbitrary language
  568. // choice and the reduction of the "primary" stream set to one language.
  569. let firstLanguage = chosen.length ? chosen[0].language : '';
  570. chosen = chosen.filter(function(stream) {
  571. return stream.language == firstLanguage;
  572. });
  573. // Now search for matches based on language preference. If any language match
  574. // is found, it overrides the selection above. Favor exact matches, then base
  575. // matches, finally different subtags. Execute in reverse order so the later
  576. // steps override the previous ones.
  577. if (preferredLanguage) {
  578. let pref = LanguageUtils.normalize(preferredLanguage);
  579. [LanguageUtils.MatchType.OTHER_SUB_LANGUAGE_OKAY,
  580. LanguageUtils.MatchType.BASE_LANGUAGE_OKAY,
  581. LanguageUtils.MatchType.EXACT]
  582. .forEach(function(matchType) {
  583. let betterLangMatchFound = false;
  584. streams.forEach(function(stream) {
  585. let lang = LanguageUtils.normalize(stream.language);
  586. if (LanguageUtils.match(matchType, pref, lang)) {
  587. if (betterLangMatchFound) {
  588. chosen.push(stream);
  589. } else {
  590. chosen = [stream];
  591. betterLangMatchFound = true;
  592. }
  593. if (languageMatches) {
  594. languageMatches[ContentType.TEXT] = true;
  595. }
  596. }
  597. }); // forEach(stream)
  598. }); // forEach(matchType)
  599. } // if (preferredLanguage)
  600. // Now refine the choice based on role preference.
  601. if (preferredRole) {
  602. let roleMatches = shaka.util.StreamUtils.filterTextStreamsByRole_(
  603. chosen, preferredRole);
  604. if (roleMatches.length) {
  605. return roleMatches;
  606. } else {
  607. shaka.log.warning('No exact match for the text role could be found.');
  608. }
  609. } else {
  610. // Prefer text streams with no roles, if they exist.
  611. let noRoleMatches = chosen.filter(function(stream) {
  612. return stream.roles.length == 0;
  613. });
  614. if (noRoleMatches.length) {
  615. return noRoleMatches;
  616. }
  617. }
  618. // Either there was no role preference, or it could not be satisfied.
  619. // Choose an arbitrary role, if there are any, and filter out any other roles.
  620. // This ensures we never adapt between roles.
  621. let allRoles = chosen.map(function(stream) {
  622. return stream.roles;
  623. }).reduce(shaka.util.Functional.collapseArrays, []);
  624. if (!allRoles.length) {
  625. return chosen;
  626. }
  627. return shaka.util.StreamUtils.filterTextStreamsByRole_(chosen, allRoles[0]);
  628. };
  629. /**
  630. * Filter Variants by role.
  631. *
  632. * @param {!Array.<shakaExtern.Variant>} variants
  633. * @param {string} preferredRole
  634. * @return {!Array.<shakaExtern.Variant>}
  635. * @private
  636. */
  637. shaka.util.StreamUtils.filterVariantsByRole_ =
  638. function(variants, preferredRole) {
  639. return variants.filter(function(variant) {
  640. return (variant.audio && variant.audio.roles.indexOf(preferredRole) >= 0) ||
  641. (variant.video && variant.video.roles.indexOf(preferredRole) >= 0);
  642. });
  643. };
  644. /**
  645. * Filter text Streams by role.
  646. *
  647. * @param {!Array.<shakaExtern.Stream>} textStreams
  648. * @param {string} preferredRole
  649. * @return {!Array.<shakaExtern.Stream>}
  650. * @private
  651. */
  652. shaka.util.StreamUtils.filterTextStreamsByRole_ =
  653. function(textStreams, preferredRole) {
  654. return textStreams.filter(function(stream) {
  655. return stream.roles.indexOf(preferredRole) >= 0;
  656. });
  657. };
  658. /**
  659. * Finds a Variant with given audio and video streams.
  660. * Returns null if no such Variant was found.
  661. *
  662. * @param {?shakaExtern.Stream} audio
  663. * @param {?shakaExtern.Stream} video
  664. * @param {!Array.<!shakaExtern.Variant>} variants
  665. * @return {?shakaExtern.Variant}
  666. */
  667. shaka.util.StreamUtils.getVariantByStreams = function(audio, video, variants) {
  668. if (audio) {
  669. goog.asserts.assert(
  670. shaka.util.StreamUtils.isAudio(audio),
  671. 'Audio streams must have the audio type.');
  672. }
  673. if (video) {
  674. goog.asserts.assert(
  675. shaka.util.StreamUtils.isVideo(video),
  676. 'Video streams must have the video type.');
  677. }
  678. for (let i = 0; i < variants.length; i++) {
  679. if (variants[i].audio == audio && variants[i].video == video) {
  680. return variants[i];
  681. }
  682. }
  683. return null;
  684. };
  685. /**
  686. * Finds a Variant with the given video and audio streams, by stream ID.
  687. * Returns null if no such Variant was found.
  688. *
  689. * @param {?number} audioId
  690. * @param {?number} videoId
  691. * @param {!Array.<shakaExtern.Variant>} variants
  692. * @return {?shakaExtern.Variant}
  693. */
  694. shaka.util.StreamUtils.getVariantByStreamIds = function(
  695. audioId, videoId, variants) {
  696. function matchesId(id, stream) {
  697. if (id == null) {
  698. return stream == null;
  699. } else {
  700. return stream.id == id;
  701. }
  702. }
  703. for (let i = 0; i < variants.length; i++) {
  704. if (matchesId(audioId, variants[i].audio) &&
  705. matchesId(videoId, variants[i].video)) {
  706. return variants[i];
  707. }
  708. }
  709. return null;
  710. };
  711. /**
  712. * Gets the index of the Period that contains the given time.
  713. * @param {shakaExtern.Manifest} manifest
  714. * @param {number} time The time in seconds from the start of the presentation.
  715. * @return {number}
  716. */
  717. shaka.util.StreamUtils.findPeriodContainingTime = function(manifest, time) {
  718. let threshold = shaka.util.ManifestParserUtils.GAP_OVERLAP_TOLERANCE_SECONDS;
  719. for (let i = manifest.periods.length - 1; i > 0; --i) {
  720. let period = manifest.periods[i];
  721. // The last segment may end right before the end of the Period because of
  722. // rounding issues.
  723. if (time + threshold >= period.startTime) {
  724. return i;
  725. }
  726. }
  727. return 0;
  728. };
  729. /**
  730. * @param {shakaExtern.Manifest} manifest
  731. * @param {shakaExtern.Stream} stream
  732. * @return {number} The index of the Period which contains |stream|, or -1 if
  733. * no Period contains |stream|.
  734. */
  735. shaka.util.StreamUtils.findPeriodContainingStream = function(manifest, stream) {
  736. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  737. for (let periodIdx = 0; periodIdx < manifest.periods.length; ++periodIdx) {
  738. let period = manifest.periods[periodIdx];
  739. if (stream.type == ContentType.TEXT) {
  740. for (let j = 0; j < period.textStreams.length; ++j) {
  741. let textStream = period.textStreams[j];
  742. if (textStream == stream) {
  743. return periodIdx;
  744. }
  745. }
  746. } else {
  747. for (let j = 0; j < period.variants.length; ++j) {
  748. let variant = period.variants[j];
  749. if (variant.audio == stream || variant.video == stream ||
  750. (variant.video && variant.video.trickModeVideo == stream)) {
  751. return periodIdx;
  752. }
  753. }
  754. }
  755. }
  756. return -1;
  757. };
  758. /**
  759. * @param {shakaExtern.Manifest} manifest
  760. * @param {shakaExtern.Variant} variant
  761. * @return {number} The index of the Period which contains |stream|, or -1 if
  762. * no Period contains |stream|.
  763. */
  764. shaka.util.StreamUtils.findPeriodContainingVariant =
  765. function(manifest, variant) {
  766. for (let periodIdx = 0; periodIdx < manifest.periods.length; ++periodIdx) {
  767. let period = manifest.periods[periodIdx];
  768. for (let j = 0; j < period.variants.length; ++j) {
  769. if (period.variants[j] == variant) {
  770. return periodIdx;
  771. }
  772. }
  773. }
  774. return -1;
  775. };
  776. /**
  777. * Gets the rebuffering goal from the manifest and configuration.
  778. *
  779. * @param {shakaExtern.Manifest} manifest
  780. * @param {shakaExtern.StreamingConfiguration} config
  781. * @param {number} scaleFactor
  782. *
  783. * @return {number}
  784. */
  785. shaka.util.StreamUtils.getRebufferingGoal = function(
  786. manifest, config, scaleFactor) {
  787. return scaleFactor *
  788. Math.max(manifest.minBufferTime || 0, config.rebufferingGoal);
  789. };
  790. /**
  791. * Checks if the given stream is an audio stream.
  792. *
  793. * @param {shakaExtern.Stream} stream
  794. * @return {boolean}
  795. */
  796. shaka.util.StreamUtils.isAudio = function(stream) {
  797. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  798. return stream.type == ContentType.AUDIO;
  799. };
  800. /**
  801. * Checks if the given stream is a video stream.
  802. *
  803. * @param {shakaExtern.Stream} stream
  804. * @return {boolean}
  805. */
  806. shaka.util.StreamUtils.isVideo = function(stream) {
  807. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  808. return stream.type == ContentType.VIDEO;
  809. };