Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HDR/SDR VIDEO-RANGE selection and priority #6007

Merged
merged 3 commits into from
Dec 4, 2023
Merged
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
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
17 changes: 12 additions & 5 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 Expand Up @@ -686,12 +691,14 @@ class AbrController implements AbrComponentAPI {
!levelInfo.supportedResult &&
!levelInfo.supportedPromise
) {
const mediaCapabilities = navigator.mediaCapabilities;
const mediaCapabilities = navigator.mediaCapabilities as
| MediaCapabilities
| undefined;
if (
typeof mediaCapabilities?.decodingInfo === 'function' &&
requiresMediaCapabilitiesDecodingInfo(
levelInfo,
audioTracksByGroup,
mediaCapabilities,
currentVideoRange,
currentFrameRate,
currentBw,
Expand Down
18 changes: 10 additions & 8 deletions src/controller/buffer-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,8 +400,8 @@ export default class BufferController implements ComponentAPI {
},
onStart: () => {},
onComplete: () => {},
onError: (e) => {
this.warn(`Failed to change ${type} SourceBuffer type`, e);
onError: (error: Error) => {
this.warn(`Failed to change ${type} SourceBuffer type`, error);
},
};

Expand Down Expand Up @@ -495,7 +495,7 @@ export default class BufferController implements ComponentAPI {
timeRanges,
});
},
onError: (err) => {
onError: (error: Error) => {
// in case any error occured while appending, put back segment in segments table
const event: ErrorData = {
type: ErrorTypes.MEDIA_ERROR,
Expand All @@ -505,12 +505,12 @@ export default class BufferController implements ComponentAPI {
frag,
part,
chunkMeta,
error: err,
err,
error,
err: error,
fatal: false,
};

if (err.code === DOMException.QUOTA_EXCEEDED_ERR) {
if ((error as DOMException).code === DOMException.QUOTA_EXCEEDED_ERR) {
// QuotaExceededError: http://www.w3.org/TR/html5/infrastructure.html#quotaexceedederror
// let's stop appending any segments, and report BUFFER_FULL_ERROR error
event.details = ErrorDetails.BUFFER_FULL_ERROR;
Expand Down Expand Up @@ -1032,7 +1032,9 @@ export default class BufferController implements ComponentAPI {
}

private _onSBUpdateError(type: SourceBufferName, event: Event) {
const error = new Error(`${type} SourceBuffer error`);
const error = new Error(
`${type} SourceBuffer error. MediaSource readyState: ${this.mediaSource?.readyState}`,
);
this.error(`${error}`, event);
// according to http://www.w3.org/TR/media-source/#sourcebuffer-append-error
// SourceBuffer errors are not necessarily fatal; if so, the HTMLMediaElement will fire an error event
Expand All @@ -1046,7 +1048,7 @@ export default class BufferController implements ComponentAPI {
// updateend is always fired after error, so we'll allow that to shift the current operation off of the queue
const operation = this.operationQueue.current(type);
if (operation) {
operation.onError(event);
operation.onError(error);
}
}

Expand Down
5 changes: 5 additions & 0 deletions src/controller/error-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,11 @@ export default class ErrorController implements NetworkComponentAPI {
data.details !== ErrorDetails.FRAG_GAP
) {
data.fatal = true;
} else if (/MediaSource readyState: ended/.test(data.error.message)) {
this.warn(
`MediaSource ended after "${data.sourceBufferName}" sourceBuffer append error. Attempting to recover from media error.`,
);
this.hls.recoverMediaError();
}
break;
case NetworkErrorAction.RetryRequest:
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,
};
}
8 changes: 3 additions & 5 deletions src/utils/mediacapabilities-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ export const SUPPORTED_INFO_CACHE: Record<
export function requiresMediaCapabilitiesDecodingInfo(
level: Level,
audioTracksByGroup: AudioTracksByGroup,
mediaCapabilities: MediaCapabilities | undefined,
currentVideoRange: VideoRange | undefined,
currentFrameRate: number,
currentBw: number,
Expand Down Expand Up @@ -75,8 +74,7 @@ export function requiresMediaCapabilitiesDecodingInfo(
}
}
return (
(typeof mediaCapabilities?.decodingInfo == 'function' &&
level.videoCodec !== undefined &&
(level.videoCodec !== undefined &&
((level.width > 1920 && level.height > 1088) ||
(level.height > 1920 && level.width > 1088) ||
level.frameRate > Math.max(currentFrameRate, 30) ||
Expand All @@ -94,11 +92,11 @@ export function requiresMediaCapabilitiesDecodingInfo(
export function getMediaDecodingInfoPromise(
level: Level,
audioTracksByGroup: AudioTracksByGroup,
mediaCapabilities: MediaCapabilities,
mediaCapabilities: MediaCapabilities | undefined,
): Promise<MediaDecodingInfo> {
const videoCodecs = level.videoCodec;
const audioCodecs = level.audioCodec;
if (!videoCodecs || !audioCodecs) {
if (!videoCodecs || !audioCodecs || !mediaCapabilities) {
return Promise.resolve(SUPPORTED_INFO_DEFAULT);
}

Expand Down
Loading
Loading