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