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