video
This commit is contained in:
132
node_modules/hls.js/src/loader/date-range.ts
generated
vendored
Normal file
132
node_modules/hls.js/src/loader/date-range.ts
generated
vendored
Normal file
@@ -0,0 +1,132 @@
|
||||
import { AttrList } from '../utils/attr-list';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
// Avoid exporting const enum so that these values can be inlined
|
||||
const enum DateRangeAttribute {
|
||||
ID = 'ID',
|
||||
CLASS = 'CLASS',
|
||||
START_DATE = 'START-DATE',
|
||||
DURATION = 'DURATION',
|
||||
END_DATE = 'END-DATE',
|
||||
END_ON_NEXT = 'END-ON-NEXT',
|
||||
PLANNED_DURATION = 'PLANNED-DURATION',
|
||||
SCTE35_OUT = 'SCTE35-OUT',
|
||||
SCTE35_IN = 'SCTE35-IN',
|
||||
}
|
||||
|
||||
export function isDateRangeCueAttribute(attrName: string): boolean {
|
||||
return (
|
||||
attrName !== DateRangeAttribute.ID &&
|
||||
attrName !== DateRangeAttribute.CLASS &&
|
||||
attrName !== DateRangeAttribute.START_DATE &&
|
||||
attrName !== DateRangeAttribute.DURATION &&
|
||||
attrName !== DateRangeAttribute.END_DATE &&
|
||||
attrName !== DateRangeAttribute.END_ON_NEXT
|
||||
);
|
||||
}
|
||||
|
||||
export function isSCTE35Attribute(attrName: string): boolean {
|
||||
return (
|
||||
attrName === DateRangeAttribute.SCTE35_OUT ||
|
||||
attrName === DateRangeAttribute.SCTE35_IN
|
||||
);
|
||||
}
|
||||
|
||||
export class DateRange {
|
||||
public attr: AttrList;
|
||||
private _startDate: Date;
|
||||
private _endDate?: Date;
|
||||
private _badValueForSameId?: string;
|
||||
|
||||
constructor(dateRangeAttr: AttrList, dateRangeWithSameId?: DateRange) {
|
||||
if (dateRangeWithSameId) {
|
||||
const previousAttr = dateRangeWithSameId.attr;
|
||||
for (const key in previousAttr) {
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(dateRangeAttr, key) &&
|
||||
dateRangeAttr[key] !== previousAttr[key]
|
||||
) {
|
||||
logger.warn(
|
||||
`DATERANGE tag attribute: "${key}" does not match for tags with ID: "${dateRangeAttr.ID}"`,
|
||||
);
|
||||
this._badValueForSameId = key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Merge DateRange tags with the same ID
|
||||
dateRangeAttr = Object.assign(
|
||||
new AttrList({}),
|
||||
previousAttr,
|
||||
dateRangeAttr,
|
||||
);
|
||||
}
|
||||
this.attr = dateRangeAttr;
|
||||
this._startDate = new Date(dateRangeAttr[DateRangeAttribute.START_DATE]);
|
||||
if (DateRangeAttribute.END_DATE in this.attr) {
|
||||
const endDate = new Date(this.attr[DateRangeAttribute.END_DATE]);
|
||||
if (Number.isFinite(endDate.getTime())) {
|
||||
this._endDate = endDate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
return this.attr.ID;
|
||||
}
|
||||
|
||||
get class(): string {
|
||||
return this.attr.CLASS;
|
||||
}
|
||||
|
||||
get startDate(): Date {
|
||||
return this._startDate;
|
||||
}
|
||||
|
||||
get endDate(): Date | null {
|
||||
if (this._endDate) {
|
||||
return this._endDate;
|
||||
}
|
||||
const duration = this.duration;
|
||||
if (duration !== null) {
|
||||
return new Date(this._startDate.getTime() + duration * 1000);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
get duration(): number | null {
|
||||
if (DateRangeAttribute.DURATION in this.attr) {
|
||||
const duration = this.attr.decimalFloatingPoint(
|
||||
DateRangeAttribute.DURATION,
|
||||
);
|
||||
if (Number.isFinite(duration)) {
|
||||
return duration;
|
||||
}
|
||||
} else if (this._endDate) {
|
||||
return (this._endDate.getTime() - this._startDate.getTime()) / 1000;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
get plannedDuration(): number | null {
|
||||
if (DateRangeAttribute.PLANNED_DURATION in this.attr) {
|
||||
return this.attr.decimalFloatingPoint(
|
||||
DateRangeAttribute.PLANNED_DURATION,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
get endOnNext(): boolean {
|
||||
return this.attr.bool(DateRangeAttribute.END_ON_NEXT);
|
||||
}
|
||||
|
||||
get isValid(): boolean {
|
||||
return (
|
||||
!!this.id &&
|
||||
!this._badValueForSameId &&
|
||||
Number.isFinite(this.startDate.getTime()) &&
|
||||
(this.duration === null || this.duration >= 0) &&
|
||||
(!this.endOnNext || !!this.class)
|
||||
);
|
||||
}
|
||||
}
|
||||
399
node_modules/hls.js/src/loader/fragment-loader.ts
generated
vendored
Normal file
399
node_modules/hls.js/src/loader/fragment-loader.ts
generated
vendored
Normal file
@@ -0,0 +1,399 @@
|
||||
import { ErrorTypes, ErrorDetails } from '../errors';
|
||||
import { Fragment } from './fragment';
|
||||
import {
|
||||
Loader,
|
||||
LoaderConfiguration,
|
||||
FragmentLoaderContext,
|
||||
} from '../types/loader';
|
||||
import { getLoaderConfigWithoutReties } from '../utils/error-helper';
|
||||
import type { HlsConfig } from '../config';
|
||||
import type { BaseSegment, Part } from './fragment';
|
||||
import type {
|
||||
ErrorData,
|
||||
FragLoadedData,
|
||||
PartsLoadedData,
|
||||
} from '../types/events';
|
||||
|
||||
const MIN_CHUNK_SIZE = Math.pow(2, 17); // 128kb
|
||||
|
||||
export default class FragmentLoader {
|
||||
private readonly config: HlsConfig;
|
||||
private loader: Loader<FragmentLoaderContext> | null = null;
|
||||
private partLoadTimeout: number = -1;
|
||||
|
||||
constructor(config: HlsConfig) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.loader) {
|
||||
this.loader.destroy();
|
||||
this.loader = null;
|
||||
}
|
||||
}
|
||||
|
||||
abort() {
|
||||
if (this.loader) {
|
||||
// Abort the loader for current fragment. Only one may load at any given time
|
||||
this.loader.abort();
|
||||
}
|
||||
}
|
||||
|
||||
load(
|
||||
frag: Fragment,
|
||||
onProgress?: FragmentLoadProgressCallback,
|
||||
): Promise<FragLoadedData> {
|
||||
const url = frag.url;
|
||||
if (!url) {
|
||||
return Promise.reject(
|
||||
new LoadError({
|
||||
type: ErrorTypes.NETWORK_ERROR,
|
||||
details: ErrorDetails.FRAG_LOAD_ERROR,
|
||||
fatal: false,
|
||||
frag,
|
||||
error: new Error(
|
||||
`Fragment does not have a ${url ? 'part list' : 'url'}`,
|
||||
),
|
||||
networkDetails: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
this.abort();
|
||||
|
||||
const config = this.config;
|
||||
const FragmentILoader = config.fLoader;
|
||||
const DefaultILoader = config.loader;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.loader) {
|
||||
this.loader.destroy();
|
||||
}
|
||||
if (frag.gap) {
|
||||
if (frag.tagList.some((tags) => tags[0] === 'GAP')) {
|
||||
reject(createGapLoadError(frag));
|
||||
return;
|
||||
} else {
|
||||
// Reset temporary treatment as GAP tag
|
||||
frag.gap = false;
|
||||
}
|
||||
}
|
||||
const loader =
|
||||
(this.loader =
|
||||
frag.loader =
|
||||
FragmentILoader
|
||||
? new FragmentILoader(config)
|
||||
: (new DefaultILoader(config) as Loader<FragmentLoaderContext>));
|
||||
const loaderContext = createLoaderContext(frag);
|
||||
const loadPolicy = getLoaderConfigWithoutReties(
|
||||
config.fragLoadPolicy.default,
|
||||
);
|
||||
const loaderConfig: LoaderConfiguration = {
|
||||
loadPolicy,
|
||||
timeout: loadPolicy.maxLoadTimeMs,
|
||||
maxRetry: 0,
|
||||
retryDelay: 0,
|
||||
maxRetryDelay: 0,
|
||||
highWaterMark: frag.sn === 'initSegment' ? Infinity : MIN_CHUNK_SIZE,
|
||||
};
|
||||
// Assign frag stats to the loader's stats reference
|
||||
frag.stats = loader.stats;
|
||||
loader.load(loaderContext, loaderConfig, {
|
||||
onSuccess: (response, stats, context, networkDetails) => {
|
||||
this.resetLoader(frag, loader);
|
||||
let payload = response.data as ArrayBuffer;
|
||||
if (context.resetIV && frag.decryptdata) {
|
||||
frag.decryptdata.iv = new Uint8Array(payload.slice(0, 16));
|
||||
payload = payload.slice(16);
|
||||
}
|
||||
resolve({
|
||||
frag,
|
||||
part: null,
|
||||
payload,
|
||||
networkDetails,
|
||||
});
|
||||
},
|
||||
onError: (response, context, networkDetails, stats) => {
|
||||
this.resetLoader(frag, loader);
|
||||
reject(
|
||||
new LoadError({
|
||||
type: ErrorTypes.NETWORK_ERROR,
|
||||
details: ErrorDetails.FRAG_LOAD_ERROR,
|
||||
fatal: false,
|
||||
frag,
|
||||
response: { url, data: undefined, ...response },
|
||||
error: new Error(`HTTP Error ${response.code} ${response.text}`),
|
||||
networkDetails,
|
||||
stats,
|
||||
}),
|
||||
);
|
||||
},
|
||||
onAbort: (stats, context, networkDetails) => {
|
||||
this.resetLoader(frag, loader);
|
||||
reject(
|
||||
new LoadError({
|
||||
type: ErrorTypes.NETWORK_ERROR,
|
||||
details: ErrorDetails.INTERNAL_ABORTED,
|
||||
fatal: false,
|
||||
frag,
|
||||
error: new Error('Aborted'),
|
||||
networkDetails,
|
||||
stats,
|
||||
}),
|
||||
);
|
||||
},
|
||||
onTimeout: (stats, context, networkDetails) => {
|
||||
this.resetLoader(frag, loader);
|
||||
reject(
|
||||
new LoadError({
|
||||
type: ErrorTypes.NETWORK_ERROR,
|
||||
details: ErrorDetails.FRAG_LOAD_TIMEOUT,
|
||||
fatal: false,
|
||||
frag,
|
||||
error: new Error(`Timeout after ${loaderConfig.timeout}ms`),
|
||||
networkDetails,
|
||||
stats,
|
||||
}),
|
||||
);
|
||||
},
|
||||
onProgress: (stats, context, data, networkDetails) => {
|
||||
if (onProgress) {
|
||||
onProgress({
|
||||
frag,
|
||||
part: null,
|
||||
payload: data as ArrayBuffer,
|
||||
networkDetails,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public loadPart(
|
||||
frag: Fragment,
|
||||
part: Part,
|
||||
onProgress: FragmentLoadProgressCallback,
|
||||
): Promise<FragLoadedData> {
|
||||
this.abort();
|
||||
|
||||
const config = this.config;
|
||||
const FragmentILoader = config.fLoader;
|
||||
const DefaultILoader = config.loader;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.loader) {
|
||||
this.loader.destroy();
|
||||
}
|
||||
if (frag.gap || part.gap) {
|
||||
reject(createGapLoadError(frag, part));
|
||||
return;
|
||||
}
|
||||
const loader =
|
||||
(this.loader =
|
||||
frag.loader =
|
||||
FragmentILoader
|
||||
? new FragmentILoader(config)
|
||||
: (new DefaultILoader(config) as Loader<FragmentLoaderContext>));
|
||||
const loaderContext = createLoaderContext(frag, part);
|
||||
// Should we define another load policy for parts?
|
||||
const loadPolicy = getLoaderConfigWithoutReties(
|
||||
config.fragLoadPolicy.default,
|
||||
);
|
||||
const loaderConfig: LoaderConfiguration = {
|
||||
loadPolicy,
|
||||
timeout: loadPolicy.maxLoadTimeMs,
|
||||
maxRetry: 0,
|
||||
retryDelay: 0,
|
||||
maxRetryDelay: 0,
|
||||
highWaterMark: MIN_CHUNK_SIZE,
|
||||
};
|
||||
// Assign part stats to the loader's stats reference
|
||||
part.stats = loader.stats;
|
||||
loader.load(loaderContext, loaderConfig, {
|
||||
onSuccess: (response, stats, context, networkDetails) => {
|
||||
this.resetLoader(frag, loader);
|
||||
this.updateStatsFromPart(frag, part);
|
||||
const partLoadedData: FragLoadedData = {
|
||||
frag,
|
||||
part,
|
||||
payload: response.data as ArrayBuffer,
|
||||
networkDetails,
|
||||
};
|
||||
onProgress(partLoadedData);
|
||||
resolve(partLoadedData);
|
||||
},
|
||||
onError: (response, context, networkDetails, stats) => {
|
||||
this.resetLoader(frag, loader);
|
||||
reject(
|
||||
new LoadError({
|
||||
type: ErrorTypes.NETWORK_ERROR,
|
||||
details: ErrorDetails.FRAG_LOAD_ERROR,
|
||||
fatal: false,
|
||||
frag,
|
||||
part,
|
||||
response: {
|
||||
url: loaderContext.url,
|
||||
data: undefined,
|
||||
...response,
|
||||
},
|
||||
error: new Error(`HTTP Error ${response.code} ${response.text}`),
|
||||
networkDetails,
|
||||
stats,
|
||||
}),
|
||||
);
|
||||
},
|
||||
onAbort: (stats, context, networkDetails) => {
|
||||
frag.stats.aborted = part.stats.aborted;
|
||||
this.resetLoader(frag, loader);
|
||||
reject(
|
||||
new LoadError({
|
||||
type: ErrorTypes.NETWORK_ERROR,
|
||||
details: ErrorDetails.INTERNAL_ABORTED,
|
||||
fatal: false,
|
||||
frag,
|
||||
part,
|
||||
error: new Error('Aborted'),
|
||||
networkDetails,
|
||||
stats,
|
||||
}),
|
||||
);
|
||||
},
|
||||
onTimeout: (stats, context, networkDetails) => {
|
||||
this.resetLoader(frag, loader);
|
||||
reject(
|
||||
new LoadError({
|
||||
type: ErrorTypes.NETWORK_ERROR,
|
||||
details: ErrorDetails.FRAG_LOAD_TIMEOUT,
|
||||
fatal: false,
|
||||
frag,
|
||||
part,
|
||||
error: new Error(`Timeout after ${loaderConfig.timeout}ms`),
|
||||
networkDetails,
|
||||
stats,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private updateStatsFromPart(frag: Fragment, part: Part) {
|
||||
const fragStats = frag.stats;
|
||||
const partStats = part.stats;
|
||||
const partTotal = partStats.total;
|
||||
fragStats.loaded += partStats.loaded;
|
||||
if (partTotal) {
|
||||
const estTotalParts = Math.round(frag.duration / part.duration);
|
||||
const estLoadedParts = Math.min(
|
||||
Math.round(fragStats.loaded / partTotal),
|
||||
estTotalParts,
|
||||
);
|
||||
const estRemainingParts = estTotalParts - estLoadedParts;
|
||||
const estRemainingBytes =
|
||||
estRemainingParts * Math.round(fragStats.loaded / estLoadedParts);
|
||||
fragStats.total = fragStats.loaded + estRemainingBytes;
|
||||
} else {
|
||||
fragStats.total = Math.max(fragStats.loaded, fragStats.total);
|
||||
}
|
||||
const fragLoading = fragStats.loading;
|
||||
const partLoading = partStats.loading;
|
||||
if (fragLoading.start) {
|
||||
// add to fragment loader latency
|
||||
fragLoading.first += partLoading.first - partLoading.start;
|
||||
} else {
|
||||
fragLoading.start = partLoading.start;
|
||||
fragLoading.first = partLoading.first;
|
||||
}
|
||||
fragLoading.end = partLoading.end;
|
||||
}
|
||||
|
||||
private resetLoader(frag: Fragment, loader: Loader<FragmentLoaderContext>) {
|
||||
frag.loader = null;
|
||||
if (this.loader === loader) {
|
||||
self.clearTimeout(this.partLoadTimeout);
|
||||
this.loader = null;
|
||||
}
|
||||
loader.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
function createLoaderContext(
|
||||
frag: Fragment,
|
||||
part: Part | null = null,
|
||||
): FragmentLoaderContext {
|
||||
const segment: BaseSegment = part || frag;
|
||||
const loaderContext: FragmentLoaderContext = {
|
||||
frag,
|
||||
part,
|
||||
responseType: 'arraybuffer',
|
||||
url: segment.url,
|
||||
headers: {},
|
||||
rangeStart: 0,
|
||||
rangeEnd: 0,
|
||||
};
|
||||
const start = segment.byteRangeStartOffset as number;
|
||||
const end = segment.byteRangeEndOffset as number;
|
||||
if (Number.isFinite(start) && Number.isFinite(end)) {
|
||||
let byteRangeStart = start;
|
||||
let byteRangeEnd = end;
|
||||
if (frag.sn === 'initSegment' && frag.decryptdata?.method === 'AES-128') {
|
||||
// MAP segment encrypted with method 'AES-128', when served with HTTP Range,
|
||||
// has the unencrypted size specified in the range.
|
||||
// Ref: https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-08#section-6.3.6
|
||||
const fragmentLen = end - start;
|
||||
if (fragmentLen % 16) {
|
||||
byteRangeEnd = end + (16 - (fragmentLen % 16));
|
||||
}
|
||||
if (start !== 0) {
|
||||
loaderContext.resetIV = true;
|
||||
byteRangeStart = start - 16;
|
||||
}
|
||||
}
|
||||
loaderContext.rangeStart = byteRangeStart;
|
||||
loaderContext.rangeEnd = byteRangeEnd;
|
||||
}
|
||||
return loaderContext;
|
||||
}
|
||||
|
||||
function createGapLoadError(frag: Fragment, part?: Part): LoadError {
|
||||
const error = new Error(`GAP ${frag.gap ? 'tag' : 'attribute'} found`);
|
||||
const errorData: FragLoadFailResult = {
|
||||
type: ErrorTypes.MEDIA_ERROR,
|
||||
details: ErrorDetails.FRAG_GAP,
|
||||
fatal: false,
|
||||
frag,
|
||||
error,
|
||||
networkDetails: null,
|
||||
};
|
||||
if (part) {
|
||||
errorData.part = part;
|
||||
}
|
||||
(part ? part : frag).stats.aborted = true;
|
||||
return new LoadError(errorData);
|
||||
}
|
||||
|
||||
export class LoadError extends Error {
|
||||
public readonly data: FragLoadFailResult;
|
||||
constructor(data: FragLoadFailResult) {
|
||||
super(data.error.message);
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
export interface FragLoadFailResult extends ErrorData {
|
||||
frag: Fragment;
|
||||
part?: Part;
|
||||
response?: {
|
||||
data: any;
|
||||
// error status code
|
||||
code: number;
|
||||
// error description
|
||||
text: string;
|
||||
url: string;
|
||||
};
|
||||
networkDetails: any;
|
||||
}
|
||||
|
||||
export type FragmentLoadProgressCallback = (
|
||||
result: FragLoadedData | PartsLoadedData,
|
||||
) => void;
|
||||
320
node_modules/hls.js/src/loader/fragment.ts
generated
vendored
Normal file
320
node_modules/hls.js/src/loader/fragment.ts
generated
vendored
Normal file
@@ -0,0 +1,320 @@
|
||||
import { buildAbsoluteURL } from 'url-toolkit';
|
||||
import { LevelKey } from './level-key';
|
||||
import { LoadStats } from './load-stats';
|
||||
import { AttrList } from '../utils/attr-list';
|
||||
import type {
|
||||
FragmentLoaderContext,
|
||||
KeyLoaderContext,
|
||||
Loader,
|
||||
PlaylistLevelType,
|
||||
} from '../types/loader';
|
||||
import type { KeySystemFormats } from '../utils/mediakeys-helper';
|
||||
|
||||
export const enum ElementaryStreamTypes {
|
||||
AUDIO = 'audio',
|
||||
VIDEO = 'video',
|
||||
AUDIOVIDEO = 'audiovideo',
|
||||
}
|
||||
|
||||
export interface ElementaryStreamInfo {
|
||||
startPTS: number;
|
||||
endPTS: number;
|
||||
startDTS: number;
|
||||
endDTS: number;
|
||||
partial?: boolean;
|
||||
}
|
||||
|
||||
export type ElementaryStreams = Record<
|
||||
ElementaryStreamTypes,
|
||||
ElementaryStreamInfo | null
|
||||
>;
|
||||
|
||||
export class BaseSegment {
|
||||
private _byteRange: [number, number] | null = null;
|
||||
private _url: string | null = null;
|
||||
|
||||
// baseurl is the URL to the playlist
|
||||
public readonly baseurl: string;
|
||||
// relurl is the portion of the URL that comes from inside the playlist.
|
||||
public relurl?: string;
|
||||
// Holds the types of data this fragment supports
|
||||
public elementaryStreams: ElementaryStreams = {
|
||||
[ElementaryStreamTypes.AUDIO]: null,
|
||||
[ElementaryStreamTypes.VIDEO]: null,
|
||||
[ElementaryStreamTypes.AUDIOVIDEO]: null,
|
||||
};
|
||||
|
||||
constructor(baseurl: string) {
|
||||
this.baseurl = baseurl;
|
||||
}
|
||||
|
||||
// setByteRange converts a EXT-X-BYTERANGE attribute into a two element array
|
||||
setByteRange(value: string, previous?: BaseSegment) {
|
||||
const params = value.split('@', 2);
|
||||
let start: number;
|
||||
if (params.length === 1) {
|
||||
start = previous?.byteRangeEndOffset || 0;
|
||||
} else {
|
||||
start = parseInt(params[1]);
|
||||
}
|
||||
this._byteRange = [start, parseInt(params[0]) + start];
|
||||
}
|
||||
|
||||
get byteRange(): [number, number] | [] {
|
||||
if (!this._byteRange) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this._byteRange;
|
||||
}
|
||||
|
||||
get byteRangeStartOffset(): number | undefined {
|
||||
return this.byteRange[0];
|
||||
}
|
||||
|
||||
get byteRangeEndOffset(): number | undefined {
|
||||
return this.byteRange[1];
|
||||
}
|
||||
|
||||
get url(): string {
|
||||
if (!this._url && this.baseurl && this.relurl) {
|
||||
this._url = buildAbsoluteURL(this.baseurl, this.relurl, {
|
||||
alwaysNormalize: true,
|
||||
});
|
||||
}
|
||||
return this._url || '';
|
||||
}
|
||||
|
||||
set url(value: string) {
|
||||
this._url = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Object representing parsed data from an HLS Segment. Found in {@link hls.js#LevelDetails.fragments}.
|
||||
*/
|
||||
export class Fragment extends BaseSegment {
|
||||
private _decryptdata: LevelKey | null = null;
|
||||
|
||||
public rawProgramDateTime: string | null = null;
|
||||
public programDateTime: number | null = null;
|
||||
public tagList: Array<string[]> = [];
|
||||
|
||||
// EXTINF has to be present for a m3u8 to be considered valid
|
||||
public duration: number = 0;
|
||||
// sn notates the sequence number for a segment, and if set to a string can be 'initSegment'
|
||||
public sn: number | 'initSegment' = 0;
|
||||
// levelkeys are the EXT-X-KEY tags that apply to this segment for decryption
|
||||
// core difference from the private field _decryptdata is the lack of the initialized IV
|
||||
// _decryptdata will set the IV for this segment based on the segment number in the fragment
|
||||
public levelkeys?: { [key: string]: LevelKey };
|
||||
// A string representing the fragment type
|
||||
public readonly type: PlaylistLevelType;
|
||||
// A reference to the loader. Set while the fragment is loading, and removed afterwards. Used to abort fragment loading
|
||||
public loader: Loader<FragmentLoaderContext> | null = null;
|
||||
// A reference to the key loader. Set while the key is loading, and removed afterwards. Used to abort key loading
|
||||
public keyLoader: Loader<KeyLoaderContext> | null = null;
|
||||
// The level/track index to which the fragment belongs
|
||||
public level: number = -1;
|
||||
// The continuity counter of the fragment
|
||||
public cc: number = 0;
|
||||
// The starting Presentation Time Stamp (PTS) of the fragment. Set after transmux complete.
|
||||
public startPTS?: number;
|
||||
// The ending Presentation Time Stamp (PTS) of the fragment. Set after transmux complete.
|
||||
public endPTS?: number;
|
||||
// The starting Decode Time Stamp (DTS) of the fragment. Set after transmux complete.
|
||||
public startDTS!: number;
|
||||
// The ending Decode Time Stamp (DTS) of the fragment. Set after transmux complete.
|
||||
public endDTS!: number;
|
||||
// The start time of the fragment, as listed in the manifest. Updated after transmux complete.
|
||||
public start: number = 0;
|
||||
// Set by `updateFragPTSDTS` in level-helper
|
||||
public deltaPTS?: number;
|
||||
// The maximum starting Presentation Time Stamp (audio/video PTS) of the fragment. Set after transmux complete.
|
||||
public maxStartPTS?: number;
|
||||
// The minimum ending Presentation Time Stamp (audio/video PTS) of the fragment. Set after transmux complete.
|
||||
public minEndPTS?: number;
|
||||
// Load/parse timing information
|
||||
public stats: LoadStats = new LoadStats();
|
||||
// Init Segment bytes (unset for media segments)
|
||||
public data?: Uint8Array;
|
||||
// A flag indicating whether the segment was downloaded in order to test bitrate, and was not buffered
|
||||
public bitrateTest: boolean = false;
|
||||
// #EXTINF segment title
|
||||
public title: string | null = null;
|
||||
// The Media Initialization Section for this segment
|
||||
public initSegment: Fragment | null = null;
|
||||
// Fragment is the last fragment in the media playlist
|
||||
public endList?: boolean;
|
||||
// Fragment is marked by an EXT-X-GAP tag indicating that it does not contain media data and should not be loaded
|
||||
public gap?: boolean;
|
||||
// Deprecated
|
||||
public urlId: number = 0;
|
||||
|
||||
constructor(type: PlaylistLevelType, baseurl: string) {
|
||||
super(baseurl);
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
get decryptdata(): LevelKey | null {
|
||||
const { levelkeys } = this;
|
||||
if (!levelkeys && !this._decryptdata) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this._decryptdata && this.levelkeys && !this.levelkeys.NONE) {
|
||||
const key = this.levelkeys.identity;
|
||||
if (key) {
|
||||
this._decryptdata = key.getDecryptData(this.sn);
|
||||
} else {
|
||||
const keyFormats = Object.keys(this.levelkeys);
|
||||
if (keyFormats.length === 1) {
|
||||
return (this._decryptdata = this.levelkeys[
|
||||
keyFormats[0]
|
||||
].getDecryptData(this.sn));
|
||||
} else {
|
||||
// Multiple keys. key-loader to call Fragment.setKeyFormat based on selected key-system.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this._decryptdata;
|
||||
}
|
||||
|
||||
get end(): number {
|
||||
return this.start + this.duration;
|
||||
}
|
||||
|
||||
get endProgramDateTime() {
|
||||
if (this.programDateTime === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!Number.isFinite(this.programDateTime)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const duration = !Number.isFinite(this.duration) ? 0 : this.duration;
|
||||
|
||||
return this.programDateTime + duration * 1000;
|
||||
}
|
||||
|
||||
get encrypted() {
|
||||
// At the m3u8-parser level we need to add support for manifest signalled keyformats
|
||||
// when we want the fragment to start reporting that it is encrypted.
|
||||
// Currently, keyFormat will only be set for identity keys
|
||||
if (this._decryptdata?.encrypted) {
|
||||
return true;
|
||||
} else if (this.levelkeys) {
|
||||
const keyFormats = Object.keys(this.levelkeys);
|
||||
const len = keyFormats.length;
|
||||
if (len > 1 || (len === 1 && this.levelkeys[keyFormats[0]].encrypted)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
setKeyFormat(keyFormat: KeySystemFormats) {
|
||||
if (this.levelkeys) {
|
||||
const key = this.levelkeys[keyFormat];
|
||||
if (key && !this._decryptdata) {
|
||||
this._decryptdata = key.getDecryptData(this.sn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abortRequests(): void {
|
||||
this.loader?.abort();
|
||||
this.keyLoader?.abort();
|
||||
}
|
||||
|
||||
setElementaryStreamInfo(
|
||||
type: ElementaryStreamTypes,
|
||||
startPTS: number,
|
||||
endPTS: number,
|
||||
startDTS: number,
|
||||
endDTS: number,
|
||||
partial: boolean = false,
|
||||
) {
|
||||
const { elementaryStreams } = this;
|
||||
const info = elementaryStreams[type];
|
||||
if (!info) {
|
||||
elementaryStreams[type] = {
|
||||
startPTS,
|
||||
endPTS,
|
||||
startDTS,
|
||||
endDTS,
|
||||
partial,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
info.startPTS = Math.min(info.startPTS, startPTS);
|
||||
info.endPTS = Math.max(info.endPTS, endPTS);
|
||||
info.startDTS = Math.min(info.startDTS, startDTS);
|
||||
info.endDTS = Math.max(info.endDTS, endDTS);
|
||||
}
|
||||
|
||||
clearElementaryStreamInfo() {
|
||||
const { elementaryStreams } = this;
|
||||
elementaryStreams[ElementaryStreamTypes.AUDIO] = null;
|
||||
elementaryStreams[ElementaryStreamTypes.VIDEO] = null;
|
||||
elementaryStreams[ElementaryStreamTypes.AUDIOVIDEO] = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Object representing parsed data from an HLS Partial Segment. Found in {@link hls.js#LevelDetails.partList}.
|
||||
*/
|
||||
export class Part extends BaseSegment {
|
||||
public readonly fragOffset: number = 0;
|
||||
public readonly duration: number = 0;
|
||||
public readonly gap: boolean = false;
|
||||
public readonly independent: boolean = false;
|
||||
public readonly relurl: string;
|
||||
public readonly fragment: Fragment;
|
||||
public readonly index: number;
|
||||
public stats: LoadStats = new LoadStats();
|
||||
|
||||
constructor(
|
||||
partAttrs: AttrList,
|
||||
frag: Fragment,
|
||||
baseurl: string,
|
||||
index: number,
|
||||
previous?: Part,
|
||||
) {
|
||||
super(baseurl);
|
||||
this.duration = partAttrs.decimalFloatingPoint('DURATION');
|
||||
this.gap = partAttrs.bool('GAP');
|
||||
this.independent = partAttrs.bool('INDEPENDENT');
|
||||
this.relurl = partAttrs.enumeratedString('URI') as string;
|
||||
this.fragment = frag;
|
||||
this.index = index;
|
||||
const byteRange = partAttrs.enumeratedString('BYTERANGE');
|
||||
if (byteRange) {
|
||||
this.setByteRange(byteRange, previous);
|
||||
}
|
||||
if (previous) {
|
||||
this.fragOffset = previous.fragOffset + previous.duration;
|
||||
}
|
||||
}
|
||||
|
||||
get start(): number {
|
||||
return this.fragment.start + this.fragOffset;
|
||||
}
|
||||
|
||||
get end(): number {
|
||||
return this.start + this.duration;
|
||||
}
|
||||
|
||||
get loaded(): boolean {
|
||||
const { elementaryStreams } = this;
|
||||
return !!(
|
||||
elementaryStreams.audio ||
|
||||
elementaryStreams.video ||
|
||||
elementaryStreams.audiovideo
|
||||
);
|
||||
}
|
||||
}
|
||||
356
node_modules/hls.js/src/loader/key-loader.ts
generated
vendored
Normal file
356
node_modules/hls.js/src/loader/key-loader.ts
generated
vendored
Normal file
@@ -0,0 +1,356 @@
|
||||
import { ErrorTypes, ErrorDetails } from '../errors';
|
||||
import {
|
||||
LoaderStats,
|
||||
LoaderResponse,
|
||||
LoaderConfiguration,
|
||||
LoaderCallbacks,
|
||||
Loader,
|
||||
KeyLoaderContext,
|
||||
PlaylistLevelType,
|
||||
} from '../types/loader';
|
||||
import { LoadError } from './fragment-loader';
|
||||
import type { HlsConfig } from '../config';
|
||||
import type { Fragment } from '../loader/fragment';
|
||||
import type { ComponentAPI } from '../types/component-api';
|
||||
import type { KeyLoadedData } from '../types/events';
|
||||
import type { LevelKey } from './level-key';
|
||||
import type EMEController from '../controller/eme-controller';
|
||||
import type { MediaKeySessionContext } from '../controller/eme-controller';
|
||||
import type { KeySystemFormats } from '../utils/mediakeys-helper';
|
||||
|
||||
export interface KeyLoaderInfo {
|
||||
decryptdata: LevelKey;
|
||||
keyLoadPromise: Promise<KeyLoadedData> | null;
|
||||
loader: Loader<KeyLoaderContext> | null;
|
||||
mediaKeySessionContext: MediaKeySessionContext | null;
|
||||
}
|
||||
export default class KeyLoader implements ComponentAPI {
|
||||
private readonly config: HlsConfig;
|
||||
public keyUriToKeyInfo: { [keyuri: string]: KeyLoaderInfo } = {};
|
||||
public emeController: EMEController | null = null;
|
||||
|
||||
constructor(config: HlsConfig) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
abort(type?: PlaylistLevelType) {
|
||||
for (const uri in this.keyUriToKeyInfo) {
|
||||
const loader = this.keyUriToKeyInfo[uri].loader;
|
||||
if (loader) {
|
||||
if (type && type !== loader.context?.frag.type) {
|
||||
return;
|
||||
}
|
||||
loader.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
detach() {
|
||||
for (const uri in this.keyUriToKeyInfo) {
|
||||
const keyInfo = this.keyUriToKeyInfo[uri];
|
||||
// Remove cached EME keys on detach
|
||||
if (
|
||||
keyInfo.mediaKeySessionContext ||
|
||||
keyInfo.decryptdata.isCommonEncryption
|
||||
) {
|
||||
delete this.keyUriToKeyInfo[uri];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.detach();
|
||||
for (const uri in this.keyUriToKeyInfo) {
|
||||
const loader = this.keyUriToKeyInfo[uri].loader;
|
||||
if (loader) {
|
||||
loader.destroy();
|
||||
}
|
||||
}
|
||||
this.keyUriToKeyInfo = {};
|
||||
}
|
||||
|
||||
createKeyLoadError(
|
||||
frag: Fragment,
|
||||
details: ErrorDetails = ErrorDetails.KEY_LOAD_ERROR,
|
||||
error: Error,
|
||||
networkDetails?: any,
|
||||
response?: { url: string; data: undefined; code: number; text: string },
|
||||
): LoadError {
|
||||
return new LoadError({
|
||||
type: ErrorTypes.NETWORK_ERROR,
|
||||
details,
|
||||
fatal: false,
|
||||
frag,
|
||||
response,
|
||||
error,
|
||||
networkDetails,
|
||||
});
|
||||
}
|
||||
|
||||
loadClear(
|
||||
loadingFrag: Fragment,
|
||||
encryptedFragments: Fragment[],
|
||||
): void | Promise<void> {
|
||||
if (this.emeController && this.config.emeEnabled) {
|
||||
// access key-system with nearest key on start (loaidng frag is unencrypted)
|
||||
const { sn, cc } = loadingFrag;
|
||||
for (let i = 0; i < encryptedFragments.length; i++) {
|
||||
const frag = encryptedFragments[i];
|
||||
if (
|
||||
cc <= frag.cc &&
|
||||
(sn === 'initSegment' || frag.sn === 'initSegment' || sn < frag.sn)
|
||||
) {
|
||||
this.emeController
|
||||
.selectKeySystemFormat(frag)
|
||||
.then((keySystemFormat) => {
|
||||
frag.setKeyFormat(keySystemFormat);
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
load(frag: Fragment): Promise<KeyLoadedData> {
|
||||
if (!frag.decryptdata && frag.encrypted && this.emeController) {
|
||||
// Multiple keys, but none selected, resolve in eme-controller
|
||||
return this.emeController
|
||||
.selectKeySystemFormat(frag)
|
||||
.then((keySystemFormat) => {
|
||||
return this.loadInternal(frag, keySystemFormat);
|
||||
});
|
||||
}
|
||||
|
||||
return this.loadInternal(frag);
|
||||
}
|
||||
|
||||
loadInternal(
|
||||
frag: Fragment,
|
||||
keySystemFormat?: KeySystemFormats,
|
||||
): Promise<KeyLoadedData> {
|
||||
if (keySystemFormat) {
|
||||
frag.setKeyFormat(keySystemFormat);
|
||||
}
|
||||
const decryptdata = frag.decryptdata;
|
||||
if (!decryptdata) {
|
||||
const error = new Error(
|
||||
keySystemFormat
|
||||
? `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, error),
|
||||
);
|
||||
}
|
||||
const uri = decryptdata.uri;
|
||||
if (!uri) {
|
||||
return Promise.reject(
|
||||
this.createKeyLoadError(
|
||||
frag,
|
||||
ErrorDetails.KEY_LOAD_ERROR,
|
||||
new Error(`Invalid key URI: "${uri}"`),
|
||||
),
|
||||
);
|
||||
}
|
||||
let keyInfo = this.keyUriToKeyInfo[uri];
|
||||
|
||||
if (keyInfo?.decryptdata.key) {
|
||||
decryptdata.key = keyInfo.decryptdata.key;
|
||||
return Promise.resolve({ frag, keyInfo });
|
||||
}
|
||||
// Return key load promise as long as it does not have a mediakey session with an unusable key status
|
||||
if (keyInfo?.keyLoadPromise) {
|
||||
switch (keyInfo.mediaKeySessionContext?.keyStatus) {
|
||||
case undefined:
|
||||
case 'status-pending':
|
||||
case 'usable':
|
||||
case 'usable-in-future':
|
||||
return keyInfo.keyLoadPromise.then((keyLoadedData) => {
|
||||
// Return the correct fragment with updated decryptdata key and loaded keyInfo
|
||||
decryptdata.key = keyLoadedData.keyInfo.decryptdata.key;
|
||||
return { frag, keyInfo };
|
||||
});
|
||||
}
|
||||
// If we have a key session and status and it is not pending or usable, continue
|
||||
// This will go back to the eme-controller for expired keys to get a new keyLoadPromise
|
||||
}
|
||||
|
||||
// Load the key or return the loading promise
|
||||
keyInfo = this.keyUriToKeyInfo[uri] = {
|
||||
decryptdata,
|
||||
keyLoadPromise: null,
|
||||
loader: null,
|
||||
mediaKeySessionContext: null,
|
||||
};
|
||||
|
||||
switch (decryptdata.method) {
|
||||
case 'ISO-23001-7':
|
||||
case 'SAMPLE-AES':
|
||||
case 'SAMPLE-AES-CENC':
|
||||
case 'SAMPLE-AES-CTR':
|
||||
if (decryptdata.keyFormat === 'identity') {
|
||||
// loadKeyHTTP handles http(s) and data URLs
|
||||
return this.loadKeyHTTP(keyInfo, frag);
|
||||
}
|
||||
return this.loadKeyEME(keyInfo, frag);
|
||||
case 'AES-128':
|
||||
return this.loadKeyHTTP(keyInfo, frag);
|
||||
default:
|
||||
return Promise.reject(
|
||||
this.createKeyLoadError(
|
||||
frag,
|
||||
ErrorDetails.KEY_LOAD_ERROR,
|
||||
new Error(
|
||||
`Key supplied with unsupported METHOD: "${decryptdata.method}"`,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
loadKeyEME(keyInfo: KeyLoaderInfo, frag: Fragment): Promise<KeyLoadedData> {
|
||||
const keyLoadedData: KeyLoadedData = { frag, keyInfo };
|
||||
if (this.emeController && this.config.emeEnabled) {
|
||||
const keySessionContextPromise =
|
||||
this.emeController.loadKey(keyLoadedData);
|
||||
if (keySessionContextPromise) {
|
||||
return (keyInfo.keyLoadPromise = keySessionContextPromise.then(
|
||||
(keySessionContext) => {
|
||||
keyInfo.mediaKeySessionContext = keySessionContext;
|
||||
return keyLoadedData;
|
||||
},
|
||||
)).catch((error) => {
|
||||
// Remove promise for license renewal or retry
|
||||
keyInfo.keyLoadPromise = null;
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
}
|
||||
return Promise.resolve(keyLoadedData);
|
||||
}
|
||||
|
||||
loadKeyHTTP(keyInfo: KeyLoaderInfo, frag: Fragment): Promise<KeyLoadedData> {
|
||||
const config = this.config;
|
||||
const Loader = config.loader;
|
||||
const keyLoader = new Loader(config) as Loader<KeyLoaderContext>;
|
||||
frag.keyLoader = keyInfo.loader = keyLoader;
|
||||
|
||||
return (keyInfo.keyLoadPromise = new Promise((resolve, reject) => {
|
||||
const loaderContext: KeyLoaderContext = {
|
||||
keyInfo,
|
||||
frag,
|
||||
responseType: 'arraybuffer',
|
||||
url: keyInfo.decryptdata.uri,
|
||||
};
|
||||
|
||||
// maxRetry is 0 so that instead of retrying the same key on the same variant multiple times,
|
||||
// key-loader will trigger an error and rely on stream-controller to handle retry logic.
|
||||
// this will also align retry logic with fragment-loader
|
||||
const loadPolicy = config.keyLoadPolicy.default;
|
||||
const loaderConfig: LoaderConfiguration = {
|
||||
loadPolicy,
|
||||
timeout: loadPolicy.maxLoadTimeMs,
|
||||
maxRetry: 0,
|
||||
retryDelay: 0,
|
||||
maxRetryDelay: 0,
|
||||
};
|
||||
|
||||
const loaderCallbacks: LoaderCallbacks<KeyLoaderContext> = {
|
||||
onSuccess: (
|
||||
response: LoaderResponse,
|
||||
stats: LoaderStats,
|
||||
context: KeyLoaderContext,
|
||||
networkDetails: any,
|
||||
) => {
|
||||
const { frag, keyInfo, url: uri } = context;
|
||||
if (!frag.decryptdata || keyInfo !== this.keyUriToKeyInfo[uri]) {
|
||||
return reject(
|
||||
this.createKeyLoadError(
|
||||
frag,
|
||||
ErrorDetails.KEY_LOAD_ERROR,
|
||||
new Error('after key load, decryptdata unset or changed'),
|
||||
networkDetails,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
keyInfo.decryptdata.key = frag.decryptdata.key = new Uint8Array(
|
||||
response.data as ArrayBuffer,
|
||||
);
|
||||
|
||||
// detach fragment key loader on load success
|
||||
frag.keyLoader = null;
|
||||
keyInfo.loader = null;
|
||||
resolve({ frag, keyInfo });
|
||||
},
|
||||
|
||||
onError: (
|
||||
response: { code: number; text: string },
|
||||
context: KeyLoaderContext,
|
||||
networkDetails: any,
|
||||
stats: LoaderStats,
|
||||
) => {
|
||||
this.resetLoader(context);
|
||||
reject(
|
||||
this.createKeyLoadError(
|
||||
frag,
|
||||
ErrorDetails.KEY_LOAD_ERROR,
|
||||
new Error(
|
||||
`HTTP Error ${response.code} loading key ${response.text}`,
|
||||
),
|
||||
networkDetails,
|
||||
{ url: loaderContext.url, data: undefined, ...response },
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
onTimeout: (
|
||||
stats: LoaderStats,
|
||||
context: KeyLoaderContext,
|
||||
networkDetails: any,
|
||||
) => {
|
||||
this.resetLoader(context);
|
||||
reject(
|
||||
this.createKeyLoadError(
|
||||
frag,
|
||||
ErrorDetails.KEY_LOAD_TIMEOUT,
|
||||
new Error('key loading timed out'),
|
||||
networkDetails,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
onAbort: (
|
||||
stats: LoaderStats,
|
||||
context: KeyLoaderContext,
|
||||
networkDetails: any,
|
||||
) => {
|
||||
this.resetLoader(context);
|
||||
reject(
|
||||
this.createKeyLoadError(
|
||||
frag,
|
||||
ErrorDetails.INTERNAL_ABORTED,
|
||||
new Error('key loading aborted'),
|
||||
networkDetails,
|
||||
),
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
keyLoader.load(loaderContext, loaderConfig, loaderCallbacks);
|
||||
}));
|
||||
}
|
||||
|
||||
private resetLoader(context: KeyLoaderContext) {
|
||||
const { frag, keyInfo, url: uri } = context;
|
||||
const loader = keyInfo.loader;
|
||||
if (frag.keyLoader === loader) {
|
||||
frag.keyLoader = null;
|
||||
keyInfo.loader = null;
|
||||
}
|
||||
delete this.keyUriToKeyInfo[uri];
|
||||
if (loader) {
|
||||
loader.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
155
node_modules/hls.js/src/loader/level-details.ts
generated
vendored
Normal file
155
node_modules/hls.js/src/loader/level-details.ts
generated
vendored
Normal file
@@ -0,0 +1,155 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* Object representing parsed data from an HLS Media Playlist. Found in {@link hls.js#Level.details}.
|
||||
*/
|
||||
export class LevelDetails {
|
||||
public PTSKnown: boolean = false;
|
||||
public alignedSliding: boolean = false;
|
||||
public averagetargetduration?: number;
|
||||
public endCC: number = 0;
|
||||
public endSN: number = 0;
|
||||
public fragments: Fragment[];
|
||||
public fragmentHint?: Fragment;
|
||||
public partList: Part[] | null = null;
|
||||
public dateRanges: Record<string, DateRange>;
|
||||
public live: boolean = true;
|
||||
public ageHeader: number = 0;
|
||||
public advancedDateTime?: number;
|
||||
public updated: boolean = true;
|
||||
public advanced: boolean = true;
|
||||
public availabilityDelay?: number; // Manifest reload synchronization
|
||||
public misses: number = 0;
|
||||
public startCC: number = 0;
|
||||
public startSN: number = 0;
|
||||
public startTimeOffset: number | null = null;
|
||||
public targetduration: number = 0;
|
||||
public totalduration: number = 0;
|
||||
public type: string | null = null;
|
||||
public url: string;
|
||||
public m3u8: string = '';
|
||||
public version: number | null = null;
|
||||
public canBlockReload: boolean = false;
|
||||
public canSkipUntil: number = 0;
|
||||
public canSkipDateRanges: boolean = false;
|
||||
public skippedSegments: number = 0;
|
||||
public recentlyRemovedDateranges?: string[];
|
||||
public partHoldBack: number = 0;
|
||||
public holdBack: number = 0;
|
||||
public partTarget: number = 0;
|
||||
public preloadHint?: AttrList;
|
||||
public renditionReports?: AttrList[];
|
||||
public tuneInGoal: number = 0;
|
||||
public deltaUpdateFailed?: boolean;
|
||||
public driftStartTime: number = 0;
|
||||
public driftEndTime: number = 0;
|
||||
public driftStart: number = 0;
|
||||
public driftEnd: number = 0;
|
||||
public encryptedFragments: Fragment[];
|
||||
public playlistParsingError: Error | null = null;
|
||||
public variableList: VariableMap | null = null;
|
||||
public hasVariableRefs = false;
|
||||
|
||||
constructor(baseUrl: string) {
|
||||
this.fragments = [];
|
||||
this.encryptedFragments = [];
|
||||
this.dateRanges = {};
|
||||
this.url = baseUrl;
|
||||
}
|
||||
|
||||
reloaded(previous: LevelDetails | undefined) {
|
||||
if (!previous) {
|
||||
this.advanced = true;
|
||||
this.updated = true;
|
||||
return;
|
||||
}
|
||||
const partSnDiff = this.lastPartSn - previous.lastPartSn;
|
||||
const partIndexDiff = this.lastPartIndex - previous.lastPartIndex;
|
||||
this.updated =
|
||||
this.endSN !== previous.endSN ||
|
||||
!!partIndexDiff ||
|
||||
!!partSnDiff ||
|
||||
!this.live;
|
||||
this.advanced =
|
||||
this.endSN > previous.endSN ||
|
||||
partSnDiff > 0 ||
|
||||
(partSnDiff === 0 && partIndexDiff > 0);
|
||||
if (this.updated || this.advanced) {
|
||||
this.misses = Math.floor(previous.misses * 0.6);
|
||||
} else {
|
||||
this.misses = previous.misses + 1;
|
||||
}
|
||||
this.availabilityDelay = previous.availabilityDelay;
|
||||
}
|
||||
|
||||
get hasProgramDateTime(): boolean {
|
||||
if (this.fragments.length) {
|
||||
return Number.isFinite(
|
||||
this.fragments[this.fragments.length - 1].programDateTime as number,
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
get levelTargetDuration(): number {
|
||||
return (
|
||||
this.averagetargetduration ||
|
||||
this.targetduration ||
|
||||
DEFAULT_TARGET_DURATION
|
||||
);
|
||||
}
|
||||
|
||||
get drift(): number {
|
||||
const runTime = this.driftEndTime - this.driftStartTime;
|
||||
if (runTime > 0) {
|
||||
const runDuration = this.driftEnd - this.driftStart;
|
||||
return (runDuration * 1000) / runTime;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
get edge(): number {
|
||||
return this.partEnd || this.fragmentEnd;
|
||||
}
|
||||
|
||||
get partEnd(): number {
|
||||
if (this.partList?.length) {
|
||||
return this.partList[this.partList.length - 1].end;
|
||||
}
|
||||
return this.fragmentEnd;
|
||||
}
|
||||
|
||||
get fragmentEnd(): number {
|
||||
if (this.fragments?.length) {
|
||||
return this.fragments[this.fragments.length - 1].end;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
get age(): number {
|
||||
if (this.advancedDateTime) {
|
||||
return Math.max(Date.now() - this.advancedDateTime, 0) / 1000;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
get lastPartIndex(): number {
|
||||
if (this.partList?.length) {
|
||||
return this.partList[this.partList.length - 1].index;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
get lastPartSn(): number {
|
||||
if (this.partList?.length) {
|
||||
return this.partList[this.partList.length - 1].fragment.sn as number;
|
||||
}
|
||||
return this.endSN;
|
||||
}
|
||||
}
|
||||
210
node_modules/hls.js/src/loader/level-key.ts
generated
vendored
Normal file
210
node_modules/hls.js/src/loader/level-key.ts
generated
vendored
Normal file
@@ -0,0 +1,210 @@
|
||||
import {
|
||||
changeEndianness,
|
||||
convertDataUriToArrayBytes,
|
||||
} from '../utils/keysystem-util';
|
||||
import { KeySystemFormats } from '../utils/mediakeys-helper';
|
||||
import { mp4pssh } from '../utils/mp4-tools';
|
||||
import { logger } from '../utils/logger';
|
||||
import { base64Decode } from '../utils/numeric-encoding-utils';
|
||||
|
||||
let keyUriToKeyIdMap: { [uri: string]: Uint8Array } = {};
|
||||
|
||||
export interface DecryptData {
|
||||
uri: string;
|
||||
method: string;
|
||||
keyFormat: string;
|
||||
keyFormatVersions: number[];
|
||||
iv: Uint8Array | null;
|
||||
key: Uint8Array | null;
|
||||
keyId: Uint8Array | null;
|
||||
pssh: Uint8Array | null;
|
||||
encrypted: boolean;
|
||||
isCommonEncryption: boolean;
|
||||
}
|
||||
|
||||
export class LevelKey implements DecryptData {
|
||||
public readonly uri: string;
|
||||
public readonly method: string;
|
||||
public readonly keyFormat: string;
|
||||
public readonly keyFormatVersions: number[];
|
||||
public readonly encrypted: boolean;
|
||||
public readonly isCommonEncryption: boolean;
|
||||
public iv: Uint8Array | null = null;
|
||||
public key: Uint8Array | null = null;
|
||||
public keyId: Uint8Array | null = null;
|
||||
public pssh: Uint8Array | null = null;
|
||||
|
||||
static clearKeyUriToKeyIdMap() {
|
||||
keyUriToKeyIdMap = {};
|
||||
}
|
||||
|
||||
constructor(
|
||||
method: string,
|
||||
uri: string,
|
||||
format: string,
|
||||
formatversions: number[] = [1],
|
||||
iv: Uint8Array | null = null,
|
||||
) {
|
||||
this.method = method;
|
||||
this.uri = uri;
|
||||
this.keyFormat = format;
|
||||
this.keyFormatVersions = formatversions;
|
||||
this.iv = iv;
|
||||
this.encrypted = method ? method !== 'NONE' : false;
|
||||
this.isCommonEncryption = this.encrypted && method !== 'AES-128';
|
||||
}
|
||||
|
||||
public isSupported(): boolean {
|
||||
// If it's Segment encryption or No encryption, just select that key system
|
||||
if (this.method) {
|
||||
if (this.method === 'AES-128' || this.method === 'NONE') {
|
||||
return true;
|
||||
}
|
||||
if (this.keyFormat === 'identity') {
|
||||
// Maintain support for clear SAMPLE-AES with MPEG-3 TS
|
||||
return this.method === 'SAMPLE-AES';
|
||||
} else if (__USE_EME_DRM__) {
|
||||
switch (this.keyFormat) {
|
||||
case KeySystemFormats.FAIRPLAY:
|
||||
case KeySystemFormats.WIDEVINE:
|
||||
case KeySystemFormats.PLAYREADY:
|
||||
case KeySystemFormats.CLEARKEY:
|
||||
return (
|
||||
[
|
||||
'ISO-23001-7',
|
||||
'SAMPLE-AES',
|
||||
'SAMPLE-AES-CENC',
|
||||
'SAMPLE-AES-CTR',
|
||||
].indexOf(this.method) !== -1
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public getDecryptData(sn: number | 'initSegment'): LevelKey | null {
|
||||
if (!this.encrypted || !this.uri) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.method === 'AES-128' && this.uri && !this.iv) {
|
||||
if (typeof sn !== 'number') {
|
||||
// We are fetching decryption data for a initialization segment
|
||||
// If the segment was encrypted with AES-128
|
||||
// It must have an IV defined. We cannot substitute the Segment Number in.
|
||||
if (this.method === 'AES-128' && !this.iv) {
|
||||
logger.warn(
|
||||
`missing IV for initialization segment with method="${this.method}" - compliance issue`,
|
||||
);
|
||||
}
|
||||
// Explicitly set sn to resulting value from implicit conversions 'initSegment' values for IV generation.
|
||||
sn = 0;
|
||||
}
|
||||
const iv = createInitializationVector(sn);
|
||||
const decryptdata = new LevelKey(
|
||||
this.method,
|
||||
this.uri,
|
||||
'identity',
|
||||
this.keyFormatVersions,
|
||||
iv,
|
||||
);
|
||||
return decryptdata;
|
||||
}
|
||||
|
||||
if (!__USE_EME_DRM__) {
|
||||
return this;
|
||||
}
|
||||
|
||||
// Initialize keyId if possible
|
||||
const keyBytes = convertDataUriToArrayBytes(this.uri);
|
||||
if (keyBytes) {
|
||||
switch (this.keyFormat) {
|
||||
case KeySystemFormats.WIDEVINE:
|
||||
this.pssh = keyBytes;
|
||||
// In case of widevine keyID is embedded in PSSH box. Read Key ID.
|
||||
if (keyBytes.length >= 22) {
|
||||
this.keyId = keyBytes.subarray(
|
||||
keyBytes.length - 22,
|
||||
keyBytes.length - 6,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case KeySystemFormats.PLAYREADY: {
|
||||
const PlayReadyKeySystemUUID = new Uint8Array([
|
||||
0x9a, 0x04, 0xf0, 0x79, 0x98, 0x40, 0x42, 0x86, 0xab, 0x92, 0xe6,
|
||||
0x5b, 0xe0, 0x88, 0x5f, 0x95,
|
||||
]);
|
||||
|
||||
this.pssh = mp4pssh(PlayReadyKeySystemUUID, null, keyBytes);
|
||||
|
||||
const keyBytesUtf16 = new Uint16Array(
|
||||
keyBytes.buffer,
|
||||
keyBytes.byteOffset,
|
||||
keyBytes.byteLength / 2,
|
||||
);
|
||||
const keyByteStr = String.fromCharCode.apply(
|
||||
null,
|
||||
Array.from(keyBytesUtf16),
|
||||
);
|
||||
|
||||
// Parse Playready WRMHeader XML
|
||||
const xmlKeyBytes = keyByteStr.substring(
|
||||
keyByteStr.indexOf('<'),
|
||||
keyByteStr.length,
|
||||
);
|
||||
const parser = new DOMParser();
|
||||
const xmlDoc = parser.parseFromString(xmlKeyBytes, 'text/xml');
|
||||
const keyData = xmlDoc.getElementsByTagName('KID')[0];
|
||||
if (keyData) {
|
||||
const keyId = keyData.childNodes[0]
|
||||
? keyData.childNodes[0].nodeValue
|
||||
: keyData.getAttribute('VALUE');
|
||||
if (keyId) {
|
||||
const keyIdArray = base64Decode(keyId).subarray(0, 16);
|
||||
// KID value in PRO is a base64-encoded little endian GUID interpretation of UUID
|
||||
// KID value in ‘tenc’ is a big endian UUID GUID interpretation of UUID
|
||||
changeEndianness(keyIdArray);
|
||||
this.keyId = keyIdArray;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
let keydata = keyBytes.subarray(0, 16);
|
||||
if (keydata.length !== 16) {
|
||||
const padded = new Uint8Array(16);
|
||||
padded.set(keydata, 16 - keydata.length);
|
||||
keydata = padded;
|
||||
}
|
||||
this.keyId = keydata;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default behavior: assign a new keyId for each uri
|
||||
if (!this.keyId || this.keyId.byteLength !== 16) {
|
||||
let keyId = keyUriToKeyIdMap[this.uri];
|
||||
if (!keyId) {
|
||||
const val =
|
||||
Object.keys(keyUriToKeyIdMap).length % Number.MAX_SAFE_INTEGER;
|
||||
keyId = new Uint8Array(16);
|
||||
const dv = new DataView(keyId.buffer, 12, 4); // Just set the last 4 bytes
|
||||
dv.setUint32(0, val);
|
||||
keyUriToKeyIdMap[this.uri] = keyId;
|
||||
}
|
||||
this.keyId = keyId;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
function createInitializationVector(segmentNumber: number): Uint8Array {
|
||||
const uint8View = new Uint8Array(16);
|
||||
for (let i = 12; i < 16; i++) {
|
||||
uint8View[i] = (segmentNumber >> (8 * (15 - i))) & 0xff;
|
||||
}
|
||||
return uint8View;
|
||||
}
|
||||
17
node_modules/hls.js/src/loader/load-stats.ts
generated
vendored
Normal file
17
node_modules/hls.js/src/loader/load-stats.ts
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
import type {
|
||||
HlsPerformanceTiming,
|
||||
HlsProgressivePerformanceTiming,
|
||||
LoaderStats,
|
||||
} from '../types/loader';
|
||||
|
||||
export class LoadStats implements LoaderStats {
|
||||
aborted: boolean = false;
|
||||
loaded: number = 0;
|
||||
retry: number = 0;
|
||||
total: number = 0;
|
||||
chunkCount: number = 0;
|
||||
bwEstimate: number = 0;
|
||||
loading: HlsProgressivePerformanceTiming = { start: 0, first: 0, end: 0 };
|
||||
parsing: HlsPerformanceTiming = { start: 0, end: 0 };
|
||||
buffering: HlsProgressivePerformanceTiming = { start: 0, first: 0, end: 0 };
|
||||
}
|
||||
915
node_modules/hls.js/src/loader/m3u8-parser.ts
generated
vendored
Normal file
915
node_modules/hls.js/src/loader/m3u8-parser.ts
generated
vendored
Normal file
@@ -0,0 +1,915 @@
|
||||
import { buildAbsoluteURL } from 'url-toolkit';
|
||||
import { DateRange } from './date-range';
|
||||
import { Fragment, Part } from './fragment';
|
||||
import { LevelDetails } from './level-details';
|
||||
import { LevelKey } from './level-key';
|
||||
import { AttrList } from '../utils/attr-list';
|
||||
import { logger } from '../utils/logger';
|
||||
import {
|
||||
addVariableDefinition,
|
||||
hasVariableReferences,
|
||||
importVariableDefinition,
|
||||
substituteVariables,
|
||||
substituteVariablesInAttributes,
|
||||
} from '../utils/variable-substitution';
|
||||
import { isCodecType } from '../utils/codecs';
|
||||
import type { CodecType } from '../utils/codecs';
|
||||
import type { MediaPlaylist, MediaAttributes } from '../types/media-playlist';
|
||||
import type { PlaylistLevelType } from '../types/loader';
|
||||
import type { LevelAttributes, LevelParsed, VariableMap } from '../types/level';
|
||||
import type { ContentSteeringOptions } from '../types/events';
|
||||
|
||||
type M3U8ParserFragments = Array<Fragment | null>;
|
||||
|
||||
export type ParsedMultivariantPlaylist = {
|
||||
contentSteering: ContentSteeringOptions | null;
|
||||
levels: LevelParsed[];
|
||||
playlistParsingError: Error | null;
|
||||
sessionData: Record<string, AttrList> | null;
|
||||
sessionKeys: LevelKey[] | null;
|
||||
startTimeOffset: number | null;
|
||||
variableList: VariableMap | null;
|
||||
hasVariableRefs: boolean;
|
||||
};
|
||||
|
||||
type ParsedMultivariantMediaOptions = {
|
||||
AUDIO?: MediaPlaylist[];
|
||||
SUBTITLES?: MediaPlaylist[];
|
||||
'CLOSED-CAPTIONS'?: MediaPlaylist[];
|
||||
};
|
||||
|
||||
const MASTER_PLAYLIST_REGEX =
|
||||
/#EXT-X-STREAM-INF:([^\r\n]*)(?:[\r\n](?:#[^\r\n]*)?)*([^\r\n]+)|#EXT-X-(SESSION-DATA|SESSION-KEY|DEFINE|CONTENT-STEERING|START):([^\r\n]*)[\r\n]+/g;
|
||||
const MASTER_PLAYLIST_MEDIA_REGEX = /#EXT-X-MEDIA:(.*)/g;
|
||||
|
||||
const IS_MEDIA_PLAYLIST = /^#EXT(?:INF|-X-TARGETDURATION):/m; // Handle empty Media Playlist (first EXTINF not signaled, but TARGETDURATION present)
|
||||
|
||||
const LEVEL_PLAYLIST_REGEX_FAST = new RegExp(
|
||||
[
|
||||
/#EXTINF:\s*(\d*(?:\.\d+)?)(?:,(.*)\s+)?/.source, // duration (#EXTINF:<duration>,<title>), group 1 => duration, group 2 => title
|
||||
/(?!#) *(\S[^\r\n]*)/.source, // segment URI, group 3 => the URI (note newline is not eaten)
|
||||
/#EXT-X-BYTERANGE:*(.+)/.source, // next segment's byterange, group 4 => range spec (x@y)
|
||||
/#EXT-X-PROGRAM-DATE-TIME:(.+)/.source, // next segment's program date/time group 5 => the datetime spec
|
||||
/#.*/.source, // All other non-segment oriented tags will match with all groups empty
|
||||
].join('|'),
|
||||
'g',
|
||||
);
|
||||
|
||||
const LEVEL_PLAYLIST_REGEX_SLOW = new RegExp(
|
||||
[
|
||||
/#(EXTM3U)/.source,
|
||||
/#EXT-X-(DATERANGE|DEFINE|KEY|MAP|PART|PART-INF|PLAYLIST-TYPE|PRELOAD-HINT|RENDITION-REPORT|SERVER-CONTROL|SKIP|START):(.+)/
|
||||
.source,
|
||||
/#EXT-X-(BITRATE|DISCONTINUITY-SEQUENCE|MEDIA-SEQUENCE|TARGETDURATION|VERSION): *(\d+)/
|
||||
.source,
|
||||
/#EXT-X-(DISCONTINUITY|ENDLIST|GAP|INDEPENDENT-SEGMENTS)/.source,
|
||||
/(#)([^:]*):(.*)/.source,
|
||||
/(#)(.*)(?:.*)\r?\n?/.source,
|
||||
].join('|'),
|
||||
);
|
||||
|
||||
export default class M3U8Parser {
|
||||
static findGroup(
|
||||
groups: (
|
||||
| { id?: string; audioCodec?: string }
|
||||
| { id?: string; textCodec?: string }
|
||||
)[],
|
||||
mediaGroupId: string,
|
||||
):
|
||||
| { id?: string; audioCodec?: string }
|
||||
| { id?: string; textCodec?: string }
|
||||
| undefined {
|
||||
for (let i = 0; i < groups.length; i++) {
|
||||
const group = groups[i];
|
||||
if (group.id === mediaGroupId) {
|
||||
return group;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static resolve(url, baseUrl) {
|
||||
return buildAbsoluteURL(baseUrl, url, { alwaysNormalize: true });
|
||||
}
|
||||
|
||||
static isMediaPlaylist(str: string): boolean {
|
||||
return IS_MEDIA_PLAYLIST.test(str);
|
||||
}
|
||||
|
||||
static parseMasterPlaylist(
|
||||
string: string,
|
||||
baseurl: string,
|
||||
): ParsedMultivariantPlaylist {
|
||||
const hasVariableRefs = __USE_VARIABLE_SUBSTITUTION__
|
||||
? hasVariableReferences(string)
|
||||
: false;
|
||||
const parsed: ParsedMultivariantPlaylist = {
|
||||
contentSteering: null,
|
||||
levels: [],
|
||||
playlistParsingError: null,
|
||||
sessionData: null,
|
||||
sessionKeys: null,
|
||||
startTimeOffset: null,
|
||||
variableList: null,
|
||||
hasVariableRefs,
|
||||
};
|
||||
const levelsWithKnownCodecs: LevelParsed[] = [];
|
||||
|
||||
MASTER_PLAYLIST_REGEX.lastIndex = 0;
|
||||
|
||||
let result: RegExpExecArray | null;
|
||||
while ((result = MASTER_PLAYLIST_REGEX.exec(string)) != null) {
|
||||
if (result[1]) {
|
||||
// '#EXT-X-STREAM-INF' is found, parse level tag in group 1
|
||||
const attrs = new AttrList(result[1]) as LevelAttributes;
|
||||
if (__USE_VARIABLE_SUBSTITUTION__) {
|
||||
substituteVariablesInAttributes(parsed, attrs, [
|
||||
'CODECS',
|
||||
'SUPPLEMENTAL-CODECS',
|
||||
'ALLOWED-CPC',
|
||||
'PATHWAY-ID',
|
||||
'STABLE-VARIANT-ID',
|
||||
'AUDIO',
|
||||
'VIDEO',
|
||||
'SUBTITLES',
|
||||
'CLOSED-CAPTIONS',
|
||||
'NAME',
|
||||
]);
|
||||
}
|
||||
const uri = __USE_VARIABLE_SUBSTITUTION__
|
||||
? substituteVariables(parsed, result[2])
|
||||
: result[2];
|
||||
const level: LevelParsed = {
|
||||
attrs,
|
||||
bitrate:
|
||||
attrs.decimalInteger('BANDWIDTH') ||
|
||||
attrs.decimalInteger('AVERAGE-BANDWIDTH'),
|
||||
name: attrs.NAME,
|
||||
url: M3U8Parser.resolve(uri, baseurl),
|
||||
};
|
||||
|
||||
const resolution = attrs.decimalResolution('RESOLUTION');
|
||||
if (resolution) {
|
||||
level.width = resolution.width;
|
||||
level.height = resolution.height;
|
||||
}
|
||||
|
||||
setCodecs(attrs.CODECS, level);
|
||||
|
||||
if (!level.unknownCodecs?.length) {
|
||||
levelsWithKnownCodecs.push(level);
|
||||
}
|
||||
|
||||
parsed.levels.push(level);
|
||||
} else if (result[3]) {
|
||||
const tag = result[3];
|
||||
const attributes = result[4];
|
||||
switch (tag) {
|
||||
case 'SESSION-DATA': {
|
||||
// #EXT-X-SESSION-DATA
|
||||
const sessionAttrs = new AttrList(attributes);
|
||||
if (__USE_VARIABLE_SUBSTITUTION__) {
|
||||
substituteVariablesInAttributes(parsed, sessionAttrs, [
|
||||
'DATA-ID',
|
||||
'LANGUAGE',
|
||||
'VALUE',
|
||||
'URI',
|
||||
]);
|
||||
}
|
||||
const dataId = sessionAttrs['DATA-ID'];
|
||||
if (dataId) {
|
||||
if (parsed.sessionData === null) {
|
||||
parsed.sessionData = {};
|
||||
}
|
||||
parsed.sessionData[dataId] = sessionAttrs;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'SESSION-KEY': {
|
||||
// #EXT-X-SESSION-KEY
|
||||
const sessionKey = parseKey(attributes, baseurl, parsed);
|
||||
if (sessionKey.encrypted && sessionKey.isSupported()) {
|
||||
if (parsed.sessionKeys === null) {
|
||||
parsed.sessionKeys = [];
|
||||
}
|
||||
parsed.sessionKeys.push(sessionKey);
|
||||
} else {
|
||||
logger.warn(
|
||||
`[Keys] Ignoring invalid EXT-X-SESSION-KEY tag: "${attributes}"`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'DEFINE': {
|
||||
// #EXT-X-DEFINE
|
||||
if (__USE_VARIABLE_SUBSTITUTION__) {
|
||||
const variableAttributes = new AttrList(attributes);
|
||||
substituteVariablesInAttributes(parsed, variableAttributes, [
|
||||
'NAME',
|
||||
'VALUE',
|
||||
'QUERYPARAM',
|
||||
]);
|
||||
addVariableDefinition(parsed, variableAttributes, baseurl);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'CONTENT-STEERING': {
|
||||
// #EXT-X-CONTENT-STEERING
|
||||
const contentSteeringAttributes = new AttrList(attributes);
|
||||
if (__USE_VARIABLE_SUBSTITUTION__) {
|
||||
substituteVariablesInAttributes(
|
||||
parsed,
|
||||
contentSteeringAttributes,
|
||||
['SERVER-URI', 'PATHWAY-ID'],
|
||||
);
|
||||
}
|
||||
parsed.contentSteering = {
|
||||
uri: M3U8Parser.resolve(
|
||||
contentSteeringAttributes['SERVER-URI'],
|
||||
baseurl,
|
||||
),
|
||||
pathwayId: contentSteeringAttributes['PATHWAY-ID'] || '.',
|
||||
};
|
||||
break;
|
||||
}
|
||||
case 'START': {
|
||||
// #EXT-X-START
|
||||
parsed.startTimeOffset = parseStartTimeOffset(attributes);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Filter out levels with unknown codecs if it does not remove all levels
|
||||
const stripUnknownCodecLevels =
|
||||
levelsWithKnownCodecs.length > 0 &&
|
||||
levelsWithKnownCodecs.length < parsed.levels.length;
|
||||
|
||||
parsed.levels = stripUnknownCodecLevels
|
||||
? levelsWithKnownCodecs
|
||||
: parsed.levels;
|
||||
if (parsed.levels.length === 0) {
|
||||
parsed.playlistParsingError = new Error('no levels found in manifest');
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
static parseMasterPlaylistMedia(
|
||||
string: string,
|
||||
baseurl: string,
|
||||
parsed: ParsedMultivariantPlaylist,
|
||||
): ParsedMultivariantMediaOptions {
|
||||
let result: RegExpExecArray | null;
|
||||
const results: ParsedMultivariantMediaOptions = {};
|
||||
const levels = parsed.levels;
|
||||
const groupsByType = {
|
||||
AUDIO: levels.map((level: LevelParsed) => ({
|
||||
id: level.attrs.AUDIO,
|
||||
audioCodec: level.audioCodec,
|
||||
})),
|
||||
SUBTITLES: levels.map((level: LevelParsed) => ({
|
||||
id: level.attrs.SUBTITLES,
|
||||
textCodec: level.textCodec,
|
||||
})),
|
||||
'CLOSED-CAPTIONS': [],
|
||||
};
|
||||
let id = 0;
|
||||
MASTER_PLAYLIST_MEDIA_REGEX.lastIndex = 0;
|
||||
while ((result = MASTER_PLAYLIST_MEDIA_REGEX.exec(string)) !== null) {
|
||||
const attrs = new AttrList(result[1]) as MediaAttributes;
|
||||
const type = attrs.TYPE;
|
||||
if (type) {
|
||||
const groups: (typeof groupsByType)[keyof typeof groupsByType] =
|
||||
groupsByType[type];
|
||||
const medias: MediaPlaylist[] = results[type] || [];
|
||||
results[type] = medias;
|
||||
if (__USE_VARIABLE_SUBSTITUTION__) {
|
||||
substituteVariablesInAttributes(parsed, attrs, [
|
||||
'URI',
|
||||
'GROUP-ID',
|
||||
'LANGUAGE',
|
||||
'ASSOC-LANGUAGE',
|
||||
'STABLE-RENDITION-ID',
|
||||
'NAME',
|
||||
'INSTREAM-ID',
|
||||
'CHARACTERISTICS',
|
||||
'CHANNELS',
|
||||
]);
|
||||
}
|
||||
const lang = attrs.LANGUAGE;
|
||||
const assocLang = attrs['ASSOC-LANGUAGE'];
|
||||
const channels = attrs.CHANNELS;
|
||||
const characteristics = attrs.CHARACTERISTICS;
|
||||
const instreamId = attrs['INSTREAM-ID'];
|
||||
const media: MediaPlaylist = {
|
||||
attrs,
|
||||
bitrate: 0,
|
||||
id: id++,
|
||||
groupId: attrs['GROUP-ID'] || '',
|
||||
name: attrs.NAME || lang || '',
|
||||
type,
|
||||
default: attrs.bool('DEFAULT'),
|
||||
autoselect: attrs.bool('AUTOSELECT'),
|
||||
forced: attrs.bool('FORCED'),
|
||||
lang,
|
||||
url: attrs.URI ? M3U8Parser.resolve(attrs.URI, baseurl) : '',
|
||||
};
|
||||
if (assocLang) {
|
||||
media.assocLang = assocLang;
|
||||
}
|
||||
if (channels) {
|
||||
media.channels = channels;
|
||||
}
|
||||
if (characteristics) {
|
||||
media.characteristics = characteristics;
|
||||
}
|
||||
if (instreamId) {
|
||||
media.instreamId = instreamId;
|
||||
}
|
||||
|
||||
if (groups?.length) {
|
||||
// If there are audio or text groups signalled in the manifest, let's look for a matching codec string for this track
|
||||
// If we don't find the track signalled, lets use the first audio groups codec we have
|
||||
// Acting as a best guess
|
||||
const groupCodec =
|
||||
M3U8Parser.findGroup(groups, media.groupId as string) || groups[0];
|
||||
assignCodec(media, groupCodec, 'audioCodec');
|
||||
assignCodec(media, groupCodec, 'textCodec');
|
||||
}
|
||||
|
||||
medias.push(media);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
static parseLevelPlaylist(
|
||||
string: string,
|
||||
baseurl: string,
|
||||
id: number,
|
||||
type: PlaylistLevelType,
|
||||
levelUrlId: number,
|
||||
multivariantVariableList: VariableMap | null,
|
||||
): LevelDetails {
|
||||
const level = new LevelDetails(baseurl);
|
||||
const fragments: M3U8ParserFragments = level.fragments;
|
||||
// The most recent init segment seen (applies to all subsequent segments)
|
||||
let currentInitSegment: Fragment | null = null;
|
||||
let currentSN = 0;
|
||||
let currentPart = 0;
|
||||
let totalduration = 0;
|
||||
let discontinuityCounter = 0;
|
||||
let prevFrag: Fragment | null = null;
|
||||
let frag: Fragment = new Fragment(type, baseurl);
|
||||
let result: RegExpExecArray | RegExpMatchArray | null;
|
||||
let i: number;
|
||||
let levelkeys: { [key: string]: LevelKey } | undefined;
|
||||
let firstPdtIndex = -1;
|
||||
let createNextFrag = false;
|
||||
let nextByteRange: string | null = null;
|
||||
|
||||
LEVEL_PLAYLIST_REGEX_FAST.lastIndex = 0;
|
||||
level.m3u8 = string;
|
||||
level.hasVariableRefs = __USE_VARIABLE_SUBSTITUTION__
|
||||
? hasVariableReferences(string)
|
||||
: false;
|
||||
|
||||
while ((result = LEVEL_PLAYLIST_REGEX_FAST.exec(string)) !== null) {
|
||||
if (createNextFrag) {
|
||||
createNextFrag = false;
|
||||
frag = new Fragment(type, baseurl);
|
||||
// setup the next fragment for part loading
|
||||
frag.start = totalduration;
|
||||
frag.sn = currentSN;
|
||||
frag.cc = discontinuityCounter;
|
||||
frag.level = id;
|
||||
if (currentInitSegment) {
|
||||
frag.initSegment = currentInitSegment;
|
||||
frag.rawProgramDateTime = currentInitSegment.rawProgramDateTime;
|
||||
currentInitSegment.rawProgramDateTime = null;
|
||||
if (nextByteRange) {
|
||||
frag.setByteRange(nextByteRange);
|
||||
nextByteRange = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const duration = result[1];
|
||||
if (duration) {
|
||||
// INF
|
||||
frag.duration = parseFloat(duration);
|
||||
// avoid sliced strings https://github.com/video-dev/hls.js/issues/939
|
||||
const title = (' ' + result[2]).slice(1);
|
||||
frag.title = title || null;
|
||||
frag.tagList.push(title ? ['INF', duration, title] : ['INF', duration]);
|
||||
} else if (result[3]) {
|
||||
// url
|
||||
if (Number.isFinite(frag.duration)) {
|
||||
frag.start = totalduration;
|
||||
if (levelkeys) {
|
||||
setFragLevelKeys(frag, levelkeys, level);
|
||||
}
|
||||
frag.sn = currentSN;
|
||||
frag.level = id;
|
||||
frag.cc = discontinuityCounter;
|
||||
fragments.push(frag);
|
||||
// avoid sliced strings https://github.com/video-dev/hls.js/issues/939
|
||||
const uri = (' ' + result[3]).slice(1);
|
||||
frag.relurl = __USE_VARIABLE_SUBSTITUTION__
|
||||
? substituteVariables(level, uri)
|
||||
: uri;
|
||||
assignProgramDateTime(frag, prevFrag);
|
||||
prevFrag = frag;
|
||||
totalduration += frag.duration;
|
||||
currentSN++;
|
||||
currentPart = 0;
|
||||
createNextFrag = true;
|
||||
}
|
||||
} else if (result[4]) {
|
||||
// X-BYTERANGE
|
||||
const data = (' ' + result[4]).slice(1);
|
||||
if (prevFrag) {
|
||||
frag.setByteRange(data, prevFrag);
|
||||
} else {
|
||||
frag.setByteRange(data);
|
||||
}
|
||||
} else if (result[5]) {
|
||||
// PROGRAM-DATE-TIME
|
||||
// avoid sliced strings https://github.com/video-dev/hls.js/issues/939
|
||||
frag.rawProgramDateTime = (' ' + result[5]).slice(1);
|
||||
frag.tagList.push(['PROGRAM-DATE-TIME', frag.rawProgramDateTime]);
|
||||
if (firstPdtIndex === -1) {
|
||||
firstPdtIndex = fragments.length;
|
||||
}
|
||||
} else {
|
||||
result = result[0].match(LEVEL_PLAYLIST_REGEX_SLOW);
|
||||
if (!result) {
|
||||
logger.warn('No matches on slow regex match for level playlist!');
|
||||
continue;
|
||||
}
|
||||
for (i = 1; i < result.length; i++) {
|
||||
if (typeof result[i] !== 'undefined') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// avoid sliced strings https://github.com/video-dev/hls.js/issues/939
|
||||
const tag = (' ' + result[i]).slice(1);
|
||||
const value1 = (' ' + result[i + 1]).slice(1);
|
||||
const value2 = result[i + 2] ? (' ' + result[i + 2]).slice(1) : '';
|
||||
|
||||
switch (tag) {
|
||||
case 'PLAYLIST-TYPE':
|
||||
level.type = value1.toUpperCase();
|
||||
break;
|
||||
case 'MEDIA-SEQUENCE':
|
||||
currentSN = level.startSN = parseInt(value1);
|
||||
break;
|
||||
case 'SKIP': {
|
||||
const skipAttrs = new AttrList(value1);
|
||||
if (__USE_VARIABLE_SUBSTITUTION__) {
|
||||
substituteVariablesInAttributes(level, skipAttrs, [
|
||||
'RECENTLY-REMOVED-DATERANGES',
|
||||
]);
|
||||
}
|
||||
const skippedSegments =
|
||||
skipAttrs.decimalInteger('SKIPPED-SEGMENTS');
|
||||
if (Number.isFinite(skippedSegments)) {
|
||||
level.skippedSegments = skippedSegments;
|
||||
// This will result in fragments[] containing undefined values, which we will fill in with `mergeDetails`
|
||||
for (let i = skippedSegments; i--; ) {
|
||||
fragments.unshift(null);
|
||||
}
|
||||
currentSN += skippedSegments;
|
||||
}
|
||||
const recentlyRemovedDateranges = skipAttrs.enumeratedString(
|
||||
'RECENTLY-REMOVED-DATERANGES',
|
||||
);
|
||||
if (recentlyRemovedDateranges) {
|
||||
level.recentlyRemovedDateranges =
|
||||
recentlyRemovedDateranges.split('\t');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'TARGETDURATION':
|
||||
level.targetduration = Math.max(parseInt(value1), 1);
|
||||
break;
|
||||
case 'VERSION':
|
||||
level.version = parseInt(value1);
|
||||
break;
|
||||
case 'INDEPENDENT-SEGMENTS':
|
||||
case 'EXTM3U':
|
||||
break;
|
||||
case 'ENDLIST':
|
||||
level.live = false;
|
||||
break;
|
||||
case '#':
|
||||
if (value1 || value2) {
|
||||
frag.tagList.push(value2 ? [value1, value2] : [value1]);
|
||||
}
|
||||
break;
|
||||
case 'DISCONTINUITY':
|
||||
discontinuityCounter++;
|
||||
frag.tagList.push(['DIS']);
|
||||
break;
|
||||
case 'GAP':
|
||||
frag.gap = true;
|
||||
frag.tagList.push([tag]);
|
||||
break;
|
||||
case 'BITRATE':
|
||||
frag.tagList.push([tag, value1]);
|
||||
break;
|
||||
case 'DATERANGE': {
|
||||
const dateRangeAttr = new AttrList(value1);
|
||||
if (__USE_VARIABLE_SUBSTITUTION__) {
|
||||
substituteVariablesInAttributes(level, dateRangeAttr, [
|
||||
'ID',
|
||||
'CLASS',
|
||||
'START-DATE',
|
||||
'END-DATE',
|
||||
'SCTE35-CMD',
|
||||
'SCTE35-OUT',
|
||||
'SCTE35-IN',
|
||||
]);
|
||||
substituteVariablesInAttributes(
|
||||
level,
|
||||
dateRangeAttr,
|
||||
dateRangeAttr.clientAttrs,
|
||||
);
|
||||
}
|
||||
const dateRange = new DateRange(
|
||||
dateRangeAttr,
|
||||
level.dateRanges[dateRangeAttr.ID],
|
||||
);
|
||||
if (dateRange.isValid || level.skippedSegments) {
|
||||
level.dateRanges[dateRange.id] = dateRange;
|
||||
} else {
|
||||
logger.warn(`Ignoring invalid DATERANGE tag: "${value1}"`);
|
||||
}
|
||||
// Add to fragment tag list for backwards compatibility (< v1.2.0)
|
||||
frag.tagList.push(['EXT-X-DATERANGE', value1]);
|
||||
break;
|
||||
}
|
||||
case 'DEFINE': {
|
||||
if (__USE_VARIABLE_SUBSTITUTION__) {
|
||||
const variableAttributes = new AttrList(value1);
|
||||
substituteVariablesInAttributes(level, variableAttributes, [
|
||||
'NAME',
|
||||
'VALUE',
|
||||
'IMPORT',
|
||||
'QUERYPARAM',
|
||||
]);
|
||||
if ('IMPORT' in variableAttributes) {
|
||||
importVariableDefinition(
|
||||
level,
|
||||
variableAttributes,
|
||||
multivariantVariableList,
|
||||
);
|
||||
} else {
|
||||
addVariableDefinition(level, variableAttributes, baseurl);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'DISCONTINUITY-SEQUENCE':
|
||||
discontinuityCounter = parseInt(value1);
|
||||
break;
|
||||
case 'KEY': {
|
||||
const levelKey = parseKey(value1, baseurl, level);
|
||||
if (levelKey.isSupported()) {
|
||||
if (levelKey.method === 'NONE') {
|
||||
levelkeys = undefined;
|
||||
break;
|
||||
}
|
||||
if (!levelkeys) {
|
||||
levelkeys = {};
|
||||
}
|
||||
if (levelkeys[levelKey.keyFormat]) {
|
||||
levelkeys = Object.assign({}, levelkeys);
|
||||
}
|
||||
levelkeys[levelKey.keyFormat] = levelKey;
|
||||
} else {
|
||||
logger.warn(`[Keys] Ignoring invalid EXT-X-KEY tag: "${value1}"`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'START':
|
||||
level.startTimeOffset = parseStartTimeOffset(value1);
|
||||
break;
|
||||
case 'MAP': {
|
||||
const mapAttrs = new AttrList(value1);
|
||||
if (__USE_VARIABLE_SUBSTITUTION__) {
|
||||
substituteVariablesInAttributes(level, mapAttrs, [
|
||||
'BYTERANGE',
|
||||
'URI',
|
||||
]);
|
||||
}
|
||||
if (frag.duration) {
|
||||
// Initial segment tag is after segment duration tag.
|
||||
// #EXTINF: 6.0
|
||||
// #EXT-X-MAP:URI="init.mp4
|
||||
const init = new Fragment(type, baseurl);
|
||||
setInitSegment(init, mapAttrs, id, levelkeys);
|
||||
currentInitSegment = init;
|
||||
frag.initSegment = currentInitSegment;
|
||||
if (
|
||||
currentInitSegment.rawProgramDateTime &&
|
||||
!frag.rawProgramDateTime
|
||||
) {
|
||||
frag.rawProgramDateTime = currentInitSegment.rawProgramDateTime;
|
||||
}
|
||||
} else {
|
||||
// Initial segment tag is before segment duration tag
|
||||
// Handle case where EXT-X-MAP is declared after EXT-X-BYTERANGE
|
||||
const end = frag.byteRangeEndOffset;
|
||||
if (end) {
|
||||
const start = frag.byteRangeStartOffset as number;
|
||||
nextByteRange = `${end - start}@${start}`;
|
||||
} else {
|
||||
nextByteRange = null;
|
||||
}
|
||||
setInitSegment(frag, mapAttrs, id, levelkeys);
|
||||
currentInitSegment = frag;
|
||||
createNextFrag = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'SERVER-CONTROL': {
|
||||
const serverControlAttrs = new AttrList(value1);
|
||||
level.canBlockReload = serverControlAttrs.bool('CAN-BLOCK-RELOAD');
|
||||
level.canSkipUntil = serverControlAttrs.optionalFloat(
|
||||
'CAN-SKIP-UNTIL',
|
||||
0,
|
||||
);
|
||||
level.canSkipDateRanges =
|
||||
level.canSkipUntil > 0 &&
|
||||
serverControlAttrs.bool('CAN-SKIP-DATERANGES');
|
||||
level.partHoldBack = serverControlAttrs.optionalFloat(
|
||||
'PART-HOLD-BACK',
|
||||
0,
|
||||
);
|
||||
level.holdBack = serverControlAttrs.optionalFloat('HOLD-BACK', 0);
|
||||
break;
|
||||
}
|
||||
case 'PART-INF': {
|
||||
const partInfAttrs = new AttrList(value1);
|
||||
level.partTarget = partInfAttrs.decimalFloatingPoint('PART-TARGET');
|
||||
break;
|
||||
}
|
||||
case 'PART': {
|
||||
let partList = level.partList;
|
||||
if (!partList) {
|
||||
partList = level.partList = [];
|
||||
}
|
||||
const previousFragmentPart =
|
||||
currentPart > 0 ? partList[partList.length - 1] : undefined;
|
||||
const index = currentPart++;
|
||||
const partAttrs = new AttrList(value1);
|
||||
if (__USE_VARIABLE_SUBSTITUTION__) {
|
||||
substituteVariablesInAttributes(level, partAttrs, [
|
||||
'BYTERANGE',
|
||||
'URI',
|
||||
]);
|
||||
}
|
||||
const part = new Part(
|
||||
partAttrs,
|
||||
frag,
|
||||
baseurl,
|
||||
index,
|
||||
previousFragmentPart,
|
||||
);
|
||||
partList.push(part);
|
||||
frag.duration += part.duration;
|
||||
break;
|
||||
}
|
||||
case 'PRELOAD-HINT': {
|
||||
const preloadHintAttrs = new AttrList(value1);
|
||||
if (__USE_VARIABLE_SUBSTITUTION__) {
|
||||
substituteVariablesInAttributes(level, preloadHintAttrs, ['URI']);
|
||||
}
|
||||
level.preloadHint = preloadHintAttrs;
|
||||
break;
|
||||
}
|
||||
case 'RENDITION-REPORT': {
|
||||
const renditionReportAttrs = new AttrList(value1);
|
||||
if (__USE_VARIABLE_SUBSTITUTION__) {
|
||||
substituteVariablesInAttributes(level, renditionReportAttrs, [
|
||||
'URI',
|
||||
]);
|
||||
}
|
||||
level.renditionReports = level.renditionReports || [];
|
||||
level.renditionReports.push(renditionReportAttrs);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
logger.warn(`line parsed but not handled: ${result}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (prevFrag && !prevFrag.relurl) {
|
||||
fragments.pop();
|
||||
totalduration -= prevFrag.duration;
|
||||
if (level.partList) {
|
||||
level.fragmentHint = prevFrag;
|
||||
}
|
||||
} else if (level.partList) {
|
||||
assignProgramDateTime(frag, prevFrag);
|
||||
frag.cc = discontinuityCounter;
|
||||
level.fragmentHint = frag;
|
||||
if (levelkeys) {
|
||||
setFragLevelKeys(frag, levelkeys, level);
|
||||
}
|
||||
}
|
||||
const fragmentLength = fragments.length;
|
||||
const firstFragment = fragments[0];
|
||||
const lastFragment = fragments[fragmentLength - 1];
|
||||
totalduration += level.skippedSegments * level.targetduration;
|
||||
if (totalduration > 0 && fragmentLength && lastFragment) {
|
||||
level.averagetargetduration = totalduration / fragmentLength;
|
||||
const lastSn = lastFragment.sn;
|
||||
level.endSN = lastSn !== 'initSegment' ? lastSn : 0;
|
||||
if (!level.live) {
|
||||
lastFragment.endList = true;
|
||||
}
|
||||
if (firstFragment) {
|
||||
level.startCC = firstFragment.cc;
|
||||
}
|
||||
} else {
|
||||
level.endSN = 0;
|
||||
level.startCC = 0;
|
||||
}
|
||||
if (level.fragmentHint) {
|
||||
totalduration += level.fragmentHint.duration;
|
||||
}
|
||||
level.totalduration = totalduration;
|
||||
level.endCC = discontinuityCounter;
|
||||
|
||||
/**
|
||||
* Backfill any missing PDT values
|
||||
* "If the first EXT-X-PROGRAM-DATE-TIME tag in a Playlist appears after
|
||||
* one or more Media Segment URIs, the client SHOULD extrapolate
|
||||
* backward from that tag (using EXTINF durations and/or media
|
||||
* timestamps) to associate dates with those segments."
|
||||
* We have already extrapolated forward, but all fragments up to the first instance of PDT do not have their PDTs
|
||||
* computed.
|
||||
*/
|
||||
if (firstPdtIndex > 0) {
|
||||
backfillProgramDateTimes(fragments, firstPdtIndex);
|
||||
}
|
||||
|
||||
return level;
|
||||
}
|
||||
}
|
||||
|
||||
function parseKey(
|
||||
keyTagAttributes: string,
|
||||
baseurl: string,
|
||||
parsed: ParsedMultivariantPlaylist | LevelDetails,
|
||||
): LevelKey {
|
||||
// https://tools.ietf.org/html/rfc8216#section-4.3.2.4
|
||||
const keyAttrs = new AttrList(keyTagAttributes);
|
||||
if (__USE_VARIABLE_SUBSTITUTION__) {
|
||||
substituteVariablesInAttributes(parsed, keyAttrs, [
|
||||
'KEYFORMAT',
|
||||
'KEYFORMATVERSIONS',
|
||||
'URI',
|
||||
'IV',
|
||||
'URI',
|
||||
]);
|
||||
}
|
||||
const decryptmethod = keyAttrs.METHOD ?? '';
|
||||
const decrypturi = keyAttrs.URI;
|
||||
const decryptiv = keyAttrs.hexadecimalInteger('IV');
|
||||
const decryptkeyformatversions = keyAttrs.KEYFORMATVERSIONS;
|
||||
// From RFC: This attribute is OPTIONAL; its absence indicates an implicit value of "identity".
|
||||
const decryptkeyformat = keyAttrs.KEYFORMAT ?? 'identity';
|
||||
|
||||
if (decrypturi && keyAttrs.IV && !decryptiv) {
|
||||
logger.error(`Invalid IV: ${keyAttrs.IV}`);
|
||||
}
|
||||
// If decrypturi is a URI with a scheme, then baseurl will be ignored
|
||||
// No uri is allowed when METHOD is NONE
|
||||
const resolvedUri = decrypturi ? M3U8Parser.resolve(decrypturi, baseurl) : '';
|
||||
const keyFormatVersions = (
|
||||
decryptkeyformatversions ? decryptkeyformatversions : '1'
|
||||
)
|
||||
.split('/')
|
||||
.map(Number)
|
||||
.filter(Number.isFinite);
|
||||
|
||||
return new LevelKey(
|
||||
decryptmethod,
|
||||
resolvedUri,
|
||||
decryptkeyformat,
|
||||
keyFormatVersions,
|
||||
decryptiv,
|
||||
);
|
||||
}
|
||||
|
||||
function parseStartTimeOffset(startAttributes: string): number | null {
|
||||
const startAttrs = new AttrList(startAttributes);
|
||||
const startTimeOffset = startAttrs.decimalFloatingPoint('TIME-OFFSET');
|
||||
if (Number.isFinite(startTimeOffset)) {
|
||||
return startTimeOffset;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function setCodecs(
|
||||
codecsAttributeValue: string | undefined,
|
||||
level: LevelParsed,
|
||||
) {
|
||||
let codecs = (codecsAttributeValue || '').split(/[ ,]+/).filter((c) => c);
|
||||
['video', 'audio', 'text'].forEach((type: CodecType) => {
|
||||
const filtered = codecs.filter((codec) => isCodecType(codec, type));
|
||||
if (filtered.length) {
|
||||
// Comma separated list of all codecs for type
|
||||
level[`${type}Codec`] = filtered.join(',');
|
||||
// Remove known codecs so that only unknownCodecs are left after iterating through each type
|
||||
codecs = codecs.filter((codec) => filtered.indexOf(codec) === -1);
|
||||
}
|
||||
});
|
||||
level.unknownCodecs = codecs;
|
||||
}
|
||||
|
||||
function assignCodec(
|
||||
media: MediaPlaylist,
|
||||
groupItem: { audioCodec?: string; textCodec?: string },
|
||||
codecProperty: 'audioCodec' | 'textCodec',
|
||||
) {
|
||||
const codecValue = groupItem[codecProperty];
|
||||
if (codecValue) {
|
||||
media[codecProperty] = codecValue;
|
||||
}
|
||||
}
|
||||
|
||||
function backfillProgramDateTimes(
|
||||
fragments: M3U8ParserFragments,
|
||||
firstPdtIndex: number,
|
||||
) {
|
||||
let fragPrev = fragments[firstPdtIndex] as Fragment;
|
||||
for (let i = firstPdtIndex; i--; ) {
|
||||
const frag = fragments[i];
|
||||
// Exit on delta-playlist skipped segments
|
||||
if (!frag) {
|
||||
return;
|
||||
}
|
||||
frag.programDateTime =
|
||||
(fragPrev.programDateTime as number) - frag.duration * 1000;
|
||||
fragPrev = frag;
|
||||
}
|
||||
}
|
||||
|
||||
function assignProgramDateTime(frag, prevFrag) {
|
||||
if (frag.rawProgramDateTime) {
|
||||
frag.programDateTime = Date.parse(frag.rawProgramDateTime);
|
||||
} else if (prevFrag?.programDateTime) {
|
||||
frag.programDateTime = prevFrag.endProgramDateTime;
|
||||
}
|
||||
|
||||
if (!Number.isFinite(frag.programDateTime)) {
|
||||
frag.programDateTime = null;
|
||||
frag.rawProgramDateTime = null;
|
||||
}
|
||||
}
|
||||
|
||||
function setInitSegment(
|
||||
frag: Fragment,
|
||||
mapAttrs: AttrList,
|
||||
id: number,
|
||||
levelkeys: { [key: string]: LevelKey } | undefined,
|
||||
) {
|
||||
frag.relurl = mapAttrs.URI;
|
||||
if (mapAttrs.BYTERANGE) {
|
||||
frag.setByteRange(mapAttrs.BYTERANGE);
|
||||
}
|
||||
frag.level = id;
|
||||
frag.sn = 'initSegment';
|
||||
if (levelkeys) {
|
||||
frag.levelkeys = levelkeys;
|
||||
}
|
||||
frag.initSegment = null;
|
||||
}
|
||||
|
||||
function setFragLevelKeys(
|
||||
frag: Fragment,
|
||||
levelkeys: { [key: string]: LevelKey },
|
||||
level: LevelDetails,
|
||||
) {
|
||||
frag.levelkeys = levelkeys;
|
||||
const { encryptedFragments } = level;
|
||||
if (
|
||||
(!encryptedFragments.length ||
|
||||
encryptedFragments[encryptedFragments.length - 1].levelkeys !==
|
||||
levelkeys) &&
|
||||
Object.keys(levelkeys).some(
|
||||
(format) => levelkeys![format].isCommonEncryption,
|
||||
)
|
||||
) {
|
||||
encryptedFragments.push(frag);
|
||||
}
|
||||
}
|
||||
716
node_modules/hls.js/src/loader/playlist-loader.ts
generated
vendored
Normal file
716
node_modules/hls.js/src/loader/playlist-loader.ts
generated
vendored
Normal file
@@ -0,0 +1,716 @@
|
||||
/**
|
||||
* PlaylistLoader - delegate for media manifest/playlist loading tasks. Takes care of parsing media to internal data-models.
|
||||
*
|
||||
* Once loaded, dispatches events with parsed data-models of manifest/levels/audio/subtitle tracks.
|
||||
*
|
||||
* Uses loader(s) set in config to do actual internal loading of resource tasks.
|
||||
*/
|
||||
|
||||
import { Events } from '../events';
|
||||
import { ErrorDetails, ErrorTypes } from '../errors';
|
||||
import { logger } from '../utils/logger';
|
||||
import M3U8Parser from './m3u8-parser';
|
||||
import type { LevelParsed, VariableMap } from '../types/level';
|
||||
import type {
|
||||
Loader,
|
||||
LoaderCallbacks,
|
||||
LoaderConfiguration,
|
||||
LoaderContext,
|
||||
LoaderResponse,
|
||||
LoaderStats,
|
||||
PlaylistLoaderContext,
|
||||
} from '../types/loader';
|
||||
import { PlaylistContextType, PlaylistLevelType } from '../types/loader';
|
||||
import { LevelDetails } from './level-details';
|
||||
import { AttrList } from '../utils/attr-list';
|
||||
import type Hls from '../hls';
|
||||
import type {
|
||||
ErrorData,
|
||||
LevelLoadingData,
|
||||
ManifestLoadingData,
|
||||
TrackLoadingData,
|
||||
} from '../types/events';
|
||||
import type { NetworkComponentAPI } from '../types/component-api';
|
||||
import type { MediaAttributes } from '../types/media-playlist';
|
||||
import type { LoaderConfig, RetryConfig } from '../config';
|
||||
|
||||
function mapContextToLevelType(
|
||||
context: PlaylistLoaderContext,
|
||||
): PlaylistLevelType {
|
||||
const { type } = context;
|
||||
|
||||
switch (type) {
|
||||
case PlaylistContextType.AUDIO_TRACK:
|
||||
return PlaylistLevelType.AUDIO;
|
||||
case PlaylistContextType.SUBTITLE_TRACK:
|
||||
return PlaylistLevelType.SUBTITLE;
|
||||
default:
|
||||
return PlaylistLevelType.MAIN;
|
||||
}
|
||||
}
|
||||
|
||||
function getResponseUrl(
|
||||
response: LoaderResponse,
|
||||
context: PlaylistLoaderContext,
|
||||
): string {
|
||||
let url = response.url;
|
||||
// responseURL not supported on some browsers (it is used to detect URL redirection)
|
||||
// data-uri mode also not supported (but no need to detect redirection)
|
||||
if (url === undefined || url.indexOf('data:') === 0) {
|
||||
// fallback to initial URL
|
||||
url = context.url;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
class PlaylistLoader implements NetworkComponentAPI {
|
||||
private readonly hls: Hls;
|
||||
private readonly loaders: {
|
||||
[key: string]: Loader<LoaderContext>;
|
||||
} = Object.create(null);
|
||||
private variableList: VariableMap | null = null;
|
||||
|
||||
constructor(hls: Hls) {
|
||||
this.hls = hls;
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
public startLoad(startPosition: number): void {}
|
||||
|
||||
public stopLoad(): void {
|
||||
this.destroyInternalLoaders();
|
||||
}
|
||||
|
||||
private registerListeners() {
|
||||
const { hls } = this;
|
||||
hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this);
|
||||
hls.on(Events.LEVEL_LOADING, this.onLevelLoading, this);
|
||||
hls.on(Events.AUDIO_TRACK_LOADING, this.onAudioTrackLoading, this);
|
||||
hls.on(Events.SUBTITLE_TRACK_LOADING, this.onSubtitleTrackLoading, this);
|
||||
}
|
||||
|
||||
private unregisterListeners() {
|
||||
const { hls } = this;
|
||||
hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this);
|
||||
hls.off(Events.LEVEL_LOADING, this.onLevelLoading, this);
|
||||
hls.off(Events.AUDIO_TRACK_LOADING, this.onAudioTrackLoading, this);
|
||||
hls.off(Events.SUBTITLE_TRACK_LOADING, this.onSubtitleTrackLoading, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns defaults or configured loader-type overloads (pLoader and loader config params)
|
||||
*/
|
||||
private createInternalLoader(
|
||||
context: PlaylistLoaderContext,
|
||||
): Loader<LoaderContext> {
|
||||
const config = this.hls.config;
|
||||
const PLoader = config.pLoader;
|
||||
const Loader = config.loader;
|
||||
const InternalLoader = PLoader || Loader;
|
||||
const loader = new InternalLoader(config) as Loader<PlaylistLoaderContext>;
|
||||
|
||||
this.loaders[context.type] = loader;
|
||||
return loader;
|
||||
}
|
||||
|
||||
private getInternalLoader(
|
||||
context: PlaylistLoaderContext,
|
||||
): Loader<LoaderContext> | undefined {
|
||||
return this.loaders[context.type];
|
||||
}
|
||||
|
||||
private resetInternalLoader(contextType): void {
|
||||
if (this.loaders[contextType]) {
|
||||
delete this.loaders[contextType];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call `destroy` on all internal loader instances mapped (one per context type)
|
||||
*/
|
||||
private destroyInternalLoaders(): void {
|
||||
for (const contextType in this.loaders) {
|
||||
const loader = this.loaders[contextType];
|
||||
if (loader) {
|
||||
loader.destroy();
|
||||
}
|
||||
|
||||
this.resetInternalLoader(contextType);
|
||||
}
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.variableList = null;
|
||||
this.unregisterListeners();
|
||||
this.destroyInternalLoaders();
|
||||
}
|
||||
|
||||
private onManifestLoading(
|
||||
event: Events.MANIFEST_LOADING,
|
||||
data: ManifestLoadingData,
|
||||
) {
|
||||
const { url } = data;
|
||||
this.variableList = null;
|
||||
this.load({
|
||||
id: null,
|
||||
level: 0,
|
||||
responseType: 'text',
|
||||
type: PlaylistContextType.MANIFEST,
|
||||
url,
|
||||
deliveryDirectives: null,
|
||||
});
|
||||
}
|
||||
|
||||
private onLevelLoading(event: Events.LEVEL_LOADING, data: LevelLoadingData) {
|
||||
const { id, level, pathwayId, url, deliveryDirectives } = data;
|
||||
this.load({
|
||||
id,
|
||||
level,
|
||||
pathwayId,
|
||||
responseType: 'text',
|
||||
type: PlaylistContextType.LEVEL,
|
||||
url,
|
||||
deliveryDirectives,
|
||||
});
|
||||
}
|
||||
|
||||
private onAudioTrackLoading(
|
||||
event: Events.AUDIO_TRACK_LOADING,
|
||||
data: TrackLoadingData,
|
||||
) {
|
||||
const { id, groupId, url, deliveryDirectives } = data;
|
||||
this.load({
|
||||
id,
|
||||
groupId,
|
||||
level: null,
|
||||
responseType: 'text',
|
||||
type: PlaylistContextType.AUDIO_TRACK,
|
||||
url,
|
||||
deliveryDirectives,
|
||||
});
|
||||
}
|
||||
|
||||
private onSubtitleTrackLoading(
|
||||
event: Events.SUBTITLE_TRACK_LOADING,
|
||||
data: TrackLoadingData,
|
||||
) {
|
||||
const { id, groupId, url, deliveryDirectives } = data;
|
||||
this.load({
|
||||
id,
|
||||
groupId,
|
||||
level: null,
|
||||
responseType: 'text',
|
||||
type: PlaylistContextType.SUBTITLE_TRACK,
|
||||
url,
|
||||
deliveryDirectives,
|
||||
});
|
||||
}
|
||||
|
||||
private load(context: PlaylistLoaderContext): void {
|
||||
const config = this.hls.config;
|
||||
|
||||
// logger.debug(`[playlist-loader]: Loading playlist of type ${context.type}, level: ${context.level}, id: ${context.id}`);
|
||||
|
||||
// Check if a loader for this context already exists
|
||||
let loader = this.getInternalLoader(context);
|
||||
if (loader) {
|
||||
const loaderContext = loader.context as PlaylistLoaderContext;
|
||||
if (
|
||||
loaderContext &&
|
||||
loaderContext.url === context.url &&
|
||||
loaderContext.level === context.level
|
||||
) {
|
||||
// same URL can't overlap
|
||||
logger.trace('[playlist-loader]: playlist request ongoing');
|
||||
return;
|
||||
}
|
||||
logger.log(
|
||||
`[playlist-loader]: aborting previous loader for type: ${context.type}`,
|
||||
);
|
||||
loader.abort();
|
||||
}
|
||||
|
||||
// apply different configs for retries depending on
|
||||
// context (manifest, level, audio/subs playlist)
|
||||
let loadPolicy: LoaderConfig;
|
||||
if (context.type === PlaylistContextType.MANIFEST) {
|
||||
loadPolicy = config.manifestLoadPolicy.default;
|
||||
} else {
|
||||
loadPolicy = Object.assign({}, config.playlistLoadPolicy.default, {
|
||||
timeoutRetry: null,
|
||||
errorRetry: null,
|
||||
});
|
||||
}
|
||||
loader = this.createInternalLoader(context);
|
||||
|
||||
// Override level/track timeout for LL-HLS requests
|
||||
// (the default of 10000ms is counter productive to blocking playlist reload requests)
|
||||
if (Number.isFinite(context.deliveryDirectives?.part)) {
|
||||
let levelDetails: LevelDetails | undefined;
|
||||
if (
|
||||
context.type === PlaylistContextType.LEVEL &&
|
||||
context.level !== null
|
||||
) {
|
||||
levelDetails = this.hls.levels[context.level].details;
|
||||
} else if (
|
||||
context.type === PlaylistContextType.AUDIO_TRACK &&
|
||||
context.id !== null
|
||||
) {
|
||||
levelDetails = this.hls.audioTracks[context.id].details;
|
||||
} else if (
|
||||
context.type === PlaylistContextType.SUBTITLE_TRACK &&
|
||||
context.id !== null
|
||||
) {
|
||||
levelDetails = this.hls.subtitleTracks[context.id].details;
|
||||
}
|
||||
if (levelDetails) {
|
||||
const partTarget = levelDetails.partTarget;
|
||||
const targetDuration = levelDetails.targetduration;
|
||||
if (partTarget && targetDuration) {
|
||||
const maxLowLatencyPlaylistRefresh =
|
||||
Math.max(partTarget * 3, targetDuration * 0.8) * 1000;
|
||||
loadPolicy = Object.assign({}, loadPolicy, {
|
||||
maxTimeToFirstByteMs: Math.min(
|
||||
maxLowLatencyPlaylistRefresh,
|
||||
loadPolicy.maxTimeToFirstByteMs,
|
||||
),
|
||||
maxLoadTimeMs: Math.min(
|
||||
maxLowLatencyPlaylistRefresh,
|
||||
loadPolicy.maxTimeToFirstByteMs,
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const legacyRetryCompatibility: RetryConfig | Record<string, void> =
|
||||
loadPolicy.errorRetry || loadPolicy.timeoutRetry || {};
|
||||
const loaderConfig: LoaderConfiguration = {
|
||||
loadPolicy,
|
||||
timeout: loadPolicy.maxLoadTimeMs,
|
||||
maxRetry: legacyRetryCompatibility.maxNumRetry || 0,
|
||||
retryDelay: legacyRetryCompatibility.retryDelayMs || 0,
|
||||
maxRetryDelay: legacyRetryCompatibility.maxRetryDelayMs || 0,
|
||||
};
|
||||
|
||||
const loaderCallbacks: LoaderCallbacks<PlaylistLoaderContext> = {
|
||||
onSuccess: (response, stats, context, networkDetails) => {
|
||||
const loader = this.getInternalLoader(context) as
|
||||
| Loader<PlaylistLoaderContext>
|
||||
| undefined;
|
||||
this.resetInternalLoader(context.type);
|
||||
|
||||
const string = response.data as string;
|
||||
|
||||
// Validate if it is an M3U8 at all
|
||||
if (string.indexOf('#EXTM3U') !== 0) {
|
||||
this.handleManifestParsingError(
|
||||
response,
|
||||
context,
|
||||
new Error('no EXTM3U delimiter'),
|
||||
networkDetails || null,
|
||||
stats,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
stats.parsing.start = performance.now();
|
||||
if (M3U8Parser.isMediaPlaylist(string)) {
|
||||
this.handleTrackOrLevelPlaylist(
|
||||
response,
|
||||
stats,
|
||||
context,
|
||||
networkDetails || null,
|
||||
loader,
|
||||
);
|
||||
} else {
|
||||
this.handleMasterPlaylist(response, stats, context, networkDetails);
|
||||
}
|
||||
},
|
||||
onError: (response, context, networkDetails, stats) => {
|
||||
this.handleNetworkError(
|
||||
context,
|
||||
networkDetails,
|
||||
false,
|
||||
response,
|
||||
stats,
|
||||
);
|
||||
},
|
||||
onTimeout: (stats, context, networkDetails) => {
|
||||
this.handleNetworkError(
|
||||
context,
|
||||
networkDetails,
|
||||
true,
|
||||
undefined,
|
||||
stats,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
// logger.debug(`[playlist-loader]: Calling internal loader delegate for URL: ${context.url}`);
|
||||
|
||||
loader.load(context, loaderConfig, loaderCallbacks);
|
||||
}
|
||||
|
||||
private handleMasterPlaylist(
|
||||
response: LoaderResponse,
|
||||
stats: LoaderStats,
|
||||
context: PlaylistLoaderContext,
|
||||
networkDetails: any,
|
||||
): void {
|
||||
const hls = this.hls;
|
||||
const string = response.data as string;
|
||||
|
||||
const url = getResponseUrl(response, context);
|
||||
|
||||
const parsedResult = M3U8Parser.parseMasterPlaylist(string, url);
|
||||
|
||||
if (parsedResult.playlistParsingError) {
|
||||
this.handleManifestParsingError(
|
||||
response,
|
||||
context,
|
||||
parsedResult.playlistParsingError,
|
||||
networkDetails,
|
||||
stats,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
contentSteering,
|
||||
levels,
|
||||
sessionData,
|
||||
sessionKeys,
|
||||
startTimeOffset,
|
||||
variableList,
|
||||
} = parsedResult;
|
||||
|
||||
this.variableList = variableList;
|
||||
|
||||
const {
|
||||
AUDIO: audioTracks = [],
|
||||
SUBTITLES: subtitles,
|
||||
'CLOSED-CAPTIONS': captions,
|
||||
} = M3U8Parser.parseMasterPlaylistMedia(string, url, parsedResult);
|
||||
|
||||
if (audioTracks.length) {
|
||||
// check if we have found an audio track embedded in main playlist (audio track without URI attribute)
|
||||
const embeddedAudioFound: boolean = audioTracks.some(
|
||||
(audioTrack) => !audioTrack.url,
|
||||
);
|
||||
|
||||
// if no embedded audio track defined, but audio codec signaled in quality level,
|
||||
// we need to signal this main audio track this could happen with playlists with
|
||||
// alt audio rendition in which quality levels (main)
|
||||
// contains both audio+video. but with mixed audio track not signaled
|
||||
if (
|
||||
!embeddedAudioFound &&
|
||||
levels[0].audioCodec &&
|
||||
!levels[0].attrs.AUDIO
|
||||
) {
|
||||
logger.log(
|
||||
'[playlist-loader]: audio codec signaled in quality level, but no embedded audio track signaled, create one',
|
||||
);
|
||||
audioTracks.unshift({
|
||||
type: 'main',
|
||||
name: 'main',
|
||||
groupId: 'main',
|
||||
default: false,
|
||||
autoselect: false,
|
||||
forced: false,
|
||||
id: -1,
|
||||
attrs: new AttrList({}) as MediaAttributes,
|
||||
bitrate: 0,
|
||||
url: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
hls.trigger(Events.MANIFEST_LOADED, {
|
||||
levels,
|
||||
audioTracks,
|
||||
subtitles,
|
||||
captions,
|
||||
contentSteering,
|
||||
url,
|
||||
stats,
|
||||
networkDetails,
|
||||
sessionData,
|
||||
sessionKeys,
|
||||
startTimeOffset,
|
||||
variableList,
|
||||
});
|
||||
}
|
||||
|
||||
private handleTrackOrLevelPlaylist(
|
||||
response: LoaderResponse,
|
||||
stats: LoaderStats,
|
||||
context: PlaylistLoaderContext,
|
||||
networkDetails: any,
|
||||
loader: Loader<PlaylistLoaderContext> | undefined,
|
||||
): void {
|
||||
const hls = this.hls;
|
||||
const { id, level, type } = context;
|
||||
|
||||
const url = getResponseUrl(response, context);
|
||||
const levelUrlId = 0;
|
||||
const levelId = Number.isFinite(level as number)
|
||||
? (level as number)
|
||||
: Number.isFinite(id as number)
|
||||
? (id as number)
|
||||
: 0;
|
||||
const levelType = mapContextToLevelType(context);
|
||||
const levelDetails: LevelDetails = M3U8Parser.parseLevelPlaylist(
|
||||
response.data as string,
|
||||
url,
|
||||
levelId,
|
||||
levelType,
|
||||
levelUrlId,
|
||||
this.variableList,
|
||||
);
|
||||
|
||||
// We have done our first request (Manifest-type) and receive
|
||||
// not a master playlist but a chunk-list (track/level)
|
||||
// We fire the manifest-loaded event anyway with the parsed level-details
|
||||
// by creating a single-level structure for it.
|
||||
if (type === PlaylistContextType.MANIFEST) {
|
||||
const singleLevel: LevelParsed = {
|
||||
attrs: new AttrList({}),
|
||||
bitrate: 0,
|
||||
details: levelDetails,
|
||||
name: '',
|
||||
url,
|
||||
};
|
||||
|
||||
hls.trigger(Events.MANIFEST_LOADED, {
|
||||
levels: [singleLevel],
|
||||
audioTracks: [],
|
||||
url,
|
||||
stats,
|
||||
networkDetails,
|
||||
sessionData: null,
|
||||
sessionKeys: null,
|
||||
contentSteering: null,
|
||||
startTimeOffset: null,
|
||||
variableList: null,
|
||||
});
|
||||
}
|
||||
|
||||
// save parsing time
|
||||
stats.parsing.end = performance.now();
|
||||
|
||||
// extend the context with the new levelDetails property
|
||||
context.levelDetails = levelDetails;
|
||||
|
||||
this.handlePlaylistLoaded(
|
||||
levelDetails,
|
||||
response,
|
||||
stats,
|
||||
context,
|
||||
networkDetails,
|
||||
loader,
|
||||
);
|
||||
}
|
||||
|
||||
private handleManifestParsingError(
|
||||
response: LoaderResponse,
|
||||
context: PlaylistLoaderContext,
|
||||
error: Error,
|
||||
networkDetails: any,
|
||||
stats: LoaderStats,
|
||||
): void {
|
||||
this.hls.trigger(Events.ERROR, {
|
||||
type: ErrorTypes.NETWORK_ERROR,
|
||||
details: ErrorDetails.MANIFEST_PARSING_ERROR,
|
||||
fatal: context.type === PlaylistContextType.MANIFEST,
|
||||
url: response.url,
|
||||
err: error,
|
||||
error,
|
||||
reason: error.message,
|
||||
response,
|
||||
context,
|
||||
networkDetails,
|
||||
stats,
|
||||
});
|
||||
}
|
||||
|
||||
private handleNetworkError(
|
||||
context: PlaylistLoaderContext,
|
||||
networkDetails: any,
|
||||
timeout = false,
|
||||
response: { code: number; text: string } | undefined,
|
||||
stats: LoaderStats,
|
||||
): void {
|
||||
let message = `A network ${
|
||||
timeout
|
||||
? 'timeout'
|
||||
: 'error' + (response ? ' (status ' + response.code + ')' : '')
|
||||
} occurred while loading ${context.type}`;
|
||||
if (context.type === PlaylistContextType.LEVEL) {
|
||||
message += `: ${context.level} id: ${context.id}`;
|
||||
} else if (
|
||||
context.type === PlaylistContextType.AUDIO_TRACK ||
|
||||
context.type === PlaylistContextType.SUBTITLE_TRACK
|
||||
) {
|
||||
message += ` id: ${context.id} group-id: "${context.groupId}"`;
|
||||
}
|
||||
const error = new Error(message);
|
||||
logger.warn(`[playlist-loader]: ${message}`);
|
||||
let details = ErrorDetails.UNKNOWN;
|
||||
let fatal = false;
|
||||
|
||||
const loader = this.getInternalLoader(context);
|
||||
|
||||
switch (context.type) {
|
||||
case PlaylistContextType.MANIFEST:
|
||||
details = timeout
|
||||
? ErrorDetails.MANIFEST_LOAD_TIMEOUT
|
||||
: ErrorDetails.MANIFEST_LOAD_ERROR;
|
||||
fatal = true;
|
||||
break;
|
||||
case PlaylistContextType.LEVEL:
|
||||
details = timeout
|
||||
? ErrorDetails.LEVEL_LOAD_TIMEOUT
|
||||
: ErrorDetails.LEVEL_LOAD_ERROR;
|
||||
fatal = false;
|
||||
break;
|
||||
case PlaylistContextType.AUDIO_TRACK:
|
||||
details = timeout
|
||||
? ErrorDetails.AUDIO_TRACK_LOAD_TIMEOUT
|
||||
: ErrorDetails.AUDIO_TRACK_LOAD_ERROR;
|
||||
fatal = false;
|
||||
break;
|
||||
case PlaylistContextType.SUBTITLE_TRACK:
|
||||
details = timeout
|
||||
? ErrorDetails.SUBTITLE_TRACK_LOAD_TIMEOUT
|
||||
: ErrorDetails.SUBTITLE_LOAD_ERROR;
|
||||
fatal = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (loader) {
|
||||
this.resetInternalLoader(context.type);
|
||||
}
|
||||
|
||||
const errorData: ErrorData = {
|
||||
type: ErrorTypes.NETWORK_ERROR,
|
||||
details,
|
||||
fatal,
|
||||
url: context.url,
|
||||
loader,
|
||||
context,
|
||||
error,
|
||||
networkDetails,
|
||||
stats,
|
||||
};
|
||||
|
||||
if (response) {
|
||||
const url = networkDetails?.url || context.url;
|
||||
errorData.response = { url, data: undefined as any, ...response };
|
||||
}
|
||||
|
||||
this.hls.trigger(Events.ERROR, errorData);
|
||||
}
|
||||
|
||||
private handlePlaylistLoaded(
|
||||
levelDetails: LevelDetails,
|
||||
response: LoaderResponse,
|
||||
stats: LoaderStats,
|
||||
context: PlaylistLoaderContext,
|
||||
networkDetails: any,
|
||||
loader: Loader<PlaylistLoaderContext> | undefined,
|
||||
): void {
|
||||
const hls = this.hls;
|
||||
const { type, level, id, groupId, deliveryDirectives } = context;
|
||||
const url = getResponseUrl(response, context);
|
||||
const parent = mapContextToLevelType(context);
|
||||
const levelIndex =
|
||||
typeof context.level === 'number' && parent === PlaylistLevelType.MAIN
|
||||
? (level as number)
|
||||
: undefined;
|
||||
if (!levelDetails.fragments.length) {
|
||||
const error = new Error('No Segments found in Playlist');
|
||||
hls.trigger(Events.ERROR, {
|
||||
type: ErrorTypes.NETWORK_ERROR,
|
||||
details: ErrorDetails.LEVEL_EMPTY_ERROR,
|
||||
fatal: false,
|
||||
url,
|
||||
error,
|
||||
reason: error.message,
|
||||
response,
|
||||
context,
|
||||
level: levelIndex,
|
||||
parent,
|
||||
networkDetails,
|
||||
stats,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!levelDetails.targetduration) {
|
||||
levelDetails.playlistParsingError = new Error('Missing Target Duration');
|
||||
}
|
||||
const error = levelDetails.playlistParsingError;
|
||||
if (error) {
|
||||
hls.trigger(Events.ERROR, {
|
||||
type: ErrorTypes.NETWORK_ERROR,
|
||||
details: ErrorDetails.LEVEL_PARSING_ERROR,
|
||||
fatal: false,
|
||||
url,
|
||||
error,
|
||||
reason: error.message,
|
||||
response,
|
||||
context,
|
||||
level: levelIndex,
|
||||
parent,
|
||||
networkDetails,
|
||||
stats,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (levelDetails.live && loader) {
|
||||
if (loader.getCacheAge) {
|
||||
levelDetails.ageHeader = loader.getCacheAge() || 0;
|
||||
}
|
||||
if (!loader.getCacheAge || isNaN(levelDetails.ageHeader)) {
|
||||
levelDetails.ageHeader = 0;
|
||||
}
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case PlaylistContextType.MANIFEST:
|
||||
case PlaylistContextType.LEVEL:
|
||||
hls.trigger(Events.LEVEL_LOADED, {
|
||||
details: levelDetails,
|
||||
level: levelIndex || 0,
|
||||
id: id || 0,
|
||||
stats,
|
||||
networkDetails,
|
||||
deliveryDirectives,
|
||||
});
|
||||
break;
|
||||
case PlaylistContextType.AUDIO_TRACK:
|
||||
hls.trigger(Events.AUDIO_TRACK_LOADED, {
|
||||
details: levelDetails,
|
||||
id: id || 0,
|
||||
groupId: groupId || '',
|
||||
stats,
|
||||
networkDetails,
|
||||
deliveryDirectives,
|
||||
});
|
||||
break;
|
||||
case PlaylistContextType.SUBTITLE_TRACK:
|
||||
hls.trigger(Events.SUBTITLE_TRACK_LOADED, {
|
||||
details: levelDetails,
|
||||
id: id || 0,
|
||||
groupId: groupId || '',
|
||||
stats,
|
||||
networkDetails,
|
||||
deliveryDirectives,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default PlaylistLoader;
|
||||
Reference in New Issue
Block a user