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