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