From fc7b62f78445056934452a25b23909854fad9e02 Mon Sep 17 00:00:00 2001 From: Paul Berberian Date: Thu, 5 Sep 2024 17:57:27 +0200 Subject: [PATCH] [Proposal]: Add experimental.enableResolutionChecks loadVideo option This is a Proposal to optionally move some "resolution checks" that were historically on the application-side on the RxPlayer side. Some devices aren't able to play video content with a higher resolution than e.g. what their screen size can display: some will fail on error, others will just infinitely freeze etc. Many of those devices have vendor-specific API that indicated whether a device was e.g. compatible to 4k content, 1080p content etc. Until now we told the application developers to do the Representation filtering themselves with the help of the `representationFilter` option from the `loadVideo` API. This is because of the vast numbers of devices out there, each with their own API, and because we didn't want to take the risk of having false negatives if it turned out some of those API were not always right. However, many of those devices are very popular (Lg and Samsung TVs, game consoles). Thus I'm wondering if it would be better that we provide some of those resolution checks ourselves, to lower the efforts an application have to make to rely on the RxPlayer on those common devices. For now I added an experimental `loadVideo` option: `experimental.enableResolutionChecks`, that has to be set to `true` to enable the behavior. The long term idea would be that for devices where it seems 100% accurate, we would enable the check by default. It will directly filter out resolutions that are too high, unless all resolutions are too high on the current video track in which case it will disable the check (as a security). In other cases, it follows the exact same rules than the `isCodecSupported` and `decipherable` properties of `Representation`. --- src/compat/browser_detection.ts | 9 + src/compat/max_resolution_detection.ts | 163 ++++++++++++++++++ src/core/cmcd/cmcd_data_builder.ts | 3 +- .../stream/adaptation/adaptation_stream.ts | 3 +- src/core/stream/period/period_stream.ts | 6 +- .../utils/get_adaptation_switch_strategy.ts | 3 +- .../api/debug/modules/general_info.ts | 7 +- src/main_thread/api/option_utils.ts | 30 ++++ src/main_thread/api/public_api.ts | 1 + .../utils/update_manifest_codec_support.ts | 18 +- .../tracks_store/track_dispatcher.ts | 9 +- src/main_thread/tracks_store/tracks_store.ts | 5 +- src/manifest/classes/__tests__/period.test.ts | 39 ++--- src/manifest/classes/adaptation.ts | 50 +++--- src/manifest/classes/manifest.ts | 29 ++-- src/manifest/classes/period.ts | 33 ++-- src/manifest/classes/representation.ts | 85 ++++++--- src/manifest/types.ts | 12 +- src/manifest/utils.ts | 25 ++- src/public_types.ts | 30 ++++ src/transports/smooth/pipelines.ts | 1 + src/transports/types.ts | 9 + 22 files changed, 458 insertions(+), 112 deletions(-) create mode 100644 src/compat/max_resolution_detection.ts diff --git a/src/compat/browser_detection.ts b/src/compat/browser_detection.ts index a54831c967..9f00cb3b4a 100644 --- a/src/compat/browser_detection.ts +++ b/src/compat/browser_detection.ts @@ -64,6 +64,9 @@ let isWebOs2021 = false; /** `true` specifically for WebOS 2022 version. */ let isWebOs2022 = false; +/** `true` specifically for Hisense TVs. */ +let isHisense = false; + /** `true` for Panasonic devices. */ let isPanasonic = false; @@ -158,6 +161,11 @@ let isXbox = false; ) { isWebOs2021 = true; } + } else if ( + navigator.userAgent.indexOf("Hisense") !== -1 && + navigator.userAgent.indexOf("VIDAA") !== -1 + ) { + isHisense = true; } else if ( navigator.userAgent.indexOf("NETTV") !== -1 && navigator.userAgent.indexOf("Philips") !== -1 @@ -175,6 +183,7 @@ export { isIE11, isIEOrEdge, isFirefox, + isHisense, isPanasonic, isPhilipsNetTv, isPlayStation4, diff --git a/src/compat/max_resolution_detection.ts b/src/compat/max_resolution_detection.ts new file mode 100644 index 0000000000..0c14efadea --- /dev/null +++ b/src/compat/max_resolution_detection.ts @@ -0,0 +1,163 @@ +import log from "../log"; +import globalScope from "../utils/global_scope"; +import isNullOrUndefined from "../utils/is_null_or_undefined"; +import { isHisense, isTizen, isWebOs } from "./browser_detection"; + +interface IWebOsDeviceCallback { + modelName: string; + modelNameAscii: string; + version: string; + versionMajor: number; + versionMinor: number; + versionDot: number; + sdkVersion: string; + screenWidth: number; + screenHeight: number; + uhd?: boolean; +} + +type IVendorGlobalScope = typeof globalScope & { + /** On Hisense TVs. */ + // eslint-disable-next-line @typescript-eslint/naming-convention + Hisense_Get4KSupportState?: (() => boolean) | null | undefined; + /** On Tizen devices. */ + webapis?: + | { + productinfo?: + | { + is8KPanelSupported?: (() => boolean) | null | undefined; + isUdPanelSupported?: (() => boolean) | null | undefined; + } + | null + | undefined; + } + | null + | undefined; + /** On LG TVs. */ + // eslint-disable-next-line @typescript-eslint/naming-convention + PalmSystem?: + | { + deviceInfo?: string | null | undefined; + } + | null + | undefined; + /** Also on LG TVs. */ + webOS?: + | { + deviceInfo: + | ((cb: (arg: IWebOsDeviceCallback) => void) => void) + | null + | undefined; + } + | null + | undefined; +}; +const global: IVendorGlobalScope = globalScope; + +export default function getMaxSupportedResolution(): { + width?: number | undefined; + height: number | undefined; +} { + try { + if (isHisense) { + if ( + navigator.userAgent.indexOf(";FHD") >= 0 || + navigator.userAgent.indexOf("/FHD") >= 0 + ) { + return { + height: 1080, + width: undefined, + }; + } + if ( + navigator.userAgent.indexOf(";HD") >= 0 || + navigator.userAgent.indexOf("/HD") >= 0 + ) { + return { + height: 720, + width: undefined, + }; + } + // Found in VIDAA Web developer documentation + if ( + "Hisense_Get4KSupportState" in global && + typeof global.Hisense_Get4KSupportState === "function" + ) { + if (global.Hisense_Get4KSupportState()) { + return { + height: undefined, + width: undefined, + }; + } + } + } + + if (isTizen) { + if ( + !isNullOrUndefined(global.webapis) && + !isNullOrUndefined(global.webapis.productinfo) + ) { + if (typeof global.webapis.productinfo.is8KPanelSupported === "function") { + return { + height: undefined, + width: undefined, + }; + } + if (typeof global.webapis.productinfo.isUdPanelSupported === "function") { + return { + height: 3840, + width: 2160, + }; + } + } + } + + if (isWebOs) { + let deviceInfo: IWebOsDeviceCallback | null | undefined; + if ( + !isNullOrUndefined(global.PalmSystem) && + typeof global.PalmSystem.deviceInfo === "string" + ) { + deviceInfo = JSON.parse(global.PalmSystem.deviceInfo) as IWebOsDeviceCallback; + } + if ( + !isNullOrUndefined(global.webOS) && + typeof global.webOS.deviceInfo === "function" + ) { + global.webOS.deviceInfo((info: IWebOsDeviceCallback) => { + deviceInfo = info; + }); + } + if (!isNullOrUndefined(deviceInfo)) { + if (deviceInfo.uhd === true) { + return { + width: undefined, + height: undefined, + }; + } + if ( + "screenWidth" in deviceInfo && + typeof deviceInfo.screenWidth === "number" && + deviceInfo.screenWidth <= 1920 && + "screenHeight" in deviceInfo && + typeof deviceInfo.screenHeight === "number" && + deviceInfo.screenHeight <= 1080 + ) { + return { + width: 1920, + height: 1080, + }; + } + } + } + } catch (err) { + log.error( + "Compat: Error when trying to call vendor API", + err instanceof Error ? err : new Error("Unknown Error"), + ); + } + return { + height: undefined, + width: undefined, + }; +} diff --git a/src/core/cmcd/cmcd_data_builder.ts b/src/core/cmcd/cmcd_data_builder.ts index bba3fd0387..4f9687a853 100644 --- a/src/core/cmcd/cmcd_data_builder.ts +++ b/src/core/cmcd/cmcd_data_builder.ts @@ -295,7 +295,8 @@ export default class CmcdDataBuilder { props.tb = content.adaptation.representations.reduce( (acc: number | undefined, representation: IRepresentation) => { if ( - representation.isSupported !== true || + representation.isCodecSupported !== true || + representation.isResolutionSupported === false || representation.decipherable === false ) { return acc; diff --git a/src/core/stream/adaptation/adaptation_stream.ts b/src/core/stream/adaptation/adaptation_stream.ts index 2bcee06812..7c054816b4 100644 --- a/src/core/stream/adaptation/adaptation_stream.ts +++ b/src/core/stream/adaptation/adaptation_stream.ts @@ -98,7 +98,8 @@ export default function AdaptationStream( (r) => arrayIncludes(initialRepIds, r.id) && r.decipherable !== false && - r.isSupported !== false, + r.isCodecSupported !== false && + r.isResolutionSupported !== false, ); /** Emit the list of Representation for the adaptive logic. */ diff --git a/src/core/stream/period/period_stream.ts b/src/core/stream/period/period_stream.ts index 531c1813ec..4c054c79aa 100644 --- a/src/core/stream/period/period_stream.ts +++ b/src/core/stream/period/period_stream.ts @@ -473,7 +473,11 @@ function createOrReuseSegmentSink( */ function getFirstDeclaredMimeType(adaptation: IAdaptation): string { const representations = adaptation.representations.filter((r) => { - return r.isSupported === true && r.decipherable !== false; + return ( + r.isCodecSupported === true && + r.decipherable !== false && + r.isResolutionSupported !== false + ); }); if (representations.length === 0) { const noRepErr = new MediaError( diff --git a/src/core/stream/period/utils/get_adaptation_switch_strategy.ts b/src/core/stream/period/utils/get_adaptation_switch_strategy.ts index 83b34df950..d141797ddb 100644 --- a/src/core/stream/period/utils/get_adaptation_switch_strategy.ts +++ b/src/core/stream/period/utils/get_adaptation_switch_strategy.ts @@ -189,8 +189,9 @@ export default function getAdaptationSwitchStrategy( function hasCompatibleCodec(adaptation: IAdaptation, segmentSinkCodec: string): boolean { return adaptation.representations.some( (rep) => - rep.isSupported === true && + rep.isCodecSupported === true && rep.decipherable !== false && + rep.isResolutionSupported !== false && areCodecsCompatible(rep.getMimeTypeString(), segmentSinkCodec), ); } diff --git a/src/main_thread/api/debug/modules/general_info.ts b/src/main_thread/api/debug/modules/general_info.ts index dcb6a498e6..d6660d7822 100644 --- a/src/main_thread/api/debug/modules/general_info.ts +++ b/src/main_thread/api/debug/modules/general_info.ts @@ -177,15 +177,16 @@ export default function constructDebugGeneralInfo( adaptations?.video?.representations.map((r) => { return ( String(r.bitrate ?? "N/A") + - (r.isSupported !== false ? "" : " U!") + - (r.decipherable !== false ? "" : " E!") + (r.isCodecSupported !== false ? "" : " U!") + + (r.decipherable !== false ? "" : " E!") + + (r.isResolutionSupported !== false ? "" : " R!") ); }) ?? []; const audioBitratesStr = adaptations?.audio?.representations.map((r) => { return ( String(r.bitrate ?? "N/A") + - (r.isSupported !== false ? "" : " U!") + + (r.isCodecSupported !== false ? "" : " U!") + (r.decipherable !== false ? "" : " E!") ); }) ?? []; diff --git a/src/main_thread/api/option_utils.ts b/src/main_thread/api/option_utils.ts index 8aa225bf39..83c6c1814e 100644 --- a/src/main_thread/api/option_utils.ts +++ b/src/main_thread/api/option_utils.ts @@ -65,29 +65,55 @@ export interface IParsedConstructorOptions { /** * Base type which the types for the parsed options of the RxPlayer's * `loadVideo` method exend. + * @see ILoadVideoOptions */ interface IParsedLoadVideoOptionsBase { + /** @see ILoadVideoOptions.url */ url: string | undefined; + /** @see ILoadVideoOptions.transport */ transport: string; + /** @see ILoadVideoOptions.autoPlay */ autoPlay: boolean; + /** @see ILoadVideoOptions.initialManifest */ initialManifest: ILoadedManifestFormat | undefined; + /** @see ILoadVideoOptions.keySystems */ keySystems: IKeySystemOption[]; + /** @see ILoadVideoOptions.lowLatencyMode */ lowLatencyMode: boolean; + /** @see ILoadVideoOptions.minimumManifestUpdateInterval */ minimumManifestUpdateInterval: number; + /** @see ILoadVideoOptions.requestConfig */ requestConfig: IRequestConfig; + /** @see ILoadVideoOptions.startAt */ startAt: IParsedStartAtOption | undefined; + /** @see ILoadVideoOptions.enableFastSwitching */ enableFastSwitching: boolean; + /** @see ILoadVideoOptions.defaultAudioTrackSwitchingMode */ defaultAudioTrackSwitchingMode: IAudioTrackSwitchingMode | undefined; + /** @see ILoadVideoOptions.onCodecSwitch */ onCodecSwitch: "continue" | "reload"; + /** @see ILoadVideoOptions.checkMediaSegmentIntegrity */ checkMediaSegmentIntegrity?: boolean | undefined; + /** @see ILoadVideoOptions.checkManifestIntegrity */ checkManifestIntegrity?: boolean | undefined; + /** @see ILoadVideoOptions.manifestLoader */ manifestLoader?: IManifestLoader | undefined; + /** @see ILoadVideoOptions.referenceDateTime */ referenceDateTime?: number | undefined; + /** @see ILoadVideoOptions.representationFilter */ representationFilter?: IRepresentationFilter | string | undefined; + /** @see ILoadVideoOptions.segmentLoader */ segmentLoader?: ISegmentLoader | undefined; + /** @see ILoadVideoOptions.serverSyncInfos */ serverSyncInfos?: IServerSyncInfos | undefined; + /** @see ILoadVideoOptions.mode */ mode: IRxPlayerMode; + /** @see ILoadVideoOptions.cmcd */ cmcd: ICmcdOptions | undefined; + /** @see ILoadVideoOptions.experimentalOptions */ + experimentalOptions: { + enableResolutionChecks: boolean; + }; __priv_manifestUpdateUrl?: string | undefined; __priv_patchLastSegmentInSidx?: boolean | undefined; } @@ -461,6 +487,10 @@ function parseLoadVideoOptions(options: ILoadVideoOptions): IParsedLoadVideoOpti mode, url, cmcd: options.cmcd, + experimentalOptions: { + enableResolutionChecks: + options.experimentalOptions?.enableResolutionChecks === true, + }, }; } diff --git a/src/main_thread/api/public_api.ts b/src/main_thread/api/public_api.ts index 4a3940e507..ed26c12258 100644 --- a/src/main_thread/api/public_api.ts +++ b/src/main_thread/api/public_api.ts @@ -952,6 +952,7 @@ class Player extends EventEmitter { manifestLoader: undefined, segmentLoader: undefined, representationFilter: options.representationFilter, + enableResolutionChecks: options.experimentalOptions.enableResolutionChecks, __priv_manifestUpdateUrl, __priv_patchLastSegmentInSidx, }; diff --git a/src/main_thread/init/utils/update_manifest_codec_support.ts b/src/main_thread/init/utils/update_manifest_codec_support.ts index 61049207f1..03c769e49a 100644 --- a/src/main_thread/init/utils/update_manifest_codec_support.ts +++ b/src/main_thread/init/utils/update_manifest_codec_support.ts @@ -11,7 +11,7 @@ import { ContentDecryptorState } from "../../decrypt"; /** * Returns a list of all codecs that the support is not known yet on the given * Manifest. - * If a representation with (`isSupported`) is undefined, we consider the + * If a representation with (`isCodecSupported`) is undefined, we consider the * codec support as unknown. * * This function iterates through all periods, adaptations, and representations, @@ -36,7 +36,7 @@ export function getCodecsWithUnknownSupport( continue; } for (const representation of adaptation.representations) { - if (representation.isSupported === undefined) { + if (representation.isCodecSupported === undefined) { codecsWithUnknownSupport.push({ mimeType: representation.mimeType ?? "", codec: representation.codecs?.[0] ?? "", @@ -131,8 +131,8 @@ export function updateManifestCodecSupport( let hasSupportedCodec: boolean = false; let hasCodecWithUndefinedSupport: boolean = false; adaptation.representations.forEach((representation) => { - if (representation.isSupported !== undefined) { - if (representation.isSupported) { + if (representation.isCodecSupported !== undefined) { + if (representation.isCodecSupported) { hasSupportedCodec = true; } // We already knew the support for that one, continue to next one @@ -148,16 +148,16 @@ export function updateManifestCodecSupport( for (const codec of codecs) { const codecSupportInfo = efficientlyGetCodecSupport(mimeType, codec); if (!isEncrypted) { - representation.isSupported = codecSupportInfo.isSupportedClear; + representation.isCodecSupported = codecSupportInfo.isSupportedClear; } else if ( - representation.isSupported !== codecSupportInfo.isSupportedEncrypted + representation.isCodecSupported !== codecSupportInfo.isSupportedEncrypted ) { - representation.isSupported = codecSupportInfo.isSupportedEncrypted; + representation.isCodecSupported = codecSupportInfo.isSupportedEncrypted; } - if (representation.isSupported === undefined) { + if (representation.isCodecSupported === undefined) { hasCodecWithUndefinedSupport = true; - } else if (representation.isSupported) { + } else if (representation.isCodecSupported) { hasSupportedCodec = true; representation.codecs = [codec]; diff --git a/src/main_thread/tracks_store/track_dispatcher.ts b/src/main_thread/tracks_store/track_dispatcher.ts index f6c436f846..58512c7428 100644 --- a/src/main_thread/tracks_store/track_dispatcher.ts +++ b/src/main_thread/tracks_store/track_dispatcher.ts @@ -188,7 +188,9 @@ export default class TrackDispatcher extends EventEmitter playableRepresentations = trackInfo.adaptation.representations.filter( (representation) => { return ( - representation.isSupported === true && representation.decipherable !== false + representation.isCodecSupported === true && + representation.decipherable !== false && + representation.isResolutionSupported === false ); }, ); @@ -202,7 +204,10 @@ export default class TrackDispatcher extends EventEmitter arrayIncludes(representationIds, r.id), ); playableRepresentations = representations.filter( - (r) => r.isSupported === true && r.decipherable !== false, + (r) => + r.isCodecSupported === true && + r.decipherable !== false && + r.isResolutionSupported !== false, ); if (playableRepresentations.length === 0) { self.trigger("noPlayableLockedRepresentation", null); diff --git a/src/main_thread/tracks_store/tracks_store.ts b/src/main_thread/tracks_store/tracks_store.ts index 4e0da399c2..aba0addf70 100644 --- a/src/main_thread/tracks_store/tracks_store.ts +++ b/src/main_thread/tracks_store/tracks_store.ts @@ -399,7 +399,10 @@ export default class TracksStore extends EventEmitter { return false; } const playableRepresentations = adaptation.representations.filter( - (r) => r.isSupported === true && r.decipherable !== false, + (r) => + r.isCodecSupported === true && + r.decipherable !== false && + r.isResolutionSupported !== false, ); return playableRepresentations.length > 0; }, diff --git a/src/manifest/classes/__tests__/period.test.ts b/src/manifest/classes/__tests__/period.test.ts index 371cd9c19f..c11cb6bbc6 100644 --- a/src/manifest/classes/__tests__/period.test.ts +++ b/src/manifest/classes/__tests__/period.test.ts @@ -30,7 +30,7 @@ describe("Manifest - Period", () => { const unsupportedAdaptations: Adaptation[] = []; try { const codecSupportCache = new CodecSupportCache([]); - period = new Period(args, unsupportedAdaptations, codecSupportCache); + period = new Period(args, { unsupportedAdaptations, codecSupportCache }); } catch (e) { errorReceived = e; } @@ -89,7 +89,7 @@ describe("Manifest - Period", () => { const unsupportedAdaptations: Adaptation[] = []; const codecSupportCache = new CodecSupportCache([]); try { - period = new Period(args, unsupportedAdaptations, codecSupportCache); + period = new Period(args, { unsupportedAdaptations, codecSupportCache }); } catch (e) { errorReceived = e; } @@ -137,7 +137,7 @@ describe("Manifest - Period", () => { const unsupportedAdaptations: Adaptation[] = []; try { const codecSupportCache = new CodecSupportCache([]); - period = new Period(args, unsupportedAdaptations, codecSupportCache); + period = new Period(args, { unsupportedAdaptations, codecSupportCache }); } catch (e) { errorReceived = e; } @@ -230,7 +230,7 @@ describe("Manifest - Period", () => { const unsupportedAdaptations: Adaptation[] = []; try { const codecSupportCache = new CodecSupportCache([]); - period = new Period(args, unsupportedAdaptations, codecSupportCache); + period = new Period(args, { unsupportedAdaptations, codecSupportCache }); } catch (e) { errorReceived = e; } @@ -330,7 +330,7 @@ describe("Manifest - Period", () => { const unsupportedAdaptations: Adaptation[] = []; try { const codecSupportCache = new CodecSupportCache([]); - period = new Period(args, unsupportedAdaptations, codecSupportCache); + period = new Period(args, { unsupportedAdaptations, codecSupportCache }); } catch (e) { errorReceived = e; } @@ -428,7 +428,7 @@ describe("Manifest - Period", () => { const unsupportedAdaptations: Adaptation[] = []; try { const codecSupportCache = new CodecSupportCache([]); - period = new Period(args, unsupportedAdaptations, codecSupportCache); + period = new Period(args, { unsupportedAdaptations, codecSupportCache }); } catch (e) { errorReceived = e; } @@ -521,7 +521,7 @@ describe("Manifest - Period", () => { const unsupportedAdaptations: Adaptation[] = []; try { const codecSupportCache = new CodecSupportCache([]); - period = new Period(args, unsupportedAdaptations, codecSupportCache); + period = new Period(args, { unsupportedAdaptations, codecSupportCache }); } catch (e) { errorReceived = e; } @@ -583,7 +583,7 @@ describe("Manifest - Period", () => { const args = { id: "12", adaptations: { video, video2 }, start: 0 }; const unsupportedAdaptations: Adaptation[] = []; const codecSupportCache = new CodecSupportCache([]); - const period = new Period(args, unsupportedAdaptations, codecSupportCache); + const period = new Period(args, { unsupportedAdaptations, codecSupportCache }); expect(unsupportedAdaptations).toHaveLength(1); expect(mockAdaptation).toHaveBeenCalledTimes(2); @@ -629,7 +629,7 @@ describe("Manifest - Period", () => { const args = { id: "12", adaptations: { bar, video }, start: 0 }; const unsupportedAdaptations: Adaptation[] = []; const codecSupportCache = new CodecSupportCache([]); - const period = new Period(args, unsupportedAdaptations, codecSupportCache); + const period = new Period(args, { unsupportedAdaptations, codecSupportCache }); expect(period.adaptations).toEqual({ video: video.map((v) => ({ ...v, @@ -685,12 +685,11 @@ describe("Manifest - Period", () => { const args = { id: "12", adaptations: { video }, start: 0 }; const unsupportedAdaptations: Adaptation[] = []; const codecSupportCache = new CodecSupportCache([]); - const period = new Period( - args, + const period = new Period(args, { unsupportedAdaptations, codecSupportCache, representationFilter, - ); + }); expect(unsupportedAdaptations).toHaveLength(0); expect(period.adaptations.video).toHaveLength(2); @@ -749,7 +748,7 @@ describe("Manifest - Period", () => { const args = { id: "12", adaptations: { video, foo }, start: 0 }; const unsupportedAdaptations: Adaptation[] = []; const codecSupportCache = new CodecSupportCache([]); - new Period(args, unsupportedAdaptations, codecSupportCache); + new Period(args, { unsupportedAdaptations, codecSupportCache }); expect(unsupportedAdaptations).toHaveLength(2); const [adap1, adap2] = unsupportedAdaptations; @@ -801,7 +800,7 @@ describe("Manifest - Period", () => { const args = { id: "12", adaptations: { video, foo }, start: 0 }; const unsupportedAdaptations: Adaptation[] = []; const codecSupportCache = new CodecSupportCache([]); - new Period(args, unsupportedAdaptations, codecSupportCache); + new Period(args, { unsupportedAdaptations, codecSupportCache }); expect(unsupportedAdaptations).toHaveLength(0); }); @@ -843,7 +842,7 @@ describe("Manifest - Period", () => { const args = { id: "12", adaptations: { video }, start: 72 }; const unsupportedAdaptations: Adaptation[] = []; const codecSupportCache = new CodecSupportCache([]); - const period = new Period(args, unsupportedAdaptations, codecSupportCache); + const period = new Period(args, { unsupportedAdaptations, codecSupportCache }); expect(unsupportedAdaptations).toHaveLength(0); expect(period.start).toEqual(72); expect(period.duration).toEqual(undefined); @@ -888,7 +887,7 @@ describe("Manifest - Period", () => { const args = { id: "12", adaptations: { video }, start: 0, duration: 12 }; const unsupportedAdaptations: Adaptation[] = []; const codecSupportCache = new CodecSupportCache([]); - const period = new Period(args, unsupportedAdaptations, codecSupportCache); + const period = new Period(args, { unsupportedAdaptations, codecSupportCache }); expect(unsupportedAdaptations).toHaveLength(0); expect(period.start).toEqual(0); expect(period.duration).toEqual(12); @@ -933,7 +932,7 @@ describe("Manifest - Period", () => { const args = { id: "12", adaptations: { video }, start: 50, duration: 12 }; const unsupportedAdaptations: Adaptation[] = []; const codecSupportCache = new CodecSupportCache([]); - const period = new Period(args, unsupportedAdaptations, codecSupportCache); + const period = new Period(args, { unsupportedAdaptations, codecSupportCache }); expect(unsupportedAdaptations).toHaveLength(0); expect(period.start).toEqual(50); expect(period.duration).toEqual(12); @@ -994,7 +993,7 @@ describe("Manifest - Period", () => { }; const unsupportedAdaptations: Adaptation[] = []; const codecSupportCache = new CodecSupportCache([]); - const period = new Period(args, unsupportedAdaptations, codecSupportCache); + const period = new Period(args, { unsupportedAdaptations, codecSupportCache }); expect(unsupportedAdaptations).toHaveLength(0); expect(period.getAdaptations()).toHaveLength(3); expect(period.getAdaptations()).toContain(period.adaptations.video?.[0]); @@ -1056,7 +1055,7 @@ describe("Manifest - Period", () => { }; const unsupportedAdaptations: Adaptation[] = []; const codecSupportCache = new CodecSupportCache([]); - const period = new Period(args, unsupportedAdaptations, codecSupportCache); + const period = new Period(args, { unsupportedAdaptations, codecSupportCache }); expect(unsupportedAdaptations).toHaveLength(0); expect(period.getAdaptationsForType("video")).toHaveLength(2); @@ -1135,7 +1134,7 @@ describe("Manifest - Period", () => { }; const unsupportedAdaptations: Adaptation[] = []; const codecSupportCache = new CodecSupportCache([]); - const period = new Period(args, [], codecSupportCache); + const period = new Period(args, { unsupportedAdaptations: [], codecSupportCache }); expect(unsupportedAdaptations).toHaveLength(0); expect(period.getAdaptation("54")).toEqual(period.adaptations.video?.[0]); expect(period.getAdaptation("55")).toEqual(period.adaptations.video?.[1]); diff --git a/src/manifest/classes/adaptation.ts b/src/manifest/classes/adaptation.ts index 4c9c7c136a..a7987f0ae9 100644 --- a/src/manifest/classes/adaptation.ts +++ b/src/manifest/classes/adaptation.ts @@ -108,14 +108,14 @@ export default class Adaptation implements IAdaptationMetadata { */ constructor( parsedAdaptation: IParsedAdaptation, - cachedCodecSupport: CodecSupportCache, options: { + codecSupportCache?: CodecSupportCache | undefined; + enableResolutionChecks?: boolean | undefined; representationFilter?: IRepresentationFilter | undefined; - isManuallyAdded?: boolean | undefined; } = {}, ) { const { trickModeTracks } = parsedAdaptation; - const { representationFilter, isManuallyAdded } = options; + const { representationFilter, enableResolutionChecks, codecSupportCache } = options; this.id = parsedAdaptation.id; this.type = parsedAdaptation.type; @@ -149,7 +149,7 @@ export default class Adaptation implements IAdaptationMetadata { if (trickModeTracks !== undefined && trickModeTracks.length > 0) { this.trickModeTracks = trickModeTracks.map( - (track) => new Adaptation(track, cachedCodecSupport), + (track) => new Adaptation(track, { codecSupportCache, enableResolutionChecks }), ); } @@ -160,12 +160,13 @@ export default class Adaptation implements IAdaptationMetadata { hasCodecWithUndefinedSupport: false, isDecipherable: false, }; + let hasSupportedResolution = false; for (let i = 0; i < argsRepresentations.length; i++) { - const representation = new Representation( - argsRepresentations[i], - this.type, - cachedCodecSupport, - ); + const representation = new Representation(argsRepresentations[i], { + trackType: this.type, + codecSupportCache, + enableResolutionChecks, + }); let shouldAdd = true; if (!isNullOrUndefined(representationFilter)) { const reprObject: IRepresentationFilterRepresentation = { @@ -195,13 +196,16 @@ export default class Adaptation implements IAdaptationMetadata { }); } if (shouldAdd) { + if (representation.isResolutionSupported !== false) { + hasSupportedResolution = true; + } representations.push(representation); - if (representation.isSupported === undefined) { + if (representation.isCodecSupported === undefined) { this.supportStatus.hasCodecWithUndefinedSupport = true; if (this.supportStatus.hasSupportedCodec === false) { this.supportStatus.hasSupportedCodec = undefined; } - } else if (representation.isSupported) { + } else if (representation.isCodecSupported) { this.supportStatus.hasSupportedCodec = true; } if (representation.decipherable === undefined) { @@ -221,11 +225,15 @@ export default class Adaptation implements IAdaptationMetadata { ); } } + if (representations.length > 0 && !hasSupportedResolution) { + for (const representation of representations) { + // As resolution checks might not be an exact science, for now if all + // seems unsupported, don't consider it. + representation.isResolutionSupported = undefined; + } + } representations.sort((a, b) => a.bitrate - b.bitrate); this.representations = representations; - - // for manuallyAdded adaptations (not in the manifest) - this.manuallyAdded = isManuallyAdded === true; } /** @@ -237,19 +245,19 @@ export default class Adaptation implements IAdaptationMetadata { * * * If the right mimetype+codec combination is found in the provided object, - * this `Adaptation`'s `isSupported` property will be updated accordingly as - * well as all of its inner `Representation`'s `isSupported` attributes. + * this `Adaptation`'s `supportStatus` property will be updated accordingly as + * well as all of its inner `Representation`'s `isCodecSupported` attributes. * - * @param {Array.} cachedCodecSupport + * @param {Array.} codecSupportCache */ - refreshCodecSupport(cachedCodecSupport: CodecSupportCache): void { + refreshCodecSupport(codecSupportCache: CodecSupportCache): void { let hasCodecWithUndefinedSupport = false; let hasSupportedRepresentation = false; for (const representation of this.representations) { - representation.refreshCodecSupport(cachedCodecSupport); - if (representation.isSupported === undefined) { + representation.refreshCodecSupport(codecSupportCache); + if (representation.isCodecSupported === undefined) { hasCodecWithUndefinedSupport = true; - } else if (representation.isSupported) { + } else if (representation.isCodecSupported) { hasSupportedRepresentation = true; } } diff --git a/src/manifest/classes/manifest.ts b/src/manifest/classes/manifest.ts index 122744ef0e..8b779e09b7 100644 --- a/src/manifest/classes/manifest.ts +++ b/src/manifest/classes/manifest.ts @@ -60,6 +60,15 @@ interface IManifestParsingOptions { * manifest will be updated fully when it needs to be refreshed, and it will * fetched through the original URL. */ manifestUpdateUrl?: string | undefined; + /** + * If `true`, we will try to detect what's the highest supported video + * resolution on the current device and if found, we will avoid playing + * video Representation (i.e. qualities) with an higher resolution. + * + * An exception is made when the currently-chosen track only has seemlingly + * unsupported Representations, in which case we'll still atempt to play them. + */ + enableResolutionChecks?: boolean | undefined; } /** Representation affected by a `decipherabilityUpdate` event. */ @@ -307,7 +316,7 @@ export default class Manifest * Caches the information if a codec is supported or not in the context of the * current content. */ - private _cachedCodecSupport: CodecSupportCache; + private _codecSupportCache: CodecSupportCache; /** * Construct a Manifest instance from a parsed Manifest object (as returned by @@ -326,23 +335,23 @@ export default class Manifest warnings: IPlayerError[], ) { super(); - const { representationFilter, manifestUpdateUrl } = options; + const { representationFilter, manifestUpdateUrl, enableResolutionChecks } = options; this.manifestFormat = ManifestMetadataFormat.Class; this.id = generateNewManifestId(); this.expired = parsedManifest.expired ?? null; this.transport = parsedManifest.transportType; this.clockOffset = parsedManifest.clockOffset; - this._cachedCodecSupport = new CodecSupportCache([]); + this._codecSupportCache = new CodecSupportCache([]); const unsupportedAdaptations: Adaptation[] = []; this.periods = parsedManifest.periods .map((parsedPeriod) => { - const period = new Period( - parsedPeriod, + const period = new Period(parsedPeriod, { + enableResolutionChecks, unsupportedAdaptations, - this._cachedCodecSupport, + codecSupportCache: this._codecSupportCache, representationFilter, - ); + }); return period; }) .sort((a, b) => a.start - b.start); @@ -396,10 +405,10 @@ export default class Manifest return null; } - this._cachedCodecSupport.addCodecs(updatedCodecSupportInfo); + this._codecSupportCache.addCodecs(updatedCodecSupportInfo); const unsupportedAdaptations: Adaptation[] = []; for (const period of this.periods) { - period.refreshCodecSupport(unsupportedAdaptations, this._cachedCodecSupport); + period.refreshCodecSupport(unsupportedAdaptations, this._codecSupportCache); } this.trigger("supportUpdate", null); if (unsupportedAdaptations.length > 0) { @@ -517,7 +526,7 @@ export default class Manifest } public updateCodecSupportList(cachedCodecSupport: CodecSupportCache) { - this._cachedCodecSupport = cachedCodecSupport; + this._codecSupportCache = cachedCodecSupport; } /** diff --git a/src/manifest/classes/period.ts b/src/manifest/classes/period.ts index e31996c27c..b4a2125048 100644 --- a/src/manifest/classes/period.ts +++ b/src/manifest/classes/period.ts @@ -59,19 +59,30 @@ export default class Period implements IPeriodMetadata { /** * @constructor * @param {Object} args - * @param {Array.} unsupportedAdaptations - Array on which + * @param {Object} options + * @param {Array.} options.unsupportedAdaptations - Array on which * `Adaptation`s objects which have no supported `Representation` will be * pushed. * This array might be useful for minor error reporting. - * @param {function|undefined} [representationFilter] + * @param {function|undefined} [options.representationFilter] + * @param {boolean|undefined} [options.enableResolutionChecks] + * @param {Object|undefined} [options.codecSupportCache] */ constructor( args: IParsedPeriod, - unsupportedAdaptations: Adaptation[], - cachedCodecSupport: CodecSupportCache, - - representationFilter?: IRepresentationFilter | undefined, + options: { + codecSupportCache: CodecSupportCache; + enableResolutionChecks?: boolean | undefined; + representationFilter?: IRepresentationFilter | undefined; + unsupportedAdaptations: Adaptation[]; + }, ) { + const { + enableResolutionChecks, + codecSupportCache, + unsupportedAdaptations, + representationFilter, + } = options; this.id = args.id; this.adaptations = ( Object.keys(args.adaptations) as ITrackType[] @@ -82,7 +93,9 @@ export default class Period implements IPeriodMetadata { } const filteredAdaptations = adaptationsForType .map((adaptation): Adaptation => { - const newAdaptation = new Adaptation(adaptation, cachedCodecSupport, { + const newAdaptation = new Adaptation(adaptation, { + codecSupportCache, + enableResolutionChecks, representationFilter, }); if ( @@ -146,11 +159,11 @@ export default class Period implements IPeriodMetadata { * `Adaptation`s objects which are now known to have no supported * `Representation` will be pushed. * This array might be useful for minor error reporting. - * @param {Array.} cachedCodecSupport + * @param {Array.} codecSupportCache */ refreshCodecSupport( unsupportedAdaptations: Adaptation[], - cachedCodecSupport: CodecSupportCache, + codecSupportCache: CodecSupportCache, ) { (Object.keys(this.adaptations) as ITrackType[]).forEach((ttype) => { const adaptationsForType = this.adaptations[ttype]; @@ -170,7 +183,7 @@ export default class Period implements IPeriodMetadata { continue; } const wasSupported = adaptation.supportStatus.hasSupportedCodec; - adaptation.refreshCodecSupport(cachedCodecSupport); + adaptation.refreshCodecSupport(codecSupportCache); if ( wasSupported !== false && adaptation.supportStatus.hasSupportedCodec === false diff --git a/src/manifest/classes/representation.ts b/src/manifest/classes/representation.ts index 2ea0bfebc2..c1cd314502 100644 --- a/src/manifest/classes/representation.ts +++ b/src/manifest/classes/representation.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import getMaxSupportedResolution from "../../compat/max_resolution_detection"; import log from "../../log"; import type { IRepresentationMetadata } from "../../manifest"; import type { @@ -24,10 +25,11 @@ import type { import type { ITrackType, IHDRInformation } from "../../public_types"; import areArraysOfNumbersEqual from "../../utils/are_arrays_of_numbers_equal"; import idGenerator from "../../utils/id_generator"; -import type codecSupportCache from "./codec_support_cache"; +import type CodecSupportCache from "./codec_support_cache"; import type { IRepresentationIndex } from "./representation_index"; const generateRepresentationUniqueId = idGenerator(); +const maxResolutionSupported = getMaxSupportedResolution(); /** * Normalized Representation structure. @@ -101,13 +103,21 @@ class Representation implements IRepresentationMetadata { */ public decipherable?: boolean | undefined; /** - * @see IRepresentationMetadata.isSupported + * @see IRepresentationMetadata.isCodecSupported * * Note that this property should __NEVER__ be updated directly on an * instanciated `Representation`, you are supposed to rely on * `Manifest` methods for this. */ - public isSupported: boolean | undefined; + public isCodecSupported: boolean | undefined; + + /** + * @see IRepresentationMetadata.isResolutionSupported + * + * Note that this property should __NEVER__ be updated directly on an + * instanciated `Representation`. + */ + public isResolutionSupported: boolean | undefined; /** * @see ITrackType */ @@ -119,8 +129,15 @@ class Representation implements IRepresentationMetadata { */ constructor( args: IParsedRepresentation, - trackType: ITrackType, - cachedCodecSupport: codecSupportCache, + { + trackType, + codecSupportCache, + enableResolutionChecks, + }: { + trackType: ITrackType; + codecSupportCache?: CodecSupportCache | undefined; + enableResolutionChecks?: boolean | undefined; + }, ) { this.id = args.id; this.uniqueId = generateRepresentationUniqueId(); @@ -161,11 +178,14 @@ class Representation implements IRepresentationMetadata { const isEncrypted = this.contentProtections !== undefined; - if (trackType === "audio" || trackType === "video") { + if ( + (trackType === "audio" || trackType === "video") && + codecSupportCache !== undefined + ) { // Supplemental codecs are defined as backwards-compatible codecs enhancing // the experience of a base layer codec if (args.supplementalCodecs !== undefined) { - const isSupplementaryCodecSupported = cachedCodecSupport.isSupported( + const isSupplementaryCodecSupported = codecSupportCache.isSupported( this.mimeType ?? "", args.supplementalCodecs ?? "", isEncrypted, @@ -173,10 +193,10 @@ class Representation implements IRepresentationMetadata { if (isSupplementaryCodecSupported !== false) { this.codecs = [args.supplementalCodecs]; - this.isSupported = isSupplementaryCodecSupported; + this.isCodecSupported = isSupplementaryCodecSupported; } } - if (this.isSupported !== true) { + if (this.isCodecSupported !== true) { if (this.codecs.length > 0) { // We couldn't check for support of another supplemental codec. // Just push that codec without testing support yet, we'll check @@ -184,7 +204,7 @@ class Representation implements IRepresentationMetadata { this.codecs.push(args.codecs ?? ""); } else { this.codecs = args.codecs === undefined ? [] : [args.codecs]; - this.isSupported = cachedCodecSupport.isSupported( + this.isCodecSupported = codecSupportCache.isSupported( this.mimeType ?? "", args.codecs ?? "", isEncrypted, @@ -195,7 +215,23 @@ class Representation implements IRepresentationMetadata { if (args.codecs !== undefined) { this.codecs.push(args.codecs); } - this.isSupported = true; + this.isCodecSupported = true; + } + + if (trackType === "video" && enableResolutionChecks !== true) { + if ( + args.height !== undefined && + maxResolutionSupported.height !== undefined && + args.height > maxResolutionSupported.height + ) { + this.isResolutionSupported = false; + } else if ( + args.width !== undefined && + maxResolutionSupported.width !== undefined && + args.width > maxResolutionSupported.width + ) { + this.isResolutionSupported = false; + } } } @@ -207,17 +243,17 @@ class Representation implements IRepresentationMetadata { * by the current environnement allows to work-around this issue. * * If the right mimetype+codec combination is found in the provided object, - * this `Representation`'s `isSupported` property will be updated accordingly. + * this `Representation`'s `isCodecSupported` property will be updated accordingly. * - * @param {Array.} cachedCodecSupport; + * @param {Array.} codecSupportCache; */ - public refreshCodecSupport(cachedCodecSupport: codecSupportCache) { - if (this.isSupported !== undefined) { + public refreshCodecSupport(codecSupportCache: CodecSupportCache) { + if (this.isCodecSupported !== undefined) { return; } const isEncrypted = this.contentProtections !== undefined; - let isSupported: boolean | undefined = false; + let isCodecSupported: boolean | undefined = false; const mimeType = this.mimeType ?? ""; let codecs = this.codecs ?? []; if (codecs.length === 0) { @@ -225,28 +261,28 @@ class Representation implements IRepresentationMetadata { } let representationHasUnknownCodecs = false; for (const codec of codecs) { - isSupported = cachedCodecSupport.isSupported(mimeType, codec, isEncrypted); - if (isSupported === true) { + isCodecSupported = codecSupportCache.isSupported(mimeType, codec, isEncrypted); + if (isCodecSupported === true) { this.codecs = [codec]; break; } - if (isSupported === undefined) { + if (isCodecSupported === undefined) { representationHasUnknownCodecs = true; } } /** If any codec is supported, the representation is supported */ - if (isSupported === true) { - this.isSupported = true; + if (isCodecSupported === true) { + this.isCodecSupported = true; } else { /** If some codecs support are not known it's too early to assume * representation is unsupported */ if (representationHasUnknownCodecs) { - this.isSupported = undefined; + this.isCodecSupported = undefined; } else { /** If all codecs support are known and none are supported, * the representation is not supported. */ - this.isSupported = false; + this.isCodecSupported = false; } } } @@ -453,7 +489,8 @@ class Representation implements IRepresentationMetadata { width: this.width, height: this.height, frameRate: this.frameRate, - isSupported: this.isSupported, + isCodecSupported: this.isCodecSupported, + isResolutionSupported: this.isResolutionSupported, hdrInfo: this.hdrInfo, contentProtections: this.contentProtections, decipherable: this.decipherable, diff --git a/src/manifest/types.ts b/src/manifest/types.ts index 0edc8e4140..bd24fe9763 100644 --- a/src/manifest/types.ts +++ b/src/manifest/types.ts @@ -362,7 +362,17 @@ export interface IRepresentationMetadata { * `undefined` for when this is not yet known (we're still in the process of * probing for support). */ - isSupported?: boolean | undefined; + isCodecSupported?: boolean | undefined; + /** + * `true` if the resolution of this Representation is known to e supported on + * the current device. + * + * `false` if it is known to be unsupported and should thus not be played. + * + * `undefined` if we don't know if it is supported or not on the current + * device. + */ + isResolutionSupported?: boolean | undefined; /** * An array of strings describing codecs that Representation relies on. * diff --git a/src/manifest/utils.ts b/src/manifest/utils.ts index 47dd9f715d..2a7234c4e7 100644 --- a/src/manifest/utils.ts +++ b/src/manifest/utils.ts @@ -238,7 +238,10 @@ export function toAudioTrack( id: adaptation.id, representations: (filterPlayable ? adaptation.representations.filter( - (r) => r.isSupported === true && r.decipherable !== false, + (r) => + r.isCodecSupported === true && + r.decipherable !== false && + r.isResolutionSupported !== false, ) : adaptation.representations ).map(toAudioRepresentation), @@ -283,7 +286,10 @@ export function toVideoTrack( const representations = ( filterPlayable ? trickModeAdaptation.representations.filter( - (r) => r.isSupported === true && r.decipherable !== false, + (r) => + r.isCodecSupported === true && + r.decipherable !== false && + r.isResolutionSupported !== false, ) : trickModeAdaptation.representations ).map(toVideoRepresentation); @@ -303,7 +309,10 @@ export function toVideoTrack( id: adaptation.id, representations: (filterPlayable ? adaptation.representations.filter( - (r) => r.isSupported === true && r.decipherable !== false, + (r) => + r.isCodecSupported === true && + r.decipherable !== false && + r.isResolutionSupported !== false, ) : adaptation.representations ).map(toVideoRepresentation), @@ -328,14 +337,14 @@ export function toVideoTrack( function toAudioRepresentation( representation: IRepresentationMetadata, ): IAudioRepresentation { - const { id, bitrate, codecs, isSpatialAudio, isSupported, decipherable } = + const { id, bitrate, codecs, isSpatialAudio, isCodecSupported, decipherable } = representation; return { id, bitrate, codec: codecs?.[0], isSpatialAudio, - isCodecSupported: isSupported, + isCodecSupported, decipherable, }; } @@ -355,7 +364,8 @@ function toVideoRepresentation( height, codecs, hdrInfo, - isSupported, + isCodecSupported, + isResolutionSupported, decipherable, contentProtections, } = representation; @@ -367,7 +377,8 @@ function toVideoRepresentation( height, codec: codecs?.[0], hdrInfo, - isCodecSupported: isSupported, + isCodecSupported, + isResolutionSupported, decipherable, contentProtections: contentProtections !== undefined diff --git a/src/public_types.ts b/src/public_types.ts index 8a7e72a9db..1fb4baa156 100644 --- a/src/public_types.ts +++ b/src/public_types.ts @@ -182,6 +182,26 @@ export interface ILoadVideoOptions { * When set to an object, enable "Common Media Client Data", or "CMCD". */ cmcd?: ICmcdOptions | undefined; + + /** + * Options which may be removed or updated at any RxPlayer release. + * + * Most of those are options which we temporarily test before making + * them part of the RxPlayer API. + */ + experimentalOptions?: + | { + /** + * If `true`, we will try to detect what's the highest supported video + * resolution on the current device and if found, we will avoid playing + * video Representation (i.e. qualities) with an higher resolution. + * + * An exception is made when the currently-chosen track only has seemlingly + * unsupported Representations, in which case we'll still atempt to play them. + */ + enableResolutionChecks?: boolean | undefined; + } + | undefined; } /** Value of the `serverSyncInfos` transport option. */ @@ -909,6 +929,16 @@ export interface IVideoRepresentation { * If `undefined`, we don't know yet if it is supported or not. */ isCodecSupported?: boolean | undefined; + /** + * `true` if the resolution of this Representation is known to e supported on + * the current device. + * + * `false` if it is known to be unsupported and should thus not be played. + * + * `undefined` if we don't know if it is supported or not on the current + * device. + */ + isResolutionSupported?: boolean | undefined; /** * If `true`, this Representation is known to be decipherable. * If `false`, it is known to be encrypted and not decipherable. diff --git a/src/transports/smooth/pipelines.ts b/src/transports/smooth/pipelines.ts index 639a5c9cbe..c2e90d6884 100644 --- a/src/transports/smooth/pipelines.ts +++ b/src/transports/smooth/pipelines.ts @@ -81,6 +81,7 @@ export default function (transportOptions: ITransportOptions): ITransportPipelin parserResult, { representationFilter: transportOptions.representationFilter, + enableResolutionChecks: transportOptions.enableResolutionChecks, }, warnings, ); diff --git a/src/transports/types.ts b/src/transports/types.ts index 2b7a137ca3..a8085df64f 100644 --- a/src/transports/types.ts +++ b/src/transports/types.ts @@ -816,6 +816,15 @@ export interface ITransportOptions { representationFilter?: IRepresentationFilter | undefined; segmentLoader?: ICustomSegmentLoader | undefined; serverSyncInfos?: IServerSyncInfos | undefined; + /** + * If `true`, we will try to detect what's the highest supported video + * resolution on the current device and if found, we will avoid playing + * video Representation (i.e. qualities) with an higher resolution. + * + * An exception is made when the currently-chosen track only has seemlingly + * unsupported Representations, in which case we'll still atempt to play them. + */ + enableResolutionChecks?: boolean | undefined; __priv_manifestUpdateUrl?: string | undefined; __priv_patchLastSegmentInSidx?: boolean | undefined; }