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