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