Skip to content

Commit

Permalink
Implement EXT-X-DEFINE Variable Substitution
Browse files Browse the repository at this point in the history
Add support EXT-X-START in Multi-Variant Playlist
Add Content-Steering Multi-Variant Playlist parsing (#5074)
  • Loading branch information
robwalch committed Jan 18, 2023
1 parent b644288 commit 3e28b7f
Show file tree
Hide file tree
Showing 25 changed files with 1,185 additions and 212 deletions.
2 changes: 2 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ module.exports = {
__USE_ALT_AUDIO__: true,
__USE_EME_DRM__: true,
__USE_CMCD__: true,
__USE_CONTENT_STEERING__: true,
__USE_VARIABLE_SUBSTITUTION__: true,
},
// see https://github.com/standard/eslint-config-standard
// 'prettier' (https://github.com/prettier/eslint-config-prettier) must be last
Expand Down
25 changes: 17 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,23 +65,26 @@ HLS.js is written in [ECMAScript6] (`*.js`) and [TypeScript] (`*.ts`) (strongly
- Retry mechanism embedded in the library
- Recovery actions can be triggered fix fatal media or network errors
- [Redundant/Failover Playlists](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/StreamingMediaGuide/UsingHTTPLiveStreaming/UsingHTTPLiveStreaming.html#//apple_ref/doc/uid/TP40008332-CH102-SW22)
- HLS Variable Substitution

### Supported M3U8 tags
### Supported HLS tags

For details on the HLS format and these tags' meanings, see https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-08
For details on the HLS format and these tags' meanings, see https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis

#### Manifest tags
#### Multi-Variant Playlist tags

- `#EXT-X-STREAM-INF:<attribute-list>`
`<URI>`
- `#EXT-X-MEDIA:<attribute-list>`
- `#EXT-X-SESSION-DATA:<attribute-list>`
- `#EXT-X-START:TIME-OFFSET=<n>`
- `#EXT-X-DEFINE` Variable Substitution

The following properties are added to their respective variants' attribute list but are not implemented in their selection and playback.

- `VIDEO-RANGE` and `HDCP-LEVEL` (See [#2489](https://github.com/video-dev/hls.js/issues/2489))

#### Playlist tags
#### Media Playlist tags

- `#EXTM3U`
- `#EXT-X-VERSION=<n>`
Expand All @@ -99,24 +102,30 @@ The following properties are added to their respective variants' attribute list
- `#EXT-X-SERVER-CONTROL:<attribute-list>`
- `#EXT-X-PART-INF:PART-TARGET=<n>`
- `#EXT-X-PART:<attribute-list>`
- `#EXT-X-PRELOAD-HINT:<attribute-list>`
- `#EXT-X-SKIP:<attribute-list>`
- `#EXT-X-SKIP:<attribute-list>` Delta Playlists
- `#EXT-X-RENDITION-REPORT:<attribute-list>`
- `#EXT-X-DATERANGE:<attribute-list>`
- `#EXT-X-DATERANGE:<attribute-list>` Metadata
- `#EXT-X-DEFINE` Variable Substitution

The following tags are added to their respective fragment's attribute list but are not implemented in streaming and playback.

- `#EXT-X-BITRATE` (Not used in ABR controller)
- `#EXT-X-GAP` (Not implemented. See [#2940](https://github.com/video-dev/hls.js/issues/2940))

Parsed but missing feature support

- `#EXT-X-CONTENT-STEERING:<attribute-list>` (See [#3988](https://github.com/video-dev/hls.js/issues/3988))
- #3988
- `#EXT-X-PRELOAD-HINT:<attribute-list>` (See [#5074](https://github.com/video-dev/hls.js/issues/3988))
- #5074

### Not Supported

For a complete list of issues, see ["Top priorities" in the Release Planning and Backlog project tab](https://github.com/video-dev/hls.js/projects/6). Codec support is dependent on the runtime environment (for example, not all browsers on the same OS support HEVC).

- Advanced variant selection based on runtime media capabilities (See issues labeled [`media-capabilities`](https://github.com/video-dev/hls.js/labels/media-capabilities))
- HLS Content Steering
- HLS Interstitials
- `#EXT-X-DEFINE` variable substitution
- `#EXT-X-GAP` filling [#2940](https://github.com/video-dev/hls.js/issues/2940)
- `#EXT-X-I-FRAME-STREAM-INF` I-frame Media Playlist files
- `SAMPLE-AES` with fmp4, aac, mp3, vtt... segments (MPEG-2 TS only)
Expand Down
17 changes: 17 additions & 0 deletions api-extractor/report/hls.js.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,8 @@ export enum ErrorDetails {
// (undocumented)
LEVEL_LOAD_TIMEOUT = "levelLoadTimeOut",
// (undocumented)
LEVEL_PARSING_ERROR = "levelParsingError",
// (undocumented)
LEVEL_SWITCH_ERROR = "levelSwitchError",
// (undocumented)
MANIFEST_INCOMPATIBLE_CODECS_ERROR = "manifestIncompatibleCodecsError",
Expand Down Expand Up @@ -1509,6 +1511,8 @@ export class LevelDetails {
// (undocumented)
partTarget: number;
// (undocumented)
playlistParsingError: Error | null;
// (undocumented)
preloadHint?: AttrList;
// (undocumented)
PTSKnown: boolean;
Expand Down Expand Up @@ -1539,6 +1543,8 @@ export class LevelDetails {
// (undocumented)
url: string;
// (undocumented)
variableList: VariableMap | null;
// (undocumented)
version: number | null;
}

Expand Down Expand Up @@ -1867,6 +1873,8 @@ export interface ManifestLoadedData {
// (undocumented)
captions?: MediaPlaylist[];
// (undocumented)
contentSteering: Object | null;
// (undocumented)
levels: LevelParsed[];
// (undocumented)
networkDetails: any;
Expand All @@ -1875,11 +1883,15 @@ export interface ManifestLoadedData {
// (undocumented)
sessionKeys: LevelKey[] | null;
// (undocumented)
startTimeOffset: number | null;
// (undocumented)
stats: LoaderStats;
// (undocumented)
subtitles?: MediaPlaylist[];
// (undocumented)
url: string;
// (undocumented)
variableList: VariableMap | null;
}

// Warning: (ae-missing-release-tag) "ManifestLoadingData" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
Expand Down Expand Up @@ -2319,6 +2331,11 @@ export interface UserdataSample {
uuid?: string;
}

// Warning: (ae-missing-release-tag) "VariableMap" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type VariableMap = Record<string, string>;

// Warnings were encountered during analysis:
//
// src/config.ts:90:3 - (ae-forgotten-export) The symbol "MediaKeySessionContext" needs to be exported by the entry point hls.d.ts
Expand Down
2 changes: 2 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -1722,6 +1722,8 @@ Full list of errors is described below:
- data: { type : `NETWORK_ERROR`, details : `Hls.ErrorDetails.LEVEL_LOAD_ERROR`, fatal : `true`, url : level URL, response : { code: error code, text: error text }, loader : URL loader }
- `Hls.ErrorDetails.LEVEL_LOAD_TIMEOUT` - raised when level loading fails because of a timeout
- data: { type : `NETWORK_ERROR`, details : `Hls.ErrorDetails.LEVEL_LOAD_TIMEOUT`, fatal : `false`, url : level URL, loader : URL loader }
- `Hls.ErrorDetails.LEVEL_PARSING_ERROR` - raised when level parsing failed or found invalid content
- data: { type : `NETWORK_ERROR`, details : `Hls.ErrorDetails.LEVEL_PARSING_ERROR`, fatal : `false`, url : level URL, error: Error }
- `Hls.ErrorDetails.AUDIO_TRACK_LOAD_ERROR` - raised when audio playlist loading fails because of a network error
- data: { type : `NETWORK_ERROR`, details : `Hls.ErrorDetails.AUDIO_TRACK_LOAD_ERROR`, fatal : `false`, url : audio URL, response : { code: error code, text: error text }, loader : URL loader }
- `Hls.ErrorDetails.AUDIO_TRACK_LOAD_TIMEOUT` - raised when audio playlist loading fails because of a timeout
Expand Down
24 changes: 20 additions & 4 deletions src/controller/base-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import type {
MediaAttachingData,
BufferFlushingData,
LevelSwitchingData,
ManifestLoadedData,
} from '../types/events';
import type { FragmentTracker } from './fragment-tracker';
import type { Level } from '../types/level';
Expand Down Expand Up @@ -83,6 +84,7 @@ export default class BaseStreamController
protected lastCurrentTime: number = 0;
protected nextLoadPosition: number = 0;
protected startPosition: number = 0;
protected startTimeOffset: number | null = null;
protected loadedmetadata: boolean = false;
protected fragLoadError: number = 0;
protected retryDate: number = 0;
Expand Down Expand Up @@ -116,6 +118,7 @@ export default class BaseStreamController
this.fragmentTracker = fragmentTracker;
this.config = hls.config;
this.decrypter = new Decrypter(hls.config);
hls.on(Events.MANIFEST_LOADED, this.onManifestLoaded, this);
hls.on(Events.LEVEL_SWITCHING, this.onLevelSwitching, this);
}

Expand Down Expand Up @@ -284,6 +287,13 @@ export default class BaseStreamController
this.startPosition = this.lastCurrentTime = 0;
}

protected onManifestLoaded(
event: Events.MANIFEST_LOADED,
data: ManifestLoadedData
): void {
this.startTimeOffset = data.startTimeOffset;
}

protected onLevelSwitching(
event: Events.LEVEL_SWITCHING,
data: LevelSwitchingData
Expand Down Expand Up @@ -1253,9 +1263,13 @@ export default class BaseStreamController
startPosition = -1;
}
if (startPosition === -1 || this.lastCurrentTime === -1) {
// first, check if start time offset has been set in playlist, if yes, use this value
const startTimeOffset = details.startTimeOffset!;
if (Number.isFinite(startTimeOffset)) {
// Use Playlist EXT-X-START:TIME-OFFSET when set
// Prioritize Multi-Variant Playlist offset so that main, audio, and subtitle stream-controller start times match
const offsetInMultiVariantPlaylist = this.startTimeOffset !== null;
const startTimeOffset = offsetInMultiVariantPlaylist
? this.startTimeOffset
: details.startTimeOffset;
if (startTimeOffset !== null && Number.isFinite(startTimeOffset)) {
startPosition = sliding + startTimeOffset;
if (startTimeOffset < 0) {
startPosition += details.totalduration;
Expand All @@ -1265,7 +1279,9 @@ export default class BaseStreamController
sliding + details.totalduration
);
this.log(
`Start time offset ${startTimeOffset} found in playlist, adjust startPosition to ${startPosition}`
`Start time offset ${startTimeOffset} found in ${
offsetInMultiVariantPlaylist ? 'multi-variant' : 'media'
} playlist, adjust startPosition to ${startPosition}`
);
this.startPosition = startPosition;
} else if (details.live) {
Expand Down
1 change: 1 addition & 0 deletions src/controller/level-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ export default class LevelController extends BasePlaylistController {
break;
case ErrorDetails.LEVEL_LOAD_ERROR:
case ErrorDetails.LEVEL_LOAD_TIMEOUT:
case ErrorDetails.LEVEL_PARSING_ERROR:
// Do not perform level switch if an error occurred using delivery directives
// Attempt to reload level without directives first
if (context) {
Expand Down
1 change: 1 addition & 0 deletions src/controller/stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -871,6 +871,7 @@ export default class StreamController
break;
case ErrorDetails.LEVEL_LOAD_ERROR:
case ErrorDetails.LEVEL_LOAD_TIMEOUT:
case ErrorDetails.LEVEL_PARSING_ERROR:
if (this.state !== State.ERROR) {
if (data.fatal) {
// if fatal error, stop processing
Expand Down
2 changes: 2 additions & 0 deletions src/define-plugin.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ declare const __USE_ALT_AUDIO__: boolean;
declare const __USE_EME_DRM__: boolean;
declare const __USE_SUBTITLES__: boolean;
declare const __USE_CMCD__: boolean;
declare const __USE_CONTENT_STEERING__: boolean;
declare const __USE_VARIABLE_SUBSTITUTION__: boolean;
2 changes: 2 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export enum ErrorDetails {
LEVEL_LOAD_ERROR = 'levelLoadError',
// Identifier for a level load timeout - data: { url : faulty URL, response : { code: error code, text: error text }}
LEVEL_LOAD_TIMEOUT = 'levelLoadTimeOut',
// Identifier for a level parse error - data: { url : faulty URL, error: Error, reason: error message }
LEVEL_PARSING_ERROR = 'levelParsingError',
// Identifier for a level switch error - data: { level : faulty level Id, event : error description}
LEVEL_SWITCH_ERROR = 'levelSwitchError',
// Identifier for an audio track load error - data: { url : faulty URL, response : { code: error code, text: error text }}
Expand Down
3 changes: 2 additions & 1 deletion src/hls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import type SubtitleTrackController from './controller/subtitle-track-controller
import type { ComponentAPI, NetworkComponentAPI } from './types/component-api';
import type { MediaPlaylist } from './types/media-playlist';
import type { HlsConfig } from './config';
import { HdcpLevel, HdcpLevels, Level } from './types/level';
import { HdcpLevel, HdcpLevels, Level, VariableMap } from './types/level';
import type { Fragment } from './loader/fragment';
import type { BufferInfo } from './utils/buffer-helper';

Expand Down Expand Up @@ -921,6 +921,7 @@ export type {
HlsUrlParameters,
LevelAttributes,
LevelParsed,
VariableMap,
} from './types/level';
export type {
PlaylistLevelType,
Expand Down
4 changes: 2 additions & 2 deletions src/loader/fragment-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,8 +314,8 @@ function createLoaderContext(

export class LoadError extends Error {
public readonly data: FragLoadFailResult;
constructor(data: FragLoadFailResult, ...params) {
super(...params);
constructor(data: FragLoadFailResult, message?: string) {
super(message);
this.data = data;
}
}
Expand Down
37 changes: 18 additions & 19 deletions src/loader/key-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,19 @@ export default class KeyLoader implements ComponentAPI {
createKeyLoadError(
frag: Fragment,
details: ErrorDetails = ErrorDetails.KEY_LOAD_ERROR,
networkDetails?: any,
message?: string
message: string,
networkDetails?: any
): LoadError {
return new LoadError({
type: ErrorTypes.NETWORK_ERROR,
details,
fatal: false,
frag,
networkDetails,
});
return new LoadError(
{
type: ErrorTypes.NETWORK_ERROR,
details,
fatal: false,
frag,
networkDetails,
},
message
);
}

loadClear(
Expand Down Expand Up @@ -127,12 +130,7 @@ export default class KeyLoader implements ComponentAPI {
? `Expected frag.decryptdata to be defined after setting format ${keySystemFormat}`
: 'Missing decryption data on fragment in onKeyLoading';
return Promise.reject(
this.createKeyLoadError(
frag,
ErrorDetails.KEY_LOAD_ERROR,
null,
errorMessage
)
this.createKeyLoadError(frag, ErrorDetails.KEY_LOAD_ERROR, errorMessage)
);
}
const uri = decryptdata.uri;
Expand All @@ -141,7 +139,6 @@ export default class KeyLoader implements ComponentAPI {
this.createKeyLoadError(
frag,
ErrorDetails.KEY_LOAD_ERROR,
null,
`Invalid key URI: "${uri}"`
)
);
Expand Down Expand Up @@ -190,7 +187,6 @@ export default class KeyLoader implements ComponentAPI {
this.createKeyLoadError(
frag,
ErrorDetails.KEY_LOAD_ERROR,
null,
`Key supplied with unsupported METHOD: "${decryptdata.method}"`
)
);
Expand Down Expand Up @@ -256,8 +252,8 @@ export default class KeyLoader implements ComponentAPI {
this.createKeyLoadError(
frag,
ErrorDetails.KEY_LOAD_ERROR,
networkDetails,
'after key load, decryptdata unset or changed'
'after key load, decryptdata unset or changed',
networkDetails
)
);
}
Expand All @@ -282,6 +278,7 @@ export default class KeyLoader implements ComponentAPI {
this.createKeyLoadError(
frag,
ErrorDetails.KEY_LOAD_ERROR,
'error loading key',
networkDetails
)
);
Expand All @@ -297,6 +294,7 @@ export default class KeyLoader implements ComponentAPI {
this.createKeyLoadError(
frag,
ErrorDetails.KEY_LOAD_TIMEOUT,
'key loading timed out',
networkDetails
)
);
Expand All @@ -312,6 +310,7 @@ export default class KeyLoader implements ComponentAPI {
this.createKeyLoadError(
frag,
ErrorDetails.INTERNAL_ABORTED,
'key loading aborted',
networkDetails
)
);
Expand Down
3 changes: 3 additions & 0 deletions src/loader/level-details.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Part } from './fragment';
import type { Fragment } from './fragment';
import type { AttrList } from '../utils/attr-list';
import type { DateRange } from './date-range';
import type { VariableMap } from '../types/level';

const DEFAULT_TARGET_DURATION = 10;

Expand Down Expand Up @@ -48,6 +49,8 @@ export class LevelDetails {
public driftStart: number = 0;
public driftEnd: number = 0;
public encryptedFragments: Fragment[];
public playlistParsingError: Error | null = null;
public variableList: VariableMap | null = null;

constructor(baseUrl) {
this.fragments = [];
Expand Down
Loading

0 comments on commit 3e28b7f

Please sign in to comment.