diff --git a/api-extractor/report/hls.js.api.md b/api-extractor/report/hls.js.api.md index e702447a243..7b5ad772301 100644 --- a/api-extractor/report/hls.js.api.md +++ b/api-extractor/report/hls.js.api.md @@ -3495,6 +3495,7 @@ export type VariableMap = Record; export type VideoSelectionOption = { preferHDR?: boolean; allowedVideoRanges?: Array; + videoCodec?: string; }; // (No @packageDocumentation comment for this package) diff --git a/src/controller/abr-controller.ts b/src/controller/abr-controller.ts index eefc5750b34..dc0a3c2cdfe 100644 --- a/src/controller/abr-controller.ts +++ b/src/controller/abr-controller.ts @@ -675,6 +675,7 @@ class AbrController extends Logger implements AbrComponentAPI { const audioTracksByGroup = this.audioTracksByGroup || (this.audioTracksByGroup = getAudioTracksByGroup(allAudioTracks)); + let minStartIndex = -1; if (firstSelection) { if (this.firstSelection !== -1) { return this.firstSelection; @@ -694,8 +695,15 @@ class AbrController extends Logger implements AbrComponentAPI { audioPreference, videoPreference, ); - const { codecSet, videoRanges, minFramerate, minBitrate, preferHDR } = - startTier; + const { + codecSet, + videoRanges, + minFramerate, + minBitrate, + minIndex, + preferHDR, + } = startTier; + minStartIndex = minIndex; currentCodecSet = codecSet; currentVideoRange = preferHDR ? videoRanges[videoRanges.length - 1] @@ -789,8 +797,10 @@ class AbrController extends Logger implements AbrComponentAPI { (levelInfo.supportedResult && !levelInfo.supportedResult.decodingInfoResults?.[0].smooth) ) { - levelsSkipped.push(i); - continue; + if (firstSelection && i !== minStartIndex) { + levelsSkipped.push(i); + continue; + } } const levelDetails = levelInfo.details; @@ -869,7 +879,7 @@ class AbrController extends Logger implements AbrComponentAPI { 1, )} fetchDuration:${fetchDuration.toFixed( 1, - )} firstSelection:${firstSelection} codecSet:${currentCodecSet} videoRange:${currentVideoRange} hls.loadLevel:${loadLevel}`, + )} firstSelection:${firstSelection} codecSet:${level.codecSet} videoRange:${level.videoRange} hls.loadLevel:${loadLevel}`, ); } if (firstSelection) { diff --git a/src/hls.ts b/src/hls.ts index 90b4e8bf9c2..eb4f3be4370 100644 --- a/src/hls.ts +++ b/src/hls.ts @@ -813,7 +813,7 @@ export default class Hls implements HlsEventEmitter { public setAudioOption( audioOption: MediaPlaylist | AudioSelectionOption | undefined, ): MediaPlaylist | null { - return this.audioTrackController?.setAudioOption(audioOption); + return this.audioTrackController?.setAudioOption(audioOption) || null; } /** * Find and select the best matching subtitle track, making a level switch when a Group change is necessary. @@ -822,8 +822,9 @@ export default class Hls implements HlsEventEmitter { public setSubtitleOption( subtitleOption: MediaPlaylist | SubtitleSelectionOption | undefined, ): MediaPlaylist | null { - this.subtitleTrackController?.setSubtitleOption(subtitleOption); - return null; + return ( + this.subtitleTrackController?.setSubtitleOption(subtitleOption) || null + ); } /** diff --git a/src/types/media-playlist.ts b/src/types/media-playlist.ts index 33e3e2ebe05..af8272e6957 100644 --- a/src/types/media-playlist.ts +++ b/src/types/media-playlist.ts @@ -1,6 +1,7 @@ import type { AttrList } from '../utils/attr-list'; import type { LevelDetails } from '../loader/level-details'; -import type { VideoRange } from './level'; +import type { Level, VideoRange } from './level'; +import type { PlaylistLevelType } from './loader'; export type AudioPlaylistType = 'AUDIO'; @@ -10,9 +11,16 @@ export type SubtitlePlaylistType = 'SUBTITLES' | 'CLOSED-CAPTIONS'; export type MediaPlaylistType = MainPlaylistType | SubtitlePlaylistType; +export type MediaSelection = { + [PlaylistLevelType.MAIN]: Level; + [PlaylistLevelType.AUDIO]?: MediaPlaylist; + [PlaylistLevelType.SUBTITLE]?: MediaPlaylist; +}; + export type VideoSelectionOption = { preferHDR?: boolean; allowedVideoRanges?: Array; + videoCodec?: string; }; export type AudioSelectionOption = { diff --git a/src/utils/hdr.ts b/src/utils/hdr.ts index 3d9549ac134..b0bb54c4bb7 100644 --- a/src/utils/hdr.ts +++ b/src/utils/hdr.ts @@ -49,16 +49,13 @@ export function getVideoSelectionOptions( if (videoPreference) { allowedVideoRanges = videoPreference.allowedVideoRanges || VideoRangeValues.slice(0); + const allowAutoPreferHDR = + allowedVideoRanges.join('') !== 'SDR' && !videoPreference.videoCodec; preferHDR = videoPreference.preferHDR !== undefined ? videoPreference.preferHDR - : isHdrSupported(); - - if (preferHDR) { - allowedVideoRanges = allowedVideoRanges.filter( - (range: VideoRange) => range !== 'SDR', - ); - } else { + : allowAutoPreferHDR && isHdrSupported(); + if (!preferHDR) { allowedVideoRanges = ['SDR']; } } diff --git a/src/utils/rendition-helper.ts b/src/utils/rendition-helper.ts index 63dee30a3de..9634c0a0e96 100644 --- a/src/utils/rendition-helper.ts +++ b/src/utils/rendition-helper.ts @@ -13,6 +13,7 @@ export type CodecSetTier = { minBitrate: number; minHeight: number; minFramerate: number; + minIndex: number; maxScore: number; videoRanges: Record; channels: Record; @@ -32,6 +33,7 @@ type StartParameters = { preferHDR: boolean; minFramerate: number; minBitrate: number; + minIndex: number; }; export function getStartCodecTier( @@ -44,13 +46,15 @@ export function getStartCodecTier( const codecSets = Object.keys(codecTiers); const channelsPreference = audioPreference?.channels; const audioCodecPreference = audioPreference?.audioCodec; + const videoCodecPreference = videoPreference?.videoCodec; const preferStereo = channelsPreference && parseInt(channelsPreference) === 2; // Use first level set to determine stereo, and minimum resolution and framerate - let hasStereo = true; + let hasStereo = false; let hasCurrentVideoRange = false; let minHeight = Infinity; let minFramerate = Infinity; let minBitrate = Infinity; + let minIndex = Infinity; let selectedScore = 0; let videoRanges: Array = []; @@ -61,7 +65,7 @@ export function getStartCodecTier( for (let i = codecSets.length; i--; ) { const tier = codecTiers[codecSets[i]]; - hasStereo = tier.channels[2] > 0; + hasStereo ||= tier.channels[2] > 0; minHeight = Math.min(minHeight, tier.minHeight); minFramerate = Math.min(minFramerate, tier.minFramerate); minBitrate = Math.min(minBitrate, tier.minBitrate); @@ -70,7 +74,6 @@ export function getStartCodecTier( ); if (matchingVideoRanges.length > 0) { hasCurrentVideoRange = true; - videoRanges = matchingVideoRanges; } } minHeight = Number.isFinite(minHeight) ? minHeight : 0; @@ -82,7 +85,6 @@ export function getStartCodecTier( // If there are no variants with matching preference, set currentVideoRange to undefined if (!hasCurrentVideoRange) { currentVideoRange = undefined; - videoRanges = []; } const codecSet = codecSets.reduce( (selected: string | undefined, candidate: string) => { @@ -91,6 +93,11 @@ export function getStartCodecTier( if (candidate === selected) { return selected; } + videoRanges = hasCurrentVideoRange + ? allowedVideoRanges.filter( + (range) => candidateTier.videoRanges[range] > 0, + ) + : []; if (candidateTier.minBitrate > currentBw) { logStartCodecCandidateIgnored( candidate, @@ -159,6 +166,16 @@ export function getStartCodecTier( ); return selected; } + if ( + videoCodecPreference && + candidate.indexOf(videoCodecPreference.substring(0, 4)) % 5 !== 0 + ) { + logStartCodecCandidateIgnored( + candidate, + `video codec preference "${videoCodecPreference}" not found`, + ); + return selected; + } if (candidateTier.maxScore < selectedScore) { logStartCodecCandidateIgnored( candidate, @@ -175,6 +192,7 @@ export function getStartCodecTier( ) { return selected; } + minIndex = candidateTier.minIndex; selectedScore = candidateTier.maxScore; return candidate; }, @@ -186,6 +204,7 @@ export function getStartCodecTier( preferHDR, minFramerate, minBitrate, + minIndex, }; } @@ -243,7 +262,7 @@ export function getCodecTiers( ): Record { return levels .slice(minAutoLevel, maxAutoLevel + 1) - .reduce((tiers: Record, level) => { + .reduce((tiers: Record, level, index) => { if (!level.codecSet) { return tiers; } @@ -254,6 +273,7 @@ export function getCodecTiers( minBitrate: Infinity, minHeight: Infinity, minFramerate: Infinity, + minIndex: index, maxScore: 0, videoRanges: { SDR: 0 }, channels: { '2': 0 }, @@ -265,6 +285,7 @@ export function getCodecTiers( const lesserWidthOrHeight = Math.min(level.height, level.width); tier.minHeight = Math.min(tier.minHeight, lesserWidthOrHeight); tier.minFramerate = Math.min(tier.minFramerate, level.frameRate); + tier.minIndex = Math.min(tier.minIndex, index); tier.maxScore = Math.max(tier.maxScore, level.score); tier.fragmentError += level.fragmentError; tier.videoRanges[level.videoRange] =