Skip to content

Commit

Permalink
Add videoPreference config option for HDR/SDR VIDEO-RANGE selection…
Browse files Browse the repository at this point in the history
… and priority

Resolves #2489
  • Loading branch information
iamboorrito authored and robwalch committed Nov 30, 2023
1 parent 202ba5f commit e651e83
Show file tree
Hide file tree
Showing 8 changed files with 142 additions and 12 deletions.
9 changes: 9 additions & 0 deletions api-extractor/report/hls.js.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -3081,6 +3081,7 @@ export type RetryConfig = {
//
// @public (undocumented)
export type SelectionPreferences = {
videoPreference?: VideoSelectionOption;
audioPreference?: AudioSelectionOption;
subtitlePreference?: SubtitleSelectionOption;
};
Expand Down Expand Up @@ -3417,6 +3418,14 @@ export interface UserdataSample {
// @public (undocumented)
export type VariableMap = Record<string, string>;

// Warning: (ae-missing-release-tag) "VideoSelectionOption" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type VideoSelectionOption = {
preferHDR?: boolean;
allowedVideoRanges?: Array<VideoRange>;
};

// (No @packageDocumentation comment for this package)

```
17 changes: 17 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ See [API Reference](https://hlsjs-dev.video-dev.org/api-docs/) for a complete li
- [`pLoader`](#ploader)
- [`xhrSetup`](#xhrsetup)
- [`fetchSetup`](#fetchsetup)
- [`videoPreference`](#videopreference)
- [`audioPreference`](#audiopreference)
- [`subtitlePreference`](#subtitlepreference)
- [`abrController`](#abrcontroller)
Expand Down Expand Up @@ -1145,6 +1146,22 @@ var config = {
};
```

### `videoPreference`

(default `undefined`)

These settings determine whether HDR video should be selected before SDR video. Which VIDEO-RANGE values are allowed, and in what order of priority can also be specified.

Format `{ preferHDR: boolean, allowedVideoRanges: ('SDR' | 'PQ' | 'HLG')[] }`

- Allow all video ranges if `allowedVideoRanges` is unspecified.
- If `preferHDR` is defined, use the value to filter `allowedVideoRanges`.
- Else check window for HDR support and set `preferHDR` to the result.

When `preferHDR` is set, skip checking if the window supports HDR and instead use the value provided to determine level selection preference via dynamic range. A value of `preferHDR === true` will attempt to use HDR levels before selecting from SDR levels.

`allowedVideoRanges` can restrict playback to a limited set of VIDEO-RANGE transfer functions and set their priority for selection. For example, to ignore all HDR variants, set `allowedVideoRanges` to `['SDR']`. Or, to ignore all HLG variants, set `allowedVideoRanges` to `['SDR', 'PQ']`. To prioritize PQ variants over HLG, set `allowedVideoRanges` to `['SDR', 'HLG', 'PQ']`.

### `audioPreference`

(default: `undefined`)
Expand Down
2 changes: 2 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import type {
import type {
AudioSelectionOption,
SubtitleSelectionOption,
VideoSelectionOption,
} from './types/media-playlist';

export type ABRControllerConfig = {
Expand Down Expand Up @@ -223,6 +224,7 @@ export type StreamControllerConfig = {
};

export type SelectionPreferences = {
videoPreference?: VideoSelectionOption;
audioPreference?: AudioSelectionOption;
subtitlePreference?: SubtitleSelectionOption;
};
Expand Down
11 changes: 8 additions & 3 deletions src/controller/abr-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -633,7 +633,8 @@ class AbrController implements AbrComponentAPI {
let currentCodecSet: string | undefined;
let currentVideoRange: VideoRange | undefined = 'SDR';
let currentFrameRate = level?.frameRate || 0;
const audioPreference = config.audioPreference;

const { audioPreference, videoPreference } = config;
const audioTracksByGroup =
this.audioTracksByGroup ||
(this.audioTracksByGroup = getAudioTracksByGroup(allAudioTracks));
Expand All @@ -654,10 +655,14 @@ class AbrController implements AbrComponentAPI {
currentVideoRange,
currentBw,
audioPreference,
videoPreference,
);
const { codecSet, videoRange, minFramerate, minBitrate } = startTier;
const { codecSet, videoRanges, minFramerate, minBitrate, preferHDR } =
startTier;
currentCodecSet = codecSet;
currentVideoRange = videoRange;
currentVideoRange = preferHDR
? videoRanges[videoRanges.length - 1]
: videoRanges[0];
currentFrameRate = minFramerate;
currentBw = Math.max(currentBw, minBitrate);
logger.log(`[abr] picked start tier ${JSON.stringify(startTier)}`);
Expand Down
2 changes: 2 additions & 0 deletions src/hls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import type {
AudioSelectionOption,
MediaPlaylist,
SubtitleSelectionOption,
VideoSelectionOption,
} from './types/media-playlist';
import type { HlsConfig } from './config';
import type { BufferInfo } from './utils/buffer-helper';
Expand Down Expand Up @@ -959,6 +960,7 @@ export default class Hls implements HlsEventEmitter {
export type {
AudioSelectionOption,
SubtitleSelectionOption,
VideoSelectionOption,
MediaPlaylist,
ErrorDetails,
ErrorTypes,
Expand Down
6 changes: 6 additions & 0 deletions src/types/media-playlist.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { AttrList } from '../utils/attr-list';
import type { LevelDetails } from '../loader/level-details';
import type { VideoRange } from './level';

export type AudioPlaylistType = 'AUDIO';

Expand All @@ -9,6 +10,11 @@ export type SubtitlePlaylistType = 'SUBTITLES' | 'CLOSED-CAPTIONS';

export type MediaPlaylistType = MainPlaylistType | SubtitlePlaylistType;

export type VideoSelectionOption = {
preferHDR?: boolean;
allowedVideoRanges?: Array<VideoRange>;
};

export type AudioSelectionOption = {
lang?: string;
assocLang?: string;
Expand Down
70 changes: 70 additions & 0 deletions src/utils/hdr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { type VideoRange, VideoRangeValues } from '../types/level';
import type { VideoSelectionOption } from '../types/media-playlist';

/**
* @returns Whether we can detect and validate HDR capability within the window context
*/
export function isHdrSupported() {
if (typeof matchMedia === 'function') {
const mediaQueryList = matchMedia('(dynamic-range: high)');
const badQuery = matchMedia('bad query');
if (mediaQueryList.media !== badQuery.media) {
return mediaQueryList.matches === true;
}
}
return false;
}

/**
* Sanitizes inputs to return the active video selection options for HDR/SDR.
* When both inputs are null:
*
* `{ preferHDR: false, allowedVideoRanges: [] }`
*
* When `currentVideoRange` non-null, maintain the active range:
*
* `{ preferHDR: currentVideoRange !== 'SDR', allowedVideoRanges: [currentVideoRange] }`
*
* When VideoSelectionOption non-null:
*
* - Allow all video ranges if `allowedVideoRanges` unspecified.
* - If `preferHDR` is non-null use the value to filter `allowedVideoRanges`.
* - Else check window for HDR support and set `preferHDR` to the result.
*
* @param currentVideoRange
* @param videoPreference
*/
export function getVideoSelectionOptions(
currentVideoRange: VideoRange | undefined,
videoPreference: VideoSelectionOption | undefined,
) {
let preferHDR = false;
let allowedVideoRanges: Array<VideoRange> = [];

if (currentVideoRange) {
preferHDR = currentVideoRange !== 'SDR';
allowedVideoRanges = [currentVideoRange];
}

if (videoPreference) {
allowedVideoRanges =
videoPreference.allowedVideoRanges || VideoRangeValues.slice(0);
preferHDR =
videoPreference.preferHDR !== undefined
? videoPreference.preferHDR
: isHdrSupported();

if (preferHDR) {
allowedVideoRanges = allowedVideoRanges.filter(
(range: VideoRange) => range !== 'SDR',
);
} else {
allowedVideoRanges = ['SDR'];
}
}

return {
preferHDR,
allowedVideoRanges,
};
}
37 changes: 28 additions & 9 deletions src/utils/rendition-helper.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { codecsSetSelectionPreferenceValue } from './codecs';
import { getVideoSelectionOptions } from './hdr';
import { logger } from './logger';
import type { Level, VideoRange } from '../types/level';
import type {
AudioSelectionOption,
MediaPlaylist,
SubtitleSelectionOption,
VideoSelectionOption,
} from '../types/media-playlist';

export type CodecSetTier = {
Expand All @@ -26,16 +28,18 @@ type AudioTrackGroup = {
};
type StartParameters = {
codecSet: string | undefined;
videoRange: VideoRange | undefined;
videoRanges: Array<VideoRange>;
preferHDR: boolean;
minFramerate: number;
minBitrate: number;
};

export function getStartCodecTier(
codecTiers: Record<string, CodecSetTier>,
videoRange: VideoRange | undefined,
currentVideoRange: VideoRange | undefined,
currentBw: number,
audioPreference: AudioSelectionOption | undefined,
videoPreference: VideoSelectionOption | undefined,
): StartParameters {
const codecSets = Object.keys(codecTiers);
const channelsPreference = audioPreference?.channels;
Expand All @@ -48,14 +52,25 @@ export function getStartCodecTier(
let minFramerate = Infinity;
let minBitrate = Infinity;
let selectedScore = 0;
let videoRanges: Array<VideoRange> = [];

const { preferHDR, allowedVideoRanges } = getVideoSelectionOptions(
currentVideoRange,
videoPreference,
);

for (let i = codecSets.length; i--; ) {
const tier = codecTiers[codecSets[i]];
hasStereo = tier.channels[2] > 0;
minHeight = Math.min(minHeight, tier.minHeight);
minFramerate = Math.min(minFramerate, tier.minFramerate);
minBitrate = Math.min(minBitrate, tier.minBitrate);
if (videoRange) {
hasCurrentVideoRange ||= tier.videoRanges[videoRange] > 0;
const matchingVideoRanges = allowedVideoRanges.filter(
(range) => tier.videoRanges[range] > 0,
);
if (matchingVideoRanges.length > 0) {
hasCurrentVideoRange = true;
videoRanges = matchingVideoRanges;
}
}
minHeight = Number.isFinite(minHeight) ? minHeight : 0;
Expand All @@ -64,9 +79,10 @@ export function getStartCodecTier(
const maxFramerate = Math.max(30, minFramerate);
minBitrate = Number.isFinite(minBitrate) ? minBitrate : currentBw;
currentBw = Math.max(minBitrate, currentBw);
// If there are no SDR variants, set currentVideoRange to undefined
// If there are no variants with matching preference, set currentVideoRange to undefined
if (!hasCurrentVideoRange) {
videoRange = undefined;
currentVideoRange = undefined;
videoRanges = [];
}
const codecSet = codecSets.reduce(
(selected: string | undefined, candidate: string) => {
Expand Down Expand Up @@ -134,10 +150,12 @@ export function getStartCodecTier(
);
return selected;
}
if (videoRange && candidateTier.videoRanges[videoRange] === 0) {
if (!videoRanges.some((range) => candidateTier.videoRanges[range] > 0)) {
logStartCodecCandidateIgnored(
candidate,
`no variants with VIDEO-RANGE of ${videoRange} found`,
`no variants with VIDEO-RANGE of ${JSON.stringify(
videoRanges,
)} found`,
);
return selected;
}
Expand All @@ -164,7 +182,8 @@ export function getStartCodecTier(
);
return {
codecSet,
videoRange,
videoRanges,
preferHDR,
minFramerate,
minBitrate,
};
Expand Down

0 comments on commit e651e83

Please sign in to comment.