Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api-extractor/report/hls.js.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ export type ABRControllerConfig = {
abrMaxWithRealBitrate: boolean;
maxStarvationDelay: number;
maxLoadingDelay: number;
abrUpSwitchToLowerFrameRateMode: 'block' | 'allow';
abrDownSwitchToHigherFrameRateMode: 'block' | 'allow';
};

// Warning: (ae-missing-release-tag) "AssetListJSON" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
Expand Down
4 changes: 4 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ export type ABRControllerConfig = {
abrMaxWithRealBitrate: boolean;
maxStarvationDelay: number;
maxLoadingDelay: number;
abrUpSwitchToLowerFrameRateMode: 'block' | 'allow';
abrDownSwitchToHigherFrameRateMode: 'block' | 'allow';
};

export type BufferControllerConfig = {
Expand Down Expand Up @@ -432,6 +434,8 @@ export const hlsDefaultConfig: HlsConfig = {
abrMaxWithRealBitrate: false, // used by abr-controller
maxStarvationDelay: 4, // used by abr-controller
maxLoadingDelay: 4, // used by abr-controller
abrUpSwitchToLowerFrameRateMode: 'block', // used by abr-controller
abrDownSwitchToHigherFrameRateMode: 'block', // used by abr-controller
minAutoBitrate: 0, // used by hls
emeEnabled: false, // used by eme-controller
widevineLicenseUrl: undefined, // used by eme-controller
Expand Down
13 changes: 11 additions & 2 deletions src/controller/abr-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -813,6 +813,12 @@ class AbrController extends Logger implements AbrComponentAPI {

const ttfbEstimateSec = this.bwEstimator.getEstimateTTFB() / 1000;
const levelsSkipped: number[] = [];

const isUpSwitchToLowerFrameRate =
this.hls.config.abrUpSwitchToLowerFrameRateMode === 'allow';
const isDownSwitchToHigherFrameRate =
this.hls.config.abrDownSwitchToHigherFrameRateMode === 'allow';

for (let i = maxAutoLevel; i >= minAutoLevel; i--) {
const levelInfo = levels[i];
const upSwitch = i > selectionBaseLevel;
Expand Down Expand Up @@ -898,10 +904,13 @@ class AbrController extends Logger implements AbrComponentAPI {
if (
(currentCodecSet && levelInfo.codecSet !== currentCodecSet) ||
(currentVideoRange && levelInfo.videoRange !== currentVideoRange) ||
(upSwitch && currentFrameRate > levelInfo.frameRate) ||
(upSwitch &&
currentFrameRate > levelInfo.frameRate &&
!isUpSwitchToLowerFrameRate) ||
(!upSwitch &&
currentFrameRate > 0 &&
currentFrameRate < levelInfo.frameRate) ||
currentFrameRate < levelInfo.frameRate &&
!isDownSwitchToHigherFrameRate) ||
Comment on lines -901 to +913
Copy link
Collaborator

@robwalch robwalch Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think unblocking selection behavior via config is a good solution to working around content that does not follow best practices when providing frame rates at various bitrates (an example with 360-720p@30, 1080p@60, and [email protected] was given offline).

First, we could loosen the checks to allow switching between almost identical frame rates. That could allow switching straight to a higher bitrate at 29.997 from 30 fps. There would still be a chance to switch up to 60fps and then not switch up any higher. The user should be presented with the resolution and frame-rates to allow manual override in these cases. Allowing the client to automatically bounce back and forth between rates should be avoided.

Adding a frameRate (preferred) and maxFrameRate options to the videoPreference: VideoSelectionOption config options interface for initial selection and capping would be preferred. You might also consider making capping part of the fps-controller, (see capLevelOnFPSDrop) but that would entail more of a refactor as that controller is meant to limit selection based on performance, not the variant frame-rate. It could do both, or either, if a simple flat capLevelByFrameRate option is preferred.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested-Workaround: If you really want to force hls.js to ignore level frameRate than change the parsed values after playback has started on hls.levels to all be the same. It may seem like a hack, but it's a valid workaround after level.supportedPromise is resolved.

Comment on lines +907 to +913
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First, we could loosen the checks to allow switching between almost identical frame rates

Suggested change
(upSwitch &&
currentFrameRate > levelInfo.frameRate &&
!isUpSwitchToLowerFrameRate) ||
(!upSwitch &&
currentFrameRate > 0 &&
currentFrameRate < levelInfo.frameRate) ||
currentFrameRate < levelInfo.frameRate &&
!isDownSwitchToHigherFrameRate) ||
(upSwitch && currentFrameRate > Math.ceil(levelInfo.frameRate)) ||
(!upSwitch &&
currentFrameRate > 0 &&
Math.ceil(currentFrameRate) < levelInfo.frameRate) ||

levelInfo.supportedResult?.decodingInfoResults?.some(
(info) => info.smooth === false,
)
Expand Down
Loading