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; }