src/types/level.ts
import { LevelDetails } from '../loader/level-details';
import { AttrList } from '../utils/attr-list';
export interface LevelParsed {
attrs: LevelAttributes;
audioCodec?: string;
bitrate: number;
details?: LevelDetails;
height?: number;
id?: number;
level?: number;
name: string;
textCodec?: string;
unknownCodecs?: string[];
url: string;
videoCodec?: string;
width?: number;
}
export interface LevelAttributes extends AttrList {
AUDIO?: string;
AUTOSELECT?: string;
'AVERAGE-BANDWIDTH'?: string;
BANDWIDTH?: string;
BYTERANGE?: string;
'CLOSED-CAPTIONS'?: string;
CHARACTERISTICS?: string;
CODECS?: string;
DEFAULT?: string;
FORCED?: string;
'FRAME-RATE'?: string;
LANGUAGE?: string;
NAME?: string;
'PROGRAM-ID'?: string;
RESOLUTION?: string;
SUBTITLES?: string;
TYPE?: string;
URI?: string;
}
export enum HlsSkip {
No = '',
Yes = 'YES',
v2 = 'v2',
}
export function getSkipValue(details: LevelDetails, msn?: number): HlsSkip {
const { canSkipUntil, canSkipDateRanges, endSN } = details;
const snChangeGoal = msn !== undefined ? msn - endSN : 0;
if (canSkipUntil && snChangeGoal < canSkipUntil) {
if (canSkipDateRanges) {
return HlsSkip.v2;
}
return HlsSkip.Yes;
}
return HlsSkip.No;
}
export class HlsUrlParameters {
msn?: number;
part?: number;
skip?: HlsSkip;
constructor(msn?: number, part?: number, skip?: HlsSkip) {
this.msn = msn;
this.part = part;
this.skip = skip;
}
addDirectives(uri: string): string | never {
const url: URL = new self.URL(uri);
if (this.msn !== undefined) {
url.searchParams.set('_HLS_msn', this.msn.toString());
}
if (this.part !== undefined) {
url.searchParams.set('_HLS_part', this.part.toString());
}
if (this.skip) {
url.searchParams.set('_HLS_skip', this.skip);
}
return url.toString();
}
}
export class Level {
public readonly attrs: LevelAttributes;
public readonly audioCodec: string | undefined;
public readonly bitrate: number;
public readonly codecSet: string;
public readonly height: number;
public readonly id: number;
public readonly name: string | undefined;
public readonly videoCodec: string | undefined;
public readonly width: number;
public readonly unknownCodecs: string[] | undefined;
public audioGroupIds?: string[];
public details?: LevelDetails;
public fragmentError: number = 0;
public loadError: number = 0;
public loaded?: { bytes: number; duration: number };
public realBitrate: number = 0;
public textGroupIds?: string[];
public url: string[];
private _urlId: number = 0;
constructor(data: LevelParsed) {
this.url = [data.url];
this.attrs = data.attrs;
this.bitrate = data.bitrate;
if (data.details) {
this.details = data.details;
}
this.id = data.id || 0;
this.name = data.name;
this.width = data.width || 0;
this.height = data.height || 0;
this.audioCodec = data.audioCodec;
this.videoCodec = data.videoCodec;
this.unknownCodecs = data.unknownCodecs;
this.codecSet = [data.videoCodec, data.audioCodec]
.filter((c) => c)
.join(',')
.replace(/\.[^.,]+/g, '');
}
get maxBitrate(): number {
return Math.max(this.realBitrate, this.bitrate);
}
get uri(): string {
return this.url[this._urlId] || '';
}
get urlId(): number {
return this._urlId;
}
set urlId(value: number) {
const newValue = value % this.url.length;
if (this._urlId !== newValue) {
this.details = undefined;
this._urlId = newValue;
}
}
}