mirror of
https://github.com/DanielnetoDotCom/YouPHPTube
synced 2025-10-05 10:49:36 +02:00
add p2p support for HLS https://github.com/Novage/p2p-media-loader
This commit is contained in:
parent
64c36d9f4e
commit
0d0338876d
1197 changed files with 121461 additions and 179724 deletions
48
node_modules/hls.js/src/config.ts
generated
vendored
48
node_modules/hls.js/src/config.ts
generated
vendored
|
@ -7,7 +7,9 @@ import BufferController from './controller/buffer-controller';
|
|||
import { TimelineController } from './controller/timeline-controller';
|
||||
import CapLevelController from './controller/cap-level-controller';
|
||||
import FPSController from './controller/fps-controller';
|
||||
import EMEController from './controller/eme-controller';
|
||||
import EMEController, {
|
||||
MediaKeySessionContext,
|
||||
} from './controller/eme-controller';
|
||||
import CMCDController from './controller/cmcd-controller';
|
||||
import XhrLoader from './utils/xhr-loader';
|
||||
import FetchLoader, { fetchSupported } from './utils/fetch-loader';
|
||||
|
@ -15,8 +17,9 @@ import Cues from './utils/cues';
|
|||
import { requestMediaKeySystemAccess } from './utils/mediakeys-helper';
|
||||
import { ILogger, logger } from './utils/logger';
|
||||
|
||||
import type Hls from './hls';
|
||||
import type { CuesInterface } from './utils/cues';
|
||||
import type { MediaKeyFunc } from './utils/mediakeys-helper';
|
||||
import type { MediaKeyFunc, KeySystems } from './utils/mediakeys-helper';
|
||||
import type {
|
||||
FragmentLoaderContext,
|
||||
Loader,
|
||||
|
@ -57,13 +60,49 @@ export type CMCDControllerConfig = {
|
|||
export type DRMSystemOptions = {
|
||||
audioRobustness?: string;
|
||||
videoRobustness?: string;
|
||||
audioEncryptionScheme?: string | null;
|
||||
videoEncryptionScheme?: string | null;
|
||||
persistentState?: MediaKeysRequirement;
|
||||
distinctiveIdentifier?: MediaKeysRequirement;
|
||||
sessionTypes?: string[];
|
||||
sessionType?: string;
|
||||
};
|
||||
|
||||
export type DRMSystemConfiguration = {
|
||||
licenseUrl: string;
|
||||
serverCertificateUrl?: string;
|
||||
generateRequest?: (
|
||||
this: Hls,
|
||||
initDataType: string,
|
||||
initData: ArrayBuffer | null,
|
||||
keyContext: MediaKeySessionContext
|
||||
) =>
|
||||
| { initDataType: string; initData: ArrayBuffer | null }
|
||||
| undefined
|
||||
| never;
|
||||
};
|
||||
|
||||
export type DRMSystemsConfiguration = Partial<
|
||||
Record<KeySystems, DRMSystemConfiguration>
|
||||
>;
|
||||
|
||||
export type EMEControllerConfig = {
|
||||
licenseXhrSetup?: (xhr: XMLHttpRequest, url: string) => void;
|
||||
licenseResponseCallback?: (xhr: XMLHttpRequest, url: string) => ArrayBuffer;
|
||||
licenseXhrSetup?: (
|
||||
this: Hls,
|
||||
xhr: XMLHttpRequest,
|
||||
url: string,
|
||||
keyContext: MediaKeySessionContext,
|
||||
licenseChallenge: Uint8Array
|
||||
) => void | Uint8Array | Promise<Uint8Array | void>;
|
||||
licenseResponseCallback?: (
|
||||
this: Hls,
|
||||
xhr: XMLHttpRequest,
|
||||
url: string,
|
||||
keyContext: MediaKeySessionContext
|
||||
) => ArrayBuffer;
|
||||
emeEnabled: boolean;
|
||||
widevineLicenseUrl?: string;
|
||||
drmSystems: DRMSystemsConfiguration;
|
||||
drmSystemOptions: DRMSystemOptions;
|
||||
requestMediaKeySystemAccessFunc: MediaKeyFunc | null;
|
||||
};
|
||||
|
@ -283,6 +322,7 @@ export const hlsDefaultConfig: HlsConfig = {
|
|||
minAutoBitrate: 0, // used by hls
|
||||
emeEnabled: false, // used by eme-controller
|
||||
widevineLicenseUrl: undefined, // used by eme-controller
|
||||
drmSystems: {}, // used by eme-controller
|
||||
drmSystemOptions: {}, // used by eme-controller
|
||||
requestMediaKeySystemAccessFunc: requestMediaKeySystemAccess, // used by eme-controller
|
||||
testBandwidth: true,
|
||||
|
|
94
node_modules/hls.js/src/controller/abr-controller.ts
generated
vendored
94
node_modules/hls.js/src/controller/abr-controller.ts
generated
vendored
|
@ -1,10 +1,8 @@
|
|||
import EwmaBandWidthEstimator from '../utils/ewma-bandwidth-estimator';
|
||||
import { Events } from '../events';
|
||||
import { BufferHelper } from '../utils/buffer-helper';
|
||||
import { ErrorDetails } from '../errors';
|
||||
import { ErrorDetails, ErrorTypes } from '../errors';
|
||||
import { PlaylistLevelType } from '../types/loader';
|
||||
import { logger } from '../utils/logger';
|
||||
import type { Bufferable } from '../utils/buffer-helper';
|
||||
import type { Fragment } from '../loader/fragment';
|
||||
import type { Part } from '../loader/fragment';
|
||||
import type { LoaderStats } from '../types/loader';
|
||||
|
@ -95,16 +93,19 @@ class AbrController implements ComponentAPI {
|
|||
*/
|
||||
private _abandonRulesCheck() {
|
||||
const { fragCurrent: frag, partCurrent: part, hls } = this;
|
||||
const { autoLevelEnabled, config, media } = hls;
|
||||
const { autoLevelEnabled, media } = hls;
|
||||
if (!frag || !media) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stats: LoaderStats = part ? part.stats : frag.stats;
|
||||
const duration = part ? part.duration : frag.duration;
|
||||
// If loading has been aborted and not in lowLatencyMode, stop timer and return
|
||||
if (stats.aborted) {
|
||||
logger.warn('frag loader destroy or aborted, disarm abandonRules');
|
||||
// If frag loading is aborted, complete, or from lowest level, stop timer and return
|
||||
if (
|
||||
stats.aborted ||
|
||||
(stats.loaded && stats.loaded === stats.total) ||
|
||||
frag.level === 0
|
||||
) {
|
||||
this.clearTimer();
|
||||
// reset forced auto level value so that next level will be selected
|
||||
this._nextAutoLevel = -1;
|
||||
|
@ -121,6 +122,11 @@ class AbrController implements ComponentAPI {
|
|||
return;
|
||||
}
|
||||
|
||||
const bufferInfo = hls.mainForwardBufferInfo;
|
||||
if (bufferInfo === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const requestDelay = performance.now() - stats.loading.start;
|
||||
const playbackRate = Math.abs(media.playbackRate);
|
||||
// In order to work with a stable bandwidth, only begin monitoring bandwidth after half of the fragment has been loaded
|
||||
|
@ -128,32 +134,25 @@ class AbrController implements ComponentAPI {
|
|||
return;
|
||||
}
|
||||
|
||||
const loadedFirstByte = stats.loaded && stats.loading.first;
|
||||
const bwEstimate: number = this.bwEstimator.getEstimate();
|
||||
const { levels, minAutoLevel } = hls;
|
||||
const level = levels[frag.level];
|
||||
const expectedLen =
|
||||
stats.total ||
|
||||
Math.max(stats.loaded, Math.round((duration * level.maxBitrate) / 8));
|
||||
const loadRate = Math.max(
|
||||
1,
|
||||
stats.bwEstimate
|
||||
? stats.bwEstimate / 8
|
||||
: (stats.loaded * 1000) / requestDelay
|
||||
);
|
||||
// fragLoadDelay is an estimate of the time (in seconds) it will take to buffer the entire fragment
|
||||
const fragLoadedDelay = (expectedLen - stats.loaded) / loadRate;
|
||||
const loadRate = loadedFirstByte ? (stats.loaded * 1000) / requestDelay : 0;
|
||||
|
||||
// fragLoadDelay is an estimate of the time (in seconds) it will take to buffer the remainder of the fragment
|
||||
const fragLoadedDelay = loadRate
|
||||
? (expectedLen - stats.loaded) / loadRate
|
||||
: (expectedLen * 8) / bwEstimate;
|
||||
|
||||
const pos = media.currentTime;
|
||||
// bufferStarvationDelay is an estimate of the amount time (in seconds) it will take to exhaust the buffer
|
||||
const bufferStarvationDelay =
|
||||
(BufferHelper.bufferInfo(media, pos, config.maxBufferHole).end - pos) /
|
||||
playbackRate;
|
||||
const bufferStarvationDelay = bufferInfo.len / playbackRate;
|
||||
|
||||
// Attempt an emergency downswitch only if less than 2 fragment lengths are buffered, and the time to finish loading
|
||||
// the current fragment is greater than the amount of buffer we have left
|
||||
if (
|
||||
bufferStarvationDelay >= (2 * duration) / playbackRate ||
|
||||
fragLoadedDelay <= bufferStarvationDelay
|
||||
) {
|
||||
// Only downswitch if the time to finish loading the current fragment is greater than the amount of buffer left
|
||||
if (fragLoadedDelay <= bufferStarvationDelay) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -169,8 +168,9 @@ class AbrController implements ComponentAPI {
|
|||
// 0.8 : consider only 80% of current bw to be conservative
|
||||
// 8 = bits per byte (bps/Bps)
|
||||
const levelNextBitrate = levels[nextLoadLevel].maxBitrate;
|
||||
fragLevelNextLoadedDelay =
|
||||
(duration * levelNextBitrate) / (8 * 0.8 * loadRate);
|
||||
fragLevelNextLoadedDelay = loadRate
|
||||
? (duration * levelNextBitrate) / (8 * 0.8 * loadRate)
|
||||
: (duration * levelNextBitrate) / bwEstimate;
|
||||
|
||||
if (fragLevelNextLoadedDelay < bufferStarvationDelay) {
|
||||
break;
|
||||
|
@ -181,7 +181,6 @@ class AbrController implements ComponentAPI {
|
|||
if (fragLevelNextLoadedDelay >= fragLoadedDelay) {
|
||||
return;
|
||||
}
|
||||
const bwEstimate: number = this.bwEstimator.getEstimate();
|
||||
logger.warn(`Fragment ${frag.sn}${
|
||||
part ? ' part ' + part.index : ''
|
||||
} of level ${
|
||||
|
@ -196,11 +195,14 @@ class AbrController implements ComponentAPI {
|
|||
)} s
|
||||
Time to underbuffer: ${bufferStarvationDelay.toFixed(3)} s`);
|
||||
hls.nextLoadLevel = nextLoadLevel;
|
||||
this.bwEstimator.sample(requestDelay, stats.loaded);
|
||||
if (loadedFirstByte) {
|
||||
// If there has been loading progress, sample bandwidth
|
||||
this.bwEstimator.sample(requestDelay, stats.loaded);
|
||||
}
|
||||
this.clearTimer();
|
||||
if (frag.loader) {
|
||||
if (frag.loader || frag.keyLoader) {
|
||||
this.fragCurrent = this.partCurrent = null;
|
||||
frag.loader.abort();
|
||||
frag.abortRequests();
|
||||
}
|
||||
hls.trigger(Events.FRAG_LOAD_EMERGENCY_ABORTED, { frag, part, stats });
|
||||
}
|
||||
|
@ -273,13 +275,21 @@ class AbrController implements ComponentAPI {
|
|||
|
||||
protected onError(event: Events.ERROR, data: ErrorData) {
|
||||
// stop timer in case of frag loading error
|
||||
switch (data.details) {
|
||||
case ErrorDetails.FRAG_LOAD_ERROR:
|
||||
case ErrorDetails.FRAG_LOAD_TIMEOUT:
|
||||
if (data.frag?.type === PlaylistLevelType.MAIN) {
|
||||
if (data.type === ErrorTypes.KEY_SYSTEM_ERROR) {
|
||||
this.clearTimer();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
return;
|
||||
}
|
||||
switch (data.details) {
|
||||
case ErrorDetails.FRAG_LOAD_ERROR:
|
||||
case ErrorDetails.FRAG_LOAD_TIMEOUT:
|
||||
case ErrorDetails.KEY_LOAD_ERROR:
|
||||
case ErrorDetails.KEY_LOAD_TIMEOUT:
|
||||
this.clearTimer();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -319,7 +329,6 @@ class AbrController implements ComponentAPI {
|
|||
: fragCurrent
|
||||
? fragCurrent.duration
|
||||
: 0;
|
||||
const pos = media ? media.currentTime : 0;
|
||||
|
||||
// playbackRate is the absolute value of the playback rate; if media.playbackRate is 0, we use 1 to load as
|
||||
// if we're playing back at the normal rate.
|
||||
|
@ -329,11 +338,9 @@ class AbrController implements ComponentAPI {
|
|||
? this.bwEstimator.getEstimate()
|
||||
: config.abrEwmaDefaultEstimate;
|
||||
// bufferStarvationDelay is the wall-clock time left until the playback buffer is exhausted.
|
||||
const bufferInfo = hls.mainForwardBufferInfo;
|
||||
const bufferStarvationDelay =
|
||||
(BufferHelper.bufferInfo(media as Bufferable, pos, config.maxBufferHole)
|
||||
.end -
|
||||
pos) /
|
||||
playbackRate;
|
||||
(bufferInfo ? bufferInfo.len : 0) / playbackRate;
|
||||
|
||||
// First, look to see if we can find a level matching with our avg bandwidth AND that could also guarantee no rebuffering at all
|
||||
let bestLevel = this.findBestLevel(
|
||||
|
@ -461,7 +468,8 @@ class AbrController implements ComponentAPI {
|
|||
// fragment fetchDuration unknown OR live stream OR fragment fetchDuration less than max allowed fetch duration, then this level matches
|
||||
// we don't account for max Fetch Duration for live streams, this is to avoid switching down when near the edge of live sliding window ...
|
||||
// special case to support startLevel = -1 (bitrateTest) on live streams : in that case we should not exit loop so that findBestLevel will return -1
|
||||
(!fetchDuration ||
|
||||
(fetchDuration === 0 ||
|
||||
!Number.isFinite(fetchDuration) ||
|
||||
(live && !this.bitrateTestDelay) ||
|
||||
fetchDuration < maxFetchDuration)
|
||||
) {
|
||||
|
|
68
node_modules/hls.js/src/controller/audio-stream-controller.ts
generated
vendored
68
node_modules/hls.js/src/controller/audio-stream-controller.ts
generated
vendored
|
@ -10,11 +10,12 @@ import TransmuxerInterface from '../demux/transmuxer-interface';
|
|||
import { ChunkMetadata } from '../types/transmuxer';
|
||||
import { fragmentWithinToleranceTest } from './fragment-finders';
|
||||
import { alignMediaPlaylistByPDT } from '../utils/discontinuities';
|
||||
import { ErrorDetails } from '../errors';
|
||||
import { ErrorDetails, ErrorTypes } from '../errors';
|
||||
import type { NetworkComponentAPI } from '../types/component-api';
|
||||
import type { FragmentTracker } from './fragment-tracker';
|
||||
import type { TransmuxerResult } from '../types/transmuxer';
|
||||
import type Hls from '../hls';
|
||||
import type { FragmentTracker } from './fragment-tracker';
|
||||
import type KeyLoader from '../loader/key-loader';
|
||||
import type { TransmuxerResult } from '../types/transmuxer';
|
||||
import type { LevelDetails } from '../loader/level-details';
|
||||
import type { TrackSet } from '../types/track';
|
||||
import type {
|
||||
|
@ -56,8 +57,12 @@ class AudioStreamController
|
|||
private bufferFlushed: boolean = false;
|
||||
private cachedTrackLoadedData: TrackLoadedData | null = null;
|
||||
|
||||
constructor(hls: Hls, fragmentTracker: FragmentTracker) {
|
||||
super(hls, fragmentTracker, '[audio-stream-controller]');
|
||||
constructor(
|
||||
hls: Hls,
|
||||
fragmentTracker: FragmentTracker,
|
||||
keyLoader: KeyLoader
|
||||
) {
|
||||
super(hls, fragmentTracker, keyLoader, '[audio-stream-controller]');
|
||||
this._registerListeners();
|
||||
}
|
||||
|
||||
|
@ -252,12 +257,6 @@ class AudioStreamController
|
|||
// Exit early if we don't have media or if the media hasn't buffered anything yet (readyState 0)
|
||||
return;
|
||||
}
|
||||
const mediaBuffer = this.mediaBuffer ? this.mediaBuffer : media;
|
||||
const buffered = mediaBuffer.buffered;
|
||||
|
||||
if (!this.loadedmetadata && buffered.length) {
|
||||
this.loadedmetadata = true;
|
||||
}
|
||||
|
||||
this.lastCurrentTime = media.currentTime;
|
||||
}
|
||||
|
@ -307,25 +306,25 @@ class AudioStreamController
|
|||
if (bufferInfo === null) {
|
||||
return;
|
||||
}
|
||||
const mainBufferInfo = this.getFwdBufferInfo(
|
||||
this.videoBuffer ? this.videoBuffer : this.media,
|
||||
PlaylistLevelType.MAIN
|
||||
);
|
||||
const bufferLen = bufferInfo.len;
|
||||
const maxBufLen = this.getMaxBufferLength(mainBufferInfo?.len);
|
||||
const audioSwitch = this.audioSwitch;
|
||||
|
||||
// if buffer length is less than maxBufLen try to load a new fragment
|
||||
if (bufferLen >= maxBufLen && !audioSwitch) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!audioSwitch && this._streamEnded(bufferInfo, trackDetails)) {
|
||||
hls.trigger(Events.BUFFER_EOS, { type: 'audio' });
|
||||
this.state = State.ENDED;
|
||||
return;
|
||||
}
|
||||
|
||||
const mainBufferInfo = this.getFwdBufferInfo(
|
||||
this.videoBuffer ? this.videoBuffer : this.media,
|
||||
PlaylistLevelType.MAIN
|
||||
);
|
||||
const bufferLen = bufferInfo.len;
|
||||
const maxBufLen = this.getMaxBufferLength(mainBufferInfo?.len);
|
||||
|
||||
// if buffer length is less than maxBufLen try to load a new fragment
|
||||
if (bufferLen >= maxBufLen && !audioSwitch) {
|
||||
return;
|
||||
}
|
||||
const fragments = trackDetails.fragments;
|
||||
const start = fragments[0].start;
|
||||
let targetBufferTime = bufferInfo.end;
|
||||
|
@ -363,11 +362,7 @@ class AudioStreamController
|
|||
return;
|
||||
}
|
||||
|
||||
if (frag.decryptdata?.keyFormat === 'identity' && !frag.decryptdata?.key) {
|
||||
this.loadKey(frag, trackDetails);
|
||||
} else {
|
||||
this.loadFragment(frag, trackDetails, targetBufferTime);
|
||||
}
|
||||
this.loadFragment(frag, trackDetails, targetBufferTime);
|
||||
}
|
||||
|
||||
protected getMaxBufferLength(mainBufferLength?: number): number {
|
||||
|
@ -400,8 +395,8 @@ class AudioStreamController
|
|||
this.trackId = data.id;
|
||||
const { fragCurrent } = this;
|
||||
|
||||
if (fragCurrent?.loader) {
|
||||
fragCurrent.loader.abort();
|
||||
if (fragCurrent) {
|
||||
fragCurrent.abortRequests();
|
||||
}
|
||||
this.fragCurrent = null;
|
||||
this.clearWaitingFragment();
|
||||
|
@ -599,6 +594,11 @@ class AudioStreamController
|
|||
onFragBuffered(event: Events.FRAG_BUFFERED, data: FragBufferedData) {
|
||||
const { frag, part } = data;
|
||||
if (frag.type !== PlaylistLevelType.AUDIO) {
|
||||
if (!this.loadedmetadata && frag.type === PlaylistLevelType.MAIN) {
|
||||
if ((this.videoBuffer || this.media)?.buffered.length) {
|
||||
this.loadedmetadata = true;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this.fragContextChanged(frag)) {
|
||||
|
@ -624,9 +624,14 @@ class AudioStreamController
|
|||
}
|
||||
|
||||
private onError(event: Events.ERROR, data: ErrorData) {
|
||||
if (data.type === ErrorTypes.KEY_SYSTEM_ERROR) {
|
||||
this.onFragmentOrKeyLoadError(PlaylistLevelType.AUDIO, data);
|
||||
return;
|
||||
}
|
||||
switch (data.details) {
|
||||
case ErrorDetails.FRAG_LOAD_ERROR:
|
||||
case ErrorDetails.FRAG_LOAD_TIMEOUT:
|
||||
case ErrorDetails.FRAG_PARSING_ERROR:
|
||||
case ErrorDetails.KEY_LOAD_ERROR:
|
||||
case ErrorDetails.KEY_LOAD_TIMEOUT:
|
||||
// TODO: Skip fragments that do not belong to this.fragCurrent audio-group id
|
||||
|
@ -683,6 +688,9 @@ class AudioStreamController
|
|||
) {
|
||||
if (type === ElementaryStreamTypes.AUDIO) {
|
||||
this.bufferFlushed = true;
|
||||
if (this.state === State.ENDED) {
|
||||
this.state = State.IDLE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -827,7 +835,7 @@ class AudioStreamController
|
|||
fragState === FragmentState.PARTIAL
|
||||
) {
|
||||
if (frag.sn === 'initSegment') {
|
||||
this._loadInitSegment(frag);
|
||||
this._loadInitSegment(frag, trackDetails);
|
||||
} else if (trackDetails.live && !Number.isFinite(this.initPTS[frag.cc])) {
|
||||
this.log(
|
||||
`Waiting for video PTS in continuity counter ${frag.cc} of live stream before loading audio fragment ${frag.sn} of level ${this.trackId}`
|
||||
|
|
1
node_modules/hls.js/src/controller/audio-track-controller.ts
generated
vendored
1
node_modules/hls.js/src/controller/audio-track-controller.ts
generated
vendored
|
@ -240,6 +240,7 @@ class AudioTrackController extends BasePlaylistController {
|
|||
}
|
||||
|
||||
protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters): void {
|
||||
super.loadPlaylist();
|
||||
const audioTrack = this.tracksInGroup[this.trackId];
|
||||
if (this.shouldLoadTrack(audioTrack)) {
|
||||
const id = audioTrack.id;
|
||||
|
|
105
node_modules/hls.js/src/controller/base-playlist-controller.ts
generated
vendored
105
node_modules/hls.js/src/controller/base-playlist-controller.ts
generated
vendored
|
@ -17,6 +17,7 @@ import { ErrorTypes } from '../errors';
|
|||
export default class BasePlaylistController implements NetworkComponentAPI {
|
||||
protected hls: Hls;
|
||||
protected timer: number = -1;
|
||||
protected requestScheduled: number = -1;
|
||||
protected canLoad: boolean = false;
|
||||
protected retryCount: number = 0;
|
||||
protected log: (msg: any) => void;
|
||||
|
@ -35,8 +36,12 @@ export default class BasePlaylistController implements NetworkComponentAPI {
|
|||
}
|
||||
|
||||
protected onError(event: Events.ERROR, data: ErrorData): void {
|
||||
if (data.fatal && data.type === ErrorTypes.NETWORK_ERROR) {
|
||||
this.clearTimer();
|
||||
if (
|
||||
data.fatal &&
|
||||
(data.type === ErrorTypes.NETWORK_ERROR ||
|
||||
data.type === ErrorTypes.KEY_SYSTEM_ERROR)
|
||||
) {
|
||||
this.stopLoad();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -48,6 +53,7 @@ export default class BasePlaylistController implements NetworkComponentAPI {
|
|||
public startLoad(): void {
|
||||
this.canLoad = true;
|
||||
this.retryCount = 0;
|
||||
this.requestScheduled = -1;
|
||||
this.loadPlaylist();
|
||||
}
|
||||
|
||||
|
@ -58,38 +64,48 @@ export default class BasePlaylistController implements NetworkComponentAPI {
|
|||
|
||||
protected switchParams(
|
||||
playlistUri: string,
|
||||
previous?: LevelDetails
|
||||
previous: LevelDetails | undefined
|
||||
): HlsUrlParameters | undefined {
|
||||
const renditionReports = previous?.renditionReports;
|
||||
if (renditionReports) {
|
||||
for (let i = 0; i < renditionReports.length; i++) {
|
||||
const attr = renditionReports[i];
|
||||
const uri = '' + attr.URI;
|
||||
let uri: string;
|
||||
try {
|
||||
uri = new self.URL(attr.URI, previous.url).href;
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`Could not construct new URL for Rendition Report: ${error}`
|
||||
);
|
||||
uri = attr.URI || '';
|
||||
}
|
||||
if (uri === playlistUri.slice(-uri.length)) {
|
||||
const msn = parseInt(attr['LAST-MSN']);
|
||||
let part = parseInt(attr['LAST-PART']);
|
||||
if (previous && this.hls.config.lowLatencyMode) {
|
||||
const msn = parseInt(attr['LAST-MSN']) || previous?.lastPartSn;
|
||||
let part = parseInt(attr['LAST-PART']) || previous?.lastPartIndex;
|
||||
if (this.hls.config.lowLatencyMode) {
|
||||
const currentGoal = Math.min(
|
||||
previous.age - previous.partTarget,
|
||||
previous.targetduration
|
||||
);
|
||||
if (part !== undefined && currentGoal > previous.partTarget) {
|
||||
if (part >= 0 && currentGoal > previous.partTarget) {
|
||||
part += 1;
|
||||
}
|
||||
}
|
||||
if (Number.isFinite(msn)) {
|
||||
return new HlsUrlParameters(
|
||||
msn,
|
||||
Number.isFinite(part) ? part : undefined,
|
||||
HlsSkip.No
|
||||
);
|
||||
}
|
||||
return new HlsUrlParameters(
|
||||
msn,
|
||||
part >= 0 ? part : undefined,
|
||||
HlsSkip.No
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters): void {}
|
||||
protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters): void {
|
||||
if (this.requestScheduled === -1) {
|
||||
this.requestScheduled = self.performance.now();
|
||||
}
|
||||
}
|
||||
|
||||
protected shouldLoadTrack(track: MediaPlaylist): boolean {
|
||||
return (
|
||||
|
@ -108,8 +124,9 @@ export default class BasePlaylistController implements NetworkComponentAPI {
|
|||
const { details, stats } = data;
|
||||
|
||||
// Set last updated date-time
|
||||
const elapsed = stats.loading.end
|
||||
? Math.max(0, self.performance.now() - stats.loading.end)
|
||||
const now = self.performance.now();
|
||||
const elapsed = stats.loading.first
|
||||
? Math.max(0, now - stats.loading.first)
|
||||
: 0;
|
||||
details.advancedDateTime = Date.now() - elapsed;
|
||||
|
||||
|
@ -204,16 +221,53 @@ export default class BasePlaylistController implements NetworkComponentAPI {
|
|||
part
|
||||
);
|
||||
}
|
||||
let reloadInterval = computeReloadInterval(details, stats);
|
||||
if (msn !== undefined && details.canBlockReload) {
|
||||
reloadInterval -= details.partTarget || 1;
|
||||
}
|
||||
this.log(
|
||||
`reload live playlist ${index} in ${Math.round(reloadInterval)} ms`
|
||||
const bufferInfo = this.hls.mainForwardBufferInfo;
|
||||
const position = bufferInfo ? bufferInfo.end - bufferInfo.len : 0;
|
||||
const distanceToLiveEdgeMs = (details.edge - position) * 1000;
|
||||
const reloadInterval = computeReloadInterval(
|
||||
details,
|
||||
distanceToLiveEdgeMs
|
||||
);
|
||||
if (!details.updated) {
|
||||
this.requestScheduled = -1;
|
||||
} else if (now > this.requestScheduled + reloadInterval) {
|
||||
this.requestScheduled = stats.loading.start;
|
||||
}
|
||||
|
||||
if (msn !== undefined && details.canBlockReload) {
|
||||
this.requestScheduled =
|
||||
stats.loading.first +
|
||||
reloadInterval -
|
||||
(details.partTarget * 1000 || 1000);
|
||||
} else {
|
||||
this.requestScheduled =
|
||||
(this.requestScheduled === -1 ? now : this.requestScheduled) +
|
||||
reloadInterval;
|
||||
}
|
||||
let estimatedTimeUntilUpdate = this.requestScheduled - now;
|
||||
estimatedTimeUntilUpdate = Math.max(0, estimatedTimeUntilUpdate);
|
||||
this.log(
|
||||
`reload live playlist ${index} in ${Math.round(
|
||||
estimatedTimeUntilUpdate
|
||||
)} ms`
|
||||
);
|
||||
// this.log(
|
||||
// `live reload ${details.updated ? 'REFRESHED' : 'MISSED'}
|
||||
// reload in ${estimatedTimeUntilUpdate / 1000}
|
||||
// round trip ${(stats.loading.end - stats.loading.start) / 1000}
|
||||
// diff ${
|
||||
// (reloadInterval -
|
||||
// (estimatedTimeUntilUpdate + stats.loading.end - stats.loading.start)) /
|
||||
// 1000
|
||||
// }
|
||||
// reload interval ${reloadInterval / 1000}
|
||||
// target duration ${details.targetduration}
|
||||
// distance to edge ${distanceToLiveEdgeMs / 1000}`
|
||||
// );
|
||||
|
||||
this.timer = self.setTimeout(
|
||||
() => this.loadPlaylist(deliveryDirectives),
|
||||
reloadInterval
|
||||
estimatedTimeUntilUpdate
|
||||
);
|
||||
} else {
|
||||
this.clearTimer();
|
||||
|
@ -239,6 +293,7 @@ export default class BasePlaylistController implements NetworkComponentAPI {
|
|||
const { config } = this.hls;
|
||||
const retry = this.retryCount < config.levelLoadingMaxRetry;
|
||||
if (retry) {
|
||||
this.requestScheduled = -1;
|
||||
this.retryCount++;
|
||||
if (
|
||||
errorEvent.details.indexOf('LoadTimeOut') > -1 &&
|
||||
|
|
322
node_modules/hls.js/src/controller/base-stream-controller.ts
generated
vendored
322
node_modules/hls.js/src/controller/base-stream-controller.ts
generated
vendored
|
@ -1,9 +1,9 @@
|
|||
import TaskLoop from '../task-loop';
|
||||
import { FragmentState } from './fragment-tracker';
|
||||
import { Bufferable, BufferHelper } from '../utils/buffer-helper';
|
||||
import { Bufferable, BufferHelper, BufferInfo } from '../utils/buffer-helper';
|
||||
import { logger } from '../utils/logger';
|
||||
import { Events } from '../events';
|
||||
import { ErrorDetails } from '../errors';
|
||||
import { ErrorDetails, ErrorTypes } from '../errors';
|
||||
import { ChunkMetadata } from '../types/transmuxer';
|
||||
import { appendUint8Array } from '../utils/mp4-tools';
|
||||
import { alignStream } from '../utils/discontinuities';
|
||||
|
@ -23,6 +23,7 @@ import FragmentLoader, {
|
|||
FragmentLoadProgressCallback,
|
||||
LoadError,
|
||||
} from '../loader/fragment-loader';
|
||||
import KeyLoader from '../loader/key-loader';
|
||||
import { LevelDetails } from '../loader/level-details';
|
||||
import Decrypter from '../crypt/decrypter';
|
||||
import TimeRanges from '../utils/time-ranges';
|
||||
|
@ -42,7 +43,6 @@ import type { Level } from '../types/level';
|
|||
import type { RemuxedTrack } from '../types/remuxer';
|
||||
import type Hls from '../hls';
|
||||
import type { HlsConfig } from '../config';
|
||||
import type { HlsEventEmitter } from '../events';
|
||||
import type { NetworkComponentAPI } from '../types/component-api';
|
||||
import type { SourceBufferName } from '../types/buffer';
|
||||
|
||||
|
@ -86,7 +86,8 @@ export default class BaseStreamController
|
|||
protected fragLoadError: number = 0;
|
||||
protected retryDate: number = 0;
|
||||
protected levels: Array<Level> | null = null;
|
||||
protected fragmentLoader!: FragmentLoader;
|
||||
protected fragmentLoader: FragmentLoader;
|
||||
protected keyLoader: KeyLoader;
|
||||
protected levelLastLoaded: number | null = null;
|
||||
protected startFragRequested: boolean = false;
|
||||
protected decrypter: Decrypter;
|
||||
|
@ -98,17 +99,22 @@ export default class BaseStreamController
|
|||
protected log: (msg: any) => void;
|
||||
protected warn: (msg: any) => void;
|
||||
|
||||
constructor(hls: Hls, fragmentTracker: FragmentTracker, logPrefix: string) {
|
||||
constructor(
|
||||
hls: Hls,
|
||||
fragmentTracker: FragmentTracker,
|
||||
keyLoader: KeyLoader,
|
||||
logPrefix: string
|
||||
) {
|
||||
super();
|
||||
this.logPrefix = logPrefix;
|
||||
this.log = logger.log.bind(logger, `${logPrefix}:`);
|
||||
this.warn = logger.warn.bind(logger, `${logPrefix}:`);
|
||||
this.hls = hls;
|
||||
this.fragmentLoader = new FragmentLoader(hls.config);
|
||||
this.keyLoader = keyLoader;
|
||||
this.fragmentTracker = fragmentTracker;
|
||||
this.config = hls.config;
|
||||
this.decrypter = new Decrypter(hls as HlsEventEmitter, hls.config);
|
||||
hls.on(Events.KEY_LOADED, this.onKeyLoaded, this);
|
||||
this.decrypter = new Decrypter(hls.config);
|
||||
hls.on(Events.LEVEL_SWITCHING, this.onLevelSwitching, this);
|
||||
}
|
||||
|
||||
|
@ -123,8 +129,10 @@ export default class BaseStreamController
|
|||
|
||||
public stopLoad() {
|
||||
this.fragmentLoader.abort();
|
||||
this.keyLoader.abort();
|
||||
const frag = this.fragCurrent;
|
||||
if (frag) {
|
||||
frag.abortRequests();
|
||||
this.fragmentTracker.removeFragment(frag);
|
||||
}
|
||||
this.resetTransmuxer();
|
||||
|
@ -135,43 +143,46 @@ export default class BaseStreamController
|
|||
this.state = State.STOPPED;
|
||||
}
|
||||
|
||||
protected _streamEnded(bufferInfo, levelDetails: LevelDetails): boolean {
|
||||
const { fragCurrent, fragmentTracker } = this;
|
||||
// we just got done loading the final fragment and there is no other buffered range after ...
|
||||
// rationale is that in case there are any buffered ranges after, it means that there are unbuffered portion in between
|
||||
// so we should not switch to ENDED in that case, to be able to buffer them
|
||||
protected _streamEnded(
|
||||
bufferInfo: BufferInfo,
|
||||
levelDetails: LevelDetails
|
||||
): boolean {
|
||||
// If playlist is live, there is another buffered range after the current range, nothing buffered, media is detached,
|
||||
// of nothing loading/loaded return false
|
||||
if (
|
||||
!levelDetails.live &&
|
||||
fragCurrent &&
|
||||
this.media &&
|
||||
// NOTE: Because of the way parts are currently parsed/represented in the playlist, we can end up
|
||||
// in situations where the current fragment is actually greater than levelDetails.endSN. While
|
||||
// this feels like the "wrong place" to account for that, this is a narrower/safer change than
|
||||
// updating e.g. M3U8Parser::parseLevelPlaylist().
|
||||
fragCurrent.sn >= levelDetails.endSN &&
|
||||
!bufferInfo.nextStart
|
||||
levelDetails.live ||
|
||||
bufferInfo.nextStart ||
|
||||
!bufferInfo.end ||
|
||||
!this.media
|
||||
) {
|
||||
const partList = levelDetails.partList;
|
||||
// Since the last part isn't guaranteed to correspond to fragCurrent for ll-hls, check instead if the last part is buffered.
|
||||
if (partList?.length) {
|
||||
const lastPart = partList[partList.length - 1];
|
||||
|
||||
// Checking the midpoint of the part for potential margin of error and related issues.
|
||||
// NOTE: Technically I believe parts could yield content that is < the computed duration (including potential a duration of 0)
|
||||
// and still be spec-compliant, so there may still be edge cases here. Likewise, there could be issues in end of stream
|
||||
// part mismatches for independent audio and video playlists/segments.
|
||||
const lastPartBuffered = BufferHelper.isBuffered(
|
||||
this.media,
|
||||
lastPart.start + lastPart.duration / 2
|
||||
);
|
||||
return lastPartBuffered;
|
||||
}
|
||||
const fragState = fragmentTracker.getState(fragCurrent);
|
||||
return (
|
||||
fragState === FragmentState.PARTIAL || fragState === FragmentState.OK
|
||||
);
|
||||
return false;
|
||||
}
|
||||
const partList = levelDetails.partList;
|
||||
// Since the last part isn't guaranteed to correspond to the last playlist segment for Low-Latency HLS,
|
||||
// check instead if the last part is buffered.
|
||||
if (partList?.length) {
|
||||
const lastPart = partList[partList.length - 1];
|
||||
|
||||
// Checking the midpoint of the part for potential margin of error and related issues.
|
||||
// NOTE: Technically I believe parts could yield content that is < the computed duration (including potential a duration of 0)
|
||||
// and still be spec-compliant, so there may still be edge cases here. Likewise, there could be issues in end of stream
|
||||
// part mismatches for independent audio and video playlists/segments.
|
||||
const lastPartBuffered = BufferHelper.isBuffered(
|
||||
this.media,
|
||||
lastPart.start + lastPart.duration / 2
|
||||
);
|
||||
return lastPartBuffered;
|
||||
}
|
||||
|
||||
const playlistType =
|
||||
levelDetails.fragments[levelDetails.fragments.length - 1].type;
|
||||
return this.fragmentTracker.isEndListAppended(playlistType);
|
||||
}
|
||||
|
||||
protected getLevelDetails(): LevelDetails | undefined {
|
||||
if (this.levels && this.levelLastLoaded !== null) {
|
||||
return this.levels[this.levelLastLoaded]?.details;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected onMediaAttached(
|
||||
|
@ -202,6 +213,9 @@ export default class BaseStreamController
|
|||
media.removeEventListener('ended', this.onvended);
|
||||
this.onvseeking = this.onvended = null;
|
||||
}
|
||||
if (this.keyLoader) {
|
||||
this.keyLoader.detach();
|
||||
}
|
||||
this.media = this.mediaBuffer = null;
|
||||
this.loadedmetadata = false;
|
||||
this.fragmentTracker.removeAllFragments();
|
||||
|
@ -223,24 +237,31 @@ export default class BaseStreamController
|
|||
}, state: ${state}`
|
||||
);
|
||||
|
||||
if (state === State.ENDED) {
|
||||
if (this.state === State.ENDED) {
|
||||
this.resetLoadingState();
|
||||
} else if (fragCurrent && !bufferInfo.len) {
|
||||
// check if we are seeking to a unbuffered area AND if frag loading is in progress
|
||||
} else if (fragCurrent) {
|
||||
// Seeking while frag load is in progress
|
||||
const tolerance = config.maxFragLookUpTolerance;
|
||||
const fragStartOffset = fragCurrent.start - tolerance;
|
||||
const fragEndOffset =
|
||||
fragCurrent.start + fragCurrent.duration + tolerance;
|
||||
const pastFragment = currentTime > fragEndOffset;
|
||||
// check if the seek position is past current fragment, and if so abort loading
|
||||
if (currentTime < fragStartOffset || pastFragment) {
|
||||
if (pastFragment && fragCurrent.loader) {
|
||||
this.log(
|
||||
'seeking outside of buffer while fragment load in progress, cancel fragment load'
|
||||
);
|
||||
fragCurrent.loader.abort();
|
||||
// if seeking out of buffered range or into new one
|
||||
if (
|
||||
!bufferInfo.len ||
|
||||
fragEndOffset < bufferInfo.start ||
|
||||
fragStartOffset > bufferInfo.end
|
||||
) {
|
||||
const pastFragment = currentTime > fragEndOffset;
|
||||
// if the seek position is outside the current fragment range
|
||||
if (currentTime < fragStartOffset || pastFragment) {
|
||||
if (pastFragment && fragCurrent.loader) {
|
||||
this.log(
|
||||
'seeking outside of buffer while fragment load in progress, cancel fragment load'
|
||||
);
|
||||
fragCurrent.abortRequests();
|
||||
}
|
||||
this.resetLoadingState();
|
||||
}
|
||||
this.resetLoadingState();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -262,21 +283,6 @@ export default class BaseStreamController
|
|||
this.startPosition = this.lastCurrentTime = 0;
|
||||
}
|
||||
|
||||
onKeyLoaded(event: Events.KEY_LOADED, data: KeyLoadedData) {
|
||||
if (
|
||||
this.state !== State.KEY_LOADING ||
|
||||
data.frag !== this.fragCurrent ||
|
||||
!this.levels
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.state = State.IDLE;
|
||||
const levelDetails = this.levels[data.frag.level].details;
|
||||
if (levelDetails) {
|
||||
this.loadFragment(data.frag, levelDetails, data.frag.start);
|
||||
}
|
||||
}
|
||||
|
||||
protected onLevelSwitching(
|
||||
event: Events.LEVEL_SWITCHING,
|
||||
data: LevelSwitchingData
|
||||
|
@ -291,11 +297,13 @@ export default class BaseStreamController
|
|||
|
||||
protected onHandlerDestroyed() {
|
||||
this.state = State.STOPPED;
|
||||
this.hls.off(Events.KEY_LOADED, this.onKeyLoaded, this);
|
||||
this.hls.off(Events.LEVEL_SWITCHING, this.onLevelSwitching, this);
|
||||
if (this.fragmentLoader) {
|
||||
this.fragmentLoader.destroy();
|
||||
}
|
||||
if (this.keyLoader) {
|
||||
this.keyLoader.destroy();
|
||||
}
|
||||
if (this.decrypter) {
|
||||
this.decrypter.destroy();
|
||||
}
|
||||
|
@ -304,23 +312,13 @@ export default class BaseStreamController
|
|||
this.log =
|
||||
this.warn =
|
||||
this.decrypter =
|
||||
this.keyLoader =
|
||||
this.fragmentLoader =
|
||||
this.fragmentTracker =
|
||||
null as any;
|
||||
super.onHandlerDestroyed();
|
||||
}
|
||||
|
||||
protected loadKey(frag: Fragment, details: LevelDetails) {
|
||||
this.log(
|
||||
`Loading key for ${frag.sn} of [${details.startSN}-${details.endSN}], ${
|
||||
this.logPrefix === '[stream-controller]' ? 'level' : 'track'
|
||||
} ${frag.level}`
|
||||
);
|
||||
this.state = State.KEY_LOADING;
|
||||
this.fragCurrent = frag;
|
||||
this.hls.trigger(Events.KEY_LOADING, { frag });
|
||||
}
|
||||
|
||||
protected loadFragment(
|
||||
frag: Fragment,
|
||||
levelDetails: LevelDetails,
|
||||
|
@ -402,8 +400,8 @@ export default class BaseStreamController
|
|||
this.hls.trigger(Events.BUFFER_FLUSHING, flushScope);
|
||||
}
|
||||
|
||||
protected _loadInitSegment(frag: Fragment) {
|
||||
this._doFragLoad(frag)
|
||||
protected _loadInitSegment(frag: Fragment, details: LevelDetails) {
|
||||
this._doFragLoad(frag, details)
|
||||
.then((data) => {
|
||||
if (!data || this.fragContextChanged(frag) || !this.levels) {
|
||||
throw new Error('init load aborted');
|
||||
|
@ -428,7 +426,7 @@ export default class BaseStreamController
|
|||
const startTime = self.performance.now();
|
||||
// decrypt the subtitles
|
||||
return this.decrypter
|
||||
.webCryptoDecrypt(
|
||||
.decrypt(
|
||||
new Uint8Array(payload),
|
||||
decryptData.key.buffer,
|
||||
decryptData.iv.buffer
|
||||
|
@ -508,11 +506,13 @@ export default class BaseStreamController
|
|||
part ? ' part: ' + part.index : ''
|
||||
} of ${this.logPrefix === '[stream-controller]' ? 'level' : 'track'} ${
|
||||
frag.level
|
||||
} ${
|
||||
} (frag:[${(frag.startPTS || NaN).toFixed(3)}-${(
|
||||
frag.endPTS || NaN
|
||||
).toFixed(3)}] > buffer:${
|
||||
media
|
||||
? TimeRanges.toString(BufferHelper.getBuffered(media))
|
||||
: '(detached)'
|
||||
}`
|
||||
})`
|
||||
);
|
||||
this.state = State.IDLE;
|
||||
if (!media) {
|
||||
|
@ -520,8 +520,9 @@ export default class BaseStreamController
|
|||
}
|
||||
if (
|
||||
!this.loadedmetadata &&
|
||||
frag.type == PlaylistLevelType.MAIN &&
|
||||
media.buffered.length &&
|
||||
this.fragCurrent === this.fragPrevious
|
||||
this.fragCurrent?.sn === this.fragPrevious?.sn
|
||||
) {
|
||||
this.loadedmetadata = true;
|
||||
this.seekToStartPos();
|
||||
|
@ -554,17 +555,44 @@ export default class BaseStreamController
|
|||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
protected _handleFragmentLoadProgress(frag: FragLoadedData) {}
|
||||
protected _handleFragmentLoadProgress(
|
||||
frag: PartsLoadedData | FragLoadedData
|
||||
) {}
|
||||
|
||||
protected _doFragLoad(
|
||||
frag: Fragment,
|
||||
details?: LevelDetails,
|
||||
details: LevelDetails,
|
||||
targetBufferTime: number | null = null,
|
||||
progressCallback?: FragmentLoadProgressCallback
|
||||
): Promise<PartsLoadedData | FragLoadedData | null> {
|
||||
if (!this.levels) {
|
||||
throw new Error('frag load aborted, missing levels');
|
||||
}
|
||||
|
||||
let keyLoadingPromise: Promise<KeyLoadedData | void> | null = null;
|
||||
if (frag.encrypted && !frag.decryptdata?.key) {
|
||||
this.log(
|
||||
`Loading key for ${frag.sn} of [${details.startSN}-${details.endSN}], ${
|
||||
this.logPrefix === '[stream-controller]' ? 'level' : 'track'
|
||||
} ${frag.level}`
|
||||
);
|
||||
this.state = State.KEY_LOADING;
|
||||
this.fragCurrent = frag;
|
||||
keyLoadingPromise = this.keyLoader.load(frag).then((keyLoadedData) => {
|
||||
if (!this.fragContextChanged(keyLoadedData.frag)) {
|
||||
this.hls.trigger(Events.KEY_LOADED, keyLoadedData);
|
||||
if (this.state === State.KEY_LOADING) {
|
||||
this.state = State.IDLE;
|
||||
}
|
||||
return keyLoadedData;
|
||||
}
|
||||
});
|
||||
this.hls.trigger(Events.KEY_LOADING, { frag });
|
||||
this.throwIfFragContextChanged('KEY_LOADING');
|
||||
} else if (!frag.encrypted && details.encryptedFragments.length) {
|
||||
this.keyLoader.loadClear(frag, details.encryptedFragments);
|
||||
}
|
||||
|
||||
targetBufferTime = Math.max(frag.start, targetBufferTime || 0);
|
||||
if (this.config.lowLatencyMode && details) {
|
||||
const partList = details.partList;
|
||||
|
@ -593,6 +621,26 @@ export default class BaseStreamController
|
|||
part: partList[partIndex],
|
||||
targetBufferTime,
|
||||
});
|
||||
this.throwIfFragContextChanged('FRAG_LOADING parts');
|
||||
if (keyLoadingPromise) {
|
||||
return keyLoadingPromise
|
||||
.then((keyLoadedData) => {
|
||||
if (
|
||||
!keyLoadedData ||
|
||||
this.fragContextChanged(keyLoadedData.frag)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return this.doFragPartsLoad(
|
||||
frag,
|
||||
partList,
|
||||
partIndex,
|
||||
progressCallback
|
||||
);
|
||||
})
|
||||
.catch((error) => this.handleFragLoadError(error));
|
||||
}
|
||||
|
||||
return this.doFragPartsLoad(
|
||||
frag,
|
||||
partList,
|
||||
|
@ -622,10 +670,44 @@ export default class BaseStreamController
|
|||
}
|
||||
this.state = State.FRAG_LOADING;
|
||||
this.hls.trigger(Events.FRAG_LOADING, { frag, targetBufferTime });
|
||||
this.throwIfFragContextChanged('FRAG_LOADING');
|
||||
|
||||
return this.fragmentLoader
|
||||
.load(frag, progressCallback)
|
||||
.catch((error: LoadError) => this.handleFragLoadError(error));
|
||||
// Load key before streaming fragment data
|
||||
const dataOnProgress = this.config.progressive;
|
||||
if (dataOnProgress && keyLoadingPromise) {
|
||||
return keyLoadingPromise
|
||||
.then((keyLoadedData) => {
|
||||
if (!keyLoadedData || this.fragContextChanged(keyLoadedData?.frag)) {
|
||||
return null;
|
||||
}
|
||||
return this.fragmentLoader.load(frag, progressCallback);
|
||||
})
|
||||
.catch((error) => this.handleFragLoadError(error));
|
||||
}
|
||||
|
||||
// load unencrypted fragment data with progress event,
|
||||
// or handle fragment result after key and fragment are finished loading
|
||||
return Promise.all([
|
||||
this.fragmentLoader.load(
|
||||
frag,
|
||||
dataOnProgress ? progressCallback : undefined
|
||||
),
|
||||
keyLoadingPromise,
|
||||
])
|
||||
.then(([fragLoadedData]) => {
|
||||
if (!dataOnProgress && fragLoadedData && progressCallback) {
|
||||
progressCallback(fragLoadedData);
|
||||
}
|
||||
return fragLoadedData;
|
||||
})
|
||||
.catch((error) => this.handleFragLoadError(error));
|
||||
}
|
||||
|
||||
private throwIfFragContextChanged(context: string): void | never {
|
||||
// exit if context changed during event loop
|
||||
if (this.fragCurrent === null) {
|
||||
throw new Error(`frag load aborted, context changed in ${context}`);
|
||||
}
|
||||
}
|
||||
|
||||
private doFragPartsLoad(
|
||||
|
@ -663,11 +745,21 @@ export default class BaseStreamController
|
|||
);
|
||||
}
|
||||
|
||||
private handleFragLoadError({ data }: LoadError) {
|
||||
if (data && data.details === ErrorDetails.INTERNAL_ABORTED) {
|
||||
this.handleFragLoadAborted(data.frag, data.part);
|
||||
private handleFragLoadError(error: LoadError | Error) {
|
||||
if ('data' in error) {
|
||||
const data = error.data;
|
||||
if (error.data && data.details === ErrorDetails.INTERNAL_ABORTED) {
|
||||
this.handleFragLoadAborted(data.frag, data.part);
|
||||
} else {
|
||||
this.hls.trigger(Events.ERROR, data as ErrorData);
|
||||
}
|
||||
} else {
|
||||
this.hls.trigger(Events.ERROR, data as ErrorData);
|
||||
this.hls.trigger(Events.ERROR, {
|
||||
type: ErrorTypes.OTHER_ERROR,
|
||||
details: ErrorDetails.INTERNAL_EXCEPTION,
|
||||
err: error,
|
||||
fatal: true,
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -675,7 +767,11 @@ export default class BaseStreamController
|
|||
protected _handleTransmuxerFlush(chunkMeta: ChunkMetadata) {
|
||||
const context = this.getCurrentContext(chunkMeta);
|
||||
if (!context || this.state !== State.PARSING) {
|
||||
if (!this.fragCurrent) {
|
||||
if (
|
||||
!this.fragCurrent &&
|
||||
this.state !== State.STOPPED &&
|
||||
this.state !== State.ERROR
|
||||
) {
|
||||
this.state = State.IDLE;
|
||||
}
|
||||
return;
|
||||
|
@ -778,12 +874,7 @@ export default class BaseStreamController
|
|||
protected getFwdBufferInfo(
|
||||
bufferable: Bufferable | null,
|
||||
type: PlaylistLevelType
|
||||
): {
|
||||
len: number;
|
||||
start: number;
|
||||
end: number;
|
||||
nextStart?: number;
|
||||
} | null {
|
||||
): BufferInfo | null {
|
||||
const { config } = this;
|
||||
const pos = this.getLoadPosition();
|
||||
if (!Number.isFinite(pos)) {
|
||||
|
@ -912,8 +1003,9 @@ export default class BaseStreamController
|
|||
break;
|
||||
}
|
||||
const loaded = part.loaded;
|
||||
if (
|
||||
!loaded &&
|
||||
if (loaded) {
|
||||
nextPart = -1;
|
||||
} else if (
|
||||
(contiguous || part.independent || independentAttrOmitted) &&
|
||||
part.fragment === frag
|
||||
) {
|
||||
|
@ -1007,7 +1099,8 @@ export default class BaseStreamController
|
|||
end: number,
|
||||
levelDetails: LevelDetails
|
||||
): Fragment | null {
|
||||
const { config, fragPrevious } = this;
|
||||
const { config } = this;
|
||||
let { fragPrevious } = this;
|
||||
let { fragments, endSN } = levelDetails;
|
||||
const { fragmentHint } = levelDetails;
|
||||
const tolerance = config.maxFragLookUpTolerance;
|
||||
|
@ -1041,6 +1134,11 @@ export default class BaseStreamController
|
|||
|
||||
if (frag) {
|
||||
const curSNIdx = frag.sn - levelDetails.startSN;
|
||||
// Move fragPrevious forward to support forcing the next fragment to load
|
||||
// when the buffer catches up to a previously buffered range.
|
||||
if (this.fragmentTracker.getState(frag) === FragmentState.OK) {
|
||||
fragPrevious = frag;
|
||||
}
|
||||
if (fragPrevious && frag.sn === fragPrevious.sn && !loadingParts) {
|
||||
// Force the next fragment to load if the previous one was already selected. This can occasionally happen with
|
||||
// non-uniform fragment durations
|
||||
|
@ -1227,8 +1325,20 @@ export default class BaseStreamController
|
|||
data: ErrorData
|
||||
) {
|
||||
if (data.fatal) {
|
||||
this.stopLoad();
|
||||
this.state = State.ERROR;
|
||||
return;
|
||||
}
|
||||
const config = this.config;
|
||||
if (data.chunkMeta) {
|
||||
// Parsing Error: no retries
|
||||
const context = this.getCurrentContext(data.chunkMeta);
|
||||
if (context) {
|
||||
data.frag = context.frag;
|
||||
data.levelRetry = true;
|
||||
this.fragLoadError = config.fragLoadingMaxRetry;
|
||||
}
|
||||
}
|
||||
const frag = data.frag;
|
||||
// Handle frag error related to caller's filterType
|
||||
if (!frag || frag.type !== filterType) {
|
||||
|
@ -1242,7 +1352,6 @@ export default class BaseStreamController
|
|||
frag.urlId === fragCurrent.urlId,
|
||||
'Frag load error must match current frag to retry'
|
||||
);
|
||||
const config = this.config;
|
||||
// keep retrying until the limit will be reached
|
||||
if (this.fragLoadError + 1 <= config.fragLoadingMaxRetry) {
|
||||
if (!this.loadedmetadata) {
|
||||
|
@ -1302,6 +1411,7 @@ export default class BaseStreamController
|
|||
}
|
||||
|
||||
protected resetLoadingState() {
|
||||
this.log('Reset loading state');
|
||||
this.fragCurrent = null;
|
||||
this.fragPrevious = null;
|
||||
this.state = State.IDLE;
|
||||
|
|
72
node_modules/hls.js/src/controller/buffer-controller.ts
generated
vendored
72
node_modules/hls.js/src/controller/buffer-controller.ts
generated
vendored
|
@ -24,8 +24,9 @@ import type {
|
|||
FragChangedData,
|
||||
} from '../types/events';
|
||||
import type { ComponentAPI } from '../types/component-api';
|
||||
import type { ChunkMetadata } from '../types/transmuxer';
|
||||
import type Hls from '../hls';
|
||||
import { LevelDetails } from '../loader/level-details';
|
||||
import type { LevelDetails } from '../loader/level-details';
|
||||
|
||||
const MediaSource = getMediaSource();
|
||||
const VIDEO_CODEC_PROFILE_REPACE = /([ha]vc.)(?:\.[^.,]+)+/;
|
||||
|
@ -54,6 +55,9 @@ export default class BufferController implements ComponentAPI {
|
|||
// A reference to the active media source
|
||||
public mediaSource: MediaSource | null = null;
|
||||
|
||||
// Last MP3 audio chunk appended
|
||||
private lastMpegAudioChunk: ChunkMetadata | null = null;
|
||||
|
||||
// counters
|
||||
public appendError: number = 0;
|
||||
|
||||
|
@ -77,6 +81,7 @@ export default class BufferController implements ComponentAPI {
|
|||
public destroy() {
|
||||
this.unregisterListeners();
|
||||
this.details = null;
|
||||
this.lastMpegAudioChunk = null;
|
||||
}
|
||||
|
||||
protected registerListeners() {
|
||||
|
@ -117,6 +122,7 @@ export default class BufferController implements ComponentAPI {
|
|||
video: [],
|
||||
audiovideo: [],
|
||||
};
|
||||
this.lastMpegAudioChunk = null;
|
||||
}
|
||||
|
||||
protected onManifestParsed(
|
||||
|
@ -153,6 +159,7 @@ export default class BufferController implements ComponentAPI {
|
|||
media.src = self.URL.createObjectURL(ms);
|
||||
// cache the locally generated object url
|
||||
this._objectUrl = media.src;
|
||||
media.addEventListener('emptied', this._onMediaEmptied);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -182,6 +189,7 @@ export default class BufferController implements ComponentAPI {
|
|||
// Detach properly the MediaSource from the HTMLMediaElement as
|
||||
// suggested in https://github.com/w3c/media-source/issues/53.
|
||||
if (media) {
|
||||
media.removeEventListener('emptied', this._onMediaEmptied);
|
||||
if (_objectUrl) {
|
||||
self.URL.revokeObjectURL(_objectUrl);
|
||||
}
|
||||
|
@ -340,23 +348,28 @@ export default class BufferController implements ComponentAPI {
|
|||
// is greater than 100ms (this is enough to handle seek for VOD or level change for LIVE videos).
|
||||
// More info here: https://github.com/video-dev/hls.js/issues/332#issuecomment-257986486
|
||||
const audioTrack = tracks.audio;
|
||||
const checkTimestampOffset =
|
||||
type === 'audio' &&
|
||||
chunkMeta.id === 1 &&
|
||||
audioTrack?.container === 'audio/mpeg';
|
||||
let checkTimestampOffset = false;
|
||||
if (type === 'audio' && audioTrack?.container === 'audio/mpeg') {
|
||||
checkTimestampOffset =
|
||||
!this.lastMpegAudioChunk ||
|
||||
chunkMeta.id === 1 ||
|
||||
this.lastMpegAudioChunk.sn !== chunkMeta.sn;
|
||||
this.lastMpegAudioChunk = chunkMeta;
|
||||
}
|
||||
|
||||
const fragStart = frag.start;
|
||||
const operation: BufferOperation = {
|
||||
execute: () => {
|
||||
chunkStats.executeStart = self.performance.now();
|
||||
if (checkTimestampOffset) {
|
||||
const sb = this.sourceBuffer[type];
|
||||
if (sb) {
|
||||
const delta = frag.start - sb.timestampOffset;
|
||||
const delta = fragStart - sb.timestampOffset;
|
||||
if (Math.abs(delta) >= 0.1) {
|
||||
logger.log(
|
||||
`[buffer-controller]: Updating audio SourceBuffer timestampOffset to ${frag.start} (delta: ${delta}) sn: ${frag.sn})`
|
||||
`[buffer-controller]: Updating audio SourceBuffer timestampOffset to ${fragStart} (delta: ${delta}) sn: ${frag.sn})`
|
||||
);
|
||||
sb.timestampOffset = frag.start;
|
||||
sb.timestampOffset = fragStart;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -515,8 +528,9 @@ export default class BufferController implements ComponentAPI {
|
|||
protected onBufferEos(event: Events.BUFFER_EOS, data: BufferEOSData) {
|
||||
const ended = this.getSourceBufferTypes().reduce((acc, type) => {
|
||||
const sb = this.sourceBuffer[type];
|
||||
if (!data.type || data.type === type) {
|
||||
if (sb && !sb.ended) {
|
||||
if (sb && (!data.type || data.type === type)) {
|
||||
sb.ending = true;
|
||||
if (!sb.ended) {
|
||||
sb.ended = true;
|
||||
logger.log(`[buffer-controller]: ${type} sourceBuffer now EOS`);
|
||||
}
|
||||
|
@ -525,11 +539,24 @@ export default class BufferController implements ComponentAPI {
|
|||
}, true);
|
||||
|
||||
if (ended) {
|
||||
logger.log(`[buffer-controller]: Queueing mediaSource.endOfStream()`);
|
||||
this.blockBuffers(() => {
|
||||
this.getSourceBufferTypes().forEach((type) => {
|
||||
const sb = this.sourceBuffer[type];
|
||||
if (sb) {
|
||||
sb.ending = false;
|
||||
}
|
||||
});
|
||||
const { mediaSource } = this;
|
||||
if (!mediaSource || mediaSource.readyState !== 'open') {
|
||||
if (mediaSource) {
|
||||
logger.info(
|
||||
`[buffer-controller]: Could not call mediaSource.endOfStream(). mediaSource.readyState: ${mediaSource.readyState}`
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
logger.log(`[buffer-controller]: Calling mediaSource.endOfStream()`);
|
||||
// Allow this to throw and be caught by the enqueueing function
|
||||
mediaSource.endOfStream();
|
||||
});
|
||||
|
@ -597,6 +624,14 @@ export default class BufferController implements ComponentAPI {
|
|||
hls.trigger(Events.LIVE_BACK_BUFFER_REACHED, {
|
||||
bufferEnd: targetBackBufferPosition,
|
||||
});
|
||||
} else if (
|
||||
sb.ended &&
|
||||
buffered.end(buffered.length - 1) - currentTime < targetDuration * 2
|
||||
) {
|
||||
logger.info(
|
||||
`[buffer-controller]: Cannot flush ${type} back buffer while SourceBuffer is in ended state`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
hls.trigger(Events.BUFFER_FLUSHING, {
|
||||
|
@ -752,11 +787,12 @@ export default class BufferController implements ComponentAPI {
|
|||
|
||||
// Keep as arrow functions so that we can directly reference these functions directly as event listeners
|
||||
private _onMediaSourceOpen = () => {
|
||||
const { hls, media, mediaSource } = this;
|
||||
const { media, mediaSource } = this;
|
||||
logger.log('[buffer-controller]: Media source opened');
|
||||
if (media) {
|
||||
media.removeEventListener('emptied', this._onMediaEmptied);
|
||||
this.updateMediaElementDuration();
|
||||
hls.trigger(Events.MEDIA_ATTACHED, { media });
|
||||
this.hls.trigger(Events.MEDIA_ATTACHED, { media });
|
||||
}
|
||||
|
||||
if (mediaSource) {
|
||||
|
@ -774,6 +810,15 @@ export default class BufferController implements ComponentAPI {
|
|||
logger.log('[buffer-controller]: Media source ended');
|
||||
};
|
||||
|
||||
private _onMediaEmptied = () => {
|
||||
const { media, _objectUrl } = this;
|
||||
if (media && media.src !== _objectUrl) {
|
||||
logger.error(
|
||||
`Media element src was set while attaching MediaSource (${_objectUrl} > ${media.src})`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private _onSBUpdateStart(type: SourceBufferName) {
|
||||
const { operationQueue } = this;
|
||||
const operation = operationQueue.current(type);
|
||||
|
@ -826,7 +871,8 @@ export default class BufferController implements ComponentAPI {
|
|||
: Infinity;
|
||||
const removeStart = Math.max(0, startOffset);
|
||||
const removeEnd = Math.min(endOffset, mediaDuration, msDuration);
|
||||
if (removeEnd > removeStart) {
|
||||
if (removeEnd > removeStart && !sb.ending) {
|
||||
sb.ended = false;
|
||||
logger.log(
|
||||
`[buffer-controller]: Removing [${removeStart},${removeEnd}] from the ${type} SourceBuffer`
|
||||
);
|
||||
|
|
1
node_modules/hls.js/src/controller/cap-level-controller.ts
generated
vendored
1
node_modules/hls.js/src/controller/cap-level-controller.ts
generated
vendored
|
@ -90,6 +90,7 @@ class CapLevelController implements ComponentAPI {
|
|||
data: MediaAttachingData
|
||||
) {
|
||||
this.media = data.media instanceof HTMLVideoElement ? data.media : null;
|
||||
this.clientRect = null;
|
||||
}
|
||||
|
||||
protected onManifestParsed(
|
||||
|
|
1612
node_modules/hls.js/src/controller/eme-controller.ts
generated
vendored
1612
node_modules/hls.js/src/controller/eme-controller.ts
generated
vendored
File diff suppressed because it is too large
Load diff
9
node_modules/hls.js/src/controller/fragment-finders.ts
generated
vendored
9
node_modules/hls.js/src/controller/fragment-finders.ts
generated
vendored
|
@ -82,7 +82,7 @@ export function findFragmentByPTS(
|
|||
fragments,
|
||||
fragmentWithinToleranceTest.bind(null, bufferEnd, maxFragLookUpTolerance)
|
||||
);
|
||||
if (foundFragment) {
|
||||
if (foundFragment && (foundFragment !== fragPrevious || !fragNext)) {
|
||||
return foundFragment;
|
||||
}
|
||||
// If no match was found return the next fragment after fragPrevious, or null
|
||||
|
@ -101,6 +101,13 @@ export function fragmentWithinToleranceTest(
|
|||
maxFragLookUpTolerance = 0,
|
||||
candidate: Fragment
|
||||
) {
|
||||
// eagerly accept an accurate match (no tolerance)
|
||||
if (
|
||||
candidate.start <= bufferEnd &&
|
||||
candidate.start + candidate.duration > bufferEnd
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
// offset should be within fragment boundary - config.maxFragLookUpTolerance
|
||||
// this is to cope with situations like
|
||||
// bufferEnd = 9.991
|
||||
|
|
53
node_modules/hls.js/src/controller/fragment-tracker.ts
generated
vendored
53
node_modules/hls.js/src/controller/fragment-tracker.ts
generated
vendored
|
@ -25,11 +25,13 @@ export enum FragmentState {
|
|||
export class FragmentTracker implements ComponentAPI {
|
||||
private activeFragment: Fragment | null = null;
|
||||
private activeParts: Part[] | null = null;
|
||||
private endListFragments: { [key in PlaylistLevelType]?: FragmentEntity } =
|
||||
Object.create(null);
|
||||
private fragments: Partial<Record<string, FragmentEntity>> =
|
||||
Object.create(null);
|
||||
private timeRanges:
|
||||
| {
|
||||
[key in SourceBufferName]: TimeRanges;
|
||||
[key in SourceBufferName]?: TimeRanges;
|
||||
}
|
||||
| null = Object.create(null);
|
||||
|
||||
|
@ -59,7 +61,13 @@ export class FragmentTracker implements ComponentAPI {
|
|||
public destroy() {
|
||||
this._unregisterListeners();
|
||||
// @ts-ignore
|
||||
this.fragments = this.timeRanges = null;
|
||||
this.fragments =
|
||||
// @ts-ignore
|
||||
this.endListFragments =
|
||||
this.timeRanges =
|
||||
this.activeFragment =
|
||||
this.activeParts =
|
||||
null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -137,13 +145,16 @@ export class FragmentTracker implements ComponentAPI {
|
|||
timeRange: TimeRanges,
|
||||
playlistType?: PlaylistLevelType
|
||||
) {
|
||||
if (this.timeRanges) {
|
||||
this.timeRanges[elementaryStream] = timeRange;
|
||||
}
|
||||
// Check if any flagged fragments have been unloaded
|
||||
Object.keys(this.fragments).forEach((key) => {
|
||||
const fragmentEntity = this.fragments[key];
|
||||
if (!fragmentEntity) {
|
||||
return;
|
||||
}
|
||||
if (!fragmentEntity.buffered) {
|
||||
if (!fragmentEntity.buffered && !fragmentEntity.loaded) {
|
||||
if (fragmentEntity.body.type === playlistType) {
|
||||
this.removeFragment(fragmentEntity.body);
|
||||
}
|
||||
|
@ -201,6 +212,9 @@ export class FragmentTracker implements ComponentAPI {
|
|||
fragmentEntity.loaded = null;
|
||||
if (Object.keys(fragmentEntity.range).length) {
|
||||
fragmentEntity.buffered = true;
|
||||
if (fragmentEntity.body.endList) {
|
||||
this.endListFragments[fragmentEntity.body.type] = fragmentEntity;
|
||||
}
|
||||
} else {
|
||||
// remove fragment if nothing was appended
|
||||
this.removeFragment(fragmentEntity.body);
|
||||
|
@ -288,6 +302,14 @@ export class FragmentTracker implements ComponentAPI {
|
|||
return bestFragment;
|
||||
}
|
||||
|
||||
public isEndListAppended(type: PlaylistLevelType): boolean {
|
||||
const lastFragmentEntity = this.endListFragments[type];
|
||||
return (
|
||||
lastFragmentEntity !== undefined &&
|
||||
(lastFragmentEntity.buffered || isPartial(lastFragmentEntity))
|
||||
);
|
||||
}
|
||||
|
||||
public getState(fragment: Fragment): FragmentState {
|
||||
const fragKey = getFragmentKey(fragment);
|
||||
const fragmentEntity = this.fragments[fragKey];
|
||||
|
@ -352,7 +374,10 @@ export class FragmentTracker implements ComponentAPI {
|
|||
) {
|
||||
const { frag, part, timeRanges } = data;
|
||||
if (frag.type === PlaylistLevelType.MAIN) {
|
||||
this.activeFragment = frag;
|
||||
if (this.activeFragment !== frag) {
|
||||
this.activeFragment = frag;
|
||||
frag.appendedPTS = undefined;
|
||||
}
|
||||
if (part) {
|
||||
let activeParts = this.activeParts;
|
||||
if (!activeParts) {
|
||||
|
@ -364,13 +389,22 @@ export class FragmentTracker implements ComponentAPI {
|
|||
}
|
||||
}
|
||||
// Store the latest timeRanges loaded in the buffer
|
||||
this.timeRanges = timeRanges as { [key in SourceBufferName]: TimeRanges };
|
||||
this.timeRanges = timeRanges;
|
||||
Object.keys(timeRanges).forEach((elementaryStream: SourceBufferName) => {
|
||||
const timeRange = timeRanges[elementaryStream] as TimeRanges;
|
||||
this.detectEvictedFragments(elementaryStream, timeRange);
|
||||
if (!part) {
|
||||
if (!part && frag.type === PlaylistLevelType.MAIN) {
|
||||
const streamInfo = frag.elementaryStreams[elementaryStream];
|
||||
if (!streamInfo) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < timeRange.length; i++) {
|
||||
frag.appendedPTS = Math.max(timeRange.end(i), frag.appendedPTS || 0);
|
||||
const rangeEnd = timeRange.end(i);
|
||||
if (rangeEnd <= streamInfo.endPTS && rangeEnd > streamInfo.startPTS) {
|
||||
frag.appendedPTS = Math.max(rangeEnd, frag.appendedPTS || 0);
|
||||
} else {
|
||||
frag.appendedPTS = streamInfo.endPTS;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -412,11 +446,16 @@ export class FragmentTracker implements ComponentAPI {
|
|||
const fragKey = getFragmentKey(fragment);
|
||||
fragment.stats.loaded = 0;
|
||||
fragment.clearElementaryStreamInfo();
|
||||
fragment.appendedPTS = undefined;
|
||||
delete this.fragments[fragKey];
|
||||
if (fragment.endList) {
|
||||
delete this.endListFragments[fragment.type];
|
||||
}
|
||||
}
|
||||
|
||||
public removeAllFragments() {
|
||||
this.fragments = Object.create(null);
|
||||
this.endListFragments = Object.create(null);
|
||||
this.activeFragment = null;
|
||||
this.activeParts = null;
|
||||
}
|
||||
|
|
47
node_modules/hls.js/src/controller/id3-track-controller.ts
generated
vendored
47
node_modules/hls.js/src/controller/id3-track-controller.ts
generated
vendored
|
@ -33,6 +33,18 @@ function getCueClass() {
|
|||
return (self.WebKitDataCue || self.VTTCue || self.TextTrackCue) as any;
|
||||
}
|
||||
|
||||
// VTTCue latest draft allows an infinite duration, fallback
|
||||
// to MAX_VALUE if necessary
|
||||
const MAX_CUE_ENDTIME = (() => {
|
||||
const Cue = getCueClass();
|
||||
try {
|
||||
new Cue(0, Number.POSITIVE_INFINITY, '');
|
||||
} catch (e) {
|
||||
return Number.MAX_VALUE;
|
||||
}
|
||||
return Number.POSITIVE_INFINITY;
|
||||
})();
|
||||
|
||||
function dateRangeDateToTimelineSeconds(date: Date, offset: number): number {
|
||||
return date.getTime() / 1000 - offset;
|
||||
}
|
||||
|
@ -151,18 +163,14 @@ class ID3TrackController implements ComponentAPI {
|
|||
return;
|
||||
}
|
||||
|
||||
const { frag: fragment, samples, details } = data;
|
||||
const { samples } = data;
|
||||
|
||||
// create track dynamically
|
||||
if (!this.id3Track) {
|
||||
this.id3Track = this.createTrack(this.media);
|
||||
}
|
||||
|
||||
// VTTCue end time must be finite, so use playlist edge or fragment end until next fragment with same frame type is found
|
||||
const maxCueTime = details.edge || fragment.end;
|
||||
const Cue = getCueClass();
|
||||
let updateCueRanges = false;
|
||||
const frameTypesAdded: Record<string, number | null> = {};
|
||||
|
||||
for (let i = 0; i < samples.length; i++) {
|
||||
const type = samples[i].type;
|
||||
|
@ -176,7 +184,11 @@ class ID3TrackController implements ComponentAPI {
|
|||
const frames = ID3.getID3Frames(samples[i].data);
|
||||
if (frames) {
|
||||
const startTime = samples[i].pts;
|
||||
let endTime: number = maxCueTime;
|
||||
let endTime: number = startTime + samples[i].duration;
|
||||
|
||||
if (endTime > MAX_CUE_ENDTIME) {
|
||||
endTime = MAX_CUE_ENDTIME;
|
||||
}
|
||||
|
||||
const timeDiff = endTime - startTime;
|
||||
if (timeDiff <= 0) {
|
||||
|
@ -187,36 +199,28 @@ class ID3TrackController implements ComponentAPI {
|
|||
const frame = frames[j];
|
||||
// Safari doesn't put the timestamp frame in the TextTrack
|
||||
if (!ID3.isTimeStampFrame(frame)) {
|
||||
// add a bounds to any unbounded cues
|
||||
this.updateId3CueEnds(startTime);
|
||||
|
||||
const cue = new Cue(startTime, endTime, '');
|
||||
cue.value = frame;
|
||||
if (type) {
|
||||
cue.type = type;
|
||||
}
|
||||
this.id3Track.addCue(cue);
|
||||
frameTypesAdded[frame.key] = null;
|
||||
updateCueRanges = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (updateCueRanges) {
|
||||
this.updateId3CueEnds(frameTypesAdded);
|
||||
}
|
||||
}
|
||||
|
||||
updateId3CueEnds(frameTypesAdded: Record<string, number | null>) {
|
||||
// Update endTime of previous cue with same IDR frame.type (Ex: TXXX cue spans to next TXXX)
|
||||
updateId3CueEnds(startTime: number) {
|
||||
const cues = this.id3Track?.cues;
|
||||
if (cues) {
|
||||
for (let i = cues.length; i--; ) {
|
||||
const cue = cues[i] as any;
|
||||
const frameType = cue.value?.key;
|
||||
if (frameType && frameType in frameTypesAdded) {
|
||||
const startTime = frameTypesAdded[frameType];
|
||||
if (startTime && cue.endTime !== startTime) {
|
||||
cue.endTime = startTime;
|
||||
}
|
||||
frameTypesAdded[frameType] = cue.startTime;
|
||||
if (cue.startTime < startTime && cue.endTime === MAX_CUE_ENDTIME) {
|
||||
cue.endTime = startTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -290,7 +294,6 @@ class ID3TrackController implements ComponentAPI {
|
|||
|
||||
const dateTimeOffset =
|
||||
(lastFragment.programDateTime as number) / 1000 - lastFragment.start;
|
||||
const maxCueTime = details.edge || lastFragment.end;
|
||||
const Cue = getCueClass();
|
||||
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
|
@ -303,7 +306,7 @@ class ID3TrackController implements ComponentAPI {
|
|||
dateRange.startDate,
|
||||
dateTimeOffset
|
||||
);
|
||||
let endTime = maxCueTime;
|
||||
let endTime = MAX_CUE_ENDTIME;
|
||||
const endDate = dateRange.endDate;
|
||||
if (endDate) {
|
||||
endTime = dateRangeDateToTimelineSeconds(endDate, dateTimeOffset);
|
||||
|
|
55
node_modules/hls.js/src/controller/level-controller.ts
generated
vendored
55
node_modules/hls.js/src/controller/level-controller.ts
generated
vendored
|
@ -11,7 +11,7 @@ import {
|
|||
ErrorData,
|
||||
LevelSwitchingData,
|
||||
} from '../types/events';
|
||||
import { Level } from '../types/level';
|
||||
import { HdcpLevel, HdcpLevels, Level } from '../types/level';
|
||||
import { Events } from '../events';
|
||||
import { ErrorTypes, ErrorDetails } from '../errors';
|
||||
import { isCodecSupportedInMp4 } from '../utils/codecs';
|
||||
|
@ -162,8 +162,27 @@ export default class LevelController extends BasePlaylistController {
|
|||
if (levels.length > 0) {
|
||||
// start bitrate is the first bitrate of the manifest
|
||||
bitrateStart = levels[0].bitrate;
|
||||
// sort level on bitrate
|
||||
levels.sort((a, b) => a.bitrate - b.bitrate);
|
||||
// sort levels from lowest to highest
|
||||
levels.sort((a, b) => {
|
||||
if (a.attrs['HDCP-LEVEL'] !== b.attrs['HDCP-LEVEL']) {
|
||||
return (a.attrs['HDCP-LEVEL'] || '') > (b.attrs['HDCP-LEVEL'] || '')
|
||||
? 1
|
||||
: -1;
|
||||
}
|
||||
if (a.bitrate !== b.bitrate) {
|
||||
return a.bitrate - b.bitrate;
|
||||
}
|
||||
if (a.attrs.SCORE !== b.attrs.SCORE) {
|
||||
return (
|
||||
a.attrs.decimalFloatingPoint('SCORE') -
|
||||
b.attrs.decimalFloatingPoint('SCORE')
|
||||
);
|
||||
}
|
||||
if (resolutionFound && a.height !== b.height) {
|
||||
return a.height - b.height;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
this._levels = levels;
|
||||
// find index of first level in sorted levels
|
||||
for (let i = 0; i < levels.length; i++) {
|
||||
|
@ -183,6 +202,8 @@ export default class LevelController extends BasePlaylistController {
|
|||
levels,
|
||||
audioTracks,
|
||||
subtitleTracks,
|
||||
sessionData: data.sessionData,
|
||||
sessionKeys: data.sessionKeys,
|
||||
firstLevel: this._firstLevel,
|
||||
stats: data.stats,
|
||||
audio: audioCodecFound,
|
||||
|
@ -362,6 +383,28 @@ export default class LevelController extends BasePlaylistController {
|
|||
}
|
||||
}
|
||||
break;
|
||||
case ErrorDetails.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED: {
|
||||
const restrictedHdcpLevel = level.attrs['HDCP-LEVEL'];
|
||||
if (restrictedHdcpLevel) {
|
||||
this.hls.maxHdcpLevel =
|
||||
HdcpLevels[
|
||||
HdcpLevels.indexOf(restrictedHdcpLevel as HdcpLevel) - 1
|
||||
];
|
||||
this.warn(
|
||||
`Restricting playback to HDCP-LEVEL of "${this.hls.maxHdcpLevel}" or lower`
|
||||
);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
case ErrorDetails.FRAG_PARSING_ERROR:
|
||||
case ErrorDetails.KEY_SYSTEM_NO_SESSION:
|
||||
levelIndex =
|
||||
data.frag?.type === PlaylistLevelType.MAIN
|
||||
? data.frag.level
|
||||
: this.currentLevelIndex;
|
||||
// Do not retry level. Escalate to fatal if switching levels fails.
|
||||
data.levelRetry = false;
|
||||
break;
|
||||
case ErrorDetails.LEVEL_LOAD_ERROR:
|
||||
case ErrorDetails.LEVEL_LOAD_TIMEOUT:
|
||||
// Do not perform level switch if an error occurred using delivery directives
|
||||
|
@ -435,6 +478,9 @@ export default class LevelController extends BasePlaylistController {
|
|||
this.warn(`${errorDetails}: switch to ${nextLevel}`);
|
||||
errorEvent.levelRetry = true;
|
||||
this.hls.nextAutoLevel = nextLevel;
|
||||
} else if (errorEvent.levelRetry === false) {
|
||||
// No levels to switch to and no more retries
|
||||
errorEvent.fatal = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -518,6 +564,7 @@ export default class LevelController extends BasePlaylistController {
|
|||
}
|
||||
|
||||
protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters) {
|
||||
super.loadPlaylist();
|
||||
const level = this.currentLevelIndex;
|
||||
const currentLevel = this._levels[level];
|
||||
|
||||
|
@ -536,7 +583,7 @@ export default class LevelController extends BasePlaylistController {
|
|||
|
||||
this.log(
|
||||
`Attempt loading level index ${level}${
|
||||
hlsUrlParameters
|
||||
hlsUrlParameters?.msn !== undefined
|
||||
? ' at sn ' +
|
||||
hlsUrlParameters.msn +
|
||||
' part ' +
|
||||
|
|
72
node_modules/hls.js/src/controller/level-helper.ts
generated
vendored
72
node_modules/hls.js/src/controller/level-helper.ts
generated
vendored
|
@ -7,7 +7,6 @@ import { logger } from '../utils/logger';
|
|||
import { Fragment, Part } from '../loader/fragment';
|
||||
import { LevelDetails } from '../loader/level-details';
|
||||
import type { Level } from '../types/level';
|
||||
import type { LoaderStats } from '../types/loader';
|
||||
import type { MediaPlaylist } from '../types/media-playlist';
|
||||
import { DateRange } from '../loader/date-range';
|
||||
|
||||
|
@ -123,7 +122,6 @@ export function updateFragPTSDTS(
|
|||
frag.duration = endPTS - startPTS;
|
||||
|
||||
const drift = startPTS - frag.start;
|
||||
frag.appendedPTS = endPTS;
|
||||
frag.start = frag.startPTS = startPTS;
|
||||
frag.maxStartPTS = maxStartPTS;
|
||||
frag.startDTS = startDTS;
|
||||
|
@ -434,61 +432,33 @@ export function addSliding(details: LevelDetails, start: number) {
|
|||
|
||||
export function computeReloadInterval(
|
||||
newDetails: LevelDetails,
|
||||
stats: LoaderStats
|
||||
distanceToLiveEdgeMs: number = Infinity
|
||||
): number {
|
||||
const reloadInterval = 1000 * newDetails.levelTargetDuration;
|
||||
const reloadIntervalAfterMiss = reloadInterval / 2;
|
||||
const timeSinceLastModified = newDetails.age;
|
||||
const useLastModified =
|
||||
timeSinceLastModified > 0 && timeSinceLastModified < reloadInterval * 3;
|
||||
const roundTrip = stats.loading.end - stats.loading.start;
|
||||
let reloadInterval = 1000 * newDetails.targetduration;
|
||||
|
||||
let estimatedTimeUntilUpdate;
|
||||
let availabilityDelay = newDetails.availabilityDelay;
|
||||
// let estimate = 'average';
|
||||
|
||||
if (newDetails.updated === false) {
|
||||
if (useLastModified) {
|
||||
// estimate = 'miss round trip';
|
||||
// We should have had a hit so try again in the time it takes to get a response,
|
||||
// but no less than 1/3 second.
|
||||
const minRetry = 333 * newDetails.misses;
|
||||
estimatedTimeUntilUpdate = Math.max(
|
||||
Math.min(reloadIntervalAfterMiss, roundTrip * 2),
|
||||
minRetry
|
||||
);
|
||||
newDetails.availabilityDelay =
|
||||
(newDetails.availabilityDelay || 0) + estimatedTimeUntilUpdate;
|
||||
} else {
|
||||
// estimate = 'miss half average';
|
||||
// follow HLS Spec, If the client reloads a Playlist file and finds that it has not
|
||||
// changed then it MUST wait for a period of one-half the target
|
||||
// duration before retrying.
|
||||
estimatedTimeUntilUpdate = reloadIntervalAfterMiss;
|
||||
if (newDetails.updated) {
|
||||
// Use last segment duration when shorter than target duration and near live edge
|
||||
const fragments = newDetails.fragments;
|
||||
const liveEdgeMaxTargetDurations = 4;
|
||||
if (
|
||||
fragments.length &&
|
||||
reloadInterval * liveEdgeMaxTargetDurations > distanceToLiveEdgeMs
|
||||
) {
|
||||
const lastSegmentDuration =
|
||||
fragments[fragments.length - 1].duration * 1000;
|
||||
if (lastSegmentDuration < reloadInterval) {
|
||||
reloadInterval = lastSegmentDuration;
|
||||
}
|
||||
}
|
||||
} else if (useLastModified) {
|
||||
// estimate = 'next modified date';
|
||||
// Get the closest we've been to timeSinceLastModified on update
|
||||
availabilityDelay = Math.min(
|
||||
availabilityDelay || reloadInterval / 2,
|
||||
timeSinceLastModified
|
||||
);
|
||||
newDetails.availabilityDelay = availabilityDelay;
|
||||
estimatedTimeUntilUpdate =
|
||||
availabilityDelay + reloadInterval - timeSinceLastModified;
|
||||
} else {
|
||||
estimatedTimeUntilUpdate = reloadInterval - roundTrip;
|
||||
// estimate = 'miss half average';
|
||||
// follow HLS Spec, If the client reloads a Playlist file and finds that it has not
|
||||
// changed then it MUST wait for a period of one-half the target
|
||||
// duration before retrying.
|
||||
reloadInterval /= 2;
|
||||
}
|
||||
|
||||
// console.log(`[computeReloadInterval] live reload ${newDetails.updated ? 'REFRESHED' : 'MISSED'}`,
|
||||
// '\n method', estimate,
|
||||
// '\n estimated time until update =>', estimatedTimeUntilUpdate,
|
||||
// '\n average target duration', reloadInterval,
|
||||
// '\n time since modified', timeSinceLastModified,
|
||||
// '\n time round trip', roundTrip,
|
||||
// '\n availability delay', availabilityDelay);
|
||||
|
||||
return Math.round(estimatedTimeUntilUpdate);
|
||||
return Math.round(reloadInterval);
|
||||
}
|
||||
|
||||
export function getFragmentWithSN(
|
||||
|
|
106
node_modules/hls.js/src/controller/stream-controller.ts
generated
vendored
106
node_modules/hls.js/src/controller/stream-controller.ts
generated
vendored
|
@ -1,20 +1,21 @@
|
|||
import BaseStreamController, { State } from './base-stream-controller';
|
||||
import { changeTypeSupported } from '../is-supported';
|
||||
import type { NetworkComponentAPI } from '../types/component-api';
|
||||
import { Events } from '../events';
|
||||
import { BufferHelper } from '../utils/buffer-helper';
|
||||
import type { FragmentTracker } from './fragment-tracker';
|
||||
import { BufferHelper, BufferInfo } from '../utils/buffer-helper';
|
||||
import { FragmentState } from './fragment-tracker';
|
||||
import type { Level } from '../types/level';
|
||||
import { PlaylistLevelType } from '../types/loader';
|
||||
import { ElementaryStreamTypes, Fragment } from '../loader/fragment';
|
||||
import TransmuxerInterface from '../demux/transmuxer-interface';
|
||||
import type { TransmuxerResult } from '../types/transmuxer';
|
||||
import { ChunkMetadata } from '../types/transmuxer';
|
||||
import GapController from './gap-controller';
|
||||
import { ErrorDetails } from '../errors';
|
||||
import { ErrorDetails, ErrorTypes } from '../errors';
|
||||
import type { NetworkComponentAPI } from '../types/component-api';
|
||||
import type Hls from '../hls';
|
||||
import type { Level } from '../types/level';
|
||||
import type { LevelDetails } from '../loader/level-details';
|
||||
import type { FragmentTracker } from './fragment-tracker';
|
||||
import type KeyLoader from '../loader/key-loader';
|
||||
import type { TransmuxerResult } from '../types/transmuxer';
|
||||
import type { TrackSet } from '../types/track';
|
||||
import type { SourceBufferName } from '../types/buffer';
|
||||
import type {
|
||||
|
@ -56,8 +57,12 @@ export default class StreamController
|
|||
private audioCodecSwitch: boolean = false;
|
||||
private videoBuffer: any | null = null;
|
||||
|
||||
constructor(hls: Hls, fragmentTracker: FragmentTracker) {
|
||||
super(hls, fragmentTracker, '[stream-controller]');
|
||||
constructor(
|
||||
hls: Hls,
|
||||
fragmentTracker: FragmentTracker,
|
||||
keyLoader: KeyLoader
|
||||
) {
|
||||
super(hls, fragmentTracker, keyLoader, '[stream-controller]');
|
||||
this._registerListeners();
|
||||
}
|
||||
|
||||
|
@ -228,6 +233,24 @@ export default class StreamController
|
|||
const levelInfo = levels[level];
|
||||
|
||||
// if buffer length is less than maxBufLen try to load a new fragment
|
||||
|
||||
const bufferInfo = this.getMainFwdBufferInfo();
|
||||
if (bufferInfo === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastDetails = this.getLevelDetails();
|
||||
if (lastDetails && this._streamEnded(bufferInfo, lastDetails)) {
|
||||
const data: BufferEOSData = {};
|
||||
if (this.altAudio) {
|
||||
data.type = 'video';
|
||||
}
|
||||
|
||||
this.hls.trigger(Events.BUFFER_EOS, data);
|
||||
this.state = State.ENDED;
|
||||
return;
|
||||
}
|
||||
|
||||
// set next load level : this will trigger a playlist load if needed
|
||||
this.level = hls.nextLoadLevel = level;
|
||||
|
||||
|
@ -240,14 +263,11 @@ export default class StreamController
|
|||
this.state === State.WAITING_LEVEL ||
|
||||
(levelDetails.live && this.levelLastLoaded !== level)
|
||||
) {
|
||||
this.level = level;
|
||||
this.state = State.WAITING_LEVEL;
|
||||
return;
|
||||
}
|
||||
|
||||
const bufferInfo = this.getMainFwdBufferInfo();
|
||||
if (bufferInfo === null) {
|
||||
return;
|
||||
}
|
||||
const bufferLen = bufferInfo.len;
|
||||
|
||||
// compute max Buffer Length that we could get from this load level, based on level bitrate. don't buffer more than 60 MB and more than 30s
|
||||
|
@ -258,17 +278,6 @@ export default class StreamController
|
|||
return;
|
||||
}
|
||||
|
||||
if (this._streamEnded(bufferInfo, levelDetails)) {
|
||||
const data: BufferEOSData = {};
|
||||
if (this.altAudio) {
|
||||
data.type = 'video';
|
||||
}
|
||||
|
||||
this.hls.trigger(Events.BUFFER_EOS, data);
|
||||
this.state = State.ENDED;
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.backtrackFragment &&
|
||||
this.backtrackFragment.start > bufferInfo.end
|
||||
|
@ -308,8 +317,12 @@ export default class StreamController
|
|||
this.audioOnly && !this.altAudio
|
||||
? ElementaryStreamTypes.AUDIO
|
||||
: ElementaryStreamTypes.VIDEO;
|
||||
if (media) {
|
||||
this.afterBufferFlushed(media, type, PlaylistLevelType.MAIN);
|
||||
const mediaBuffer =
|
||||
(type === ElementaryStreamTypes.VIDEO
|
||||
? this.videoBuffer
|
||||
: this.mediaBuffer) || this.media;
|
||||
if (mediaBuffer) {
|
||||
this.afterBufferFlushed(mediaBuffer, type, PlaylistLevelType.MAIN);
|
||||
}
|
||||
frag = this.getNextFragment(this.nextLoadPosition, levelDetails);
|
||||
}
|
||||
|
@ -320,13 +333,7 @@ export default class StreamController
|
|||
frag = frag.initSegment;
|
||||
}
|
||||
|
||||
// We want to load the key if we're dealing with an identity key, because we will decrypt
|
||||
// this content using the key we fetch. Other keys will be handled by the DRM CDM via EME.
|
||||
if (frag.decryptdata?.keyFormat === 'identity' && !frag.decryptdata?.key) {
|
||||
this.loadKey(frag, levelDetails);
|
||||
} else {
|
||||
this.loadFragment(frag, levelDetails, targetBufferTime);
|
||||
}
|
||||
this.loadFragment(frag, levelDetails, targetBufferTime);
|
||||
}
|
||||
|
||||
protected loadFragment(
|
||||
|
@ -339,12 +346,12 @@ export default class StreamController
|
|||
this.fragCurrent = frag;
|
||||
if (fragState === FragmentState.NOT_LOADED) {
|
||||
if (frag.sn === 'initSegment') {
|
||||
this._loadInitSegment(frag);
|
||||
this._loadInitSegment(frag, levelDetails);
|
||||
} else if (this.bitrateTest) {
|
||||
this.log(
|
||||
`Fragment ${frag.sn} of level ${frag.level} is being downloaded to test bitrate and will not be buffered`
|
||||
);
|
||||
this._loadBitrateTestFrag(frag);
|
||||
this._loadBitrateTestFrag(frag, levelDetails);
|
||||
} else {
|
||||
this.startFragRequested = true;
|
||||
super.loadFragment(frag, levelDetails, targetBufferTime);
|
||||
|
@ -465,8 +472,8 @@ export default class StreamController
|
|||
const fragCurrent = this.fragCurrent;
|
||||
this.fragCurrent = null;
|
||||
this.backtrackFragment = null;
|
||||
if (fragCurrent?.loader) {
|
||||
fragCurrent.loader.abort();
|
||||
if (fragCurrent) {
|
||||
fragCurrent.abortRequests();
|
||||
}
|
||||
switch (this.state) {
|
||||
case State.KEY_LOADING:
|
||||
|
@ -618,7 +625,7 @@ export default class StreamController
|
|||
if (fragCurrent.level !== data.level && fragCurrent.loader) {
|
||||
this.state = State.IDLE;
|
||||
this.backtrackFragment = null;
|
||||
fragCurrent.loader.abort();
|
||||
fragCurrent.abortRequests();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -740,9 +747,9 @@ export default class StreamController
|
|||
this.mediaBuffer = this.media;
|
||||
const fragCurrent = this.fragCurrent;
|
||||
// we need to refill audio buffer from main: cancel any frag loading to speed up audio switch
|
||||
if (fragCurrent?.loader) {
|
||||
if (fragCurrent) {
|
||||
this.log('Switching to main audio track, cancel main fragment load');
|
||||
fragCurrent.loader.abort();
|
||||
fragCurrent.abortRequests();
|
||||
}
|
||||
// destroy transmuxer to force init segment generation (following audio switch)
|
||||
this.resetTransmuxer();
|
||||
|
@ -850,9 +857,14 @@ export default class StreamController
|
|||
}
|
||||
|
||||
private onError(event: Events.ERROR, data: ErrorData) {
|
||||
if (data.type === ErrorTypes.KEY_SYSTEM_ERROR) {
|
||||
this.onFragmentOrKeyLoadError(PlaylistLevelType.MAIN, data);
|
||||
return;
|
||||
}
|
||||
switch (data.details) {
|
||||
case ErrorDetails.FRAG_LOAD_ERROR:
|
||||
case ErrorDetails.FRAG_LOAD_TIMEOUT:
|
||||
case ErrorDetails.FRAG_PARSING_ERROR:
|
||||
case ErrorDetails.KEY_LOAD_ERROR:
|
||||
case ErrorDetails.KEY_LOAD_TIMEOUT:
|
||||
this.onFragmentOrKeyLoadError(PlaylistLevelType.MAIN, data);
|
||||
|
@ -942,11 +954,11 @@ export default class StreamController
|
|||
type !== ElementaryStreamTypes.AUDIO ||
|
||||
(this.audioOnly && !this.altAudio)
|
||||
) {
|
||||
const media =
|
||||
const mediaBuffer =
|
||||
(type === ElementaryStreamTypes.VIDEO
|
||||
? this.videoBuffer
|
||||
: this.mediaBuffer) || this.media;
|
||||
this.afterBufferFlushed(media, type, PlaylistLevelType.MAIN);
|
||||
this.afterBufferFlushed(mediaBuffer, type, PlaylistLevelType.MAIN);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1013,11 +1025,11 @@ export default class StreamController
|
|||
return audioCodec;
|
||||
}
|
||||
|
||||
private _loadBitrateTestFrag(frag: Fragment) {
|
||||
private _loadBitrateTestFrag(frag: Fragment, levelDetails: LevelDetails) {
|
||||
frag.bitrateTest = true;
|
||||
this._doFragLoad(frag).then((data) => {
|
||||
this._doFragLoad(frag, levelDetails).then((data) => {
|
||||
const { hls } = this;
|
||||
if (!data || hls.nextLoadLevel || this.fragContextChanged(frag)) {
|
||||
if (!data || this.fragContextChanged(frag)) {
|
||||
return;
|
||||
}
|
||||
this.fragLoadError = 0;
|
||||
|
@ -1094,7 +1106,7 @@ export default class StreamController
|
|||
endDTS,
|
||||
};
|
||||
} else {
|
||||
if (video.firstKeyFrame && video.independent) {
|
||||
if (video.firstKeyFrame && video.independent && chunkMeta.id === 1) {
|
||||
this.couldBacktrack = true;
|
||||
}
|
||||
if (video.dropped && video.independent) {
|
||||
|
@ -1276,7 +1288,7 @@ export default class StreamController
|
|||
this.tick();
|
||||
}
|
||||
|
||||
private getMainFwdBufferInfo() {
|
||||
public getMainFwdBufferInfo(): BufferInfo | null {
|
||||
return this.getFwdBufferInfo(
|
||||
this.mediaBuffer ? this.mediaBuffer : this.media,
|
||||
PlaylistLevelType.MAIN
|
||||
|
@ -1327,13 +1339,13 @@ export default class StreamController
|
|||
fragPlaying.level !== fragCurrentLevel ||
|
||||
fragPlayingCurrent.urlId !== fragPlaying.urlId
|
||||
) {
|
||||
this.fragPlaying = fragPlayingCurrent;
|
||||
this.hls.trigger(Events.FRAG_CHANGED, { frag: fragPlayingCurrent });
|
||||
if (!fragPlaying || fragPlaying.level !== fragCurrentLevel) {
|
||||
this.hls.trigger(Events.LEVEL_SWITCHED, {
|
||||
level: fragCurrentLevel,
|
||||
});
|
||||
}
|
||||
this.fragPlaying = fragPlayingCurrent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
92
node_modules/hls.js/src/controller/subtitle-stream-controller.ts
generated
vendored
92
node_modules/hls.js/src/controller/subtitle-stream-controller.ts
generated
vendored
|
@ -7,9 +7,10 @@ import { FragmentState } from './fragment-tracker';
|
|||
import BaseStreamController, { State } from './base-stream-controller';
|
||||
import { PlaylistLevelType } from '../types/loader';
|
||||
import { Level } from '../types/level';
|
||||
import type { FragmentTracker } from './fragment-tracker';
|
||||
import type { NetworkComponentAPI } from '../types/component-api';
|
||||
import type Hls from '../hls';
|
||||
import type { FragmentTracker } from './fragment-tracker';
|
||||
import type KeyLoader from '../loader/key-loader';
|
||||
import type { LevelDetails } from '../loader/level-details';
|
||||
import type { Fragment } from '../loader/fragment';
|
||||
import type {
|
||||
|
@ -21,6 +22,7 @@ import type {
|
|||
TrackSwitchedData,
|
||||
BufferFlushingData,
|
||||
LevelLoadedData,
|
||||
FragBufferedData,
|
||||
} from '../types/events';
|
||||
|
||||
const TICK_INTERVAL = 500; // how often to tick in ms
|
||||
|
@ -40,8 +42,12 @@ export class SubtitleStreamController
|
|||
private tracksBuffered: Array<TimeRange[]> = [];
|
||||
private mainDetails: LevelDetails | null = null;
|
||||
|
||||
constructor(hls: Hls, fragmentTracker: FragmentTracker) {
|
||||
super(hls, fragmentTracker, '[subtitle-stream-controller]');
|
||||
constructor(
|
||||
hls: Hls,
|
||||
fragmentTracker: FragmentTracker,
|
||||
keyLoader: KeyLoader
|
||||
) {
|
||||
super(hls, fragmentTracker, keyLoader, '[subtitle-stream-controller]');
|
||||
this._registerListeners();
|
||||
}
|
||||
|
||||
|
@ -62,6 +68,7 @@ export class SubtitleStreamController
|
|||
hls.on(Events.SUBTITLE_TRACK_LOADED, this.onSubtitleTrackLoaded, this);
|
||||
hls.on(Events.SUBTITLE_FRAG_PROCESSED, this.onSubtitleFragProcessed, this);
|
||||
hls.on(Events.BUFFER_FLUSHING, this.onBufferFlushing, this);
|
||||
hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this);
|
||||
}
|
||||
|
||||
private _unregisterListeners() {
|
||||
|
@ -76,13 +83,20 @@ export class SubtitleStreamController
|
|||
hls.off(Events.SUBTITLE_TRACK_LOADED, this.onSubtitleTrackLoaded, this);
|
||||
hls.off(Events.SUBTITLE_FRAG_PROCESSED, this.onSubtitleFragProcessed, this);
|
||||
hls.off(Events.BUFFER_FLUSHING, this.onBufferFlushing, this);
|
||||
hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this);
|
||||
}
|
||||
|
||||
startLoad() {
|
||||
startLoad(startPosition: number) {
|
||||
this.stopLoad();
|
||||
this.state = State.IDLE;
|
||||
|
||||
this.setInterval(TICK_INTERVAL);
|
||||
|
||||
this.nextLoadPosition =
|
||||
this.startPosition =
|
||||
this.lastCurrentTime =
|
||||
startPosition;
|
||||
|
||||
this.tick();
|
||||
}
|
||||
|
||||
|
@ -174,6 +188,14 @@ export class SubtitleStreamController
|
|||
}
|
||||
}
|
||||
|
||||
onFragBuffered(event: Events.FRAG_BUFFERED, data: FragBufferedData) {
|
||||
if (!this.loadedmetadata && data.frag.type === PlaylistLevelType.MAIN) {
|
||||
if (this.media?.buffered.length) {
|
||||
this.loadedmetadata = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If something goes wrong, proceed to next frag, if we were processing one.
|
||||
onError(event: Events.ERROR, data: ErrorData) {
|
||||
const frag = data.frag;
|
||||
|
@ -182,8 +204,8 @@ export class SubtitleStreamController
|
|||
return;
|
||||
}
|
||||
|
||||
if (this.fragCurrent?.loader) {
|
||||
this.fragCurrent.loader.abort();
|
||||
if (this.fragCurrent) {
|
||||
this.fragCurrent.abortRequests();
|
||||
}
|
||||
|
||||
this.state = State.IDLE;
|
||||
|
@ -244,6 +266,7 @@ export class SubtitleStreamController
|
|||
return;
|
||||
}
|
||||
this.mediaBuffer = this.mediaBufferTimeRanges;
|
||||
let sliding = 0;
|
||||
if (newDetails.live || track.details?.live) {
|
||||
const mainDetails = this.mainDetails;
|
||||
if (newDetails.deltaUpdateFailed || !mainDetails) {
|
||||
|
@ -253,21 +276,28 @@ export class SubtitleStreamController
|
|||
if (!track.details) {
|
||||
if (newDetails.hasProgramDateTime && mainDetails.hasProgramDateTime) {
|
||||
alignMediaPlaylistByPDT(newDetails, mainDetails);
|
||||
sliding = newDetails.fragments[0].start;
|
||||
} else if (mainSlidingStartFragment) {
|
||||
// line up live playlist with main so that fragments in range are loaded
|
||||
addSliding(newDetails, mainSlidingStartFragment.start);
|
||||
sliding = mainSlidingStartFragment.start;
|
||||
addSliding(newDetails, sliding);
|
||||
}
|
||||
} else {
|
||||
const sliding = this.alignPlaylists(newDetails, track.details);
|
||||
sliding = this.alignPlaylists(newDetails, track.details);
|
||||
if (sliding === 0 && mainSlidingStartFragment) {
|
||||
// realign with main when there is no overlap with last refresh
|
||||
addSliding(newDetails, mainSlidingStartFragment.start);
|
||||
sliding = mainSlidingStartFragment.start;
|
||||
addSliding(newDetails, sliding);
|
||||
}
|
||||
}
|
||||
}
|
||||
track.details = newDetails;
|
||||
this.levelLastLoaded = trackId;
|
||||
|
||||
if (!this.startFragRequested && (this.mainDetails || !newDetails.live)) {
|
||||
this.setStartPosition(track.details, sliding);
|
||||
}
|
||||
|
||||
// trigger handler right now
|
||||
this.tick();
|
||||
|
||||
|
@ -311,7 +341,7 @@ export class SubtitleStreamController
|
|||
const startTime = performance.now();
|
||||
// decrypt the subtitles
|
||||
this.decrypter
|
||||
.webCryptoDecrypt(
|
||||
.decrypt(
|
||||
new Uint8Array(payload),
|
||||
decryptData.key.buffer,
|
||||
decryptData.iv.buffer
|
||||
|
@ -326,6 +356,10 @@ export class SubtitleStreamController
|
|||
tdecrypt: endTime,
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
this.warn(`${err.name}: ${err.message}`);
|
||||
this.state = State.IDLE;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -349,15 +383,21 @@ export class SubtitleStreamController
|
|||
// Expand range of subs loaded by one target-duration in either direction to make up for misaligned playlists
|
||||
const trackDetails = levels[currentTrackId].details as LevelDetails;
|
||||
const targetDuration = trackDetails.targetduration;
|
||||
const { config, media } = this;
|
||||
const { config } = this;
|
||||
const currentTime = this.getLoadPosition();
|
||||
const bufferedInfo = BufferHelper.bufferedInfo(
|
||||
this.tracksBuffered[this.currentTrackId] || [],
|
||||
media.currentTime - targetDuration,
|
||||
currentTime - targetDuration,
|
||||
config.maxBufferHole
|
||||
);
|
||||
const { end: targetBufferTime, len: bufferLen } = bufferedInfo;
|
||||
|
||||
const maxBufLen = this.getMaxBufferLength() + targetDuration;
|
||||
const mainBufferInfo = this.getFwdBufferInfo(
|
||||
this.media,
|
||||
PlaylistLevelType.MAIN
|
||||
);
|
||||
const maxBufLen =
|
||||
this.getMaxBufferLength(mainBufferInfo?.len) + targetDuration;
|
||||
|
||||
if (bufferLen > maxBufLen) {
|
||||
return;
|
||||
|
@ -371,7 +411,7 @@ export class SubtitleStreamController
|
|||
const fragLen = fragments.length;
|
||||
const end = trackDetails.edge;
|
||||
|
||||
let foundFrag: Fragment | null;
|
||||
let foundFrag: Fragment | null = null;
|
||||
const fragPrevious = this.fragPrevious;
|
||||
if (targetBufferTime < end) {
|
||||
const { maxFragLookUpTolerance } = config;
|
||||
|
@ -391,27 +431,28 @@ export class SubtitleStreamController
|
|||
} else {
|
||||
foundFrag = fragments[fragLen - 1];
|
||||
}
|
||||
|
||||
foundFrag = this.mapToInitFragWhenRequired(foundFrag);
|
||||
if (!foundFrag) {
|
||||
return;
|
||||
}
|
||||
|
||||
// only load if fragment is not loaded
|
||||
foundFrag = this.mapToInitFragWhenRequired(foundFrag) as Fragment;
|
||||
if (
|
||||
this.fragmentTracker.getState(foundFrag) !== FragmentState.NOT_LOADED
|
||||
this.fragmentTracker.getState(foundFrag) === FragmentState.NOT_LOADED
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (foundFrag.encrypted) {
|
||||
this.loadKey(foundFrag, trackDetails);
|
||||
} else {
|
||||
// only load if fragment is not loaded
|
||||
this.loadFragment(foundFrag, trackDetails, targetBufferTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected getMaxBufferLength(mainBufferLength?: number): number {
|
||||
const maxConfigBuffer = super.getMaxBufferLength();
|
||||
if (!mainBufferLength) {
|
||||
return maxConfigBuffer;
|
||||
}
|
||||
return Math.max(maxConfigBuffer, mainBufferLength);
|
||||
}
|
||||
|
||||
protected loadFragment(
|
||||
frag: Fragment,
|
||||
levelDetails: LevelDetails,
|
||||
|
@ -419,8 +460,9 @@ export class SubtitleStreamController
|
|||
) {
|
||||
this.fragCurrent = frag;
|
||||
if (frag.sn === 'initSegment') {
|
||||
this._loadInitSegment(frag);
|
||||
this._loadInitSegment(frag, levelDetails);
|
||||
} else {
|
||||
this.startFragRequested = true;
|
||||
super.loadFragment(frag, levelDetails, targetBufferTime);
|
||||
}
|
||||
}
|
||||
|
|
1
node_modules/hls.js/src/controller/subtitle-track-controller.ts
generated
vendored
1
node_modules/hls.js/src/controller/subtitle-track-controller.ts
generated
vendored
|
@ -275,6 +275,7 @@ class SubtitleTrackController extends BasePlaylistController {
|
|||
}
|
||||
|
||||
protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters): void {
|
||||
super.loadPlaylist();
|
||||
const currentTrack = this.tracksInGroup[this.trackId];
|
||||
if (this.shouldLoadTrack(currentTrack)) {
|
||||
const id = currentTrack.id;
|
||||
|
|
27
node_modules/hls.js/src/controller/timeline-controller.ts
generated
vendored
27
node_modules/hls.js/src/controller/timeline-controller.ts
generated
vendored
|
@ -442,6 +442,11 @@ export class TimelineController implements ComponentAPI {
|
|||
}
|
||||
}
|
||||
|
||||
private closedCaptionsForLevel(frag: Fragment): string | undefined {
|
||||
const level = this.hls.levels[frag.level];
|
||||
return level?.attrs['CLOSED-CAPTIONS'];
|
||||
}
|
||||
|
||||
private onFragLoading(event: Events.FRAG_LOADING, data: FragLoadingData) {
|
||||
const { cea608Parser1, cea608Parser2, lastSn, lastPartIndex } = this;
|
||||
if (!this.enabled || !(cea608Parser1 && cea608Parser2)) {
|
||||
|
@ -492,12 +497,7 @@ export class TimelineController implements ComponentAPI {
|
|||
// fragment after decryption has a stats object
|
||||
const decrypted = 'stats' in data;
|
||||
// If the subtitles are not encrypted, parse VTTs now. Otherwise, we need to wait.
|
||||
if (
|
||||
decryptData == null ||
|
||||
decryptData.key == null ||
|
||||
decryptData.method !== 'AES-128' ||
|
||||
decrypted
|
||||
) {
|
||||
if (decryptData == null || !decryptData.encrypted || decrypted) {
|
||||
const trackPlaylistMedia = this.tracks[frag.level];
|
||||
const vttCCs = this.vttCCs;
|
||||
if (!vttCCs[frag.cc]) {
|
||||
|
@ -654,14 +654,21 @@ export class TimelineController implements ComponentAPI {
|
|||
return;
|
||||
}
|
||||
|
||||
const { frag, samples } = data;
|
||||
if (
|
||||
frag.type === PlaylistLevelType.MAIN &&
|
||||
this.closedCaptionsForLevel(frag) === 'NONE'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// If the event contains captions (found in the bytes property), push all bytes into the parser immediately
|
||||
// It will create the proper timestamps based on the PTS value
|
||||
for (let i = 0; i < data.samples.length; i++) {
|
||||
const ccBytes = data.samples[i].bytes;
|
||||
for (let i = 0; i < samples.length; i++) {
|
||||
const ccBytes = samples[i].bytes;
|
||||
if (ccBytes) {
|
||||
const ccdatas = this.extractCea608Data(ccBytes);
|
||||
cea608Parser1.addData(data.samples[i].pts, ccdatas[0]);
|
||||
cea608Parser2.addData(data.samples[i].pts, ccdatas[1]);
|
||||
cea608Parser1.addData(samples[i].pts, ccdatas[0]);
|
||||
cea608Parser2.addData(samples[i].pts, ccdatas[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
4
node_modules/hls.js/src/crypt/aes-crypto.ts
generated
vendored
4
node_modules/hls.js/src/crypt/aes-crypto.ts
generated
vendored
|
@ -1,8 +1,8 @@
|
|||
export default class AESCrypto {
|
||||
private subtle: SubtleCrypto;
|
||||
private aesIV: ArrayBuffer;
|
||||
private aesIV: Uint8Array;
|
||||
|
||||
constructor(subtle: SubtleCrypto, iv: ArrayBuffer) {
|
||||
constructor(subtle: SubtleCrypto, iv: Uint8Array) {
|
||||
this.subtle = subtle;
|
||||
this.aesIV = iv;
|
||||
}
|
||||
|
|
84
node_modules/hls.js/src/crypt/decrypter.ts
generated
vendored
84
node_modules/hls.js/src/crypt/decrypter.ts
generated
vendored
|
@ -5,14 +5,11 @@ import { logger } from '../utils/logger';
|
|||
import { appendUint8Array } from '../utils/mp4-tools';
|
||||
import { sliceUint8 } from '../utils/typed-array';
|
||||
import type { HlsConfig } from '../config';
|
||||
import type { HlsEventEmitter } from '../events';
|
||||
|
||||
const CHUNK_SIZE = 16; // 16 bytes, 128 bits
|
||||
|
||||
export default class Decrypter {
|
||||
private logEnabled: boolean = true;
|
||||
private observer: HlsEventEmitter;
|
||||
private config: HlsConfig;
|
||||
private removePKCS7Padding: boolean;
|
||||
private subtle: SubtleCrypto | null = null;
|
||||
private softwareDecrypter: AESDecryptor | null = null;
|
||||
|
@ -21,14 +18,10 @@ export default class Decrypter {
|
|||
private remainderData: Uint8Array | null = null;
|
||||
private currentIV: ArrayBuffer | null = null;
|
||||
private currentResult: ArrayBuffer | null = null;
|
||||
private useSoftware: boolean;
|
||||
|
||||
constructor(
|
||||
observer: HlsEventEmitter,
|
||||
config: HlsConfig,
|
||||
{ removePKCS7Padding = true } = {}
|
||||
) {
|
||||
this.observer = observer;
|
||||
this.config = config;
|
||||
constructor(config: HlsConfig, { removePKCS7Padding = true } = {}) {
|
||||
this.useSoftware = config.enableSoftwareAES;
|
||||
this.removePKCS7Padding = removePKCS7Padding;
|
||||
// built in decryptor expects PKCS7 padding
|
||||
if (removePKCS7Padding) {
|
||||
|
@ -44,24 +37,29 @@ export default class Decrypter {
|
|||
}
|
||||
}
|
||||
if (this.subtle === null) {
|
||||
this.config.enableSoftwareAES = true;
|
||||
this.useSoftware = true;
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// @ts-ignore
|
||||
this.observer = null;
|
||||
this.subtle = null;
|
||||
this.softwareDecrypter = null;
|
||||
this.key = null;
|
||||
this.fastAesKey = null;
|
||||
this.remainderData = null;
|
||||
this.currentIV = null;
|
||||
this.currentResult = null;
|
||||
}
|
||||
|
||||
public isSync() {
|
||||
return this.config.enableSoftwareAES;
|
||||
return this.useSoftware;
|
||||
}
|
||||
|
||||
public flush(): Uint8Array | void {
|
||||
const { currentResult } = this;
|
||||
if (!currentResult) {
|
||||
public flush(): Uint8Array | null {
|
||||
const { currentResult, remainderData } = this;
|
||||
if (!currentResult || remainderData) {
|
||||
this.reset();
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
const data = new Uint8Array(currentResult);
|
||||
this.reset();
|
||||
|
@ -83,20 +81,24 @@ export default class Decrypter {
|
|||
public decrypt(
|
||||
data: Uint8Array | ArrayBuffer,
|
||||
key: ArrayBuffer,
|
||||
iv: ArrayBuffer,
|
||||
callback: (decryptedData: ArrayBuffer) => void
|
||||
) {
|
||||
if (this.config.enableSoftwareAES) {
|
||||
this.softwareDecrypt(new Uint8Array(data), key, iv);
|
||||
const decryptResult = this.flush();
|
||||
if (decryptResult) {
|
||||
callback(decryptResult.buffer);
|
||||
}
|
||||
} else {
|
||||
this.webCryptoDecrypt(new Uint8Array(data), key, iv).then(callback);
|
||||
iv: ArrayBuffer
|
||||
): Promise<ArrayBuffer> {
|
||||
if (this.useSoftware) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.softwareDecrypt(new Uint8Array(data), key, iv);
|
||||
const decryptResult = this.flush();
|
||||
if (decryptResult) {
|
||||
resolve(decryptResult.buffer);
|
||||
} else {
|
||||
reject(new Error('[softwareDecrypt] Failed to decrypt data'));
|
||||
}
|
||||
});
|
||||
}
|
||||
return this.webCryptoDecrypt(new Uint8Array(data), key, iv);
|
||||
}
|
||||
|
||||
// Software decryption is progressive. Progressive decryption may not return a result on each call. Any cached
|
||||
// data is handled in the flush() call
|
||||
public softwareDecrypt(
|
||||
data: Uint8Array,
|
||||
key: ArrayBuffer,
|
||||
|
@ -158,20 +160,28 @@ export default class Decrypter {
|
|||
if (!subtle) {
|
||||
return Promise.reject(new Error('web crypto not initialized'));
|
||||
}
|
||||
|
||||
const crypto = new AESCrypto(subtle, iv);
|
||||
this.logOnce('WebCrypto AES decrypt');
|
||||
const crypto = new AESCrypto(subtle, new Uint8Array(iv));
|
||||
return crypto.decrypt(data.buffer, aesKey);
|
||||
})
|
||||
.catch((err) => {
|
||||
return this.onWebCryptoError(err, data, key, iv) as ArrayBuffer;
|
||||
logger.warn(
|
||||
`[decrypter]: WebCrypto Error, disable WebCrypto API, ${err.name}: ${err.message}`
|
||||
);
|
||||
|
||||
return this.onWebCryptoError(data, key, iv);
|
||||
});
|
||||
}
|
||||
|
||||
private onWebCryptoError(err, data, key, iv): ArrayBuffer | null {
|
||||
logger.warn('[decrypter.ts]: WebCrypto Error, disable WebCrypto API:', err);
|
||||
this.config.enableSoftwareAES = true;
|
||||
private onWebCryptoError(data, key, iv): ArrayBuffer | never {
|
||||
this.useSoftware = true;
|
||||
this.logEnabled = true;
|
||||
return this.softwareDecrypt(data, key, iv);
|
||||
this.softwareDecrypt(data, key, iv);
|
||||
const decryptResult = this.flush();
|
||||
if (decryptResult) {
|
||||
return decryptResult.buffer;
|
||||
}
|
||||
throw new Error('WebCrypto and softwareDecrypt: failed to decrypt data');
|
||||
}
|
||||
|
||||
private getValidChunk(data: Uint8Array): Uint8Array {
|
||||
|
@ -188,7 +198,7 @@ export default class Decrypter {
|
|||
if (!this.logEnabled) {
|
||||
return;
|
||||
}
|
||||
logger.log(`[decrypter.ts]: ${msg}`);
|
||||
logger.log(`[decrypter]: ${msg}`);
|
||||
this.logEnabled = false;
|
||||
}
|
||||
}
|
||||
|
|
22
node_modules/hls.js/src/demux/adts.ts
generated
vendored
22
node_modules/hls.js/src/demux/adts.ts
generated
vendored
|
@ -33,18 +33,18 @@ export function getAudioConfig(
|
|||
): AudioConfig | void {
|
||||
let adtsObjectType: number;
|
||||
let adtsExtensionSamplingIndex: number;
|
||||
let adtsChanelConfig: number;
|
||||
let adtsChannelConfig: number;
|
||||
let config: number[];
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
const manifestCodec = audioCodec;
|
||||
const adtsSampleingRates = [
|
||||
const adtsSamplingRates = [
|
||||
96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025,
|
||||
8000, 7350,
|
||||
];
|
||||
// byte 2
|
||||
adtsObjectType = ((data[offset + 2] & 0xc0) >>> 6) + 1;
|
||||
const adtsSamplingIndex = (data[offset + 2] & 0x3c) >>> 2;
|
||||
if (adtsSamplingIndex > adtsSampleingRates.length - 1) {
|
||||
if (adtsSamplingIndex > adtsSamplingRates.length - 1) {
|
||||
observer.trigger(Events.ERROR, {
|
||||
type: ErrorTypes.MEDIA_ERROR,
|
||||
details: ErrorDetails.FRAG_PARSING_ERROR,
|
||||
|
@ -53,9 +53,9 @@ export function getAudioConfig(
|
|||
});
|
||||
return;
|
||||
}
|
||||
adtsChanelConfig = (data[offset + 2] & 0x01) << 2;
|
||||
adtsChannelConfig = (data[offset + 2] & 0x01) << 2;
|
||||
// byte 3
|
||||
adtsChanelConfig |= (data[offset + 3] & 0xc0) >>> 6;
|
||||
adtsChannelConfig |= (data[offset + 3] & 0xc0) >>> 6;
|
||||
logger.log(
|
||||
`manifest codec:${audioCodec}, ADTS type:${adtsObjectType}, samplingIndex:${adtsSamplingIndex}`
|
||||
);
|
||||
|
@ -101,9 +101,9 @@ export function getAudioConfig(
|
|||
if (
|
||||
(audioCodec &&
|
||||
audioCodec.indexOf('mp4a.40.2') !== -1 &&
|
||||
((adtsSamplingIndex >= 6 && adtsChanelConfig === 1) ||
|
||||
((adtsSamplingIndex >= 6 && adtsChannelConfig === 1) ||
|
||||
/vivaldi/i.test(userAgent))) ||
|
||||
(!audioCodec && adtsChanelConfig === 1)
|
||||
(!audioCodec && adtsChannelConfig === 1)
|
||||
) {
|
||||
adtsObjectType = 2;
|
||||
config = new Array(2);
|
||||
|
@ -150,9 +150,9 @@ export function getAudioConfig(
|
|||
config[0] |= (adtsSamplingIndex & 0x0e) >> 1;
|
||||
config[1] |= (adtsSamplingIndex & 0x01) << 7;
|
||||
// channelConfiguration
|
||||
config[1] |= adtsChanelConfig << 3;
|
||||
config[1] |= adtsChannelConfig << 3;
|
||||
if (adtsObjectType === 5) {
|
||||
// adtsExtensionSampleingIndex
|
||||
// adtsExtensionSamplingIndex
|
||||
config[1] |= (adtsExtensionSamplingIndex & 0x0e) >> 1;
|
||||
config[2] = (adtsExtensionSamplingIndex & 0x01) << 7;
|
||||
// adtsObjectType (force to 2, chrome is checking that object type is less than 5 ???
|
||||
|
@ -162,8 +162,8 @@ export function getAudioConfig(
|
|||
}
|
||||
return {
|
||||
config,
|
||||
samplerate: adtsSampleingRates[adtsSamplingIndex],
|
||||
channelCount: adtsChanelConfig,
|
||||
samplerate: adtsSamplingRates[adtsSamplingIndex],
|
||||
channelCount: adtsChannelConfig,
|
||||
codec: 'mp4a.40.' + adtsObjectType,
|
||||
manifestCodec,
|
||||
};
|
||||
|
|
22
node_modules/hls.js/src/demux/base-audio-demuxer.ts
generated
vendored
22
node_modules/hls.js/src/demux/base-audio-demuxer.ts
generated
vendored
|
@ -21,6 +21,7 @@ class BaseAudioDemuxer implements Demuxer {
|
|||
protected cachedData: Uint8Array | null = null;
|
||||
protected basePTS: number | null = null;
|
||||
protected initPTS: number | null = null;
|
||||
protected lastPTS: number | null = null;
|
||||
|
||||
resetInitSegment(
|
||||
initSegment: Uint8Array | undefined,
|
||||
|
@ -46,6 +47,7 @@ class BaseAudioDemuxer implements Demuxer {
|
|||
|
||||
resetContiguity(): void {
|
||||
this.basePTS = null;
|
||||
this.lastPTS = null;
|
||||
this.frameIndex = 0;
|
||||
}
|
||||
|
||||
|
@ -69,7 +71,6 @@ class BaseAudioDemuxer implements Demuxer {
|
|||
let id3Data: Uint8Array | undefined = ID3.getID3Data(data, 0);
|
||||
let offset = id3Data ? id3Data.length : 0;
|
||||
let lastDataIndex;
|
||||
let pts;
|
||||
const track = this._audioTrack;
|
||||
const id3Track = this._id3Track;
|
||||
const timestamp = id3Data ? ID3.getTimeStamp(id3Data) : undefined;
|
||||
|
@ -80,26 +81,30 @@ class BaseAudioDemuxer implements Demuxer {
|
|||
(this.frameIndex === 0 && Number.isFinite(timestamp))
|
||||
) {
|
||||
this.basePTS = initPTSFn(timestamp, timeOffset, this.initPTS);
|
||||
this.lastPTS = this.basePTS;
|
||||
}
|
||||
|
||||
if (this.lastPTS === null) {
|
||||
this.lastPTS = this.basePTS;
|
||||
}
|
||||
|
||||
// more expressive than alternative: id3Data?.length
|
||||
if (id3Data && id3Data.length > 0) {
|
||||
id3Track.samples.push({
|
||||
pts: this.basePTS,
|
||||
dts: this.basePTS,
|
||||
pts: this.lastPTS,
|
||||
dts: this.lastPTS,
|
||||
data: id3Data,
|
||||
type: MetadataSchema.audioId3,
|
||||
duration: Number.POSITIVE_INFINITY,
|
||||
});
|
||||
}
|
||||
|
||||
pts = this.basePTS;
|
||||
|
||||
while (offset < length) {
|
||||
if (this.canParse(data, offset)) {
|
||||
const frame = this.appendFrame(track, data, offset);
|
||||
if (frame) {
|
||||
this.frameIndex++;
|
||||
pts = frame.sample.pts;
|
||||
this.lastPTS = frame.sample.pts;
|
||||
offset += frame.length;
|
||||
lastDataIndex = offset;
|
||||
} else {
|
||||
|
@ -109,10 +114,11 @@ class BaseAudioDemuxer implements Demuxer {
|
|||
// after a ID3.canParse, a call to ID3.getID3Data *should* always returns some data
|
||||
id3Data = ID3.getID3Data(data, offset)!;
|
||||
id3Track.samples.push({
|
||||
pts: pts,
|
||||
dts: pts,
|
||||
pts: this.lastPTS,
|
||||
dts: this.lastPTS,
|
||||
data: id3Data,
|
||||
type: MetadataSchema.audioId3,
|
||||
duration: Number.POSITIVE_INFINITY,
|
||||
});
|
||||
offset += id3Data.length;
|
||||
lastDataIndex = offset;
|
||||
|
|
5
node_modules/hls.js/src/demux/exp-golomb.ts
generated
vendored
5
node_modules/hls.js/src/demux/exp-golomb.ts
generated
vendored
|
@ -41,13 +41,14 @@ class ExpGolomb {
|
|||
// (count:int):void
|
||||
skipBits(count: number): void {
|
||||
let skipBytes; // :int
|
||||
count = Math.min(count, this.bytesAvailable * 8 + this.bitsAvailable);
|
||||
if (this.bitsAvailable > count) {
|
||||
this.word <<= count;
|
||||
this.bitsAvailable -= count;
|
||||
} else {
|
||||
count -= this.bitsAvailable;
|
||||
skipBytes = count >> 3;
|
||||
count -= skipBytes >> 3;
|
||||
count -= skipBytes << 3;
|
||||
this.bytesAvailable -= skipBytes;
|
||||
this.loadWord();
|
||||
this.word <<= count;
|
||||
|
@ -68,6 +69,8 @@ class ExpGolomb {
|
|||
this.word <<= bits;
|
||||
} else if (this.bytesAvailable > 0) {
|
||||
this.loadWord();
|
||||
} else {
|
||||
throw new Error('no bits available');
|
||||
}
|
||||
|
||||
bits = size - bits;
|
||||
|
|
17
node_modules/hls.js/src/demux/mp4demuxer.ts
generated
vendored
17
node_modules/hls.js/src/demux/mp4demuxer.ts
generated
vendored
|
@ -42,12 +42,11 @@ class MP4Demuxer implements Demuxer {
|
|||
public resetTimeStamp() {}
|
||||
|
||||
public resetInitSegment(
|
||||
initSegment: Uint8Array,
|
||||
initSegment: Uint8Array | undefined,
|
||||
audioCodec: string | undefined,
|
||||
videoCodec: string | undefined,
|
||||
trackDuration: number
|
||||
) {
|
||||
const initData = parseInitSegment(initSegment);
|
||||
const videoTrack = (this.videoTrack = dummyTrack(
|
||||
'video',
|
||||
1
|
||||
|
@ -64,6 +63,11 @@ class MP4Demuxer implements Demuxer {
|
|||
this.id3Track = dummyTrack('id3', 1) as DemuxedMetadataTrack;
|
||||
this.timeOffset = 0;
|
||||
|
||||
if (!initSegment || !initSegment.byteLength) {
|
||||
return;
|
||||
}
|
||||
const initData = parseInitSegment(initSegment);
|
||||
|
||||
if (initData.video) {
|
||||
const { id, timescale, codec } = initData.video;
|
||||
videoTrack.id = id;
|
||||
|
@ -155,6 +159,14 @@ class MP4Demuxer implements Demuxer {
|
|||
? emsgInfo.presentationTime! / emsgInfo.timeScale
|
||||
: timeOffset +
|
||||
emsgInfo.presentationTimeDelta! / emsgInfo.timeScale;
|
||||
let duration =
|
||||
emsgInfo.eventDuration === 0xffffffff
|
||||
? Number.POSITIVE_INFINITY
|
||||
: emsgInfo.eventDuration / emsgInfo.timeScale;
|
||||
// Safari takes anything <= 0.001 seconds and maps it to Infinity
|
||||
if (duration <= 0.001) {
|
||||
duration = Number.POSITIVE_INFINITY;
|
||||
}
|
||||
const payload = emsgInfo.payload;
|
||||
id3Track.samples.push({
|
||||
data: payload,
|
||||
|
@ -162,6 +174,7 @@ class MP4Demuxer implements Demuxer {
|
|||
dts: pts,
|
||||
pts: pts,
|
||||
type: MetadataSchema.emsg,
|
||||
duration: duration,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
60
node_modules/hls.js/src/demux/sample-aes.ts
generated
vendored
60
node_modules/hls.js/src/demux/sample-aes.ts
generated
vendored
|
@ -12,7 +12,7 @@ import type {
|
|||
DemuxedVideoTrack,
|
||||
KeyData,
|
||||
} from '../types/demuxer';
|
||||
import { discardEPB } from './tsdemuxer';
|
||||
import { discardEPB } from '../utils/mp4-tools';
|
||||
|
||||
class SampleAesDecrypter {
|
||||
private keyData: KeyData;
|
||||
|
@ -20,20 +20,16 @@ class SampleAesDecrypter {
|
|||
|
||||
constructor(observer: HlsEventEmitter, config: HlsConfig, keyData: KeyData) {
|
||||
this.keyData = keyData;
|
||||
this.decrypter = new Decrypter(observer, config, {
|
||||
this.decrypter = new Decrypter(config, {
|
||||
removePKCS7Padding: false,
|
||||
});
|
||||
}
|
||||
|
||||
decryptBuffer(
|
||||
encryptedData: Uint8Array | ArrayBuffer,
|
||||
callback: (decryptedData: ArrayBuffer) => void
|
||||
) {
|
||||
this.decrypter.decrypt(
|
||||
decryptBuffer(encryptedData: Uint8Array | ArrayBuffer): Promise<ArrayBuffer> {
|
||||
return this.decrypter.decrypt(
|
||||
encryptedData,
|
||||
this.keyData.key.buffer,
|
||||
this.keyData.iv.buffer,
|
||||
callback
|
||||
this.keyData.iv.buffer
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -41,8 +37,7 @@ class SampleAesDecrypter {
|
|||
private decryptAacSample(
|
||||
samples: AudioSample[],
|
||||
sampleIndex: number,
|
||||
callback: () => void,
|
||||
sync: boolean
|
||||
callback: () => void
|
||||
) {
|
||||
const curUnit = samples[sampleIndex].unit;
|
||||
if (curUnit.length <= 16) {
|
||||
|
@ -59,13 +54,12 @@ class SampleAesDecrypter {
|
|||
encryptedData.byteOffset + encryptedData.length
|
||||
);
|
||||
|
||||
const localthis = this;
|
||||
this.decryptBuffer(encryptedBuffer, (decryptedBuffer: ArrayBuffer) => {
|
||||
this.decryptBuffer(encryptedBuffer).then((decryptedBuffer: ArrayBuffer) => {
|
||||
const decryptedData = new Uint8Array(decryptedBuffer);
|
||||
curUnit.set(decryptedData, 16);
|
||||
|
||||
if (!sync) {
|
||||
localthis.decryptAacSamples(samples, sampleIndex + 1, callback);
|
||||
if (!this.decrypter.isSync()) {
|
||||
this.decryptAacSamples(samples, sampleIndex + 1, callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -85,11 +79,9 @@ class SampleAesDecrypter {
|
|||
continue;
|
||||
}
|
||||
|
||||
const sync = this.decrypter.isSync();
|
||||
this.decryptAacSample(samples, sampleIndex, callback);
|
||||
|
||||
this.decryptAacSample(samples, sampleIndex, callback, sync);
|
||||
|
||||
if (!sync) {
|
||||
if (!this.decrypter.isSync()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -140,28 +132,17 @@ class SampleAesDecrypter {
|
|||
sampleIndex: number,
|
||||
unitIndex: number,
|
||||
callback: () => void,
|
||||
curUnit: AvcSampleUnit,
|
||||
sync: boolean
|
||||
curUnit: AvcSampleUnit
|
||||
) {
|
||||
const decodedData = discardEPB(curUnit.data);
|
||||
const encryptedData = this.getAvcEncryptedData(decodedData);
|
||||
const localthis = this;
|
||||
|
||||
this.decryptBuffer(
|
||||
encryptedData.buffer,
|
||||
function (decryptedBuffer: ArrayBuffer) {
|
||||
curUnit.data = localthis.getAvcDecryptedUnit(
|
||||
decodedData,
|
||||
decryptedBuffer
|
||||
);
|
||||
this.decryptBuffer(encryptedData.buffer).then(
|
||||
(decryptedBuffer: ArrayBuffer) => {
|
||||
curUnit.data = this.getAvcDecryptedUnit(decodedData, decryptedBuffer);
|
||||
|
||||
if (!sync) {
|
||||
localthis.decryptAvcSamples(
|
||||
samples,
|
||||
sampleIndex,
|
||||
unitIndex + 1,
|
||||
callback
|
||||
);
|
||||
if (!this.decrypter.isSync()) {
|
||||
this.decryptAvcSamples(samples, sampleIndex, unitIndex + 1, callback);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -197,18 +178,15 @@ class SampleAesDecrypter {
|
|||
continue;
|
||||
}
|
||||
|
||||
const sync = this.decrypter.isSync();
|
||||
|
||||
this.decryptAvcSample(
|
||||
samples,
|
||||
sampleIndex,
|
||||
unitIndex,
|
||||
callback,
|
||||
curUnit,
|
||||
sync
|
||||
curUnit
|
||||
);
|
||||
|
||||
if (!sync) {
|
||||
if (!this.decrypter.isSync()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
77
node_modules/hls.js/src/demux/transmuxer-interface.ts
generated
vendored
77
node_modules/hls.js/src/demux/transmuxer-interface.ts
generated
vendored
|
@ -1,4 +1,4 @@
|
|||
import * as work from 'webworkify-webpack';
|
||||
import work from './webworkify-webpack';
|
||||
import { Events } from '../events';
|
||||
import Transmuxer, {
|
||||
TransmuxConfig,
|
||||
|
@ -24,6 +24,7 @@ export default class TransmuxerInterface {
|
|||
private observer: HlsEventEmitter;
|
||||
private frag: Fragment | null = null;
|
||||
private part: Part | null = null;
|
||||
private useWorker: boolean;
|
||||
private worker: any;
|
||||
private onwmsg?: Function;
|
||||
private transmuxer: Transmuxer | null = null;
|
||||
|
@ -36,18 +37,18 @@ export default class TransmuxerInterface {
|
|||
onTransmuxComplete: (transmuxResult: TransmuxerResult) => void,
|
||||
onFlush: (chunkMeta: ChunkMetadata) => void
|
||||
) {
|
||||
const config = hls.config;
|
||||
this.hls = hls;
|
||||
this.id = id;
|
||||
this.useWorker = !!config.enableWorker;
|
||||
this.onTransmuxComplete = onTransmuxComplete;
|
||||
this.onFlush = onFlush;
|
||||
|
||||
const config = hls.config;
|
||||
|
||||
const forwardMessage = (ev, data) => {
|
||||
data = data || {};
|
||||
data.frag = this.frag;
|
||||
data.id = this.id;
|
||||
hls.trigger(ev, data);
|
||||
this.hls.trigger(ev, data);
|
||||
};
|
||||
|
||||
// forward events to main thread
|
||||
|
@ -63,7 +64,7 @@ export default class TransmuxerInterface {
|
|||
// navigator.vendor is not always available in Web Worker
|
||||
// refer to https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope/navigator
|
||||
const vendor = navigator.vendor;
|
||||
if (config.enableWorker && typeof Worker !== 'undefined') {
|
||||
if (this.useWorker && typeof Worker !== 'undefined') {
|
||||
logger.log('demuxing in webworker');
|
||||
let worker;
|
||||
try {
|
||||
|
@ -73,10 +74,12 @@ export default class TransmuxerInterface {
|
|||
this.onwmsg = this.onWorkerMessage.bind(this);
|
||||
worker.addEventListener('message', this.onwmsg);
|
||||
worker.onerror = (event) => {
|
||||
hls.trigger(Events.ERROR, {
|
||||
this.useWorker = false;
|
||||
logger.warn('Exception in webworker, fallback to inline');
|
||||
this.hls.trigger(Events.ERROR, {
|
||||
type: ErrorTypes.OTHER_ERROR,
|
||||
details: ErrorDetails.INTERNAL_EXCEPTION,
|
||||
fatal: true,
|
||||
fatal: false,
|
||||
event: 'demuxerWorker',
|
||||
error: new Error(
|
||||
`${event.message} (${event.filename}:${event.lineno})`
|
||||
|
@ -159,6 +162,7 @@ export default class TransmuxerInterface {
|
|||
chunkMeta.transmuxing.start = self.performance.now();
|
||||
const { transmuxer, worker } = this;
|
||||
const timeOffset = part ? part.start : frag.start;
|
||||
// TODO: push "clear-lead" decrypt data for unencrypted fragments in streams with encrypted ones
|
||||
const decryptdata = frag.decryptdata;
|
||||
const lastFrag = this.frag;
|
||||
|
||||
|
@ -235,10 +239,20 @@ export default class TransmuxerInterface {
|
|||
state
|
||||
);
|
||||
if (isPromise(transmuxResult)) {
|
||||
transmuxResult.then((data) => {
|
||||
this.handleTransmuxComplete(data);
|
||||
});
|
||||
transmuxer.async = true;
|
||||
transmuxResult
|
||||
.then((data) => {
|
||||
this.handleTransmuxComplete(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.transmuxerError(
|
||||
error,
|
||||
chunkMeta,
|
||||
'transmuxer-interface push error'
|
||||
);
|
||||
});
|
||||
} else {
|
||||
transmuxer.async = false;
|
||||
this.handleTransmuxComplete(transmuxResult as TransmuxerResult);
|
||||
}
|
||||
}
|
||||
|
@ -248,16 +262,29 @@ export default class TransmuxerInterface {
|
|||
chunkMeta.transmuxing.start = self.performance.now();
|
||||
const { transmuxer, worker } = this;
|
||||
if (worker) {
|
||||
1;
|
||||
worker.postMessage({
|
||||
cmd: 'flush',
|
||||
chunkMeta,
|
||||
});
|
||||
} else if (transmuxer) {
|
||||
const transmuxResult = transmuxer.flush(chunkMeta);
|
||||
if (isPromise(transmuxResult)) {
|
||||
transmuxResult.then((data) => {
|
||||
this.handleFlushResult(data, chunkMeta);
|
||||
});
|
||||
let transmuxResult = transmuxer.flush(chunkMeta);
|
||||
const asyncFlush = isPromise(transmuxResult);
|
||||
if (asyncFlush || transmuxer.async) {
|
||||
if (!isPromise(transmuxResult)) {
|
||||
transmuxResult = Promise.resolve(transmuxResult);
|
||||
}
|
||||
transmuxResult
|
||||
.then((data) => {
|
||||
this.handleFlushResult(data, chunkMeta);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.transmuxerError(
|
||||
error,
|
||||
chunkMeta,
|
||||
'transmuxer-interface flush error'
|
||||
);
|
||||
});
|
||||
} else {
|
||||
this.handleFlushResult(
|
||||
transmuxResult as Array<TransmuxerResult>,
|
||||
|
@ -267,6 +294,25 @@ export default class TransmuxerInterface {
|
|||
}
|
||||
}
|
||||
|
||||
private transmuxerError(
|
||||
error: Error,
|
||||
chunkMeta: ChunkMetadata,
|
||||
reason: string
|
||||
) {
|
||||
if (!this.hls) {
|
||||
return;
|
||||
}
|
||||
this.hls.trigger(Events.ERROR, {
|
||||
type: ErrorTypes.MEDIA_ERROR,
|
||||
details: ErrorDetails.FRAG_PARSING_ERROR,
|
||||
chunkMeta,
|
||||
fatal: false,
|
||||
error,
|
||||
err: error,
|
||||
reason,
|
||||
});
|
||||
}
|
||||
|
||||
private handleFlushResult(
|
||||
results: Array<TransmuxerResult>,
|
||||
chunkMeta: ChunkMetadata
|
||||
|
@ -304,7 +350,6 @@ export default class TransmuxerInterface {
|
|||
}
|
||||
break;
|
||||
|
||||
/* falls through */
|
||||
default: {
|
||||
data.data = data.data || {};
|
||||
data.data.frag = this.frag;
|
||||
|
|
49
node_modules/hls.js/src/demux/transmuxer-worker.ts
generated
vendored
49
node_modules/hls.js/src/demux/transmuxer-worker.ts
generated
vendored
|
@ -4,6 +4,7 @@ import { ILogFunction, enableLogs, logger } from '../utils/logger';
|
|||
import { EventEmitter } from 'eventemitter3';
|
||||
import type { RemuxedTrack, RemuxerResult } from '../types/remuxer';
|
||||
import type { TransmuxerResult, ChunkMetadata } from '../types/transmuxer';
|
||||
import { ErrorDetails, ErrorTypes } from '../errors';
|
||||
|
||||
export default function TransmuxerWorker(self) {
|
||||
const observer = new EventEmitter();
|
||||
|
@ -41,7 +42,7 @@ export default function TransmuxerWorker(self) {
|
|||
data.vendor,
|
||||
data.id
|
||||
);
|
||||
enableLogs(config.debug);
|
||||
enableLogs(config.debug, data.id);
|
||||
forwardWorkerLogs();
|
||||
forwardMessage('init', null);
|
||||
break;
|
||||
|
@ -59,21 +60,51 @@ export default function TransmuxerWorker(self) {
|
|||
data.state
|
||||
);
|
||||
if (isPromise(transmuxResult)) {
|
||||
transmuxResult.then((data) => {
|
||||
emitTransmuxComplete(self, data);
|
||||
});
|
||||
self.transmuxer.async = true;
|
||||
transmuxResult
|
||||
.then((data) => {
|
||||
emitTransmuxComplete(self, data);
|
||||
})
|
||||
.catch((error) => {
|
||||
forwardMessage(Events.ERROR, {
|
||||
type: ErrorTypes.MEDIA_ERROR,
|
||||
details: ErrorDetails.FRAG_PARSING_ERROR,
|
||||
chunkMeta: data.chunkMeta,
|
||||
fatal: false,
|
||||
error,
|
||||
err: error,
|
||||
reason: `transmuxer-worker push error`,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
self.transmuxer.async = false;
|
||||
emitTransmuxComplete(self, transmuxResult);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'flush': {
|
||||
const id = data.chunkMeta;
|
||||
const transmuxResult = self.transmuxer.flush(id);
|
||||
if (isPromise(transmuxResult)) {
|
||||
transmuxResult.then((results: Array<TransmuxerResult>) => {
|
||||
handleFlushResult(self, results as Array<TransmuxerResult>, id);
|
||||
});
|
||||
let transmuxResult = self.transmuxer.flush(id);
|
||||
const asyncFlush = isPromise(transmuxResult);
|
||||
if (asyncFlush || self.transmuxer.async) {
|
||||
if (!isPromise(transmuxResult)) {
|
||||
transmuxResult = Promise.resolve(transmuxResult);
|
||||
}
|
||||
transmuxResult
|
||||
.then((results: Array<TransmuxerResult>) => {
|
||||
handleFlushResult(self, results as Array<TransmuxerResult>, id);
|
||||
})
|
||||
.catch((error) => {
|
||||
forwardMessage(Events.ERROR, {
|
||||
type: ErrorTypes.MEDIA_ERROR,
|
||||
details: ErrorDetails.FRAG_PARSING_ERROR,
|
||||
chunkMeta: data.chunkMeta,
|
||||
fatal: false,
|
||||
error,
|
||||
err: error,
|
||||
reason: `transmuxer-worker flush error`,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
handleFlushResult(
|
||||
self,
|
||||
|
|
91
node_modules/hls.js/src/demux/transmuxer.ts
generated
vendored
91
node_modules/hls.js/src/demux/transmuxer.ts
generated
vendored
|
@ -13,7 +13,7 @@ import type { Demuxer, DemuxerResult, KeyData } from '../types/demuxer';
|
|||
import type { Remuxer } from '../types/remuxer';
|
||||
import type { TransmuxerResult, ChunkMetadata } from '../types/transmuxer';
|
||||
import type { HlsConfig } from '../config';
|
||||
import type { LevelKey } from '../loader/level-key';
|
||||
import type { DecryptData } from '../loader/level-key';
|
||||
import type { PlaylistLevelType } from '../types/loader';
|
||||
|
||||
let now;
|
||||
|
@ -26,19 +26,20 @@ try {
|
|||
}
|
||||
|
||||
type MuxConfig =
|
||||
| { demux: typeof TSDemuxer; remux: typeof MP4Remuxer }
|
||||
| { demux: typeof MP4Demuxer; remux: typeof PassThroughRemuxer }
|
||||
| { demux: typeof TSDemuxer; remux: typeof MP4Remuxer }
|
||||
| { demux: typeof AACDemuxer; remux: typeof MP4Remuxer }
|
||||
| { demux: typeof MP3Demuxer; remux: typeof MP4Remuxer };
|
||||
|
||||
const muxConfig: MuxConfig[] = [
|
||||
{ demux: TSDemuxer, remux: MP4Remuxer },
|
||||
{ demux: MP4Demuxer, remux: PassThroughRemuxer },
|
||||
{ demux: TSDemuxer, remux: MP4Remuxer },
|
||||
{ demux: AACDemuxer, remux: MP4Remuxer },
|
||||
{ demux: MP3Demuxer, remux: MP4Remuxer },
|
||||
];
|
||||
|
||||
export default class Transmuxer {
|
||||
public async: boolean = false;
|
||||
private observer: HlsEventEmitter;
|
||||
private typeSupported: TypeSupported;
|
||||
private config: HlsConfig;
|
||||
|
@ -75,7 +76,7 @@ export default class Transmuxer {
|
|||
|
||||
push(
|
||||
data: ArrayBuffer,
|
||||
decryptdata: LevelKey | null,
|
||||
decryptdata: DecryptData | null,
|
||||
chunkMeta: ChunkMetadata,
|
||||
state?: TransmuxState
|
||||
): TransmuxerResult | Promise<TransmuxerResult> {
|
||||
|
@ -83,7 +84,7 @@ export default class Transmuxer {
|
|||
stats.executeStart = now();
|
||||
|
||||
let uintData: Uint8Array = new Uint8Array(data);
|
||||
const { config, currentTransmuxState, transmuxConfig } = this;
|
||||
const { currentTransmuxState, transmuxConfig } = this;
|
||||
if (state) {
|
||||
this.currentTransmuxState = state;
|
||||
}
|
||||
|
@ -104,31 +105,23 @@ export default class Transmuxer {
|
|||
initSegmentData,
|
||||
} = transmuxConfig;
|
||||
|
||||
// Reset muxers before probing to ensure that their state is clean, even if flushing occurs before a successful probe
|
||||
if (discontinuity || trackSwitch || initSegmentChange) {
|
||||
this.resetInitSegment(initSegmentData, audioCodec, videoCodec, duration);
|
||||
}
|
||||
|
||||
if (discontinuity || initSegmentChange) {
|
||||
this.resetInitialTimestamp(defaultInitPts);
|
||||
}
|
||||
|
||||
if (!contiguous) {
|
||||
this.resetContiguity();
|
||||
}
|
||||
|
||||
const keyData = getEncryptionType(uintData, decryptdata);
|
||||
if (keyData && keyData.method === 'AES-128') {
|
||||
const decrypter = this.getDecrypter();
|
||||
// Software decryption is synchronous; webCrypto is not
|
||||
if (config.enableSoftwareAES) {
|
||||
if (decrypter.isSync()) {
|
||||
// Software decryption is progressive. Progressive decryption may not return a result on each call. Any cached
|
||||
// data is handled in the flush() call
|
||||
const decryptedData = decrypter.softwareDecrypt(
|
||||
let decryptedData = decrypter.softwareDecrypt(
|
||||
uintData,
|
||||
keyData.key.buffer,
|
||||
keyData.iv.buffer
|
||||
);
|
||||
// For Low-Latency HLS Parts, decrypt in place, since part parsing is expected on push progress
|
||||
const loadingParts = chunkMeta.part > -1;
|
||||
if (loadingParts) {
|
||||
decryptedData = decrypter.flush();
|
||||
}
|
||||
if (!decryptedData) {
|
||||
stats.executeEnd = now();
|
||||
return emptyResult(chunkMeta);
|
||||
|
@ -152,8 +145,27 @@ export default class Transmuxer {
|
|||
}
|
||||
}
|
||||
|
||||
if (this.needsProbing(uintData, discontinuity, trackSwitch)) {
|
||||
this.configureTransmuxer(uintData, transmuxConfig);
|
||||
const resetMuxers = this.needsProbing(discontinuity, trackSwitch);
|
||||
if (resetMuxers) {
|
||||
this.configureTransmuxer(uintData);
|
||||
}
|
||||
|
||||
if (discontinuity || trackSwitch || initSegmentChange || resetMuxers) {
|
||||
this.resetInitSegment(
|
||||
initSegmentData,
|
||||
audioCodec,
|
||||
videoCodec,
|
||||
duration,
|
||||
decryptdata
|
||||
);
|
||||
}
|
||||
|
||||
if (discontinuity || initSegmentChange || resetMuxers) {
|
||||
this.resetInitialTimestamp(defaultInitPts);
|
||||
}
|
||||
|
||||
if (!contiguous) {
|
||||
this.resetContiguity();
|
||||
}
|
||||
|
||||
const result = this.transmux(
|
||||
|
@ -283,7 +295,8 @@ export default class Transmuxer {
|
|||
initSegmentData: Uint8Array | undefined,
|
||||
audioCodec: string | undefined,
|
||||
videoCodec: string | undefined,
|
||||
trackDuration: number
|
||||
trackDuration: number,
|
||||
decryptdata: DecryptData | null
|
||||
) {
|
||||
const { demuxer, remuxer } = this;
|
||||
if (!demuxer || !remuxer) {
|
||||
|
@ -295,7 +308,12 @@ export default class Transmuxer {
|
|||
videoCodec,
|
||||
trackDuration
|
||||
);
|
||||
remuxer.resetInitSegment(initSegmentData, audioCodec, videoCodec);
|
||||
remuxer.resetInitSegment(
|
||||
initSegmentData,
|
||||
audioCodec,
|
||||
videoCodec,
|
||||
decryptdata
|
||||
);
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
|
@ -388,18 +406,8 @@ export default class Transmuxer {
|
|||
});
|
||||
}
|
||||
|
||||
private configureTransmuxer(
|
||||
data: Uint8Array,
|
||||
transmuxConfig: TransmuxConfig
|
||||
) {
|
||||
private configureTransmuxer(data: Uint8Array) {
|
||||
const { config, observer, typeSupported, vendor } = this;
|
||||
const {
|
||||
audioCodec,
|
||||
defaultInitPts,
|
||||
duration,
|
||||
initSegmentData,
|
||||
videoCodec,
|
||||
} = transmuxConfig;
|
||||
// probe for content type
|
||||
let mux;
|
||||
for (let i = 0, len = muxConfig.length; i < len; i++) {
|
||||
|
@ -427,16 +435,9 @@ export default class Transmuxer {
|
|||
this.demuxer = new Demuxer(observer, config, typeSupported);
|
||||
this.probe = Demuxer.probe;
|
||||
}
|
||||
// Ensure that muxers are always initialized with an initSegment
|
||||
this.resetInitSegment(initSegmentData, audioCodec, videoCodec, duration);
|
||||
this.resetInitialTimestamp(defaultInitPts);
|
||||
}
|
||||
|
||||
private needsProbing(
|
||||
data: Uint8Array,
|
||||
discontinuity: boolean,
|
||||
trackSwitch: boolean
|
||||
): boolean {
|
||||
private needsProbing(discontinuity: boolean, trackSwitch: boolean): boolean {
|
||||
// in case of continuity change, or track switch
|
||||
// we might switch from content type (AAC container to TS container, or TS to fmp4 for example)
|
||||
return !this.demuxer || !this.remuxer || discontinuity || trackSwitch;
|
||||
|
@ -445,7 +446,7 @@ export default class Transmuxer {
|
|||
private getDecrypter(): Decrypter {
|
||||
let decrypter = this.decrypter;
|
||||
if (!decrypter) {
|
||||
decrypter = this.decrypter = new Decrypter(this.observer, this.config);
|
||||
decrypter = this.decrypter = new Decrypter(this.config);
|
||||
}
|
||||
return decrypter;
|
||||
}
|
||||
|
@ -453,7 +454,7 @@ export default class Transmuxer {
|
|||
|
||||
function getEncryptionType(
|
||||
data: Uint8Array,
|
||||
decryptData: LevelKey | null
|
||||
decryptData: DecryptData | null
|
||||
): KeyData | null {
|
||||
let encryptionType: KeyData | null = null;
|
||||
if (
|
||||
|
|
132
node_modules/hls.js/src/demux/tsdemuxer.ts
generated
vendored
132
node_modules/hls.js/src/demux/tsdemuxer.ts
generated
vendored
|
@ -56,6 +56,8 @@ export interface TypeSupported {
|
|||
mp4: boolean;
|
||||
}
|
||||
|
||||
const PACKET_LENGTH = 188;
|
||||
|
||||
class TSDemuxer implements Demuxer {
|
||||
private readonly observer: HlsEventEmitter;
|
||||
private readonly config: HlsConfig;
|
||||
|
@ -87,8 +89,37 @@ class TSDemuxer implements Demuxer {
|
|||
}
|
||||
|
||||
static probe(data: Uint8Array) {
|
||||
// a TS init segment should contain at least 2 TS packets: PAT and PMT, each starting with 0x47
|
||||
return data[0] === 0x47 && data[188] === 0x47;
|
||||
const syncOffset = TSDemuxer.syncOffset(data);
|
||||
if (syncOffset > 0) {
|
||||
logger.warn(
|
||||
`MPEG2-TS detected but first sync word found @ offset ${syncOffset}`
|
||||
);
|
||||
}
|
||||
return syncOffset !== -1;
|
||||
}
|
||||
|
||||
static syncOffset(data: Uint8Array): number {
|
||||
const scanwindow =
|
||||
Math.min(PACKET_LENGTH * 5, data.length - PACKET_LENGTH) + 1;
|
||||
let i = 0;
|
||||
while (i < scanwindow) {
|
||||
// a TS init segment should contain at least 2 TS packets: PAT and PMT, each starting with 0x47
|
||||
let foundPat = false;
|
||||
for (let j = 0; j < scanwindow; j += PACKET_LENGTH) {
|
||||
if (data[j] === 0x47) {
|
||||
if (!foundPat && parsePID(data, j) === 0) {
|
||||
foundPat = true;
|
||||
}
|
||||
if (foundPat && j + PACKET_LENGTH > scanwindow) {
|
||||
return i;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -161,6 +192,8 @@ class TSDemuxer implements Demuxer {
|
|||
_id3Track.pesData = null;
|
||||
}
|
||||
this.aacOverFlow = null;
|
||||
this.avcSample = null;
|
||||
this.remainderData = null;
|
||||
}
|
||||
|
||||
public demux(
|
||||
|
@ -197,7 +230,7 @@ class TSDemuxer implements Demuxer {
|
|||
this.remainderData = null;
|
||||
}
|
||||
|
||||
if (len < 188 && !flush) {
|
||||
if (len < PACKET_LENGTH && !flush) {
|
||||
this.remainderData = data;
|
||||
return {
|
||||
audioTrack,
|
||||
|
@ -207,7 +240,8 @@ class TSDemuxer implements Demuxer {
|
|||
};
|
||||
}
|
||||
|
||||
len -= len % 188;
|
||||
const syncOffset = Math.max(0, TSDemuxer.syncOffset(data));
|
||||
len -= (len - syncOffset) % PACKET_LENGTH;
|
||||
if (len < data.byteLength && !flush) {
|
||||
this.remainderData = new Uint8Array(
|
||||
data.buffer,
|
||||
|
@ -218,11 +252,10 @@ class TSDemuxer implements Demuxer {
|
|||
|
||||
// loop through TS packets
|
||||
let tsPacketErrors = 0;
|
||||
for (let start = 0; start < len; start += 188) {
|
||||
for (let start = syncOffset; start < len; start += PACKET_LENGTH) {
|
||||
if (data[start] === 0x47) {
|
||||
const stt = !!(data[start + 1] & 0x40);
|
||||
// pid is a 13-bit field starting at the last bit of TS[1]
|
||||
const pid = ((data[start + 1] & 0x1f) << 8) + data[start + 2];
|
||||
const pid = parsePID(data, start);
|
||||
const atf = (data[start + 3] & 0x30) >> 4;
|
||||
|
||||
// if an adaption field is present, its length is specified by the fifth byte of the TS packet header.
|
||||
|
@ -230,7 +263,7 @@ class TSDemuxer implements Demuxer {
|
|||
if (atf > 1) {
|
||||
offset = start + 5 + data[start + 4];
|
||||
// continue if there is only adaptation field
|
||||
if (offset === start + 188) {
|
||||
if (offset === start + PACKET_LENGTH) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
|
@ -246,8 +279,8 @@ class TSDemuxer implements Demuxer {
|
|||
avcData = { data: [], size: 0 };
|
||||
}
|
||||
if (avcData) {
|
||||
avcData.data.push(data.subarray(offset, start + 188));
|
||||
avcData.size += start + 188 - offset;
|
||||
avcData.data.push(data.subarray(offset, start + PACKET_LENGTH));
|
||||
avcData.size += start + PACKET_LENGTH - offset;
|
||||
}
|
||||
break;
|
||||
case audioId:
|
||||
|
@ -265,8 +298,8 @@ class TSDemuxer implements Demuxer {
|
|||
audioData = { data: [], size: 0 };
|
||||
}
|
||||
if (audioData) {
|
||||
audioData.data.push(data.subarray(offset, start + 188));
|
||||
audioData.size += start + 188 - offset;
|
||||
audioData.data.push(data.subarray(offset, start + PACKET_LENGTH));
|
||||
audioData.size += start + PACKET_LENGTH - offset;
|
||||
}
|
||||
break;
|
||||
case id3Id:
|
||||
|
@ -278,8 +311,8 @@ class TSDemuxer implements Demuxer {
|
|||
id3Data = { data: [], size: 0 };
|
||||
}
|
||||
if (id3Data) {
|
||||
id3Data.data.push(data.subarray(offset, start + 188));
|
||||
id3Data.size += start + 188 - offset;
|
||||
id3Data.data.push(data.subarray(offset, start + PACKET_LENGTH));
|
||||
id3Data.size += start + PACKET_LENGTH - offset;
|
||||
}
|
||||
break;
|
||||
case 0:
|
||||
|
@ -288,6 +321,7 @@ class TSDemuxer implements Demuxer {
|
|||
}
|
||||
|
||||
pmtId = this._pmtId = parsePAT(data, offset);
|
||||
// logger.log('PMT PID:' + this._pmtId);
|
||||
break;
|
||||
case pmtId: {
|
||||
if (stt) {
|
||||
|
@ -325,11 +359,13 @@ class TSDemuxer implements Demuxer {
|
|||
if (unknownPID !== null && !pmtParsed) {
|
||||
logger.log(`unknown PID '${unknownPID}' in TS found`);
|
||||
unknownPID = null;
|
||||
// we set it to -188, the += 188 in the for loop will reset start to 0
|
||||
start = syncOffset - 188;
|
||||
}
|
||||
pmtParsed = this.pmtParsed = true;
|
||||
break;
|
||||
}
|
||||
case 17:
|
||||
case 0x11:
|
||||
case 0x1fff:
|
||||
break;
|
||||
default:
|
||||
|
@ -574,7 +610,8 @@ class TSDemuxer implements Demuxer {
|
|||
avcSample.debug += 'SEI ';
|
||||
}
|
||||
parseSEIMessageFromNALu(
|
||||
discardEPB(unit.data),
|
||||
unit.data,
|
||||
1,
|
||||
pes.pts as number,
|
||||
textTrack.samples
|
||||
);
|
||||
|
@ -938,6 +975,7 @@ class TSDemuxer implements Demuxer {
|
|||
}
|
||||
const id3Sample = Object.assign({}, pes as Required<PES>, {
|
||||
type: this._avcTrack ? MetadataSchema.emsg : MetadataSchema.audioId3,
|
||||
duration: Number.POSITIVE_INFINITY,
|
||||
});
|
||||
id3Track.samples.push(id3Sample);
|
||||
}
|
||||
|
@ -960,13 +998,22 @@ function createAVCSample(
|
|||
};
|
||||
}
|
||||
|
||||
function parsePAT(data, offset) {
|
||||
// skip the PSI header and parse the first PMT entry
|
||||
return ((data[offset + 10] & 0x1f) << 8) | data[offset + 11];
|
||||
// logger.log('PMT PID:' + this._pmtId);
|
||||
function parsePID(data: Uint8Array, offset: number): number {
|
||||
// pid is a 13-bit field starting at the last bit of TS[1]
|
||||
return ((data[offset + 1] & 0x1f) << 8) + data[offset + 2];
|
||||
}
|
||||
|
||||
function parsePMT(data, offset, typeSupported, isSampleAes) {
|
||||
function parsePAT(data: Uint8Array, offset: number): number {
|
||||
// skip the PSI header and parse the first PMT entry
|
||||
return ((data[offset + 10] & 0x1f) << 8) | data[offset + 11];
|
||||
}
|
||||
|
||||
function parsePMT(
|
||||
data: Uint8Array,
|
||||
offset: number,
|
||||
typeSupported: TypeSupported,
|
||||
isSampleAes: boolean
|
||||
) {
|
||||
const result = { audio: -1, avc: -1, id3: -1, segmentCodec: 'aac' };
|
||||
const sectionLength = ((data[offset + 1] & 0x0f) << 8) | data[offset + 2];
|
||||
const tableEnd = offset + 3 + sectionLength - 4;
|
||||
|
@ -977,7 +1024,7 @@ function parsePMT(data, offset, typeSupported, isSampleAes) {
|
|||
// advance the offset to the first entry in the mapping table
|
||||
offset += 12 + programInfoLength;
|
||||
while (offset < tableEnd) {
|
||||
const pid = ((data[offset + 1] & 0x1f) << 8) | data[offset + 2];
|
||||
const pid = parsePID(data, offset);
|
||||
switch (data[offset]) {
|
||||
case 0xcf: // SAMPLE-AES AAC
|
||||
if (!isSampleAes) {
|
||||
|
@ -1173,45 +1220,4 @@ function pushAccessUnit(avcSample: ParsedAvcSample, avcTrack: DemuxedAvcTrack) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* remove Emulation Prevention bytes from a RBSP
|
||||
*/
|
||||
export function discardEPB(data: Uint8Array): Uint8Array {
|
||||
const length = data.byteLength;
|
||||
const EPBPositions = [] as Array<number>;
|
||||
let i = 1;
|
||||
|
||||
// Find all `Emulation Prevention Bytes`
|
||||
while (i < length - 2) {
|
||||
if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0x03) {
|
||||
EPBPositions.push(i + 2);
|
||||
i += 2;
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// If no Emulation Prevention Bytes were found just return the original
|
||||
// array
|
||||
if (EPBPositions.length === 0) {
|
||||
return data;
|
||||
}
|
||||
|
||||
// Create a new array to hold the NAL unit data
|
||||
const newLength = length - EPBPositions.length;
|
||||
const newData = new Uint8Array(newLength);
|
||||
let sourceIndex = 0;
|
||||
|
||||
for (i = 0; i < newLength; sourceIndex++, i++) {
|
||||
if (sourceIndex === EPBPositions[0]) {
|
||||
// Skip this byte
|
||||
sourceIndex++;
|
||||
// Remove this position index
|
||||
EPBPositions.shift();
|
||||
}
|
||||
newData[i] = data[sourceIndex];
|
||||
}
|
||||
return newData;
|
||||
}
|
||||
|
||||
export default TSDemuxer;
|
||||
|
|
7
node_modules/hls.js/src/errors.ts
generated
vendored
7
node_modules/hls.js/src/errors.ts
generated
vendored
|
@ -19,8 +19,13 @@ export enum ErrorDetails {
|
|||
KEY_SYSTEM_NO_KEYS = 'keySystemNoKeys',
|
||||
KEY_SYSTEM_NO_ACCESS = 'keySystemNoAccess',
|
||||
KEY_SYSTEM_NO_SESSION = 'keySystemNoSession',
|
||||
KEY_SYSTEM_NO_CONFIGURED_LICENSE = 'keySystemNoConfiguredLicense',
|
||||
KEY_SYSTEM_LICENSE_REQUEST_FAILED = 'keySystemLicenseRequestFailed',
|
||||
KEY_SYSTEM_NO_INIT_DATA = 'keySystemNoInitData',
|
||||
KEY_SYSTEM_SERVER_CERTIFICATE_REQUEST_FAILED = 'keySystemServerCertificateRequestFailed',
|
||||
KEY_SYSTEM_SERVER_CERTIFICATE_UPDATE_FAILED = 'keySystemServerCertificateUpdateFailed',
|
||||
KEY_SYSTEM_SESSION_UPDATE_FAILED = 'keySystemSessionUpdateFailed',
|
||||
KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED = 'keySystemStatusOutputRestricted',
|
||||
KEY_SYSTEM_STATUS_INTERNAL_ERROR = 'keySystemStatusInternalError',
|
||||
// Identifier for a manifest load error - data: { url : faulty URL, response : { code: error code, text: error text }}
|
||||
MANIFEST_LOAD_ERROR = 'manifestLoadError',
|
||||
// Identifier for a manifest load timeout - data: { url : faulty URL, response : { code: error code, text: error text }}
|
||||
|
|
2
node_modules/hls.js/src/events.ts
generated
vendored
2
node_modules/hls.js/src/events.ts
generated
vendored
|
@ -160,7 +160,7 @@ export enum Events {
|
|||
DESTROYING = 'hlsDestroying',
|
||||
// fired when a decrypt key loading starts - data: { frag : fragment object }
|
||||
KEY_LOADING = 'hlsKeyLoading',
|
||||
// fired when a decrypt key loading is completed - data: { frag : fragment object, payload : key payload, stats : LoaderStats }
|
||||
// fired when a decrypt key loading is completed - data: { frag : fragment object, keyInfo : KeyLoaderInfo }
|
||||
KEY_LOADED = 'hlsKeyLoaded',
|
||||
// deprecated; please use BACK_BUFFER_REACHED - data : { bufferEnd: number }
|
||||
LIVE_BACK_BUFFER_REACHED = 'hlsLiveBackBufferReached',
|
||||
|
|
96
node_modules/hls.js/src/hls.ts
generated
vendored
96
node_modules/hls.js/src/hls.ts
generated
vendored
|
@ -1,10 +1,10 @@
|
|||
import * as URLToolkit from 'url-toolkit';
|
||||
import PlaylistLoader from './loader/playlist-loader';
|
||||
import KeyLoader from './loader/key-loader';
|
||||
import ID3TrackController from './controller/id3-track-controller';
|
||||
import LatencyController from './controller/latency-controller';
|
||||
import LevelController from './controller/level-controller';
|
||||
import { FragmentTracker } from './controller/fragment-tracker';
|
||||
import KeyLoader from './loader/key-loader';
|
||||
import StreamController from './controller/stream-controller';
|
||||
import { isSupported } from './is-supported';
|
||||
import { logger, enableLogs } from './utils/logger';
|
||||
|
@ -23,8 +23,9 @@ import type SubtitleTrackController from './controller/subtitle-track-controller
|
|||
import type { ComponentAPI, NetworkComponentAPI } from './types/component-api';
|
||||
import type { MediaPlaylist } from './types/media-playlist';
|
||||
import type { HlsConfig } from './config';
|
||||
import type { Level } from './types/level';
|
||||
import { HdcpLevel, HdcpLevels, Level } from './types/level';
|
||||
import type { Fragment } from './loader/fragment';
|
||||
import { BufferInfo } from './utils/buffer-helper';
|
||||
|
||||
/**
|
||||
* @module Hls
|
||||
|
@ -42,6 +43,7 @@ export default class Hls implements HlsEventEmitter {
|
|||
|
||||
private _emitter: HlsEventEmitter = new EventEmitter();
|
||||
private _autoLevelCapping: number;
|
||||
private _maxHdcpLevel: HdcpLevel = null;
|
||||
private abrController: AbrController;
|
||||
private bufferController: BufferController;
|
||||
private capLevelController: CapLevelController;
|
||||
|
@ -100,7 +102,7 @@ export default class Hls implements HlsEventEmitter {
|
|||
constructor(userConfig: Partial<HlsConfig> = {}) {
|
||||
const config = (this.config = mergeConfig(Hls.DefaultConfig, userConfig));
|
||||
this.userConfig = userConfig;
|
||||
enableLogs(config.debug);
|
||||
enableLogs(config.debug, 'Hls instance');
|
||||
|
||||
this._autoLevelCapping = -1;
|
||||
|
||||
|
@ -123,16 +125,17 @@ export default class Hls implements HlsEventEmitter {
|
|||
|
||||
const fpsController = new ConfigFpsController(this);
|
||||
const playListLoader = new PlaylistLoader(this);
|
||||
const keyLoader = new KeyLoader(this);
|
||||
const id3TrackController = new ID3TrackController(this);
|
||||
|
||||
// network controllers
|
||||
const levelController = (this.levelController = new LevelController(this));
|
||||
// FragmentTracker must be defined before StreamController because the order of event handling is important
|
||||
const fragmentTracker = new FragmentTracker(this);
|
||||
const keyLoader = new KeyLoader(this.config);
|
||||
const streamController = (this.streamController = new StreamController(
|
||||
this,
|
||||
fragmentTracker
|
||||
fragmentTracker,
|
||||
keyLoader
|
||||
));
|
||||
|
||||
// Cap level controller uses streamController to flush the buffer
|
||||
|
@ -140,15 +143,14 @@ export default class Hls implements HlsEventEmitter {
|
|||
// fpsController uses streamController to switch when frames are being dropped
|
||||
fpsController.setStreamController(streamController);
|
||||
|
||||
const networkControllers = [
|
||||
const networkControllers: NetworkComponentAPI[] = [
|
||||
playListLoader,
|
||||
keyLoader,
|
||||
levelController,
|
||||
streamController,
|
||||
];
|
||||
|
||||
this.networkControllers = networkControllers;
|
||||
const coreComponents = [
|
||||
const coreComponents: ComponentAPI[] = [
|
||||
abrController,
|
||||
bufferController,
|
||||
capLevelController,
|
||||
|
@ -159,50 +161,45 @@ export default class Hls implements HlsEventEmitter {
|
|||
|
||||
this.audioTrackController = this.createController(
|
||||
config.audioTrackController,
|
||||
null,
|
||||
networkControllers
|
||||
);
|
||||
this.createController(
|
||||
config.audioStreamController,
|
||||
fragmentTracker,
|
||||
networkControllers
|
||||
);
|
||||
// subtitleTrackController must be defined before because the order of event handling is important
|
||||
const AudioStreamControllerClass = config.audioStreamController;
|
||||
if (AudioStreamControllerClass) {
|
||||
networkControllers.push(
|
||||
new AudioStreamControllerClass(this, fragmentTracker, keyLoader)
|
||||
);
|
||||
}
|
||||
// subtitleTrackController must be defined before subtitleStreamController because the order of event handling is important
|
||||
this.subtitleTrackController = this.createController(
|
||||
config.subtitleTrackController,
|
||||
null,
|
||||
networkControllers
|
||||
);
|
||||
this.createController(
|
||||
config.subtitleStreamController,
|
||||
fragmentTracker,
|
||||
networkControllers
|
||||
);
|
||||
this.createController(config.timelineController, null, coreComponents);
|
||||
this.emeController = this.createController(
|
||||
const SubtitleStreamControllerClass = config.subtitleStreamController;
|
||||
if (SubtitleStreamControllerClass) {
|
||||
networkControllers.push(
|
||||
new SubtitleStreamControllerClass(this, fragmentTracker, keyLoader)
|
||||
);
|
||||
}
|
||||
this.createController(config.timelineController, coreComponents);
|
||||
keyLoader.emeController = this.emeController = this.createController(
|
||||
config.emeController,
|
||||
null,
|
||||
coreComponents
|
||||
);
|
||||
this.cmcdController = this.createController(
|
||||
config.cmcdController,
|
||||
null,
|
||||
coreComponents
|
||||
);
|
||||
this.latencyController = this.createController(
|
||||
LatencyController,
|
||||
null,
|
||||
coreComponents
|
||||
);
|
||||
|
||||
this.coreComponents = coreComponents;
|
||||
}
|
||||
|
||||
createController(ControllerClass, fragmentTracker, components) {
|
||||
createController(ControllerClass, components) {
|
||||
if (ControllerClass) {
|
||||
const controllerInstance = fragmentTracker
|
||||
? new ControllerClass(this, fragmentTracker)
|
||||
: new ControllerClass(this);
|
||||
const controllerInstance = new ControllerClass(this);
|
||||
if (components) {
|
||||
components.push(controllerInstance);
|
||||
}
|
||||
|
@ -596,6 +593,16 @@ export default class Hls implements HlsEventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
get maxHdcpLevel(): HdcpLevel {
|
||||
return this._maxHdcpLevel;
|
||||
}
|
||||
|
||||
set maxHdcpLevel(value: HdcpLevel) {
|
||||
if (HdcpLevels.indexOf(value) > -1) {
|
||||
this._maxHdcpLevel = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* True when automatic level selection enabled
|
||||
* @type {boolean}
|
||||
|
@ -638,7 +645,7 @@ export default class Hls implements HlsEventEmitter {
|
|||
* @type {number}
|
||||
*/
|
||||
get maxAutoLevel(): number {
|
||||
const { levels, autoLevelCapping } = this;
|
||||
const { levels, autoLevelCapping, maxHdcpLevel } = this;
|
||||
|
||||
let maxAutoLevel;
|
||||
if (autoLevelCapping === -1 && levels && levels.length) {
|
||||
|
@ -647,6 +654,15 @@ export default class Hls implements HlsEventEmitter {
|
|||
maxAutoLevel = autoLevelCapping;
|
||||
}
|
||||
|
||||
if (maxHdcpLevel) {
|
||||
for (let i = maxAutoLevel; i--; ) {
|
||||
const hdcpLevel = levels[i].attrs['HDCP-LEVEL'];
|
||||
if (hdcpLevel && hdcpLevel <= maxHdcpLevel) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return maxAutoLevel;
|
||||
}
|
||||
|
||||
|
@ -682,6 +698,10 @@ export default class Hls implements HlsEventEmitter {
|
|||
return this.streamController.currentProgramDateTime;
|
||||
}
|
||||
|
||||
public get mainForwardBufferInfo(): BufferInfo | null {
|
||||
return this.streamController.getMainFwdBufferInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {AudioTrack[]}
|
||||
*/
|
||||
|
@ -867,7 +887,11 @@ export type {
|
|||
TSDemuxerConfig,
|
||||
} from './config';
|
||||
export type { CuesInterface } from './utils/cues';
|
||||
export type { MediaKeyFunc, KeySystems } from './utils/mediakeys-helper';
|
||||
export type {
|
||||
MediaKeyFunc,
|
||||
KeySystems,
|
||||
KeySystemFormats,
|
||||
} from './utils/mediakeys-helper';
|
||||
export type { DateRange } from './loader/date-range';
|
||||
export type { LoadStats } from './loader/load-stats';
|
||||
export type { LevelKey } from './loader/level-key';
|
||||
|
@ -879,10 +903,12 @@ export type {
|
|||
UserdataSample,
|
||||
} from './types/demuxer';
|
||||
export type {
|
||||
LevelParsed,
|
||||
LevelAttributes,
|
||||
HlsUrlParameters,
|
||||
HdcpLevel,
|
||||
HdcpLevels,
|
||||
HlsSkip,
|
||||
HlsUrlParameters,
|
||||
LevelAttributes,
|
||||
LevelParsed,
|
||||
} from './types/level';
|
||||
export type {
|
||||
PlaylistLevelType,
|
||||
|
|
2
node_modules/hls.js/src/is-supported.ts
generated
vendored
2
node_modules/hls.js/src/is-supported.ts
generated
vendored
|
@ -1,5 +1,5 @@
|
|||
import { getMediaSource } from './utils/mediasource-helper';
|
||||
import { ExtendedSourceBuffer } from './types/buffer';
|
||||
import type { ExtendedSourceBuffer } from './types/buffer';
|
||||
|
||||
function getSourceBuffer(): typeof self.SourceBuffer {
|
||||
return self.SourceBuffer || (self as any).WebKitSourceBuffer;
|
||||
|
|
32
node_modules/hls.js/src/loader/fragment-loader.ts
generated
vendored
32
node_modules/hls.js/src/loader/fragment-loader.ts
generated
vendored
|
@ -7,7 +7,7 @@ import {
|
|||
} from '../types/loader';
|
||||
import type { HlsConfig } from '../config';
|
||||
import type { BaseSegment, Part } from './fragment';
|
||||
import type { FragLoadedData } from '../types/events';
|
||||
import type { FragLoadedData, PartsLoadedData } from '../types/events';
|
||||
|
||||
const MIN_CHUNK_SIZE = Math.pow(2, 17); // 128kb
|
||||
|
||||
|
@ -82,10 +82,15 @@ export default class FragmentLoader {
|
|||
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: response.data as ArrayBuffer,
|
||||
payload,
|
||||
networkDetails,
|
||||
});
|
||||
},
|
||||
|
@ -286,8 +291,23 @@ function createLoaderContext(
|
|||
const start = segment.byteRangeStartOffset;
|
||||
const end = segment.byteRangeEndOffset;
|
||||
if (Number.isFinite(start) && Number.isFinite(end)) {
|
||||
loaderContext.rangeStart = start;
|
||||
loaderContext.rangeEnd = 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;
|
||||
}
|
||||
|
@ -315,4 +335,6 @@ export interface FragLoadFailResult {
|
|||
networkDetails: any;
|
||||
}
|
||||
|
||||
export type FragmentLoadProgressCallback = (result: FragLoadedData) => void;
|
||||
export type FragmentLoadProgressCallback = (
|
||||
result: FragLoadedData | PartsLoadedData
|
||||
) => void;
|
||||
|
|
99
node_modules/hls.js/src/loader/fragment.ts
generated
vendored
99
node_modules/hls.js/src/loader/fragment.ts
generated
vendored
|
@ -1,13 +1,14 @@
|
|||
import { buildAbsoluteURL } from 'url-toolkit';
|
||||
import { logger } from '../utils/logger';
|
||||
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 enum ElementaryStreamTypes {
|
||||
AUDIO = 'audio',
|
||||
|
@ -101,14 +102,16 @@ export class Fragment extends BaseSegment {
|
|||
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;
|
||||
// levelkey is the EXT-X-KEY that applies to this segment for decryption
|
||||
// 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 levelkey?: LevelKey;
|
||||
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
|
||||
|
@ -141,6 +144,8 @@ export class Fragment extends BaseSegment {
|
|||
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;
|
||||
|
||||
constructor(type: PlaylistLevelType, baseurl: string) {
|
||||
super(baseurl);
|
||||
|
@ -148,36 +153,25 @@ export class Fragment extends BaseSegment {
|
|||
}
|
||||
|
||||
get decryptdata(): LevelKey | null {
|
||||
if (!this.levelkey && !this._decryptdata) {
|
||||
const { levelkeys } = this;
|
||||
if (!levelkeys && !this._decryptdata) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this._decryptdata && this.levelkey) {
|
||||
let sn = this.sn;
|
||||
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.levelkey &&
|
||||
this.levelkey.method === 'AES-128' &&
|
||||
!this.levelkey.iv
|
||||
) {
|
||||
logger.warn(
|
||||
`missing IV for initialization segment with method="${this.levelkey.method}" - compliance issue`
|
||||
);
|
||||
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.
|
||||
}
|
||||
|
||||
/*
|
||||
Be converted to a Number.
|
||||
'initSegment' will become NaN.
|
||||
NaN, which when converted through ToInt32() -> +0.
|
||||
---
|
||||
Explicitly set sn to resulting value from implicit conversions 'initSegment' values for IV generation.
|
||||
*/
|
||||
sn = 0;
|
||||
}
|
||||
this._decryptdata = this.setDecryptDataFromLevelKey(this.levelkey, sn);
|
||||
}
|
||||
|
||||
return this._decryptdata;
|
||||
|
@ -205,48 +199,31 @@ export class Fragment extends BaseSegment {
|
|||
// 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?.keyFormat && this.decryptdata.uri) {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method for parseLevelPlaylist to create an initialization vector for a given segment
|
||||
* @param {number} segmentNumber - segment number to generate IV with
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
createInitializationVector(segmentNumber: number): Uint8Array {
|
||||
const uint8View = new Uint8Array(16);
|
||||
|
||||
for (let i = 12; i < 16; i++) {
|
||||
uint8View[i] = (segmentNumber >> (8 * (15 - i))) & 0xff;
|
||||
setKeyFormat(keyFormat: KeySystemFormats) {
|
||||
if (this.levelkeys) {
|
||||
const key = this.levelkeys[keyFormat];
|
||||
if (key && !this._decryptdata) {
|
||||
this._decryptdata = key.getDecryptData(this.sn);
|
||||
}
|
||||
}
|
||||
|
||||
return uint8View;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method for parseLevelPlaylist to get a fragment's decryption data from the currently parsed encryption key data
|
||||
* @param levelkey - a playlist's encryption info
|
||||
* @param segmentNumber - the fragment's segment number
|
||||
* @returns {LevelKey} - an object to be applied as a fragment's decryptdata
|
||||
*/
|
||||
setDecryptDataFromLevelKey(
|
||||
levelkey: LevelKey,
|
||||
segmentNumber: number
|
||||
): LevelKey {
|
||||
let decryptdata = levelkey;
|
||||
|
||||
if (levelkey?.method === 'AES-128' && levelkey.uri && !levelkey.iv) {
|
||||
decryptdata = LevelKey.fromURI(levelkey.uri);
|
||||
decryptdata.method = levelkey.method;
|
||||
decryptdata.iv = this.createInitializationVector(segmentNumber);
|
||||
decryptdata.keyFormat = 'identity';
|
||||
}
|
||||
|
||||
return decryptdata;
|
||||
abortRequests(): void {
|
||||
this.loader?.abort();
|
||||
this.keyLoader?.abort();
|
||||
}
|
||||
|
||||
setElementaryStreamInfo(
|
||||
|
|
409
node_modules/hls.js/src/loader/key-loader.ts
generated
vendored
409
node_modules/hls.js/src/loader/key-loader.ts
generated
vendored
|
@ -1,101 +1,235 @@
|
|||
/*
|
||||
* Decrypt key Loader
|
||||
*/
|
||||
import { Events } from '../events';
|
||||
import { ErrorTypes, ErrorDetails } from '../errors';
|
||||
import { logger } from '../utils/logger';
|
||||
import type Hls from '../hls';
|
||||
import { Fragment } from './fragment';
|
||||
import {
|
||||
LoaderStats,
|
||||
LoaderResponse,
|
||||
LoaderContext,
|
||||
LoaderConfiguration,
|
||||
LoaderCallbacks,
|
||||
Loader,
|
||||
FragmentLoaderContext,
|
||||
KeyLoaderContext,
|
||||
} from '../types/loader';
|
||||
import type { NetworkComponentAPI } from '../types/component-api';
|
||||
import type { KeyLoadingData } from '../types/events';
|
||||
import { LoadError } from './fragment-loader';
|
||||
import type { HlsConfig } from '../hls';
|
||||
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';
|
||||
|
||||
interface KeyLoaderContext extends LoaderContext {
|
||||
frag: Fragment;
|
||||
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;
|
||||
|
||||
export default class KeyLoader implements NetworkComponentAPI {
|
||||
private hls: Hls;
|
||||
public loaders = {};
|
||||
public decryptkey: Uint8Array | null = null;
|
||||
public decrypturl: string | null = null;
|
||||
|
||||
constructor(hls: Hls) {
|
||||
this.hls = hls;
|
||||
|
||||
this.registerListeners();
|
||||
constructor(config: HlsConfig) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
public startLoad(startPosition: number): void {}
|
||||
|
||||
public stopLoad(): void {
|
||||
this.destroyInternalLoaders();
|
||||
abort() {
|
||||
for (const uri in this.keyUriToKeyInfo) {
|
||||
const loader = this.keyUriToKeyInfo[uri].loader;
|
||||
if (loader) {
|
||||
loader.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private registerListeners() {
|
||||
this.hls.on(Events.KEY_LOADING, this.onKeyLoading, this);
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private unregisterListeners() {
|
||||
this.hls.off(Events.KEY_LOADING, this.onKeyLoading);
|
||||
}
|
||||
|
||||
private destroyInternalLoaders(): void {
|
||||
for (const loaderName in this.loaders) {
|
||||
const loader = this.loaders[loaderName];
|
||||
destroy() {
|
||||
this.detach();
|
||||
for (const uri in this.keyUriToKeyInfo) {
|
||||
const loader = this.keyUriToKeyInfo[uri].loader;
|
||||
if (loader) {
|
||||
loader.destroy();
|
||||
}
|
||||
}
|
||||
this.loaders = {};
|
||||
this.keyUriToKeyInfo = {};
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.unregisterListeners();
|
||||
this.destroyInternalLoaders();
|
||||
createKeyLoadError(
|
||||
frag: Fragment,
|
||||
details: ErrorDetails = ErrorDetails.KEY_LOAD_ERROR,
|
||||
networkDetails?: any,
|
||||
message?: string
|
||||
): LoadError {
|
||||
return new LoadError({
|
||||
type: ErrorTypes.NETWORK_ERROR,
|
||||
details,
|
||||
fatal: false,
|
||||
frag,
|
||||
networkDetails,
|
||||
});
|
||||
}
|
||||
|
||||
onKeyLoading(event: Events.KEY_LOADING, data: KeyLoadingData) {
|
||||
const { frag } = data;
|
||||
const type = frag.type;
|
||||
const loader = this.loaders[type];
|
||||
if (!frag.decryptdata) {
|
||||
logger.warn('Missing decryption data on fragment in onKeyLoading');
|
||||
return;
|
||||
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' || 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);
|
||||
});
|
||||
}
|
||||
|
||||
// Load the key if the uri is different from previous one, or if the decrypt key has not yet been retrieved
|
||||
const uri = frag.decryptdata.uri;
|
||||
if (uri !== this.decrypturl || this.decryptkey === null) {
|
||||
const config = this.hls.config;
|
||||
if (loader) {
|
||||
logger.warn(`abort previous key loader for type:${type}`);
|
||||
loader.abort();
|
||||
}
|
||||
if (!uri) {
|
||||
logger.warn('key uri is falsy');
|
||||
return;
|
||||
}
|
||||
const Loader = config.loader;
|
||||
const fragLoader =
|
||||
(frag.loader =
|
||||
this.loaders[type] =
|
||||
new Loader(config) as Loader<FragmentLoaderContext>);
|
||||
this.decrypturl = uri;
|
||||
this.decryptkey = null;
|
||||
return this.loadInternal(frag);
|
||||
}
|
||||
|
||||
loadInternal(
|
||||
frag: Fragment,
|
||||
keySystemFormat?: KeySystemFormats
|
||||
): Promise<KeyLoadedData> {
|
||||
if (keySystemFormat) {
|
||||
frag.setKeyFormat(keySystemFormat);
|
||||
}
|
||||
const decryptdata = frag.decryptdata;
|
||||
if (!decryptdata) {
|
||||
const errorMessage = 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,
|
||||
null,
|
||||
errorMessage
|
||||
)
|
||||
);
|
||||
}
|
||||
const uri = decryptdata.uri;
|
||||
if (!uri) {
|
||||
return Promise.reject(
|
||||
this.createKeyLoadError(
|
||||
frag,
|
||||
ErrorDetails.KEY_LOAD_ERROR,
|
||||
null,
|
||||
`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;
|
||||
}
|
||||
// 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,
|
||||
null,
|
||||
`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 = {
|
||||
url: uri,
|
||||
frag: frag,
|
||||
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,
|
||||
|
@ -110,69 +244,94 @@ export default class KeyLoader implements NetworkComponentAPI {
|
|||
};
|
||||
|
||||
const loaderCallbacks: LoaderCallbacks<KeyLoaderContext> = {
|
||||
onSuccess: this.loadsuccess.bind(this),
|
||||
onError: this.loaderror.bind(this),
|
||||
onTimeout: this.loadtimeout.bind(this),
|
||||
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,
|
||||
networkDetails,
|
||||
'after key load, decryptdata unset or changed'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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: (
|
||||
error: { code: number; text: string },
|
||||
context: KeyLoaderContext,
|
||||
networkDetails: any
|
||||
) => {
|
||||
this.resetLoader(context);
|
||||
reject(
|
||||
this.createKeyLoadError(
|
||||
frag,
|
||||
ErrorDetails.KEY_LOAD_ERROR,
|
||||
networkDetails
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
onTimeout: (
|
||||
stats: LoaderStats,
|
||||
context: KeyLoaderContext,
|
||||
networkDetails: any
|
||||
) => {
|
||||
this.resetLoader(context);
|
||||
reject(
|
||||
this.createKeyLoadError(
|
||||
frag,
|
||||
ErrorDetails.KEY_LOAD_TIMEOUT,
|
||||
networkDetails
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
onAbort: (
|
||||
stats: LoaderStats,
|
||||
context: KeyLoaderContext,
|
||||
networkDetails: any
|
||||
) => {
|
||||
this.resetLoader(context);
|
||||
reject(
|
||||
this.createKeyLoadError(
|
||||
frag,
|
||||
ErrorDetails.INTERNAL_ABORTED,
|
||||
networkDetails
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
fragLoader.load(loaderContext, loaderConfig, loaderCallbacks);
|
||||
} else if (this.decryptkey) {
|
||||
// Return the key if it's already been loaded
|
||||
frag.decryptdata.key = this.decryptkey;
|
||||
this.hls.trigger(Events.KEY_LOADED, { frag: frag });
|
||||
}
|
||||
keyLoader.load(loaderContext, loaderConfig, loaderCallbacks);
|
||||
}));
|
||||
}
|
||||
|
||||
loadsuccess(
|
||||
response: LoaderResponse,
|
||||
stats: LoaderStats,
|
||||
context: KeyLoaderContext
|
||||
) {
|
||||
const frag = context.frag;
|
||||
if (!frag.decryptdata) {
|
||||
logger.error('after key load, decryptdata unset');
|
||||
return;
|
||||
private resetLoader(context: KeyLoaderContext) {
|
||||
const { frag, keyInfo, url: uri } = context;
|
||||
const loader = keyInfo.loader;
|
||||
if (frag.keyLoader === loader) {
|
||||
frag.keyLoader = null;
|
||||
keyInfo.loader = null;
|
||||
}
|
||||
this.decryptkey = frag.decryptdata.key = new Uint8Array(
|
||||
response.data as ArrayBuffer
|
||||
);
|
||||
|
||||
// detach fragment loader on load success
|
||||
frag.loader = null;
|
||||
delete this.loaders[frag.type];
|
||||
this.hls.trigger(Events.KEY_LOADED, { frag: frag });
|
||||
}
|
||||
|
||||
loaderror(response: LoaderResponse, context: KeyLoaderContext) {
|
||||
const frag = context.frag;
|
||||
const loader = frag.loader;
|
||||
delete this.keyUriToKeyInfo[uri];
|
||||
if (loader) {
|
||||
loader.abort();
|
||||
loader.destroy();
|
||||
}
|
||||
|
||||
delete this.loaders[frag.type];
|
||||
this.hls.trigger(Events.ERROR, {
|
||||
type: ErrorTypes.NETWORK_ERROR,
|
||||
details: ErrorDetails.KEY_LOAD_ERROR,
|
||||
fatal: false,
|
||||
frag,
|
||||
response,
|
||||
});
|
||||
}
|
||||
|
||||
loadtimeout(stats: LoaderStats, context: KeyLoaderContext) {
|
||||
const frag = context.frag;
|
||||
const loader = frag.loader;
|
||||
if (loader) {
|
||||
loader.abort();
|
||||
}
|
||||
|
||||
delete this.loaders[frag.type];
|
||||
this.hls.trigger(Events.ERROR, {
|
||||
type: ErrorTypes.NETWORK_ERROR,
|
||||
details: ErrorDetails.KEY_LOAD_TIMEOUT,
|
||||
fatal: false,
|
||||
frag,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
3
node_modules/hls.js/src/loader/level-details.ts
generated
vendored
3
node_modules/hls.js/src/loader/level-details.ts
generated
vendored
|
@ -22,7 +22,6 @@ export class LevelDetails {
|
|||
public advanced: boolean = true;
|
||||
public availabilityDelay?: number; // Manifest reload synchronization
|
||||
public misses: number = 0;
|
||||
public needSidxRanges: boolean = false;
|
||||
public startCC: number = 0;
|
||||
public startSN: number = 0;
|
||||
public startTimeOffset: number | null = null;
|
||||
|
@ -48,9 +47,11 @@ export class LevelDetails {
|
|||
public driftEndTime: number = 0;
|
||||
public driftStart: number = 0;
|
||||
public driftEnd: number = 0;
|
||||
public encryptedFragments: Fragment[];
|
||||
|
||||
constructor(baseUrl) {
|
||||
this.fragments = [];
|
||||
this.encryptedFragments = [];
|
||||
this.dateRanges = {};
|
||||
this.url = baseUrl;
|
||||
}
|
||||
|
|
213
node_modules/hls.js/src/loader/level-key.ts
generated
vendored
213
node_modules/hls.js/src/loader/level-key.ts
generated
vendored
|
@ -1,33 +1,204 @@
|
|||
import { buildAbsoluteURL } from 'url-toolkit';
|
||||
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';
|
||||
|
||||
export class LevelKey {
|
||||
private _uri: string | null = null;
|
||||
public method: string | null = null;
|
||||
public keyFormat: string | null = null;
|
||||
public keyFormatVersions: string | null = null;
|
||||
public keyID: string | null = null;
|
||||
public key: Uint8Array | null = null;
|
||||
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 fromURL(baseUrl: string, relativeUrl: string): LevelKey {
|
||||
return new LevelKey(baseUrl, relativeUrl);
|
||||
static clearKeyUriToKeyIdMap() {
|
||||
keyUriToKeyIdMap = {};
|
||||
}
|
||||
|
||||
static fromURI(uri: string): LevelKey {
|
||||
return new LevelKey(uri);
|
||||
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';
|
||||
}
|
||||
|
||||
private constructor(absoluteOrBaseURI: string, relativeURL?: string) {
|
||||
if (relativeURL) {
|
||||
this._uri = buildAbsoluteURL(absoluteOrBaseURI, relativeURL, {
|
||||
alwaysNormalize: true,
|
||||
});
|
||||
} else {
|
||||
this._uri = absoluteOrBaseURI;
|
||||
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;
|
||||
}
|
||||
switch (this.keyFormat) {
|
||||
case 'identity':
|
||||
// Maintain support for clear SAMPLE-AES with MPEG-3 TS
|
||||
return this.method === 'SAMPLE-AES';
|
||||
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;
|
||||
}
|
||||
|
||||
get uri() {
|
||||
return this._uri;
|
||||
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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
|
220
node_modules/hls.js/src/loader/m3u8-parser.ts
generated
vendored
220
node_modules/hls.js/src/loader/m3u8-parser.ts
generated
vendored
|
@ -1,10 +1,8 @@
|
|||
import * as URLToolkit from 'url-toolkit';
|
||||
|
||||
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 type { CodecType } from '../utils/codecs';
|
||||
|
@ -19,9 +17,15 @@ import type { LevelAttributes, LevelParsed } from '../types/level';
|
|||
|
||||
type M3U8ParserFragments = Array<Fragment | null>;
|
||||
|
||||
type ParsedMultiVariantPlaylist = {
|
||||
levels: LevelParsed[];
|
||||
sessionData: Record<string, AttrList> | null;
|
||||
sessionKeys: LevelKey[] | null;
|
||||
};
|
||||
|
||||
// https://regex101.com is your friend
|
||||
const MASTER_PLAYLIST_REGEX =
|
||||
/#EXT-X-STREAM-INF:([^\r\n]*)(?:[\r\n](?:#[^\r\n]*)?)*([^\r\n]+)|#EXT-X-SESSION-DATA:([^\r\n]*)[\r\n]+/g;
|
||||
/#EXT-X-STREAM-INF:([^\r\n]*)(?:[\r\n](?:#[^\r\n]*)?)*([^\r\n]+)|#EXT-X-SESSION-DATA:([^\r\n]*)[\r\n]+|#EXT-X-SESSION-KEY:([^\n\r]*)[\r\n]+/g;
|
||||
const MASTER_PLAYLIST_MEDIA_REGEX = /#EXT-X-MEDIA:(.*)/g;
|
||||
|
||||
const LEVEL_PLAYLIST_REGEX_FAST = new RegExp(
|
||||
|
@ -48,12 +52,6 @@ const LEVEL_PLAYLIST_REGEX_SLOW = new RegExp(
|
|||
].join('|')
|
||||
);
|
||||
|
||||
const MP4_REGEX_SUFFIX = /\.(mp4|m4s|m4v|m4a)$/i;
|
||||
|
||||
function isMP4Url(url: string): boolean {
|
||||
return MP4_REGEX_SUFFIX.test(URLToolkit.parseURL(url)?.path ?? '');
|
||||
}
|
||||
|
||||
export default class M3U8Parser {
|
||||
static findGroup(
|
||||
groups: Array<AudioGroup>,
|
||||
|
@ -80,12 +78,18 @@ export default class M3U8Parser {
|
|||
}
|
||||
|
||||
static resolve(url, baseUrl) {
|
||||
return URLToolkit.buildAbsoluteURL(baseUrl, url, { alwaysNormalize: true });
|
||||
return buildAbsoluteURL(baseUrl, url, { alwaysNormalize: true });
|
||||
}
|
||||
|
||||
static parseMasterPlaylist(string: string, baseurl: string) {
|
||||
const levels: Array<LevelParsed> = [];
|
||||
static parseMasterPlaylist(
|
||||
string: string,
|
||||
baseurl: string
|
||||
): ParsedMultiVariantPlaylist {
|
||||
const levels: LevelParsed[] = [];
|
||||
const levelsWithKnownCodecs: LevelParsed[] = [];
|
||||
const sessionData: Record<string, AttrList> = {};
|
||||
const sessionKeys: LevelKey[] = [];
|
||||
|
||||
let hasSessionData = false;
|
||||
MASTER_PLAYLIST_REGEX.lastIndex = 0;
|
||||
|
||||
|
@ -118,6 +122,10 @@ export default class M3U8Parser {
|
|||
level.videoCodec = M3U8Parser.convertAVC1ToAVCOTI(level.videoCodec);
|
||||
}
|
||||
|
||||
if (!level.unknownCodecs?.length) {
|
||||
levelsWithKnownCodecs.push(level);
|
||||
}
|
||||
|
||||
levels.push(level);
|
||||
} else if (result[3]) {
|
||||
// '#EXT-X-SESSION-DATA' is found, parse session data in group 3
|
||||
|
@ -126,11 +134,28 @@ export default class M3U8Parser {
|
|||
hasSessionData = true;
|
||||
sessionData[sessionAttrs['DATA-ID']] = sessionAttrs;
|
||||
}
|
||||
} else if (result[4]) {
|
||||
// '#EXT-X-SESSION-KEY' is found
|
||||
const keyTag = result[4];
|
||||
const sessionKey = parseKey(keyTag, baseurl);
|
||||
if (sessionKey.encrypted && sessionKey.isSupported()) {
|
||||
sessionKeys.push(sessionKey);
|
||||
} else {
|
||||
logger.warn(
|
||||
`[Keys] Ignoring invalid EXT-X-SESSION-KEY tag: "${keyTag}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Filter out levels with unknown codecs if it does not remove all levels
|
||||
const stripUnknownCodecLevels =
|
||||
levelsWithKnownCodecs.length > 0 &&
|
||||
levelsWithKnownCodecs.length < levels.length;
|
||||
|
||||
return {
|
||||
levels,
|
||||
levels: stripUnknownCodecLevels ? levelsWithKnownCodecs : levels,
|
||||
sessionData: hasSessionData ? sessionData : null,
|
||||
sessionKeys: sessionKeys.length ? sessionKeys : null,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -197,7 +222,7 @@ export default class M3U8Parser {
|
|||
let frag: Fragment = new Fragment(type, baseurl);
|
||||
let result: RegExpExecArray | RegExpMatchArray | null;
|
||||
let i: number;
|
||||
let levelkey: LevelKey | undefined;
|
||||
let levelkeys: { [key: string]: LevelKey } | undefined;
|
||||
let firstPdtIndex = -1;
|
||||
let createNextFrag = false;
|
||||
|
||||
|
@ -232,8 +257,8 @@ export default class M3U8Parser {
|
|||
// url
|
||||
if (Number.isFinite(frag.duration)) {
|
||||
frag.start = totalduration;
|
||||
if (levelkey) {
|
||||
frag.levelkey = levelkey;
|
||||
if (levelkeys) {
|
||||
setFragLevelKeys(frag, levelkeys, level);
|
||||
}
|
||||
frag.sn = currentSN;
|
||||
frag.level = id;
|
||||
|
@ -355,66 +380,21 @@ export default class M3U8Parser {
|
|||
discontinuityCounter = parseInt(value1);
|
||||
break;
|
||||
case 'KEY': {
|
||||
// https://tools.ietf.org/html/rfc8216#section-4.3.2.4
|
||||
const keyAttrs = new AttrList(value1);
|
||||
const decryptmethod = keyAttrs.enumeratedString('METHOD');
|
||||
const decrypturi = keyAttrs.URI;
|
||||
const decryptiv = keyAttrs.hexadecimalInteger('IV');
|
||||
const decryptkeyformatversions =
|
||||
keyAttrs.enumeratedString('KEYFORMATVERSIONS');
|
||||
const decryptkeyid = keyAttrs.enumeratedString('KEYID');
|
||||
// From RFC: This attribute is OPTIONAL; its absence indicates an implicit value of "identity".
|
||||
const decryptkeyformat =
|
||||
keyAttrs.enumeratedString('KEYFORMAT') ?? 'identity';
|
||||
|
||||
const unsupportedKnownKeyformatsInManifest = [
|
||||
'com.apple.streamingkeydelivery',
|
||||
'com.microsoft.playready',
|
||||
'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', // widevine (v2)
|
||||
'com.widevine', // earlier widevine (v1)
|
||||
];
|
||||
|
||||
if (
|
||||
unsupportedKnownKeyformatsInManifest.indexOf(decryptkeyformat) >
|
||||
-1
|
||||
) {
|
||||
logger.warn(
|
||||
`Keyformat ${decryptkeyformat} is not supported from the manifest`
|
||||
);
|
||||
continue;
|
||||
} else if (decryptkeyformat !== 'identity') {
|
||||
// We are supposed to skip keys we don't understand.
|
||||
// As we currently only officially support identity keys
|
||||
// from the manifest we shouldn't save any other key.
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: multiple keys can be defined on a fragment, and we need to support this
|
||||
// for clients that support both playready and widevine
|
||||
if (decryptmethod) {
|
||||
// TODO: need to determine if the level key is actually a relative URL
|
||||
// if it isn't, then we should instead construct the LevelKey using fromURI.
|
||||
levelkey = LevelKey.fromURL(baseurl, decrypturi);
|
||||
if (
|
||||
decrypturi &&
|
||||
['AES-128', 'SAMPLE-AES', 'SAMPLE-AES-CENC'].indexOf(
|
||||
decryptmethod
|
||||
) >= 0
|
||||
) {
|
||||
levelkey.method = decryptmethod;
|
||||
levelkey.keyFormat = decryptkeyformat;
|
||||
|
||||
if (decryptkeyid) {
|
||||
levelkey.keyID = decryptkeyid;
|
||||
}
|
||||
|
||||
if (decryptkeyformatversions) {
|
||||
levelkey.keyFormatVersions = decryptkeyformatversions;
|
||||
}
|
||||
|
||||
// Initialization Vector (IV)
|
||||
levelkey.iv = decryptiv;
|
||||
const levelKey = parseKey(value1, baseurl);
|
||||
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;
|
||||
}
|
||||
|
@ -435,7 +415,7 @@ export default class M3U8Parser {
|
|||
// #EXTINF: 6.0
|
||||
// #EXT-X-MAP:URI="init.mp4
|
||||
const init = new Fragment(type, baseurl);
|
||||
setInitSegment(init, mapAttrs, id, levelkey);
|
||||
setInitSegment(init, mapAttrs, id, levelkeys);
|
||||
currentInitSegment = init;
|
||||
frag.initSegment = currentInitSegment;
|
||||
if (
|
||||
|
@ -446,7 +426,7 @@ export default class M3U8Parser {
|
|||
}
|
||||
} else {
|
||||
// Initial segment tag is before segment duration tag
|
||||
setInitSegment(frag, mapAttrs, id, levelkey);
|
||||
setInitSegment(frag, mapAttrs, id, levelkeys);
|
||||
currentInitSegment = frag;
|
||||
createNextFrag = true;
|
||||
}
|
||||
|
@ -520,6 +500,9 @@ export default class M3U8Parser {
|
|||
assignProgramDateTime(frag, prevFrag);
|
||||
frag.cc = discontinuityCounter;
|
||||
level.fragmentHint = frag;
|
||||
if (levelkeys) {
|
||||
setFragLevelKeys(frag, levelkeys, level);
|
||||
}
|
||||
}
|
||||
const fragmentLength = fragments.length;
|
||||
const firstFragment = fragments[0];
|
||||
|
@ -529,28 +512,11 @@ export default class M3U8Parser {
|
|||
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;
|
||||
if (!firstFragment.initSegment) {
|
||||
// this is a bit lurky but HLS really has no other way to tell us
|
||||
// if the fragments are TS or MP4, except if we download them :/
|
||||
// but this is to be able to handle SIDX.
|
||||
if (
|
||||
level.fragments.every(
|
||||
(frag) => frag.relurl && isMP4Url(frag.relurl)
|
||||
)
|
||||
) {
|
||||
logger.warn(
|
||||
'MP4 fragments found but no init segment (probably no MAP, incomplete M3U8), trying to fetch SIDX'
|
||||
);
|
||||
frag = new Fragment(type, baseurl);
|
||||
frag.relurl = lastFragment.relurl;
|
||||
frag.level = id;
|
||||
frag.sn = 'initSegment';
|
||||
firstFragment.initSegment = frag;
|
||||
level.needSidxRanges = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
level.endSN = 0;
|
||||
|
@ -579,6 +545,39 @@ export default class M3U8Parser {
|
|||
}
|
||||
}
|
||||
|
||||
function parseKey(keyTag: string, baseurl: string): LevelKey {
|
||||
// https://tools.ietf.org/html/rfc8216#section-4.3.2.4
|
||||
const keyAttrs = new AttrList(keyTag);
|
||||
const decryptmethod = keyAttrs.enumeratedString('METHOD') ?? '';
|
||||
const decrypturi = keyAttrs.URI;
|
||||
const decryptiv = keyAttrs.hexadecimalInteger('IV');
|
||||
const decryptkeyformatversions =
|
||||
keyAttrs.enumeratedString('KEYFORMATVERSIONS');
|
||||
// From RFC: This attribute is OPTIONAL; its absence indicates an implicit value of "identity".
|
||||
const decryptkeyformat = keyAttrs.enumeratedString('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 setCodecs(codecs: Array<string>, level: LevelParsed) {
|
||||
['video', 'audio', 'text'].forEach((type: CodecType) => {
|
||||
const filtered = codecs.filter((codec) => isCodecType(codec, type));
|
||||
|
@ -640,7 +639,7 @@ function setInitSegment(
|
|||
frag: Fragment,
|
||||
mapAttrs: AttrList,
|
||||
id: number,
|
||||
levelkey: LevelKey | undefined
|
||||
levelkeys: { [key: string]: LevelKey } | undefined
|
||||
) {
|
||||
frag.relurl = mapAttrs.URI;
|
||||
if (mapAttrs.BYTERANGE) {
|
||||
|
@ -648,8 +647,27 @@ function setInitSegment(
|
|||
}
|
||||
frag.level = id;
|
||||
frag.sn = 'initSegment';
|
||||
if (levelkey) {
|
||||
frag.levelkey = levelkey;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
70
node_modules/hls.js/src/loader/playlist-loader.ts
generated
vendored
70
node_modules/hls.js/src/loader/playlist-loader.ts
generated
vendored
|
@ -12,7 +12,6 @@
|
|||
import { Events } from '../events';
|
||||
import { ErrorDetails, ErrorTypes } from '../errors';
|
||||
import { logger } from '../utils/logger';
|
||||
import { parseSegmentIndex, findBox } from '../utils/mp4-tools';
|
||||
import M3U8Parser from './m3u8-parser';
|
||||
import type { LevelParsed } from '../types/level';
|
||||
import type {
|
||||
|
@ -316,12 +315,6 @@ class PlaylistLoader implements NetworkComponentAPI {
|
|||
context: PlaylistLoaderContext,
|
||||
networkDetails: any = null
|
||||
): void {
|
||||
if (context.isSidxRequest) {
|
||||
this.handleSidxRequest(response, context);
|
||||
this.handlePlaylistLoaded(response, stats, context, networkDetails);
|
||||
return;
|
||||
}
|
||||
|
||||
this.resetInternalLoader(context.type);
|
||||
|
||||
const string = response.data as string;
|
||||
|
@ -376,7 +369,10 @@ class PlaylistLoader implements NetworkComponentAPI {
|
|||
|
||||
const url = getResponseUrl(response, context);
|
||||
|
||||
const { levels, sessionData } = M3U8Parser.parseMasterPlaylist(string, url);
|
||||
const { levels, sessionData, sessionKeys } = M3U8Parser.parseMasterPlaylist(
|
||||
string,
|
||||
url
|
||||
);
|
||||
if (!levels.length) {
|
||||
this.handleManifestParsingError(
|
||||
response,
|
||||
|
@ -457,6 +453,7 @@ class PlaylistLoader implements NetworkComponentAPI {
|
|||
stats,
|
||||
networkDetails,
|
||||
sessionData,
|
||||
sessionKeys,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -513,74 +510,19 @@ class PlaylistLoader implements NetworkComponentAPI {
|
|||
stats,
|
||||
networkDetails,
|
||||
sessionData: null,
|
||||
sessionKeys: null,
|
||||
});
|
||||
}
|
||||
|
||||
// save parsing time
|
||||
stats.parsing.end = performance.now();
|
||||
|
||||
// in case we need SIDX ranges
|
||||
// return early after calling load for
|
||||
// the SIDX box.
|
||||
if (levelDetails.needSidxRanges) {
|
||||
const sidxUrl = levelDetails.fragments[0].initSegment?.url as string;
|
||||
this.load({
|
||||
url: sidxUrl,
|
||||
isSidxRequest: true,
|
||||
type,
|
||||
level,
|
||||
levelDetails,
|
||||
id,
|
||||
groupId: null,
|
||||
rangeStart: 0,
|
||||
rangeEnd: 2048,
|
||||
responseType: 'arraybuffer',
|
||||
deliveryDirectives: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// extend the context with the new levelDetails property
|
||||
context.levelDetails = levelDetails;
|
||||
|
||||
this.handlePlaylistLoaded(response, stats, context, networkDetails);
|
||||
}
|
||||
|
||||
private handleSidxRequest(
|
||||
response: LoaderResponse,
|
||||
context: PlaylistLoaderContext
|
||||
): void {
|
||||
const data = new Uint8Array(response.data as ArrayBuffer);
|
||||
const sidxBox = findBox(data, ['sidx'])[0];
|
||||
// if provided fragment does not contain sidx, early return
|
||||
if (!sidxBox) {
|
||||
return;
|
||||
}
|
||||
const sidxInfo = parseSegmentIndex(sidxBox);
|
||||
if (!sidxInfo) {
|
||||
return;
|
||||
}
|
||||
const sidxReferences = sidxInfo.references;
|
||||
const levelDetails = context.levelDetails as LevelDetails;
|
||||
sidxReferences.forEach((segmentRef, index) => {
|
||||
const segRefInfo = segmentRef.info;
|
||||
const frag = levelDetails.fragments[index];
|
||||
|
||||
if (frag.byteRange.length === 0) {
|
||||
frag.setByteRange(
|
||||
String(1 + segRefInfo.end - segRefInfo.start) +
|
||||
'@' +
|
||||
String(segRefInfo.start)
|
||||
);
|
||||
}
|
||||
if (frag.initSegment) {
|
||||
const moovBox = findBox(data, ['moov'])[0];
|
||||
const moovEndOffset = moovBox ? moovBox.length : null;
|
||||
frag.initSegment.setByteRange(String(moovEndOffset) + '@0');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private handleManifestParsingError(
|
||||
response: LoaderResponse,
|
||||
context: PlaylistLoaderContext,
|
||||
|
|
28
node_modules/hls.js/src/remux/mp4-remuxer.ts
generated
vendored
28
node_modules/hls.js/src/remux/mp4-remuxer.ts
generated
vendored
|
@ -452,19 +452,21 @@ export default class MP4Remuxer implements Remuxer {
|
|||
)} ms (${delta}dts) overlapping between fragments detected`
|
||||
);
|
||||
}
|
||||
firstDTS = nextAvcDts;
|
||||
const firstPTS = inputSamples[0].pts - delta;
|
||||
inputSamples[0].dts = firstDTS;
|
||||
inputSamples[0].pts = firstPTS;
|
||||
logger.log(
|
||||
`Video: First PTS/DTS adjusted: ${toMsFromMpegTsClock(
|
||||
firstPTS,
|
||||
true
|
||||
)}/${toMsFromMpegTsClock(
|
||||
firstDTS,
|
||||
true
|
||||
)}, delta: ${toMsFromMpegTsClock(delta, true)} ms`
|
||||
);
|
||||
if (!foundOverlap || nextAvcDts > inputSamples[0].pts) {
|
||||
firstDTS = nextAvcDts;
|
||||
const firstPTS = inputSamples[0].pts - delta;
|
||||
inputSamples[0].dts = firstDTS;
|
||||
inputSamples[0].pts = firstPTS;
|
||||
logger.log(
|
||||
`Video: First PTS/DTS adjusted: ${toMsFromMpegTsClock(
|
||||
firstPTS,
|
||||
true
|
||||
)}/${toMsFromMpegTsClock(
|
||||
firstDTS,
|
||||
true
|
||||
)}, delta: ${toMsFromMpegTsClock(delta, true)} ms`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
12
node_modules/hls.js/src/remux/passthrough-remuxer.ts
generated
vendored
12
node_modules/hls.js/src/remux/passthrough-remuxer.ts
generated
vendored
|
@ -2,7 +2,11 @@ import {
|
|||
flushTextTrackMetadataCueSamples,
|
||||
flushTextTrackUserdataCueSamples,
|
||||
} from './mp4-remuxer';
|
||||
import type { InitData, InitDataTrack } from '../utils/mp4-tools';
|
||||
import {
|
||||
InitData,
|
||||
InitDataTrack,
|
||||
patchEncyptionData,
|
||||
} from '../utils/mp4-tools';
|
||||
import {
|
||||
getDuration,
|
||||
getStartDTS,
|
||||
|
@ -24,6 +28,7 @@ import type {
|
|||
DemuxedUserdataTrack,
|
||||
PassthroughTrack,
|
||||
} from '../types/demuxer';
|
||||
import type { DecryptData } from '../loader/level-key';
|
||||
|
||||
class PassThroughRemuxer implements Remuxer {
|
||||
private emitInitSegment: boolean = false;
|
||||
|
@ -48,11 +53,12 @@ class PassThroughRemuxer implements Remuxer {
|
|||
public resetInitSegment(
|
||||
initSegment: Uint8Array | undefined,
|
||||
audioCodec: string | undefined,
|
||||
videoCodec: string | undefined
|
||||
videoCodec: string | undefined,
|
||||
decryptdata: DecryptData | null
|
||||
) {
|
||||
this.audioCodec = audioCodec;
|
||||
this.videoCodec = videoCodec;
|
||||
this.generateInitSegment(initSegment);
|
||||
this.generateInitSegment(patchEncyptionData(initSegment, decryptdata));
|
||||
this.emitInitSegment = true;
|
||||
}
|
||||
|
||||
|
|
1
node_modules/hls.js/src/types/buffer.ts
generated
vendored
1
node_modules/hls.js/src/types/buffer.ts
generated
vendored
|
@ -3,6 +3,7 @@ export type SourceBufferName = 'video' | 'audio' | 'audiovideo';
|
|||
// eslint-disable-next-line no-restricted-globals
|
||||
export type ExtendedSourceBuffer = SourceBuffer & {
|
||||
ended?: boolean;
|
||||
ending?: boolean;
|
||||
changeType?: (type: string) => void;
|
||||
};
|
||||
|
||||
|
|
1
node_modules/hls.js/src/types/demuxer.ts
generated
vendored
1
node_modules/hls.js/src/types/demuxer.ts
generated
vendored
|
@ -96,6 +96,7 @@ export enum MetadataSchema {
|
|||
export interface MetadataSample {
|
||||
pts: number;
|
||||
dts: number;
|
||||
duration: number;
|
||||
len?: number;
|
||||
data: Uint8Array;
|
||||
type: MetadataSchema;
|
||||
|
|
7
node_modules/hls.js/src/types/events.ts
generated
vendored
7
node_modules/hls.js/src/types/events.ts
generated
vendored
|
@ -21,6 +21,8 @@ import type { ErrorDetails, ErrorTypes } from '../errors';
|
|||
import type { MetadataSample, UserdataSample } from './demuxer';
|
||||
import type { AttrList } from '../utils/attr-list';
|
||||
import type { HlsListeners } from '../events';
|
||||
import { KeyLoaderInfo } from '../loader/key-loader';
|
||||
import { LevelKey } from '../loader/level-key';
|
||||
|
||||
export interface MediaAttachingData {
|
||||
media: HTMLMediaElement;
|
||||
|
@ -82,6 +84,7 @@ export interface ManifestLoadedData {
|
|||
levels: LevelParsed[];
|
||||
networkDetails: any;
|
||||
sessionData: Record<string, AttrList> | null;
|
||||
sessionKeys: LevelKey[] | null;
|
||||
stats: LoaderStats;
|
||||
subtitles?: MediaPlaylist[];
|
||||
url: string;
|
||||
|
@ -91,6 +94,8 @@ export interface ManifestParsedData {
|
|||
levels: Level[];
|
||||
audioTracks: MediaPlaylist[];
|
||||
subtitleTracks: MediaPlaylist[];
|
||||
sessionData: Record<string, AttrList> | null;
|
||||
sessionKeys: LevelKey[] | null;
|
||||
firstLevel: number;
|
||||
stats: LoaderStats;
|
||||
audio: boolean;
|
||||
|
@ -215,6 +220,7 @@ export interface ErrorData {
|
|||
fatal: boolean;
|
||||
buffer?: number;
|
||||
bytes?: number;
|
||||
chunkMeta?: ChunkMetadata;
|
||||
context?: PlaylistLoaderContext;
|
||||
error?: Error;
|
||||
event?: keyof HlsListeners | 'demuxerWorker';
|
||||
|
@ -338,6 +344,7 @@ export interface KeyLoadingData {
|
|||
|
||||
export interface KeyLoadedData {
|
||||
frag: Fragment;
|
||||
keyInfo: KeyLoaderInfo;
|
||||
}
|
||||
|
||||
export interface BackBufferData {
|
||||
|
|
10
node_modules/hls.js/src/types/level.ts
generated
vendored
10
node_modules/hls.js/src/types/level.ts
generated
vendored
|
@ -18,6 +18,7 @@ export interface LevelParsed {
|
|||
}
|
||||
|
||||
export interface LevelAttributes extends AttrList {
|
||||
'ALLOWED-CPC'?: string;
|
||||
AUDIO?: string;
|
||||
AUTOSELECT?: string;
|
||||
'AVERAGE-BANDWIDTH'?: string;
|
||||
|
@ -29,15 +30,22 @@ export interface LevelAttributes extends AttrList {
|
|||
DEFAULT?: string;
|
||||
FORCED?: string;
|
||||
'FRAME-RATE'?: string;
|
||||
'HDCP-LEVEL'?: string;
|
||||
LANGUAGE?: string;
|
||||
NAME?: string;
|
||||
'PATHWAY-ID'?: string;
|
||||
'PROGRAM-ID'?: string;
|
||||
RESOLUTION?: string;
|
||||
SCORE?: string;
|
||||
SUBTITLES?: string;
|
||||
TYPE?: string;
|
||||
URI?: string;
|
||||
'VIDEO-RANGE'?: string;
|
||||
}
|
||||
|
||||
export const HdcpLevels = ['NONE', 'TYPE-0', 'TYPE-1', 'TYPE-2', null] as const;
|
||||
export type HdcpLevel = typeof HdcpLevels[number];
|
||||
|
||||
export enum HlsSkip {
|
||||
No = '',
|
||||
Yes = 'YES',
|
||||
|
@ -78,7 +86,7 @@ export class HlsUrlParameters {
|
|||
if (this.skip) {
|
||||
url.searchParams.set('_HLS_skip', this.skip);
|
||||
}
|
||||
return url.toString();
|
||||
return url.href;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
9
node_modules/hls.js/src/types/loader.ts
generated
vendored
9
node_modules/hls.js/src/types/loader.ts
generated
vendored
|
@ -1,5 +1,6 @@
|
|||
import type { Fragment } from '../loader/fragment';
|
||||
import type { Part } from '../loader/fragment';
|
||||
import type { KeyLoaderInfo } from '../loader/key-loader';
|
||||
import type { LevelDetails } from '../loader/level-details';
|
||||
import type { HlsUrlParameters } from './level';
|
||||
|
||||
|
@ -21,6 +22,12 @@ export interface LoaderContext {
|
|||
export interface FragmentLoaderContext extends LoaderContext {
|
||||
frag: Fragment;
|
||||
part: Part | null;
|
||||
resetIV?: boolean;
|
||||
}
|
||||
|
||||
export interface KeyLoaderContext extends LoaderContext {
|
||||
keyInfo: KeyLoaderInfo;
|
||||
frag: Fragment;
|
||||
}
|
||||
|
||||
export interface LoaderConfiguration {
|
||||
|
@ -159,8 +166,6 @@ export interface PlaylistLoaderContext extends LoaderContext {
|
|||
id: number | null;
|
||||
// track group id
|
||||
groupId: string | null;
|
||||
// defines if the loader is handling a sidx request for the playlist
|
||||
isSidxRequest?: boolean;
|
||||
// internal representation of a parsed m3u8 level playlist
|
||||
levelDetails?: LevelDetails;
|
||||
// Blocking playlist request delivery directives (or null id none were added to playlist url
|
||||
|
|
4
node_modules/hls.js/src/types/remuxer.ts
generated
vendored
4
node_modules/hls.js/src/types/remuxer.ts
generated
vendored
|
@ -9,6 +9,7 @@ import {
|
|||
} from './demuxer';
|
||||
import type { SourceBufferName } from './buffer';
|
||||
import type { PlaylistLevelType } from './loader';
|
||||
import type { DecryptData } from '../loader/level-key';
|
||||
|
||||
export interface Remuxer {
|
||||
remux(
|
||||
|
@ -24,7 +25,8 @@ export interface Remuxer {
|
|||
resetInitSegment(
|
||||
initSegment: Uint8Array | undefined,
|
||||
audioCodec: string | undefined,
|
||||
videoCodec: string | undefined
|
||||
videoCodec: string | undefined,
|
||||
decryptdata: DecryptData | null
|
||||
): void;
|
||||
resetTimeStamp(defaultInitPTS): void;
|
||||
resetNextTimestamp(): void;
|
||||
|
|
28
node_modules/hls.js/src/utils/cea-608-parser.ts
generated
vendored
28
node_modules/hls.js/src/utils/cea-608-parser.ts
generated
vendored
|
@ -221,9 +221,10 @@ class CaptionsLogger {
|
|||
public time: number | null = null;
|
||||
public verboseLevel: VerboseLevel = VerboseLevel.ERROR;
|
||||
|
||||
log(severity: VerboseLevel, msg: string): void {
|
||||
log(severity: VerboseLevel, msg: string | (() => string)): void {
|
||||
if (this.verboseLevel >= severity) {
|
||||
logger.log(`${this.time} [${severity}] ${msg}`);
|
||||
const m: string = typeof msg === 'function' ? msg() : msg;
|
||||
logger.log(`${this.time} [${severity}] ${m}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -491,7 +492,8 @@ export class Row {
|
|||
if (this.pos >= NR_COLS) {
|
||||
this.logger.log(
|
||||
VerboseLevel.ERROR,
|
||||
'Cannot insert ' +
|
||||
() =>
|
||||
'Cannot insert ' +
|
||||
byte.toString(16) +
|
||||
' (' +
|
||||
char +
|
||||
|
@ -642,7 +644,10 @@ export class CaptionScreen {
|
|||
}
|
||||
|
||||
setPAC(pacData: PACData) {
|
||||
this.logger.log(VerboseLevel.INFO, 'pacData = ' + JSON.stringify(pacData));
|
||||
this.logger.log(
|
||||
VerboseLevel.INFO,
|
||||
() => 'pacData = ' + JSON.stringify(pacData)
|
||||
);
|
||||
let newRow = pacData.row - 1;
|
||||
if (this.nrRollUpRows && newRow < this.nrRollUpRows - 1) {
|
||||
newRow = this.nrRollUpRows - 1;
|
||||
|
@ -696,7 +701,10 @@ export class CaptionScreen {
|
|||
* Set background/extra foreground, but first do back_space, and then insert space (backwards compatibility).
|
||||
*/
|
||||
setBkgData(bkgData: Partial<PenStyles>) {
|
||||
this.logger.log(VerboseLevel.INFO, 'bkgData = ' + JSON.stringify(bkgData));
|
||||
this.logger.log(
|
||||
VerboseLevel.INFO,
|
||||
() => 'bkgData = ' + JSON.stringify(bkgData)
|
||||
);
|
||||
this.backSpace();
|
||||
this.setPen(bkgData);
|
||||
this.insertChar(0x20); // Space
|
||||
|
@ -714,7 +722,7 @@ export class CaptionScreen {
|
|||
);
|
||||
return; // Not properly setup
|
||||
}
|
||||
this.logger.log(VerboseLevel.TEXT, this.getDisplayText());
|
||||
this.logger.log(VerboseLevel.TEXT, () => this.getDisplayText());
|
||||
const topRowIndex = this.currRow + 1 - this.nrRollUpRows;
|
||||
const topRow = this.rows.splice(topRowIndex, 1)[0];
|
||||
topRow.clear();
|
||||
|
@ -832,7 +840,7 @@ class Cea608Channel {
|
|||
}
|
||||
|
||||
this.mode = newMode;
|
||||
this.logger.log(VerboseLevel.INFO, 'MODE=' + newMode);
|
||||
this.logger.log(VerboseLevel.INFO, () => 'MODE=' + newMode);
|
||||
if (this.mode === 'MODE_POP-ON') {
|
||||
this.writeScreen = this.nonDisplayedMemory;
|
||||
} else {
|
||||
|
@ -855,12 +863,12 @@ class Cea608Channel {
|
|||
this.writeScreen === this.displayedMemory ? 'DISP' : 'NON_DISP';
|
||||
this.logger.log(
|
||||
VerboseLevel.INFO,
|
||||
screen + ': ' + this.writeScreen.getDisplayText(true)
|
||||
() => screen + ': ' + this.writeScreen.getDisplayText(true)
|
||||
);
|
||||
if (this.mode === 'MODE_PAINT-ON' || this.mode === 'MODE_ROLL-UP') {
|
||||
this.logger.log(
|
||||
VerboseLevel.TEXT,
|
||||
'DISPLAYED: ' + this.displayedMemory.getDisplayText(true)
|
||||
() => 'DISPLAYED: ' + this.displayedMemory.getDisplayText(true)
|
||||
);
|
||||
this.outputDataUpdate();
|
||||
}
|
||||
|
@ -962,7 +970,7 @@ class Cea608Channel {
|
|||
this.writeScreen = this.nonDisplayedMemory;
|
||||
this.logger.log(
|
||||
VerboseLevel.TEXT,
|
||||
'DISP: ' + this.displayedMemory.getDisplayText()
|
||||
() => 'DISP: ' + this.displayedMemory.getDisplayText()
|
||||
);
|
||||
}
|
||||
this.outputDataUpdate(true);
|
||||
|
|
3
node_modules/hls.js/src/utils/codecs.ts
generated
vendored
3
node_modules/hls.js/src/utils/codecs.ts
generated
vendored
|
@ -25,6 +25,7 @@ const sampleEntryCodesISO = {
|
|||
mp4a: true,
|
||||
'raw ': true,
|
||||
Opus: true,
|
||||
opus: true, // browsers expect this to be lowercase despite MP4RA says 'Opus'
|
||||
samr: true,
|
||||
sawb: true,
|
||||
sawp: true,
|
||||
|
@ -42,7 +43,9 @@ const sampleEntryCodesISO = {
|
|||
avcp: true,
|
||||
av01: true,
|
||||
drac: true,
|
||||
dva1: true,
|
||||
dvav: true,
|
||||
dvh1: true,
|
||||
dvhe: true,
|
||||
encv: true,
|
||||
hev1: true,
|
||||
|
|
60
node_modules/hls.js/src/utils/discontinuities.ts
generated
vendored
60
node_modules/hls.js/src/utils/discontinuities.ts
generated
vendored
|
@ -1,10 +1,10 @@
|
|||
import { logger } from './logger';
|
||||
import { adjustSliding } from '../controller/level-helper';
|
||||
|
||||
import type { Fragment } from '../loader/fragment';
|
||||
import type { LevelDetails } from '../loader/level-details';
|
||||
import type { Level } from '../types/level';
|
||||
import type { RequiredProperties } from '../types/general';
|
||||
import { adjustSliding } from '../controller/level-helper';
|
||||
|
||||
export function findFirstFragWithCC(fragments: Fragment[], cc: number) {
|
||||
let firstFrag: Fragment | null = null;
|
||||
|
@ -39,7 +39,8 @@ export function shouldAlignOnDiscontinuities(
|
|||
// Find the first frag in the previous level which matches the CC of the first frag of the new level
|
||||
export function findDiscontinuousReferenceFrag(
|
||||
prevDetails: LevelDetails,
|
||||
curDetails: LevelDetails
|
||||
curDetails: LevelDetails,
|
||||
referenceIndex: number = 0
|
||||
) {
|
||||
const prevFrags = prevDetails.fragments;
|
||||
const curFrags = curDetails.fragments;
|
||||
|
@ -174,14 +175,6 @@ export function alignPDT(details: LevelDetails, lastDetails: LevelDetails) {
|
|||
}
|
||||
}
|
||||
|
||||
export function alignFragmentByPDTDelta(frag: Fragment, delta: number) {
|
||||
const { programDateTime } = frag;
|
||||
if (!programDateTime) return;
|
||||
const start = (programDateTime - delta) / 1000;
|
||||
frag.start = frag.startPTS = start;
|
||||
frag.endPTS = start + frag.duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures appropriate time-alignment between renditions based on PDT. Unlike `alignPDT`, which adjusts
|
||||
* the timeline based on the delta between PDTs of the 0th fragment of two playlists/`LevelDetails`,
|
||||
|
@ -199,30 +192,31 @@ export function alignMediaPlaylistByPDT(
|
|||
details: LevelDetails,
|
||||
refDetails: LevelDetails
|
||||
) {
|
||||
// This check protects the unsafe "!" usage below for null program date time access.
|
||||
if (
|
||||
!refDetails.fragments.length ||
|
||||
!details.hasProgramDateTime ||
|
||||
!refDetails.hasProgramDateTime
|
||||
) {
|
||||
if (!details.hasProgramDateTime || !refDetails.hasProgramDateTime) {
|
||||
return;
|
||||
}
|
||||
const refPDT = refDetails.fragments[0].programDateTime!; // hasProgramDateTime check above makes this safe.
|
||||
const refStart = refDetails.fragments[0].start;
|
||||
// Use the delta between the reference details' presentation timeline's start time and its PDT
|
||||
// to align the other rendition's timeline.
|
||||
const delta = refPDT - refStart * 1000;
|
||||
// Per spec: "If any Media Playlist in a Master Playlist contains an EXT-X-PROGRAM-DATE-TIME tag, then all
|
||||
// Media Playlists in that Master Playlist MUST contain EXT-X-PROGRAM-DATE-TIME tags with consistent mappings
|
||||
// of date and time to media timestamps."
|
||||
// So we should be able to use each rendition's PDT as a reference time and use the delta to compute our relevant
|
||||
// start and end times.
|
||||
// NOTE: This code assumes each level/details timelines have already been made "internally consistent"
|
||||
details.fragments.forEach((frag) => {
|
||||
alignFragmentByPDTDelta(frag, delta);
|
||||
});
|
||||
if (details.fragmentHint) {
|
||||
alignFragmentByPDTDelta(details.fragmentHint, delta);
|
||||
|
||||
const fragments = details.fragments;
|
||||
const refFragments = refDetails.fragments;
|
||||
if (!fragments.length || !refFragments.length) {
|
||||
return;
|
||||
}
|
||||
details.alignedSliding = true;
|
||||
|
||||
// Calculate a delta to apply to all fragments according to the delta in PDT times and start times
|
||||
// of a fragment in the reference details, and a fragment in the target details of the same discontinuity.
|
||||
// If a fragment of the same discontinuity was not found use the middle fragment of both.
|
||||
const middleFrag = Math.round(refFragments.length / 2) - 1;
|
||||
const refFrag = refFragments[middleFrag];
|
||||
const frag =
|
||||
findFirstFragWithCC(fragments, refFrag.cc) ||
|
||||
fragments[Math.round(fragments.length / 2) - 1];
|
||||
|
||||
const refPDT = refFrag.programDateTime;
|
||||
const targetPDT = frag.programDateTime;
|
||||
if (refPDT === null || targetPDT === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const delta = (targetPDT - refPDT) / 1000 - (frag.start - refFrag.start);
|
||||
adjustSlidingStart(delta, details);
|
||||
}
|
||||
|
|
5
node_modules/hls.js/src/utils/fetch-loader.ts
generated
vendored
5
node_modules/hls.js/src/utils/fetch-loader.ts
generated
vendored
|
@ -133,7 +133,10 @@ class FetchLoader implements Loader<LoaderContext> {
|
|||
self.performance.now(),
|
||||
stats.loading.first
|
||||
);
|
||||
stats.loaded = stats.total = responseData[LENGTH];
|
||||
const total = responseData[LENGTH];
|
||||
if (total) {
|
||||
stats.loaded = stats.total = total;
|
||||
}
|
||||
|
||||
const loaderResponse = {
|
||||
url: response.url,
|
||||
|
|
2
node_modules/hls.js/src/utils/hex.ts
generated
vendored
2
node_modules/hls.js/src/utils/hex.ts
generated
vendored
|
@ -3,7 +3,7 @@
|
|||
*/
|
||||
|
||||
const Hex = {
|
||||
hexDump: function (array) {
|
||||
hexDump: function (array: Uint8Array) {
|
||||
let str = '';
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
let h = array[i].toString(16);
|
||||
|
|
5
node_modules/hls.js/src/utils/imsc1-ttml-parser.ts
generated
vendored
5
node_modules/hls.js/src/utils/imsc1-ttml-parser.ts
generated
vendored
|
@ -104,11 +104,6 @@ function parseTTML(ttml: string, syncTime: number): Array<VTTCue> {
|
|||
const region = regionElements[cueElement.getAttribute('region')];
|
||||
const style = styleElements[cueElement.getAttribute('style')];
|
||||
|
||||
// TODO: Add regions to track and cue (origin and extend)
|
||||
// These values are hard-coded (for now) to simulate region settings in the demo
|
||||
cue.position = 10;
|
||||
cue.size = 80;
|
||||
|
||||
// Apply styles to cue
|
||||
const styles = getTtmlStyles(region, style, styleElements);
|
||||
const { textAlign } = styles;
|
||||
|
|
4
node_modules/hls.js/src/utils/logger.ts
generated
vendored
4
node_modules/hls.js/src/utils/logger.ts
generated
vendored
|
@ -52,7 +52,7 @@ function exportLoggerFunctions(
|
|||
});
|
||||
}
|
||||
|
||||
export function enableLogs(debugConfig: boolean | ILogger): void {
|
||||
export function enableLogs(debugConfig: boolean | ILogger, id: string): void {
|
||||
// check that console is available
|
||||
if (
|
||||
(self.console && debugConfig === true) ||
|
||||
|
@ -71,7 +71,7 @@ export function enableLogs(debugConfig: boolean | ILogger): void {
|
|||
// Some browsers don't allow to use bind on console object anyway
|
||||
// fallback to default if needed
|
||||
try {
|
||||
exportedLogger.log();
|
||||
exportedLogger.log(`Debug logs enabled for "${id}"`);
|
||||
} catch (e) {
|
||||
exportedLogger = fakeLogger;
|
||||
}
|
||||
|
|
147
node_modules/hls.js/src/utils/mediakeys-helper.ts
generated
vendored
147
node_modules/hls.js/src/utils/mediakeys-helper.ts
generated
vendored
|
@ -1,16 +1,98 @@
|
|||
import type { DRMSystemOptions, EMEControllerConfig } from '../config';
|
||||
|
||||
/**
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/requestMediaKeySystemAccess
|
||||
*/
|
||||
export enum KeySystems {
|
||||
WIDEVINE = 'com.widevine.alpha',
|
||||
CLEARKEY = 'org.w3.clearkey',
|
||||
FAIRPLAY = 'com.apple.fps',
|
||||
PLAYREADY = 'com.microsoft.playready',
|
||||
WIDEVINE = 'com.widevine.alpha',
|
||||
}
|
||||
|
||||
// Playlist #EXT-X-KEY KEYFORMAT values
|
||||
export enum KeySystemFormats {
|
||||
CLEARKEY = 'org.w3.clearkey',
|
||||
FAIRPLAY = 'com.apple.streamingkeydelivery',
|
||||
PLAYREADY = 'com.microsoft.playready',
|
||||
WIDEVINE = 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed',
|
||||
}
|
||||
|
||||
export function keySystemFormatToKeySystemDomain(
|
||||
format: KeySystemFormats
|
||||
): KeySystems | undefined {
|
||||
switch (format) {
|
||||
case KeySystemFormats.FAIRPLAY:
|
||||
return KeySystems.FAIRPLAY;
|
||||
case KeySystemFormats.PLAYREADY:
|
||||
return KeySystems.PLAYREADY;
|
||||
case KeySystemFormats.WIDEVINE:
|
||||
return KeySystems.WIDEVINE;
|
||||
case KeySystemFormats.CLEARKEY:
|
||||
return KeySystems.CLEARKEY;
|
||||
}
|
||||
}
|
||||
|
||||
// System IDs for which we can extract a key ID from "encrypted" event PSSH
|
||||
export enum KeySystemIds {
|
||||
// CENC = '1077efecc0b24d02ace33c1e52e2fb4b'
|
||||
// CLEARKEY = 'e2719d58a985b3c9781ab030af78d30e',
|
||||
// FAIRPLAY = '94ce86fb07ff4f43adb893d2fa968ca2',
|
||||
// PLAYREADY = '9a04f07998404286ab92e65be0885f95',
|
||||
WIDEVINE = 'edef8ba979d64acea3c827dcd51d21ed',
|
||||
}
|
||||
|
||||
export function keySystemIdToKeySystemDomain(
|
||||
systemId: KeySystemIds
|
||||
): KeySystems | undefined {
|
||||
if (systemId === KeySystemIds.WIDEVINE) {
|
||||
return KeySystems.WIDEVINE;
|
||||
// } else if (systemId === KeySystemIds.PLAYREADY) {
|
||||
// return KeySystems.PLAYREADY;
|
||||
// } else if (systemId === KeySystemIds.CENC || systemId === KeySystemIds.CLEARKEY) {
|
||||
// return KeySystems.CLEARKEY;
|
||||
}
|
||||
}
|
||||
|
||||
export function keySystemDomainToKeySystemFormat(
|
||||
keySystem: KeySystems
|
||||
): KeySystemFormats | undefined {
|
||||
switch (keySystem) {
|
||||
case KeySystems.FAIRPLAY:
|
||||
return KeySystemFormats.FAIRPLAY;
|
||||
case KeySystems.PLAYREADY:
|
||||
return KeySystemFormats.PLAYREADY;
|
||||
case KeySystems.WIDEVINE:
|
||||
return KeySystemFormats.WIDEVINE;
|
||||
case KeySystems.CLEARKEY:
|
||||
return KeySystemFormats.CLEARKEY;
|
||||
}
|
||||
}
|
||||
|
||||
export function getKeySystemsForConfig(
|
||||
config: EMEControllerConfig
|
||||
): KeySystems[] {
|
||||
const { drmSystems, widevineLicenseUrl } = config;
|
||||
const keySystemsToAttempt: KeySystems[] = drmSystems
|
||||
? [
|
||||
KeySystems.FAIRPLAY,
|
||||
KeySystems.WIDEVINE,
|
||||
KeySystems.PLAYREADY,
|
||||
KeySystems.CLEARKEY,
|
||||
].filter((keySystem) => !!drmSystems[keySystem])
|
||||
: [];
|
||||
if (!keySystemsToAttempt[KeySystems.WIDEVINE] && widevineLicenseUrl) {
|
||||
keySystemsToAttempt.push(KeySystems.WIDEVINE);
|
||||
}
|
||||
return keySystemsToAttempt;
|
||||
}
|
||||
|
||||
export type MediaKeyFunc = (
|
||||
keySystem: KeySystems,
|
||||
supportedConfigurations: MediaKeySystemConfiguration[]
|
||||
) => Promise<MediaKeySystemAccess>;
|
||||
const requestMediaKeySystemAccess = (function (): MediaKeyFunc | null {
|
||||
|
||||
export const requestMediaKeySystemAccess = (function (): MediaKeyFunc | null {
|
||||
if (
|
||||
typeof self !== 'undefined' &&
|
||||
self.navigator &&
|
||||
|
@ -22,4 +104,63 @@ const requestMediaKeySystemAccess = (function (): MediaKeyFunc | null {
|
|||
}
|
||||
})();
|
||||
|
||||
export { requestMediaKeySystemAccess };
|
||||
/**
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/MediaKeySystemConfiguration
|
||||
*/
|
||||
export function getSupportedMediaKeySystemConfigurations(
|
||||
keySystem: KeySystems,
|
||||
audioCodecs: string[],
|
||||
videoCodecs: string[],
|
||||
drmSystemOptions: DRMSystemOptions
|
||||
): MediaKeySystemConfiguration[] {
|
||||
let initDataTypes: string[];
|
||||
switch (keySystem) {
|
||||
case KeySystems.FAIRPLAY:
|
||||
initDataTypes = ['cenc', 'sinf'];
|
||||
break;
|
||||
case KeySystems.WIDEVINE:
|
||||
case KeySystems.PLAYREADY:
|
||||
initDataTypes = ['cenc'];
|
||||
break;
|
||||
case KeySystems.CLEARKEY:
|
||||
initDataTypes = ['cenc', 'keyids'];
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown key-system: ${keySystem}`);
|
||||
}
|
||||
return createMediaKeySystemConfigurations(
|
||||
initDataTypes,
|
||||
audioCodecs,
|
||||
videoCodecs,
|
||||
drmSystemOptions
|
||||
);
|
||||
}
|
||||
|
||||
function createMediaKeySystemConfigurations(
|
||||
initDataTypes: string[],
|
||||
audioCodecs: string[],
|
||||
videoCodecs: string[],
|
||||
drmSystemOptions: DRMSystemOptions
|
||||
): MediaKeySystemConfiguration[] {
|
||||
const baseConfig: MediaKeySystemConfiguration = {
|
||||
initDataTypes: initDataTypes,
|
||||
persistentState: drmSystemOptions.persistentState || 'not-allowed',
|
||||
distinctiveIdentifier:
|
||||
drmSystemOptions.distinctiveIdentifier || 'not-allowed',
|
||||
sessionTypes: drmSystemOptions.sessionTypes || [
|
||||
drmSystemOptions.sessionType || 'temporary',
|
||||
],
|
||||
audioCapabilities: audioCodecs.map((codec) => ({
|
||||
contentType: `audio/mp4; codecs="${codec}"`,
|
||||
robustness: drmSystemOptions.audioRobustness || '',
|
||||
encryptionScheme: drmSystemOptions.audioEncryptionScheme || null,
|
||||
})),
|
||||
videoCapabilities: videoCodecs.map((codec) => ({
|
||||
contentType: `video/mp4; codecs="${codec}"`,
|
||||
robustness: drmSystemOptions.videoRobustness || '',
|
||||
encryptionScheme: drmSystemOptions.videoEncryptionScheme || null,
|
||||
})),
|
||||
};
|
||||
|
||||
return [baseConfig];
|
||||
}
|
||||
|
|
199
node_modules/hls.js/src/utils/mp4-tools.ts
generated
vendored
199
node_modules/hls.js/src/utils/mp4-tools.ts
generated
vendored
|
@ -1,7 +1,10 @@
|
|||
import { sliceUint8 } from './typed-array';
|
||||
import { ElementaryStreamTypes } from '../loader/fragment';
|
||||
import { PassthroughTrack, UserdataSample } from '../types/demuxer';
|
||||
import { sliceUint8 } from './typed-array';
|
||||
import { utf8ArrayToStr } from '../demux/id3';
|
||||
import { logger } from '../utils/logger';
|
||||
import Hex from './hex';
|
||||
import type { PassthroughTrack, UserdataSample } from '../types/demuxer';
|
||||
import type { DecryptData } from '../loader/level-key';
|
||||
|
||||
const UINT32_MAX = Math.pow(2, 32) - 1;
|
||||
const push = [].push;
|
||||
|
@ -272,6 +275,65 @@ export function parseInitSegment(initSegment: Uint8Array): InitData {
|
|||
return result;
|
||||
}
|
||||
|
||||
export function patchEncyptionData(
|
||||
initSegment: Uint8Array | undefined,
|
||||
decryptdata: DecryptData | null
|
||||
): Uint8Array | undefined {
|
||||
if (!initSegment || !decryptdata) {
|
||||
return initSegment;
|
||||
}
|
||||
const keyId = decryptdata.keyId;
|
||||
if (keyId && decryptdata.isCommonEncryption) {
|
||||
const traks = findBox(initSegment, ['moov', 'trak']);
|
||||
traks.forEach((trak) => {
|
||||
const stsd = findBox(trak, ['mdia', 'minf', 'stbl', 'stsd'])[0];
|
||||
|
||||
// skip the sample entry count
|
||||
const sampleEntries = stsd.subarray(8);
|
||||
let encBoxes = findBox(sampleEntries, ['enca']);
|
||||
const isAudio = encBoxes.length > 0;
|
||||
if (!isAudio) {
|
||||
encBoxes = findBox(sampleEntries, ['encv']);
|
||||
}
|
||||
encBoxes.forEach((enc) => {
|
||||
const encBoxChildren = isAudio ? enc.subarray(28) : enc.subarray(78);
|
||||
const sinfBoxes = findBox(encBoxChildren, ['sinf']);
|
||||
sinfBoxes.forEach((sinf) => {
|
||||
const tenc = parseSinf(sinf);
|
||||
if (tenc) {
|
||||
// Look for default key id (keyID offset is always 8 within the tenc box):
|
||||
const tencKeyId = tenc.subarray(8, 24);
|
||||
if (!tencKeyId.some((b) => b !== 0)) {
|
||||
logger.log(
|
||||
`[eme] Patching keyId in 'enc${
|
||||
isAudio ? 'a' : 'v'
|
||||
}>sinf>>tenc' box: ${Hex.hexDump(tencKeyId)} -> ${Hex.hexDump(
|
||||
keyId
|
||||
)}`
|
||||
);
|
||||
tenc.set(keyId, 8);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return initSegment;
|
||||
}
|
||||
|
||||
export function parseSinf(sinf: Uint8Array): Uint8Array | null {
|
||||
const schm = findBox(sinf, ['schm'])[0];
|
||||
if (schm) {
|
||||
const scheme = bin2str(schm.subarray(4, 8));
|
||||
if (scheme === 'cbcs' || scheme === 'cenc') {
|
||||
return findBox(sinf, ['schi', 'tenc'])[0];
|
||||
}
|
||||
}
|
||||
logger.error(`[eme] missing 'schm' box`);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the base media decode start time, in seconds, for an MP4
|
||||
* fragment. If multiple fragments are specified, the earliest time is
|
||||
|
@ -681,14 +743,14 @@ export function parseSamples(
|
|||
while (naluTotalSize < sampleSize) {
|
||||
const naluSize = readUint32(videoData, sampleOffset);
|
||||
sampleOffset += 4;
|
||||
const naluType = videoData[sampleOffset] & 0x1f;
|
||||
if (isSEIMessage(isHEVCFlavor, naluType)) {
|
||||
if (isSEIMessage(isHEVCFlavor, videoData[sampleOffset])) {
|
||||
const data = videoData.subarray(
|
||||
sampleOffset,
|
||||
sampleOffset + naluSize
|
||||
);
|
||||
parseSEIMessageFromNALu(
|
||||
data,
|
||||
isHEVCFlavor ? 2 : 1,
|
||||
timeOffset + compositionOffset / timescale,
|
||||
seiSamples
|
||||
);
|
||||
|
@ -723,19 +785,26 @@ function isHEVC(codec: string) {
|
|||
);
|
||||
}
|
||||
|
||||
function isSEIMessage(isHEVCFlavor: boolean, naluType: number) {
|
||||
return isHEVCFlavor ? naluType === 39 || naluType === 40 : naluType === 6;
|
||||
function isSEIMessage(isHEVCFlavor: boolean, naluHeader: number) {
|
||||
if (isHEVCFlavor) {
|
||||
const naluType = (naluHeader >> 1) & 0x3f;
|
||||
return naluType === 39 || naluType === 40;
|
||||
} else {
|
||||
const naluType = naluHeader & 0x1f;
|
||||
return naluType === 6;
|
||||
}
|
||||
}
|
||||
|
||||
export function parseSEIMessageFromNALu(
|
||||
unescapedData: Uint8Array,
|
||||
headerSize: number,
|
||||
pts: number,
|
||||
samples: UserdataSample[]
|
||||
) {
|
||||
const data = discardEPB(unescapedData);
|
||||
let seiPtr = 0;
|
||||
// skip frameType
|
||||
seiPtr++;
|
||||
// skip nal header
|
||||
seiPtr += headerSize;
|
||||
let payloadType = 0;
|
||||
let payloadSize = 0;
|
||||
let endOfCaptions = false;
|
||||
|
@ -840,7 +909,7 @@ export function parseSEIMessageFromNALu(
|
|||
/**
|
||||
* remove Emulation Prevention bytes from a RBSP
|
||||
*/
|
||||
function discardEPB(data: Uint8Array): Uint8Array {
|
||||
export function discardEPB(data: Uint8Array): Uint8Array {
|
||||
const length = data.byteLength;
|
||||
const EPBPositions = [] as Array<number>;
|
||||
let i = 1;
|
||||
|
@ -962,3 +1031,115 @@ export function parseEmsg(data: Uint8Array): IEmsgParsingData {
|
|||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
export function mp4Box(type: ArrayLike<number>, ...payload: Uint8Array[]) {
|
||||
const len = payload.length;
|
||||
let size = 8;
|
||||
let i = len;
|
||||
while (i--) {
|
||||
size += payload[i].byteLength;
|
||||
}
|
||||
const result = new Uint8Array(size);
|
||||
result[0] = (size >> 24) & 0xff;
|
||||
result[1] = (size >> 16) & 0xff;
|
||||
result[2] = (size >> 8) & 0xff;
|
||||
result[3] = size & 0xff;
|
||||
result.set(type, 4);
|
||||
for (i = 0, size = 8; i < len; i++) {
|
||||
result.set(payload[i], size);
|
||||
size += payload[i].byteLength;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function mp4pssh(
|
||||
systemId: Uint8Array,
|
||||
keyids: Array<Uint8Array> | null,
|
||||
data: Uint8Array
|
||||
) {
|
||||
if (systemId.byteLength !== 16) {
|
||||
throw new RangeError('Invalid system id');
|
||||
}
|
||||
let version;
|
||||
let kids;
|
||||
if (keyids) {
|
||||
version = 1;
|
||||
kids = new Uint8Array(keyids.length * 16);
|
||||
for (let ix = 0; ix < keyids.length; ix++) {
|
||||
const k = keyids[ix]; // uint8array
|
||||
if (k.byteLength !== 16) {
|
||||
throw new RangeError('Invalid key');
|
||||
}
|
||||
kids.set(k, ix * 16);
|
||||
}
|
||||
} else {
|
||||
version = 0;
|
||||
kids = new Uint8Array();
|
||||
}
|
||||
let kidCount;
|
||||
if (version > 0) {
|
||||
kidCount = new Uint8Array(4);
|
||||
if (keyids!.length > 0) {
|
||||
new DataView(kidCount.buffer).setUint32(0, keyids!.length, false);
|
||||
}
|
||||
} else {
|
||||
kidCount = new Uint8Array();
|
||||
}
|
||||
const dataSize = new Uint8Array(4);
|
||||
if (data && data.byteLength > 0) {
|
||||
new DataView(dataSize.buffer).setUint32(0, data.byteLength, false);
|
||||
}
|
||||
return mp4Box(
|
||||
[112, 115, 115, 104],
|
||||
new Uint8Array([
|
||||
version,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00, // Flags
|
||||
]),
|
||||
systemId, // 16 bytes
|
||||
kidCount,
|
||||
kids,
|
||||
dataSize,
|
||||
data || new Uint8Array()
|
||||
);
|
||||
}
|
||||
|
||||
export function parsePssh(initData: ArrayBuffer) {
|
||||
if (!(initData instanceof ArrayBuffer) || initData.byteLength < 32) {
|
||||
return null;
|
||||
}
|
||||
const result = {
|
||||
version: 0,
|
||||
systemId: '',
|
||||
kids: null as null | Uint8Array[],
|
||||
data: null as null | Uint8Array,
|
||||
};
|
||||
const view = new DataView(initData);
|
||||
const boxSize = view.getUint32(0);
|
||||
if (initData.byteLength !== boxSize && boxSize > 44) {
|
||||
return null;
|
||||
}
|
||||
const type = view.getUint32(4);
|
||||
if (type !== 0x70737368) {
|
||||
return null;
|
||||
}
|
||||
result.version = view.getUint32(8) >>> 24;
|
||||
if (result.version > 1) {
|
||||
return null;
|
||||
}
|
||||
result.systemId = Hex.hexDump(new Uint8Array(initData, 12, 16));
|
||||
const dataSizeOrKidCount = view.getUint32(28);
|
||||
if (result.version === 0) {
|
||||
if (boxSize - 32 < dataSizeOrKidCount) {
|
||||
return null;
|
||||
}
|
||||
result.data = new Uint8Array(initData, 32, dataSizeOrKidCount);
|
||||
} else if (result.version === 1) {
|
||||
result.kids = [];
|
||||
for (let i = 0; i < dataSizeOrKidCount; i++) {
|
||||
result.kids.push(new Uint8Array(initData, 32 + i * 16, 16));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
2
node_modules/hls.js/src/utils/time-ranges.ts
generated
vendored
2
node_modules/hls.js/src/utils/time-ranges.ts
generated
vendored
|
@ -7,7 +7,7 @@ const TimeRanges = {
|
|||
let log = '';
|
||||
const len = r.length;
|
||||
for (let i = 0; i < len; i++) {
|
||||
log += '[' + r.start(i).toFixed(3) + ',' + r.end(i).toFixed(3) + ']';
|
||||
log += `[${r.start(i).toFixed(3)}-${r.end(i).toFixed(3)}]`;
|
||||
}
|
||||
|
||||
return log;
|
||||
|
|
9
node_modules/hls.js/src/utils/xhr-loader.ts
generated
vendored
9
node_modules/hls.js/src/utils/xhr-loader.ts
generated
vendored
|
@ -163,14 +163,19 @@ class XhrLoader implements Loader<LoaderContext> {
|
|||
xhr.onprogress = null;
|
||||
const status = xhr.status;
|
||||
// http status between 200 to 299 are all successful
|
||||
if (status >= 200 && status < 300) {
|
||||
const isArrayBuffer = xhr.responseType === 'arraybuffer';
|
||||
if (
|
||||
status >= 200 &&
|
||||
status < 300 &&
|
||||
((isArrayBuffer && xhr.response) || xhr.responseText !== null)
|
||||
) {
|
||||
stats.loading.end = Math.max(
|
||||
self.performance.now(),
|
||||
stats.loading.first
|
||||
);
|
||||
let data;
|
||||
let len: number;
|
||||
if (context.responseType === 'arraybuffer') {
|
||||
if (isArrayBuffer) {
|
||||
data = xhr.response;
|
||||
len = data.byteLength;
|
||||
} else {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue