Source: lib/util/player_configuration.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.util.PlayerConfiguration');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.abr.SimpleAbrManager');
  9. goog.require('shaka.config.AutoShowText');
  10. goog.require('shaka.config.CodecSwitchingStrategy');
  11. goog.require('shaka.config.CrossBoundaryStrategy');
  12. goog.require('shaka.config.RepeatMode');
  13. goog.require('shaka.device.DeviceFactory');
  14. goog.require('shaka.drm.DrmUtils');
  15. goog.require('shaka.drm.FairPlay');
  16. goog.require('shaka.log');
  17. goog.require('shaka.media.Capabilities');
  18. goog.require('shaka.media.PreferenceBasedCriteria');
  19. goog.require('shaka.net.NetworkingEngine');
  20. goog.require('shaka.util.ConfigUtils');
  21. goog.require('shaka.util.LanguageUtils');
  22. goog.require('shaka.util.ManifestParserUtils');
  23. /**
  24. * @final
  25. * @export
  26. */
  27. shaka.util.PlayerConfiguration = class {
  28. /**
  29. * @return {shaka.extern.PlayerConfiguration}
  30. * @export
  31. */
  32. static createDefault() {
  33. // This is a relatively safe default in the absence of clues from the
  34. // browser. For slower connections, the default estimate may be too high.
  35. const bandwidthEstimate = 1e6; // 1Mbps
  36. const minBytes = 16e3;
  37. let abrMaxHeight = Infinity;
  38. const device = shaka.device.DeviceFactory.getDevice();
  39. // Some browsers implement the Network Information API, which allows
  40. // retrieving information about a user's network connection.
  41. if (navigator.connection) {
  42. // If the user has checked a box in the browser to ask it to use less
  43. // data, the browser will expose this intent via connection.saveData.
  44. // When that is true, we will default the max ABR height to 360p. Apps
  45. // can override this if they wish.
  46. //
  47. // The decision to use 360p was somewhat arbitrary. We needed a default
  48. // limit, and rather than restrict to a certain bandwidth, we decided to
  49. // restrict resolution. This will implicitly restrict bandwidth and
  50. // therefore save data. We (Shaka+Chrome) judged that:
  51. // - HD would be inappropriate
  52. // - If a user is asking their browser to save data, 360p it reasonable
  53. // - 360p would not look terrible on small mobile device screen
  54. // We also found that:
  55. // - YouTube's website on mobile defaults to 360p (as of 2018)
  56. // - iPhone 6, in portrait mode, has a physical resolution big enough
  57. // for 360p widescreen, but a little smaller than 480p widescreen
  58. // (https://apple.co/2yze4es)
  59. // If the content's lowest resolution is above 360p, AbrManager will use
  60. // the lowest resolution.
  61. if (navigator.connection.saveData) {
  62. abrMaxHeight = 360;
  63. }
  64. }
  65. const drm = {
  66. retryParameters: shaka.net.NetworkingEngine.defaultRetryParameters(),
  67. // These will all be verified by special cases in mergeConfigObjects_():
  68. servers: {}, // key is arbitrary key system ID, value must be string
  69. clearKeys: {}, // key is arbitrary key system ID, value must be string
  70. advanced: {}, // key is arbitrary key system ID, value is a record type
  71. delayLicenseRequestUntilPlayed: false,
  72. persistentSessionOnlinePlayback: false,
  73. persistentSessionsMetadata: [],
  74. initDataTransform: (initData, initDataType, drmInfo) => {
  75. if (shaka.drm.DrmUtils.isMediaKeysPolyfilled('apple') &&
  76. initDataType == 'skd') {
  77. const cert = drmInfo.serverCertificate;
  78. const contentId =
  79. shaka.drm.FairPlay.defaultGetContentId(initData);
  80. initData = shaka.drm.FairPlay.initDataTransform(
  81. initData, contentId, cert);
  82. }
  83. return initData;
  84. },
  85. logLicenseExchange: false,
  86. updateExpirationTime: 1,
  87. preferredKeySystems: [],
  88. keySystemsMapping: {},
  89. parseInbandPsshEnabled: false,
  90. minHdcpVersion: '',
  91. ignoreDuplicateInitData: true,
  92. defaultAudioRobustnessForWidevine: 'SW_SECURE_CRYPTO',
  93. defaultVideoRobustnessForWidevine: 'SW_SECURE_DECODE',
  94. };
  95. let codecSwitchingStrategy = shaka.config.CodecSwitchingStrategy.RELOAD;
  96. let multiTypeVariantsAllowed = false;
  97. if (shaka.media.Capabilities.isChangeTypeSupported() &&
  98. device.supportsSmoothCodecSwitching()) {
  99. codecSwitchingStrategy = shaka.config.CodecSwitchingStrategy.SMOOTH;
  100. multiTypeVariantsAllowed = true;
  101. }
  102. const manifest = {
  103. retryParameters: shaka.net.NetworkingEngine.defaultRetryParameters(),
  104. availabilityWindowOverride: NaN,
  105. disableAudio: false,
  106. disableVideo: false,
  107. disableText: false,
  108. disableThumbnails: false,
  109. disableIFrames: false,
  110. defaultPresentationDelay: 0,
  111. segmentRelativeVttTiming: false,
  112. raiseFatalErrorOnManifestUpdateRequestFailure: false,
  113. continueLoadingWhenPaused: true,
  114. ignoreSupplementalCodecs: false,
  115. updatePeriod: -1,
  116. ignoreDrmInfo: false,
  117. dash: {
  118. clockSyncUri: '',
  119. disableXlinkProcessing: true,
  120. xlinkFailGracefully: false,
  121. ignoreMinBufferTime: false,
  122. autoCorrectDrift: true,
  123. initialSegmentLimit: 1000,
  124. ignoreSuggestedPresentationDelay: false,
  125. ignoreEmptyAdaptationSet: false,
  126. ignoreMaxSegmentDuration: false,
  127. keySystemsByURI: {
  128. 'urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b':
  129. 'org.w3.clearkey',
  130. 'urn:uuid:e2719d58-a985-b3c9-781a-b030af78d30e':
  131. 'org.w3.clearkey',
  132. 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed':
  133. 'com.widevine.alpha',
  134. 'urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95':
  135. 'com.microsoft.playready',
  136. 'urn:uuid:79f0049a-4098-8642-ab92-e65be0885f95':
  137. 'com.microsoft.playready',
  138. 'urn:uuid:94ce86fb-07ff-4f43-adb8-93d2fa968ca2':
  139. 'com.apple.fps',
  140. 'urn:uuid:3d5e6d35-9b9a-41e8-b843-dd3c6e72c42c':
  141. 'com.huawei.wiseplay',
  142. },
  143. manifestPreprocessor:
  144. shaka.util.PlayerConfiguration.defaultManifestPreprocessor,
  145. manifestPreprocessorTXml:
  146. shaka.util.PlayerConfiguration.defaultManifestPreprocessorTXml,
  147. sequenceMode: false,
  148. multiTypeVariantsAllowed,
  149. useStreamOnceInPeriodFlattening: false,
  150. enableFastSwitching: true,
  151. },
  152. hls: {
  153. ignoreTextStreamFailures: false,
  154. ignoreImageStreamFailures: false,
  155. defaultAudioCodec: 'mp4a.40.2',
  156. defaultVideoCodec: 'avc1.42E01E',
  157. ignoreManifestProgramDateTime: false,
  158. ignoreManifestProgramDateTimeForTypes: [],
  159. mediaPlaylistFullMimeType:
  160. 'video/mp2t; codecs="avc1.42E01E, mp4a.40.2"',
  161. liveSegmentsDelay: 3,
  162. sequenceMode: device.supportsSequenceMode(),
  163. ignoreManifestTimestampsInSegmentsMode: false,
  164. disableCodecGuessing: false,
  165. disableClosedCaptionsDetection: false,
  166. allowLowLatencyByteRangeOptimization: true,
  167. allowRangeRequestsToGuessMimeType: false,
  168. },
  169. mss: {
  170. manifestPreprocessor:
  171. shaka.util.PlayerConfiguration.defaultManifestPreprocessor,
  172. manifestPreprocessorTXml:
  173. shaka.util.PlayerConfiguration.defaultManifestPreprocessorTXml,
  174. sequenceMode: false,
  175. keySystemsBySystemId: {
  176. '9a04f079-9840-4286-ab92-e65be0885f95':
  177. 'com.microsoft.playready',
  178. '79f0049a-4098-8642-ab92-e65be0885f95':
  179. 'com.microsoft.playready',
  180. },
  181. },
  182. };
  183. const streaming = {
  184. retryParameters: shaka.net.NetworkingEngine.defaultRetryParameters(),
  185. // Need some operation in the callback or else closure may remove calls
  186. // to the function as it would be a no-op. The operation can't just be a
  187. // log message, because those are stripped in the compiled build.
  188. failureCallback: (error) => {
  189. shaka.log.error('Unhandled streaming error', error);
  190. return shaka.util.ConfigUtils.referenceParametersAndReturn(
  191. [error],
  192. undefined);
  193. },
  194. rebufferingGoal: 0,
  195. bufferingGoal: 10,
  196. bufferBehind: 30,
  197. evictionGoal: 1,
  198. ignoreTextStreamFailures: false,
  199. alwaysStreamText: false,
  200. startAtSegmentBoundary: false,
  201. gapDetectionThreshold: 0.5,
  202. gapPadding: 0,
  203. gapJumpTimerTime: 0.25 /* seconds */,
  204. durationBackoff: 1,
  205. // Offset by 5 seconds since Chromecast takes a few seconds to start
  206. // playing after a seek, even when buffered.
  207. safeSeekOffset: 5,
  208. safeSeekEndOffset: 0,
  209. stallEnabled: true,
  210. stallThreshold: 1 /* seconds */,
  211. stallSkip: 0.1 /* seconds */,
  212. useNativeHlsForFairPlay: true,
  213. // If we are within 2 seconds of the start of a live segment, fetch the
  214. // previous one. This allows for segment drift, but won't download an
  215. // extra segment if we aren't close to the start.
  216. // When low latency streaming is enabled, inaccurateManifestTolerance
  217. // will default to 0 if not specified.
  218. inaccurateManifestTolerance: 2,
  219. lowLatencyMode: false,
  220. preferNativeDash: false,
  221. preferNativeHls: false,
  222. updateIntervalSeconds: 1,
  223. observeQualityChanges: false,
  224. maxDisabledTime: 30,
  225. // When low latency streaming is enabled, segmentPrefetchLimit will
  226. // default to 2 if not specified.
  227. segmentPrefetchLimit: 1,
  228. prefetchAudioLanguages: [],
  229. disableAudioPrefetch: false,
  230. disableTextPrefetch: false,
  231. disableVideoPrefetch: false,
  232. liveSync: {
  233. enabled: false,
  234. targetLatency: 0.5,
  235. targetLatencyTolerance: 0.5,
  236. maxPlaybackRate: 1.1,
  237. minPlaybackRate: 0.95,
  238. panicMode: false,
  239. panicThreshold: 60,
  240. dynamicTargetLatency: {
  241. enabled: false,
  242. stabilityThreshold: 60,
  243. rebufferIncrement: 0.5,
  244. maxAttempts: 10,
  245. maxLatency: 4,
  246. minLatency: 1,
  247. },
  248. },
  249. allowMediaSourceRecoveries: true,
  250. minTimeBetweenRecoveries: 5,
  251. vodDynamicPlaybackRate: false,
  252. vodDynamicPlaybackRateLowBufferRate: 0.95,
  253. vodDynamicPlaybackRateBufferRatio: 0.5,
  254. preloadNextUrlWindow: 30,
  255. loadTimeout: 30,
  256. clearDecodingCache: false,
  257. dontChooseCodecs: false,
  258. shouldFixTimestampOffset: false,
  259. avoidEvictionOnQuotaExceededError: false,
  260. crossBoundaryStrategy: shaka.config.CrossBoundaryStrategy.KEEP,
  261. returnToEndOfLiveWindowWhenOutside: false,
  262. };
  263. const networking = {
  264. forceHTTP: false,
  265. forceHTTPS: false,
  266. minBytesForProgressEvents: minBytes,
  267. };
  268. const offline = {
  269. // We need to set this to a throw-away implementation for now as our
  270. // default implementation will need to reference other fields in the
  271. // config. We will set it to our intended implementation after we have
  272. // the top-level object created.
  273. // eslint-disable-next-line require-await
  274. trackSelectionCallback: async (tracks) => tracks,
  275. downloadSizeCallback: async (sizeEstimate) => {
  276. if (navigator.storage && navigator.storage.estimate) {
  277. const estimate = await navigator.storage.estimate();
  278. // Limit to 95% of quota.
  279. return estimate.usage + sizeEstimate < estimate.quota * 0.95;
  280. } else {
  281. return true;
  282. }
  283. },
  284. // Need some operation in the callback or else closure may remove calls
  285. // to the function as it would be a no-op. The operation can't just be a
  286. // log message, because those are stripped in the compiled build.
  287. progressCallback: (content, progress) => {
  288. return shaka.util.ConfigUtils.referenceParametersAndReturn(
  289. [content, progress],
  290. undefined);
  291. },
  292. // By default we use persistent licenses as forces errors to surface if
  293. // a platform does not support offline licenses rather than causing
  294. // unexpected behaviours when someone tries to plays downloaded content
  295. // without a persistent license.
  296. usePersistentLicense: true,
  297. numberOfParallelDownloads: 5,
  298. };
  299. const abr = {
  300. enabled: true,
  301. useNetworkInformation: true,
  302. defaultBandwidthEstimate: bandwidthEstimate,
  303. switchInterval: 8,
  304. bandwidthUpgradeTarget: 0.85,
  305. bandwidthDowngradeTarget: 0.95,
  306. restrictions: {
  307. minWidth: 0,
  308. maxWidth: Infinity,
  309. minHeight: 0,
  310. maxHeight: abrMaxHeight,
  311. minPixels: 0,
  312. maxPixels: Infinity,
  313. minFrameRate: 0,
  314. maxFrameRate: Infinity,
  315. minBandwidth: 0,
  316. maxBandwidth: Infinity,
  317. minChannelsCount: 0,
  318. maxChannelsCount: Infinity,
  319. },
  320. advanced: {
  321. minTotalBytes: 128e3,
  322. minBytes,
  323. fastHalfLife: 2,
  324. slowHalfLife: 5,
  325. },
  326. restrictToElementSize: false,
  327. restrictToScreenSize: false,
  328. ignoreDevicePixelRatio: false,
  329. clearBufferSwitch: false,
  330. safeMarginSwitch: 0,
  331. cacheLoadThreshold: 20,
  332. minTimeToSwitch: 0,
  333. preferNetworkInformationBandwidth: false,
  334. removeLatencyFromFirstPacketTime: true,
  335. };
  336. const cmcd = {
  337. enabled: false,
  338. sessionId: '',
  339. contentId: '',
  340. rtpSafetyFactor: 5,
  341. useHeaders: false,
  342. includeKeys: [],
  343. version: 1,
  344. };
  345. const cmsd = {
  346. enabled: true,
  347. applyMaximumSuggestedBitrate: true,
  348. estimatedThroughputWeightRatio: 0.5,
  349. };
  350. const lcevc = {
  351. enabled: false,
  352. dynamicPerformanceScaling: true,
  353. logLevel: 0,
  354. drawLogo: false,
  355. poster: true,
  356. };
  357. const mediaSource = {
  358. codecSwitchingStrategy: codecSwitchingStrategy,
  359. addExtraFeaturesToSourceBuffer: (mimeType) => {
  360. return shaka.util.ConfigUtils.referenceParametersAndReturn(
  361. [mimeType],
  362. '');
  363. },
  364. forceTransmux: false,
  365. insertFakeEncryptionInInit: true,
  366. correctEc3Enca: false,
  367. modifyCueCallback: (cue, uri) => {
  368. return shaka.util.ConfigUtils.referenceParametersAndReturn(
  369. [cue, uri],
  370. undefined);
  371. },
  372. dispatchAllEmsgBoxes: false,
  373. useSourceElements: true,
  374. durationReductionEmitsUpdateEnd: true,
  375. };
  376. const ads = {
  377. customPlayheadTracker: false,
  378. skipPlayDetection: false,
  379. supportsMultipleMediaElements: true,
  380. disableHLSInterstitial: false,
  381. disableDASHInterstitial: false,
  382. allowPreloadOnDomElements: true,
  383. allowStartInMiddleOfInterstitial: true,
  384. };
  385. const textDisplayer = {
  386. captionsUpdatePeriod: 0.25,
  387. fontScaleFactor: 1,
  388. };
  389. const queue = {
  390. preloadNextUrlWindow: 30,
  391. repeatMode: shaka.config.RepeatMode.OFF,
  392. };
  393. const AutoShowText = shaka.config.AutoShowText;
  394. /** @type {shaka.extern.PlayerConfiguration} */
  395. const config = {
  396. drm: drm,
  397. manifest: manifest,
  398. streaming: streaming,
  399. networking: networking,
  400. mediaSource: mediaSource,
  401. offline: offline,
  402. abrFactory: () => new shaka.abr.SimpleAbrManager(),
  403. adaptationSetCriteriaFactory:
  404. (...args) => new shaka.media.PreferenceBasedCriteria(...args),
  405. abr: abr,
  406. autoShowText: AutoShowText.IF_SUBTITLES_MAY_BE_NEEDED,
  407. preferredAudioLanguage: '',
  408. preferredAudioLabel: '',
  409. preferredTextLanguage: '',
  410. preferredVariantRole: '',
  411. preferredTextRole: '',
  412. preferredAudioChannelCount: 2,
  413. preferredVideoHdrLevel: 'AUTO',
  414. preferredVideoLayout: '',
  415. preferredVideoLabel: '',
  416. preferredVideoCodecs: [],
  417. preferredAudioCodecs: [],
  418. preferredTextFormats: [],
  419. preferForcedSubs: false,
  420. preferSpatialAudio: false,
  421. preferredDecodingAttributes: [],
  422. restrictions: {
  423. minWidth: 0,
  424. maxWidth: Infinity,
  425. minHeight: 0,
  426. maxHeight: Infinity,
  427. minPixels: 0,
  428. maxPixels: Infinity,
  429. minFrameRate: 0,
  430. maxFrameRate: Infinity,
  431. minBandwidth: 0,
  432. maxBandwidth: Infinity,
  433. minChannelsCount: 0,
  434. maxChannelsCount: Infinity,
  435. },
  436. playRangeStart: 0,
  437. playRangeEnd: Infinity,
  438. textDisplayer: textDisplayer,
  439. textDisplayFactory: () => null,
  440. cmcd: cmcd,
  441. cmsd: cmsd,
  442. lcevc: lcevc,
  443. ads: ads,
  444. ignoreHardwareResolution: false,
  445. queue: queue,
  446. };
  447. // Add this callback so that we can reference the preferred audio language
  448. // through the config object so that if it gets updated, we have the
  449. // updated value.
  450. // eslint-disable-next-line require-await
  451. offline.trackSelectionCallback = async (tracks) => {
  452. return shaka.util.PlayerConfiguration.defaultTrackSelect(
  453. tracks, config.preferredAudioLanguage,
  454. config.preferredVideoHdrLevel);
  455. };
  456. return device.adjustConfig(config);
  457. }
  458. /**
  459. * @return {!Object}
  460. * @export
  461. */
  462. static createDefaultForLL() {
  463. return {
  464. streaming: {
  465. inaccurateManifestTolerance: 0,
  466. segmentPrefetchLimit: 2,
  467. updateIntervalSeconds: 0.1,
  468. maxDisabledTime: 1,
  469. retryParameters: {
  470. baseDelay: 100,
  471. },
  472. },
  473. manifest: {
  474. dash: {
  475. autoCorrectDrift: false,
  476. },
  477. retryParameters: {
  478. baseDelay: 100,
  479. },
  480. },
  481. drm: {
  482. retryParameters: {
  483. baseDelay: 100,
  484. },
  485. },
  486. };
  487. }
  488. /**
  489. * Merges the given configuration changes into the given destination. This
  490. * uses the default Player configurations as the template.
  491. *
  492. * @param {shaka.extern.PlayerConfiguration} destination
  493. * @param {!Object} updates
  494. * @param {shaka.extern.PlayerConfiguration=} template
  495. * @return {boolean}
  496. * @export
  497. */
  498. static mergeConfigObjects(destination, updates, template) {
  499. const overrides = {
  500. '.drm.keySystemsMapping': '',
  501. '.drm.servers': '',
  502. '.drm.clearKeys': '',
  503. '.drm.advanced': {
  504. distinctiveIdentifierRequired: false,
  505. persistentStateRequired: false,
  506. videoRobustness: [],
  507. audioRobustness: [],
  508. sessionType: '',
  509. serverCertificate: new Uint8Array(0),
  510. serverCertificateUri: '',
  511. individualizationServer: '',
  512. headers: {},
  513. },
  514. };
  515. return shaka.util.ConfigUtils.mergeConfigObjects(
  516. destination, updates,
  517. template || shaka.util.PlayerConfiguration.createDefault(), overrides,
  518. '');
  519. }
  520. /**
  521. * @param {!Array<shaka.extern.Track>} tracks
  522. * @param {string} preferredAudioLanguage
  523. * @param {string} preferredVideoHdrLevel
  524. * @return {!Array<shaka.extern.Track>}
  525. */
  526. static defaultTrackSelect(
  527. tracks, preferredAudioLanguage, preferredVideoHdrLevel) {
  528. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  529. const LanguageUtils = shaka.util.LanguageUtils;
  530. let hdrLevel = preferredVideoHdrLevel;
  531. if (hdrLevel == 'AUTO') {
  532. const someHLG = tracks.some((track) => {
  533. if (track.hdr && track.hdr == 'HLG') {
  534. return true;
  535. }
  536. return false;
  537. });
  538. const device = shaka.device.DeviceFactory.getDevice();
  539. hdrLevel = device.getHdrLevel(someHLG);
  540. }
  541. /** @type {!Array<shaka.extern.Track>} */
  542. const allVariants = tracks.filter((track) => {
  543. if (track.type != 'variant') {
  544. return false;
  545. }
  546. if (track.hdr && track.hdr != hdrLevel) {
  547. return false;
  548. }
  549. return true;
  550. });
  551. /** @type {!Array<shaka.extern.Track>} */
  552. let selectedVariants = [];
  553. // Find the locale that best matches our preferred audio locale.
  554. const closestLocale = LanguageUtils.findClosestLocale(
  555. preferredAudioLanguage,
  556. allVariants.map((variant) => variant.language));
  557. // If we found a locale that was close to our preference, then only use
  558. // variants that use that locale.
  559. if (closestLocale) {
  560. selectedVariants = allVariants.filter((variant) => {
  561. const locale = LanguageUtils.normalize(variant.language);
  562. return locale == closestLocale;
  563. });
  564. }
  565. // If we failed to get a language match, go with primary.
  566. if (selectedVariants.length == 0) {
  567. selectedVariants = allVariants.filter((variant) => {
  568. return variant.primary;
  569. });
  570. }
  571. // Otherwise, there is no good way to choose the language, so we don't
  572. // choose a language at all.
  573. if (selectedVariants.length == 0) {
  574. // Issue a warning, but only if the content has multiple languages.
  575. // Otherwise, this warning would just be noise.
  576. const languages = new Set(allVariants.map((track) => {
  577. return track.language;
  578. }));
  579. if (languages.size > 1) {
  580. shaka.log.warning('Could not choose a good audio track based on ' +
  581. 'language preferences or primary tracks. An ' +
  582. 'arbitrary language will be stored!');
  583. }
  584. // Default back to all variants.
  585. selectedVariants = allVariants;
  586. }
  587. // From previously selected variants, choose the SD ones (height <= 480).
  588. const tracksByHeight = selectedVariants.filter((track) => {
  589. return track.height && track.height <= 480;
  590. });
  591. // If variants don't have video or no video with height <= 480 was
  592. // found, proceed with the previously selected tracks.
  593. if (tracksByHeight.length) {
  594. // Sort by resolution, then select all variants which match the height
  595. // of the highest SD res. There may be multiple audio bitrates for the
  596. // same video resolution.
  597. tracksByHeight.sort((a, b) => {
  598. // The items in this list have already been screened for height, but the
  599. // compiler doesn't know that.
  600. goog.asserts.assert(a.height != null, 'Null height');
  601. goog.asserts.assert(b.height != null, 'Null height');
  602. return b.height - a.height;
  603. });
  604. selectedVariants = tracksByHeight.filter((track) => {
  605. return track.height == tracksByHeight[0].height;
  606. });
  607. }
  608. /** @type {!Array<shaka.extern.Track>} */
  609. const selectedTracks = [];
  610. // If there are multiple matches at different audio bitrates, select the
  611. // middle bandwidth one.
  612. if (selectedVariants.length) {
  613. const middleIndex = Math.floor(selectedVariants.length / 2);
  614. selectedVariants.sort((a, b) => a.bandwidth - b.bandwidth);
  615. selectedTracks.push(selectedVariants[middleIndex]);
  616. }
  617. // Since this default callback is used primarily by our own demo app and by
  618. // app developers who haven't thought about which tracks they want, we
  619. // should select all image/text tracks, regardless of language. This makes
  620. // for a better demo for us, and does not rely on user preferences for the
  621. // unconfigured app.
  622. for (const track of tracks) {
  623. if (track.type == ContentType.TEXT || track.type == ContentType.IMAGE) {
  624. selectedTracks.push(track);
  625. }
  626. }
  627. return selectedTracks;
  628. }
  629. /**
  630. * @param {!Element} element
  631. * @return {!Element}
  632. */
  633. static defaultManifestPreprocessor(element) {
  634. return shaka.util.ConfigUtils.referenceParametersAndReturn(
  635. [element],
  636. element);
  637. }
  638. /**
  639. * @param {!shaka.extern.xml.Node} element
  640. * @return {!shaka.extern.xml.Node}
  641. */
  642. static defaultManifestPreprocessorTXml(element) {
  643. return shaka.util.ConfigUtils.referenceParametersAndReturn(
  644. [element],
  645. element);
  646. }
  647. };