/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.util.PlayerConfiguration');
goog.require('goog.asserts');
goog.require('shaka.abr.SimpleAbrManager');
goog.require('shaka.config.AutoShowText');
goog.require('shaka.config.CodecSwitchingStrategy');
goog.require('shaka.log');
goog.require('shaka.media.Capabilities');
goog.require('shaka.net.NetworkingEngine');
goog.require('shaka.util.ConfigUtils');
goog.require('shaka.util.FairPlayUtils');
goog.require('shaka.util.LanguageUtils');
goog.require('shaka.util.ManifestParserUtils');
goog.require('shaka.util.Platform');
/**
* @final
* @export
*/
shaka.util.PlayerConfiguration = class {
/**
* @return {shaka.extern.PlayerConfiguration}
* @export
*/
static createDefault() {
// This is a relatively safe default in the absence of clues from the
// browser. For slower connections, the default estimate may be too high.
const bandwidthEstimate = 1e6; // 1Mbps
const minBytes = 16e3;
let abrMaxHeight = Infinity;
// Some browsers implement the Network Information API, which allows
// retrieving information about a user's network connection.
if (navigator.connection) {
// If the user has checked a box in the browser to ask it to use less
// data, the browser will expose this intent via connection.saveData.
// When that is true, we will default the max ABR height to 360p. Apps
// can override this if they wish.
//
// The decision to use 360p was somewhat arbitrary. We needed a default
// limit, and rather than restrict to a certain bandwidth, we decided to
// restrict resolution. This will implicitly restrict bandwidth and
// therefore save data. We (Shaka+Chrome) judged that:
// - HD would be inappropriate
// - If a user is asking their browser to save data, 360p it reasonable
// - 360p would not look terrible on small mobile device screen
// We also found that:
// - YouTube's website on mobile defaults to 360p (as of 2018)
// - iPhone 6, in portrait mode, has a physical resolution big enough
// for 360p widescreen, but a little smaller than 480p widescreen
// (https://apple.co/2yze4es)
// If the content's lowest resolution is above 360p, AbrManager will use
// the lowest resolution.
if (navigator.connection.saveData) {
abrMaxHeight = 360;
}
}
const drm = {
retryParameters: shaka.net.NetworkingEngine.defaultRetryParameters(),
// These will all be verified by special cases in mergeConfigObjects_():
servers: {}, // key is arbitrary key system ID, value must be string
clearKeys: {}, // key is arbitrary key system ID, value must be string
advanced: {}, // key is arbitrary key system ID, value is a record type
delayLicenseRequestUntilPlayed: false,
persistentSessionOnlinePlayback: false,
persistentSessionsMetadata: [],
initDataTransform: (initData, initDataType, drmInfo) => {
if (shaka.util.Platform.isMediaKeysPolyfilled() &&
initDataType == 'skd') {
const cert = drmInfo.serverCertificate;
const contentId =
shaka.util.FairPlayUtils.defaultGetContentId(initData);
initData = shaka.util.FairPlayUtils.initDataTransform(
initData, contentId, cert);
}
return initData;
},
logLicenseExchange: false,
updateExpirationTime: 1,
preferredKeySystems: [],
keySystemsMapping: {},
// The Xbox One browser does not detect DRM key changes signalled by a
// change in the PSSH in media segments. We need to parse PSSH from media
// segments to detect key changes.
parseInbandPsshEnabled: shaka.util.Platform.isXboxOne(),
minHdcpVersion: '',
ignoreDuplicateInitData: !shaka.util.Platform.isTizen2(),
};
// The Xbox One and PS4 only support the Playready DRM, so they should
// prefer that key system by default to improve startup performance.
if (shaka.util.Platform.isXboxOne() ||
shaka.util.Platform.isPS4()) {
drm.preferredKeySystems.push('com.microsoft.playready');
}
let codecSwitchingStrategy = shaka.config.CodecSwitchingStrategy.RELOAD;
let multiTypeVariantsAllowed = false;
if (shaka.media.Capabilities.isChangeTypeSupported() &&
shaka.util.Platform.supportsSmoothCodecSwitching()) {
codecSwitchingStrategy = shaka.config.CodecSwitchingStrategy.SMOOTH;
multiTypeVariantsAllowed = true;
}
const manifest = {
retryParameters: shaka.net.NetworkingEngine.defaultRetryParameters(),
availabilityWindowOverride: NaN,
disableAudio: false,
disableVideo: false,
disableText: false,
disableThumbnails: false,
disableIFrames: false,
defaultPresentationDelay: 0,
segmentRelativeVttTiming: false,
raiseFatalErrorOnManifestUpdateRequestFailure: false,
continueLoadingWhenPaused: true,
dash: {
clockSyncUri: '',
ignoreDrmInfo: false,
disableXlinkProcessing: true,
xlinkFailGracefully: false,
ignoreMinBufferTime: false,
autoCorrectDrift: true,
initialSegmentLimit: 1000,
ignoreSuggestedPresentationDelay: false,
ignoreEmptyAdaptationSet: false,
ignoreMaxSegmentDuration: false,
keySystemsByURI: {
'urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b':
'org.w3.clearkey',
'urn:uuid:e2719d58-a985-b3c9-781a-b030af78d30e':
'org.w3.clearkey',
'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed':
'com.widevine.alpha',
'urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95':
'com.microsoft.playready',
'urn:uuid:79f0049a-4098-8642-ab92-e65be0885f95':
'com.microsoft.playready',
'urn:uuid:94ce86fb-07ff-4f43-adb8-93d2fa968ca2':
'com.apple.fps',
},
manifestPreprocessor:
shaka.util.PlayerConfiguration.defaultManifestPreprocessor,
manifestPreprocessorTXml:
shaka.util.PlayerConfiguration.defaultManifestPreprocessorTXml,
sequenceMode: false,
multiTypeVariantsAllowed,
useStreamOnceInPeriodFlattening: false,
updatePeriod: -1,
enableFastSwitching: true,
},
hls: {
ignoreTextStreamFailures: false,
ignoreImageStreamFailures: false,
defaultAudioCodec: 'mp4a.40.2',
defaultVideoCodec: 'avc1.42E01E',
ignoreManifestProgramDateTime: false,
ignoreManifestProgramDateTimeForTypes: [],
mediaPlaylistFullMimeType:
'video/mp2t; codecs="avc1.42E01E, mp4a.40.2"',
liveSegmentsDelay: 3,
sequenceMode: shaka.util.Platform.supportsSequenceMode(),
ignoreManifestTimestampsInSegmentsMode: false,
disableCodecGuessing: false,
disableClosedCaptionsDetection: false,
updatePeriod: -1,
allowLowLatencyByteRangeOptimization: true,
},
mss: {
manifestPreprocessor:
shaka.util.PlayerConfiguration.defaultManifestPreprocessor,
manifestPreprocessorTXml:
shaka.util.PlayerConfiguration.defaultManifestPreprocessorTXml,
sequenceMode: false,
keySystemsBySystemId: {
'9a04f079-9840-4286-ab92-e65be0885f95':
'com.microsoft.playready',
'79f0049a-4098-8642-ab92-e65be0885f95':
'com.microsoft.playready',
},
},
};
const streaming = {
retryParameters: shaka.net.NetworkingEngine.defaultRetryParameters(),
// Need some operation in the callback or else closure may remove calls
// to the function as it would be a no-op. The operation can't just be a
// log message, because those are stripped in the compiled build.
failureCallback: (error) => {
shaka.log.error('Unhandled streaming error', error);
return shaka.util.ConfigUtils.referenceParametersAndReturn(
[error],
undefined);
},
// When low latency streaming is enabled, rebufferingGoal will default to
// 0.01 if not specified.
rebufferingGoal: 2,
bufferingGoal: 10,
bufferBehind: 30,
evictionGoal: 1,
ignoreTextStreamFailures: false,
alwaysStreamText: false,
startAtSegmentBoundary: false,
gapDetectionThreshold: 0.5,
gapPadding: 0.01,
gapJumpTimerTime: 0.25 /* seconds */,
durationBackoff: 1,
// Offset by 5 seconds since Chromecast takes a few seconds to start
// playing after a seek, even when buffered.
safeSeekOffset: 5,
safeSeekEndOffset: 0,
stallEnabled: true,
stallThreshold: 1 /* seconds */,
stallSkip: 0.1 /* seconds */,
useNativeHlsForFairPlay: true,
// If we are within 2 seconds of the start of a live segment, fetch the
// previous one. This allows for segment drift, but won't download an
// extra segment if we aren't close to the start.
// When low latency streaming is enabled, inaccurateManifestTolerance
// will default to 0 if not specified.
inaccurateManifestTolerance: 2,
lowLatencyMode: false,
autoLowLatencyMode: false,
forceHTTP: false,
forceHTTPS: false,
minBytesForProgressEvents: minBytes,
preferNativeHls: false,
updateIntervalSeconds: 1,
observeQualityChanges: false,
maxDisabledTime: 30,
// When low latency streaming is enabled, segmentPrefetchLimit will
// default to 2 if not specified.
segmentPrefetchLimit: 0,
prefetchAudioLanguages: [],
disableAudioPrefetch: false,
disableTextPrefetch: false,
disableVideoPrefetch: false,
liveSync: {
enabled: false,
targetLatency: 0.5,
targetLatencyTolerance: 0.5,
maxPlaybackRate: 1.1,
minPlaybackRate: 0.95,
panicMode: false,
panicThreshold: 60,
dynamicTargetLatency: {
enabled: false,
stabilityThreshold: 60,
rebufferIncrement: 0.5,
maxAttempts: 10,
maxLatency: 4,
minLatency: 1,
},
},
allowMediaSourceRecoveries: true,
minTimeBetweenRecoveries: 5,
vodDynamicPlaybackRate: false,
vodDynamicPlaybackRateLowBufferRate: 0.95,
vodDynamicPlaybackRateBufferRatio: 0.5,
infiniteLiveStreamDuration: false,
preloadNextUrlWindow: 30,
loadTimeout: 30,
clearDecodingCache: shaka.util.Platform.isPS4() ||
shaka.util.Platform.isPS5(),
dontChooseCodecs: false,
shouldFixTimestampOffset: shaka.util.Platform.isWebOS() ||
shaka.util.Platform.isTizen(),
};
// WebOS, Tizen, Chromecast and Hisense have long hardware pipelines
// that respond slowly to seeking.
// Therefore we should not seek when we detect a stall
// on one of these platforms. Instead, default stallSkip to 0 to force the
// stall detector to pause and play instead.
if (shaka.util.Platform.isWebOS() ||
shaka.util.Platform.isTizen() ||
shaka.util.Platform.isChromecast() ||
shaka.util.Platform.isHisense()) {
streaming.stallSkip = 0;
}
if (shaka.util.Platform.isTizen()) {
streaming.gapPadding = 2;
}
const offline = {
// We need to set this to a throw-away implementation for now as our
// default implementation will need to reference other fields in the
// config. We will set it to our intended implementation after we have
// the top-level object created.
// eslint-disable-next-line require-await
trackSelectionCallback: async (tracks) => tracks,
downloadSizeCallback: async (sizeEstimate) => {
if (navigator.storage && navigator.storage.estimate) {
const estimate = await navigator.storage.estimate();
// Limit to 95% of quota.
return estimate.usage + sizeEstimate < estimate.quota * 0.95;
} else {
return true;
}
},
// Need some operation in the callback or else closure may remove calls
// to the function as it would be a no-op. The operation can't just be a
// log message, because those are stripped in the compiled build.
progressCallback: (content, progress) => {
return shaka.util.ConfigUtils.referenceParametersAndReturn(
[content, progress],
undefined);
},
// By default we use persistent licenses as forces errors to surface if
// a platform does not support offline licenses rather than causing
// unexpected behaviours when someone tries to plays downloaded content
// without a persistent license.
usePersistentLicense: true,
numberOfParallelDownloads: 5,
};
const abr = {
enabled: true,
useNetworkInformation: true,
defaultBandwidthEstimate: bandwidthEstimate,
switchInterval: 8,
bandwidthUpgradeTarget: 0.85,
bandwidthDowngradeTarget: 0.95,
restrictions: {
minWidth: 0,
maxWidth: Infinity,
minHeight: 0,
maxHeight: abrMaxHeight,
minPixels: 0,
maxPixels: Infinity,
minFrameRate: 0,
maxFrameRate: Infinity,
minBandwidth: 0,
maxBandwidth: Infinity,
minChannelsCount: 0,
maxChannelsCount: Infinity,
},
advanced: {
minTotalBytes: 128e3,
minBytes,
fastHalfLife: 2,
slowHalfLife: 5,
},
restrictToElementSize: false,
restrictToScreenSize: false,
ignoreDevicePixelRatio: false,
clearBufferSwitch: false,
safeMarginSwitch: 0,
cacheLoadThreshold: 20,
minTimeToSwitch: shaka.util.Platform.isApple() ? 0.5 : 0,
preferNetworkInformationBandwidth: false,
};
const cmcd = {
enabled: false,
sessionId: '',
contentId: '',
rtpSafetyFactor: 5,
useHeaders: false,
includeKeys: [],
};
const cmsd = {
enabled: true,
applyMaximumSuggestedBitrate: true,
estimatedThroughputWeightRatio: 0.5,
};
const lcevc = {
enabled: false,
dynamicPerformanceScaling: true,
logLevel: 0,
drawLogo: false,
};
const mediaSource = {
codecSwitchingStrategy: codecSwitchingStrategy,
addExtraFeaturesToSourceBuffer: (mimeType) => {
return shaka.util.ConfigUtils.referenceParametersAndReturn(
[mimeType],
'');
},
forceTransmux: false,
insertFakeEncryptionInInit: true,
modifyCueCallback: (cue, uri) => {
return shaka.util.ConfigUtils.referenceParametersAndReturn(
[cue, uri],
undefined);
},
dispatchAllEmsgBoxes: false,
};
let customPlayheadTracker = false;
let skipPlayDetection = false;
let supportsMultipleMediaElements = true;
if (shaka.util.Platform.isSmartTV()) {
customPlayheadTracker = true;
skipPlayDetection = true;
supportsMultipleMediaElements = false;
}
const ads = {
customPlayheadTracker,
skipPlayDetection,
supportsMultipleMediaElements,
disableHLSInterstitial: false,
disableDASHInterstitial: false,
};
const textDisplayer = {
captionsUpdatePeriod: 0.25,
};
const AutoShowText = shaka.config.AutoShowText;
/** @type {shaka.extern.PlayerConfiguration} */
const config = {
drm: drm,
manifest: manifest,
streaming: streaming,
mediaSource: mediaSource,
offline: offline,
abrFactory: () => new shaka.abr.SimpleAbrManager(),
abr: abr,
autoShowText: AutoShowText.IF_SUBTITLES_MAY_BE_NEEDED,
preferredAudioLanguage: '',
preferredAudioLabel: '',
preferredTextLanguage: '',
preferredVariantRole: '',
preferredTextRole: '',
preferredAudioChannelCount: 2,
preferredVideoHdrLevel: 'AUTO',
preferredVideoLayout: '',
preferredVideoLabel: '',
preferredVideoCodecs: [],
preferredAudioCodecs: [],
preferredTextFormats: [],
preferForcedSubs: false,
preferSpatialAudio: false,
preferredDecodingAttributes: [],
restrictions: {
minWidth: 0,
maxWidth: Infinity,
minHeight: 0,
maxHeight: Infinity,
minPixels: 0,
maxPixels: Infinity,
minFrameRate: 0,
maxFrameRate: Infinity,
minBandwidth: 0,
maxBandwidth: Infinity,
minChannelsCount: 0,
maxChannelsCount: Infinity,
},
playRangeStart: 0,
playRangeEnd: Infinity,
textDisplayer: textDisplayer,
textDisplayFactory: () => null,
cmcd: cmcd,
cmsd: cmsd,
lcevc: lcevc,
ads: ads,
ignoreHardwareResolution: false,
};
// Add this callback so that we can reference the preferred audio language
// through the config object so that if it gets updated, we have the
// updated value.
// eslint-disable-next-line require-await
offline.trackSelectionCallback = async (tracks) => {
return shaka.util.PlayerConfiguration.defaultTrackSelect(
tracks, config.preferredAudioLanguage,
config.preferredVideoHdrLevel);
};
return config;
}
/**
* Merges the given configuration changes into the given destination. This
* uses the default Player configurations as the template.
*
* @param {shaka.extern.PlayerConfiguration} destination
* @param {!Object} updates
* @param {shaka.extern.PlayerConfiguration=} template
* @return {boolean}
* @export
*/
static mergeConfigObjects(destination, updates, template) {
const overrides = {
'.drm.keySystemsMapping': '',
'.drm.servers': '',
'.drm.clearKeys': '',
'.drm.advanced': {
distinctiveIdentifierRequired: false,
persistentStateRequired: false,
videoRobustness: '',
audioRobustness: '',
sessionType: '',
serverCertificate: new Uint8Array(0),
serverCertificateUri: '',
individualizationServer: '',
headers: {},
},
};
return shaka.util.ConfigUtils.mergeConfigObjects(
destination, updates,
template || shaka.util.PlayerConfiguration.createDefault(), overrides,
'');
}
/**
* @param {!Array.<shaka.extern.Track>} tracks
* @param {string} preferredAudioLanguage
* @param {string} preferredVideoHdrLevel
* @return {!Array.<shaka.extern.Track>}
*/
static defaultTrackSelect(
tracks, preferredAudioLanguage, preferredVideoHdrLevel) {
const ContentType = shaka.util.ManifestParserUtils.ContentType;
const LanguageUtils = shaka.util.LanguageUtils;
let hdrLevel = preferredVideoHdrLevel;
if (hdrLevel == 'AUTO') {
// Auto detect the ideal HDR level.
if (window.matchMedia('(color-gamut: p3)').matches) {
hdrLevel = 'PQ';
} else {
hdrLevel = 'SDR';
}
}
/** @type {!Array.<shaka.extern.Track>} */
const allVariants = tracks.filter((track) => {
if (track.type != 'variant') {
return false;
}
if (track.hdr && track.hdr != hdrLevel) {
return false;
}
return true;
});
/** @type {!Array.<shaka.extern.Track>} */
let selectedVariants = [];
// Find the locale that best matches our preferred audio locale.
const closestLocale = LanguageUtils.findClosestLocale(
preferredAudioLanguage,
allVariants.map((variant) => variant.language));
// If we found a locale that was close to our preference, then only use
// variants that use that locale.
if (closestLocale) {
selectedVariants = allVariants.filter((variant) => {
const locale = LanguageUtils.normalize(variant.language);
return locale == closestLocale;
});
}
// If we failed to get a language match, go with primary.
if (selectedVariants.length == 0) {
selectedVariants = allVariants.filter((variant) => {
return variant.primary;
});
}
// Otherwise, there is no good way to choose the language, so we don't
// choose a language at all.
if (selectedVariants.length == 0) {
// Issue a warning, but only if the content has multiple languages.
// Otherwise, this warning would just be noise.
const languages = new Set(allVariants.map((track) => {
return track.language;
}));
if (languages.size > 1) {
shaka.log.warning('Could not choose a good audio track based on ' +
'language preferences or primary tracks. An ' +
'arbitrary language will be stored!');
}
// Default back to all variants.
selectedVariants = allVariants;
}
// From previously selected variants, choose the SD ones (height <= 480).
const tracksByHeight = selectedVariants.filter((track) => {
return track.height && track.height <= 480;
});
// If variants don't have video or no video with height <= 480 was
// found, proceed with the previously selected tracks.
if (tracksByHeight.length) {
// Sort by resolution, then select all variants which match the height
// of the highest SD res. There may be multiple audio bitrates for the
// same video resolution.
tracksByHeight.sort((a, b) => {
// The items in this list have already been screened for height, but the
// compiler doesn't know that.
goog.asserts.assert(a.height != null, 'Null height');
goog.asserts.assert(b.height != null, 'Null height');
return b.height - a.height;
});
selectedVariants = tracksByHeight.filter((track) => {
return track.height == tracksByHeight[0].height;
});
}
/** @type {!Array.<shaka.extern.Track>} */
const selectedTracks = [];
// If there are multiple matches at different audio bitrates, select the
// middle bandwidth one.
if (selectedVariants.length) {
const middleIndex = Math.floor(selectedVariants.length / 2);
selectedVariants.sort((a, b) => a.bandwidth - b.bandwidth);
selectedTracks.push(selectedVariants[middleIndex]);
}
// Since this default callback is used primarily by our own demo app and by
// app developers who haven't thought about which tracks they want, we
// should select all image/text tracks, regardless of language. This makes
// for a better demo for us, and does not rely on user preferences for the
// unconfigured app.
for (const track of tracks) {
if (track.type == ContentType.TEXT || track.type == ContentType.IMAGE) {
selectedTracks.push(track);
}
}
return selectedTracks;
}
/**
* @param {!Element} element
* @return {!Element}
*/
static defaultManifestPreprocessor(element) {
return shaka.util.ConfigUtils.referenceParametersAndReturn(
[element],
element);
}
/**
* @param {!shaka.extern.xml.Node} element
* @return {!shaka.extern.xml.Node}
*/
static defaultManifestPreprocessorTXml(element) {
return shaka.util.ConfigUtils.referenceParametersAndReturn(
[element],
element);
}
};