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