diff --git a/extern/hls.js/README.md b/extern/hls.js/README.md deleted file mode 100644 index 0a614cc3..00000000 --- a/extern/hls.js/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# HLS.js - -This is a build of the master branch of https://github.com/video-dev/hls.js - -This build contains "HLS Interstitials" support (https://github.com/video-dev/hls.js/issues/5730), which has recently been merged in master. - -Once HLS.js publishes v1.6.0, this package will be removed. Use with caution, and do not rely on this package in production. diff --git a/extern/hls.js/hls.js.d.ts b/extern/hls.js/hls.js.d.ts deleted file mode 100644 index 947a6e0c..00000000 --- a/extern/hls.js/hls.js.d.ts +++ /dev/null @@ -1,3718 +0,0 @@ -export declare interface AbrComponentAPI extends ComponentAPI { - firstAutoLevel: number; - forcedAutoLevel: number; - nextAutoLevel: number; - readonly bwEstimator?: EwmaBandWidthEstimator; - resetEstimator(abrEwmaDefaultEstimate: number): any; -} - -export declare class AbrController extends Logger implements AbrComponentAPI { - protected hls: Hls; - private lastLevelLoadSec; - private lastLoadedFragLevel; - private firstSelection; - private _nextAutoLevel; - private nextAutoLevelKey; - private audioTracksByGroup; - private codecTiers; - private timer; - private fragCurrent; - private partCurrent; - private bitrateTestDelay; - bwEstimator: EwmaBandWidthEstimator; - constructor(hls: Hls); - resetEstimator(abrEwmaDefaultEstimate?: number): void; - private initEstimator; - protected registerListeners(): void; - protected unregisterListeners(): void; - destroy(): void; - protected onManifestLoading(event: Events.MANIFEST_LOADING, data: ManifestLoadingData): void; - private onLevelsUpdated; - private onMaxAutoLevelUpdated; - protected onFragLoading(event: Events.FRAG_LOADING, data: FragLoadingData): void; - protected onLevelSwitching(event: Events.LEVEL_SWITCHING, data: LevelSwitchingData): void; - protected onError(event: Events.ERROR, data: ErrorData): void; - private getTimeToLoadFrag; - protected onLevelLoaded(event: Events.LEVEL_LOADED, data: LevelLoadedData): void; - private _abandonRulesCheck; - protected onFragLoaded(event: Events.FRAG_LOADED, { frag, part }: FragLoadedData): void; - protected onFragBuffered(event: Events.FRAG_BUFFERED, data: FragBufferedData): void; - private ignoreFragment; - clearTimer(): void; - get firstAutoLevel(): number; - get forcedAutoLevel(): number; - get nextAutoLevel(): number; - private getAutoLevelKey; - private getNextABRAutoLevel; - private getStarvationDelay; - private getBwEstimate; - private findBestLevel; - set nextAutoLevel(nextLevel: number); -} - -export declare type ABRControllerConfig = { - abrEwmaFastLive: number; - abrEwmaSlowLive: number; - abrEwmaFastVoD: number; - abrEwmaSlowVoD: number; - /** - * Default bandwidth estimate in bits/s prior to collecting fragment bandwidth samples - */ - abrEwmaDefaultEstimate: number; - abrEwmaDefaultEstimateMax: number; - abrBandWidthFactor: number; - abrBandWidthUpFactor: number; - abrMaxWithRealBitrate: boolean; - maxStarvationDelay: number; - maxLoadingDelay: number; -}; - -export declare type AssetListJSON = { - ASSETS: Array<{ - URI: string; - DURATION: string; - }>; -}; - -export declare interface AssetListLoadedData { - event: InterstitialEventWithAssetList; - assetListResponse: AssetListJSON; - networkDetails: any; -} - -export declare interface AssetListLoadingData { - event: InterstitialEventWithAssetList; -} - -export declare type AttachMediaSourceData = { - media: HTMLMediaElement; - mediaSource: MediaSource | null; - tracks: SourceBufferTrackSet; -}; - -export declare class AttrList { - [key: string]: any; - constructor(attrs: string | Record, parsed?: Pick); - get clientAttrs(): string[]; - decimalInteger(attrName: string): number; - hexadecimalInteger(attrName: string): Uint8Array | null; - hexadecimalIntegerAsNumber(attrName: string): number; - decimalFloatingPoint(attrName: string): number; - optionalFloat(attrName: string, defaultValue: number): number; - enumeratedString(attrName: string): string | undefined; - enumeratedStringList(attrName: string, dict: T): { - [key in keyof T]: boolean; - }; - bool(attrName: string): boolean; - decimalResolution(attrName: string): { - width: number; - height: number; - } | undefined; - static parseAttrList(input: string, parsed?: Pick): Record; -} - -export declare type AudioPlaylistType = 'AUDIO'; - -export declare type AudioSelectionOption = { - lang?: string; - assocLang?: string; - characteristics?: string; - channels?: string; - name?: string; - audioCodec?: string; - groupId?: string; - default?: boolean; -}; - -export declare class AudioStreamController extends BaseStreamController implements NetworkComponentAPI { - private videoAnchor; - private mainFragLoading; - private bufferedTrack; - private switchingTrack; - private trackId; - private waitingData; - private mainDetails; - private flushing; - private bufferFlushed; - private cachedTrackLoadedData; - constructor(hls: Hls, fragmentTracker: FragmentTracker, keyLoader: KeyLoader); - protected onHandlerDestroying(): void; - protected registerListeners(): void; - protected unregisterListeners(): void; - onInitPtsFound(event: Events.INIT_PTS_FOUND, { frag, id, initPTS, timescale }: InitPTSFoundData): void; - private findSyncFrag; - startLoad(startPosition: number): void; - doTick(): void; - clearWaitingFragment(): void; - protected resetLoadingState(): void; - protected onTickEnd(): void; - private doTickIdle; - protected onMediaDetaching(event: Events.MEDIA_DETACHING, data: MediaDetachingData): void; - private onAudioTracksUpdated; - private onAudioTrackSwitching; - protected onManifestLoading(): void; - private onLevelLoaded; - private onAudioTrackLoaded; - _handleFragmentLoadProgress(data: FragLoadedData): void; - protected _handleFragmentLoadComplete(fragLoadedData: FragLoadedData): void; - private onBufferReset; - private onBufferCreated; - private onFragLoading; - private onFragBuffered; - protected onError(event: Events.ERROR, data: ErrorData): void; - private onBufferFlushing; - private onBufferFlushed; - private _handleTransmuxComplete; - private _bufferInitSegment; - protected loadFragment(frag: Fragment, track: Level, targetBufferTime: number): void; - private flushAudioIfNeeded; - private completeAudioSwitch; -} - -export declare class AudioTrackController extends BasePlaylistController { - private tracks; - private groupIds; - private tracksInGroup; - private trackId; - private currentTrack; - private selectDefaultTrack; - constructor(hls: Hls); - private registerListeners; - private unregisterListeners; - destroy(): void; - protected onManifestLoading(): void; - protected onManifestParsed(event: Events.MANIFEST_PARSED, data: ManifestParsedData): void; - protected onAudioTrackLoaded(event: Events.AUDIO_TRACK_LOADED, data: AudioTrackLoadedData): void; - protected onLevelLoading(event: Events.LEVEL_LOADING, data: LevelLoadingData): void; - protected onLevelSwitching(event: Events.LEVEL_SWITCHING, data: LevelSwitchingData): void; - private switchLevel; - protected onError(event: Events.ERROR, data: ErrorData): void; - get allAudioTracks(): MediaPlaylist[]; - get audioTracks(): MediaPlaylist[]; - get audioTrack(): number; - set audioTrack(newId: number); - setAudioOption(audioOption: MediaPlaylist | AudioSelectionOption | undefined): MediaPlaylist | null; - private setAudioTrack; - private findTrackId; - protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters): void; -} - -export declare interface AudioTrackLoadedData extends TrackLoadedData { -} - -export declare interface AudioTracksUpdatedData { - audioTracks: MediaPlaylist[]; -} - -export declare interface AudioTrackSwitchedData extends MediaPlaylist { -} - -export declare interface AudioTrackSwitchingData extends MediaPlaylist { -} - -export declare interface AudioTrackUpdatedData { - details: LevelDetails; - id: number; - groupId: string; -} - -export declare interface BackBufferData { - bufferEnd: number; -} - -export declare type BaseData = { - url: string; -}; - -export declare class BasePlaylistController extends Logger implements NetworkComponentAPI { - protected hls: Hls; - protected timer: number; - protected requestScheduled: number; - protected canLoad: boolean; - constructor(hls: Hls, logPrefix: string); - destroy(): void; - protected clearTimer(): void; - startLoad(): void; - stopLoad(): void; - protected switchParams(playlistUri: string, previous: LevelDetails | undefined, current: LevelDetails | undefined): HlsUrlParameters | undefined; - protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters): void; - protected shouldLoadPlaylist(playlist: Level | MediaPlaylist | null | undefined): boolean; - protected shouldReloadPlaylist(playlist: Level | MediaPlaylist | null | undefined): boolean; - protected playlistLoaded(index: number, data: LevelLoadedData | AudioTrackLoadedData | TrackLoadedData, previousDetails?: LevelDetails): void; - private getDeliveryDirectives; - protected checkRetry(errorEvent: ErrorData): boolean; -} - -export declare class BaseSegment { - private _byteRange; - private _url; - readonly baseurl: string; - relurl?: string; - elementaryStreams: ElementaryStreams; - constructor(baseurl: string); - setByteRange(value: string, previous?: BaseSegment): void; - get byteRange(): [number, number] | []; - get byteRangeStartOffset(): number | undefined; - get byteRangeEndOffset(): number | undefined; - get url(): string; - set url(value: string); - clearElementaryStreamInfo(): void; -} - -export declare class BaseStreamController extends TaskLoop implements NetworkComponentAPI { - protected hls: Hls; - protected fragPrevious: MediaFragment | null; - protected fragCurrent: Fragment | null; - protected fragmentTracker: FragmentTracker; - protected transmuxer: TransmuxerInterface | null; - protected _state: string; - protected playlistType: PlaylistLevelType; - protected media: HTMLMediaElement | null; - protected mediaBuffer: Bufferable | null; - protected config: HlsConfig; - protected bitrateTest: boolean; - protected lastCurrentTime: number; - protected nextLoadPosition: number; - protected startPosition: number; - protected startTimeOffset: number | null; - protected retryDate: number; - protected levels: Array | null; - protected fragmentLoader: FragmentLoader; - protected keyLoader: KeyLoader; - protected levelLastLoaded: Level | null; - protected startFragRequested: boolean; - protected decrypter: Decrypter; - protected initPTS: RationalTimestamp[]; - protected buffering: boolean; - protected loadingParts: boolean; - private loopSn?; - constructor(hls: Hls, fragmentTracker: FragmentTracker, keyLoader: KeyLoader, logPrefix: string, playlistType: PlaylistLevelType); - protected registerListeners(): void; - protected unregisterListeners(): void; - protected doTick(): void; - protected onTickEnd(): void; - startLoad(startPosition: number): void; - stopLoad(): void; - get startPositionValue(): number; - get bufferingEnabled(): boolean; - pauseBuffering(): void; - resumeBuffering(): void; - protected _streamEnded(bufferInfo: BufferInfo, levelDetails: LevelDetails): boolean; - getLevelDetails(): LevelDetails | undefined; - protected onMediaAttached(event: Events.MEDIA_ATTACHED, data: MediaAttachedData): void; - protected onMediaDetaching(event: Events.MEDIA_DETACHING, data: MediaDetachingData): void; - protected onManifestLoading(): void; - protected onError(event: Events.ERROR, data: ErrorData): void; - protected onMediaSeeking: () => void; - protected onMediaEnded: () => void; - protected triggerEnded(): void; - protected onManifestLoaded(event: Events.MANIFEST_LOADED, data: ManifestLoadedData): void; - protected onHandlerDestroying(): void; - protected onHandlerDestroyed(): void; - protected loadFragment(frag: Fragment, level: Level, targetBufferTime: number): void; - private _loadFragForPlayback; - protected clearTrackerIfNeeded(frag: Fragment): void; - protected checkLiveUpdate(details: LevelDetails): void; - protected flushMainBuffer(startOffset: number, endOffset: number, type?: SourceBufferName | null): void; - protected _loadInitSegment(frag: Fragment, level: Level): void; - private completeInitSegmentLoad; - protected fragContextChanged(frag: Fragment | null): boolean; - protected fragBufferedComplete(frag: Fragment, part: Part | null): void; - protected _handleFragmentLoadComplete(fragLoadedEndData: PartsLoadedData): void; - protected _handleFragmentLoadProgress(frag: PartsLoadedData | FragLoadedData): void; - protected _doFragLoad(frag: Fragment, level: Level, targetBufferTime?: number | null, progressCallback?: FragmentLoadProgressCallback): Promise; - private doFragPartsLoad; - private handleFragLoadError; - protected _handleTransmuxerFlush(chunkMeta: ChunkMetadata): void; - private shouldLoadParts; - protected getCurrentContext(chunkMeta: ChunkMetadata): { - frag: MediaFragment; - part: Part | null; - level: Level; - } | null; - protected bufferFragmentData(data: RemuxedTrack, frag: Fragment, part: Part | null, chunkMeta: ChunkMetadata, noBacktracking?: boolean): void; - protected flushBufferGap(frag: Fragment): void; - protected getFwdBufferInfo(bufferable: Bufferable | null, type: PlaylistLevelType): BufferInfo | null; - private getFwdBufferInfoAtPos; - protected getMaxBufferLength(levelBitrate?: number): number; - protected reduceMaxBufferLength(threshold: number, fragDuration: number): boolean; - protected getAppendedFrag(position: number, playlistType?: PlaylistLevelType): Fragment | null; - protected getNextFragment(pos: number, levelDetails: LevelDetails): Fragment | null; - protected isLoopLoading(frag: Fragment, targetBufferTime: number): boolean; - protected getNextFragmentLoopLoading(frag: Fragment, levelDetails: LevelDetails, bufferInfo: BufferInfo, playlistType: PlaylistLevelType, maxBufLen: number): Fragment | null; - mapToInitFragWhenRequired(frag: Fragment | null): typeof frag; - getNextPart(partList: Part[], frag: Fragment, targetBufferTime: number): number; - private loadedEndOfParts; - protected getInitialLiveFragment(levelDetails: LevelDetails, fragments: MediaFragment[]): MediaFragment | null; - protected getFragmentAtPosition(bufferEnd: number, end: number, levelDetails: LevelDetails): MediaFragment | null; - protected alignPlaylists(details: LevelDetails, previousDetails: LevelDetails | undefined, switchDetails: LevelDetails | undefined): number; - protected waitForCdnTuneIn(details: LevelDetails): boolean | 0; - protected setStartPosition(details: LevelDetails, sliding: number): void; - protected getLoadPosition(): number; - private handleFragLoadAborted; - protected resetFragmentLoading(frag: Fragment): void; - protected onFragmentOrKeyLoadError(filterType: PlaylistLevelType, data: ErrorData): void; - protected reduceLengthAndFlushBuffer(data: ErrorData): boolean; - protected resetFragmentErrors(filterType: PlaylistLevelType): void; - protected afterBufferFlushed(media: Bufferable, bufferType: SourceBufferName, playlistType: PlaylistLevelType): void; - protected resetLoadingState(): void; - protected resetStartWhenNotLoaded(level: Level | null): void; - protected resetWhenMissingContext(chunkMeta: ChunkMetadata): void; - protected removeUnbufferedFrags(start?: number): void; - private updateLevelTiming; - private playlistLabel; - private fragInfo; - protected resetTransmuxer(): void; - protected recoverWorkerError(data: ErrorData): void; - set state(nextState: string); - get state(): string; -} - -export declare interface BaseTrack { - id: 'audio' | 'main'; - container: string; - codec?: string; - levelCodec?: string; - pendingCodec?: string; - metadata?: { - channelCount?: number; - width?: number; - height?: number; - }; -} - -export declare type BaseTrackSet = Partial>; - -export declare type Bufferable = { - buffered: TimeRanges; -}; - -export declare interface BufferAppendedData { - type: SourceBufferName; - frag: Fragment; - part: Part | null; - chunkMeta: ChunkMetadata; - parent: PlaylistLevelType; - timeRanges: Partial>; -} - -export declare interface BufferAppendingData { - type: SourceBufferName; - frag: Fragment; - part: Part | null; - chunkMeta: ChunkMetadata; - parent: PlaylistLevelType; - data: Uint8Array; -} - -export declare interface BufferCodecsData { - video?: ParsedTrack; - audio?: ParsedTrack; - audiovideo?: ParsedTrack; - tracks?: BaseTrackSet; -} - -export declare class BufferController extends Logger implements ComponentAPI { - private hls; - private fragmentTracker; - private details; - private _objectUrl; - private operationQueue; - private bufferCodecEventsTotal; - media: HTMLMediaElement | null; - mediaSource: MediaSource | null; - private lastMpegAudioChunk; - private blockedAudioAppend; - private lastVideoAppendEnd; - private appendSource; - private transferData?; - private overrides?; - private appendErrors; - private tracks; - private sourceBuffers; - constructor(hls: Hls, fragmentTracker: FragmentTracker); - hasSourceTypes(): boolean; - destroy(): void; - protected registerListeners(): void; - protected unregisterListeners(): void; - transferMedia(): AttachMediaSourceData | null; - private initTracks; - private onManifestLoading; - protected onManifestParsed(event: Events.MANIFEST_PARSED, data: ManifestParsedData): void; - protected onMediaAttaching(event: Events.MEDIA_ATTACHING, data: MediaAttachingData): void; - private assignMediaSource; - private attachTransferred; - private _onEndStreaming; - private _onStartStreaming; - protected onMediaDetaching(event: Events.MEDIA_DETACHING, data: MediaDetachingData): void; - protected onBufferReset(): void; - private resetBuffer; - private removeBuffer; - private resetQueue; - protected onBufferCodecs(event: Events.BUFFER_CODECS, data: BufferCodecsData): void; - get sourceBufferTracks(): BaseTrackSet; - protected appendChangeType(type: SourceBufferName, container: string, codec: string): void; - private blockAudio; - private unblockAudio; - protected onBufferAppending(event: Events.BUFFER_APPENDING, eventData: BufferAppendingData): void; - private getFlushOp; - protected onBufferFlushing(event: Events.BUFFER_FLUSHING, data: BufferFlushingData): void; - protected onFragParsed(event: Events.FRAG_PARSED, data: FragParsedData): void; - private onFragChanged; - get bufferedToEnd(): boolean; - protected onBufferEos(event: Events.BUFFER_EOS, data: BufferEOSData): void; - protected onLevelUpdated(event: Events.LEVEL_UPDATED, { details }: LevelUpdatedData): void; - private updateDuration; - private onError; - private resetAppendErrors; - private trimBuffers; - private flushBackBuffer; - private flushFrontBuffer; - /** - * Update Media Source duration to current level duration or override to Infinity if configuration parameter - * 'liveDurationInfinity` is set to `true` - * More details: https://github.com/video-dev/hls.js/issues/355 - */ - private getDurationAndRange; - private updateMediaSource; - private get tracksReady(); - protected checkPendingTracks(): void; - private bufferCreated; - private createSourceBuffers; - private getTrackCodec; - private trackSourceBuffer; - private _onMediaSourceOpen; - private _onMediaSourceClose; - private _onMediaSourceEnded; - private _onMediaEmptied; - private get mediaSrc(); - private onSBUpdateStart; - private onSBUpdateEnd; - private onSBUpdateError; - private removeExecutor; - private appendExecutor; - private blockUntilOpen; - private isUpdating; - private isQueued; - private isPending; - private blockBuffers; - private stepOperationQueue; - private append; - private appendBlocker; - private currentOp; - private executeNext; - private shiftAndExecuteNext; - private get pendingTrackCount(); - private get sourceBufferCount(); - private get sourceBufferTypes(); - private addBufferListener; - private removeBufferListeners; -} - -export declare type BufferControllerConfig = { - appendErrorMaxRetry: number; - backBufferLength: number; - frontBufferFlushThreshold: number; - liveDurationInfinity: boolean; - /** - * @deprecated use backBufferLength - */ - liveBackBufferLength: number | null; -}; - -export declare interface BufferCreatedData { - tracks: BufferCreatedTrackSet; -} - -export declare interface BufferCreatedTrack extends BaseTrack { - buffer: ExtendedSourceBuffer; -} - -export declare type BufferCreatedTrackSet = Partial>; - -export declare interface BufferEOSData { - type?: SourceBufferName; -} - -export declare interface BufferFlushedData { - type: SourceBufferName; -} - -export declare interface BufferFlushingData { - startOffset: number; - endOffset: number; - endOffsetSubtitles?: number; - type: SourceBufferName | null; -} - -export declare type BufferInfo = { - len: number; - start: number; - end: number; - nextStart?: number; -}; - -export declare class CapLevelController implements ComponentAPI { - private hls; - private autoLevelCapping; - private firstLevel; - private media; - private restrictedLevels; - private timer; - private clientRect; - private streamController?; - constructor(hls: Hls); - setStreamController(streamController: StreamController): void; - destroy(): void; - protected registerListeners(): void; - protected unregisterListener(): void; - protected onFpsDropLevelCapping(event: Events.FPS_DROP_LEVEL_CAPPING, data: FPSDropLevelCappingData): void; - protected onMediaAttaching(event: Events.MEDIA_ATTACHING, data: MediaAttachingData): void; - protected onManifestParsed(event: Events.MANIFEST_PARSED, data: ManifestParsedData): void; - private onLevelsUpdated; - protected onBufferCodecs(event: Events.BUFFER_CODECS, data: BufferCodecsData): void; - protected onMediaDetaching(): void; - detectPlayerSize(): void; - getMaxLevel(capLevelIndex: number): number; - startCapping(): void; - stopCapping(): void; - getDimensions(): { - width: number; - height: number; - }; - get mediaWidth(): number; - get mediaHeight(): number; - get contentScaleFactor(): number; - private isLevelAllowed; - static getMaxLevelByMediaSize(levels: Array, width: number, height: number): number; -} - -export declare type CapLevelControllerConfig = { - capLevelToPlayerSize: boolean; -}; - -/** - * Keep a CEA-608 screen of 32x15 styled characters - * @constructor - */ -export declare class CaptionScreen { - rows: Row[]; - currRow: number; - nrRollUpRows: number | null; - lastOutputScreen: CaptionScreen | null; - logger: CaptionsLogger; - constructor(logger: CaptionsLogger); - reset(): void; - equals(other: CaptionScreen): boolean; - copy(other: CaptionScreen): void; - isEmpty(): boolean; - backSpace(): void; - clearToEndOfRow(): void; - /** - * Insert a character (without styling) in the current row. - */ - insertChar(char: number): void; - setPen(styles: Partial): void; - moveCursor(relPos: number): void; - setCursor(absPos: number): void; - setPAC(pacData: PACData): void; - /** - * Set background/extra foreground, but first do back_space, and then insert space (backwards compatibility). - */ - setBkgData(bkgData: Partial): void; - setRollUpRows(nrRows: number | null): void; - rollUp(): void; - /** - * Get all non-empty rows with as unicode text. - */ - getDisplayText(asOneRow?: boolean): string; - getTextAndFormat(): Row[]; -} - -declare class CaptionsLogger { - time: number | null; - verboseLevel: VerboseLevel; - log(severity: VerboseLevel, msg: string | (() => string)): void; -} - -export declare class ChunkMetadata { - readonly level: number; - readonly sn: number; - readonly part: number; - readonly id: number; - readonly size: number; - readonly partial: boolean; - readonly transmuxing: HlsChunkPerformanceTiming; - readonly buffering: { - [key in SourceBufferName]: HlsChunkPerformanceTiming; - }; - constructor(level: number, sn: number, id: number, size?: number, part?: number, partial?: boolean); -} - -/** - * Controller to deal with Common Media Client Data (CMCD) - * @see https://cdn.cta.tech/cta/media/media/resources/standards/pdfs/cta-5004-final.pdf - */ -export declare class CMCDController implements ComponentAPI { - private hls; - private config; - private media?; - private sid?; - private cid?; - private useHeaders; - private includeKeys?; - private initialized; - private starved; - private buffering; - private audioBuffer?; - private videoBuffer?; - constructor(hls: Hls); - private registerListeners; - private unregisterListeners; - destroy(): void; - private onMediaAttached; - private onMediaDetached; - private onBufferCreated; - private onWaiting; - private onPlaying; - /** - * Create baseline CMCD data - */ - private createData; - /** - * Apply CMCD data to a request. - */ - private apply; - /** - * Apply CMCD data to a manifest request. - */ - private applyPlaylistData; - /** - * Apply CMCD data to a segment request - */ - private applyFragmentData; - private getNextFrag; - private getNextPart; - /** - * The CMCD object type. - */ - private getObjectType; - /** - * Get the highest bitrate. - */ - private getTopBandwidth; - /** - * Get the buffer length for a media type in milliseconds - */ - private getBufferLength; - /** - * Create a playlist loader - */ - private createPlaylistLoader; - /** - * Create a playlist loader - */ - private createFragmentLoader; -} - -export declare type CMCDControllerConfig = { - sessionId?: string; - contentId?: string; - useHeaders?: boolean; - includeKeys?: string[]; -}; - -export declare interface ComponentAPI { - destroy(): void; -} - -export declare class ContentSteeringController extends Logger implements NetworkComponentAPI { - private readonly hls; - private loader; - private uri; - private pathwayId; - private _pathwayPriority; - private timeToLoad; - private reloadTimer; - private updated; - private started; - private enabled; - private levels; - private audioTracks; - private subtitleTracks; - private penalizedPathways; - constructor(hls: Hls); - private registerListeners; - private unregisterListeners; - pathways(): string[]; - get pathwayPriority(): string[] | null; - set pathwayPriority(pathwayPriority: string[]); - startLoad(): void; - stopLoad(): void; - clearTimeout(): void; - destroy(): void; - removeLevel(levelToRemove: Level): void; - private onManifestLoading; - private onManifestLoaded; - private onManifestParsed; - private onError; - filterParsedLevels(levels: Level[]): Level[]; - private getLevelsForPathway; - private updatePathwayPriority; - private getPathwayForGroupId; - private clonePathways; - private loadSteeringManifest; - private scheduleRefresh; -} - -export declare type ContentSteeringOptions = { - uri: string; - pathwayId: string; -}; - -export declare interface CuesInterface { - newCue(track: TextTrack | null, startTime: number, endTime: number, captionScreen: CaptionScreen): VTTCue[]; -} - -export declare interface CuesParsedData { - type: 'captions' | 'subtitles'; - cues: any; - track: string; -} - -export declare class DateRange { - attr: AttrList; - tagAnchor: Fragment | null; - tagOrder: number; - private _startDate; - private _endDate?; - private _dateAtEnd?; - private _cue?; - private _badValueForSameId?; - constructor(dateRangeAttr: AttrList, dateRangeWithSameId?: DateRange | undefined, tagCount?: number); - get id(): string; - get class(): string; - get cue(): DateRangeCue; - get startTime(): number; - get startDate(): Date; - get endDate(): Date | null; - get duration(): number | null; - get plannedDuration(): number | null; - get endOnNext(): boolean; - get isInterstitial(): boolean; - get isValid(): boolean; -} - -export declare type DateRangeCue = { - pre: boolean; - post: boolean; - once: boolean; -}; - -export declare 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 declare class Decrypter { - private logEnabled; - private removePKCS7Padding; - private subtle; - private softwareDecrypter; - private key; - private fastAesKey; - private remainderData; - private currentIV; - private currentResult; - private useSoftware; - private enableSoftwareAES; - constructor(config: HlsConfig, { removePKCS7Padding }?: { - removePKCS7Padding?: boolean | undefined; - }); - destroy(): void; - isSync(): boolean; - flush(): Uint8Array | null; - reset(): void; - decrypt(data: Uint8Array | ArrayBuffer, key: ArrayBuffer, iv: ArrayBuffer, aesMode: DecrypterAesMode): Promise; - softwareDecrypt(data: Uint8Array, key: ArrayBuffer, iv: ArrayBuffer, aesMode: DecrypterAesMode): ArrayBuffer | null; - webCryptoDecrypt(data: Uint8Array, key: ArrayBuffer, iv: ArrayBuffer, aesMode: DecrypterAesMode): Promise; - private onWebCryptoError; - private getValidChunk; - private logOnce; -} - -export declare const enum DecrypterAesMode { - cbc = 0, - ctr = 1 -} - -export declare type DRMSystemConfiguration = { - licenseUrl: string; - serverCertificateUrl?: string; - generateRequest?: (this: Hls, initDataType: string, initData: ArrayBuffer | null, keyContext: MediaKeySessionContext) => { - initDataType: string; - initData: ArrayBuffer | null; - } | undefined | never; -}; - -export declare type DRMSystemOptions = { - audioRobustness?: string; - videoRobustness?: string; - audioEncryptionScheme?: string | null; - videoEncryptionScheme?: string | null; - persistentState?: MediaKeysRequirement; - distinctiveIdentifier?: MediaKeysRequirement; - sessionTypes?: string[]; - sessionType?: string; -}; - -export declare type DRMSystemsConfiguration = Partial>; - -export declare interface ElementaryStreamInfo { - startPTS: number; - endPTS: number; - startDTS: number; - endDTS: number; - partial?: boolean; -} - -export declare type ElementaryStreams = Record; - -export declare const enum ElementaryStreamTypes { - AUDIO = "audio", - VIDEO = "video", - AUDIOVIDEO = "audiovideo" -} - -/** - * Controller to deal with encrypted media extensions (EME) - * @see https://developer.mozilla.org/en-US/docs/Web/API/Encrypted_Media_Extensions_API - * - * @class - * @constructor - */ -export declare class EMEController extends Logger implements ComponentAPI { - static CDMCleanupPromise: Promise | void; - private readonly hls; - private readonly config; - private media; - private keyFormatPromise; - private keySystemAccessPromises; - private _requestLicenseFailureCount; - private mediaKeySessions; - private keyIdToKeySessionPromise; - private setMediaKeysQueue; - constructor(hls: Hls); - destroy(): void; - private registerListeners; - private unregisterListeners; - private getLicenseServerUrl; - private getLicenseServerUrlOrThrow; - private getServerCertificateUrl; - private attemptKeySystemAccess; - private requestMediaKeySystemAccess; - private getMediaKeysPromise; - private createMediaKeySessionContext; - private renewKeySession; - private getKeyIdString; - private updateKeySession; - selectKeySystemFormat(frag: Fragment): Promise; - private getKeyFormatPromise; - loadKey(data: KeyLoadedData): Promise; - private throwIfDestroyed; - private handleError; - private getKeySystemForKeyPromise; - private getKeySystemSelectionPromise; - private onMediaEncrypted; - private onWaitingForKey; - private attemptSetMediaKeys; - private generateRequestWithPreferredKeySession; - private onKeyStatusChange; - private fetchServerCertificate; - private setMediaKeysServerCertificate; - private renewLicense; - private unpackPlayReadyKeyMessage; - private setupLicenseXHR; - private requestLicense; - private onMediaAttached; - private onMediaDetached; - private onManifestLoading; - private onManifestLoaded; - private removeSession; -} - -export declare type EMEControllerConfig = { - licenseXhrSetup?: (this: Hls, xhr: XMLHttpRequest, url: string, keyContext: MediaKeySessionContext, licenseChallenge: Uint8Array) => void | Uint8Array | Promise; - licenseResponseCallback?: (this: Hls, xhr: XMLHttpRequest, url: string, keyContext: MediaKeySessionContext) => ArrayBuffer; - emeEnabled: boolean; - widevineLicenseUrl?: string; - drmSystems: DRMSystemsConfiguration; - drmSystemOptions: DRMSystemOptions; - requestMediaKeySystemAccessFunc: MediaKeyFunc | null; -}; - -export declare const enum ErrorActionFlags { - None = 0, - MoveAllAlternatesMatchingHost = 1, - MoveAllAlternatesMatchingHDCP = 2, - SwitchToSDR = 4 -} - -export declare class ErrorController extends Logger implements NetworkComponentAPI { - private readonly hls; - private playlistError; - private penalizedRenditions; - constructor(hls: Hls); - private registerListeners; - private unregisterListeners; - destroy(): void; - startLoad(startPosition: number): void; - stopLoad(): void; - private getVariantLevelIndex; - private onManifestLoading; - private onLevelUpdated; - private onError; - private keySystemError; - private getPlaylistRetryOrSwitchAction; - private getFragRetryOrSwitchAction; - private getLevelSwitchAction; - onErrorOut(event: Events.ERROR, data: ErrorData): void; - private sendAlternateToPenaltyBox; - private switchLevel; -} - -export declare interface ErrorData { - type: ErrorTypes; - details: ErrorDetails; - error: Error; - fatal: boolean; - errorAction?: IErrorAction; - buffer?: number; - bytes?: number; - chunkMeta?: ChunkMetadata; - context?: PlaylistLoaderContext; - event?: keyof HlsListeners | 'demuxerWorker'; - frag?: Fragment; - part?: Part | null; - level?: number | undefined; - levelRetry?: boolean; - loader?: Loader; - networkDetails?: any; - stats?: LoaderStats; - mimeType?: string; - reason?: string; - response?: LoaderResponse; - url?: string; - parent?: PlaylistLevelType; - sourceBufferName?: SourceBufferName; - interstitial?: InterstitialEvent; - /** - * @deprecated Use ErrorData.error - */ - err?: { - message: string; - }; -} - -export declare 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_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", - MANIFEST_LOAD_ERROR = "manifestLoadError", - MANIFEST_LOAD_TIMEOUT = "manifestLoadTimeOut", - MANIFEST_PARSING_ERROR = "manifestParsingError", - MANIFEST_INCOMPATIBLE_CODECS_ERROR = "manifestIncompatibleCodecsError", - LEVEL_EMPTY_ERROR = "levelEmptyError", - LEVEL_LOAD_ERROR = "levelLoadError", - LEVEL_LOAD_TIMEOUT = "levelLoadTimeOut", - LEVEL_PARSING_ERROR = "levelParsingError", - LEVEL_SWITCH_ERROR = "levelSwitchError", - AUDIO_TRACK_LOAD_ERROR = "audioTrackLoadError", - AUDIO_TRACK_LOAD_TIMEOUT = "audioTrackLoadTimeOut", - SUBTITLE_LOAD_ERROR = "subtitleTrackLoadError", - SUBTITLE_TRACK_LOAD_TIMEOUT = "subtitleTrackLoadTimeOut", - FRAG_LOAD_ERROR = "fragLoadError", - FRAG_LOAD_TIMEOUT = "fragLoadTimeOut", - FRAG_DECRYPT_ERROR = "fragDecryptError", - FRAG_PARSING_ERROR = "fragParsingError", - FRAG_GAP = "fragGap", - REMUX_ALLOC_ERROR = "remuxAllocError", - KEY_LOAD_ERROR = "keyLoadError", - KEY_LOAD_TIMEOUT = "keyLoadTimeOut", - BUFFER_ADD_CODEC_ERROR = "bufferAddCodecError", - BUFFER_INCOMPATIBLE_CODECS_ERROR = "bufferIncompatibleCodecsError", - BUFFER_APPEND_ERROR = "bufferAppendError", - BUFFER_APPENDING_ERROR = "bufferAppendingError", - BUFFER_STALLED_ERROR = "bufferStalledError", - BUFFER_FULL_ERROR = "bufferFullError", - BUFFER_SEEK_OVER_HOLE = "bufferSeekOverHole", - BUFFER_NUDGE_ON_STALL = "bufferNudgeOnStall", - ASSET_LIST_LOAD_ERROR = "assetListLoadError", - ASSET_LIST_LOAD_TIMEOUT = "assetListLoadTimeout", - ASSET_LIST_PARSING_ERROR = "assetListParsingError", - INTERSTITIAL_ASSET_ITEM_ERROR = "interstitialAssetItemError", - INTERNAL_EXCEPTION = "internalException", - INTERNAL_ABORTED = "aborted", - ATTACH_MEDIA_ERROR = "attachMediaError", - UNKNOWN = "unknown" -} - -export declare enum ErrorTypes { - NETWORK_ERROR = "networkError", - MEDIA_ERROR = "mediaError", - KEY_SYSTEM_ERROR = "keySystemError", - MUX_ERROR = "muxError", - OTHER_ERROR = "otherError" -} - -export declare enum Events { - MEDIA_ATTACHING = "hlsMediaAttaching", - MEDIA_ATTACHED = "hlsMediaAttached", - MEDIA_DETACHING = "hlsMediaDetaching", - MEDIA_DETACHED = "hlsMediaDetached", - MEDIA_ENDED = "hlsMediaEnded", - BUFFER_RESET = "hlsBufferReset", - BUFFER_CODECS = "hlsBufferCodecs", - BUFFER_CREATED = "hlsBufferCreated", - BUFFER_APPENDING = "hlsBufferAppending", - BUFFER_APPENDED = "hlsBufferAppended", - BUFFER_EOS = "hlsBufferEos", - BUFFERED_TO_END = "hlsBufferedToEnd", - BUFFER_FLUSHING = "hlsBufferFlushing", - BUFFER_FLUSHED = "hlsBufferFlushed", - MANIFEST_LOADING = "hlsManifestLoading", - MANIFEST_LOADED = "hlsManifestLoaded", - MANIFEST_PARSED = "hlsManifestParsed", - LEVEL_SWITCHING = "hlsLevelSwitching", - LEVEL_SWITCHED = "hlsLevelSwitched", - LEVEL_LOADING = "hlsLevelLoading", - LEVEL_LOADED = "hlsLevelLoaded", - LEVEL_UPDATED = "hlsLevelUpdated", - LEVEL_PTS_UPDATED = "hlsLevelPtsUpdated", - LEVELS_UPDATED = "hlsLevelsUpdated", - AUDIO_TRACKS_UPDATED = "hlsAudioTracksUpdated", - AUDIO_TRACK_SWITCHING = "hlsAudioTrackSwitching", - AUDIO_TRACK_SWITCHED = "hlsAudioTrackSwitched", - AUDIO_TRACK_LOADING = "hlsAudioTrackLoading", - AUDIO_TRACK_LOADED = "hlsAudioTrackLoaded", - AUDIO_TRACK_UPDATED = "hlsAudioTrackUpdated", - SUBTITLE_TRACKS_UPDATED = "hlsSubtitleTracksUpdated", - SUBTITLE_TRACKS_CLEARED = "hlsSubtitleTracksCleared", - SUBTITLE_TRACK_SWITCH = "hlsSubtitleTrackSwitch", - SUBTITLE_TRACK_LOADING = "hlsSubtitleTrackLoading", - SUBTITLE_TRACK_LOADED = "hlsSubtitleTrackLoaded", - SUBTITLE_TRACK_UPDATED = "hlsSubtitleTrackUpdated", - SUBTITLE_FRAG_PROCESSED = "hlsSubtitleFragProcessed", - CUES_PARSED = "hlsCuesParsed", - NON_NATIVE_TEXT_TRACKS_FOUND = "hlsNonNativeTextTracksFound", - INIT_PTS_FOUND = "hlsInitPtsFound", - FRAG_LOADING = "hlsFragLoading", - FRAG_LOAD_EMERGENCY_ABORTED = "hlsFragLoadEmergencyAborted", - FRAG_LOADED = "hlsFragLoaded", - FRAG_DECRYPTED = "hlsFragDecrypted", - FRAG_PARSING_INIT_SEGMENT = "hlsFragParsingInitSegment", - FRAG_PARSING_USERDATA = "hlsFragParsingUserdata", - FRAG_PARSING_METADATA = "hlsFragParsingMetadata", - FRAG_PARSED = "hlsFragParsed", - FRAG_BUFFERED = "hlsFragBuffered", - FRAG_CHANGED = "hlsFragChanged", - FPS_DROP = "hlsFpsDrop", - FPS_DROP_LEVEL_CAPPING = "hlsFpsDropLevelCapping", - MAX_AUTO_LEVEL_UPDATED = "hlsMaxAutoLevelUpdated", - ERROR = "hlsError", - DESTROYING = "hlsDestroying", - KEY_LOADING = "hlsKeyLoading", - KEY_LOADED = "hlsKeyLoaded", - LIVE_BACK_BUFFER_REACHED = "hlsLiveBackBufferReached", - BACK_BUFFER_REACHED = "hlsBackBufferReached", - STEERING_MANIFEST_LOADED = "hlsSteeringManifestLoaded", - ASSET_LIST_LOADING = "hlsAssetListLoading", - ASSET_LIST_LOADED = "hlsAssetListLoaded", - INTERSTITIALS_UPDATED = "hlsInterstitialsUpdated", - INTERSTITIALS_BUFFERED_TO_BOUNDARY = "hlsInterstitialsBufferedToBoundary", - INTERSTITIAL_ASSET_PLAYER_CREATED = "hlsInterstitialAssetPlayerCreated", - INTERSTITIAL_STARTED = "hlsInterstitialStarted", - INTERSTITIAL_ASSET_STARTED = "hlsInterstitialAssetStarted", - INTERSTITIAL_ASSET_ENDED = "hlsInterstitialAssetEnded", - INTERSTITIAL_ASSET_ERROR = "hlsInterstitialAssetError", - INTERSTITIAL_ENDED = "hlsInterstitialEnded", - INTERSTITIALS_PRIMARY_RESUMED = "hlsInterstitialsPrimaryResumed", - PLAYOUT_LIMIT_REACHED = "hlsPlayoutLimitReached" -} - -export declare class EwmaBandWidthEstimator { - private defaultEstimate_; - private minWeight_; - private minDelayMs_; - private slow_; - private fast_; - private defaultTTFB_; - private ttfb_; - constructor(slow: number, fast: number, defaultEstimate: number, defaultTTFB?: number); - update(slow: number, fast: number): void; - sample(durationMs: number, numBytes: number): void; - sampleTTFB(ttfb: number): void; - canEstimate(): boolean; - getEstimate(): number; - getEstimateTTFB(): number; - get defaultEstimate(): number; - destroy(): void; -} - -export declare type ExtendedSourceBuffer = SourceBuffer & { - onbufferedchange?: ((this: SourceBuffer, ev: Event) => any) | null; -}; - -export declare class FPSController implements ComponentAPI { - private hls; - private isVideoPlaybackQualityAvailable; - private timer?; - private media; - private lastTime; - private lastDroppedFrames; - private lastDecodedFrames; - private streamController; - constructor(hls: Hls); - setStreamController(streamController: StreamController): void; - protected registerListeners(): void; - protected unregisterListeners(): void; - destroy(): void; - protected onMediaAttaching(event: Events.MEDIA_ATTACHING, data: MediaAttachingData): void; - private onMediaDetaching; - checkFPS(video: HTMLVideoElement, decodedFrames: number, droppedFrames: number): void; - checkFPSInterval(): void; -} - -export declare type FPSControllerConfig = { - capLevelOnFPSDrop: boolean; - fpsDroppedMonitoringPeriod: number; - fpsDroppedMonitoringThreshold: number; -}; - -export declare interface FPSDropData { - currentDropped: number; - currentDecoded: number; - totalDroppedFrames: number; -} - -export declare interface FPSDropLevelCappingData { - droppedLevel: number; - level: number; -} - -export declare interface FragBufferedData { - stats: LoadStats; - frag: Fragment; - part: Part | null; - id: string; -} - -export declare interface FragChangedData { - frag: Fragment; -} - -export declare interface FragDecryptedData { - frag: Fragment; - payload: ArrayBuffer; - stats: { - tstart: number; - tdecrypt: number; - }; -} - -export declare interface FragLoadedData { - frag: Fragment; - part: Part | null; - payload: ArrayBuffer; - networkDetails: unknown; -} - -export declare interface FragLoadEmergencyAbortedData { - frag: Fragment; - part: Part | null; - stats: LoaderStats; -} - -export declare interface FragLoadFailResult extends ErrorData { - frag: Fragment; - part?: Part; - response?: { - data: any; - code: number; - text: string; - url: string; - }; - networkDetails: any; -} - -export declare interface FragLoadingData { - frag: Fragment; - part?: Part; - targetBufferTime: number | null; -} - -/** - * Object representing parsed data from an HLS Segment. Found in {@link hls.js#LevelDetails.fragments}. - */ -export declare class Fragment extends BaseSegment { - private _decryptdata; - rawProgramDateTime: string | null; - programDateTime: number | null; - tagList: Array; - duration: number; - sn: number | 'initSegment'; - levelkeys?: { - [key: string]: LevelKey; - }; - readonly type: PlaylistLevelType; - loader: Loader | null; - keyLoader: Loader | null; - level: number; - cc: number; - startPTS?: number; - endPTS?: number; - startDTS?: number; - endDTS?: number; - start: number; - playlistOffset: number; - deltaPTS?: number; - maxStartPTS?: number; - minEndPTS?: number; - stats: LoadStats; - data?: Uint8Array; - bitrateTest: boolean; - title: string | null; - initSegment: Fragment | null; - endList?: boolean; - gap?: boolean; - urlId: number; - constructor(type: PlaylistLevelType, baseurl: string); - get decryptdata(): LevelKey | null; - get end(): number; - get endProgramDateTime(): number | null; - get encrypted(): boolean; - setKeyFormat(keyFormat: KeySystemFormats): void; - abortRequests(): void; - setElementaryStreamInfo(type: ElementaryStreamTypes, startPTS: number, endPTS: number, startDTS: number, endDTS: number, partial?: boolean): void; -} - -export declare class FragmentLoader { - private readonly config; - private loader; - private partLoadTimeout; - constructor(config: HlsConfig); - destroy(): void; - abort(): void; - load(frag: Fragment, onProgress?: FragmentLoadProgressCallback): Promise; - loadPart(frag: Fragment, part: Part, onProgress: FragmentLoadProgressCallback): Promise; - private updateStatsFromPart; - private resetLoader; -} - -/** - * @deprecated use fragLoadPolicy.default - */ -export declare type FragmentLoaderConfig = { - fragLoadingTimeOut: number; - fragLoadingMaxRetry: number; - fragLoadingRetryDelay: number; - fragLoadingMaxRetryTimeout: number; -}; - -export declare interface FragmentLoaderConstructor { - new (confg: HlsConfig): Loader; -} - -export declare interface FragmentLoaderContext extends LoaderContext { - frag: Fragment; - part: Part | null; - resetIV?: boolean; -} - -export declare type FragmentLoadProgressCallback = (result: FragLoadedData | PartsLoadedData) => void; - -export declare const enum FragmentState { - NOT_LOADED = "NOT_LOADED", - APPENDING = "APPENDING", - PARTIAL = "PARTIAL", - OK = "OK" -} - -export declare class FragmentTracker implements ComponentAPI { - private activePartLists; - private endListFragments; - private fragments; - private timeRanges; - private bufferPadding; - private hls; - private hasGaps; - constructor(hls: Hls); - private _registerListeners; - private _unregisterListeners; - destroy(): void; - /** - * Return a Fragment or Part with an appended range that matches the position and levelType - * Otherwise, return null - */ - getAppendedFrag(position: number, levelType: PlaylistLevelType): Fragment | Part | null; - /** - * Return a buffered Fragment that matches the position and levelType. - * A buffered Fragment is one whose loading, parsing and appending is done (completed or "partial" meaning aborted). - * If not found any Fragment, return null - */ - getBufferedFrag(position: number, levelType: PlaylistLevelType): MediaFragment | null; - getFragAtPos(position: number, levelType: PlaylistLevelType, buffered?: boolean): MediaFragment | null; - /** - * Partial fragments effected by coded frame eviction will be removed - * The browser will unload parts of the buffer to free up memory for new buffer data - * Fragments will need to be reloaded when the buffer is freed up, removing partial fragments will allow them to reload(since there might be parts that are still playable) - */ - detectEvictedFragments(elementaryStream: SourceBufferName, timeRange: TimeRanges, playlistType: PlaylistLevelType, appendedPart?: Part | null, removeAppending?: boolean): void; - /** - * Checks if the fragment passed in is loaded in the buffer properly - * Partially loaded fragments will be registered as a partial fragment - */ - detectPartialFragments(data: FragBufferedData): void; - private removeParts; - fragBuffered(frag: MediaFragment, force?: true): void; - private getBufferedTimes; - /** - * Gets the partial fragment for a certain time - */ - getPartialFragment(time: number): Fragment | null; - isEndListAppended(type: PlaylistLevelType): boolean; - getState(fragment: Fragment): FragmentState; - private isTimeBuffered; - private onManifestLoading; - private onFragLoaded; - private onBufferAppended; - private onFragBuffered; - private hasFragment; - hasFragments(type?: PlaylistLevelType): boolean; - hasParts(type: PlaylistLevelType): boolean; - removeFragmentsInRange(start: number, end: number, playlistType: PlaylistLevelType, withGapOnly?: boolean, unbufferedOnly?: boolean): void; - removeFragment(fragment: Fragment): void; - removeAllFragments(): void; -} - -export declare interface FragParsedData { - frag: Fragment; - part: Part | null; -} - -export declare interface FragParsingInitSegmentData { -} - -export declare interface FragParsingMetadataData { - id: string; - frag: Fragment; - details: LevelDetails; - samples: MetadataSample[]; -} - -export declare interface FragParsingUserdataData { - id: string; - frag: Fragment; - details: LevelDetails; - samples: UserdataSample[]; -} - -export declare type HdcpLevel = (typeof HdcpLevels)[number]; - -export declare const HdcpLevels: readonly ["NONE", "TYPE-0", "TYPE-1", null]; - -/** - * The `Hls` class is the core of the HLS.js library used to instantiate player instances. - * @public - */ -declare class Hls implements HlsEventEmitter { - private static defaultConfig; - /** - * The runtime configuration used by the player. At instantiation this is combination of `hls.userConfig` merged over `Hls.DefaultConfig`. - */ - readonly config: HlsConfig; - /** - * The configuration object provided on player instantiation. - */ - readonly userConfig: Partial; - /** - * The logger functions used by this player instance, configured on player instantiation. - */ - readonly logger: ILogger; - private coreComponents; - private networkControllers; - private _emitter; - private _autoLevelCapping; - private _maxHdcpLevel; - private abrController; - private bufferController; - private capLevelController; - private latencyController; - private levelController; - private streamController; - private audioTrackController?; - private subtitleTrackController?; - private interstitialsController?; - private emeController?; - private cmcdController?; - private _media; - private _url; - private triggeringException?; - private _sessionId?; - /** - * Get the video-dev/hls.js package version. - */ - static get version(): string; - /** - * Check if the required MediaSource Extensions are available. - */ - static isMSESupported(): boolean; - /** - * Check if MediaSource Extensions are available and isTypeSupported checks pass for any baseline codecs. - */ - static isSupported(): boolean; - /** - * Get the MediaSource global used for MSE playback (ManagedMediaSource, MediaSource, or WebKitMediaSource). - */ - static getMediaSource(): typeof MediaSource | undefined; - static get Events(): typeof Events; - static get MetadataSchema(): typeof MetadataSchema; - static get ErrorTypes(): typeof ErrorTypes; - static get ErrorDetails(): typeof ErrorDetails; - /** - * Get the default configuration applied to new instances. - */ - static get DefaultConfig(): HlsConfig; - /** - * Replace the default configuration applied to new instances. - */ - static set DefaultConfig(defaultConfig: HlsConfig); - /** - * Creates an instance of an HLS client that can attach to exactly one `HTMLMediaElement`. - * @param userConfig - Configuration options applied over `Hls.DefaultConfig` - */ - constructor(userConfig?: Partial); - createController(ControllerClass: any, components: any): any; - on(event: E, listener: HlsListeners[E], context?: Context): void; - once(event: E, listener: HlsListeners[E], context?: Context): void; - removeAllListeners(event?: E | undefined): void; - off(event: E, listener?: HlsListeners[E] | undefined, context?: Context, once?: boolean | undefined): void; - listeners(event: E): HlsListeners[E][]; - emit(event: E, name: E, eventObject: Parameters[1]): boolean; - trigger(event: E, eventObject: Parameters[1]): boolean; - listenerCount(event: E): number; - /** - * Dispose of the instance - */ - destroy(): void; - /** - * Attaches Hls.js to a media element - */ - attachMedia(data: HTMLMediaElement | MediaAttachingData): void; - /** - * Detach Hls.js from the media - */ - detachMedia(): void; - /** - * Detach HTMLMediaElement, MediaSource, and SourceBuffers without reset, for attaching to another instance - */ - transferMedia(): AttachMediaSourceData | null; - /** - * Set the source URL. Can be relative or absolute. - */ - loadSource(url: string): void; - /** - * Gets the currently loaded URL - */ - get url(): string | null; - /** - * Whether or not enough has been buffered to seek to start position or use `media.currentTime` to determine next load position - */ - get hasEnoughToStart(): boolean; - /** - * Get the startPosition set on startLoad(position) or on autostart with config.startPosition - */ - get startPosition(): number; - /** - * Start loading data from the stream source. - * Depending on default config, client starts loading automatically when a source is set. - * - * @param startPosition - Set the start position to stream from. - * Defaults to -1 (None: starts from earliest point) - */ - startLoad(startPosition?: number, skipSeekToStartPosition?: boolean): void; - /** - * Stop loading of any stream data. - */ - stopLoad(): void; - /** - * Returns state of fragment loading toggled by calling `pauseBuffering()` and `resumeBuffering()`. - */ - get bufferingEnabled(): boolean; - /** - * Resumes stream controller segment loading after `pauseBuffering` has been called. - */ - resumeBuffering(): void; - /** - * Prevents stream controller from loading new segments until `resumeBuffering` is called. - * This allows for media buffering to be paused without interupting playlist loading. - */ - pauseBuffering(): void; - /** - * Swap through possible audio codecs in the stream (for example to switch from stereo to 5.1) - */ - swapAudioCodec(): void; - /** - * When the media-element fails, this allows to detach and then re-attach it - * as one call (convenience method). - * - * Automatic recovery of media-errors by this process is configurable. - */ - recoverMediaError(): void; - removeLevel(levelIndex: number): void; - /** - * @returns a UUID for this player instance - */ - get sessionId(): string; - /** - * @returns an array of levels (variants) sorted by HDCP-LEVEL, RESOLUTION (height), FRAME-RATE, CODECS, VIDEO-RANGE, and BANDWIDTH - */ - get levels(): Level[]; - get latestLevelDetails(): LevelDetails | null; - /** - * Index of quality level (variant) currently played - */ - get currentLevel(): number; - /** - * Set quality level index immediately. This will flush the current buffer to replace the quality asap. That means playback will interrupt at least shortly to re-buffer and re-sync eventually. Set to -1 for automatic level selection. - */ - set currentLevel(newLevel: number); - /** - * Index of next quality level loaded as scheduled by stream controller. - */ - get nextLevel(): number; - /** - * Set quality level index for next loaded data. - * This will switch the video quality asap, without interrupting playback. - * May abort current loading of data, and flush parts of buffer (outside currently played fragment region). - * @param newLevel - Pass -1 for automatic level selection - */ - set nextLevel(newLevel: number); - /** - * Return the quality level of the currently or last (of none is loaded currently) segment - */ - get loadLevel(): number; - /** - * Set quality level index for next loaded data in a conservative way. - * This will switch the quality without flushing, but interrupt current loading. - * Thus the moment when the quality switch will appear in effect will only be after the already existing buffer. - * @param newLevel - Pass -1 for automatic level selection - */ - set loadLevel(newLevel: number); - /** - * get next quality level loaded - */ - get nextLoadLevel(): number; - /** - * Set quality level of next loaded segment in a fully "non-destructive" way. - * Same as `loadLevel` but will wait for next switch (until current loading is done). - */ - set nextLoadLevel(level: number); - /** - * Return "first level": like a default level, if not set, - * falls back to index of first level referenced in manifest - */ - get firstLevel(): number; - /** - * Sets "first-level", see getter. - */ - set firstLevel(newLevel: number); - /** - * Return the desired start level for the first fragment that will be loaded. - * The default value of -1 indicates automatic start level selection. - * Setting hls.nextAutoLevel without setting a startLevel will result in - * the nextAutoLevel value being used for one fragment load. - */ - get startLevel(): number; - /** - * set start level (level of first fragment that will be played back) - * if not overrided by user, first level appearing in manifest will be used as start level - * if -1 : automatic start level selection, playback will start from level matching download bandwidth - * (determined from download of first segment) - */ - set startLevel(newLevel: number); - /** - * Whether level capping is enabled. - * Default value is set via `config.capLevelToPlayerSize`. - */ - get capLevelToPlayerSize(): boolean; - /** - * Enables or disables level capping. If disabled after previously enabled, `nextLevelSwitch` will be immediately called. - */ - set capLevelToPlayerSize(shouldStartCapping: boolean); - /** - * Capping/max level value that should be used by automatic level selection algorithm (`ABRController`) - */ - get autoLevelCapping(): number; - /** - * Returns the current bandwidth estimate in bits per second, when available. Otherwise, `NaN` is returned. - */ - get bandwidthEstimate(): number; - set bandwidthEstimate(abrEwmaDefaultEstimate: number); - get abrEwmaDefaultEstimate(): number; - /** - * get time to first byte estimate - * @type {number} - */ - get ttfbEstimate(): number; - /** - * Capping/max level value that should be used by automatic level selection algorithm (`ABRController`) - */ - set autoLevelCapping(newLevel: number); - get maxHdcpLevel(): HdcpLevel; - set maxHdcpLevel(value: HdcpLevel); - /** - * True when automatic level selection enabled - */ - get autoLevelEnabled(): boolean; - /** - * Level set manually (if any) - */ - get manualLevel(): number; - /** - * min level selectable in auto mode according to config.minAutoBitrate - */ - get minAutoLevel(): number; - /** - * max level selectable in auto mode according to autoLevelCapping - */ - get maxAutoLevel(): number; - get firstAutoLevel(): number; - /** - * next automatically selected quality level - */ - get nextAutoLevel(): number; - /** - * this setter is used to force next auto level. - * this is useful to force a switch down in auto mode: - * in case of load error on level N, hls.js can set nextAutoLevel to N-1 for example) - * forced value is valid for one fragment. upon successful frag loading at forced level, - * this value will be resetted to -1 by ABR controller. - */ - set nextAutoLevel(nextLevel: number); - /** - * get the datetime value relative to media.currentTime for the active level Program Date Time if present - */ - get playingDate(): Date | null; - get mainForwardBufferInfo(): BufferInfo | null; - get maxBufferLength(): number; - /** - * Find and select the best matching audio track, making a level switch when a Group change is necessary. - * Updates `hls.config.audioPreference`. Returns the selected track, or null when no matching track is found. - */ - setAudioOption(audioOption: MediaPlaylist | AudioSelectionOption | undefined): MediaPlaylist | null; - /** - * Find and select the best matching subtitle track, making a level switch when a Group change is necessary. - * Updates `hls.config.subtitlePreference`. Returns the selected track, or null when no matching track is found. - */ - setSubtitleOption(subtitleOption: MediaPlaylist | SubtitleSelectionOption | undefined): MediaPlaylist | null; - /** - * Get the complete list of audio tracks across all media groups - */ - get allAudioTracks(): MediaPlaylist[]; - /** - * Get the list of selectable audio tracks - */ - get audioTracks(): MediaPlaylist[]; - /** - * index of the selected audio track (index in audio track lists) - */ - get audioTrack(): number; - /** - * selects an audio track, based on its index in audio track lists - */ - set audioTrack(audioTrackId: number); - /** - * get the complete list of subtitle tracks across all media groups - */ - get allSubtitleTracks(): MediaPlaylist[]; - /** - * get alternate subtitle tracks list from playlist - */ - get subtitleTracks(): MediaPlaylist[]; - /** - * index of the selected subtitle track (index in subtitle track lists) - */ - get subtitleTrack(): number; - get media(): HTMLMediaElement | null; - /** - * select an subtitle track, based on its index in subtitle track lists - */ - set subtitleTrack(subtitleTrackId: number); - /** - * Whether subtitle display is enabled or not - */ - get subtitleDisplay(): boolean; - /** - * Enable/disable subtitle display rendering - */ - set subtitleDisplay(value: boolean); - /** - * get mode for Low-Latency HLS loading - */ - get lowLatencyMode(): boolean; - /** - * Enable/disable Low-Latency HLS part playlist and segment loading, and start live streams at playlist PART-HOLD-BACK rather than HOLD-BACK. - */ - set lowLatencyMode(mode: boolean); - /** - * Position (in seconds) of live sync point (ie edge of live position minus safety delay defined by ```hls.config.liveSyncDuration```) - * @returns null prior to loading live Playlist - */ - get liveSyncPosition(): number | null; - /** - * Estimated position (in seconds) of live edge (ie edge of live playlist plus time sync playlist advanced) - * @returns 0 before first playlist is loaded - */ - get latency(): number; - /** - * maximum distance from the edge before the player seeks forward to ```hls.liveSyncPosition``` - * configured using ```liveMaxLatencyDurationCount``` (multiple of target duration) or ```liveMaxLatencyDuration``` - * @returns 0 before first playlist is loaded - */ - get maxLatency(): number; - /** - * target distance from the edge as calculated by the latency controller - */ - get targetLatency(): number | null; - set targetLatency(latency: number); - /** - * the rate at which the edge of the current live playlist is advancing or 1 if there is none - */ - get drift(): number | null; - /** - * set to true when startLoad is called before MANIFEST_PARSED event - */ - get forceStartLoad(): boolean; - /** - * ContentSteering pathwayPriority getter/setter - */ - get pathwayPriority(): string[] | null; - set pathwayPriority(pathwayPriority: string[]); - /** - * returns true when all SourceBuffers are buffered to the end - */ - get bufferedToEnd(): boolean; - /** - * returns Interstitials Program Manager - */ - get interstitialsManager(): InterstitialsManager | null; - /** - * returns mediaCapabilities.decodingInfo for a variant/rendition - */ - getMediaDecodingInfo(level: Level, audioTracks?: MediaPlaylist[]): Promise; -} -export default Hls; - -export declare class HlsAssetPlayer { - readonly hls: Hls; - readonly interstitial: InterstitialEvent; - readonly assetItem: InterstitialAssetItem; - tracks: Partial | null; - private hasDetails; - private mediaAttached; - private playoutOffset; - constructor(HlsPlayerClass: typeof Hls, userConfig: Partial, interstitial: InterstitialEvent, assetItem: InterstitialAssetItem); - private checkPlayout; - get destroyed(): boolean; - get assetId(): InterstitialAssetId; - get interstitialId(): InterstitialId; - get media(): HTMLMediaElement | null; - get bufferedEnd(): number; - get currentTime(): number; - get duration(): number; - get remaining(): number; - get timelineOffset(): number; - set timelineOffset(value: number); - private getAssetTime; - private removeMediaListeners; - destroy(): void; - attachMedia(data: HTMLMediaElement | MediaAttachingData): void; - detachMedia(): void; - resumeBuffering(): void; - pauseBuffering(): void; - transferMedia(): AttachMediaSourceData | null; - on(event: E, listener: HlsListeners[E], context?: Context): void; - once(event: E, listener: HlsListeners[E], context?: Context): void; - off(event: E, listener: HlsListeners[E], context?: Context): void; - toString(): string; -} - -export declare interface HlsChunkPerformanceTiming extends HlsPerformanceTiming { - executeStart: number; - executeEnd: number; -} - -export declare type HlsConfig = { - debug: boolean | ILogger; - enableWorker: boolean; - workerPath: null | string; - enableSoftwareAES: boolean; - minAutoBitrate: number; - ignoreDevicePixelRatio: boolean; - preferManagedMediaSource: boolean; - timelineOffset?: number; - loader: { - new (confg: HlsConfig): Loader; - }; - fLoader?: FragmentLoaderConstructor; - pLoader?: PlaylistLoaderConstructor; - fetchSetup?: (context: LoaderContext, initParams: any) => Promise | Request; - xhrSetup?: (xhr: XMLHttpRequest, url: string) => Promise | void; - audioStreamController?: typeof AudioStreamController; - audioTrackController?: typeof AudioTrackController; - subtitleStreamController?: typeof SubtitleStreamController; - subtitleTrackController?: typeof SubtitleTrackController; - timelineController?: typeof TimelineController; - emeController?: typeof EMEController; - cmcd?: CMCDControllerConfig; - cmcdController?: typeof CMCDController; - contentSteeringController?: typeof ContentSteeringController; - interstitialsController?: typeof InterstitialsController; - enableInterstitialPlayback: boolean; - interstitialAppendInPlace: boolean; - interstitialLiveLookAhead: number; - assetPlayerId?: string; - useMediaCapabilities: boolean; - abrController: typeof AbrController; - bufferController: typeof BufferController; - capLevelController: typeof CapLevelController; - errorController: typeof ErrorController; - fpsController: typeof FPSController; - progressive: boolean; - lowLatencyMode: boolean; - primarySessionId?: string; -} & ABRControllerConfig & BufferControllerConfig & CapLevelControllerConfig & EMEControllerConfig & FPSControllerConfig & LevelControllerConfig & MP4RemuxerConfig & StreamControllerConfig & SelectionPreferences & LatencyControllerConfig & MetadataControllerConfig & TimelineControllerConfig & TSDemuxerConfig & HlsLoadPolicies & FragmentLoaderConfig & PlaylistLoaderConfig; - -export declare interface HlsEventEmitter { - on(event: E, listener: HlsListeners[E], context?: Context): void; - once(event: E, listener: HlsListeners[E], context?: Context): void; - removeAllListeners(event?: E): void; - off(event: E, listener?: HlsListeners[E], context?: Context, once?: boolean): void; - listeners(event: E): HlsListeners[E][]; - emit(event: E, name: E, eventObject: Parameters[1]): boolean; - listenerCount(event: E): number; -} - -/** - * Defines each Event type and payload by Event name. Used in {@link hls.js#HlsEventEmitter} to strongly type the event listener API. - */ -export declare interface HlsListeners { - [Events.MEDIA_ATTACHING]: (event: Events.MEDIA_ATTACHING, data: MediaAttachingData) => void; - [Events.MEDIA_ATTACHED]: (event: Events.MEDIA_ATTACHED, data: MediaAttachedData) => void; - [Events.MEDIA_DETACHING]: (event: Events.MEDIA_DETACHING, data: MediaDetachingData) => void; - [Events.MEDIA_DETACHED]: (event: Events.MEDIA_DETACHED, data: MediaDetachedData) => void; - [Events.MEDIA_ENDED]: (event: Events.MEDIA_ENDED, data: MediaEndedData) => void; - [Events.BUFFER_RESET]: (event: Events.BUFFER_RESET) => void; - [Events.BUFFER_CODECS]: (event: Events.BUFFER_CODECS, data: BufferCodecsData) => void; - [Events.BUFFER_CREATED]: (event: Events.BUFFER_CREATED, data: BufferCreatedData) => void; - [Events.BUFFER_APPENDING]: (event: Events.BUFFER_APPENDING, data: BufferAppendingData) => void; - [Events.BUFFER_APPENDED]: (event: Events.BUFFER_APPENDED, data: BufferAppendedData) => void; - [Events.BUFFER_EOS]: (event: Events.BUFFER_EOS, data: BufferEOSData) => void; - [Events.BUFFERED_TO_END]: (event: Events.BUFFERED_TO_END) => void; - [Events.BUFFER_FLUSHING]: (event: Events.BUFFER_FLUSHING, data: BufferFlushingData) => void; - [Events.BUFFER_FLUSHED]: (event: Events.BUFFER_FLUSHED, data: BufferFlushedData) => void; - [Events.MANIFEST_LOADING]: (event: Events.MANIFEST_LOADING, data: ManifestLoadingData) => void; - [Events.MANIFEST_LOADED]: (event: Events.MANIFEST_LOADED, data: ManifestLoadedData) => void; - [Events.MANIFEST_PARSED]: (event: Events.MANIFEST_PARSED, data: ManifestParsedData) => void; - [Events.LEVEL_SWITCHING]: (event: Events.LEVEL_SWITCHING, data: LevelSwitchingData) => void; - [Events.LEVEL_SWITCHED]: (event: Events.LEVEL_SWITCHED, data: LevelSwitchedData) => void; - [Events.LEVEL_LOADING]: (event: Events.LEVEL_LOADING, data: LevelLoadingData) => void; - [Events.LEVEL_LOADED]: (event: Events.LEVEL_LOADED, data: LevelLoadedData) => void; - [Events.LEVEL_UPDATED]: (event: Events.LEVEL_UPDATED, data: LevelUpdatedData) => void; - [Events.LEVEL_PTS_UPDATED]: (event: Events.LEVEL_PTS_UPDATED, data: LevelPTSUpdatedData) => void; - [Events.LEVELS_UPDATED]: (event: Events.LEVELS_UPDATED, data: LevelsUpdatedData) => void; - [Events.AUDIO_TRACKS_UPDATED]: (event: Events.AUDIO_TRACKS_UPDATED, data: AudioTracksUpdatedData) => void; - [Events.AUDIO_TRACK_SWITCHING]: (event: Events.AUDIO_TRACK_SWITCHING, data: AudioTrackSwitchingData) => void; - [Events.AUDIO_TRACK_SWITCHED]: (event: Events.AUDIO_TRACK_SWITCHED, data: AudioTrackSwitchedData) => void; - [Events.AUDIO_TRACK_LOADING]: (event: Events.AUDIO_TRACK_LOADING, data: TrackLoadingData) => void; - [Events.AUDIO_TRACK_LOADED]: (event: Events.AUDIO_TRACK_LOADED, data: AudioTrackLoadedData) => void; - [Events.AUDIO_TRACK_UPDATED]: (event: Events.AUDIO_TRACK_UPDATED, data: AudioTrackUpdatedData) => void; - [Events.SUBTITLE_TRACKS_UPDATED]: (event: Events.SUBTITLE_TRACKS_UPDATED, data: SubtitleTracksUpdatedData) => void; - [Events.SUBTITLE_TRACKS_CLEARED]: (event: Events.SUBTITLE_TRACKS_CLEARED) => void; - [Events.SUBTITLE_TRACK_SWITCH]: (event: Events.SUBTITLE_TRACK_SWITCH, data: SubtitleTrackSwitchData) => void; - [Events.SUBTITLE_TRACK_LOADING]: (event: Events.SUBTITLE_TRACK_LOADING, data: TrackLoadingData) => void; - [Events.SUBTITLE_TRACK_LOADED]: (event: Events.SUBTITLE_TRACK_LOADED, data: SubtitleTrackLoadedData) => void; - [Events.SUBTITLE_TRACK_UPDATED]: (event: Events.SUBTITLE_TRACK_UPDATED, data: SubtitleTrackUpdatedData) => void; - [Events.SUBTITLE_FRAG_PROCESSED]: (event: Events.SUBTITLE_FRAG_PROCESSED, data: SubtitleFragProcessedData) => void; - [Events.CUES_PARSED]: (event: Events.CUES_PARSED, data: CuesParsedData) => void; - [Events.NON_NATIVE_TEXT_TRACKS_FOUND]: (event: Events.NON_NATIVE_TEXT_TRACKS_FOUND, data: NonNativeTextTracksData) => void; - [Events.INIT_PTS_FOUND]: (event: Events.INIT_PTS_FOUND, data: InitPTSFoundData) => void; - [Events.FRAG_LOADING]: (event: Events.FRAG_LOADING, data: FragLoadingData) => void; - [Events.FRAG_LOAD_EMERGENCY_ABORTED]: (event: Events.FRAG_LOAD_EMERGENCY_ABORTED, data: FragLoadEmergencyAbortedData) => void; - [Events.FRAG_LOADED]: (event: Events.FRAG_LOADED, data: FragLoadedData) => void; - [Events.FRAG_DECRYPTED]: (event: Events.FRAG_DECRYPTED, data: FragDecryptedData) => void; - [Events.FRAG_PARSING_INIT_SEGMENT]: (event: Events.FRAG_PARSING_INIT_SEGMENT, data: FragParsingInitSegmentData) => void; - [Events.FRAG_PARSING_USERDATA]: (event: Events.FRAG_PARSING_USERDATA, data: FragParsingUserdataData) => void; - [Events.FRAG_PARSING_METADATA]: (event: Events.FRAG_PARSING_METADATA, data: FragParsingMetadataData) => void; - [Events.FRAG_PARSED]: (event: Events.FRAG_PARSED, data: FragParsedData) => void; - [Events.FRAG_BUFFERED]: (event: Events.FRAG_BUFFERED, data: FragBufferedData) => void; - [Events.FRAG_CHANGED]: (event: Events.FRAG_CHANGED, data: FragChangedData) => void; - [Events.FPS_DROP]: (event: Events.FPS_DROP, data: FPSDropData) => void; - [Events.FPS_DROP_LEVEL_CAPPING]: (event: Events.FPS_DROP_LEVEL_CAPPING, data: FPSDropLevelCappingData) => void; - [Events.MAX_AUTO_LEVEL_UPDATED]: (event: Events.MAX_AUTO_LEVEL_UPDATED, data: MaxAutoLevelUpdatedData) => void; - [Events.ERROR]: (event: Events.ERROR, data: ErrorData) => void; - [Events.DESTROYING]: (event: Events.DESTROYING) => void; - [Events.KEY_LOADING]: (event: Events.KEY_LOADING, data: KeyLoadingData) => void; - [Events.KEY_LOADED]: (event: Events.KEY_LOADED, data: KeyLoadedData) => void; - [Events.LIVE_BACK_BUFFER_REACHED]: (event: Events.LIVE_BACK_BUFFER_REACHED, data: LiveBackBufferData) => void; - [Events.BACK_BUFFER_REACHED]: (event: Events.BACK_BUFFER_REACHED, data: BackBufferData) => void; - [Events.STEERING_MANIFEST_LOADED]: (event: Events.STEERING_MANIFEST_LOADED, data: SteeringManifestLoadedData) => void; - [Events.ASSET_LIST_LOADING]: (event: Events.ASSET_LIST_LOADING, data: AssetListLoadingData) => void; - [Events.ASSET_LIST_LOADED]: (event: Events.ASSET_LIST_LOADED, data: AssetListLoadedData) => void; - [Events.INTERSTITIALS_UPDATED]: (event: Events.INTERSTITIALS_UPDATED, data: InterstitialsUpdatedData) => void; - [Events.INTERSTITIALS_BUFFERED_TO_BOUNDARY]: (event: Events.INTERSTITIALS_BUFFERED_TO_BOUNDARY, data: InterstitialsBufferedToBoundaryData) => void; - [Events.INTERSTITIAL_ASSET_PLAYER_CREATED]: (event: Events.INTERSTITIAL_ASSET_PLAYER_CREATED, data: InterstitialAssetPlayerCreatedData) => void; - [Events.INTERSTITIAL_STARTED]: (event: Events.INTERSTITIAL_STARTED, data: InterstitialStartedData) => void; - [Events.INTERSTITIAL_ASSET_STARTED]: (event: Events.INTERSTITIAL_ASSET_STARTED, data: InterstitialAssetStartedData) => void; - [Events.INTERSTITIAL_ASSET_ENDED]: (event: Events.INTERSTITIAL_ASSET_ENDED, data: InterstitialAssetEndedData) => void; - [Events.INTERSTITIAL_ASSET_ERROR]: (event: Events.INTERSTITIAL_ASSET_ERROR, data: InterstitialAssetErrorData) => void; - [Events.INTERSTITIAL_ENDED]: (event: Events.INTERSTITIAL_ENDED, data: InterstitialEndedData) => void; - [Events.INTERSTITIALS_PRIMARY_RESUMED]: (event: Events.INTERSTITIALS_PRIMARY_RESUMED, data: InterstitialsPrimaryResumed) => void; - [Events.PLAYOUT_LIMIT_REACHED]: (event: Events.PLAYOUT_LIMIT_REACHED, data: {}) => void; -} - -export declare type HlsLoadPolicies = { - fragLoadPolicy: LoadPolicy; - keyLoadPolicy: LoadPolicy; - certLoadPolicy: LoadPolicy; - playlistLoadPolicy: LoadPolicy; - manifestLoadPolicy: LoadPolicy; - steeringManifestLoadPolicy: LoadPolicy; - interstitialAssetListLoadPolicy: LoadPolicy; -}; - -export declare interface HlsPerformanceTiming { - start: number; - end: number; -} - -export declare interface HlsProgressivePerformanceTiming extends HlsPerformanceTiming { - first: number; -} - -export declare const enum HlsSkip { - No = "", - Yes = "YES", - v2 = "v2" -} - -export declare class HlsUrlParameters { - msn?: number; - part?: number; - skip?: HlsSkip; - constructor(msn?: number, part?: number, skip?: HlsSkip); - addDirectives(uri: string): string | never; -} - -export declare type IErrorAction = { - action: NetworkErrorAction; - flags: ErrorActionFlags; - retryCount?: number; - retryConfig?: RetryConfig; - hdcpLevel?: HdcpLevel; - nextAutoLevel?: number; - resolved?: boolean; -}; - -export declare interface ILogFunction { - (message?: any, ...optionalParams: any[]): void; -} - -export declare interface ILogger { - trace: ILogFunction; - debug: ILogFunction; - log: ILogFunction; - warn: ILogFunction; - info: ILogFunction; - error: ILogFunction; -} - -export declare interface InitPTSFoundData { - id: PlaylistLevelType; - frag: MediaFragment; - initPTS: number; - timescale: number; -} - -export declare interface InitSegmentData { - tracks?: TrackSet; - initPTS: number | undefined; - timescale: number | undefined; -} - -export declare interface InterstitialAssetEndedData { - asset: InterstitialAssetItem; - assetListIndex: number; - event: InterstitialEvent; - schedule: InterstitialScheduleItem[]; - scheduleIndex: number; - player: HlsAssetPlayer; -} - -export declare type InterstitialAssetErrorData = { - asset: InterstitialAssetItem | null; - assetListIndex: number; - event: InterstitialEvent | null; - schedule: InterstitialScheduleItem[] | null; - scheduleIndex: number; - player: HlsAssetPlayer | null; -} & ErrorData; - -export declare type InterstitialAssetId = string; - -export declare type InterstitialAssetItem = { - parentIdentifier: InterstitialId; - identifier: InterstitialAssetId; - duration: number | null; - startOffset: number; - timelineStart: number; - uri: string; - error?: Error; -}; - -export declare interface InterstitialAssetPlayerCreatedData { - asset: InterstitialAssetItem; - assetListIndex: number; - assetListResponse?: AssetListJSON; - event: InterstitialEvent; - player: HlsAssetPlayer; -} - -export declare interface InterstitialAssetStartedData { - asset: InterstitialAssetItem; - assetListIndex: number; - event: InterstitialEvent; - schedule: InterstitialScheduleItem[]; - scheduleIndex: number; - player: HlsAssetPlayer; -} - -export declare interface InterstitialEndedData { - event: InterstitialEvent; - schedule: InterstitialScheduleItem[]; - scheduleIndex: number; -} - -export declare class InterstitialEvent { - private base; - private _duration; - private _timelineStart; - private appendInPlaceDisabled?; - appendInPlaceStarted?: boolean; - dateRange: DateRange; - hasPlayed: boolean; - cumulativeDuration: number; - resumeOffset: number; - playoutLimit: number; - restrictions: PlaybackRestrictions; - snapOptions: SnapOptions; - assetList: InterstitialAssetItem[]; - assetListLoader?: Loader; - assetListResponse: AssetListJSON | null; - resumeAnchor?: Fragment; - error?: Error; - constructor(dateRange: DateRange, base: BaseData); - setDateRange(dateRange: DateRange): void; - reset(): void; - isAssetPastPlayoutLimit(assetIndex: number): boolean; - findAssetIndex(asset: InterstitialAssetItem): number; - get identifier(): InterstitialId; - get startDate(): Date; - get startTime(): number; - get startOffset(): number; - get resumptionOffset(): number; - get resumeTime(): number; - get appendInPlace(): boolean; - set appendInPlace(value: boolean); - get timelineStart(): number; - set timelineStart(value: number); - get duration(): number; - set duration(value: number); - get cue(): DateRangeCue; - get timelineOccupancy(): TimelineOccupancy; - get supplementsPrimary(): boolean; - get contentMayVary(): boolean; - get assetUrl(): string | undefined; - get assetListUrl(): string | undefined; - get baseUrl(): string; - toString(): string; -} - -export declare interface InterstitialEventWithAssetList extends InterstitialEvent { - assetListUrl: string; -} - -export declare type InterstitialId = string; - -export declare interface InterstitialsBufferedToBoundaryData { - events: InterstitialEvent[]; - schedule: InterstitialScheduleItem[]; - bufferingIndex: number; - playingIndex: number; -} - -export declare type InterstitialScheduleDurations = { - primary: number; - playout: number; - integrated: number; -}; - -export declare type InterstitialScheduleEventItem = { - event: InterstitialEvent; - start: number; - end: number; - playout: { - start: number; - end: number; - }; - integrated: { - start: number; - end: number; - }; -}; - -export declare type InterstitialScheduleItem = InterstitialScheduleEventItem | InterstitialSchedulePrimaryItem; - -export declare type InterstitialSchedulePrimaryItem = { - nextEvent: InterstitialEvent | null; - previousEvent: InterstitialEvent | null; - event?: undefined; - start: number; - end: number; - playout: { - start: number; - end: number; - }; - integrated: { - start: number; - end: number; - }; -}; - -export declare class InterstitialsController extends Logger implements NetworkComponentAPI { - private readonly HlsPlayerClass; - private readonly hls; - private readonly assetListLoader; - private mediaSelection; - private altSelection; - private media; - private detachedData; - private requiredTracks; - private manager; - private playerQueue; - private bufferedPos; - private timelinePos; - private schedule; - private playingItem; - private bufferingItem; - private waitingItem; - private playingAsset; - private bufferingAsset; - private shouldPlay; - constructor(hls: Hls, HlsPlayerClass: typeof Hls); - private registerListeners; - private unregisterListeners; - startLoad(): void; - stopLoad(): void; - resumeBuffering(): void; - pauseBuffering(): void; - destroy(): void; - private onDestroying; - private removeMediaListeners; - private onMediaAttaching; - private onMediaAttached; - private clearScheduleState; - private onMediaDetaching; - get interstitialsManager(): InterstitialsManager | null; - private get playingLastItem(); - private get playbackStarted(); - private get currentTime(); - private get primaryMedia(); - private isInterstitial; - private retreiveMediaSource; - private transferMediaFromPlayer; - private transferMediaTo; - private onPlay; - private onSeeking; - private onTimeupdate; - private checkStart; - private advanceAfterAssetEnded; - private setScheduleToAssetAtTime; - private setSchedulePosition; - private get playbackDisabled(); - private get primaryDetails(); - private get primaryLive(); - private resumePrimary; - private getPrimaryResumption; - private isAssetBuffered; - private attachPrimary; - private onManifestLoading; - private onLevelUpdated; - private onAudioTrackUpdated; - private onSubtitleTrackUpdated; - private onAudioTrackSwitching; - private onSubtitleTrackSwitch; - private onBufferCodecs; - private onBufferAppended; - private onBufferFlushed; - private onBufferedToEnd; - private onMediaEnded; - private onScheduleUpdate; - private updateItem; - private itemsMatch; - private eventItemsMatch; - private findItemIndex; - private updateSchedule; - private checkBuffer; - private updateBufferedPos; - private setBufferingItem; - private bufferedToItem; - private preloadPrimary; - private bufferedToEvent; - private preloadAssets; - private flushFrontBuffer; - private getAssetPlayerQueueIndex; - private getAssetPlayer; - private getBufferingPlayer; - private createAsset; - private createAssetPlayer; - private clearInterstitial; - private clearAssetPlayer; - private emptyPlayerQueue; - private startAssetPlayer; - private bufferAssetPlayer; - private handleAssetItemError; - private primaryFallback; - private onAssetListLoaded; - private onError; -} - -export declare interface InterstitialsManager { - events: InterstitialEvent[]; - playerQueue: HlsAssetPlayer[]; - schedule: InterstitialScheduleItem[]; - bufferingPlayer: HlsAssetPlayer | null; - bufferingAsset: InterstitialAssetItem | null; - bufferingItem: InterstitialScheduleItem | null; - bufferingIndex: number; - playingAsset: InterstitialAssetItem | null; - playingItem: InterstitialScheduleItem | null; - playingIndex: number; - waitingIndex: number; - primary: PlayheadTimes; - playout: PlayheadTimes; - integrated: PlayheadTimes; - skip: () => void; -} - -export declare interface InterstitialsPrimaryResumed { - schedule: InterstitialScheduleItem[]; - scheduleIndex: number; -} - -export declare interface InterstitialStartedData { - event: InterstitialEvent; - schedule: InterstitialScheduleItem[]; - scheduleIndex: number; -} - -export declare interface InterstitialsUpdatedData { - events: InterstitialEvent[]; - schedule: InterstitialScheduleItem[]; - durations: InterstitialScheduleDurations; - removedIds: string[]; -} - -export declare interface KeyLoadedData { - frag: Fragment; - keyInfo: KeyLoaderInfo; -} - -export declare class KeyLoader implements ComponentAPI { - private readonly config; - keyUriToKeyInfo: { - [keyuri: string]: KeyLoaderInfo; - }; - emeController: EMEController | null; - constructor(config: HlsConfig); - abort(type?: PlaylistLevelType): void; - detach(): void; - destroy(): void; - createKeyLoadError(frag: Fragment, details: ErrorDetails | undefined, error: Error, networkDetails?: any, response?: { - url: string; - data: undefined; - code: number; - text: string; - }): LoadError; - loadClear(loadingFrag: Fragment, encryptedFragments: Fragment[]): void | Promise; - load(frag: Fragment): Promise; - loadInternal(frag: Fragment, keySystemFormat?: KeySystemFormats): Promise; - loadKeyEME(keyInfo: KeyLoaderInfo, frag: Fragment): Promise; - loadKeyHTTP(keyInfo: KeyLoaderInfo, frag: Fragment): Promise; - private resetLoader; -} - -export declare interface KeyLoaderContext extends LoaderContext { - keyInfo: KeyLoaderInfo; - frag: Fragment; -} - -export declare interface KeyLoaderInfo { - decryptdata: LevelKey; - keyLoadPromise: Promise | null; - loader: Loader | null; - mediaKeySessionContext: MediaKeySessionContext | null; -} - -export declare interface KeyLoadingData { - frag: Fragment; -} - -export declare const enum KeySystemFormats { - CLEARKEY = "org.w3.clearkey", - FAIRPLAY = "com.apple.streamingkeydelivery", - PLAYREADY = "com.microsoft.playready", - WIDEVINE = "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed" -} - -/** - * @see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/requestMediaKeySystemAccess - */ -export declare const enum KeySystems { - CLEARKEY = "org.w3.clearkey", - FAIRPLAY = "com.apple.fps", - PLAYREADY = "com.microsoft.playready", - WIDEVINE = "com.widevine.alpha" -} - -export declare type LatencyControllerConfig = { - liveSyncDurationCount: number; - liveMaxLatencyDurationCount: number; - liveSyncDuration?: number; - liveMaxLatencyDuration?: number; - maxLiveSyncPlaybackRate: number; - liveSyncOnStallIncrease: number; -}; - -export declare class Level { - readonly _attrs: LevelAttributes[]; - readonly audioCodec: string | undefined; - readonly bitrate: number; - readonly codecSet: string; - readonly url: string[]; - readonly frameRate: number; - readonly height: number; - readonly id: number; - readonly name: string; - readonly videoCodec: string | undefined; - readonly width: number; - details?: LevelDetails; - fragmentError: number; - loadError: number; - loaded?: { - bytes: number; - duration: number; - }; - realBitrate: number; - supportedPromise?: Promise; - supportedResult?: MediaDecodingInfo; - private _avgBitrate; - private _audioGroups?; - private _subtitleGroups?; - private readonly _urlId; - constructor(data: LevelParsed | MediaPlaylist); - get maxBitrate(): number; - get averageBitrate(): number; - get attrs(): LevelAttributes; - get codecs(): string; - get pathwayId(): string; - get videoRange(): VideoRange; - get score(): number; - get uri(): string; - hasAudioGroup(groupId: string | undefined): boolean; - hasSubtitleGroup(groupId: string | undefined): boolean; - get audioGroups(): (string | undefined)[] | undefined; - get subtitleGroups(): (string | undefined)[] | undefined; - addGroupId(type: string, groupId: string | undefined): void; - get urlId(): number; - set urlId(value: number); - get audioGroupIds(): (string | undefined)[] | undefined; - get textGroupIds(): (string | undefined)[] | undefined; - get audioGroupId(): string | undefined; - get textGroupId(): string | undefined; - addFallback(): void; -} - -export declare interface LevelAttributes extends AttrList { - 'ALLOWED-CPC'?: string; - AUDIO?: string; - 'AVERAGE-BANDWIDTH'?: string; - BANDWIDTH?: string; - 'CLOSED-CAPTIONS'?: string; - CODECS?: string; - 'FRAME-RATE'?: string; - 'HDCP-LEVEL'?: 'TYPE-0' | 'TYPE-1' | 'NONE'; - 'PATHWAY-ID'?: string; - RESOLUTION?: string; - SCORE?: string; - 'STABLE-VARIANT-ID'?: string; - SUBTITLES?: string; - 'SUPPLEMENTAL-CODECS'?: string; - VIDEO?: string; - 'VIDEO-RANGE'?: VideoRange; -} - -export declare type LevelControllerConfig = { - startLevel?: number; -}; - -/** - * Object representing parsed data from an HLS Media Playlist. Found in {@link hls.js#Level.details}. - */ -export declare class LevelDetails { - PTSKnown: boolean; - alignedSliding: boolean; - averagetargetduration?: number; - endCC: number; - endSN: number; - fragments: MediaFragment[]; - fragmentHint?: MediaFragment; - partList: Part[] | null; - dateRanges: Record; - dateRangeTagCount: number; - live: boolean; - ageHeader: number; - advancedDateTime?: number; - updated: boolean; - advanced: boolean; - availabilityDelay?: number; - misses: number; - startCC: number; - startSN: number; - startTimeOffset: number | null; - targetduration: number; - totalduration: number; - type: string | null; - url: string; - m3u8: string; - version: number | null; - canBlockReload: boolean; - canSkipUntil: number; - canSkipDateRanges: boolean; - skippedSegments: number; - recentlyRemovedDateranges?: string[]; - partHoldBack: number; - holdBack: number; - partTarget: number; - preloadHint?: AttrList; - renditionReports?: AttrList[]; - tuneInGoal: number; - deltaUpdateFailed?: boolean; - driftStartTime: number; - driftEndTime: number; - driftStart: number; - driftEnd: number; - encryptedFragments: Fragment[]; - playlistParsingError: Error | null; - variableList: VariableMap | null; - hasVariableRefs: boolean; - appliedTimelineOffset?: number; - constructor(baseUrl: string); - reloaded(previous: LevelDetails | undefined): void; - get hasProgramDateTime(): boolean; - get levelTargetDuration(): number; - get drift(): number; - get edge(): number; - get partEnd(): number; - get fragmentEnd(): number; - get fragmentStart(): number; - get age(): number; - get lastPartIndex(): number; - get lastPartSn(): number; -} - -export declare class LevelKey implements DecryptData { - readonly uri: string; - readonly method: string; - readonly keyFormat: string; - readonly keyFormatVersions: number[]; - readonly encrypted: boolean; - readonly isCommonEncryption: boolean; - iv: Uint8Array | null; - key: Uint8Array | null; - keyId: Uint8Array | null; - pssh: Uint8Array | null; - static clearKeyUriToKeyIdMap(): void; - constructor(method: string, uri: string, format: string, formatversions?: number[], iv?: Uint8Array | null); - isSupported(): boolean; - getDecryptData(sn: number | 'initSegment'): LevelKey | null; -} - -export declare interface LevelLoadedData { - details: LevelDetails; - id: number; - level: number; - networkDetails: any; - stats: LoaderStats; - deliveryDirectives: HlsUrlParameters | null; -} - -export declare interface LevelLoadingData { - id: number; - level: number; - pathwayId: string | undefined; - url: string; - deliveryDirectives: HlsUrlParameters | null; -} - -export declare interface LevelParsed { - attrs: LevelAttributes; - audioCodec?: string; - bitrate: number; - details?: LevelDetails; - height?: number; - id?: number; - name: string; - textCodec?: string; - unknownCodecs?: string[]; - url: string; - videoCodec?: string; - width?: number; -} - -export declare interface LevelPTSUpdatedData { - details: LevelDetails; - level: Level; - drift: number; - type: string; - frag: Fragment; - start: number; - end: number; -} - -export declare interface LevelsUpdatedData { - levels: Array; -} - -export declare interface LevelSwitchedData { - level: number; -} - -export declare interface LevelSwitchingData { - level: number; - attrs: LevelAttributes; - details: LevelDetails | undefined; - bitrate: number; - averageBitrate: number; - maxBitrate: number; - realBitrate: number; - width: number; - height: number; - codecSet: string; - audioCodec: string | undefined; - videoCodec: string | undefined; - audioGroups: (string | undefined)[] | undefined; - subtitleGroups: (string | undefined)[] | undefined; - loaded: { - bytes: number; - duration: number; - } | undefined; - loadError: number; - fragmentError: number; - name: string | undefined; - id: number; - uri: string; - url: string[]; - urlId: 0; - audioGroupIds: (string | undefined)[] | undefined; - textGroupIds: (string | undefined)[] | undefined; -} - -export declare interface LevelUpdatedData { - details: LevelDetails; - level: number; -} - -/** - * @deprecated Use BackBufferData - */ -export declare interface LiveBackBufferData extends BackBufferData { -} - -export declare interface Loader { - destroy(): void; - abort(): void; - load(context: T, config: LoaderConfiguration, callbacks: LoaderCallbacks): void; - /** - * `getCacheAge()` is called by hls.js to get the duration that a given object - * has been sitting in a cache proxy when playing live. If implemented, - * this should return a value in seconds. - * - * For HTTP based loaders, this should return the contents of the "age" header. - * - * @returns time object being lodaded - */ - getCacheAge?: () => number | null; - getResponseHeader?: (name: string) => string | null; - context: T | null; - stats: LoaderStats; -} - -export declare interface LoaderCallbacks { - onSuccess: LoaderOnSuccess; - onError: LoaderOnError; - onTimeout: LoaderOnTimeout; - onAbort?: LoaderOnAbort; - onProgress?: LoaderOnProgress; -} - -export declare type LoaderConfig = { - maxTimeToFirstByteMs: number; - maxLoadTimeMs: number; - timeoutRetry: RetryConfig | null; - errorRetry: RetryConfig | null; -}; - -export declare interface LoaderConfiguration { - loadPolicy: LoaderConfig; - /** - * @deprecated use LoaderConfig timeoutRetry and errorRetry maxNumRetry - */ - maxRetry: number; - /** - * @deprecated use LoaderConfig maxTimeToFirstByteMs and maxLoadTimeMs - */ - timeout: number; - /** - * @deprecated use LoaderConfig timeoutRetry and errorRetry retryDelayMs - */ - retryDelay: number; - /** - * @deprecated use LoaderConfig timeoutRetry and errorRetry maxRetryDelayMs - */ - maxRetryDelay: number; - highWaterMark?: number; -} - -export declare interface LoaderContext { - url: string; - responseType: string; - headers?: Record; - rangeStart?: number; - rangeEnd?: number; - progressData?: boolean; -} - -export declare type LoaderOnAbort = (stats: LoaderStats, context: T, networkDetails: any) => void; - -export declare type LoaderOnError = (error: { - code: number; - text: string; -}, context: T, networkDetails: any, stats: LoaderStats) => void; - -export declare type LoaderOnProgress = (stats: LoaderStats, context: T, data: string | ArrayBuffer, networkDetails: any) => void; - -export declare type LoaderOnSuccess = (response: LoaderResponse, stats: LoaderStats, context: T, networkDetails: any) => void; - -export declare type LoaderOnTimeout = (stats: LoaderStats, context: T, networkDetails: any) => void; - -export declare interface LoaderResponse { - url: string; - data?: string | ArrayBuffer | Object; - code?: number; - text?: string; -} - -export declare class LoadError extends Error { - readonly data: FragLoadFailResult; - constructor(data: FragLoadFailResult); -} - -export declare interface LoaderStats { - aborted: boolean; - loaded: number; - retry: number; - total: number; - chunkCount: number; - bwEstimate: number; - loading: HlsProgressivePerformanceTiming; - parsing: HlsPerformanceTiming; - buffering: HlsProgressivePerformanceTiming; -} - -export declare type LoadPolicy = { - default: LoaderConfig; -}; - -export declare class LoadStats implements LoaderStats { - aborted: boolean; - loaded: number; - retry: number; - total: number; - chunkCount: number; - bwEstimate: number; - loading: HlsProgressivePerformanceTiming; - parsing: HlsPerformanceTiming; - buffering: HlsProgressivePerformanceTiming; -} - -export declare class Logger implements ILogger { - trace: ILogFunction; - debug: ILogFunction; - log: ILogFunction; - warn: ILogFunction; - info: ILogFunction; - error: ILogFunction; - constructor(label: string, logger: ILogger); -} - -export declare type MainPlaylistType = AudioPlaylistType | 'VIDEO'; - -export declare interface ManifestLoadedData { - audioTracks: MediaPlaylist[]; - captions?: MediaPlaylist[]; - contentSteering: ContentSteeringOptions | null; - levels: LevelParsed[]; - networkDetails: any; - sessionData: Record | null; - sessionKeys: LevelKey[] | null; - startTimeOffset: number | null; - stats: LoaderStats; - subtitles?: MediaPlaylist[]; - url: string; - variableList: VariableMap | null; -} - -export declare interface ManifestLoadingData { - url: string; -} - -export declare interface ManifestParsedData { - levels: Level[]; - audioTracks: MediaPlaylist[]; - subtitleTracks: MediaPlaylist[]; - sessionData: Record | null; - sessionKeys: LevelKey[] | null; - firstLevel: number; - stats: LoaderStats; - audio: boolean; - video: boolean; - altAudio: boolean; -} - -export declare interface MaxAutoLevelUpdatedData { - autoLevelCapping: number; - levels: Level[] | null; - maxAutoLevel: number; - minAutoLevel: number; - maxHdcpLevel: HdcpLevel; -} - -export declare interface MediaAttachedData { - media: HTMLMediaElement; - mediaSource?: MediaSource; -} - -export declare interface MediaAttachingData { - media: HTMLMediaElement; - mediaSource?: MediaSource | null; - tracks?: SourceBufferTrackSet; - overrides?: MediaOverrides; -} - -export declare interface MediaAttributes extends AttrList { - 'ASSOC-LANGUAGE'?: string; - AUTOSELECT?: 'YES' | 'NO'; - CHANNELS?: string; - CHARACTERISTICS?: string; - DEFAULT?: 'YES' | 'NO'; - FORCED?: 'YES' | 'NO'; - 'GROUP-ID': string; - 'INSTREAM-ID'?: string; - LANGUAGE?: string; - NAME: string; - 'PATHWAY-ID'?: string; - 'STABLE-RENDITION-ID'?: string; - TYPE?: 'AUDIO' | 'VIDEO' | 'SUBTITLES' | 'CLOSED-CAPTIONS'; - URI?: string; -} - -export declare type MediaDecodingInfo = { - supported: boolean; - configurations: readonly MediaDecodingConfiguration[]; - decodingInfoResults: readonly MediaCapabilitiesDecodingInfo[]; - error?: Error; -}; - -export declare interface MediaDetachedData { - transferMedia?: AttachMediaSourceData | null; -} - -export declare interface MediaDetachingData { - transferMedia?: AttachMediaSourceData | null; -} - -export declare interface MediaEndedData { - stalled: boolean; -} - -export declare interface MediaFragment extends Fragment { - sn: number; -} - -export declare type MediaKeyFunc = (keySystem: KeySystems, supportedConfigurations: MediaKeySystemConfiguration[]) => Promise; - -export declare interface MediaKeySessionContext { - keySystem: KeySystems; - mediaKeys: MediaKeys; - decryptdata: LevelKey; - mediaKeysSession: MediaKeySession; - keyStatus: MediaKeyStatus; - licenseXhr?: XMLHttpRequest; - _onmessage?: (this: MediaKeySession, ev: MediaKeyMessageEvent) => any; - _onkeystatuseschange?: (this: MediaKeySession, ev: Event) => any; -} - -export declare type MediaOverrides = { - duration?: number; - endOfStream?: boolean; - cueRemoval?: boolean; -}; - -export declare interface MediaPlaylist { - attrs: MediaAttributes; - audioCodec?: string; - autoselect: boolean; - bitrate: number; - channels?: string; - characteristics?: string; - details?: LevelDetails; - height?: number; - default: boolean; - forced: boolean; - groupId: string; - id: number; - instreamId?: string; - lang?: string; - assocLang?: string; - name: string; - textCodec?: string; - unknownCodecs?: string[]; - type: MediaPlaylistType | 'main'; - url: string; - videoCodec?: string; - width?: number; -} - -export declare type MediaPlaylistType = MainPlaylistType | SubtitlePlaylistType; - -export declare type MetadataControllerConfig = { - enableDateRangeMetadataCues: boolean; - enableEmsgMetadataCues: boolean; - enableEmsgKLVMetadata: boolean; - enableID3MetadataCues: boolean; -}; - -export declare interface MetadataSample { - pts: number; - dts: number; - duration: number; - len?: number; - data: Uint8Array; - type: MetadataSchema; -} - -export declare enum MetadataSchema { - audioId3 = "org.id3", - dateRange = "com.apple.quicktime.HLS", - emsg = "https://aomedia.org/emsg/ID3", - misbklv = "urn:misb:KLV:bin:1910.1" -} - -export declare type MP4RemuxerConfig = { - stretchShortVideoTrack: boolean; - maxAudioFramesDrift: number; -}; - -export declare interface NetworkComponentAPI extends ComponentAPI { - startLoad(startPosition: number, skipSeekToStartPosition?: boolean): void; - stopLoad(): void; - pauseBuffering?(): void; - resumeBuffering?(): void; -} - -export declare const enum NetworkErrorAction { - DoNothing = 0, - SendEndCallback = 1,// Reserved for future use - SendAlternateToPenaltyBox = 2, - RemoveAlternatePermanently = 3,// Reserved for future use - InsertDiscontinuity = 4,// Reserved for future use - RetryRequest = 5 -} - -export declare interface NonNativeTextTrack { - _id?: string; - label: any; - kind: string; - default: boolean; - closedCaptions?: MediaPlaylist; - subtitleTrack?: MediaPlaylist; -} - -export declare interface NonNativeTextTracksData { - tracks: Array; -} - -declare interface PACData { - row: number; - indent: number | null; - color: string | null; - underline: boolean; - italics: boolean; -} - -export declare type ParsedMultivariantPlaylist = { - contentSteering: ContentSteeringOptions | null; - levels: LevelParsed[]; - playlistParsingError: Error | null; - sessionData: Record | null; - sessionKeys: LevelKey[] | null; - startTimeOffset: number | null; - variableList: VariableMap | null; - hasVariableRefs: boolean; -}; - -export declare interface ParsedTrack extends BaseTrack { - initSegment?: Uint8Array; -} - -/** - * Object representing parsed data from an HLS Partial Segment. Found in {@link hls.js#LevelDetails.partList}. - */ -export declare class Part extends BaseSegment { - readonly fragOffset: number; - readonly duration: number; - readonly gap: boolean; - readonly independent: boolean; - readonly relurl: string; - readonly fragment: MediaFragment; - readonly index: number; - stats: LoadStats; - constructor(partAttrs: AttrList, frag: MediaFragment, baseurl: string, index: number, previous?: Part); - get start(): number; - get end(): number; - get loaded(): boolean; -} - -export declare interface PartsLoadedData { - frag: Fragment; - part: Part | null; - partsLoaded?: FragLoadedData[]; -} - -export declare type PathwayClone = { - 'BASE-ID': string; - ID: string; - 'URI-REPLACEMENT': UriReplacement; -}; - -declare class PenState { - foreground: string; - underline: boolean; - italics: boolean; - background: string; - flash: boolean; - reset(): void; - setStyles(styles: Partial): void; - isDefault(): boolean; - equals(other: PenState): boolean; - copy(newPenState: PenState): void; - toString(): string; -} - -declare type PenStyles = { - foreground: string | null; - underline: boolean; - italics: boolean; - background: string; - flash: boolean; -}; - -export declare type PlaybackRestrictions = { - skip: boolean; - jump: boolean; -}; - -export declare type PlayheadTimes = { - bufferedEnd: number; - currentTime: number; - duration: number; - seekableStart: number; - seekTo: (time: number) => void; -}; - -export declare const enum PlaylistContextType { - MANIFEST = "manifest", - LEVEL = "level", - AUDIO_TRACK = "audioTrack", - SUBTITLE_TRACK = "subtitleTrack" -} - -export declare const enum PlaylistLevelType { - MAIN = "main", - AUDIO = "audio", - SUBTITLE = "subtitle" -} - -/** - * @deprecated use manifestLoadPolicy.default and playlistLoadPolicy.default - */ -export declare type PlaylistLoaderConfig = { - manifestLoadingTimeOut: number; - manifestLoadingMaxRetry: number; - manifestLoadingRetryDelay: number; - manifestLoadingMaxRetryTimeout: number; - levelLoadingTimeOut: number; - levelLoadingMaxRetry: number; - levelLoadingRetryDelay: number; - levelLoadingMaxRetryTimeout: number; -}; - -export declare interface PlaylistLoaderConstructor { - new (confg: HlsConfig): Loader; -} - -export declare interface PlaylistLoaderContext extends LoaderContext { - type: PlaylistContextType; - level: number | null; - id: number | null; - groupId?: string; - pathwayId?: string; - levelDetails?: LevelDetails; - deliveryDirectives: HlsUrlParameters | null; -} - -export declare type RationalTimestamp = { - baseTime: number; - timescale: number; -}; - -export declare interface RemuxedMetadata { - samples: MetadataSample[]; -} - -export declare interface RemuxedTrack { - data1: Uint8Array; - data2?: Uint8Array; - startPTS: number; - endPTS: number; - startDTS: number; - endDTS: number; - type: SourceBufferName; - hasAudio: boolean; - hasVideo: boolean; - independent?: boolean; - firstKeyFrame?: number; - firstKeyFramePTS?: number; - nb: number; - transferredData1?: ArrayBuffer; - transferredData2?: ArrayBuffer; - dropped?: number; -} - -export declare interface RemuxedUserdata { - samples: UserdataSample[]; -} - -export declare interface RemuxerResult { - audio?: RemuxedTrack; - video?: RemuxedTrack; - text?: RemuxedUserdata; - id3?: RemuxedMetadata; - initSegment?: InitSegmentData; - independent?: boolean; -} - -export declare type RetryConfig = { - maxNumRetry: number; - retryDelayMs: number; - maxRetryDelayMs: number; - backoff?: 'exponential' | 'linear'; - shouldRetry?: (retryConfig: RetryConfig | null | undefined, retryCount: number, isTimeout: boolean, loaderResponse: LoaderResponse | undefined, retry: boolean) => boolean; -}; - -/** - * CEA-608 row consisting of NR_COLS instances of StyledUnicodeChar. - * @constructor - */ -declare class Row { - chars: StyledUnicodeChar[]; - pos: number; - currPenState: PenState; - cueStartTime: number | null; - private logger; - constructor(logger: CaptionsLogger); - equals(other: Row): boolean; - copy(other: Row): void; - isEmpty(): boolean; - /** - * Set the cursor to a valid column. - */ - setCursor(absPos: number): void; - /** - * Move the cursor relative to current position. - */ - moveCursor(relPos: number): void; - /** - * Backspace, move one step back and clear character. - */ - backSpace(): void; - insertChar(byte: number): void; - clearFromPos(startPos: number): void; - clear(): void; - clearToEndOfRow(): void; - getTextString(): string; - setPenStyles(styles: Partial): void; -} - -export declare type SelectionPreferences = { - videoPreference?: VideoSelectionOption; - audioPreference?: AudioSelectionOption; - subtitlePreference?: SubtitleSelectionOption; -}; - -export declare type SnapOptions = { - out: boolean; - in: boolean; -}; - -export declare interface SourceBufferListener { - event: string; - listener: EventListener; -} - -export declare type SourceBufferName = 'video' | 'audio' | 'audiovideo'; - -export declare interface SourceBufferTrack extends BaseTrack { - buffer?: ExtendedSourceBuffer; - listeners: SourceBufferListener[]; - ending?: boolean; - ended?: boolean; -} - -export declare type SourceBufferTrackSet = Partial>; - -export declare type SteeringManifest = { - VERSION: 1; - TTL: number; - 'RELOAD-URI'?: string; - 'PATHWAY-PRIORITY': string[]; - 'PATHWAY-CLONES'?: PathwayClone[]; -}; - -export declare interface SteeringManifestLoadedData { - steeringManifest: SteeringManifest; - url: string; -} - -export declare class StreamController extends BaseStreamController implements NetworkComponentAPI { - private audioCodecSwap; - private gapController; - private level; - private _forceStartLoad; - private _hasEnoughToStart; - private altAudio; - private audioOnly; - private fragPlaying; - private fragLastKbps; - private couldBacktrack; - private backtrackFragment; - private audioCodecSwitch; - private videoBuffer; - constructor(hls: Hls, fragmentTracker: FragmentTracker, keyLoader: KeyLoader); - protected registerListeners(): void; - protected unregisterListeners(): void; - protected onHandlerDestroying(): void; - startLoad(startPosition: number, skipSeekToStartPosition?: boolean): void; - stopLoad(): void; - protected doTick(): void; - protected onTickEnd(): void; - private doTickIdle; - protected loadFragment(frag: Fragment, level: Level, targetBufferTime: number): void; - private getBufferedFrag; - private followingBufferedFrag; - immediateLevelSwitch(): void; - /** - * try to switch ASAP without breaking video playback: - * in order to ensure smooth but quick level switching, - * we need to find the next flushable buffer range - * we should take into account new segment fetch time - */ - nextLevelSwitch(): void; - private abortCurrentFrag; - protected flushMainBuffer(startOffset: number, endOffset: number): void; - protected onMediaAttached(event: Events.MEDIA_ATTACHED, data: MediaAttachedData): void; - protected onMediaDetaching(event: Events.MEDIA_DETACHING, data: MediaDetachingData): void; - private onMediaPlaying; - private onMediaSeeked; - protected triggerEnded(): void; - protected onManifestLoading(): void; - private onManifestParsed; - private onLevelLoading; - private onLevelLoaded; - private synchronizeToLiveEdge; - protected _handleFragmentLoadProgress(data: FragLoadedData): void; - private onAudioTrackSwitching; - private onAudioTrackSwitched; - private onBufferCreated; - private onFragBuffered; - get hasEnoughToStart(): boolean; - protected onError(event: Events.ERROR, data: ErrorData): void; - private checkBuffer; - private onFragLoadEmergencyAborted; - private onBufferFlushed; - private onLevelsUpdated; - swapAudioCodec(): void; - /** - * Seeks to the set startPosition if not equal to the mediaElement's current time. - */ - protected seekToStartPos(): void; - private _getAudioCodec; - private _loadBitrateTestFrag; - private _handleTransmuxComplete; - private _bufferInitSegment; - getMainFwdBufferInfo(): BufferInfo | null; - get maxBufferLength(): number; - private backtrack; - private checkFragmentChanged; - get nextLevel(): number; - get currentFrag(): Fragment | null; - get currentProgramDateTime(): Date | null; - get currentLevel(): number; - get nextBufferedFrag(): MediaFragment | null; - get forceStartLoad(): boolean; -} - -export declare type StreamControllerConfig = { - autoStartLoad: boolean; - startPosition: number; - defaultAudioCodec?: string; - initialLiveManifestSize: number; - maxBufferLength: number; - maxBufferSize: number; - maxBufferHole: number; - highBufferWatchdogPeriod: number; - nudgeOffset: number; - nudgeMaxRetry: number; - maxFragLookUpTolerance: number; - maxMaxBufferLength: number; - startFragPrefetch: boolean; - testBandwidth: boolean; -}; - -/** - * Unicode character with styling and background. - * @constructor - */ -declare class StyledUnicodeChar { - uchar: string; - penState: PenState; - reset(): void; - setChar(uchar: string, newPenState: PenState): void; - setPenState(newPenState: PenState): void; - equals(other: StyledUnicodeChar): boolean; - copy(newChar: StyledUnicodeChar): void; - isEmpty(): boolean; -} - -export declare interface SubtitleFragProcessedData { - success: boolean; - frag: Fragment; - error?: Error; -} - -export declare type SubtitlePlaylistType = 'SUBTITLES' | 'CLOSED-CAPTIONS'; - -export declare type SubtitleSelectionOption = { - id?: number; - lang?: string; - assocLang?: string; - characteristics?: string; - name?: string; - groupId?: string; - default?: boolean; - forced?: boolean; -}; - -export declare class SubtitleStreamController extends BaseStreamController implements NetworkComponentAPI { - private currentTrackId; - private tracksBuffered; - private mainDetails; - constructor(hls: Hls, fragmentTracker: FragmentTracker, keyLoader: KeyLoader); - protected onHandlerDestroying(): void; - protected registerListeners(): void; - protected unregisterListeners(): void; - startLoad(startPosition: number): void; - protected onManifestLoading(): void; - protected onMediaDetaching(event: Events.MEDIA_DETACHING, data: MediaDetachingData): void; - private onLevelLoaded; - private onSubtitleFragProcessed; - private onBufferFlushing; - protected onError(event: Events.ERROR, data: ErrorData): void; - private onSubtitleTracksUpdated; - private onSubtitleTrackSwitch; - private onSubtitleTrackLoaded; - _handleFragmentLoadComplete(fragLoadedData: FragLoadedData): void; - doTick(): void; - protected loadFragment(frag: Fragment, level: Level, targetBufferTime: number): void; - get mediaBufferTimeRanges(): Bufferable; -} - -export declare class SubtitleTrackController extends BasePlaylistController { - private media; - private tracks; - private groupIds; - private tracksInGroup; - private trackId; - private currentTrack; - private selectDefaultTrack; - private queuedDefaultTrack; - private useTextTrackPolling; - private subtitlePollingInterval; - private _subtitleDisplay; - private asyncPollTrackChange; - constructor(hls: Hls); - destroy(): void; - get subtitleDisplay(): boolean; - set subtitleDisplay(value: boolean); - private registerListeners; - private unregisterListeners; - protected onMediaAttached(event: Events.MEDIA_ATTACHED, data: MediaAttachedData): void; - private pollTrackChange; - protected onMediaDetaching(event: Events.MEDIA_DETACHING, data: MediaDetachingData): void; - protected onManifestLoading(): void; - protected onManifestParsed(event: Events.MANIFEST_PARSED, data: ManifestParsedData): void; - protected onSubtitleTrackLoaded(event: Events.SUBTITLE_TRACK_LOADED, data: TrackLoadedData): void; - protected onLevelLoading(event: Events.LEVEL_LOADING, data: LevelLoadingData): void; - protected onLevelSwitching(event: Events.LEVEL_SWITCHING, data: LevelSwitchingData): void; - private switchLevel; - private findTrackId; - private findTrackForTextTrack; - protected onError(event: Events.ERROR, data: ErrorData): void; - get allSubtitleTracks(): MediaPlaylist[]; - /** get alternate subtitle tracks list from playlist **/ - get subtitleTracks(): MediaPlaylist[]; - /** get/set index of the selected subtitle track (based on index in subtitle track lists) **/ - get subtitleTrack(): number; - set subtitleTrack(newId: number); - setSubtitleOption(subtitleOption: MediaPlaylist | SubtitleSelectionOption | undefined): MediaPlaylist | null; - protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters): void; - /** - * Disables the old subtitleTrack and sets current mode on the next subtitleTrack. - * This operates on the DOM textTracks. - * A value of -1 will disable all subtitle tracks. - */ - private toggleTrackModes; - /** - * This method is responsible for validating the subtitle index and periodically reloading if live. - * Dispatches the SUBTITLE_TRACK_SWITCH event, which instructs the subtitle-stream-controller to load the selected track. - */ - private setSubtitleTrack; - private onTextTracksChanged; -} - -export declare interface SubtitleTrackLoadedData extends TrackLoadedData { -} - -export declare interface SubtitleTracksUpdatedData { - subtitleTracks: MediaPlaylist[]; -} - -export declare interface SubtitleTrackSwitchData { - id: number; - name?: string; - groupId?: string; - type?: MediaPlaylistType | 'main'; - url?: string; -} - -export declare interface SubtitleTrackUpdatedData { - details: LevelDetails; - id: number; - groupId: string; -} - -/** - * @ignore - * Sub-class specialization of EventHandler base class. - * - * TaskLoop allows to schedule a task function being called (optionnaly repeatedly) on the main loop, - * scheduled asynchroneously, avoiding recursive calls in the same tick. - * - * The task itself is implemented in `doTick`. It can be requested and called for single execution - * using the `tick` method. - * - * It will be assured that the task execution method (`tick`) only gets called once per main loop "tick", - * no matter how often it gets requested for execution. Execution in further ticks will be scheduled accordingly. - * - * If further execution requests have already been scheduled on the next tick, it can be checked with `hasNextTick`, - * and cancelled with `clearNextTick`. - * - * The task can be scheduled as an interval repeatedly with a period as parameter (see `setInterval`, `clearInterval`). - * - * Sub-classes need to implement the `doTick` method which will effectively have the task execution routine. - * - * Further explanations: - * - * The baseclass has a `tick` method that will schedule the doTick call. It may be called synchroneously - * only for a stack-depth of one. On re-entrant calls, sub-sequent calls are scheduled for next main loop ticks. - * - * When the task execution (`tick` method) is called in re-entrant way this is detected and - * we are limiting the task execution per call stack to exactly one, but scheduling/post-poning further - * task processing on the next main loop iteration (also known as "next tick" in the Node/JS runtime lingo). - */ -export declare class TaskLoop extends Logger { - private readonly _boundTick; - private _tickTimer; - private _tickInterval; - private _tickCallCount; - constructor(label: string, logger: ILogger); - destroy(): void; - protected onHandlerDestroying(): void; - protected onHandlerDestroyed(): void; - hasInterval(): boolean; - hasNextTick(): boolean; - /** - * @param millis - Interval time (ms) - * @eturns True when interval has been scheduled, false when already scheduled (no effect) - */ - setInterval(millis: number): boolean; - /** - * @returns True when interval was cleared, false when none was set (no effect) - */ - clearInterval(): boolean; - /** - * @returns True when timeout was cleared, false when none was set (no effect) - */ - clearNextTick(): boolean; - /** - * Will call the subclass doTick implementation in this main loop tick - * or in the next one (via setTimeout(,0)) in case it has already been called - * in this tick (in case this is a re-entrant call). - */ - tick(): void; - tickImmediate(): void; - /** - * For subclass to implement task logic - * @abstract - */ - protected doTick(): void; -} - -export declare class TimelineController implements ComponentAPI { - private hls; - private media; - private config; - private enabled; - private Cues; - private textTracks; - private tracks; - private initPTS; - private unparsedVttFrags; - private captionsTracks; - private nonNativeCaptionsTracks; - private cea608Parser1?; - private cea608Parser2?; - private lastCc; - private lastSn; - private lastPartIndex; - private prevCC; - private vttCCs; - private captionsProperties; - constructor(hls: Hls); - destroy(): void; - private initCea608Parsers; - addCues(trackName: string, startTime: number, endTime: number, screen: CaptionScreen, cueRanges: Array<[number, number]>): void; - private onInitPtsFound; - private getExistingTrack; - createCaptionsTrack(trackName: string): void; - private createNativeTrack; - private createNonNativeTrack; - private createTextTrack; - private onMediaAttaching; - private onMediaDetaching; - private onManifestLoading; - private _cleanTracks; - private onSubtitleTracksUpdated; - private onManifestLoaded; - private closedCaptionsForLevel; - private onFragLoading; - private onFragLoaded; - private _parseIMSC1; - private _parseVTTs; - private _fallbackToIMSC1; - private _appendCues; - private onFragDecrypted; - private onSubtitleTracksCleared; - private onFragParsingUserdata; - onBufferFlushing(event: Events.BUFFER_FLUSHING, { startOffset, endOffset, endOffsetSubtitles, type }: BufferFlushingData): void; - private extractCea608Data; -} - -export declare type TimelineControllerConfig = { - cueHandler: CuesInterface; - enableWebVTT: boolean; - enableIMSC1: boolean; - enableCEA708Captions: boolean; - captionsTextTrack1Label: string; - captionsTextTrack1LanguageCode: string; - captionsTextTrack2Label: string; - captionsTextTrack2LanguageCode: string; - captionsTextTrack3Label: string; - captionsTextTrack3LanguageCode: string; - captionsTextTrack4Label: string; - captionsTextTrack4LanguageCode: string; - renderTextTracksNatively: boolean; -}; - -export declare enum TimelineOccupancy { - Point = 0, - Range = 1 -} - -export declare interface Track extends BaseTrack { - buffer?: SourceBuffer; - initSegment?: Uint8Array; -} - -export declare interface TrackLoadedData { - details: LevelDetails; - id: number; - groupId: string; - networkDetails: any; - stats: LoaderStats; - deliveryDirectives: HlsUrlParameters | null; -} - -export declare interface TrackLoadingData { - id: number; - groupId: string; - url: string; - deliveryDirectives: HlsUrlParameters | null; -} - -export declare interface TrackSet { - audio?: Track; - video?: Track; - audiovideo?: Track; -} - -export declare class TransmuxerInterface { - error: Error | null; - private hls; - private id; - private instanceNo; - private observer; - private frag; - private part; - private useWorker; - private workerContext; - private transmuxer; - private onTransmuxComplete; - private onFlush; - constructor(hls: Hls, id: PlaylistLevelType, onTransmuxComplete: (transmuxResult: TransmuxerResult) => void, onFlush: (chunkMeta: ChunkMetadata) => void); - reset(): void; - private terminateWorker; - destroy(): void; - push(data: ArrayBuffer, initSegmentData: Uint8Array | undefined, audioCodec: string | undefined, videoCodec: string | undefined, frag: MediaFragment, part: Part | null, duration: number, accurateTimeOffset: boolean, chunkMeta: ChunkMetadata, defaultInitPTS?: RationalTimestamp): void; - flush(chunkMeta: ChunkMetadata): void; - private transmuxerError; - private handleFlushResult; - private onWorkerMessage; - private onWorkerError; - private configureTransmuxer; - private handleTransmuxComplete; -} - -export declare interface TransmuxerResult { - remuxResult: RemuxerResult; - chunkMeta: ChunkMetadata; -} - -export declare type TSDemuxerConfig = { - forceKeyFrameOnDiscontinuity: boolean; -}; - -export declare type UriReplacement = { - HOST?: string; - PARAMS?: { - [queryParameter: string]: string; - }; - 'PER-VARIANT-URIS'?: { - [stableVariantId: string]: string; - }; - 'PER-RENDITION-URIS'?: { - [stableRenditionId: string]: string; - }; -}; - -export declare interface UserdataSample { - pts: number; - bytes?: Uint8Array; - type?: number; - payloadType?: number; - uuid?: string; - userData?: string; - userDataBytes?: Uint8Array; -} - -export declare type VariableMap = Record; - -declare const enum VerboseLevel { - ERROR = 0, - TEXT = 1, - WARNING = 2, - INFO = 2, - DEBUG = 3, - DATA = 3 -} - -export declare type VideoRange = (typeof VideoRangeValues)[number]; - -export declare const VideoRangeValues: readonly ["SDR", "PQ", "HLG"]; - -export declare type VideoSelectionOption = { - preferHDR?: boolean; - allowedVideoRanges?: Array; - videoCodec?: string; -}; - -export { } diff --git a/extern/hls.js/hls.mjs b/extern/hls.js/hls.mjs deleted file mode 100644 index e003e768..00000000 --- a/extern/hls.js/hls.mjs +++ /dev/null @@ -1,33789 +0,0 @@ -function getDefaultExportFromCjs (x) { - return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; -} - -var urlToolkit = {exports: {}}; - -(function (module, exports) { - // see https://tools.ietf.org/html/rfc1808 - - (function (root) { - var URL_REGEX = - /^(?=((?:[a-zA-Z0-9+\-.]+:)?))\1(?=((?:\/\/[^\/?#]*)?))\2(?=((?:(?:[^?#\/]*\/)*[^;?#\/]*)?))\3((?:;[^?#]*)?)(\?[^#]*)?(#[^]*)?$/; - var FIRST_SEGMENT_REGEX = /^(?=([^\/?#]*))\1([^]*)$/; - var SLASH_DOT_REGEX = /(?:\/|^)\.(?=\/)/g; - var SLASH_DOT_DOT_REGEX = /(?:\/|^)\.\.\/(?!\.\.\/)[^\/]*(?=\/)/g; - - var URLToolkit = { - // If opts.alwaysNormalize is true then the path will always be normalized even when it starts with / or // - // E.g - // With opts.alwaysNormalize = false (default, spec compliant) - // http://a.com/b/cd + /e/f/../g => http://a.com/e/f/../g - // With opts.alwaysNormalize = true (not spec compliant) - // http://a.com/b/cd + /e/f/../g => http://a.com/e/g - buildAbsoluteURL: function (baseURL, relativeURL, opts) { - opts = opts || {}; - // remove any remaining space and CRLF - baseURL = baseURL.trim(); - relativeURL = relativeURL.trim(); - if (!relativeURL) { - // 2a) If the embedded URL is entirely empty, it inherits the - // entire base URL (i.e., is set equal to the base URL) - // and we are done. - if (!opts.alwaysNormalize) { - return baseURL; - } - var basePartsForNormalise = URLToolkit.parseURL(baseURL); - if (!basePartsForNormalise) { - throw new Error('Error trying to parse base URL.'); - } - basePartsForNormalise.path = URLToolkit.normalizePath( - basePartsForNormalise.path - ); - return URLToolkit.buildURLFromParts(basePartsForNormalise); - } - var relativeParts = URLToolkit.parseURL(relativeURL); - if (!relativeParts) { - throw new Error('Error trying to parse relative URL.'); - } - if (relativeParts.scheme) { - // 2b) If the embedded URL starts with a scheme name, it is - // interpreted as an absolute URL and we are done. - if (!opts.alwaysNormalize) { - return relativeURL; - } - relativeParts.path = URLToolkit.normalizePath(relativeParts.path); - return URLToolkit.buildURLFromParts(relativeParts); - } - var baseParts = URLToolkit.parseURL(baseURL); - if (!baseParts) { - throw new Error('Error trying to parse base URL.'); - } - if (!baseParts.netLoc && baseParts.path && baseParts.path[0] !== '/') { - // If netLoc missing and path doesn't start with '/', assume everthing before the first '/' is the netLoc - // This causes 'example.com/a' to be handled as '//example.com/a' instead of '/example.com/a' - var pathParts = FIRST_SEGMENT_REGEX.exec(baseParts.path); - baseParts.netLoc = pathParts[1]; - baseParts.path = pathParts[2]; - } - if (baseParts.netLoc && !baseParts.path) { - baseParts.path = '/'; - } - var builtParts = { - // 2c) Otherwise, the embedded URL inherits the scheme of - // the base URL. - scheme: baseParts.scheme, - netLoc: relativeParts.netLoc, - path: null, - params: relativeParts.params, - query: relativeParts.query, - fragment: relativeParts.fragment, - }; - if (!relativeParts.netLoc) { - // 3) If the embedded URL's is non-empty, we skip to - // Step 7. Otherwise, the embedded URL inherits the - // (if any) of the base URL. - builtParts.netLoc = baseParts.netLoc; - // 4) If the embedded URL path is preceded by a slash "/", the - // path is not relative and we skip to Step 7. - if (relativeParts.path[0] !== '/') { - if (!relativeParts.path) { - // 5) If the embedded URL path is empty (and not preceded by a - // slash), then the embedded URL inherits the base URL path - builtParts.path = baseParts.path; - // 5a) if the embedded URL's is non-empty, we skip to - // step 7; otherwise, it inherits the of the base - // URL (if any) and - if (!relativeParts.params) { - builtParts.params = baseParts.params; - // 5b) if the embedded URL's is non-empty, we skip to - // step 7; otherwise, it inherits the of the base - // URL (if any) and we skip to step 7. - if (!relativeParts.query) { - builtParts.query = baseParts.query; - } - } - } else { - // 6) The last segment of the base URL's path (anything - // following the rightmost slash "/", or the entire path if no - // slash is present) is removed and the embedded URL's path is - // appended in its place. - var baseURLPath = baseParts.path; - var newPath = - baseURLPath.substring(0, baseURLPath.lastIndexOf('/') + 1) + - relativeParts.path; - builtParts.path = URLToolkit.normalizePath(newPath); - } - } - } - if (builtParts.path === null) { - builtParts.path = opts.alwaysNormalize - ? URLToolkit.normalizePath(relativeParts.path) - : relativeParts.path; - } - return URLToolkit.buildURLFromParts(builtParts); - }, - parseURL: function (url) { - var parts = URL_REGEX.exec(url); - if (!parts) { - return null; - } - return { - scheme: parts[1] || '', - netLoc: parts[2] || '', - path: parts[3] || '', - params: parts[4] || '', - query: parts[5] || '', - fragment: parts[6] || '', - }; - }, - normalizePath: function (path) { - // The following operations are - // then applied, in order, to the new path: - // 6a) All occurrences of "./", where "." is a complete path - // segment, are removed. - // 6b) If the path ends with "." as a complete path segment, - // that "." is removed. - path = path.split('').reverse().join('').replace(SLASH_DOT_REGEX, ''); - // 6c) All occurrences of "/../", where is a - // complete path segment not equal to "..", are removed. - // Removal of these path segments is performed iteratively, - // removing the leftmost matching pattern on each iteration, - // until no matching pattern remains. - // 6d) If the path ends with "/..", where is a - // complete path segment not equal to "..", that - // "/.." is removed. - while ( - path.length !== (path = path.replace(SLASH_DOT_DOT_REGEX, '')).length - ) {} - return path.split('').reverse().join(''); - }, - buildURLFromParts: function (parts) { - return ( - parts.scheme + - parts.netLoc + - parts.path + - parts.params + - parts.query + - parts.fragment - ); - }, - }; - - module.exports = URLToolkit; - })(); -} (urlToolkit)); - -var urlToolkitExports = urlToolkit.exports; - -function _defineProperty(e, r, t) { - return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { - value: t, - enumerable: !0, - configurable: !0, - writable: !0 - }) : e[r] = t, e; -} -function _extends() { - return _extends = Object.assign ? Object.assign.bind() : function (n) { - for (var e = 1; e < arguments.length; e++) { - var t = arguments[e]; - for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); - } - return n; - }, _extends.apply(null, arguments); -} -function ownKeys(e, r) { - var t = Object.keys(e); - if (Object.getOwnPropertySymbols) { - var o = Object.getOwnPropertySymbols(e); - r && (o = o.filter(function (r) { - return Object.getOwnPropertyDescriptor(e, r).enumerable; - })), t.push.apply(t, o); - } - return t; -} -function _objectSpread2(e) { - for (var r = 1; r < arguments.length; r++) { - var t = null != arguments[r] ? arguments[r] : {}; - r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { - _defineProperty(e, r, t[r]); - }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { - Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); - }); - } - return e; -} -function _toPrimitive(t, r) { - if ("object" != typeof t || !t) return t; - var e = t[Symbol.toPrimitive]; - if (void 0 !== e) { - var i = e.call(t, r || "default"); - if ("object" != typeof i) return i; - throw new TypeError("@@toPrimitive must return a primitive value."); - } - return ("string" === r ? String : Number)(t); -} -function _toPropertyKey(t) { - var i = _toPrimitive(t, "string"); - return "symbol" == typeof i ? i : i + ""; -} - -// https://caniuse.com/mdn-javascript_builtins_number_isfinite -const isFiniteNumber = Number.isFinite || function (value) { - return typeof value === 'number' && isFinite(value); -}; - -// https://caniuse.com/mdn-javascript_builtins_number_issafeinteger -const isSafeInteger = Number.isSafeInteger || function (value) { - return typeof value === 'number' && Math.abs(value) <= MAX_SAFE_INTEGER; -}; -const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991; - -let Events = /*#__PURE__*/function (Events) { - Events["MEDIA_ATTACHING"] = "hlsMediaAttaching"; - Events["MEDIA_ATTACHED"] = "hlsMediaAttached"; - Events["MEDIA_DETACHING"] = "hlsMediaDetaching"; - Events["MEDIA_DETACHED"] = "hlsMediaDetached"; - Events["MEDIA_ENDED"] = "hlsMediaEnded"; - Events["BUFFER_RESET"] = "hlsBufferReset"; - Events["BUFFER_CODECS"] = "hlsBufferCodecs"; - Events["BUFFER_CREATED"] = "hlsBufferCreated"; - Events["BUFFER_APPENDING"] = "hlsBufferAppending"; - Events["BUFFER_APPENDED"] = "hlsBufferAppended"; - Events["BUFFER_EOS"] = "hlsBufferEos"; - Events["BUFFERED_TO_END"] = "hlsBufferedToEnd"; - Events["BUFFER_FLUSHING"] = "hlsBufferFlushing"; - Events["BUFFER_FLUSHED"] = "hlsBufferFlushed"; - Events["MANIFEST_LOADING"] = "hlsManifestLoading"; - Events["MANIFEST_LOADED"] = "hlsManifestLoaded"; - Events["MANIFEST_PARSED"] = "hlsManifestParsed"; - Events["LEVEL_SWITCHING"] = "hlsLevelSwitching"; - Events["LEVEL_SWITCHED"] = "hlsLevelSwitched"; - Events["LEVEL_LOADING"] = "hlsLevelLoading"; - Events["LEVEL_LOADED"] = "hlsLevelLoaded"; - Events["LEVEL_UPDATED"] = "hlsLevelUpdated"; - Events["LEVEL_PTS_UPDATED"] = "hlsLevelPtsUpdated"; - Events["LEVELS_UPDATED"] = "hlsLevelsUpdated"; - Events["AUDIO_TRACKS_UPDATED"] = "hlsAudioTracksUpdated"; - Events["AUDIO_TRACK_SWITCHING"] = "hlsAudioTrackSwitching"; - Events["AUDIO_TRACK_SWITCHED"] = "hlsAudioTrackSwitched"; - Events["AUDIO_TRACK_LOADING"] = "hlsAudioTrackLoading"; - Events["AUDIO_TRACK_LOADED"] = "hlsAudioTrackLoaded"; - Events["AUDIO_TRACK_UPDATED"] = "hlsAudioTrackUpdated"; - Events["SUBTITLE_TRACKS_UPDATED"] = "hlsSubtitleTracksUpdated"; - Events["SUBTITLE_TRACKS_CLEARED"] = "hlsSubtitleTracksCleared"; - Events["SUBTITLE_TRACK_SWITCH"] = "hlsSubtitleTrackSwitch"; - Events["SUBTITLE_TRACK_LOADING"] = "hlsSubtitleTrackLoading"; - Events["SUBTITLE_TRACK_LOADED"] = "hlsSubtitleTrackLoaded"; - Events["SUBTITLE_TRACK_UPDATED"] = "hlsSubtitleTrackUpdated"; - Events["SUBTITLE_FRAG_PROCESSED"] = "hlsSubtitleFragProcessed"; - Events["CUES_PARSED"] = "hlsCuesParsed"; - Events["NON_NATIVE_TEXT_TRACKS_FOUND"] = "hlsNonNativeTextTracksFound"; - Events["INIT_PTS_FOUND"] = "hlsInitPtsFound"; - Events["FRAG_LOADING"] = "hlsFragLoading"; - Events["FRAG_LOAD_EMERGENCY_ABORTED"] = "hlsFragLoadEmergencyAborted"; - Events["FRAG_LOADED"] = "hlsFragLoaded"; - Events["FRAG_DECRYPTED"] = "hlsFragDecrypted"; - Events["FRAG_PARSING_INIT_SEGMENT"] = "hlsFragParsingInitSegment"; - Events["FRAG_PARSING_USERDATA"] = "hlsFragParsingUserdata"; - Events["FRAG_PARSING_METADATA"] = "hlsFragParsingMetadata"; - Events["FRAG_PARSED"] = "hlsFragParsed"; - Events["FRAG_BUFFERED"] = "hlsFragBuffered"; - Events["FRAG_CHANGED"] = "hlsFragChanged"; - Events["FPS_DROP"] = "hlsFpsDrop"; - Events["FPS_DROP_LEVEL_CAPPING"] = "hlsFpsDropLevelCapping"; - Events["MAX_AUTO_LEVEL_UPDATED"] = "hlsMaxAutoLevelUpdated"; - Events["ERROR"] = "hlsError"; - Events["DESTROYING"] = "hlsDestroying"; - Events["KEY_LOADING"] = "hlsKeyLoading"; - Events["KEY_LOADED"] = "hlsKeyLoaded"; - Events["LIVE_BACK_BUFFER_REACHED"] = "hlsLiveBackBufferReached"; - Events["BACK_BUFFER_REACHED"] = "hlsBackBufferReached"; - Events["STEERING_MANIFEST_LOADED"] = "hlsSteeringManifestLoaded"; - Events["ASSET_LIST_LOADING"] = "hlsAssetListLoading"; - Events["ASSET_LIST_LOADED"] = "hlsAssetListLoaded"; - Events["INTERSTITIALS_UPDATED"] = "hlsInterstitialsUpdated"; - Events["INTERSTITIALS_BUFFERED_TO_BOUNDARY"] = "hlsInterstitialsBufferedToBoundary"; - Events["INTERSTITIAL_ASSET_PLAYER_CREATED"] = "hlsInterstitialAssetPlayerCreated"; - Events["INTERSTITIAL_STARTED"] = "hlsInterstitialStarted"; - Events["INTERSTITIAL_ASSET_STARTED"] = "hlsInterstitialAssetStarted"; - Events["INTERSTITIAL_ASSET_ENDED"] = "hlsInterstitialAssetEnded"; - Events["INTERSTITIAL_ASSET_ERROR"] = "hlsInterstitialAssetError"; - Events["INTERSTITIAL_ENDED"] = "hlsInterstitialEnded"; - Events["INTERSTITIALS_PRIMARY_RESUMED"] = "hlsInterstitialsPrimaryResumed"; - Events["PLAYOUT_LIMIT_REACHED"] = "hlsPlayoutLimitReached"; - return Events; -}({}); - -/** - * Defines each Event type and payload by Event name. Used in {@link hls.js#HlsEventEmitter} to strongly type the event listener API. - */ - -let ErrorTypes = /*#__PURE__*/function (ErrorTypes) { - ErrorTypes["NETWORK_ERROR"] = "networkError"; - ErrorTypes["MEDIA_ERROR"] = "mediaError"; - ErrorTypes["KEY_SYSTEM_ERROR"] = "keySystemError"; - ErrorTypes["MUX_ERROR"] = "muxError"; - ErrorTypes["OTHER_ERROR"] = "otherError"; - return ErrorTypes; -}({}); -let ErrorDetails = /*#__PURE__*/function (ErrorDetails) { - ErrorDetails["KEY_SYSTEM_NO_KEYS"] = "keySystemNoKeys"; - ErrorDetails["KEY_SYSTEM_NO_ACCESS"] = "keySystemNoAccess"; - ErrorDetails["KEY_SYSTEM_NO_SESSION"] = "keySystemNoSession"; - ErrorDetails["KEY_SYSTEM_NO_CONFIGURED_LICENSE"] = "keySystemNoConfiguredLicense"; - ErrorDetails["KEY_SYSTEM_LICENSE_REQUEST_FAILED"] = "keySystemLicenseRequestFailed"; - ErrorDetails["KEY_SYSTEM_SERVER_CERTIFICATE_REQUEST_FAILED"] = "keySystemServerCertificateRequestFailed"; - ErrorDetails["KEY_SYSTEM_SERVER_CERTIFICATE_UPDATE_FAILED"] = "keySystemServerCertificateUpdateFailed"; - ErrorDetails["KEY_SYSTEM_SESSION_UPDATE_FAILED"] = "keySystemSessionUpdateFailed"; - ErrorDetails["KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED"] = "keySystemStatusOutputRestricted"; - ErrorDetails["KEY_SYSTEM_STATUS_INTERNAL_ERROR"] = "keySystemStatusInternalError"; - ErrorDetails["MANIFEST_LOAD_ERROR"] = "manifestLoadError"; - ErrorDetails["MANIFEST_LOAD_TIMEOUT"] = "manifestLoadTimeOut"; - ErrorDetails["MANIFEST_PARSING_ERROR"] = "manifestParsingError"; - ErrorDetails["MANIFEST_INCOMPATIBLE_CODECS_ERROR"] = "manifestIncompatibleCodecsError"; - ErrorDetails["LEVEL_EMPTY_ERROR"] = "levelEmptyError"; - ErrorDetails["LEVEL_LOAD_ERROR"] = "levelLoadError"; - ErrorDetails["LEVEL_LOAD_TIMEOUT"] = "levelLoadTimeOut"; - ErrorDetails["LEVEL_PARSING_ERROR"] = "levelParsingError"; - ErrorDetails["LEVEL_SWITCH_ERROR"] = "levelSwitchError"; - ErrorDetails["AUDIO_TRACK_LOAD_ERROR"] = "audioTrackLoadError"; - ErrorDetails["AUDIO_TRACK_LOAD_TIMEOUT"] = "audioTrackLoadTimeOut"; - ErrorDetails["SUBTITLE_LOAD_ERROR"] = "subtitleTrackLoadError"; - ErrorDetails["SUBTITLE_TRACK_LOAD_TIMEOUT"] = "subtitleTrackLoadTimeOut"; - ErrorDetails["FRAG_LOAD_ERROR"] = "fragLoadError"; - ErrorDetails["FRAG_LOAD_TIMEOUT"] = "fragLoadTimeOut"; - ErrorDetails["FRAG_DECRYPT_ERROR"] = "fragDecryptError"; - ErrorDetails["FRAG_PARSING_ERROR"] = "fragParsingError"; - ErrorDetails["FRAG_GAP"] = "fragGap"; - ErrorDetails["REMUX_ALLOC_ERROR"] = "remuxAllocError"; - ErrorDetails["KEY_LOAD_ERROR"] = "keyLoadError"; - ErrorDetails["KEY_LOAD_TIMEOUT"] = "keyLoadTimeOut"; - ErrorDetails["BUFFER_ADD_CODEC_ERROR"] = "bufferAddCodecError"; - ErrorDetails["BUFFER_INCOMPATIBLE_CODECS_ERROR"] = "bufferIncompatibleCodecsError"; - ErrorDetails["BUFFER_APPEND_ERROR"] = "bufferAppendError"; - ErrorDetails["BUFFER_APPENDING_ERROR"] = "bufferAppendingError"; - ErrorDetails["BUFFER_STALLED_ERROR"] = "bufferStalledError"; - ErrorDetails["BUFFER_FULL_ERROR"] = "bufferFullError"; - ErrorDetails["BUFFER_SEEK_OVER_HOLE"] = "bufferSeekOverHole"; - ErrorDetails["BUFFER_NUDGE_ON_STALL"] = "bufferNudgeOnStall"; - ErrorDetails["ASSET_LIST_LOAD_ERROR"] = "assetListLoadError"; - ErrorDetails["ASSET_LIST_LOAD_TIMEOUT"] = "assetListLoadTimeout"; - ErrorDetails["ASSET_LIST_PARSING_ERROR"] = "assetListParsingError"; - ErrorDetails["INTERSTITIAL_ASSET_ITEM_ERROR"] = "interstitialAssetItemError"; - ErrorDetails["INTERNAL_EXCEPTION"] = "internalException"; - ErrorDetails["INTERNAL_ABORTED"] = "aborted"; - ErrorDetails["ATTACH_MEDIA_ERROR"] = "attachMediaError"; - ErrorDetails["UNKNOWN"] = "unknown"; - return ErrorDetails; -}({}); - -class Logger { - constructor(label, logger) { - this.trace = void 0; - this.debug = void 0; - this.log = void 0; - this.warn = void 0; - this.info = void 0; - this.error = void 0; - const lb = `[${label}]:`; - this.trace = noop; - this.debug = logger.debug.bind(null, lb); - this.log = logger.log.bind(null, lb); - this.warn = logger.warn.bind(null, lb); - this.info = logger.info.bind(null, lb); - this.error = logger.error.bind(null, lb); - } -} -const noop = function noop() {}; -const fakeLogger = { - trace: noop, - debug: noop, - log: noop, - warn: noop, - info: noop, - error: noop -}; -function createLogger() { - return _extends({}, fakeLogger); -} - -// let lastCallTime; -// function formatMsgWithTimeInfo(type, msg) { -// const now = Date.now(); -// const diff = lastCallTime ? '+' + (now - lastCallTime) : '0'; -// lastCallTime = now; -// msg = (new Date(now)).toISOString() + ' | [' + type + '] > ' + msg + ' ( ' + diff + ' ms )'; -// return msg; -// } - -function consolePrintFn(type, id) { - const func = self.console[type]; - return func ? func.bind(self.console, `${id ? '[' + id + '] ' : ''}[${type}] >`) : noop; -} -function getLoggerFn(key, debugConfig, id) { - return debugConfig[key] ? debugConfig[key].bind(debugConfig) : consolePrintFn(key, id); -} -const exportedLogger = createLogger(); -function enableLogs(debugConfig, context, id) { - // check that console is available - const newLogger = createLogger(); - if (typeof console === 'object' && debugConfig === true || typeof debugConfig === 'object') { - const keys = [ - // Remove out from list here to hard-disable a log-level - // 'trace', - 'debug', 'log', 'info', 'warn', 'error']; - keys.forEach(key => { - newLogger[key] = getLoggerFn(key, debugConfig, id); - }); - // Some browsers don't allow to use bind on console object anyway - // fallback to default if needed - try { - newLogger.log(`Debug logs enabled for "${context}" in hls.js version ${undefined}`); - } catch (e) { - /* log fn threw an exception. All logger methods are no-ops. */ - return createLogger(); - } - // global exported logger uses the same functions as new logger without `id` - keys.forEach(key => { - exportedLogger[key] = getLoggerFn(key, debugConfig); - }); - } else { - // Reset global exported logger - _extends(exportedLogger, newLogger); - } - return newLogger; -} -const logger = exportedLogger; - -const VARIABLE_REPLACEMENT_REGEX = /\{\$([a-zA-Z0-9-_]+)\}/g; -function hasVariableReferences(str) { - return VARIABLE_REPLACEMENT_REGEX.test(str); -} -function substituteVariables(parsed, value) { - if (parsed.variableList !== null || parsed.hasVariableRefs) { - const variableList = parsed.variableList; - return value.replace(VARIABLE_REPLACEMENT_REGEX, variableReference => { - const variableName = variableReference.substring(2, variableReference.length - 1); - const variableValue = variableList == null ? void 0 : variableList[variableName]; - if (variableValue === undefined) { - parsed.playlistParsingError || (parsed.playlistParsingError = new Error(`Missing preceding EXT-X-DEFINE tag for Variable Reference: "${variableName}"`)); - return variableReference; - } - return variableValue; - }); - } - return value; -} -function addVariableDefinition(parsed, attr, parentUrl) { - let variableList = parsed.variableList; - if (!variableList) { - parsed.variableList = variableList = {}; - } - let NAME; - let VALUE; - if ('QUERYPARAM' in attr) { - NAME = attr.QUERYPARAM; - try { - const searchParams = new self.URL(parentUrl).searchParams; - if (searchParams.has(NAME)) { - VALUE = searchParams.get(NAME); - } else { - throw new Error(`"${NAME}" does not match any query parameter in URI: "${parentUrl}"`); - } - } catch (error) { - parsed.playlistParsingError || (parsed.playlistParsingError = new Error(`EXT-X-DEFINE QUERYPARAM: ${error.message}`)); - } - } else { - NAME = attr.NAME; - VALUE = attr.VALUE; - } - if (NAME in variableList) { - parsed.playlistParsingError || (parsed.playlistParsingError = new Error(`EXT-X-DEFINE duplicate Variable Name declarations: "${NAME}"`)); - } else { - variableList[NAME] = VALUE || ''; - } -} -function importVariableDefinition(parsed, attr, sourceVariableList) { - const IMPORT = attr.IMPORT; - if (sourceVariableList && IMPORT in sourceVariableList) { - let variableList = parsed.variableList; - if (!variableList) { - parsed.variableList = variableList = {}; - } - variableList[IMPORT] = sourceVariableList[IMPORT]; - } else { - parsed.playlistParsingError || (parsed.playlistParsingError = new Error(`EXT-X-DEFINE IMPORT attribute not found in Multivariant Playlist: "${IMPORT}"`)); - } -} - -const DECIMAL_RESOLUTION_REGEX = /^(\d+)x(\d+)$/; -const ATTR_LIST_REGEX = /(.+?)=(".*?"|.*?)(?:,|$)/g; - -// adapted from https://github.com/kanongil/node-m3u8parse/blob/master/attrlist.js -class AttrList { - constructor(attrs, parsed) { - if (typeof attrs === 'string') { - attrs = AttrList.parseAttrList(attrs, parsed); - } - _extends(this, attrs); - } - get clientAttrs() { - return Object.keys(this).filter(attr => attr.substring(0, 2) === 'X-'); - } - decimalInteger(attrName) { - const intValue = parseInt(this[attrName], 10); - if (intValue > Number.MAX_SAFE_INTEGER) { - return Infinity; - } - return intValue; - } - hexadecimalInteger(attrName) { - if (this[attrName]) { - let stringValue = (this[attrName] || '0x').slice(2); - stringValue = (stringValue.length & 1 ? '0' : '') + stringValue; - const value = new Uint8Array(stringValue.length / 2); - for (let i = 0; i < stringValue.length / 2; i++) { - value[i] = parseInt(stringValue.slice(i * 2, i * 2 + 2), 16); - } - return value; - } else { - return null; - } - } - hexadecimalIntegerAsNumber(attrName) { - const intValue = parseInt(this[attrName], 16); - if (intValue > Number.MAX_SAFE_INTEGER) { - return Infinity; - } - return intValue; - } - decimalFloatingPoint(attrName) { - return parseFloat(this[attrName]); - } - optionalFloat(attrName, defaultValue) { - const value = this[attrName]; - return value ? parseFloat(value) : defaultValue; - } - enumeratedString(attrName) { - return this[attrName]; - } - enumeratedStringList(attrName, dict) { - const attrValue = this[attrName]; - return (attrValue ? attrValue.split(/[ ,]+/) : []).reduce((result, identifier) => { - result[identifier.toLowerCase()] = true; - return result; - }, dict); - } - bool(attrName) { - return this[attrName] === 'YES'; - } - decimalResolution(attrName) { - const res = DECIMAL_RESOLUTION_REGEX.exec(this[attrName]); - if (res === null) { - return undefined; - } - return { - width: parseInt(res[1], 10), - height: parseInt(res[2], 10) - }; - } - static parseAttrList(input, parsed) { - let match; - const attrs = {}; - const quote = '"'; - ATTR_LIST_REGEX.lastIndex = 0; - while ((match = ATTR_LIST_REGEX.exec(input)) !== null) { - const name = match[1].trim(); - let value = match[2]; - const quotedString = value.indexOf(quote) === 0 && value.lastIndexOf(quote) === value.length - 1; - let hexadecimalSequence = false; - if (quotedString) { - value = value.slice(1, -1); - } else { - switch (name) { - case 'IV': - case 'SCTE35-CMD': - case 'SCTE35-IN': - case 'SCTE35-OUT': - hexadecimalSequence = true; - } - } - if (parsed && (quotedString || hexadecimalSequence)) { - { - value = substituteVariables(parsed, value); - } - } else if (!hexadecimalSequence && !quotedString) { - switch (name) { - case 'CLOSED-CAPTIONS': - if (value === 'NONE') { - break; - } - // falls through - case 'ALLOWED-CPC': - case 'CLASS': - case 'ASSOC-LANGUAGE': - case 'AUDIO': - case 'BYTERANGE': - case 'CHANNELS': - case 'CHARACTERISTICS': - case 'CODECS': - case 'DATA-ID': - case 'END-DATE': - case 'GROUP-ID': - case 'ID': - case 'IMPORT': - case 'INSTREAM-ID': - case 'KEYFORMAT': - case 'KEYFORMATVERSIONS': - case 'LANGUAGE': - case 'NAME': - case 'PATHWAY-ID': - case 'QUERYPARAM': - case 'RECENTLY-REMOVED-DATERANGES': - case 'SERVER-URI': - case 'STABLE-RENDITION-ID': - case 'STABLE-VARIANT-ID': - case 'START-DATE': - case 'SUBTITLES': - case 'SUPPLEMENTAL-CODECS': - case 'URI': - case 'VALUE': - case 'VIDEO': - case 'X-ASSET-LIST': - case 'X-ASSET-URI': - // Since we are not checking tag:attribute combination, just warn rather than ignoring attribute - logger.warn(`${input}: attribute ${name} is missing quotes`); - // continue; - } - } - attrs[name] = value; - } - return attrs; - } -} - -// Avoid exporting const enum so that these values can be inlined - -const CLASS_INTERSTITIAL = 'com.apple.hls.interstitial'; -function isDateRangeCueAttribute(attrName) { - return attrName !== "ID" && attrName !== "CLASS" && attrName !== "CUE" && attrName !== "START-DATE" && attrName !== "DURATION" && attrName !== "END-DATE" && attrName !== "END-ON-NEXT"; -} -function isSCTE35Attribute(attrName) { - return attrName === "SCTE35-OUT" || attrName === "SCTE35-IN" || attrName === "SCTE35-CMD"; -} -class DateRange { - constructor(dateRangeAttr, dateRangeWithSameId, tagCount = 0) { - var _dateRangeWithSameId$; - this.attr = void 0; - this.tagAnchor = void 0; - this.tagOrder = void 0; - this._startDate = void 0; - this._endDate = void 0; - this._dateAtEnd = void 0; - this._cue = void 0; - this._badValueForSameId = void 0; - this.tagAnchor = (dateRangeWithSameId == null ? void 0 : dateRangeWithSameId.tagAnchor) || null; - this.tagOrder = (_dateRangeWithSameId$ = dateRangeWithSameId == null ? void 0 : dateRangeWithSameId.tagOrder) != null ? _dateRangeWithSameId$ : tagCount; - if (dateRangeWithSameId) { - const previousAttr = dateRangeWithSameId.attr; - for (const key in previousAttr) { - if (Object.prototype.hasOwnProperty.call(dateRangeAttr, key) && dateRangeAttr[key] !== previousAttr[key]) { - logger.warn(`DATERANGE tag attribute: "${key}" does not match for tags with ID: "${dateRangeAttr.ID}"`); - this._badValueForSameId = key; - break; - } - } - // Merge DateRange tags with the same ID - dateRangeAttr = _extends(new AttrList({}), previousAttr, dateRangeAttr); - } - this.attr = dateRangeAttr; - if (dateRangeWithSameId) { - this._startDate = dateRangeWithSameId._startDate; - this._cue = dateRangeWithSameId._cue; - this._endDate = dateRangeWithSameId._endDate; - this._dateAtEnd = dateRangeWithSameId._dateAtEnd; - } else { - this._startDate = new Date(dateRangeAttr["START-DATE"]); - } - if ("END-DATE" in this.attr) { - const endDate = (dateRangeWithSameId == null ? void 0 : dateRangeWithSameId.endDate) || new Date(this.attr["END-DATE"]); - if (isFiniteNumber(endDate.getTime())) { - this._endDate = endDate; - } - } - } - get id() { - return this.attr.ID; - } - get class() { - return this.attr.CLASS; - } - get cue() { - const _cue = this._cue; - if (_cue === undefined) { - return this._cue = this.attr.enumeratedStringList(this.attr.CUE ? 'CUE' : 'X-CUE', { - pre: false, - post: false, - once: false - }); - } - return _cue; - } - get startTime() { - const { - tagAnchor - } = this; - // eslint-disable-next-line @typescript-eslint/prefer-optional-chain - if (tagAnchor === null || tagAnchor.programDateTime === null) { - logger.warn(`Expected tagAnchor Fragment with PDT set for DateRange "${this.id}": ${tagAnchor}`); - return NaN; - } - return tagAnchor.start + (this.startDate.getTime() - tagAnchor.programDateTime) / 1000; - } - get startDate() { - return this._startDate; - } - get endDate() { - const dateAtEnd = this._endDate || this._dateAtEnd; - if (dateAtEnd) { - return dateAtEnd; - } - const duration = this.duration; - if (duration !== null) { - return this._dateAtEnd = new Date(this._startDate.getTime() + duration * 1000); - } - return null; - } - get duration() { - if ("DURATION" in this.attr) { - const duration = this.attr.decimalFloatingPoint("DURATION"); - if (isFiniteNumber(duration)) { - return duration; - } - } else if (this._endDate) { - return (this._endDate.getTime() - this._startDate.getTime()) / 1000; - } - return null; - } - get plannedDuration() { - if ("PLANNED-DURATION" in this.attr) { - return this.attr.decimalFloatingPoint("PLANNED-DURATION"); - } - return null; - } - get endOnNext() { - return this.attr.bool("END-ON-NEXT"); - } - get isInterstitial() { - return this.class === CLASS_INTERSTITIAL; - } - get isValid() { - return !!this.id && !this._badValueForSameId && isFiniteNumber(this.startDate.getTime()) && (this.duration === null || this.duration >= 0) && (!this.endOnNext || !!this.class) && (!this.attr.CUE || !this.cue.pre && !this.cue.post || this.cue.pre !== this.cue.post) && (!this.isInterstitial || 'X-ASSET-URI' in this.attr || 'X-ASSET-LIST' in this.attr); - } -} - -class LoadStats { - constructor() { - this.aborted = false; - this.loaded = 0; - this.retry = 0; - this.total = 0; - this.chunkCount = 0; - this.bwEstimate = 0; - this.loading = { - start: 0, - first: 0, - end: 0 - }; - this.parsing = { - start: 0, - end: 0 - }; - this.buffering = { - start: 0, - first: 0, - end: 0 - }; - } -} - -var ElementaryStreamTypes = { - AUDIO: "audio", - VIDEO: "video", - AUDIOVIDEO: "audiovideo" -}; -class BaseSegment { - constructor(baseurl) { - this._byteRange = null; - this._url = null; - // baseurl is the URL to the playlist - this.baseurl = void 0; - // relurl is the portion of the URL that comes from inside the playlist. - this.relurl = void 0; - // Holds the types of data this fragment supports - this.elementaryStreams = { - [ElementaryStreamTypes.AUDIO]: null, - [ElementaryStreamTypes.VIDEO]: null, - [ElementaryStreamTypes.AUDIOVIDEO]: null - }; - this.baseurl = baseurl; - } - - // setByteRange converts a EXT-X-BYTERANGE attribute into a two element array - setByteRange(value, previous) { - const params = value.split('@', 2); - let start; - if (params.length === 1) { - start = (previous == null ? void 0 : previous.byteRangeEndOffset) || 0; - } else { - start = parseInt(params[1]); - } - this._byteRange = [start, parseInt(params[0]) + start]; - } - get byteRange() { - if (!this._byteRange) { - return []; - } - return this._byteRange; - } - get byteRangeStartOffset() { - return this.byteRange[0]; - } - get byteRangeEndOffset() { - return this.byteRange[1]; - } - get url() { - if (!this._url && this.baseurl && this.relurl) { - this._url = urlToolkitExports.buildAbsoluteURL(this.baseurl, this.relurl, { - alwaysNormalize: true - }); - } - return this._url || ''; - } - set url(value) { - this._url = value; - } - clearElementaryStreamInfo() { - const { - elementaryStreams - } = this; - elementaryStreams[ElementaryStreamTypes.AUDIO] = null; - elementaryStreams[ElementaryStreamTypes.VIDEO] = null; - elementaryStreams[ElementaryStreamTypes.AUDIOVIDEO] = null; - } -} -/** - * Object representing parsed data from an HLS Segment. Found in {@link hls.js#LevelDetails.fragments}. - */ -class Fragment extends BaseSegment { - constructor(type, baseurl) { - super(baseurl); - this._decryptdata = null; - this.rawProgramDateTime = null; - this.programDateTime = null; - this.tagList = []; - // EXTINF has to be present for a m3u8 to be considered valid - this.duration = 0; - // sn notates the sequence number for a segment, and if set to a string can be 'initSegment' - this.sn = 0; - // levelkeys are the EXT-X-KEY tags that apply to this segment for decryption - // core difference from the private field _decryptdata is the lack of the initialized IV - // _decryptdata will set the IV for this segment based on the segment number in the fragment - this.levelkeys = void 0; - // A string representing the fragment type - this.type = void 0; - // A reference to the loader. Set while the fragment is loading, and removed afterwards. Used to abort fragment loading - this.loader = null; - // A reference to the key loader. Set while the key is loading, and removed afterwards. Used to abort key loading - this.keyLoader = null; - // The level/track index to which the fragment belongs - this.level = -1; - // The continuity counter of the fragment - this.cc = 0; - // The starting Presentation Time Stamp (PTS) of the fragment. Set after transmux complete. - this.startPTS = void 0; - // The ending Presentation Time Stamp (PTS) of the fragment. Set after transmux complete. - this.endPTS = void 0; - // The starting Decode Time Stamp (DTS) of the fragment. Set after transmux complete. - this.startDTS = void 0; - // The ending Decode Time Stamp (DTS) of the fragment. Set after transmux complete. - this.endDTS = void 0; - // The start time of the fragment, as listed in the manifest. Updated after transmux complete. - this.start = 0; - // The offset time (seconds) of the fragment from the start of the Playlist - this.playlistOffset = 0; - // Set by `updateFragPTSDTS` in level-helper - this.deltaPTS = void 0; - // The maximum starting Presentation Time Stamp (audio/video PTS) of the fragment. Set after transmux complete. - this.maxStartPTS = void 0; - // The minimum ending Presentation Time Stamp (audio/video PTS) of the fragment. Set after transmux complete. - this.minEndPTS = void 0; - // Load/parse timing information - this.stats = new LoadStats(); - // Init Segment bytes (unset for media segments) - this.data = void 0; - // A flag indicating whether the segment was downloaded in order to test bitrate, and was not buffered - this.bitrateTest = false; - // #EXTINF segment title - this.title = null; - // The Media Initialization Section for this segment - this.initSegment = null; - // Fragment is the last fragment in the media playlist - this.endList = void 0; - // Fragment is marked by an EXT-X-GAP tag indicating that it does not contain media data and should not be loaded - this.gap = void 0; - // Deprecated - this.urlId = 0; - this.type = type; - } - get decryptdata() { - const { - levelkeys - } = this; - if (!levelkeys && !this._decryptdata) { - return null; - } - if (!this._decryptdata && this.levelkeys && !this.levelkeys.NONE) { - const key = this.levelkeys.identity; - if (key) { - this._decryptdata = key.getDecryptData(this.sn); - } else { - const keyFormats = Object.keys(this.levelkeys); - if (keyFormats.length === 1) { - return this._decryptdata = this.levelkeys[keyFormats[0]].getDecryptData(this.sn); - } - } - } - return this._decryptdata; - } - get end() { - return this.start + this.duration; - } - get endProgramDateTime() { - if (this.programDateTime === null) { - return null; - } - if (!isFiniteNumber(this.programDateTime)) { - return null; - } - const duration = !isFiniteNumber(this.duration) ? 0 : this.duration; - return this.programDateTime + duration * 1000; - } - get encrypted() { - var _this$_decryptdata; - // 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 = this._decryptdata) != null && _this$_decryptdata.encrypted) { - return true; - } else if (this.levelkeys) { - const keyFormats = Object.keys(this.levelkeys); - const len = keyFormats.length; - if (len > 1 || len === 1 && this.levelkeys[keyFormats[0]].encrypted) { - return true; - } - } - return false; - } - setKeyFormat(keyFormat) { - if (this.levelkeys) { - const key = this.levelkeys[keyFormat]; - if (key && !this._decryptdata) { - this._decryptdata = key.getDecryptData(this.sn); - } - } - } - abortRequests() { - var _this$loader, _this$keyLoader; - (_this$loader = this.loader) == null ? void 0 : _this$loader.abort(); - (_this$keyLoader = this.keyLoader) == null ? void 0 : _this$keyLoader.abort(); - } - setElementaryStreamInfo(type, startPTS, endPTS, startDTS, endDTS, partial = false) { - const { - elementaryStreams - } = this; - const info = elementaryStreams[type]; - if (!info) { - elementaryStreams[type] = { - startPTS, - endPTS, - startDTS, - endDTS, - partial - }; - return; - } - info.startPTS = Math.min(info.startPTS, startPTS); - info.endPTS = Math.max(info.endPTS, endPTS); - info.startDTS = Math.min(info.startDTS, startDTS); - info.endDTS = Math.max(info.endDTS, endDTS); - } -} - -/** - * Object representing parsed data from an HLS Partial Segment. Found in {@link hls.js#LevelDetails.partList}. - */ -class Part extends BaseSegment { - constructor(partAttrs, frag, baseurl, index, previous) { - super(baseurl); - this.fragOffset = 0; - this.duration = 0; - this.gap = false; - this.independent = false; - this.relurl = void 0; - this.fragment = void 0; - this.index = void 0; - this.stats = new LoadStats(); - this.duration = partAttrs.decimalFloatingPoint('DURATION'); - this.gap = partAttrs.bool('GAP'); - this.independent = partAttrs.bool('INDEPENDENT'); - this.relurl = partAttrs.enumeratedString('URI'); - this.fragment = frag; - this.index = index; - const byteRange = partAttrs.enumeratedString('BYTERANGE'); - if (byteRange) { - this.setByteRange(byteRange, previous); - } - if (previous) { - this.fragOffset = previous.fragOffset + previous.duration; - } - } - get start() { - return this.fragment.start + this.fragOffset; - } - get end() { - return this.start + this.duration; - } - get loaded() { - const { - elementaryStreams - } = this; - return !!(elementaryStreams.audio || elementaryStreams.video || elementaryStreams.audiovideo); - } -} - -const DEFAULT_TARGET_DURATION = 10; - -/** - * Object representing parsed data from an HLS Media Playlist. Found in {@link hls.js#Level.details}. - */ -class LevelDetails { - constructor(baseUrl) { - this.PTSKnown = false; - this.alignedSliding = false; - this.averagetargetduration = void 0; - this.endCC = 0; - this.endSN = 0; - this.fragments = void 0; - this.fragmentHint = void 0; - this.partList = null; - this.dateRanges = void 0; - this.dateRangeTagCount = 0; - this.live = true; - this.ageHeader = 0; - this.advancedDateTime = void 0; - this.updated = true; - this.advanced = true; - this.availabilityDelay = void 0; - // Manifest reload synchronization - this.misses = 0; - this.startCC = 0; - this.startSN = 0; - this.startTimeOffset = null; - this.targetduration = 0; - this.totalduration = 0; - this.type = null; - this.url = void 0; - this.m3u8 = ''; - this.version = null; - this.canBlockReload = false; - this.canSkipUntil = 0; - this.canSkipDateRanges = false; - this.skippedSegments = 0; - this.recentlyRemovedDateranges = void 0; - this.partHoldBack = 0; - this.holdBack = 0; - this.partTarget = 0; - this.preloadHint = void 0; - this.renditionReports = void 0; - this.tuneInGoal = 0; - this.deltaUpdateFailed = void 0; - this.driftStartTime = 0; - this.driftEndTime = 0; - this.driftStart = 0; - this.driftEnd = 0; - this.encryptedFragments = void 0; - this.playlistParsingError = null; - this.variableList = null; - this.hasVariableRefs = false; - this.appliedTimelineOffset = void 0; - this.fragments = []; - this.encryptedFragments = []; - this.dateRanges = {}; - this.url = baseUrl; - } - reloaded(previous) { - if (!previous) { - this.advanced = true; - this.updated = true; - return; - } - const partSnDiff = this.lastPartSn - previous.lastPartSn; - const partIndexDiff = this.lastPartIndex - previous.lastPartIndex; - this.updated = this.endSN !== previous.endSN || !!partIndexDiff || !!partSnDiff || !this.live; - this.advanced = this.endSN > previous.endSN || partSnDiff > 0 || partSnDiff === 0 && partIndexDiff > 0; - if (this.updated || this.advanced) { - this.misses = Math.floor(previous.misses * 0.6); - } else { - this.misses = previous.misses + 1; - } - this.availabilityDelay = previous.availabilityDelay; - } - get hasProgramDateTime() { - if (this.fragments.length) { - return isFiniteNumber(this.fragments[this.fragments.length - 1].programDateTime); - } - return false; - } - get levelTargetDuration() { - return this.averagetargetduration || this.targetduration || DEFAULT_TARGET_DURATION; - } - get drift() { - const runTime = this.driftEndTime - this.driftStartTime; - if (runTime > 0) { - const runDuration = this.driftEnd - this.driftStart; - return runDuration * 1000 / runTime; - } - return 1; - } - get edge() { - return this.partEnd || this.fragmentEnd; - } - get partEnd() { - var _this$partList; - if ((_this$partList = this.partList) != null && _this$partList.length) { - return this.partList[this.partList.length - 1].end; - } - return this.fragmentEnd; - } - get fragmentEnd() { - var _this$fragments; - if ((_this$fragments = this.fragments) != null && _this$fragments.length) { - return this.fragments[this.fragments.length - 1].end; - } - return 0; - } - get fragmentStart() { - var _this$fragments2; - if ((_this$fragments2 = this.fragments) != null && _this$fragments2.length) { - return this.fragments[0].start; - } - return 0; - } - get age() { - if (this.advancedDateTime) { - return Math.max(Date.now() - this.advancedDateTime, 0) / 1000; - } - return 0; - } - get lastPartIndex() { - var _this$partList2; - if ((_this$partList2 = this.partList) != null && _this$partList2.length) { - return this.partList[this.partList.length - 1].index; - } - return -1; - } - get lastPartSn() { - var _this$partList3; - if ((_this$partList3 = this.partList) != null && _this$partList3.length) { - return this.partList[this.partList.length - 1].fragment.sn; - } - return this.endSN; - } -} - -function base64Decode(base64encodedStr) { - return Uint8Array.from(atob(base64encodedStr), c => c.charCodeAt(0)); -} - -// breaking up those two types in order to clarify what is happening in the decoding path. - -// http://stackoverflow.com/questions/8936984/uint8array-to-string-in-javascript/22373197 -// http://www.onicos.com/staff/iz/amuse/javascript/expert/utf.txt -/* utf.js - UTF-8 <=> UTF-16 convertion - * - * Copyright (C) 1999 Masanao Izumo - * Version: 1.0 - * LastModified: Dec 25 1999 - * This library is free. You can redistribute it and/or modify it. - */ - -function strToUtf8array(str) { - return Uint8Array.from(unescape(encodeURIComponent(str)), c => c.charCodeAt(0)); -} - -function getKeyIdBytes(str) { - const keyIdbytes = strToUtf8array(str).subarray(0, 16); - const paddedkeyIdbytes = new Uint8Array(16); - paddedkeyIdbytes.set(keyIdbytes, 16 - keyIdbytes.length); - return paddedkeyIdbytes; -} -function changeEndianness(keyId) { - const swap = function swap(array, from, to) { - const cur = array[from]; - array[from] = array[to]; - array[to] = cur; - }; - swap(keyId, 0, 3); - swap(keyId, 1, 2); - swap(keyId, 4, 5); - swap(keyId, 6, 7); -} -function convertDataUriToArrayBytes(uri) { - // data:[ - const colonsplit = uri.split(':'); - let keydata = null; - if (colonsplit[0] === 'data' && colonsplit.length === 2) { - const semicolonsplit = colonsplit[1].split(';'); - const commasplit = semicolonsplit[semicolonsplit.length - 1].split(','); - if (commasplit.length === 2) { - const isbase64 = commasplit[0] === 'base64'; - const data = commasplit[1]; - if (isbase64) { - semicolonsplit.splice(-1, 1); // remove from processing - keydata = base64Decode(data); - } else { - keydata = getKeyIdBytes(data); - } - } - } - return keydata; -} - -var DecrypterAesMode = { - cbc: 0, - ctr: 1 -}; - -function isFullSegmentEncryption(method) { - return method === 'AES-128' || method === 'AES-256' || method === 'AES-256-CTR'; -} -function getAesModeFromFullSegmentMethod(method) { - switch (method) { - case 'AES-128': - case 'AES-256': - return DecrypterAesMode.cbc; - case 'AES-256-CTR': - return DecrypterAesMode.ctr; - default: - throw new Error(`invalid full segment method ${method}`); - } -} - -/** returns `undefined` is `self` is missing, e.g. in node */ -const optionalSelf = typeof self !== 'undefined' ? self : undefined; - -/** - * @see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/requestMediaKeySystemAccess - */ -var KeySystems = { - CLEARKEY: "org.w3.clearkey", - FAIRPLAY: "com.apple.fps", - PLAYREADY: "com.microsoft.playready", - WIDEVINE: "com.widevine.alpha" -}; - -// Playlist #EXT-X-KEY KEYFORMAT values -var KeySystemFormats = { - CLEARKEY: "org.w3.clearkey", - FAIRPLAY: "com.apple.streamingkeydelivery", - PLAYREADY: "com.microsoft.playready", - WIDEVINE: "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed" -}; -function keySystemFormatToKeySystemDomain(format) { - 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 -var KeySystemIds = { - CENC: "1077efecc0b24d02ace33c1e52e2fb4b", - CLEARKEY: "e2719d58a985b3c9781ab030af78d30e", - FAIRPLAY: "94ce86fb07ff4f43adb893d2fa968ca2", - PLAYREADY: "9a04f07998404286ab92e65be0885f95", - WIDEVINE: "edef8ba979d64acea3c827dcd51d21ed" -}; -function keySystemIdToKeySystemDomain(systemId) { - 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; - } -} -function keySystemDomainToKeySystemFormat(keySystem) { - 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; - } -} -function getKeySystemsForConfig(config) { - const { - drmSystems, - widevineLicenseUrl - } = config; - const keySystemsToAttempt = drmSystems ? [KeySystems.FAIRPLAY, KeySystems.WIDEVINE, KeySystems.PLAYREADY, KeySystems.CLEARKEY].filter(keySystem => !!drmSystems[keySystem]) : []; - if (!keySystemsToAttempt[KeySystems.WIDEVINE] && widevineLicenseUrl) { - keySystemsToAttempt.push(KeySystems.WIDEVINE); - } - return keySystemsToAttempt; -} -const requestMediaKeySystemAccess = function (_optionalSelf$navigat) { - if (optionalSelf != null && (_optionalSelf$navigat = optionalSelf.navigator) != null && _optionalSelf$navigat.requestMediaKeySystemAccess) { - return self.navigator.requestMediaKeySystemAccess.bind(self.navigator); - } else { - return null; - } -}(); - -/** - * @see https://developer.mozilla.org/en-US/docs/Web/API/MediaKeySystemConfiguration - */ -function getSupportedMediaKeySystemConfigurations(keySystem, audioCodecs, videoCodecs, drmSystemOptions) { - let initDataTypes; - 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, audioCodecs, videoCodecs, drmSystemOptions) { - const baseConfig = { - initDataTypes: initDataTypes, - persistentState: drmSystemOptions.persistentState || 'optional', - distinctiveIdentifier: drmSystemOptions.distinctiveIdentifier || 'optional', - 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]; -} -function parsePlayReadyWRM(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); - return keyIdArray; - } - } - return null; -} - -function sliceUint8(array, start, end) { - // @ts-expect-error This polyfills IE11 usage of Uint8Array slice. - // It always exists in the TypeScript definition so fails, but it fails at runtime on IE11. - return Uint8Array.prototype.slice ? array.slice(start, end) : new Uint8Array(Array.prototype.slice.call(array, start, end)); -} - -// http://stackoverflow.com/questions/8936984/uint8array-to-string-in-javascript/22373197 -// http://www.onicos.com/staff/iz/amuse/javascript/expert/utf.txt -/* utf.js - UTF-8 <=> UTF-16 convertion - * - * Copyright (C) 1999 Masanao Izumo - * Version: 1.0 - * LastModified: Dec 25 1999 - * This library is free. You can redistribute it and/or modify it. - */ -/** - * Converts a UTF-8 array to a string. - * - * @param array - The UTF-8 array to convert - * - * @returns The string - * - * @group Utils - * - * @beta - */ -function utf8ArrayToStr(array, exitOnNull = false) { - if (typeof TextDecoder !== 'undefined') { - const decoder = new TextDecoder('utf-8'); - const decoded = decoder.decode(array); - if (exitOnNull) { - // grab up to the first null - const idx = decoded.indexOf('\0'); - return idx !== -1 ? decoded.substring(0, idx) : decoded; - } - // remove any null characters - return decoded.replace(/\0/g, ''); - } - const len = array.length; - let c; - let char2; - let char3; - let out = ''; - let i = 0; - while (i < len) { - c = array[i++]; - if (c === 0x00 && exitOnNull) { - return out; - } else if (c === 0x00 || c === 0x03) { - // If the character is 3 (END_OF_TEXT) or 0 (NULL) then skip it - continue; - } - switch (c >> 4) { - case 0: - case 1: - case 2: - case 3: - case 4: - case 5: - case 6: - case 7: - // 0xxxxxxx - out += String.fromCharCode(c); - break; - case 12: - case 13: - // 110x xxxx 10xx xxxx - char2 = array[i++]; - out += String.fromCharCode((c & 0x1f) << 6 | char2 & 0x3f); - break; - case 14: - // 1110 xxxx 10xx xxxx 10xx xxxx - char2 = array[i++]; - char3 = array[i++]; - out += String.fromCharCode((c & 0x0f) << 12 | (char2 & 0x3f) << 6 | (char3 & 0x3f) << 0); - break; - } - } - return out; -} - -/** - * hex dump helper class - */ - -const Hex = { - hexDump: function (array) { - let str = ''; - for (let i = 0; i < array.length; i++) { - let h = array[i].toString(16); - if (h.length < 2) { - h = '0' + h; - } - str += h; - } - return str; - } -}; - -const UINT32_MAX$1 = Math.pow(2, 32) - 1; -const push = [].push; - -// We are using fixed track IDs for driving the MP4 remuxer -// instead of following the TS PIDs. -// There is no reason not to do this and some browsers/SourceBuffer-demuxers -// may not like if there are TrackID "switches" -// See https://github.com/video-dev/hls.js/issues/1331 -// Here we are mapping our internal track types to constant MP4 track IDs -// With MSE currently one can only have one track of each, and we are muxing -// whatever video/audio rendition in them. -const RemuxerTrackIdConfig = { - video: 1, - audio: 2, - id3: 3, - text: 4 -}; -function bin2str(data) { - return String.fromCharCode.apply(null, data); -} -function readUint16(buffer, offset) { - const val = buffer[offset] << 8 | buffer[offset + 1]; - return val < 0 ? 65536 + val : val; -} -function readUint32(buffer, offset) { - const val = readSint32(buffer, offset); - return val < 0 ? 4294967296 + val : val; -} -function readUint64(buffer, offset) { - let result = readUint32(buffer, offset); - result *= Math.pow(2, 32); - result += readUint32(buffer, offset + 4); - return result; -} -function readSint32(buffer, offset) { - return buffer[offset] << 24 | buffer[offset + 1] << 16 | buffer[offset + 2] << 8 | buffer[offset + 3]; -} -function writeUint32(buffer, offset, value) { - buffer[offset] = value >> 24; - buffer[offset + 1] = value >> 16 & 0xff; - buffer[offset + 2] = value >> 8 & 0xff; - buffer[offset + 3] = value & 0xff; -} - -// Find "moof" box -function hasMoofData(data) { - const end = data.byteLength; - for (let i = 0; i < end;) { - const size = readUint32(data, i); - if (size > 8 && data[i + 4] === 0x6d && data[i + 5] === 0x6f && data[i + 6] === 0x6f && data[i + 7] === 0x66) { - return true; - } - i = size > 1 ? i + size : end; - } - return false; -} - -// Find the data for a box specified by its path -function findBox(data, path) { - const results = []; - if (!path.length) { - // short-circuit the search for empty paths - return results; - } - const end = data.byteLength; - for (let i = 0; i < end;) { - const size = readUint32(data, i); - const type = bin2str(data.subarray(i + 4, i + 8)); - const endbox = size > 1 ? i + size : end; - if (type === path[0]) { - if (path.length === 1) { - // this is the end of the path and we've found the box we were - // looking for - results.push(data.subarray(i + 8, endbox)); - } else { - // recursively search for the next box along the path - const subresults = findBox(data.subarray(i + 8, endbox), path.slice(1)); - if (subresults.length) { - push.apply(results, subresults); - } - } - } - i = endbox; - } - - // we've finished searching all of data - return results; -} -function parseSegmentIndex(sidx) { - const references = []; - const version = sidx[0]; - - // set initial offset, we skip the reference ID (not needed) - let index = 8; - const timescale = readUint32(sidx, index); - index += 4; - let earliestPresentationTime = 0; - let firstOffset = 0; - if (version === 0) { - earliestPresentationTime = readUint32(sidx, index); - firstOffset = readUint32(sidx, index + 4); - index += 8; - } else { - earliestPresentationTime = readUint64(sidx, index); - firstOffset = readUint64(sidx, index + 8); - index += 16; - } - - // skip reserved - index += 2; - let startByte = sidx.length + firstOffset; - const referencesCount = readUint16(sidx, index); - index += 2; - for (let i = 0; i < referencesCount; i++) { - let referenceIndex = index; - const referenceInfo = readUint32(sidx, referenceIndex); - referenceIndex += 4; - const referenceSize = referenceInfo & 0x7fffffff; - const referenceType = (referenceInfo & 0x80000000) >>> 31; - if (referenceType === 1) { - logger.warn('SIDX has hierarchical references (not supported)'); - return null; - } - const subsegmentDuration = readUint32(sidx, referenceIndex); - referenceIndex += 4; - references.push({ - referenceSize, - subsegmentDuration, - // unscaled - info: { - duration: subsegmentDuration / timescale, - start: startByte, - end: startByte + referenceSize - 1 - } - }); - startByte += referenceSize; - - // Skipping 1 bit for |startsWithSap|, 3 bits for |sapType|, and 28 bits - // for |sapDelta|. - referenceIndex += 4; - - // skip to next ref - index = referenceIndex; - } - return { - earliestPresentationTime, - timescale, - version, - referencesCount, - references - }; -} - -/** - * Parses an MP4 initialization segment and extracts stream type and - * timescale values for any declared tracks. Timescale values indicate the - * number of clock ticks per second to assume for time-based values - * elsewhere in the MP4. - * - * To determine the start time of an MP4, you need two pieces of - * information: the timescale unit and the earliest base media decode - * time. Multiple timescales can be specified within an MP4 but the - * base media decode time is always expressed in the timescale from - * the media header box for the track: - * ``` - * moov > trak > mdia > mdhd.timescale - * moov > trak > mdia > hdlr - * ``` - * @param initSegment the bytes of the init segment - * @returns a hash of track type to timescale values or null if - * the init segment is malformed. - */ - -function parseInitSegment(initSegment) { - const result = []; - const traks = findBox(initSegment, ['moov', 'trak']); - for (let i = 0; i < traks.length; i++) { - const trak = traks[i]; - const tkhd = findBox(trak, ['tkhd'])[0]; - if (tkhd) { - let version = tkhd[0]; - const trackId = readUint32(tkhd, version === 0 ? 12 : 20); - const mdhd = findBox(trak, ['mdia', 'mdhd'])[0]; - if (mdhd) { - version = mdhd[0]; - const timescale = readUint32(mdhd, version === 0 ? 12 : 20); - const hdlr = findBox(trak, ['mdia', 'hdlr'])[0]; - if (hdlr) { - const hdlrType = bin2str(hdlr.subarray(8, 12)); - const type = { - soun: ElementaryStreamTypes.AUDIO, - vide: ElementaryStreamTypes.VIDEO - }[hdlrType]; - if (type) { - // Parse codec details - const stsd = findBox(trak, ['mdia', 'minf', 'stbl', 'stsd'])[0]; - const stsdData = parseStsd(stsd); - result[trackId] = { - timescale, - type - }; - result[type] = _objectSpread2({ - timescale, - id: trackId - }, stsdData); - } - } - } - } - } - const trex = findBox(initSegment, ['moov', 'mvex', 'trex']); - trex.forEach(trex => { - const trackId = readUint32(trex, 4); - const track = result[trackId]; - if (track) { - track.default = { - duration: readUint32(trex, 12), - flags: readUint32(trex, 20) - }; - } - }); - return result; -} -function parseStsd(stsd) { - const sampleEntries = stsd.subarray(8); - const sampleEntriesEnd = sampleEntries.subarray(8 + 78); - const fourCC = bin2str(sampleEntries.subarray(4, 8)); - let codec = fourCC; - const encrypted = fourCC === 'enca' || fourCC === 'encv'; - if (encrypted) { - const encBox = findBox(sampleEntries, [fourCC])[0]; - const encBoxChildren = encBox.subarray(fourCC === 'enca' ? 28 : 78); - const sinfs = findBox(encBoxChildren, ['sinf']); - sinfs.forEach(sinf => { - const schm = findBox(sinf, ['schm'])[0]; - if (schm) { - const scheme = bin2str(schm.subarray(4, 8)); - if (scheme === 'cbcs' || scheme === 'cenc') { - const frma = findBox(sinf, ['frma'])[0]; - if (frma) { - // for encrypted content codec fourCC will be in frma - codec = bin2str(frma); - } - } - } - }); - } - switch (codec) { - case 'avc1': - case 'avc2': - case 'avc3': - case 'avc4': - { - // extract profile + compatibility + level out of avcC box - const avcCBox = findBox(sampleEntriesEnd, ['avcC'])[0]; - codec += '.' + toHex(avcCBox[1]) + toHex(avcCBox[2]) + toHex(avcCBox[3]); - break; - } - case 'mp4a': - { - const codecBox = findBox(sampleEntries, [fourCC])[0]; - const esdsBox = findBox(codecBox.subarray(28), ['esds'])[0]; - if (esdsBox && esdsBox.length > 7) { - let i = 4; - // ES Descriptor tag - if (esdsBox[i++] !== 0x03) { - break; - } - i = skipBERInteger(esdsBox, i); - i += 2; // skip es_id; - const flags = esdsBox[i++]; - if (flags & 0x80) { - i += 2; // skip dependency es_id - } - if (flags & 0x40) { - i += esdsBox[i++]; // skip URL - } - // Decoder config descriptor - if (esdsBox[i++] !== 0x04) { - break; - } - i = skipBERInteger(esdsBox, i); - const objectType = esdsBox[i++]; - if (objectType === 0x40) { - codec += '.' + toHex(objectType); - } else { - break; - } - i += 12; - // Decoder specific info - if (esdsBox[i++] !== 0x05) { - break; - } - i = skipBERInteger(esdsBox, i); - const firstByte = esdsBox[i++]; - let audioObjectType = (firstByte & 0xf8) >> 3; - if (audioObjectType === 31) { - audioObjectType += 1 + ((firstByte & 0x7) << 3) + ((esdsBox[i] & 0xe0) >> 5); - } - codec += '.' + audioObjectType; - } - break; - } - case 'hvc1': - case 'hev1': - { - const hvcCBox = findBox(sampleEntriesEnd, ['hvcC'])[0]; - const profileByte = hvcCBox[1]; - const profileSpace = ['', 'A', 'B', 'C'][profileByte >> 6]; - const generalProfileIdc = profileByte & 0x1f; - const profileCompat = readUint32(hvcCBox, 2); - const tierFlag = (profileByte & 0x20) >> 5 ? 'H' : 'L'; - const levelIDC = hvcCBox[12]; - const constraintIndicator = hvcCBox.subarray(6, 12); - codec += '.' + profileSpace + generalProfileIdc; - codec += '.' + profileCompat.toString(16).toUpperCase(); - codec += '.' + tierFlag + levelIDC; - let constraintString = ''; - for (let i = constraintIndicator.length; i--;) { - const byte = constraintIndicator[i]; - if (byte || constraintString) { - const encodedByte = byte.toString(16).toUpperCase(); - constraintString = '.' + encodedByte + constraintString; - } - } - codec += constraintString; - break; - } - case 'dvh1': - case 'dvhe': - { - const dvcCBox = findBox(sampleEntriesEnd, ['dvcC'])[0]; - const profile = dvcCBox[2] >> 1 & 0x7f; - const level = dvcCBox[2] << 5 & 0x20 | dvcCBox[3] >> 3 & 0x1f; - codec += '.' + addLeadingZero(profile) + '.' + addLeadingZero(level); - break; - } - case 'vp09': - { - const vpcCBox = findBox(sampleEntriesEnd, ['vpcC'])[0]; - const profile = vpcCBox[4]; - const level = vpcCBox[5]; - const bitDepth = vpcCBox[6] >> 4 & 0x0f; - codec += '.' + addLeadingZero(profile) + '.' + addLeadingZero(level) + '.' + addLeadingZero(bitDepth); - break; - } - case 'av01': - { - const av1CBox = findBox(sampleEntriesEnd, ['av1C'])[0]; - const profile = av1CBox[1] >>> 5; - const level = av1CBox[1] & 0x1f; - const tierFlag = av1CBox[2] >>> 7 ? 'H' : 'M'; - const highBitDepth = (av1CBox[2] & 0x40) >> 6; - const twelveBit = (av1CBox[2] & 0x20) >> 5; - const bitDepth = profile === 2 && highBitDepth ? twelveBit ? 12 : 10 : highBitDepth ? 10 : 8; - const monochrome = (av1CBox[2] & 0x10) >> 4; - const chromaSubsamplingX = (av1CBox[2] & 0x08) >> 3; - const chromaSubsamplingY = (av1CBox[2] & 0x04) >> 2; - const chromaSamplePosition = av1CBox[2] & 0x03; - // TODO: parse color_description_present_flag - // default it to BT.709/limited range for now - // more info https://aomediacodec.github.io/av1-isobmff/#av1codecconfigurationbox-syntax - const colorPrimaries = 1; - const transferCharacteristics = 1; - const matrixCoefficients = 1; - const videoFullRangeFlag = 0; - codec += '.' + profile + '.' + addLeadingZero(level) + tierFlag + '.' + addLeadingZero(bitDepth) + '.' + monochrome + '.' + chromaSubsamplingX + chromaSubsamplingY + chromaSamplePosition + '.' + addLeadingZero(colorPrimaries) + '.' + addLeadingZero(transferCharacteristics) + '.' + addLeadingZero(matrixCoefficients) + '.' + videoFullRangeFlag; - break; - } - } - return { - codec, - encrypted - }; -} -function skipBERInteger(bytes, i) { - const limit = i + 5; - while (bytes[i++] & 0x80 && i < limit) { - /* do nothing */ - } - return i; -} -function toHex(x) { - return ('0' + x.toString(16).toUpperCase()).slice(-2); -} -function addLeadingZero(num) { - return (num < 10 ? '0' : '') + num; -} -function patchEncyptionData(initSegment, decryptdata) { - 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; -} -function parseSinf(sinf) { - 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]; - } - } - return null; -} - -/** - * Determine the base media decode start time, in seconds, for an MP4 - * fragment. If multiple fragments are specified, the earliest time is - * returned. - * - * The base media decode time can be parsed from track fragment - * metadata: - * ``` - * moof > traf > tfdt.baseMediaDecodeTime - * ``` - * It requires the timescale value from the mdhd to interpret. - * - * @param initData - a hash of track type to timescale values - * @param fmp4 - the bytes of the mp4 fragment - * @returns the earliest base media decode start time for the - * fragment, in seconds - */ -function getStartDTS(initData, fmp4) { - // we need info from two children of each track fragment box - return findBox(fmp4, ['moof', 'traf']).reduce((result, traf) => { - const tfdt = findBox(traf, ['tfdt'])[0]; - const version = tfdt[0]; - const start = findBox(traf, ['tfhd']).reduce((result, tfhd) => { - // get the track id from the tfhd - const id = readUint32(tfhd, 4); - const track = initData[id]; - if (track) { - let baseTime = readUint32(tfdt, 4); - if (version === 1) { - // If value is too large, assume signed 64-bit. Negative track fragment decode times are invalid, but they exist in the wild. - // This prevents large values from being used for initPTS, which can cause playlist sync issues. - // https://github.com/video-dev/hls.js/issues/5303 - if (baseTime === UINT32_MAX$1) { - logger.warn(`[mp4-demuxer]: Ignoring assumed invalid signed 64-bit track fragment decode time`); - return result; - } - baseTime *= UINT32_MAX$1 + 1; - baseTime += readUint32(tfdt, 8); - } - // assume a 90kHz clock if no timescale was specified - const scale = track.timescale || 90e3; - // convert base time to seconds - const startTime = baseTime / scale; - if (isFiniteNumber(startTime) && (result === null || startTime < result)) { - return startTime; - } - } - return result; - }, null); - if (start !== null && isFiniteNumber(start) && (result === null || start < result)) { - return start; - } - return result; - }, null); -} - -/* - For Reference: - aligned(8) class TrackFragmentHeaderBox - extends FullBox(‘tfhd’, 0, tf_flags){ - unsigned int(32) track_ID; - // all the following are optional fields - unsigned int(64) base_data_offset; - unsigned int(32) sample_description_index; - unsigned int(32) default_sample_duration; - unsigned int(32) default_sample_size; - unsigned int(32) default_sample_flags - } - */ -function getDuration(data, initData) { - let rawDuration = 0; - let videoDuration = 0; - let audioDuration = 0; - const trafs = findBox(data, ['moof', 'traf']); - for (let i = 0; i < trafs.length; i++) { - const traf = trafs[i]; - // There is only one tfhd & trun per traf - // This is true for CMAF style content, and we should perhaps check the ftyp - // and only look for a single trun then, but for ISOBMFF we should check - // for multiple track runs. - const tfhd = findBox(traf, ['tfhd'])[0]; - // get the track id from the tfhd - const id = readUint32(tfhd, 4); - const track = initData[id]; - if (!track) { - continue; - } - const trackDefault = track.default; - const tfhdFlags = readUint32(tfhd, 0) | (trackDefault == null ? void 0 : trackDefault.flags); - let sampleDuration = trackDefault == null ? void 0 : trackDefault.duration; - if (tfhdFlags & 0x000008) { - // 0x000008 indicates the presence of the default_sample_duration field - if (tfhdFlags & 0x000002) { - // 0x000002 indicates the presence of the sample_description_index field, which precedes default_sample_duration - // If present, the default_sample_duration exists at byte offset 12 - sampleDuration = readUint32(tfhd, 12); - } else { - // Otherwise, the duration is at byte offset 8 - sampleDuration = readUint32(tfhd, 8); - } - } - // assume a 90kHz clock if no timescale was specified - const timescale = track.timescale || 90e3; - const truns = findBox(traf, ['trun']); - for (let j = 0; j < truns.length; j++) { - rawDuration = computeRawDurationFromSamples(truns[j]); - if (!rawDuration && sampleDuration) { - const sampleCount = readUint32(truns[j], 4); - rawDuration = sampleDuration * sampleCount; - } - if (track.type === ElementaryStreamTypes.VIDEO) { - videoDuration += rawDuration / timescale; - } else if (track.type === ElementaryStreamTypes.AUDIO) { - audioDuration += rawDuration / timescale; - } - } - } - if (videoDuration === 0 && audioDuration === 0) { - // If duration samples are not available in the traf use sidx subsegment_duration - let sidxMinStart = Infinity; - let sidxMaxEnd = 0; - let sidxDuration = 0; - const sidxs = findBox(data, ['sidx']); - for (let i = 0; i < sidxs.length; i++) { - const sidx = parseSegmentIndex(sidxs[i]); - if (sidx != null && sidx.references) { - sidxMinStart = Math.min(sidxMinStart, sidx.earliestPresentationTime / sidx.timescale); - const subSegmentDuration = sidx.references.reduce((dur, ref) => dur + ref.info.duration || 0, 0); - sidxMaxEnd = Math.max(sidxMaxEnd, subSegmentDuration + sidx.earliestPresentationTime / sidx.timescale); - sidxDuration = sidxMaxEnd - sidxMinStart; - } - } - if (sidxDuration && isFiniteNumber(sidxDuration)) { - return sidxDuration; - } - } - if (videoDuration) { - return videoDuration; - } - return audioDuration; -} - -/* - For Reference: - aligned(8) class TrackRunBox - extends FullBox(‘trun’, version, tr_flags) { - unsigned int(32) sample_count; - // the following are optional fields - signed int(32) data_offset; - unsigned int(32) first_sample_flags; - // all fields in the following array are optional - { - unsigned int(32) sample_duration; - unsigned int(32) sample_size; - unsigned int(32) sample_flags - if (version == 0) - { unsigned int(32) - else - { signed int(32) - }[ sample_count ] - } - */ -function computeRawDurationFromSamples(trun) { - const flags = readUint32(trun, 0); - // Flags are at offset 0, non-optional sample_count is at offset 4. Therefore we start 8 bytes in. - // Each field is an int32, which is 4 bytes - let offset = 8; - // data-offset-present flag - if (flags & 0x000001) { - offset += 4; - } - // first-sample-flags-present flag - if (flags & 0x000004) { - offset += 4; - } - let duration = 0; - const sampleCount = readUint32(trun, 4); - for (let i = 0; i < sampleCount; i++) { - // sample-duration-present flag - if (flags & 0x000100) { - const sampleDuration = readUint32(trun, offset); - duration += sampleDuration; - offset += 4; - } - // sample-size-present flag - if (flags & 0x000200) { - offset += 4; - } - // sample-flags-present flag - if (flags & 0x000400) { - offset += 4; - } - // sample-composition-time-offsets-present flag - if (flags & 0x000800) { - offset += 4; - } - } - return duration; -} -function offsetStartDTS(initData, fmp4, timeOffset) { - findBox(fmp4, ['moof', 'traf']).forEach(traf => { - findBox(traf, ['tfhd']).forEach(tfhd => { - // get the track id from the tfhd - const id = readUint32(tfhd, 4); - const track = initData[id]; - if (!track) { - return; - } - // assume a 90kHz clock if no timescale was specified - const timescale = track.timescale || 90e3; - // get the base media decode time from the tfdt - findBox(traf, ['tfdt']).forEach(tfdt => { - const version = tfdt[0]; - const offset = timeOffset * timescale; - if (offset) { - let baseMediaDecodeTime = readUint32(tfdt, 4); - if (version === 0) { - baseMediaDecodeTime -= offset; - baseMediaDecodeTime = Math.max(baseMediaDecodeTime, 0); - writeUint32(tfdt, 4, baseMediaDecodeTime); - } else { - baseMediaDecodeTime *= Math.pow(2, 32); - baseMediaDecodeTime += readUint32(tfdt, 8); - baseMediaDecodeTime -= offset; - baseMediaDecodeTime = Math.max(baseMediaDecodeTime, 0); - const upper = Math.floor(baseMediaDecodeTime / (UINT32_MAX$1 + 1)); - const lower = Math.floor(baseMediaDecodeTime % (UINT32_MAX$1 + 1)); - writeUint32(tfdt, 4, upper); - writeUint32(tfdt, 8, lower); - } - } - }); - }); - }); -} - -// TODO: Check if the last moof+mdat pair is part of the valid range -function segmentValidRange(data) { - const segmentedRange = { - valid: null, - remainder: null - }; - const moofs = findBox(data, ['moof']); - if (moofs.length < 2) { - segmentedRange.remainder = data; - return segmentedRange; - } - const last = moofs[moofs.length - 1]; - // Offset by 8 bytes; findBox offsets the start by as much - segmentedRange.valid = sliceUint8(data, 0, last.byteOffset - 8); - segmentedRange.remainder = sliceUint8(data, last.byteOffset - 8); - return segmentedRange; -} -function appendUint8Array(data1, data2) { - const temp = new Uint8Array(data1.length + data2.length); - temp.set(data1); - temp.set(data2, data1.length); - return temp; -} -function parseSamples(timeOffset, track) { - const seiSamples = []; - const videoData = track.samples; - const timescale = track.timescale; - const trackId = track.id; - let isHEVCFlavor = false; - const moofs = findBox(videoData, ['moof']); - moofs.map(moof => { - const moofOffset = moof.byteOffset - 8; - const trafs = findBox(moof, ['traf']); - trafs.map(traf => { - // get the base media decode time from the tfdt - const baseTime = findBox(traf, ['tfdt']).map(tfdt => { - const version = tfdt[0]; - let result = readUint32(tfdt, 4); - if (version === 1) { - result *= Math.pow(2, 32); - result += readUint32(tfdt, 8); - } - return result / timescale; - })[0]; - if (baseTime !== undefined) { - timeOffset = baseTime; - } - return findBox(traf, ['tfhd']).map(tfhd => { - const id = readUint32(tfhd, 4); - const tfhdFlags = readUint32(tfhd, 0) & 0xffffff; - const baseDataOffsetPresent = (tfhdFlags & 0x000001) !== 0; - const sampleDescriptionIndexPresent = (tfhdFlags & 0x000002) !== 0; - const defaultSampleDurationPresent = (tfhdFlags & 0x000008) !== 0; - let defaultSampleDuration = 0; - const defaultSampleSizePresent = (tfhdFlags & 0x000010) !== 0; - let defaultSampleSize = 0; - const defaultSampleFlagsPresent = (tfhdFlags & 0x000020) !== 0; - let tfhdOffset = 8; - if (id === trackId) { - if (baseDataOffsetPresent) { - tfhdOffset += 8; - } - if (sampleDescriptionIndexPresent) { - tfhdOffset += 4; - } - if (defaultSampleDurationPresent) { - defaultSampleDuration = readUint32(tfhd, tfhdOffset); - tfhdOffset += 4; - } - if (defaultSampleSizePresent) { - defaultSampleSize = readUint32(tfhd, tfhdOffset); - tfhdOffset += 4; - } - if (defaultSampleFlagsPresent) { - tfhdOffset += 4; - } - if (track.type === 'video') { - isHEVCFlavor = isHEVC(track.codec); - } - findBox(traf, ['trun']).map(trun => { - const version = trun[0]; - const flags = readUint32(trun, 0) & 0xffffff; - const dataOffsetPresent = (flags & 0x000001) !== 0; - let dataOffset = 0; - const firstSampleFlagsPresent = (flags & 0x000004) !== 0; - const sampleDurationPresent = (flags & 0x000100) !== 0; - let sampleDuration = 0; - const sampleSizePresent = (flags & 0x000200) !== 0; - let sampleSize = 0; - const sampleFlagsPresent = (flags & 0x000400) !== 0; - const sampleCompositionOffsetsPresent = (flags & 0x000800) !== 0; - let compositionOffset = 0; - const sampleCount = readUint32(trun, 4); - let trunOffset = 8; // past version, flags, and sample count - - if (dataOffsetPresent) { - dataOffset = readUint32(trun, trunOffset); - trunOffset += 4; - } - if (firstSampleFlagsPresent) { - trunOffset += 4; - } - let sampleOffset = dataOffset + moofOffset; - for (let ix = 0; ix < sampleCount; ix++) { - if (sampleDurationPresent) { - sampleDuration = readUint32(trun, trunOffset); - trunOffset += 4; - } else { - sampleDuration = defaultSampleDuration; - } - if (sampleSizePresent) { - sampleSize = readUint32(trun, trunOffset); - trunOffset += 4; - } else { - sampleSize = defaultSampleSize; - } - if (sampleFlagsPresent) { - trunOffset += 4; - } - if (sampleCompositionOffsetsPresent) { - if (version === 0) { - compositionOffset = readUint32(trun, trunOffset); - } else { - compositionOffset = readSint32(trun, trunOffset); - } - trunOffset += 4; - } - if (track.type === ElementaryStreamTypes.VIDEO) { - let naluTotalSize = 0; - while (naluTotalSize < sampleSize) { - const naluSize = readUint32(videoData, sampleOffset); - sampleOffset += 4; - if (isSEIMessage(isHEVCFlavor, videoData[sampleOffset])) { - const data = videoData.subarray(sampleOffset, sampleOffset + naluSize); - parseSEIMessageFromNALu(data, isHEVCFlavor ? 2 : 1, timeOffset + compositionOffset / timescale, seiSamples); - } - sampleOffset += naluSize; - naluTotalSize += naluSize + 4; - } - } - timeOffset += sampleDuration / timescale; - } - }); - } - }); - }); - }); - return seiSamples; -} -function isHEVC(codec) { - if (!codec) { - return false; - } - const delimit = codec.indexOf('.'); - const baseCodec = delimit < 0 ? codec : codec.substring(0, delimit); - return baseCodec === 'hvc1' || baseCodec === 'hev1' || - // Dolby Vision - baseCodec === 'dvh1' || baseCodec === 'dvhe'; -} -function isSEIMessage(isHEVCFlavor, naluHeader) { - if (isHEVCFlavor) { - const naluType = naluHeader >> 1 & 0x3f; - return naluType === 39 || naluType === 40; - } else { - const naluType = naluHeader & 0x1f; - return naluType === 6; - } -} -function parseSEIMessageFromNALu(unescapedData, headerSize, pts, samples) { - const data = discardEPB(unescapedData); - let seiPtr = 0; - // skip nal header - seiPtr += headerSize; - let payloadType = 0; - let payloadSize = 0; - let b = 0; - while (seiPtr < data.length) { - payloadType = 0; - do { - if (seiPtr >= data.length) { - break; - } - b = data[seiPtr++]; - payloadType += b; - } while (b === 0xff); - - // Parse payload size. - payloadSize = 0; - do { - if (seiPtr >= data.length) { - break; - } - b = data[seiPtr++]; - payloadSize += b; - } while (b === 0xff); - const leftOver = data.length - seiPtr; - // Create a variable to process the payload - let payPtr = seiPtr; - - // Increment the seiPtr to the end of the payload - if (payloadSize < leftOver) { - seiPtr += payloadSize; - } else if (payloadSize > leftOver) { - // Some type of corruption has happened? - logger.error(`Malformed SEI payload. ${payloadSize} is too small, only ${leftOver} bytes left to parse.`); - // We might be able to parse some data, but let's be safe and ignore it. - break; - } - if (payloadType === 4) { - const countryCode = data[payPtr++]; - if (countryCode === 181) { - const providerCode = readUint16(data, payPtr); - payPtr += 2; - if (providerCode === 49) { - const userStructure = readUint32(data, payPtr); - payPtr += 4; - if (userStructure === 0x47413934) { - const userDataType = data[payPtr++]; - - // Raw CEA-608 bytes wrapped in CEA-708 packet - if (userDataType === 3) { - const firstByte = data[payPtr++]; - const totalCCs = 0x1f & firstByte; - const enabled = 0x40 & firstByte; - const totalBytes = enabled ? 2 + totalCCs * 3 : 0; - const byteArray = new Uint8Array(totalBytes); - if (enabled) { - byteArray[0] = firstByte; - for (let i = 1; i < totalBytes; i++) { - byteArray[i] = data[payPtr++]; - } - } - samples.push({ - type: userDataType, - payloadType, - pts, - bytes: byteArray - }); - } - } - } - } - } else if (payloadType === 5) { - if (payloadSize > 16) { - const uuidStrArray = []; - for (let i = 0; i < 16; i++) { - const _b = data[payPtr++].toString(16); - uuidStrArray.push(_b.length == 1 ? '0' + _b : _b); - if (i === 3 || i === 5 || i === 7 || i === 9) { - uuidStrArray.push('-'); - } - } - const length = payloadSize - 16; - const userDataBytes = new Uint8Array(length); - for (let i = 0; i < length; i++) { - userDataBytes[i] = data[payPtr++]; - } - samples.push({ - payloadType, - pts, - uuid: uuidStrArray.join(''), - userData: utf8ArrayToStr(userDataBytes), - userDataBytes - }); - } - } - } -} - -/** - * remove Emulation Prevention bytes from a RBSP - */ -function discardEPB(data) { - const length = data.byteLength; - const EPBPositions = []; - 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; -} -function parseEmsg(data) { - const version = data[0]; - let schemeIdUri = ''; - let value = ''; - let timeScale = 0; - let presentationTimeDelta = 0; - let presentationTime = 0; - let eventDuration = 0; - let id = 0; - let offset = 0; - if (version === 0) { - while (bin2str(data.subarray(offset, offset + 1)) !== '\0') { - schemeIdUri += bin2str(data.subarray(offset, offset + 1)); - offset += 1; - } - schemeIdUri += bin2str(data.subarray(offset, offset + 1)); - offset += 1; - while (bin2str(data.subarray(offset, offset + 1)) !== '\0') { - value += bin2str(data.subarray(offset, offset + 1)); - offset += 1; - } - value += bin2str(data.subarray(offset, offset + 1)); - offset += 1; - timeScale = readUint32(data, 12); - presentationTimeDelta = readUint32(data, 16); - eventDuration = readUint32(data, 20); - id = readUint32(data, 24); - offset = 28; - } else if (version === 1) { - offset += 4; - timeScale = readUint32(data, offset); - offset += 4; - const leftPresentationTime = readUint32(data, offset); - offset += 4; - const rightPresentationTime = readUint32(data, offset); - offset += 4; - presentationTime = 2 ** 32 * leftPresentationTime + rightPresentationTime; - if (!isSafeInteger(presentationTime)) { - presentationTime = Number.MAX_SAFE_INTEGER; - logger.warn('Presentation time exceeds safe integer limit and wrapped to max safe integer in parsing emsg box'); - } - eventDuration = readUint32(data, offset); - offset += 4; - id = readUint32(data, offset); - offset += 4; - while (bin2str(data.subarray(offset, offset + 1)) !== '\0') { - schemeIdUri += bin2str(data.subarray(offset, offset + 1)); - offset += 1; - } - schemeIdUri += bin2str(data.subarray(offset, offset + 1)); - offset += 1; - while (bin2str(data.subarray(offset, offset + 1)) !== '\0') { - value += bin2str(data.subarray(offset, offset + 1)); - offset += 1; - } - value += bin2str(data.subarray(offset, offset + 1)); - offset += 1; - } - const payload = data.subarray(offset, data.byteLength); - return { - schemeIdUri, - value, - timeScale, - presentationTime, - presentationTimeDelta, - eventDuration, - id, - payload - }; -} -function mp4Box(type, ...payload) { - 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; -} -function mp4pssh(systemId, keyids, data) { - if (systemId.byteLength !== 16) { - throw new RangeError('Invalid system id'); - } - let version; - let kids; - { - 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()); -} -function parseMultiPssh(initData) { - const results = []; - if (initData instanceof ArrayBuffer) { - const length = initData.byteLength; - let offset = 0; - while (offset + 32 < length) { - const view = new DataView(initData, offset); - const pssh = parsePssh(view); - results.push(pssh); - offset += pssh.size; - } - } - return results; -} -function parsePssh(view) { - const size = view.getUint32(0); - const offset = view.byteOffset; - const length = view.byteLength; - if (length < size) { - return { - offset, - size: length - }; - } - const type = view.getUint32(4); - if (type !== 0x70737368) { - return { - offset, - size - }; - } - const version = view.getUint32(8) >>> 24; - if (version !== 0 && version !== 1) { - return { - offset, - size - }; - } - const buffer = view.buffer; - const systemId = Hex.hexDump(new Uint8Array(buffer, offset + 12, 16)); - const dataSizeOrKidCount = view.getUint32(28); - let kids = null; - let data = null; - if (version === 0) { - if (size - 32 < dataSizeOrKidCount || dataSizeOrKidCount < 22) { - return { - offset, - size - }; - } - data = new Uint8Array(buffer, offset + 32, dataSizeOrKidCount); - } else if (version === 1) { - if (!dataSizeOrKidCount || length < offset + 32 + dataSizeOrKidCount * 16 + 16) { - return { - offset, - size - }; - } - kids = []; - for (let i = 0; i < dataSizeOrKidCount; i++) { - kids.push(new Uint8Array(buffer, offset + 32 + i * 16, 16)); - } - } - return { - version, - systemId, - kids, - data, - offset, - size - }; -} - -let keyUriToKeyIdMap = {}; -class LevelKey { - static clearKeyUriToKeyIdMap() { - keyUriToKeyIdMap = {}; - } - constructor(method, uri, format, formatversions = [1], iv = null) { - this.uri = void 0; - this.method = void 0; - this.keyFormat = void 0; - this.keyFormatVersions = void 0; - this.encrypted = void 0; - this.isCommonEncryption = void 0; - this.iv = null; - this.key = null; - this.keyId = null; - this.pssh = 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 && !isFullSegmentEncryption(method); - } - isSupported() { - // If it's Segment encryption or No encryption, just select that key system - if (this.method) { - if (isFullSegmentEncryption(this.method) || this.method === 'NONE') { - return true; - } - if (this.keyFormat === 'identity') { - // Maintain support for clear SAMPLE-AES with MPEG-3 TS - return this.method === 'SAMPLE-AES'; - } else { - switch (this.keyFormat) { - case KeySystemFormats.FAIRPLAY: - case KeySystemFormats.WIDEVINE: - case KeySystemFormats.PLAYREADY: - case KeySystemFormats.CLEARKEY: - return ['ISO-23001-7', 'SAMPLE-AES', 'SAMPLE-AES-CENC', 'SAMPLE-AES-CTR'].indexOf(this.method) !== -1; - } - } - } - return false; - } - getDecryptData(sn) { - if (!this.encrypted || !this.uri) { - return null; - } - if (isFullSegmentEncryption(this.method) && 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/256 - // It must have an IV defined. We cannot substitute the Segment Number in. - 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: - // Setting `pssh` on this LevelKey/DecryptData allows HLS.js to generate a session using - // the playlist-key before the "encrypted" event. (Comment out to only use "encrypted" path.) - 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]); - - // Setting `pssh` on this LevelKey/DecryptData allows HLS.js to generate a session using - // the playlist-key before the "encrypted" event. (Comment out to only use "encrypted" path.) - this.pssh = mp4pssh(PlayReadyKeySystemUUID, null, keyBytes); - this.keyId = parsePlayReadyWRM(keyBytes); - 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) { - const uint8View = new Uint8Array(16); - for (let i = 12; i < 16; i++) { - uint8View[i] = segmentNumber >> 8 * (15 - i) & 0xff; - } - return uint8View; -} - -function getMediaSource(preferManagedMediaSource = true) { - if (typeof self === 'undefined') return undefined; - const mms = (preferManagedMediaSource || !self.MediaSource) && self.ManagedMediaSource; - return mms || self.MediaSource || self.WebKitMediaSource; -} -function isManagedMediaSource(source) { - return typeof self !== 'undefined' && source === self.ManagedMediaSource; -} -function isCompatibleTrackChange(currentTracks, requiredTracks) { - const trackNames = Object.keys(currentTracks); - const requiredTrackNames = Object.keys(requiredTracks); - const trackCount = trackNames.length; - const requiredTrackCount = requiredTrackNames.length; - return !trackCount || !requiredTrackCount || trackCount === requiredTrackCount && !trackNames.some(name => requiredTrackNames.indexOf(name) === -1); -} - -// from http://mp4ra.org/codecs.html -// values indicate codec selection preference (lower is higher priority) -const sampleEntryCodesISO = { - audio: { - a3ds: 1, - 'ac-3': 0.95, - 'ac-4': 1, - alac: 0.9, - alaw: 1, - dra1: 1, - 'dts+': 1, - 'dts-': 1, - dtsc: 1, - dtse: 1, - dtsh: 1, - 'ec-3': 0.9, - enca: 1, - fLaC: 0.9, - // MP4-RA listed codec entry for FLAC - flac: 0.9, - // legacy browser codec name for FLAC - FLAC: 0.9, - // some manifests may list "FLAC" with Apple's tools - g719: 1, - g726: 1, - m4ae: 1, - mha1: 1, - mha2: 1, - mhm1: 1, - mhm2: 1, - mlpa: 1, - mp4a: 1, - 'raw ': 1, - Opus: 1, - opus: 1, - // browsers expect this to be lowercase despite MP4RA says 'Opus' - samr: 1, - sawb: 1, - sawp: 1, - sevc: 1, - sqcp: 1, - ssmv: 1, - twos: 1, - ulaw: 1 - }, - video: { - avc1: 1, - avc2: 1, - avc3: 1, - avc4: 1, - avcp: 1, - av01: 0.8, - drac: 1, - dva1: 1, - dvav: 1, - dvh1: 0.7, - dvhe: 0.7, - encv: 1, - hev1: 0.75, - hvc1: 0.75, - mjp2: 1, - mp4v: 1, - mvc1: 1, - mvc2: 1, - mvc3: 1, - mvc4: 1, - resv: 1, - rv60: 1, - s263: 1, - svc1: 1, - svc2: 1, - 'vc-1': 1, - vp08: 1, - vp09: 0.9 - }, - text: { - stpp: 1, - wvtt: 1 - } -}; -function isCodecType(codec, type) { - const typeCodes = sampleEntryCodesISO[type]; - return !!typeCodes && !!typeCodes[codec.slice(0, 4)]; -} -function areCodecsMediaSourceSupported(codecs, type, preferManagedMediaSource = true) { - return !codecs.split(',').some(codec => !isCodecMediaSourceSupported(codec, type, preferManagedMediaSource)); -} -function isCodecMediaSourceSupported(codec, type, preferManagedMediaSource = true) { - var _MediaSource$isTypeSu; - const MediaSource = getMediaSource(preferManagedMediaSource); - return (_MediaSource$isTypeSu = MediaSource == null ? void 0 : MediaSource.isTypeSupported(mimeTypeForCodec(codec, type))) != null ? _MediaSource$isTypeSu : false; -} -function mimeTypeForCodec(codec, type) { - return `${type}/mp4;codecs=${codec}`; -} -function videoCodecPreferenceValue(videoCodec) { - if (videoCodec) { - const fourCC = videoCodec.substring(0, 4); - return sampleEntryCodesISO.video[fourCC]; - } - return 2; -} -function codecsSetSelectionPreferenceValue(codecSet) { - return codecSet.split(',').reduce((num, fourCC) => { - const preferenceValue = sampleEntryCodesISO.video[fourCC]; - if (preferenceValue) { - return (preferenceValue * 2 + num) / (num ? 3 : 2); - } - return (sampleEntryCodesISO.audio[fourCC] + num) / (num ? 2 : 1); - }, 0); -} -const CODEC_COMPATIBLE_NAMES = {}; -function getCodecCompatibleNameLower(lowerCaseCodec, preferManagedMediaSource = true) { - if (CODEC_COMPATIBLE_NAMES[lowerCaseCodec]) { - return CODEC_COMPATIBLE_NAMES[lowerCaseCodec]; - } - const codecsToCheck = { - // Idealy fLaC and Opus would be first (spec-compliant) but - // some browsers will report that fLaC is supported then fail. - // see: https://bugs.chromium.org/p/chromium/issues/detail?id=1422728 - flac: ['flac', 'fLaC', 'FLAC'], - opus: ['opus', 'Opus'], - // Replace audio codec info if browser does not support mp4a.40.34, - // and demuxer can fallback to 'audio/mpeg' or 'audio/mp4;codecs="mp3"' - 'mp4a.40.34': ['mp3'] - }[lowerCaseCodec]; - for (let i = 0; i < codecsToCheck.length; i++) { - var _getMediaSource; - if (isCodecMediaSourceSupported(codecsToCheck[i], 'audio', preferManagedMediaSource)) { - CODEC_COMPATIBLE_NAMES[lowerCaseCodec] = codecsToCheck[i]; - return codecsToCheck[i]; - } else if (codecsToCheck[i] === 'mp3' && (_getMediaSource = getMediaSource(preferManagedMediaSource)) != null && _getMediaSource.isTypeSupported('audio/mpeg')) { - return ''; - } - } - return lowerCaseCodec; -} -const AUDIO_CODEC_REGEXP = /flac|opus|mp4a\.40\.34/i; -function getCodecCompatibleName(codec, preferManagedMediaSource = true) { - return codec.replace(AUDIO_CODEC_REGEXP, m => getCodecCompatibleNameLower(m.toLowerCase(), preferManagedMediaSource)); -} -function pickMostCompleteCodecName(parsedCodec, levelCodec) { - // Parsing of mp4a codecs strings in mp4-tools from media is incomplete as of d8c6c7a - // so use level codec is parsed codec is unavailable or incomplete - if (parsedCodec && (parsedCodec.length > 4 || ['ac-3', 'ec-3', 'alac', 'fLaC', 'Opus'].indexOf(parsedCodec) !== -1)) { - return parsedCodec; - } - if (levelCodec) { - const levelCodecs = levelCodec.split(','); - if (levelCodecs.length > 1) { - if (parsedCodec) { - for (let i = levelCodecs.length; i--;) { - if (levelCodecs[i].substring(0, 4) === parsedCodec.substring(0, 4)) { - return levelCodecs[i]; - } - } - } - return levelCodecs[0]; - } - } - return levelCodec || parsedCodec; -} -function convertAVC1ToAVCOTI(videoCodecs) { - // Convert avc1 codec string from RFC-4281 to RFC-6381 for MediaSource.isTypeSupported - // Examples: avc1.66.30 to avc1.42001e and avc1.77.30,avc1.66.30 to avc1.4d001e,avc1.42001e. - const codecs = videoCodecs.split(','); - for (let i = 0; i < codecs.length; i++) { - const avcdata = codecs[i].split('.'); - if (avcdata.length > 2) { - let result = avcdata.shift() + '.'; - result += parseInt(avcdata.shift()).toString(16); - result += ('000' + parseInt(avcdata.shift()).toString(16)).slice(-4); - codecs[i] = result; - } - } - return codecs.join(','); -} -function fillInMissingAV01Params(videoCodec) { - // Used to fill in incomplete AV1 playlist CODECS strings for mediaCapabilities.decodingInfo queries - if (videoCodec.startsWith('av01.')) { - const av1params = videoCodec.split('.'); - const placeholders = ['0', '111', '01', '01', '01', '0']; - for (let i = av1params.length; i > 4 && i < 10; i++) { - av1params[i] = placeholders[i - 4]; - } - return av1params.join('.'); - } - return videoCodec; -} -function getM2TSSupportedAudioTypes(preferManagedMediaSource) { - const MediaSource = getMediaSource(preferManagedMediaSource) || { - isTypeSupported: () => false - }; - return { - mpeg: MediaSource.isTypeSupported('audio/mpeg'), - mp3: MediaSource.isTypeSupported('audio/mp4; codecs="mp3"'), - ac3: MediaSource.isTypeSupported('audio/mp4; codecs="ac-3"') - }; -} - -const MASTER_PLAYLIST_REGEX = /#EXT-X-STREAM-INF:([^\r\n]*)(?:[\r\n](?:#[^\r\n]*)?)*([^\r\n]+)|#EXT-X-(SESSION-DATA|SESSION-KEY|DEFINE|CONTENT-STEERING|START):([^\r\n]*)[\r\n]+/g; -const MASTER_PLAYLIST_MEDIA_REGEX = /#EXT-X-MEDIA:(.*)/g; -const IS_MEDIA_PLAYLIST = /^#EXT(?:INF|-X-TARGETDURATION):/m; // Handle empty Media Playlist (first EXTINF not signaled, but TARGETDURATION present) - -const LEVEL_PLAYLIST_REGEX_FAST = new RegExp([/#EXTINF:\s*(\d*(?:\.\d+)?)(?:,(.*)\s+)?/.source, -// duration (#EXTINF:,), group 1 => duration, group 2 => title -/(?!#) *(\S[^\r\n]*)/.source, -// segment URI, group 3 => the URI (note newline is not eaten) -/#EXT-X-BYTERANGE:*(.+)/.source, -// next segment's byterange, group 4 => range spec (x@y) -/#EXT-X-PROGRAM-DATE-TIME:(.+)/.source, -// next segment's program date/time group 5 => the datetime spec -/#.*/.source // All other non-segment oriented tags will match with all groups empty -].join('|'), 'g'); -const LEVEL_PLAYLIST_REGEX_SLOW = new RegExp([/#(EXTM3U)/.source, /#EXT-X-(DATERANGE|DEFINE|KEY|MAP|PART|PART-INF|PLAYLIST-TYPE|PRELOAD-HINT|RENDITION-REPORT|SERVER-CONTROL|SKIP|START):(.+)/.source, /#EXT-X-(BITRATE|DISCONTINUITY-SEQUENCE|MEDIA-SEQUENCE|TARGETDURATION|VERSION): *(\d+)/.source, /#EXT-X-(DISCONTINUITY|ENDLIST|GAP|INDEPENDENT-SEGMENTS)/.source, /(#)([^:]*):(.*)/.source, /(#)(.*)(?:.*)\r?\n?/.source].join('|')); -class M3U8Parser { - static findGroup(groups, mediaGroupId) { - for (let i = 0; i < groups.length; i++) { - const group = groups[i]; - if (group.id === mediaGroupId) { - return group; - } - } - } - static resolve(url, baseUrl) { - return urlToolkitExports.buildAbsoluteURL(baseUrl, url, { - alwaysNormalize: true - }); - } - static isMediaPlaylist(str) { - return IS_MEDIA_PLAYLIST.test(str); - } - static parseMasterPlaylist(string, baseurl) { - const hasVariableRefs = hasVariableReferences(string) ; - const parsed = { - contentSteering: null, - levels: [], - playlistParsingError: null, - sessionData: null, - sessionKeys: null, - startTimeOffset: null, - variableList: null, - hasVariableRefs - }; - const levelsWithKnownCodecs = []; - MASTER_PLAYLIST_REGEX.lastIndex = 0; - let result; - while ((result = MASTER_PLAYLIST_REGEX.exec(string)) != null) { - if (result[1]) { - var _level$unknownCodecs; - // '#EXT-X-STREAM-INF' is found, parse level tag in group 1 - const attrs = new AttrList(result[1], parsed); - const uri = substituteVariables(parsed, result[2]) ; - const level = { - attrs, - bitrate: attrs.decimalInteger('BANDWIDTH') || attrs.decimalInteger('AVERAGE-BANDWIDTH'), - name: attrs.NAME, - url: M3U8Parser.resolve(uri, baseurl) - }; - const resolution = attrs.decimalResolution('RESOLUTION'); - if (resolution) { - level.width = resolution.width; - level.height = resolution.height; - } - setCodecs(attrs.CODECS, level); - if (!((_level$unknownCodecs = level.unknownCodecs) != null && _level$unknownCodecs.length)) { - levelsWithKnownCodecs.push(level); - } - parsed.levels.push(level); - } else if (result[3]) { - const tag = result[3]; - const attributes = result[4]; - switch (tag) { - case 'SESSION-DATA': - { - // #EXT-X-SESSION-DATA - const sessionAttrs = new AttrList(attributes, parsed); - const dataId = sessionAttrs['DATA-ID']; - if (dataId) { - if (parsed.sessionData === null) { - parsed.sessionData = {}; - } - parsed.sessionData[dataId] = sessionAttrs; - } - break; - } - case 'SESSION-KEY': - { - // #EXT-X-SESSION-KEY - const sessionKey = parseKey(attributes, baseurl, parsed); - if (sessionKey.encrypted && sessionKey.isSupported()) { - if (parsed.sessionKeys === null) { - parsed.sessionKeys = []; - } - parsed.sessionKeys.push(sessionKey); - } else { - logger.warn(`[Keys] Ignoring invalid EXT-X-SESSION-KEY tag: "${attributes}"`); - } - break; - } - case 'DEFINE': - { - // #EXT-X-DEFINE - { - const variableAttributes = new AttrList(attributes, parsed); - addVariableDefinition(parsed, variableAttributes, baseurl); - } - break; - } - case 'CONTENT-STEERING': - { - // #EXT-X-CONTENT-STEERING - const contentSteeringAttributes = new AttrList(attributes, parsed); - parsed.contentSteering = { - uri: M3U8Parser.resolve(contentSteeringAttributes['SERVER-URI'], baseurl), - pathwayId: contentSteeringAttributes['PATHWAY-ID'] || '.' - }; - break; - } - case 'START': - { - // #EXT-X-START - parsed.startTimeOffset = parseStartTimeOffset(attributes); - break; - } - } - } - } - // Filter out levels with unknown codecs if it does not remove all levels - const stripUnknownCodecLevels = levelsWithKnownCodecs.length > 0 && levelsWithKnownCodecs.length < parsed.levels.length; - parsed.levels = stripUnknownCodecLevels ? levelsWithKnownCodecs : parsed.levels; - if (parsed.levels.length === 0) { - parsed.playlistParsingError = new Error('no levels found in manifest'); - } - return parsed; - } - static parseMasterPlaylistMedia(string, baseurl, parsed) { - let result; - const results = {}; - const levels = parsed.levels; - const groupsByType = { - AUDIO: levels.map(level => ({ - id: level.attrs.AUDIO, - audioCodec: level.audioCodec - })), - SUBTITLES: levels.map(level => ({ - id: level.attrs.SUBTITLES, - textCodec: level.textCodec - })), - 'CLOSED-CAPTIONS': [] - }; - let id = 0; - MASTER_PLAYLIST_MEDIA_REGEX.lastIndex = 0; - while ((result = MASTER_PLAYLIST_MEDIA_REGEX.exec(string)) !== null) { - const attrs = new AttrList(result[1], parsed); - const type = attrs.TYPE; - if (type) { - const groups = groupsByType[type]; - const medias = results[type] || []; - results[type] = medias; - const lang = attrs.LANGUAGE; - const assocLang = attrs['ASSOC-LANGUAGE']; - const channels = attrs.CHANNELS; - const characteristics = attrs.CHARACTERISTICS; - const instreamId = attrs['INSTREAM-ID']; - const media = { - attrs, - bitrate: 0, - id: id++, - groupId: attrs['GROUP-ID'] || '', - name: attrs.NAME || lang || '', - type, - default: attrs.bool('DEFAULT'), - autoselect: attrs.bool('AUTOSELECT'), - forced: attrs.bool('FORCED'), - lang, - url: attrs.URI ? M3U8Parser.resolve(attrs.URI, baseurl) : '' - }; - if (assocLang) { - media.assocLang = assocLang; - } - if (channels) { - media.channels = channels; - } - if (characteristics) { - media.characteristics = characteristics; - } - if (instreamId) { - media.instreamId = instreamId; - } - if (groups != null && groups.length) { - // If there are audio or text groups signalled in the manifest, let's look for a matching codec string for this track - // If we don't find the track signalled, lets use the first audio groups codec we have - // Acting as a best guess - const groupCodec = M3U8Parser.findGroup(groups, media.groupId) || groups[0]; - assignCodec(media, groupCodec, 'audioCodec'); - assignCodec(media, groupCodec, 'textCodec'); - } - medias.push(media); - } - } - return results; - } - static parseLevelPlaylist(string, baseurl, id, type, levelUrlId, multivariantVariableList) { - const level = new LevelDetails(baseurl); - const fragments = level.fragments; - const programDateTimes = []; - // The most recent init segment seen (applies to all subsequent segments) - let currentInitSegment = null; - let currentSN = 0; - let currentPart = 0; - let totalduration = 0; - let discontinuityCounter = 0; - let prevFrag = null; - let frag = new Fragment(type, baseurl); - let result; - let i; - let levelkeys; - let firstPdtIndex = -1; - let createNextFrag = false; - let nextByteRange = null; - LEVEL_PLAYLIST_REGEX_FAST.lastIndex = 0; - level.m3u8 = string; - level.hasVariableRefs = hasVariableReferences(string) ; - while ((result = LEVEL_PLAYLIST_REGEX_FAST.exec(string)) !== null) { - if (createNextFrag) { - createNextFrag = false; - frag = new Fragment(type, baseurl); - // setup the next fragment for part loading - frag.playlistOffset = totalduration; - frag.start = totalduration; - frag.sn = currentSN; - frag.cc = discontinuityCounter; - frag.level = id; - if (currentInitSegment) { - frag.initSegment = currentInitSegment; - frag.rawProgramDateTime = currentInitSegment.rawProgramDateTime; - currentInitSegment.rawProgramDateTime = null; - if (nextByteRange) { - frag.setByteRange(nextByteRange); - nextByteRange = null; - } - } - } - const duration = result[1]; - if (duration) { - // INF - frag.duration = parseFloat(duration); - // avoid sliced strings https://github.com/video-dev/hls.js/issues/939 - const title = (' ' + result[2]).slice(1); - frag.title = title || null; - frag.tagList.push(title ? ['INF', duration, title] : ['INF', duration]); - } else if (result[3]) { - // url - if (isFiniteNumber(frag.duration)) { - frag.playlistOffset = totalduration; - frag.start = totalduration; - if (levelkeys) { - setFragLevelKeys(frag, levelkeys, level); - } - frag.sn = currentSN; - frag.level = id; - frag.cc = discontinuityCounter; - fragments.push(frag); - // avoid sliced strings https://github.com/video-dev/hls.js/issues/939 - const uri = (' ' + result[3]).slice(1); - frag.relurl = substituteVariables(level, uri) ; - assignProgramDateTime(frag, prevFrag, programDateTimes); - prevFrag = frag; - totalduration += frag.duration; - currentSN++; - currentPart = 0; - createNextFrag = true; - } - } else if (result[4]) { - // X-BYTERANGE - const data = (' ' + result[4]).slice(1); - if (prevFrag) { - frag.setByteRange(data, prevFrag); - } else { - frag.setByteRange(data); - } - } else if (result[5]) { - // PROGRAM-DATE-TIME - // avoid sliced strings https://github.com/video-dev/hls.js/issues/939 - frag.rawProgramDateTime = (' ' + result[5]).slice(1); - frag.tagList.push(['PROGRAM-DATE-TIME', frag.rawProgramDateTime]); - if (firstPdtIndex === -1) { - firstPdtIndex = fragments.length; - } - } else { - result = result[0].match(LEVEL_PLAYLIST_REGEX_SLOW); - if (!result) { - logger.warn('No matches on slow regex match for level playlist!'); - continue; - } - for (i = 1; i < result.length; i++) { - if (typeof result[i] !== 'undefined') { - break; - } - } - - // avoid sliced strings https://github.com/video-dev/hls.js/issues/939 - const tag = (' ' + result[i]).slice(1); - const value1 = (' ' + result[i + 1]).slice(1); - const value2 = result[i + 2] ? (' ' + result[i + 2]).slice(1) : ''; - switch (tag) { - case 'PLAYLIST-TYPE': - level.type = value1.toUpperCase(); - break; - case 'MEDIA-SEQUENCE': - currentSN = level.startSN = parseInt(value1); - break; - case 'SKIP': - { - if (level.skippedSegments) { - level.playlistParsingError = new Error(`#EXT-X-SKIP MUST NOT appear more than once in a Playlist`); - } - const skipAttrs = new AttrList(value1, level); - const skippedSegments = skipAttrs.decimalInteger('SKIPPED-SEGMENTS'); - if (isFiniteNumber(skippedSegments)) { - level.skippedSegments += skippedSegments; - // This will result in fragments[] containing undefined values, which we will fill in with `mergeDetails` - for (let _i = skippedSegments; _i--;) { - fragments.push(null); - } - currentSN += skippedSegments; - } - const recentlyRemovedDateranges = skipAttrs.enumeratedString('RECENTLY-REMOVED-DATERANGES'); - if (recentlyRemovedDateranges) { - level.recentlyRemovedDateranges = (level.recentlyRemovedDateranges || []).concat(recentlyRemovedDateranges.split('\t')); - } - break; - } - case 'TARGETDURATION': - level.targetduration = Math.max(parseInt(value1), 1); - break; - case 'VERSION': - level.version = parseInt(value1); - break; - case 'INDEPENDENT-SEGMENTS': - case 'EXTM3U': - break; - case 'ENDLIST': - level.live = false; - break; - case '#': - if (value1 || value2) { - frag.tagList.push(value2 ? [value1, value2] : [value1]); - } - break; - case 'DISCONTINUITY': - discontinuityCounter++; - frag.tagList.push(['DIS']); - break; - case 'GAP': - frag.gap = true; - frag.tagList.push([tag]); - break; - case 'BITRATE': - frag.tagList.push([tag, value1]); - break; - case 'DATERANGE': - { - const dateRangeAttr = new AttrList(value1, level); - const dateRange = new DateRange(dateRangeAttr, level.dateRanges[dateRangeAttr.ID], level.dateRangeTagCount); - level.dateRangeTagCount++; - if (dateRange.isValid || level.skippedSegments) { - level.dateRanges[dateRange.id] = dateRange; - } else { - logger.warn(`Ignoring invalid DATERANGE tag: "${value1}"`); - } - // Add to fragment tag list for backwards compatibility (< v1.2.0) - frag.tagList.push(['EXT-X-DATERANGE', value1]); - break; - } - case 'DEFINE': - { - { - const variableAttributes = new AttrList(value1, level); - if ('IMPORT' in variableAttributes) { - importVariableDefinition(level, variableAttributes, multivariantVariableList); - } else { - addVariableDefinition(level, variableAttributes, baseurl); - } - } - break; - } - case 'DISCONTINUITY-SEQUENCE': - discontinuityCounter = parseInt(value1); - break; - case 'KEY': - { - const levelKey = parseKey(value1, baseurl, level); - if (levelKey.isSupported()) { - if (levelKey.method === 'NONE') { - levelkeys = undefined; - break; - } - if (!levelkeys) { - levelkeys = {}; - } - if (levelkeys[levelKey.keyFormat]) { - levelkeys = _extends({}, levelkeys); - } - levelkeys[levelKey.keyFormat] = levelKey; - } else { - logger.warn(`[Keys] Ignoring invalid EXT-X-KEY tag: "${value1}"`); - } - break; - } - case 'START': - level.startTimeOffset = parseStartTimeOffset(value1); - break; - case 'MAP': - { - const mapAttrs = new AttrList(value1, level); - if (frag.duration) { - // Initial segment tag is after segment duration tag. - // #EXTINF: 6.0 - // #EXT-X-MAP:URI="init.mp4 - const init = new Fragment(type, baseurl); - setInitSegment(init, mapAttrs, id, levelkeys); - currentInitSegment = init; - frag.initSegment = currentInitSegment; - if (currentInitSegment.rawProgramDateTime && !frag.rawProgramDateTime) { - frag.rawProgramDateTime = currentInitSegment.rawProgramDateTime; - } - } else { - // Initial segment tag is before segment duration tag - // Handle case where EXT-X-MAP is declared after EXT-X-BYTERANGE - const end = frag.byteRangeEndOffset; - if (end) { - const start = frag.byteRangeStartOffset; - nextByteRange = `${end - start}@${start}`; - } else { - nextByteRange = null; - } - setInitSegment(frag, mapAttrs, id, levelkeys); - currentInitSegment = frag; - createNextFrag = true; - } - currentInitSegment.cc = discontinuityCounter; - break; - } - case 'SERVER-CONTROL': - { - const serverControlAttrs = new AttrList(value1); - level.canBlockReload = serverControlAttrs.bool('CAN-BLOCK-RELOAD'); - level.canSkipUntil = serverControlAttrs.optionalFloat('CAN-SKIP-UNTIL', 0); - level.canSkipDateRanges = level.canSkipUntil > 0 && serverControlAttrs.bool('CAN-SKIP-DATERANGES'); - level.partHoldBack = serverControlAttrs.optionalFloat('PART-HOLD-BACK', 0); - level.holdBack = serverControlAttrs.optionalFloat('HOLD-BACK', 0); - break; - } - case 'PART-INF': - { - const partInfAttrs = new AttrList(value1); - level.partTarget = partInfAttrs.decimalFloatingPoint('PART-TARGET'); - break; - } - case 'PART': - { - let partList = level.partList; - if (!partList) { - partList = level.partList = []; - } - const previousFragmentPart = currentPart > 0 ? partList[partList.length - 1] : undefined; - const index = currentPart++; - const partAttrs = new AttrList(value1, level); - const part = new Part(partAttrs, frag, baseurl, index, previousFragmentPart); - partList.push(part); - frag.duration += part.duration; - break; - } - case 'PRELOAD-HINT': - { - const preloadHintAttrs = new AttrList(value1, level); - level.preloadHint = preloadHintAttrs; - break; - } - case 'RENDITION-REPORT': - { - const renditionReportAttrs = new AttrList(value1, level); - level.renditionReports = level.renditionReports || []; - level.renditionReports.push(renditionReportAttrs); - break; - } - default: - logger.warn(`line parsed but not handled: ${result}`); - break; - } - } - } - if (prevFrag && !prevFrag.relurl) { - fragments.pop(); - totalduration -= prevFrag.duration; - if (level.partList) { - level.fragmentHint = prevFrag; - } - } else if (level.partList) { - assignProgramDateTime(frag, prevFrag, programDateTimes); - frag.cc = discontinuityCounter; - level.fragmentHint = frag; - if (levelkeys) { - setFragLevelKeys(frag, levelkeys, level); - } - } - const fragmentLength = fragments.length; - const firstFragment = fragments[0]; - const lastFragment = fragments[fragmentLength - 1]; - totalduration += level.skippedSegments * level.targetduration; - if (totalduration > 0 && fragmentLength && lastFragment) { - level.averagetargetduration = totalduration / fragmentLength; - const lastSn = lastFragment.sn; - level.endSN = lastSn !== 'initSegment' ? lastSn : 0; - if (!level.live) { - lastFragment.endList = true; - } - if (firstFragment) { - level.startCC = firstFragment.cc; - } - /** - * Backfill any missing PDT values - * "If the first EXT-X-PROGRAM-DATE-TIME tag in a Playlist appears after - * one or more Media Segment URIs, the client SHOULD extrapolate - * backward from that tag (using EXTINF durations and/or media - * timestamps) to associate dates with those segments." - * We have already extrapolated forward, but all fragments up to the first instance of PDT do not have their PDTs - * computed. - */ - if (firstPdtIndex > 0) { - backfillProgramDateTimes(fragments, firstPdtIndex); - if (firstFragment) { - programDateTimes.unshift(firstFragment); - } - } - } else { - level.endSN = 0; - level.startCC = 0; - } - if (level.fragmentHint) { - totalduration += level.fragmentHint.duration; - } - level.totalduration = totalduration; - if (programDateTimes.length && level.dateRangeTagCount && firstFragment) { - mapDateRanges(programDateTimes, level); - } - level.endCC = discontinuityCounter; - return level; - } -} -function mapDateRanges(programDateTimes, details) { - // Make sure DateRanges are mapped to a ProgramDateTime tag that applies a date to a segment that overlaps with its start date - const programDateTimeCount = programDateTimes.length; - const lastProgramDateTime = programDateTimes[programDateTimeCount - 1]; - const playlistEnd = details.live ? Infinity : details.totalduration; - const dateRangeIds = Object.keys(details.dateRanges); - for (let i = dateRangeIds.length; i--;) { - const dateRange = details.dateRanges[dateRangeIds[i]]; - const startDateTime = dateRange.startDate.getTime(); - dateRange.tagAnchor = lastProgramDateTime; - for (let j = programDateTimeCount; j--;) { - const fragIndex = findFragmentWithStartDate(details, startDateTime, programDateTimes, j, playlistEnd); - if (fragIndex !== -1) { - dateRange.tagAnchor = details.fragments[fragIndex]; - break; - } - } - } -} -function findFragmentWithStartDate(details, startDateTime, programDateTimes, index, endTime) { - const pdtFragment = programDateTimes[index]; - if (pdtFragment) { - var _programDateTimes; - // find matching range between PDT tags - const durationBetweenPdt = (((_programDateTimes = programDateTimes[index + 1]) == null ? void 0 : _programDateTimes.start) || endTime) - pdtFragment.start; - const pdtStart = pdtFragment.programDateTime; - if ((startDateTime >= pdtStart || index === 0) && startDateTime <= pdtStart + durationBetweenPdt * 1000) { - // map to fragment with date-time range - const startIndex = programDateTimes[index].sn - details.startSN; - const fragments = details.fragments; - if (fragments.length > programDateTimes.length) { - const endSegment = programDateTimes[index + 1] || fragments[fragments.length - 1]; - const endIndex = endSegment.sn - details.startSN; - for (let i = endIndex; i > startIndex; i--) { - const fragStartDateTime = fragments[i].programDateTime; - if (startDateTime >= fragStartDateTime && startDateTime < fragStartDateTime + fragments[i].duration * 1000) { - return i; - } - } - } - return startIndex; - } - } - return -1; -} -function parseKey(keyTagAttributes, baseurl, parsed) { - var _keyAttrs$METHOD, _keyAttrs$KEYFORMAT; - // https://tools.ietf.org/html/rfc8216#section-4.3.2.4 - const keyAttrs = new AttrList(keyTagAttributes, parsed); - const decryptmethod = (_keyAttrs$METHOD = keyAttrs.METHOD) != null ? _keyAttrs$METHOD : ''; - const decrypturi = keyAttrs.URI; - const decryptiv = keyAttrs.hexadecimalInteger('IV'); - const decryptkeyformatversions = keyAttrs.KEYFORMATVERSIONS; - // From RFC: This attribute is OPTIONAL; its absence indicates an implicit value of "identity". - const decryptkeyformat = (_keyAttrs$KEYFORMAT = keyAttrs.KEYFORMAT) != null ? _keyAttrs$KEYFORMAT : 'identity'; - if (decrypturi && keyAttrs.IV && !decryptiv) { - logger.error(`Invalid IV: ${keyAttrs.IV}`); - } - // If decrypturi is a URI with a scheme, then baseurl will be ignored - // No uri is allowed when METHOD is NONE - const resolvedUri = decrypturi ? M3U8Parser.resolve(decrypturi, baseurl) : ''; - const keyFormatVersions = (decryptkeyformatversions ? decryptkeyformatversions : '1').split('/').map(Number).filter(Number.isFinite); - return new LevelKey(decryptmethod, resolvedUri, decryptkeyformat, keyFormatVersions, decryptiv); -} -function parseStartTimeOffset(startAttributes) { - const startAttrs = new AttrList(startAttributes); - const startTimeOffset = startAttrs.decimalFloatingPoint('TIME-OFFSET'); - if (isFiniteNumber(startTimeOffset)) { - return startTimeOffset; - } - return null; -} -function setCodecs(codecsAttributeValue, level) { - let codecs = (codecsAttributeValue || '').split(/[ ,]+/).filter(c => c); - ['video', 'audio', 'text'].forEach(type => { - const filtered = codecs.filter(codec => isCodecType(codec, type)); - if (filtered.length) { - // Comma separated list of all codecs for type - level[`${type}Codec`] = filtered.join(','); - // Remove known codecs so that only unknownCodecs are left after iterating through each type - codecs = codecs.filter(codec => filtered.indexOf(codec) === -1); - } - }); - level.unknownCodecs = codecs; -} -function assignCodec(media, groupItem, codecProperty) { - const codecValue = groupItem[codecProperty]; - if (codecValue) { - media[codecProperty] = codecValue; - } -} -function backfillProgramDateTimes(fragments, firstPdtIndex) { - let fragPrev = fragments[firstPdtIndex]; - for (let i = firstPdtIndex; i--;) { - const frag = fragments[i]; - // Exit on delta-playlist skipped segments - if (!frag) { - return; - } - frag.programDateTime = fragPrev.programDateTime - frag.duration * 1000; - fragPrev = frag; - } -} -function assignProgramDateTime(frag, prevFrag, programDateTimes) { - if (frag.rawProgramDateTime) { - frag.programDateTime = Date.parse(frag.rawProgramDateTime); - if (!isFiniteNumber(frag.programDateTime)) { - frag.programDateTime = null; - frag.rawProgramDateTime = null; - return; - } - programDateTimes.push(frag); - } else if (prevFrag != null && prevFrag.programDateTime) { - frag.programDateTime = prevFrag.endProgramDateTime; - } -} -function setInitSegment(frag, mapAttrs, id, levelkeys) { - frag.relurl = mapAttrs.URI; - if (mapAttrs.BYTERANGE) { - frag.setByteRange(mapAttrs.BYTERANGE); - } - frag.level = id; - frag.sn = 'initSegment'; - if (levelkeys) { - frag.levelkeys = levelkeys; - } - frag.initSegment = null; -} -function setFragLevelKeys(frag, levelkeys, level) { - 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); - } -} - -var PlaylistContextType = { - MANIFEST: "manifest", - LEVEL: "level", - AUDIO_TRACK: "audioTrack", - SUBTITLE_TRACK: "subtitleTrack" -}; -var PlaylistLevelType = { - MAIN: "main", - AUDIO: "audio", - SUBTITLE: "subtitle" -}; - -function mapContextToLevelType(context) { - const { - type - } = context; - switch (type) { - case PlaylistContextType.AUDIO_TRACK: - return PlaylistLevelType.AUDIO; - case PlaylistContextType.SUBTITLE_TRACK: - return PlaylistLevelType.SUBTITLE; - default: - return PlaylistLevelType.MAIN; - } -} -function getResponseUrl(response, context) { - let url = response.url; - // responseURL not supported on some browsers (it is used to detect URL redirection) - // data-uri mode also not supported (but no need to detect redirection) - if (url === undefined || url.indexOf('data:') === 0) { - // fallback to initial URL - url = context.url; - } - return url; -} -class PlaylistLoader { - constructor(hls) { - this.hls = void 0; - this.loaders = Object.create(null); - this.variableList = null; - this.hls = hls; - this.registerListeners(); - } - startLoad(startPosition) {} - stopLoad() { - this.destroyInternalLoaders(); - } - registerListeners() { - const { - hls - } = this; - hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); - hls.on(Events.LEVEL_LOADING, this.onLevelLoading, this); - hls.on(Events.AUDIO_TRACK_LOADING, this.onAudioTrackLoading, this); - hls.on(Events.SUBTITLE_TRACK_LOADING, this.onSubtitleTrackLoading, this); - } - unregisterListeners() { - const { - hls - } = this; - hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); - hls.off(Events.LEVEL_LOADING, this.onLevelLoading, this); - hls.off(Events.AUDIO_TRACK_LOADING, this.onAudioTrackLoading, this); - hls.off(Events.SUBTITLE_TRACK_LOADING, this.onSubtitleTrackLoading, this); - } - - /** - * Returns defaults or configured loader-type overloads (pLoader and loader config params) - */ - createInternalLoader(context) { - const config = this.hls.config; - const PLoader = config.pLoader; - const Loader = config.loader; - const InternalLoader = PLoader || Loader; - const loader = new InternalLoader(config); - this.loaders[context.type] = loader; - return loader; - } - getInternalLoader(context) { - return this.loaders[context.type]; - } - resetInternalLoader(contextType) { - if (this.loaders[contextType]) { - delete this.loaders[contextType]; - } - } - - /** - * Call `destroy` on all internal loader instances mapped (one per context type) - */ - destroyInternalLoaders() { - for (const contextType in this.loaders) { - const loader = this.loaders[contextType]; - if (loader) { - loader.destroy(); - } - this.resetInternalLoader(contextType); - } - } - destroy() { - this.variableList = null; - this.unregisterListeners(); - this.destroyInternalLoaders(); - } - onManifestLoading(event, data) { - const { - url - } = data; - this.variableList = null; - this.load({ - id: null, - level: 0, - responseType: 'text', - type: PlaylistContextType.MANIFEST, - url, - deliveryDirectives: null - }); - } - onLevelLoading(event, data) { - const { - id, - level, - pathwayId, - url, - deliveryDirectives - } = data; - this.load({ - id, - level, - pathwayId, - responseType: 'text', - type: PlaylistContextType.LEVEL, - url, - deliveryDirectives - }); - } - onAudioTrackLoading(event, data) { - const { - id, - groupId, - url, - deliveryDirectives - } = data; - this.load({ - id, - groupId, - level: null, - responseType: 'text', - type: PlaylistContextType.AUDIO_TRACK, - url, - deliveryDirectives - }); - } - onSubtitleTrackLoading(event, data) { - const { - id, - groupId, - url, - deliveryDirectives - } = data; - this.load({ - id, - groupId, - level: null, - responseType: 'text', - type: PlaylistContextType.SUBTITLE_TRACK, - url, - deliveryDirectives - }); - } - load(context) { - var _context$deliveryDire; - const config = this.hls.config; - - // logger.debug(`[playlist-loader]: Loading playlist of type ${context.type}, level: ${context.level}, id: ${context.id}`); - - // Check if a loader for this context already exists - let loader = this.getInternalLoader(context); - if (loader) { - const loaderContext = loader.context; - if (loaderContext && loaderContext.url === context.url && loaderContext.level === context.level) { - // same URL can't overlap - this.hls.logger.trace('[playlist-loader]: playlist request ongoing'); - return; - } - this.hls.logger.log(`[playlist-loader]: aborting previous loader for type: ${context.type}`); - loader.abort(); - } - - // apply different configs for retries depending on - // context (manifest, level, audio/subs playlist) - let loadPolicy; - if (context.type === PlaylistContextType.MANIFEST) { - loadPolicy = config.manifestLoadPolicy.default; - } else { - loadPolicy = _extends({}, config.playlistLoadPolicy.default, { - timeoutRetry: null, - errorRetry: null - }); - } - loader = this.createInternalLoader(context); - - // Override level/track timeout for LL-HLS requests - // (the default of 10000ms is counter productive to blocking playlist reload requests) - if (isFiniteNumber((_context$deliveryDire = context.deliveryDirectives) == null ? void 0 : _context$deliveryDire.part)) { - let levelDetails; - if (context.type === PlaylistContextType.LEVEL && context.level !== null) { - levelDetails = this.hls.levels[context.level].details; - } else if (context.type === PlaylistContextType.AUDIO_TRACK && context.id !== null) { - levelDetails = this.hls.audioTracks[context.id].details; - } else if (context.type === PlaylistContextType.SUBTITLE_TRACK && context.id !== null) { - levelDetails = this.hls.subtitleTracks[context.id].details; - } - if (levelDetails) { - const partTarget = levelDetails.partTarget; - const targetDuration = levelDetails.targetduration; - if (partTarget && targetDuration) { - const maxLowLatencyPlaylistRefresh = Math.max(partTarget * 3, targetDuration * 0.8) * 1000; - loadPolicy = _extends({}, loadPolicy, { - maxTimeToFirstByteMs: Math.min(maxLowLatencyPlaylistRefresh, loadPolicy.maxTimeToFirstByteMs), - maxLoadTimeMs: Math.min(maxLowLatencyPlaylistRefresh, loadPolicy.maxTimeToFirstByteMs) - }); - } - } - } - const legacyRetryCompatibility = loadPolicy.errorRetry || loadPolicy.timeoutRetry || {}; - const loaderConfig = { - loadPolicy, - timeout: loadPolicy.maxLoadTimeMs, - maxRetry: legacyRetryCompatibility.maxNumRetry || 0, - retryDelay: legacyRetryCompatibility.retryDelayMs || 0, - maxRetryDelay: legacyRetryCompatibility.maxRetryDelayMs || 0 - }; - const loaderCallbacks = { - onSuccess: (response, stats, context, networkDetails) => { - const loader = this.getInternalLoader(context); - this.resetInternalLoader(context.type); - const string = response.data; - - // Validate if it is an M3U8 at all - if (string.indexOf('#EXTM3U') !== 0) { - this.handleManifestParsingError(response, context, new Error('no EXTM3U delimiter'), networkDetails || null, stats); - return; - } - stats.parsing.start = performance.now(); - if (M3U8Parser.isMediaPlaylist(string)) { - this.handleTrackOrLevelPlaylist(response, stats, context, networkDetails || null, loader); - } else { - this.handleMasterPlaylist(response, stats, context, networkDetails); - } - }, - onError: (response, context, networkDetails, stats) => { - this.handleNetworkError(context, networkDetails, false, response, stats); - }, - onTimeout: (stats, context, networkDetails) => { - this.handleNetworkError(context, networkDetails, true, undefined, stats); - } - }; - - // logger.debug(`[playlist-loader]: Calling internal loader delegate for URL: ${context.url}`); - - loader.load(context, loaderConfig, loaderCallbacks); - } - handleMasterPlaylist(response, stats, context, networkDetails) { - const hls = this.hls; - const string = response.data; - const url = getResponseUrl(response, context); - const parsedResult = M3U8Parser.parseMasterPlaylist(string, url); - if (parsedResult.playlistParsingError) { - this.handleManifestParsingError(response, context, parsedResult.playlistParsingError, networkDetails, stats); - return; - } - const { - contentSteering, - levels, - sessionData, - sessionKeys, - startTimeOffset, - variableList - } = parsedResult; - this.variableList = variableList; - const { - AUDIO: audioTracks = [], - SUBTITLES: subtitles, - 'CLOSED-CAPTIONS': captions - } = M3U8Parser.parseMasterPlaylistMedia(string, url, parsedResult); - if (audioTracks.length) { - // check if we have found an audio track embedded in main playlist (audio track without URI attribute) - const embeddedAudioFound = audioTracks.some(audioTrack => !audioTrack.url); - - // if no embedded audio track defined, but audio codec signaled in quality level, - // we need to signal this main audio track this could happen with playlists with - // alt audio rendition in which quality levels (main) - // contains both audio+video. but with mixed audio track not signaled - if (!embeddedAudioFound && levels[0].audioCodec && !levels[0].attrs.AUDIO) { - this.hls.logger.log('[playlist-loader]: audio codec signaled in quality level, but no embedded audio track signaled, create one'); - audioTracks.unshift({ - type: 'main', - name: 'main', - groupId: 'main', - default: false, - autoselect: false, - forced: false, - id: -1, - attrs: new AttrList({}), - bitrate: 0, - url: '' - }); - } - } - hls.trigger(Events.MANIFEST_LOADED, { - levels, - audioTracks, - subtitles, - captions, - contentSteering, - url, - stats, - networkDetails, - sessionData, - sessionKeys, - startTimeOffset, - variableList - }); - } - handleTrackOrLevelPlaylist(response, stats, context, networkDetails, loader) { - const hls = this.hls; - const { - id, - level, - type - } = context; - const url = getResponseUrl(response, context); - const levelId = isFiniteNumber(level) ? level : isFiniteNumber(id) ? id : 0; - const levelType = mapContextToLevelType(context); - const levelDetails = M3U8Parser.parseLevelPlaylist(response.data, url, levelId, levelType, 0, this.variableList); - - // We have done our first request (Manifest-type) and receive - // not a master playlist but a chunk-list (track/level) - // We fire the manifest-loaded event anyway with the parsed level-details - // by creating a single-level structure for it. - if (type === PlaylistContextType.MANIFEST) { - const singleLevel = { - attrs: new AttrList({}), - bitrate: 0, - details: levelDetails, - name: '', - url - }; - hls.trigger(Events.MANIFEST_LOADED, { - levels: [singleLevel], - audioTracks: [], - url, - stats, - networkDetails, - sessionData: null, - sessionKeys: null, - contentSteering: null, - startTimeOffset: null, - variableList: null - }); - } - - // save parsing time - stats.parsing.end = performance.now(); - - // extend the context with the new levelDetails property - context.levelDetails = levelDetails; - this.handlePlaylistLoaded(levelDetails, response, stats, context, networkDetails, loader); - } - handleManifestParsingError(response, context, error, networkDetails, stats) { - this.hls.trigger(Events.ERROR, { - type: ErrorTypes.NETWORK_ERROR, - details: ErrorDetails.MANIFEST_PARSING_ERROR, - fatal: context.type === PlaylistContextType.MANIFEST, - url: response.url, - err: error, - error, - reason: error.message, - response, - context, - networkDetails, - stats - }); - } - handleNetworkError(context, networkDetails, timeout = false, response, stats) { - let message = `A network ${timeout ? 'timeout' : 'error' + (response ? ' (status ' + response.code + ')' : '')} occurred while loading ${context.type}`; - if (context.type === PlaylistContextType.LEVEL) { - message += `: ${context.level} id: ${context.id}`; - } else if (context.type === PlaylistContextType.AUDIO_TRACK || context.type === PlaylistContextType.SUBTITLE_TRACK) { - message += ` id: ${context.id} group-id: "${context.groupId}"`; - } - const error = new Error(message); - this.hls.logger.warn(`[playlist-loader]: ${message}`); - let details = ErrorDetails.UNKNOWN; - let fatal = false; - const loader = this.getInternalLoader(context); - switch (context.type) { - case PlaylistContextType.MANIFEST: - details = timeout ? ErrorDetails.MANIFEST_LOAD_TIMEOUT : ErrorDetails.MANIFEST_LOAD_ERROR; - fatal = true; - break; - case PlaylistContextType.LEVEL: - details = timeout ? ErrorDetails.LEVEL_LOAD_TIMEOUT : ErrorDetails.LEVEL_LOAD_ERROR; - fatal = false; - break; - case PlaylistContextType.AUDIO_TRACK: - details = timeout ? ErrorDetails.AUDIO_TRACK_LOAD_TIMEOUT : ErrorDetails.AUDIO_TRACK_LOAD_ERROR; - fatal = false; - break; - case PlaylistContextType.SUBTITLE_TRACK: - details = timeout ? ErrorDetails.SUBTITLE_TRACK_LOAD_TIMEOUT : ErrorDetails.SUBTITLE_LOAD_ERROR; - fatal = false; - break; - } - if (loader) { - this.resetInternalLoader(context.type); - } - const errorData = { - type: ErrorTypes.NETWORK_ERROR, - details, - fatal, - url: context.url, - loader, - context, - error, - networkDetails, - stats - }; - if (response) { - const url = (networkDetails == null ? void 0 : networkDetails.url) || context.url; - errorData.response = _objectSpread2({ - url, - data: undefined - }, response); - } - this.hls.trigger(Events.ERROR, errorData); - } - handlePlaylistLoaded(levelDetails, response, stats, context, networkDetails, loader) { - const hls = this.hls; - const { - type, - level, - id, - groupId, - deliveryDirectives - } = context; - const url = getResponseUrl(response, context); - const parent = mapContextToLevelType(context); - const levelIndex = typeof context.level === 'number' && parent === PlaylistLevelType.MAIN ? level : undefined; - if (!levelDetails.fragments.length) { - const _error = new Error('No Segments found in Playlist'); - hls.trigger(Events.ERROR, { - type: ErrorTypes.NETWORK_ERROR, - details: ErrorDetails.LEVEL_EMPTY_ERROR, - fatal: false, - url, - error: _error, - reason: _error.message, - response, - context, - level: levelIndex, - parent, - networkDetails, - stats - }); - return; - } - if (!levelDetails.targetduration) { - levelDetails.playlistParsingError = new Error('Missing Target Duration'); - } - const error = levelDetails.playlistParsingError; - if (error) { - hls.trigger(Events.ERROR, { - type: ErrorTypes.NETWORK_ERROR, - details: ErrorDetails.LEVEL_PARSING_ERROR, - fatal: false, - url, - error, - reason: error.message, - response, - context, - level: levelIndex, - parent, - networkDetails, - stats - }); - return; - } - if (levelDetails.live && loader) { - if (loader.getCacheAge) { - levelDetails.ageHeader = loader.getCacheAge() || 0; - } - if (!loader.getCacheAge || isNaN(levelDetails.ageHeader)) { - levelDetails.ageHeader = 0; - } - } - switch (type) { - case PlaylistContextType.MANIFEST: - case PlaylistContextType.LEVEL: - hls.trigger(Events.LEVEL_LOADED, { - details: levelDetails, - level: levelIndex || 0, - id: id || 0, - stats, - networkDetails, - deliveryDirectives - }); - break; - case PlaylistContextType.AUDIO_TRACK: - hls.trigger(Events.AUDIO_TRACK_LOADED, { - details: levelDetails, - id: id || 0, - groupId: groupId || '', - stats, - networkDetails, - deliveryDirectives - }); - break; - case PlaylistContextType.SUBTITLE_TRACK: - hls.trigger(Events.SUBTITLE_TRACK_LOADED, { - details: levelDetails, - id: id || 0, - groupId: groupId || '', - stats, - networkDetails, - deliveryDirectives - }); - break; - } - } -} - -function sendAddTrackEvent(track, videoEl) { - let event; - try { - event = new Event('addtrack'); - } catch (err) { - // for IE11 - event = document.createEvent('Event'); - event.initEvent('addtrack', false, false); - } - event.track = track; - videoEl.dispatchEvent(event); -} -function addCueToTrack(track, cue) { - // Sometimes there are cue overlaps on segmented vtts so the same - // cue can appear more than once in different vtt files. - // This avoid showing duplicated cues with same timecode and text. - const mode = track.mode; - if (mode === 'disabled') { - track.mode = 'hidden'; - } - if (track.cues && !track.cues.getCueById(cue.id)) { - try { - track.addCue(cue); - if (!track.cues.getCueById(cue.id)) { - throw new Error(`addCue is failed for: ${cue}`); - } - } catch (err) { - logger.debug(`[texttrack-utils]: ${err}`); - try { - const textTrackCue = new self.TextTrackCue(cue.startTime, cue.endTime, cue.text); - textTrackCue.id = cue.id; - track.addCue(textTrackCue); - } catch (err2) { - logger.debug(`[texttrack-utils]: Legacy TextTrackCue fallback failed: ${err2}`); - } - } - } - if (mode === 'disabled') { - track.mode = mode; - } -} -function clearCurrentCues(track) { - // When track.mode is disabled, track.cues will be null. - // To guarantee the removal of cues, we need to temporarily - // change the mode to hidden - const mode = track.mode; - if (mode === 'disabled') { - track.mode = 'hidden'; - } - if (track.cues) { - for (let i = track.cues.length; i--;) { - track.removeCue(track.cues[i]); - } - } - if (mode === 'disabled') { - track.mode = mode; - } -} -function removeCuesInRange(track, start, end, predicate) { - const mode = track.mode; - if (mode === 'disabled') { - track.mode = 'hidden'; - } - if (track.cues && track.cues.length > 0) { - const cues = getCuesInRange(track.cues, start, end); - for (let i = 0; i < cues.length; i++) { - if (!predicate || predicate(cues[i])) { - track.removeCue(cues[i]); - } - } - } - if (mode === 'disabled') { - track.mode = mode; - } -} - -// Find first cue starting after given time. -// Modified version of binary search O(log(n)). -function getFirstCueIndexAfterTime(cues, time) { - // If first cue starts after time, start there - if (time < cues[0].startTime) { - return 0; - } - // If the last cue ends before time there is no overlap - const len = cues.length - 1; - if (time > cues[len].endTime) { - return -1; - } - let left = 0; - let right = len; - while (left <= right) { - const mid = Math.floor((right + left) / 2); - if (time < cues[mid].startTime) { - right = mid - 1; - } else if (time > cues[mid].startTime && left < len) { - left = mid + 1; - } else { - // If it's not lower or higher, it must be equal. - return mid; - } - } - // At this point, left and right have swapped. - // No direct match was found, left or right element must be the closest. Check which one has the smallest diff. - return cues[left].startTime - time < time - cues[right].startTime ? left : right; -} -function getCuesInRange(cues, start, end) { - const cuesFound = []; - const firstCueInRange = getFirstCueIndexAfterTime(cues, start); - if (firstCueInRange > -1) { - for (let i = firstCueInRange, len = cues.length; i < len; i++) { - const cue = cues[i]; - if (cue.startTime >= start && cue.endTime <= end) { - cuesFound.push(cue); - } else if (cue.startTime > end) { - return cuesFound; - } - } - } - return cuesFound; -} -function filterSubtitleTracks(textTrackList) { - const tracks = []; - for (let i = 0; i < textTrackList.length; i++) { - const track = textTrackList[i]; - // Edge adds a track without a label; we don't want to use it - if ((track.kind === 'subtitles' || track.kind === 'captions') && track.label) { - tracks.push(textTrackList[i]); - } - } - return tracks; -} - -let MetadataSchema = /*#__PURE__*/function (MetadataSchema) { - MetadataSchema["audioId3"] = "org.id3"; - MetadataSchema["dateRange"] = "com.apple.quicktime.HLS"; - MetadataSchema["emsg"] = "https://aomedia.org/emsg/ID3"; - MetadataSchema["misbklv"] = "urn:misb:KLV:bin:1910.1"; - return MetadataSchema; -}({}); - -/** - * Decode an ID3 PRIV frame. - * - * @param frame - the ID3 PRIV frame - * - * @returns The decoded ID3 PRIV frame - * - * @internal - * - * @group ID3 - */ -function decodeId3PrivFrame(frame) { - /* - Format: <text string>\0<binary data> - */ - if (frame.size < 2) { - return undefined; - } - const owner = utf8ArrayToStr(frame.data, true); - const privateData = new Uint8Array(frame.data.subarray(owner.length + 1)); - return { - key: frame.type, - info: owner, - data: privateData.buffer - }; -} - -/** - * Decodes an ID3 text frame - * - * @param frame - the ID3 text frame - * - * @returns The decoded ID3 text frame - * - * @internal - * - * @group ID3 - */ -function decodeId3TextFrame(frame) { - if (frame.size < 2) { - return undefined; - } - if (frame.type === 'TXXX') { - /* - Format: - [0] = {Text Encoding} - [1-?] = {Description}\0{Value} - */ - let index = 1; - const description = utf8ArrayToStr(frame.data.subarray(index), true); - index += description.length + 1; - const value = utf8ArrayToStr(frame.data.subarray(index)); - return { - key: frame.type, - info: description, - data: value - }; - } - /* - Format: - [0] = {Text Encoding} - [1-?] = {Value} - */ - const text = utf8ArrayToStr(frame.data.subarray(1)); - return { - key: frame.type, - info: '', - data: text - }; -} - -/** - * Decode a URL frame - * - * @param frame - the ID3 URL frame - * - * @returns The decoded ID3 URL frame - * - * @internal - * - * @group ID3 - */ -function decodeId3UrlFrame(frame) { - if (frame.type === 'WXXX') { - /* - Format: - [0] = {Text Encoding} - [1-?] = {Description}\0{URL} - */ - if (frame.size < 2) { - return undefined; - } - let index = 1; - const description = utf8ArrayToStr(frame.data.subarray(index), true); - index += description.length + 1; - const value = utf8ArrayToStr(frame.data.subarray(index)); - return { - key: frame.type, - info: description, - data: value - }; - } - /* - Format: - [0-?] = {URL} - */ - const url = utf8ArrayToStr(frame.data); - return { - key: frame.type, - info: '', - data: url - }; -} - -function toUint8(data, offset = 0, length = Infinity) { - return view(data, offset, length, Uint8Array); -} -function view(data, offset, length, Type) { - const buffer = unsafeGetArrayBuffer(data); - let bytesPerElement = 1; - if ('BYTES_PER_ELEMENT' in Type) { - bytesPerElement = Type.BYTES_PER_ELEMENT; - } - // Absolute end of the |data| view within |buffer|. - const dataOffset = isArrayBufferView(data) ? data.byteOffset : 0; - const dataEnd = (dataOffset + data.byteLength) / bytesPerElement; - // Absolute start of the result within |buffer|. - const rawStart = (dataOffset + offset) / bytesPerElement; - const start = Math.floor(Math.max(0, Math.min(rawStart, dataEnd))); - // Absolute end of the result within |buffer|. - const end = Math.floor(Math.min(start + Math.max(length, 0), dataEnd)); - return new Type(buffer, start, end - start); -} -function unsafeGetArrayBuffer(view) { - if (view instanceof ArrayBuffer) { - return view; - } else { - return view.buffer; - } -} -function isArrayBufferView(obj) { - return obj && obj.buffer instanceof ArrayBuffer && obj.byteLength !== undefined && obj.byteOffset !== undefined; -} - -function toArrayBuffer(view) { - if (view instanceof ArrayBuffer) { - return view; - } else { - if (view.byteOffset == 0 && view.byteLength == view.buffer.byteLength) { - // This is a TypedArray over the whole buffer. - return view.buffer; - } - // This is a 'view' on the buffer. Create a new buffer that only contains - // the data. Note that since this isn't an ArrayBuffer, the 'new' call - // will allocate a new buffer to hold the copy. - return new Uint8Array(view).buffer; - } -} - -/** - * Encodes binary data to base64 - * - * @param binary - The binary data to encode - * @returns The base64 encoded string - * - * @group Utils - * - * @beta - */ -function base64encode(binary) { - return btoa(String.fromCharCode(...binary)); -} - -/** - * This implements the rounding procedure described in step 2 of the "Serializing a Decimal" specification. - * This rounding style is known as "even rounding", "banker's rounding", or "commercial rounding". - * - * @param value - The value to round - * @param precision - The number of decimal places to round to - * @returns The rounded value - * - * @group Utils - * - * @beta - */ -function roundToEven(value, precision) { - if (value < 0) { - return -roundToEven(-value, precision); - } - const decimalShift = Math.pow(10, precision); - const isEquidistant = Math.abs(value * decimalShift % 1 - 0.5) < Number.EPSILON; - if (isEquidistant) { - // If the tail of the decimal place is 'equidistant' we round to the nearest even value - const flooredValue = Math.floor(value * decimalShift); - return (flooredValue % 2 === 0 ? flooredValue : flooredValue + 1) / decimalShift; - } else { - // Otherwise, proceed as normal - return Math.round(value * decimalShift) / decimalShift; - } -} - -/** - * Constructs a relative path from a URL. - * - * @param url - The destination URL - * @param base - The base URL - * @returns The relative path - * - * @group Utils - * - * @beta - */ -function urlToRelativePath(url, base) { - const to = new URL(url); - const from = new URL(base); - if (to.origin !== from.origin) { - return url; - } - const toPath = to.pathname.split('/').slice(1); - const fromPath = from.pathname.split('/').slice(1, -1); - // remove common parents - while (toPath[0] === fromPath[0]) { - toPath.shift(); - fromPath.shift(); - } - // add back paths - while (fromPath.length) { - fromPath.shift(); - toPath.unshift('..'); - } - return toPath.join('/'); -} - -/** - * Generate a random v4 UUID - * - * @returns A random v4 UUID - * - * @group Utils - * - * @beta - */ -function uuid() { - try { - return crypto.randomUUID(); - } catch (error) { - try { - const url = URL.createObjectURL(new Blob()); - const uuid = url.toString(); - URL.revokeObjectURL(url); - return uuid.slice(uuid.lastIndexOf('/') + 1); - } catch (error) { - let dt = new Date().getTime(); - const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { - const r = (dt + Math.random() * 16) % 16 | 0; - dt = Math.floor(dt / 16); - return (c == 'x' ? r : r & 0x3 | 0x8).toString(16); - }); - return uuid; - } - } -} - -function decodeId3ImageFrame(frame) { - const metadataFrame = { - key: frame.type, - description: '', - data: '', - mimeType: null, - pictureType: null - }; - const utf8Encoding = 0x03; - if (frame.size < 2) { - return undefined; - } - if (frame.data[0] !== utf8Encoding) { - console.log('Ignore frame with unrecognized character ' + 'encoding'); - return undefined; - } - const mimeTypeEndIndex = frame.data.subarray(1).indexOf(0); - if (mimeTypeEndIndex === -1) { - return undefined; - } - const mimeType = utf8ArrayToStr(toUint8(frame.data, 1, mimeTypeEndIndex)); - const pictureType = frame.data[2 + mimeTypeEndIndex]; - const descriptionEndIndex = frame.data.subarray(3 + mimeTypeEndIndex).indexOf(0); - if (descriptionEndIndex === -1) { - return undefined; - } - const description = utf8ArrayToStr(toUint8(frame.data, 3 + mimeTypeEndIndex, descriptionEndIndex)); - let data; - if (mimeType === '-->') { - data = utf8ArrayToStr(toUint8(frame.data, 4 + mimeTypeEndIndex + descriptionEndIndex)); - } else { - data = toArrayBuffer(frame.data.subarray(4 + mimeTypeEndIndex + descriptionEndIndex)); - } - metadataFrame.mimeType = mimeType; - metadataFrame.pictureType = pictureType; - metadataFrame.description = description; - metadataFrame.data = data; - return metadataFrame; -} - -/** - * Decode an ID3 frame. - * - * @param frame - the ID3 frame - * - * @returns The decoded ID3 frame - * - * @internal - * - * @group ID3 - */ -function decodeId3Frame(frame) { - if (frame.type === 'PRIV') { - return decodeId3PrivFrame(frame); - } else if (frame.type[0] === 'W') { - return decodeId3UrlFrame(frame); - } else if (frame.type === 'APIC') { - return decodeId3ImageFrame(frame); - } - return decodeId3TextFrame(frame); -} - -/** - * Read ID3 size - * - * @param data - The data to read from - * @param offset - The offset at which to start reading - * - * @returns The size - * - * @internal - * - * @group ID3 - */ -function readId3Size(data, offset) { - let size = 0; - size = (data[offset] & 0x7f) << 21; - size |= (data[offset + 1] & 0x7f) << 14; - size |= (data[offset + 2] & 0x7f) << 7; - size |= data[offset + 3] & 0x7f; - return size; -} - -/** - * Returns the data of an ID3 frame. - * - * @param data - The data to read from - * - * @returns The data of the ID3 frame - * - * @internal - * - * @group ID3 - */ -function getId3FrameData(data) { - /* - Frame ID $xx xx xx xx (four characters) - Size $xx xx xx xx - Flags $xx xx - */ - const type = String.fromCharCode(data[0], data[1], data[2], data[3]); - const size = readId3Size(data, 4); - // skip frame id, size, and flags - const offset = 10; - return { - type, - size, - data: data.subarray(offset, offset + size) - }; -} - -/** - * Returns true if an ID3 footer can be found at offset in data - * - * @param data - The data to search in - * @param offset - The offset at which to start searching - * - * @returns `true` if an ID3 footer is found - * - * @internal - * - * @group ID3 - */ -function isId3Footer(data, offset) { - /* - * The footer is a copy of the header, but with a different identifier - */ - if (offset + 10 <= data.length) { - // look for '3DI' identifier - if (data[offset] === 0x33 && data[offset + 1] === 0x44 && data[offset + 2] === 0x49) { - // check version is within range - if (data[offset + 3] < 0xff && data[offset + 4] < 0xff) { - // check size is within range - if (data[offset + 6] < 0x80 && data[offset + 7] < 0x80 && data[offset + 8] < 0x80 && data[offset + 9] < 0x80) { - return true; - } - } - } - } - return false; -} - -/** - * Returns true if an ID3 header can be found at offset in data - * - * @param data - The data to search in - * @param offset - The offset at which to start searching - * - * @returns `true` if an ID3 header is found - * - * @internal - * - * @group ID3 - */ -function isId3Header(data, offset) { - /* - * http://id3.org/id3v2.3.0 - * [0] = 'I' - * [1] = 'D' - * [2] = '3' - * [3,4] = {Version} - * [5] = {Flags} - * [6-9] = {ID3 Size} - * - * An ID3v2 tag can be detected with the following pattern: - * $49 44 33 yy yy xx zz zz zz zz - * Where yy is less than $FF, xx is the 'flags' byte and zz is less than $80 - */ - if (offset + 10 <= data.length) { - // look for 'ID3' identifier - if (data[offset] === 0x49 && data[offset + 1] === 0x44 && data[offset + 2] === 0x33) { - // check version is within range - if (data[offset + 3] < 0xff && data[offset + 4] < 0xff) { - // check size is within range - if (data[offset + 6] < 0x80 && data[offset + 7] < 0x80 && data[offset + 8] < 0x80 && data[offset + 9] < 0x80) { - return true; - } - } - } - } - return false; -} - -const HEADER_FOOTER_SIZE = 10; -const FRAME_SIZE = 10; -/** - * Returns an array of ID3 frames found in all the ID3 tags in the id3Data - * - * @param id3Data - The ID3 data containing one or more ID3 tags - * - * @returns Array of ID3 frame objects - * - * @group ID3 - * - * @beta - */ -function getId3Frames(id3Data) { - let offset = 0; - const frames = []; - while (isId3Header(id3Data, offset)) { - const size = readId3Size(id3Data, offset + 6); - if (id3Data[offset + 5] >> 6 & 1) { - // skip extended header - offset += HEADER_FOOTER_SIZE; - } - // skip past ID3 header - offset += HEADER_FOOTER_SIZE; - const end = offset + size; - // loop through frames in the ID3 tag - while (offset + FRAME_SIZE < end) { - const frameData = getId3FrameData(id3Data.subarray(offset)); - const frame = decodeId3Frame(frameData); - if (frame) { - frames.push(frame); - } - // skip frame header and frame data - offset += frameData.size + HEADER_FOOTER_SIZE; - } - if (isId3Footer(id3Data, offset)) { - offset += HEADER_FOOTER_SIZE; - } - } - return frames; -} - -/** - * Returns true if the ID3 frame is an Elementary Stream timestamp frame - * - * @param frame - the ID3 frame - * - * @returns `true` if the ID3 frame is an Elementary Stream timestamp frame - * - * @internal - * - * @group ID3 - */ -function isId3TimestampFrame(frame) { - return frame && frame.key === 'PRIV' && frame.info === 'com.apple.streaming.transportStreamTimestamp'; -} - -const MIN_CUE_DURATION = 0.25; -function getCueClass() { - if (typeof self === 'undefined') return undefined; - return self.VTTCue || self.TextTrackCue; -} -function createCueWithDataFields(Cue, startTime, endTime, data, type) { - let cue = new Cue(startTime, endTime, ''); - try { - cue.value = data; - if (type) { - cue.type = type; - } - } catch (e) { - cue = new Cue(startTime, endTime, JSON.stringify(type ? _objectSpread2({ - type - }, data) : data)); - } - return cue; -} - -// VTTCue latest draft allows an infinite duration, fallback -// to MAX_VALUE if necessary -const MAX_CUE_ENDTIME = (() => { - const Cue = getCueClass(); - try { - Cue && new Cue(0, Number.POSITIVE_INFINITY, ''); - } catch (e) { - return Number.MAX_VALUE; - } - return Number.POSITIVE_INFINITY; -})(); -function hexToArrayBuffer(str) { - return Uint8Array.from(str.replace(/^0x/, '').replace(/([\da-fA-F]{2}) ?/g, '0x$1 ').replace(/ +$/, '').split(' ')).buffer; -} -class ID3TrackController { - constructor(hls) { - this.hls = void 0; - this.id3Track = null; - this.media = null; - this.dateRangeCuesAppended = {}; - this.removeCues = true; - this.hls = hls; - this._registerListeners(); - } - destroy() { - this._unregisterListeners(); - this.id3Track = null; - this.media = null; - this.dateRangeCuesAppended = {}; - // @ts-ignore - this.hls = null; - } - _registerListeners() { - const { - hls - } = this; - hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); - hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this); - hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); - hls.on(Events.FRAG_PARSING_METADATA, this.onFragParsingMetadata, this); - hls.on(Events.BUFFER_FLUSHING, this.onBufferFlushing, this); - hls.on(Events.LEVEL_UPDATED, this.onLevelUpdated, this); - hls.on(Events.LEVEL_PTS_UPDATED, this.onLevelPtsUpdated, this); - } - _unregisterListeners() { - const { - hls - } = this; - hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); - hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this); - hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); - hls.off(Events.FRAG_PARSING_METADATA, this.onFragParsingMetadata, this); - hls.off(Events.BUFFER_FLUSHING, this.onBufferFlushing, this); - hls.off(Events.LEVEL_UPDATED, this.onLevelUpdated, this); - hls.off(Events.LEVEL_PTS_UPDATED, this.onLevelPtsUpdated, this); - } - - // Add ID3 metatadata text track. - onMediaAttaching(event, data) { - var _data$overrides; - this.media = data.media; - if (((_data$overrides = data.overrides) == null ? void 0 : _data$overrides.cueRemoval) === false) { - this.removeCues = false; - } - } - onMediaDetaching(event, data) { - this.media = null; - const transferringMedia = !!data.transferMedia; - if (transferringMedia) { - return; - } - if (this.id3Track) { - if (this.removeCues) { - clearCurrentCues(this.id3Track); - } - this.id3Track = null; - } - this.dateRangeCuesAppended = {}; - } - onManifestLoading() { - this.dateRangeCuesAppended = {}; - } - createTrack(media) { - const track = this.getID3Track(media.textTracks); - track.mode = 'hidden'; - return track; - } - getID3Track(textTracks) { - if (!this.media) { - return; - } - for (let i = 0; i < textTracks.length; i++) { - const textTrack = textTracks[i]; - if (textTrack.kind === 'metadata' && textTrack.label === 'id3') { - // send 'addtrack' when reusing the textTrack for metadata, - // same as what we do for captions - sendAddTrackEvent(textTrack, this.media); - return textTrack; - } - } - return this.media.addTextTrack('metadata', 'id3'); - } - onFragParsingMetadata(event, data) { - if (!this.media) { - return; - } - const { - hls: { - config: { - enableEmsgMetadataCues, - enableID3MetadataCues - } - } - } = this; - if (!enableEmsgMetadataCues && !enableID3MetadataCues) { - return; - } - const { - samples - } = data; - - // create track dynamically - if (!this.id3Track) { - this.id3Track = this.createTrack(this.media); - } - const Cue = getCueClass(); - if (!Cue) { - return; - } - for (let i = 0; i < samples.length; i++) { - const type = samples[i].type; - if (type === MetadataSchema.emsg && !enableEmsgMetadataCues || !enableID3MetadataCues) { - continue; - } - const frames = getId3Frames(samples[i].data); - if (frames) { - const startTime = samples[i].pts; - let endTime = startTime + samples[i].duration; - if (endTime > MAX_CUE_ENDTIME) { - endTime = MAX_CUE_ENDTIME; - } - const timeDiff = endTime - startTime; - if (timeDiff <= 0) { - endTime = startTime + MIN_CUE_DURATION; - } - for (let j = 0; j < frames.length; j++) { - const frame = frames[j]; - // Safari doesn't put the timestamp frame in the TextTrack - if (!isId3TimestampFrame(frame)) { - // add a bounds to any unbounded cues - this.updateId3CueEnds(startTime, type); - const cue = createCueWithDataFields(Cue, startTime, endTime, frame, type); - if (cue) { - this.id3Track.addCue(cue); - } - } - } - } - } - } - updateId3CueEnds(startTime, type) { - var _this$id3Track; - const cues = (_this$id3Track = this.id3Track) == null ? void 0 : _this$id3Track.cues; - if (cues) { - for (let i = cues.length; i--;) { - const cue = cues[i]; - if (cue.type === type && cue.startTime < startTime && cue.endTime === MAX_CUE_ENDTIME) { - cue.endTime = startTime; - } - } - } - } - onBufferFlushing(event, { - startOffset, - endOffset, - type - }) { - const { - id3Track, - hls - } = this; - if (!hls) { - return; - } - const { - config: { - enableEmsgMetadataCues, - enableID3MetadataCues - } - } = hls; - if (id3Track && (enableEmsgMetadataCues || enableID3MetadataCues)) { - let predicate; - if (type === 'audio') { - predicate = cue => cue.type === MetadataSchema.audioId3 && enableID3MetadataCues; - } else if (type === 'video') { - predicate = cue => cue.type === MetadataSchema.emsg && enableEmsgMetadataCues; - } else { - predicate = cue => cue.type === MetadataSchema.audioId3 && enableID3MetadataCues || cue.type === MetadataSchema.emsg && enableEmsgMetadataCues; - } - removeCuesInRange(id3Track, startOffset, endOffset, predicate); - } - } - onLevelUpdated(event, { - details - }) { - this.updateDateRangeCues(details, true); - } - onLevelPtsUpdated(event, data) { - if (Math.abs(data.drift) > 0.01) { - this.updateDateRangeCues(data.details); - } - } - updateDateRangeCues(details, removeOldCues) { - if (!this.media || !details.hasProgramDateTime || !this.hls.config.enableDateRangeMetadataCues) { - return; - } - const { - id3Track - } = this; - const { - dateRanges - } = details; - const ids = Object.keys(dateRanges); - let dateRangeCuesAppended = this.dateRangeCuesAppended; - // Remove cues from track not found in details.dateRanges - if (id3Track && removeOldCues) { - var _id3Track$cues; - if ((_id3Track$cues = id3Track.cues) != null && _id3Track$cues.length) { - const idsToRemove = Object.keys(dateRangeCuesAppended).filter(id => !ids.includes(id)); - for (let i = idsToRemove.length; i--;) { - const id = idsToRemove[i]; - const cues = dateRangeCuesAppended[id].cues; - delete dateRangeCuesAppended[id]; - Object.keys(cues).forEach(key => { - try { - id3Track.removeCue(cues[key]); - } catch (e) { - /* no-op */ - } - }); - } - } else { - dateRangeCuesAppended = this.dateRangeCuesAppended = {}; - } - } - // Exit if the playlist does not have Date Ranges or does not have Program Date Time - const lastFragment = details.fragments[details.fragments.length - 1]; - if (ids.length === 0 || !isFiniteNumber(lastFragment == null ? void 0 : lastFragment.programDateTime)) { - return; - } - if (!this.id3Track) { - this.id3Track = this.createTrack(this.media); - } - const Cue = getCueClass(); - for (let i = 0; i < ids.length; i++) { - const id = ids[i]; - const dateRange = dateRanges[id]; - const startTime = dateRange.startTime; - - // Process DateRanges to determine end-time (known DURATION, END-DATE, or END-ON-NEXT) - const appendedDateRangeCues = dateRangeCuesAppended[id]; - const cues = (appendedDateRangeCues == null ? void 0 : appendedDateRangeCues.cues) || {}; - let durationKnown = (appendedDateRangeCues == null ? void 0 : appendedDateRangeCues.durationKnown) || false; - let endTime = MAX_CUE_ENDTIME; - const { - duration, - endDate - } = dateRange; - if (endDate && duration !== null) { - endTime = startTime + duration; - durationKnown = true; - } else if (dateRange.endOnNext && !durationKnown) { - const nextDateRangeWithSameClass = ids.reduce((candidateDateRange, id) => { - if (id !== dateRange.id) { - const otherDateRange = dateRanges[id]; - if (otherDateRange.class === dateRange.class && otherDateRange.startDate > dateRange.startDate && (!candidateDateRange || dateRange.startDate < candidateDateRange.startDate)) { - return otherDateRange; - } - } - return candidateDateRange; - }, null); - if (nextDateRangeWithSameClass) { - endTime = nextDateRangeWithSameClass.startTime; - durationKnown = true; - } - } - - // Create TextTrack Cues for each MetadataGroup Item (select DateRange attribute) - // This is to emulate Safari HLS playback handling of DateRange tags - const attributes = Object.keys(dateRange.attr); - for (let j = 0; j < attributes.length; j++) { - const key = attributes[j]; - if (!isDateRangeCueAttribute(key)) { - continue; - } - const cue = cues[key]; - if (cue) { - if (durationKnown && !appendedDateRangeCues.durationKnown) { - cue.endTime = endTime; - } else if (Math.abs(cue.startTime - startTime) > 0.01) { - cue.startTime = startTime; - cue.endTime = endTime; - } - } else if (Cue) { - let data = dateRange.attr[key]; - if (isSCTE35Attribute(key)) { - data = hexToArrayBuffer(data); - } - const _cue = createCueWithDataFields(Cue, startTime, endTime, { - key, - data - }, MetadataSchema.dateRange); - if (_cue) { - _cue.id = id; - this.id3Track.addCue(_cue); - cues[key] = _cue; - } - } - } - - // Keep track of processed DateRanges by ID for updating cues with new DateRange tag attributes - dateRangeCuesAppended[id] = { - cues, - dateRange, - durationKnown - }; - } - } -} - -class LatencyController { - constructor(hls) { - this.hls = void 0; - this.config = void 0; - this.media = null; - this.currentTime = 0; - this.stallCount = 0; - this._latency = null; - this._targetLatencyUpdated = false; - this.onTimeupdate = () => { - const { - media - } = this; - const levelDetails = this.hls.latestLevelDetails; - if (!media || !levelDetails) { - return; - } - this.currentTime = media.currentTime; - const latency = this.computeLatency(); - if (latency === null) { - return; - } - this._latency = latency; - - // Adapt playbackRate to meet target latency in low-latency mode - const { - lowLatencyMode, - maxLiveSyncPlaybackRate - } = this.config; - if (!lowLatencyMode || maxLiveSyncPlaybackRate === 1 || !levelDetails.live) { - return; - } - const targetLatency = this.targetLatency; - if (targetLatency === null) { - return; - } - const distanceFromTarget = latency - targetLatency; - // Only adjust playbackRate when within one target duration of targetLatency - // and more than one second from under-buffering. - // Playback further than one target duration from target can be considered DVR playback. - const liveMinLatencyDuration = Math.min(this.maxLatency, targetLatency + levelDetails.targetduration); - const inLiveRange = distanceFromTarget < liveMinLatencyDuration; - if (inLiveRange && distanceFromTarget > 0.05 && this.forwardBufferLength > 1) { - const max = Math.min(2, Math.max(1.0, maxLiveSyncPlaybackRate)); - const rate = Math.round(2 / (1 + Math.exp(-0.75 * distanceFromTarget - this.edgeStalled)) * 20) / 20; - media.playbackRate = Math.min(max, Math.max(1, rate)); - } else if (media.playbackRate !== 1 && media.playbackRate !== 0) { - media.playbackRate = 1; - } - }; - this.hls = hls; - this.config = hls.config; - this.registerListeners(); - } - get latency() { - return this._latency || 0; - } - get maxLatency() { - const { - config - } = this; - if (config.liveMaxLatencyDuration !== undefined) { - return config.liveMaxLatencyDuration; - } - const levelDetails = this.hls.latestLevelDetails; - return levelDetails ? config.liveMaxLatencyDurationCount * levelDetails.targetduration : 0; - } - get targetLatency() { - const levelDetails = this.hls.latestLevelDetails; - if (levelDetails === null) { - return null; - } - const { - holdBack, - partHoldBack, - targetduration - } = levelDetails; - const { - liveSyncDuration, - liveSyncDurationCount, - lowLatencyMode - } = this.config; - const userConfig = this.hls.userConfig; - let targetLatency = lowLatencyMode ? partHoldBack || holdBack : holdBack; - if (this._targetLatencyUpdated || userConfig.liveSyncDuration || userConfig.liveSyncDurationCount || targetLatency === 0) { - targetLatency = liveSyncDuration !== undefined ? liveSyncDuration : liveSyncDurationCount * targetduration; - } - const maxLiveSyncOnStallIncrease = targetduration; - return targetLatency + Math.min(this.stallCount * this.config.liveSyncOnStallIncrease, maxLiveSyncOnStallIncrease); - } - set targetLatency(latency) { - this.stallCount = 0; - this.config.liveSyncDuration = latency; - this._targetLatencyUpdated = true; - } - get liveSyncPosition() { - const liveEdge = this.estimateLiveEdge(); - const targetLatency = this.targetLatency; - if (liveEdge === null || targetLatency === null) { - return null; - } - const levelDetails = this.hls.latestLevelDetails; - if (levelDetails === null) { - return null; - } - const edge = levelDetails.edge; - const syncPosition = liveEdge - targetLatency - this.edgeStalled; - const min = edge - levelDetails.totalduration; - const max = edge - (this.config.lowLatencyMode && levelDetails.partTarget || levelDetails.targetduration); - return Math.min(Math.max(min, syncPosition), max); - } - get drift() { - const levelDetails = this.hls.latestLevelDetails; - if (levelDetails === null) { - return 1; - } - return levelDetails.drift; - } - get edgeStalled() { - const levelDetails = this.hls.latestLevelDetails; - if (levelDetails === null) { - return 0; - } - const maxLevelUpdateAge = (this.config.lowLatencyMode && levelDetails.partTarget || levelDetails.targetduration) * 3; - return Math.max(levelDetails.age - maxLevelUpdateAge, 0); - } - get forwardBufferLength() { - const { - media - } = this; - const levelDetails = this.hls.latestLevelDetails; - if (!media || !levelDetails) { - return 0; - } - const bufferedRanges = media.buffered.length; - return (bufferedRanges ? media.buffered.end(bufferedRanges - 1) : levelDetails.edge) - this.currentTime; - } - destroy() { - this.unregisterListeners(); - this.onMediaDetaching(); - // @ts-ignore - this.hls = null; - } - registerListeners() { - this.hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this); - this.hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this); - this.hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); - this.hls.on(Events.LEVEL_UPDATED, this.onLevelUpdated, this); - this.hls.on(Events.ERROR, this.onError, this); - } - unregisterListeners() { - this.hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this); - this.hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this); - this.hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); - this.hls.off(Events.LEVEL_UPDATED, this.onLevelUpdated, this); - this.hls.off(Events.ERROR, this.onError, this); - } - onMediaAttached(event, data) { - this.media = data.media; - this.media.addEventListener('timeupdate', this.onTimeupdate); - } - onMediaDetaching() { - if (this.media) { - this.media.removeEventListener('timeupdate', this.onTimeupdate); - this.media = null; - } - } - onManifestLoading() { - this._latency = null; - this.stallCount = 0; - } - onLevelUpdated(event, { - details - }) { - if (details.advanced) { - this.onTimeupdate(); - } - if (!details.live && this.media) { - this.media.removeEventListener('timeupdate', this.onTimeupdate); - } - } - onError(event, data) { - var _this$hls$latestLevel; - if (data.details !== ErrorDetails.BUFFER_STALLED_ERROR) { - return; - } - this.stallCount++; - if ((_this$hls$latestLevel = this.hls.latestLevelDetails) != null && _this$hls$latestLevel.live) { - this.hls.logger.warn('[latency-controller]: Stall detected, adjusting target latency'); - } - } - estimateLiveEdge() { - const levelDetails = this.hls.latestLevelDetails; - if (levelDetails === null) { - return null; - } - return levelDetails.edge + levelDetails.age; - } - computeLatency() { - const liveEdge = this.estimateLiveEdge(); - if (liveEdge === null) { - return null; - } - return liveEdge - this.currentTime; - } -} - -const HdcpLevels = ['NONE', 'TYPE-0', 'TYPE-1', null]; -function isHdcpLevel(value) { - return HdcpLevels.indexOf(value) > -1; -} -const VideoRangeValues = ['SDR', 'PQ', 'HLG']; -function isVideoRange(value) { - return !!value && VideoRangeValues.indexOf(value) > -1; -} -var HlsSkip = { - No: "", - Yes: "YES", - v2: "v2" -}; -function getSkipValue(details) { - const { - canSkipUntil, - canSkipDateRanges, - age - } = details; - // A Client SHOULD NOT request a Playlist Delta Update unless it already - // has a version of the Playlist that is no older than one-half of the Skip Boundary. - // @see: https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#section-6.3.7 - const playlistRecentEnough = age < canSkipUntil / 2; - if (canSkipUntil && playlistRecentEnough) { - if (canSkipDateRanges) { - return HlsSkip.v2; - } - return HlsSkip.Yes; - } - return HlsSkip.No; -} -class HlsUrlParameters { - constructor(msn, part, skip) { - this.msn = void 0; - this.part = void 0; - this.skip = void 0; - this.msn = msn; - this.part = part; - this.skip = skip; - } - addDirectives(uri) { - const url = new self.URL(uri); - if (this.msn !== undefined) { - url.searchParams.set('_HLS_msn', this.msn.toString()); - } - if (this.part !== undefined) { - url.searchParams.set('_HLS_part', this.part.toString()); - } - if (this.skip) { - url.searchParams.set('_HLS_skip', this.skip); - } - return url.href; - } -} -class Level { - constructor(data) { - this._attrs = void 0; - this.audioCodec = void 0; - this.bitrate = void 0; - this.codecSet = void 0; - this.url = void 0; - this.frameRate = void 0; - this.height = void 0; - this.id = void 0; - this.name = void 0; - this.videoCodec = void 0; - this.width = void 0; - this.details = void 0; - this.fragmentError = 0; - this.loadError = 0; - this.loaded = void 0; - this.realBitrate = 0; - this.supportedPromise = void 0; - this.supportedResult = void 0; - this._avgBitrate = 0; - this._audioGroups = void 0; - this._subtitleGroups = void 0; - // Deprecated (retained for backwards compatibility) - this._urlId = 0; - this.url = [data.url]; - this._attrs = [data.attrs]; - this.bitrate = data.bitrate; - if (data.details) { - this.details = data.details; - } - this.id = data.id || 0; - this.name = data.name; - this.width = data.width || 0; - this.height = data.height || 0; - this.frameRate = data.attrs.optionalFloat('FRAME-RATE', 0); - this._avgBitrate = data.attrs.decimalInteger('AVERAGE-BANDWIDTH'); - this.audioCodec = data.audioCodec; - this.videoCodec = data.videoCodec; - this.codecSet = [data.videoCodec, data.audioCodec].filter(c => !!c).map(s => s.substring(0, 4)).join(','); - this.addGroupId('audio', data.attrs.AUDIO); - this.addGroupId('text', data.attrs.SUBTITLES); - } - get maxBitrate() { - return Math.max(this.realBitrate, this.bitrate); - } - get averageBitrate() { - return this._avgBitrate || this.realBitrate || this.bitrate; - } - get attrs() { - return this._attrs[0]; - } - get codecs() { - return this.attrs.CODECS || ''; - } - get pathwayId() { - return this.attrs['PATHWAY-ID'] || '.'; - } - get videoRange() { - return this.attrs['VIDEO-RANGE'] || 'SDR'; - } - get score() { - return this.attrs.optionalFloat('SCORE', 0); - } - get uri() { - return this.url[0] || ''; - } - hasAudioGroup(groupId) { - return hasGroup(this._audioGroups, groupId); - } - hasSubtitleGroup(groupId) { - return hasGroup(this._subtitleGroups, groupId); - } - get audioGroups() { - return this._audioGroups; - } - get subtitleGroups() { - return this._subtitleGroups; - } - addGroupId(type, groupId) { - if (!groupId) { - return; - } - if (type === 'audio') { - let audioGroups = this._audioGroups; - if (!audioGroups) { - audioGroups = this._audioGroups = []; - } - if (audioGroups.indexOf(groupId) === -1) { - audioGroups.push(groupId); - } - } else if (type === 'text') { - let subtitleGroups = this._subtitleGroups; - if (!subtitleGroups) { - subtitleGroups = this._subtitleGroups = []; - } - if (subtitleGroups.indexOf(groupId) === -1) { - subtitleGroups.push(groupId); - } - } - } - - // Deprecated methods (retained for backwards compatibility) - get urlId() { - return 0; - } - set urlId(value) {} - get audioGroupIds() { - return this.audioGroups ? [this.audioGroupId] : undefined; - } - get textGroupIds() { - return this.subtitleGroups ? [this.textGroupId] : undefined; - } - get audioGroupId() { - var _this$audioGroups; - return (_this$audioGroups = this.audioGroups) == null ? void 0 : _this$audioGroups[0]; - } - get textGroupId() { - var _this$subtitleGroups; - return (_this$subtitleGroups = this.subtitleGroups) == null ? void 0 : _this$subtitleGroups[0]; - } - addFallback() {} -} -function hasGroup(groups, groupId) { - if (!groupId || !groups) { - return false; - } - return groups.indexOf(groupId) !== -1; -} - -function updateFromToPTS(fragFrom, fragTo) { - const fragToPTS = fragTo.startPTS; - // if we know startPTS[toIdx] - if (isFiniteNumber(fragToPTS)) { - // update fragment duration. - // it helps to fix drifts between playlist reported duration and fragment real duration - let duration = 0; - let frag; - if (fragTo.sn > fragFrom.sn) { - duration = fragToPTS - fragFrom.start; - frag = fragFrom; - } else { - duration = fragFrom.start - fragToPTS; - frag = fragTo; - } - if (frag.duration !== duration) { - frag.duration = duration; - } - // we dont know startPTS[toIdx] - } else if (fragTo.sn > fragFrom.sn) { - const contiguous = fragFrom.cc === fragTo.cc; - // TODO: With part-loading end/durations we need to confirm the whole fragment is loaded before using (or setting) minEndPTS - if (contiguous && fragFrom.minEndPTS) { - fragTo.start = fragFrom.start + (fragFrom.minEndPTS - fragFrom.start); - } else { - fragTo.start = fragFrom.start + fragFrom.duration; - } - } else { - fragTo.start = Math.max(fragFrom.start - fragTo.duration, 0); - } -} -function updateFragPTSDTS(details, frag, startPTS, endPTS, startDTS, endDTS) { - const parsedMediaDuration = endPTS - startPTS; - if (parsedMediaDuration <= 0) { - logger.warn('Fragment should have a positive duration', frag); - endPTS = startPTS + frag.duration; - endDTS = startDTS + frag.duration; - } - let maxStartPTS = startPTS; - let minEndPTS = endPTS; - const fragStartPts = frag.startPTS; - const fragEndPts = frag.endPTS; - if (isFiniteNumber(fragStartPts)) { - // delta PTS between audio and video - const deltaPTS = Math.abs(fragStartPts - startPTS); - if (!isFiniteNumber(frag.deltaPTS)) { - frag.deltaPTS = deltaPTS; - } else { - frag.deltaPTS = Math.max(deltaPTS, frag.deltaPTS); - } - maxStartPTS = Math.max(startPTS, fragStartPts); - startPTS = Math.min(startPTS, fragStartPts); - startDTS = Math.min(startDTS, frag.startDTS); - minEndPTS = Math.min(endPTS, fragEndPts); - endPTS = Math.max(endPTS, fragEndPts); - endDTS = Math.max(endDTS, frag.endDTS); - } - const drift = startPTS - frag.start; - if (frag.start !== 0) { - frag.start = startPTS; - } - frag.duration = endPTS - frag.start; - frag.startPTS = startPTS; - frag.maxStartPTS = maxStartPTS; - frag.startDTS = startDTS; - frag.endPTS = endPTS; - frag.minEndPTS = minEndPTS; - frag.endDTS = endDTS; - const sn = frag.sn; - // exit if sn out of range - if (!details || sn < details.startSN || sn > details.endSN) { - return 0; - } - let i; - const fragIdx = sn - details.startSN; - const fragments = details.fragments; - // update frag reference in fragments array - // rationale is that fragments array might not contain this frag object. - // this will happen if playlist has been refreshed between frag loading and call to updateFragPTSDTS() - // if we don't update frag, we won't be able to propagate PTS info on the playlist - // resulting in invalid sliding computation - fragments[fragIdx] = frag; - // adjust fragment PTS/duration from seqnum-1 to frag 0 - for (i = fragIdx; i > 0; i--) { - updateFromToPTS(fragments[i], fragments[i - 1]); - } - - // adjust fragment PTS/duration from seqnum to last frag - for (i = fragIdx; i < fragments.length - 1; i++) { - updateFromToPTS(fragments[i], fragments[i + 1]); - } - if (details.fragmentHint) { - updateFromToPTS(fragments[fragments.length - 1], details.fragmentHint); - } - details.PTSKnown = details.alignedSliding = true; - return drift; -} -function mergeDetails(oldDetails, newDetails) { - // Track the last initSegment processed. Initialize it to the last one on the timeline. - let currentInitSegment = null; - const oldFragments = oldDetails.fragments; - for (let i = oldFragments.length - 1; i >= 0; i--) { - const oldInit = oldFragments[i].initSegment; - if (oldInit) { - currentInitSegment = oldInit; - break; - } - } - if (oldDetails.fragmentHint) { - // prevent PTS and duration from being adjusted on the next hint - delete oldDetails.fragmentHint.endPTS; - } - // check if old/new playlists have fragments in common - // loop through overlapping SN and update startPTS , cc, and duration if any found - let ccOffset = 0; - let PTSFrag; - mapFragmentIntersection(oldDetails, newDetails, (oldFrag, newFrag) => { - if (oldFrag.relurl) { - // Do not compare CC if the old fragment has no url. This is a level.fragmentHint used by LL-HLS parts. - // It maybe be off by 1 if it was created before any parts or discontinuity tags were appended to the end - // of the playlist. - ccOffset = oldFrag.cc - newFrag.cc; - } - if (isFiniteNumber(oldFrag.startPTS) && isFiniteNumber(oldFrag.endPTS)) { - newFrag.start = newFrag.startPTS = oldFrag.startPTS; - newFrag.startDTS = oldFrag.startDTS; - newFrag.maxStartPTS = oldFrag.maxStartPTS; - newFrag.endPTS = oldFrag.endPTS; - newFrag.endDTS = oldFrag.endDTS; - newFrag.minEndPTS = oldFrag.minEndPTS; - newFrag.duration = oldFrag.endPTS - oldFrag.startPTS; - if (newFrag.duration) { - PTSFrag = newFrag; - } - - // PTS is known when any segment has startPTS and endPTS - newDetails.PTSKnown = newDetails.alignedSliding = true; - } - newFrag.elementaryStreams = oldFrag.elementaryStreams; - newFrag.loader = oldFrag.loader; - newFrag.stats = oldFrag.stats; - if (oldFrag.initSegment) { - newFrag.initSegment = oldFrag.initSegment; - currentInitSegment = oldFrag.initSegment; - } - }); - const fragmentsToCheck = newDetails.fragmentHint ? newDetails.fragments.concat(newDetails.fragmentHint) : newDetails.fragments; - if (currentInitSegment) { - fragmentsToCheck.forEach(frag => { - var _currentInitSegment; - if (frag && (!frag.initSegment || frag.initSegment.relurl === ((_currentInitSegment = currentInitSegment) == null ? void 0 : _currentInitSegment.relurl))) { - frag.initSegment = currentInitSegment; - } - }); - } - if (newDetails.skippedSegments) { - newDetails.deltaUpdateFailed = newDetails.fragments.some(frag => !frag); - if (newDetails.deltaUpdateFailed) { - logger.warn('[level-helper] Previous playlist missing segments skipped in delta playlist'); - for (let i = newDetails.skippedSegments; i--;) { - newDetails.fragments.shift(); - } - newDetails.startSN = newDetails.fragments[0].sn; - newDetails.startCC = newDetails.fragments[0].cc; - } else { - if (newDetails.canSkipDateRanges) { - newDetails.dateRanges = mergeDateRanges(oldDetails.dateRanges, newDetails); - } - const programDateTimes = oldDetails.fragments.filter(frag => frag.rawProgramDateTime); - if (oldDetails.hasProgramDateTime && !newDetails.hasProgramDateTime) { - for (let i = 1; i < fragmentsToCheck.length; i++) { - if (fragmentsToCheck[i].programDateTime === null) { - assignProgramDateTime(fragmentsToCheck[i], fragmentsToCheck[i - 1], programDateTimes); - } - } - } - mapDateRanges(programDateTimes, newDetails); - } - } - const newFragments = newDetails.fragments; - if (ccOffset) { - logger.warn('discontinuity sliding from playlist, take drift into account'); - for (let i = 0; i < newFragments.length; i++) { - newFragments[i].cc += ccOffset; - } - } - if (newDetails.skippedSegments) { - newDetails.startCC = newDetails.fragments[0].cc; - } - - // Merge parts - mapPartIntersection(oldDetails.partList, newDetails.partList, (oldPart, newPart) => { - newPart.elementaryStreams = oldPart.elementaryStreams; - newPart.stats = oldPart.stats; - }); - - // if at least one fragment contains PTS info, recompute PTS information for all fragments - if (PTSFrag) { - updateFragPTSDTS(newDetails, PTSFrag, PTSFrag.startPTS, PTSFrag.endPTS, PTSFrag.startDTS, PTSFrag.endDTS); - } else { - // ensure that delta is within oldFragments range - // also adjust sliding in case delta is 0 (we could have old=[50-60] and new=old=[50-61]) - // in that case we also need to adjust start offset of all fragments - adjustSliding(oldDetails, newDetails); - } - if (newFragments.length) { - newDetails.totalduration = newDetails.edge - newFragments[0].start; - } - newDetails.driftStartTime = oldDetails.driftStartTime; - newDetails.driftStart = oldDetails.driftStart; - const advancedDateTime = newDetails.advancedDateTime; - if (newDetails.advanced && advancedDateTime) { - const edge = newDetails.edge; - if (!newDetails.driftStart) { - newDetails.driftStartTime = advancedDateTime; - newDetails.driftStart = edge; - } - newDetails.driftEndTime = advancedDateTime; - newDetails.driftEnd = edge; - } else { - newDetails.driftEndTime = oldDetails.driftEndTime; - newDetails.driftEnd = oldDetails.driftEnd; - newDetails.advancedDateTime = oldDetails.advancedDateTime; - } -} -function mergeDateRanges(oldDateRanges, newDetails) { - const { - dateRanges: deltaDateRanges, - recentlyRemovedDateranges - } = newDetails; - const dateRanges = _extends({}, oldDateRanges); - if (recentlyRemovedDateranges) { - recentlyRemovedDateranges.forEach(id => { - delete dateRanges[id]; - }); - } - const mergeIds = Object.keys(dateRanges); - const mergeCount = mergeIds.length; - if (mergeCount) { - Object.keys(deltaDateRanges).forEach(id => { - const mergedDateRange = dateRanges[id]; - const dateRange = new DateRange(deltaDateRanges[id].attr, mergedDateRange); - if (dateRange.isValid) { - dateRanges[id] = dateRange; - if (!mergedDateRange) { - dateRange.tagOrder += mergeCount; - } - } else { - logger.warn(`Ignoring invalid Playlist Delta Update DATERANGE tag: "${JSON.stringify(deltaDateRanges[id].attr)}"`); - } - }); - } - return dateRanges; -} -function mapPartIntersection(oldParts, newParts, intersectionFn) { - if (oldParts && newParts) { - let delta = 0; - for (let i = 0, len = oldParts.length; i <= len; i++) { - const oldPart = oldParts[i]; - const newPart = newParts[i + delta]; - if (oldPart && newPart && oldPart.index === newPart.index && oldPart.fragment.sn === newPart.fragment.sn) { - intersectionFn(oldPart, newPart); - } else { - delta--; - } - } - } -} -function mapFragmentIntersection(oldDetails, newDetails, intersectionFn) { - const skippedSegments = newDetails.skippedSegments; - const start = Math.max(oldDetails.startSN, newDetails.startSN) - newDetails.startSN; - const end = (oldDetails.fragmentHint ? 1 : 0) + (skippedSegments ? newDetails.endSN : Math.min(oldDetails.endSN, newDetails.endSN)) - newDetails.startSN; - const delta = newDetails.startSN - oldDetails.startSN; - const newFrags = newDetails.fragmentHint ? newDetails.fragments.concat(newDetails.fragmentHint) : newDetails.fragments; - const oldFrags = oldDetails.fragmentHint ? oldDetails.fragments.concat(oldDetails.fragmentHint) : oldDetails.fragments; - for (let i = start; i <= end; i++) { - const oldFrag = oldFrags[delta + i]; - let newFrag = newFrags[i]; - if (skippedSegments && !newFrag && oldFrag) { - // Fill in skipped segments in delta playlist - newFrag = newDetails.fragments[i] = oldFrag; - } - if (oldFrag && newFrag) { - intersectionFn(oldFrag, newFrag); - } - } -} -function adjustSliding(oldDetails, newDetails, matchingStableVariantOrRendition = true) { - const delta = newDetails.startSN + newDetails.skippedSegments - oldDetails.startSN; - const oldFragments = oldDetails.fragments; - const advancedOrStable = delta >= 0; - let sliding = 0; - if (advancedOrStable && delta < oldFragments.length) { - sliding = oldFragments[delta].start; - } else if (advancedOrStable && matchingStableVariantOrRendition) { - // align new start with old end (updated playlist start sequence is past end sequence of last update) - sliding = oldDetails.edge; - } else if (!newDetails.skippedSegments && newDetails.fragments[0].start === 0) { - // align new start with old (playlist switch has a sequence with no overlap and should not be used for alignment) - sliding = oldDetails.fragments[0].start; - } else { - // new details already has a sliding offset or has skipped segments - return; - } - addSliding(newDetails, sliding); -} -function addSliding(details, start) { - if (start) { - const fragments = details.fragments; - for (let i = details.skippedSegments; i < fragments.length; i++) { - fragments[i].start += start; - } - if (details.fragmentHint) { - details.fragmentHint.start += start; - } - } -} -function computeReloadInterval(newDetails, distanceToLiveEdgeMs = Infinity) { - let reloadInterval = 1000 * newDetails.targetduration; - 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 { - // 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; - } - return Math.round(reloadInterval); -} -function getFragmentWithSN(details, sn, fragCurrent) { - if (!details) { - return null; - } - let fragment = details.fragments[sn - details.startSN]; - if (fragment) { - return fragment; - } - fragment = details.fragmentHint; - if (fragment && fragment.sn === sn) { - return fragment; - } - if (sn < details.startSN && fragCurrent && fragCurrent.sn === sn) { - return fragCurrent; - } - return null; -} -function getPartWith(details, sn, partIndex) { - if (!details) { - return null; - } - return findPart(details.partList, sn, partIndex); -} -function findPart(partList, sn, partIndex) { - if (partList) { - for (let i = partList.length; i--;) { - const part = partList[i]; - if (part.index === partIndex && part.fragment.sn === sn) { - return part; - } - } - } - return null; -} -function reassignFragmentLevelIndexes(levels) { - levels.forEach((level, index) => { - const { - details - } = level; - if (details != null && details.fragments) { - details.fragments.forEach(fragment => { - fragment.level = index; - }); - } - }); -} - -function isTimeoutError(error) { - switch (error.details) { - case ErrorDetails.FRAG_LOAD_TIMEOUT: - case ErrorDetails.KEY_LOAD_TIMEOUT: - case ErrorDetails.LEVEL_LOAD_TIMEOUT: - case ErrorDetails.MANIFEST_LOAD_TIMEOUT: - return true; - } - return false; -} -function getRetryConfig(loadPolicy, error) { - const isTimeout = isTimeoutError(error); - return loadPolicy.default[`${isTimeout ? 'timeout' : 'error'}Retry`]; -} -function getRetryDelay(retryConfig, retryCount) { - // exponential backoff capped to max retry delay - const backoffFactor = retryConfig.backoff === 'linear' ? 1 : Math.pow(2, retryCount); - return Math.min(backoffFactor * retryConfig.retryDelayMs, retryConfig.maxRetryDelayMs); -} -function getLoaderConfigWithoutReties(loderConfig) { - return _objectSpread2(_objectSpread2({}, loderConfig), { - errorRetry: null, - timeoutRetry: null - }); -} -function shouldRetry(retryConfig, retryCount, isTimeout, loaderResponse) { - if (!retryConfig) { - return false; - } - const httpStatus = loaderResponse == null ? void 0 : loaderResponse.code; - const retry = retryCount < retryConfig.maxNumRetry && (retryForHttpStatus(httpStatus) || !!isTimeout); - return retryConfig.shouldRetry ? retryConfig.shouldRetry(retryConfig, retryCount, isTimeout, loaderResponse, retry) : retry; -} -function retryForHttpStatus(httpStatus) { - // Do not retry on status 4xx, status 0 (CORS error), or undefined (decrypt/gap/parse error) - return httpStatus === 0 && navigator.onLine === false || !!httpStatus && (httpStatus < 400 || httpStatus > 499); -} - -const BinarySearch = { - /** - * Searches for an item in an array which matches a certain condition. - * This requires the condition to only match one item in the array, - * and for the array to be ordered. - * - * @param list The array to search. - * @param comparisonFn - * Called and provided a candidate item as the first argument. - * Should return: - * > -1 if the item should be located at a lower index than the provided item. - * > 1 if the item should be located at a higher index than the provided item. - * > 0 if the item is the item you're looking for. - * - * @returns the object if found, otherwise returns null - */ - search: function (list, comparisonFn) { - let minIndex = 0; - let maxIndex = list.length - 1; - let currentIndex = null; - let currentElement = null; - while (minIndex <= maxIndex) { - currentIndex = (minIndex + maxIndex) / 2 | 0; - currentElement = list[currentIndex]; - const comparisonResult = comparisonFn(currentElement); - if (comparisonResult > 0) { - minIndex = currentIndex + 1; - } else if (comparisonResult < 0) { - maxIndex = currentIndex - 1; - } else { - return currentElement; - } - } - return null; - } -}; - -/** - * Returns first fragment whose endPdt value exceeds the given PDT, or null. - * @param fragments - The array of candidate fragments - * @param PDTValue - The PDT value which must be exceeded - * @param maxFragLookUpTolerance - The amount of time that a fragment's start/end can be within in order to be considered contiguous - */ -function findFragmentByPDT(fragments, PDTValue, maxFragLookUpTolerance) { - if (PDTValue === null || !Array.isArray(fragments) || !fragments.length || !isFiniteNumber(PDTValue)) { - return null; - } - - // if less than start - const startPDT = fragments[0].programDateTime; - if (PDTValue < (startPDT || 0)) { - return null; - } - const endPDT = fragments[fragments.length - 1].endProgramDateTime; - if (PDTValue >= (endPDT || 0)) { - return null; - } - maxFragLookUpTolerance = maxFragLookUpTolerance || 0; - for (let seg = 0; seg < fragments.length; ++seg) { - const frag = fragments[seg]; - if (pdtWithinToleranceTest(PDTValue, maxFragLookUpTolerance, frag)) { - return frag; - } - } - return null; -} - -/** - * Finds a fragment based on the SN of the previous fragment; or based on the needs of the current buffer. - * This method compensates for small buffer gaps by applying a tolerance to the start of any candidate fragment, thus - * breaking any traps which would cause the same fragment to be continuously selected within a small range. - * @param fragPrevious - The last frag successfully appended - * @param fragments - The array of candidate fragments - * @param bufferEnd - The end of the contiguous buffered range the playhead is currently within - * @param maxFragLookUpTolerance - The amount of time that a fragment's start/end can be within in order to be considered contiguous - * @returns a matching fragment or null - */ -function findFragmentByPTS(fragPrevious, fragments, bufferEnd = 0, maxFragLookUpTolerance = 0, nextFragLookupTolerance = 0.005) { - let fragNext = null; - if (fragPrevious) { - fragNext = fragments[1 + fragPrevious.sn - fragments[0].sn] || null; - // check for buffer-end rounding error - const bufferEdgeError = fragPrevious.endDTS - bufferEnd; - if (bufferEdgeError > 0 && bufferEdgeError < 0.0000015) { - bufferEnd += 0.0000015; - } - if (fragNext && fragPrevious.level !== fragNext.level && fragNext.end <= fragPrevious.end) { - fragNext = fragments[2 + fragPrevious.sn - fragments[0].sn] || null; - } - } else if (bufferEnd === 0 && fragments[0].start === 0) { - fragNext = fragments[0]; - } - // Prefer the next fragment if it's within tolerance - if (fragNext && ((!fragPrevious || fragPrevious.level === fragNext.level) && fragmentWithinToleranceTest(bufferEnd, maxFragLookUpTolerance, fragNext) === 0 || fragmentWithinFastStartSwitch(fragNext, fragPrevious, Math.min(nextFragLookupTolerance, maxFragLookUpTolerance)))) { - return fragNext; - } - // We might be seeking past the tolerance so find the best match - const foundFragment = BinarySearch.search(fragments, fragmentWithinToleranceTest.bind(null, bufferEnd, maxFragLookUpTolerance)); - if (foundFragment && (foundFragment !== fragPrevious || !fragNext)) { - return foundFragment; - } - // If no match was found return the next fragment after fragPrevious, or null - return fragNext; -} -function fragmentWithinFastStartSwitch(fragNext, fragPrevious, nextFragLookupTolerance) { - if (fragPrevious && fragPrevious.start === 0 && fragPrevious.level < fragNext.level && (fragPrevious.endPTS || 0) > 0) { - const firstDuration = fragPrevious.tagList.reduce((duration, tag) => { - if (tag[0] === 'INF') { - duration += parseFloat(tag[1]); - } - return duration; - }, nextFragLookupTolerance); - return fragNext.start <= firstDuration; - } - return false; -} - -/** - * The test function used by the findFragmentBySn's BinarySearch to look for the best match to the current buffer conditions. - * @param candidate - The fragment to test - * @param bufferEnd - The end of the current buffered range the playhead is currently within - * @param maxFragLookUpTolerance - The amount of time that a fragment's start can be within in order to be considered contiguous - * @returns 0 if it matches, 1 if too low, -1 if too high - */ -function fragmentWithinToleranceTest(bufferEnd = 0, maxFragLookUpTolerance = 0, candidate) { - // 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 - // frag[Ø] : [0,10] - // frag[1] : [10,20] - // bufferEnd is within frag[0] range ... although what we are expecting is to return frag[1] here - // frag start frag start+duration - // |-----------------------------| - // <---> <---> - // ...--------><-----------------------------><---------.... - // previous frag matching fragment next frag - // return -1 return 0 return 1 - // logger.log(`level/sn/start/end/bufEnd:${level}/${candidate.sn}/${candidate.start}/${(candidate.start+candidate.duration)}/${bufferEnd}`); - // Set the lookup tolerance to be small enough to detect the current segment - ensures we don't skip over very small segments - const candidateLookupTolerance = Math.min(maxFragLookUpTolerance, candidate.duration + (candidate.deltaPTS ? candidate.deltaPTS : 0)); - if (candidate.start + candidate.duration - candidateLookupTolerance <= bufferEnd) { - return 1; - } else if (candidate.start - candidateLookupTolerance > bufferEnd && candidate.start) { - // if maxFragLookUpTolerance will have negative value then don't return -1 for first element - return -1; - } - return 0; -} - -/** - * The test function used by the findFragmentByPdt's BinarySearch to look for the best match to the current buffer conditions. - * This function tests the candidate's program date time values, as represented in Unix time - * @param candidate - The fragment to test - * @param pdtBufferEnd - The Unix time representing the end of the current buffered range - * @param maxFragLookUpTolerance - The amount of time that a fragment's start can be within in order to be considered contiguous - * @returns true if contiguous, false otherwise - */ -function pdtWithinToleranceTest(pdtBufferEnd, maxFragLookUpTolerance, candidate) { - const candidateLookupTolerance = Math.min(maxFragLookUpTolerance, candidate.duration + (candidate.deltaPTS ? candidate.deltaPTS : 0)) * 1000; - - // endProgramDateTime can be null, default to zero - const endProgramDateTime = candidate.endProgramDateTime || 0; - return endProgramDateTime - candidateLookupTolerance > pdtBufferEnd; -} -function findFragWithCC(fragments, cc) { - return BinarySearch.search(fragments, candidate => { - if (candidate.cc < cc) { - return 1; - } else if (candidate.cc > cc) { - return -1; - } else { - return 0; - } - }); -} -function findNearestWithCC(details, cc, fragment) { - if (details) { - if (details.startCC <= cc && details.endCC >= cc) { - const start = fragment.start; - const end = fragment.end; - let fragments = details.fragments; - if (!fragment.relurl) { - const { - fragmentHint - } = details; - if (fragmentHint) { - fragments = fragments.concat(fragmentHint); - } - } - return BinarySearch.search(fragments, candidate => { - if (candidate.cc < cc || candidate.end <= start) { - return 1; - } else if (candidate.cc > cc || candidate.start >= end) { - return -1; - } else { - return 0; - } - }); - } - } - return null; -} - -var NetworkErrorAction = { - DoNothing: 0, - SendEndCallback: 1, - SendAlternateToPenaltyBox: 2, - RemoveAlternatePermanently: 3, - InsertDiscontinuity: 4, - RetryRequest: 5 -}; -var ErrorActionFlags = { - None: 0, - MoveAllAlternatesMatchingHost: 1, - MoveAllAlternatesMatchingHDCP: 2, - SwitchToSDR: 4 -}; // Reserved for future use -class ErrorController extends Logger { - constructor(hls) { - super('error-controller', hls.logger); - this.hls = void 0; - this.playlistError = 0; - this.penalizedRenditions = {}; - this.hls = hls; - this.registerListeners(); - } - registerListeners() { - const hls = this.hls; - hls.on(Events.ERROR, this.onError, this); - hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); - hls.on(Events.LEVEL_UPDATED, this.onLevelUpdated, this); - } - unregisterListeners() { - const hls = this.hls; - if (!hls) { - return; - } - hls.off(Events.ERROR, this.onError, this); - hls.off(Events.ERROR, this.onErrorOut, this); - hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); - hls.off(Events.LEVEL_UPDATED, this.onLevelUpdated, this); - } - destroy() { - this.unregisterListeners(); - // @ts-ignore - this.hls = null; - this.penalizedRenditions = {}; - } - startLoad(startPosition) {} - stopLoad() { - this.playlistError = 0; - } - getVariantLevelIndex(frag) { - return (frag == null ? void 0 : frag.type) === PlaylistLevelType.MAIN ? frag.level : this.hls.loadLevel; - } - onManifestLoading() { - this.playlistError = 0; - this.penalizedRenditions = {}; - } - onLevelUpdated() { - this.playlistError = 0; - } - onError(event, data) { - var _data$frag; - if (data.fatal) { - return; - } - const hls = this.hls; - const context = data.context; - switch (data.details) { - case ErrorDetails.FRAG_LOAD_ERROR: - case ErrorDetails.FRAG_LOAD_TIMEOUT: - case ErrorDetails.KEY_LOAD_ERROR: - case ErrorDetails.KEY_LOAD_TIMEOUT: - data.errorAction = this.getFragRetryOrSwitchAction(data); - return; - case ErrorDetails.FRAG_PARSING_ERROR: - // ignore empty segment errors marked as gap - if ((_data$frag = data.frag) != null && _data$frag.gap) { - data.errorAction = createDoNothingErrorAction(); - return; - } - // falls through - case ErrorDetails.FRAG_GAP: - case ErrorDetails.FRAG_DECRYPT_ERROR: - { - // Switch level if possible, otherwise allow retry count to reach max error retries - data.errorAction = this.getFragRetryOrSwitchAction(data); - data.errorAction.action = NetworkErrorAction.SendAlternateToPenaltyBox; - return; - } - case ErrorDetails.LEVEL_EMPTY_ERROR: - case ErrorDetails.LEVEL_PARSING_ERROR: - { - var _data$context, _data$context$levelDe; - // Only retry when empty and live - const levelIndex = data.parent === PlaylistLevelType.MAIN ? data.level : hls.loadLevel; - if (data.details === ErrorDetails.LEVEL_EMPTY_ERROR && !!((_data$context = data.context) != null && (_data$context$levelDe = _data$context.levelDetails) != null && _data$context$levelDe.live)) { - data.errorAction = this.getPlaylistRetryOrSwitchAction(data, levelIndex); - } else { - // Escalate to fatal if not retrying or switching - data.levelRetry = false; - data.errorAction = this.getLevelSwitchAction(data, levelIndex); - } - } - return; - case ErrorDetails.LEVEL_LOAD_ERROR: - case ErrorDetails.LEVEL_LOAD_TIMEOUT: - if (typeof (context == null ? void 0 : context.level) === 'number') { - data.errorAction = this.getPlaylistRetryOrSwitchAction(data, context.level); - } - return; - case ErrorDetails.AUDIO_TRACK_LOAD_ERROR: - case ErrorDetails.AUDIO_TRACK_LOAD_TIMEOUT: - case ErrorDetails.SUBTITLE_LOAD_ERROR: - case ErrorDetails.SUBTITLE_TRACK_LOAD_TIMEOUT: - if (context) { - const level = hls.levels[hls.loadLevel]; - if (level && (context.type === PlaylistContextType.AUDIO_TRACK && level.hasAudioGroup(context.groupId) || context.type === PlaylistContextType.SUBTITLE_TRACK && level.hasSubtitleGroup(context.groupId))) { - // Perform Pathway switch or Redundant failover if possible for fastest recovery - // otherwise allow playlist retry count to reach max error retries - data.errorAction = this.getPlaylistRetryOrSwitchAction(data, hls.loadLevel); - data.errorAction.action = NetworkErrorAction.SendAlternateToPenaltyBox; - data.errorAction.flags = ErrorActionFlags.MoveAllAlternatesMatchingHost; - return; - } - } - return; - case ErrorDetails.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED: - { - const level = hls.levels[hls.loadLevel]; - const restrictedHdcpLevel = level == null ? void 0 : level.attrs['HDCP-LEVEL']; - if (restrictedHdcpLevel) { - data.errorAction = { - action: NetworkErrorAction.SendAlternateToPenaltyBox, - flags: ErrorActionFlags.MoveAllAlternatesMatchingHDCP, - hdcpLevel: restrictedHdcpLevel - }; - } else { - this.keySystemError(data); - } - } - return; - case ErrorDetails.BUFFER_ADD_CODEC_ERROR: - case ErrorDetails.REMUX_ALLOC_ERROR: - case ErrorDetails.BUFFER_APPEND_ERROR: - // Buffer-controller can set errorAction when append errors can be ignored or resolved locally - if (!data.errorAction) { - var _data$level; - data.errorAction = this.getLevelSwitchAction(data, (_data$level = data.level) != null ? _data$level : hls.loadLevel); - } - return; - case ErrorDetails.INTERNAL_EXCEPTION: - case ErrorDetails.BUFFER_APPENDING_ERROR: - case ErrorDetails.BUFFER_FULL_ERROR: - case ErrorDetails.LEVEL_SWITCH_ERROR: - case ErrorDetails.BUFFER_STALLED_ERROR: - case ErrorDetails.BUFFER_SEEK_OVER_HOLE: - case ErrorDetails.BUFFER_NUDGE_ON_STALL: - data.errorAction = createDoNothingErrorAction(); - return; - } - if (data.type === ErrorTypes.KEY_SYSTEM_ERROR) { - this.keySystemError(data); - } - } - keySystemError(data) { - const levelIndex = this.getVariantLevelIndex(data.frag); - // Do not retry level. Escalate to fatal if switching levels fails. - data.levelRetry = false; - data.errorAction = this.getLevelSwitchAction(data, levelIndex); - } - getPlaylistRetryOrSwitchAction(data, levelIndex) { - const hls = this.hls; - const retryConfig = getRetryConfig(hls.config.playlistLoadPolicy, data); - const retryCount = this.playlistError++; - const retry = shouldRetry(retryConfig, retryCount, isTimeoutError(data), data.response); - if (retry) { - return { - action: NetworkErrorAction.RetryRequest, - flags: ErrorActionFlags.None, - retryConfig, - retryCount - }; - } - const errorAction = this.getLevelSwitchAction(data, levelIndex); - if (retryConfig) { - errorAction.retryConfig = retryConfig; - errorAction.retryCount = retryCount; - } - return errorAction; - } - getFragRetryOrSwitchAction(data) { - const hls = this.hls; - // Share fragment error count accross media options (main, audio, subs) - // This allows for level based rendition switching when media option assets fail - const variantLevelIndex = this.getVariantLevelIndex(data.frag); - const level = hls.levels[variantLevelIndex]; - const { - fragLoadPolicy, - keyLoadPolicy - } = hls.config; - const retryConfig = getRetryConfig(data.details.startsWith('key') ? keyLoadPolicy : fragLoadPolicy, data); - const fragmentErrors = hls.levels.reduce((acc, level) => acc + level.fragmentError, 0); - // Switch levels when out of retried or level index out of bounds - if (level) { - if (data.details !== ErrorDetails.FRAG_GAP) { - level.fragmentError++; - } - const retry = shouldRetry(retryConfig, fragmentErrors, isTimeoutError(data), data.response); - if (retry) { - return { - action: NetworkErrorAction.RetryRequest, - flags: ErrorActionFlags.None, - retryConfig, - retryCount: fragmentErrors - }; - } - } - // Reach max retry count, or Missing level reference - // Switch to valid index - const errorAction = this.getLevelSwitchAction(data, variantLevelIndex); - // Add retry details to allow skipping of FRAG_PARSING_ERROR - if (retryConfig) { - errorAction.retryConfig = retryConfig; - errorAction.retryCount = fragmentErrors; - } - return errorAction; - } - getLevelSwitchAction(data, levelIndex) { - const hls = this.hls; - if (levelIndex === null || levelIndex === undefined) { - levelIndex = hls.loadLevel; - } - const level = this.hls.levels[levelIndex]; - if (level) { - var _data$frag2, _data$context2; - const errorDetails = data.details; - level.loadError++; - if (errorDetails === ErrorDetails.BUFFER_APPEND_ERROR) { - level.fragmentError++; - } - // Search for next level to retry - let nextLevel = -1; - const { - levels, - loadLevel, - minAutoLevel, - maxAutoLevel - } = hls; - if (!hls.autoLevelEnabled) { - hls.loadLevel = -1; - } - const fragErrorType = (_data$frag2 = data.frag) == null ? void 0 : _data$frag2.type; - // Find alternate audio codec if available on audio codec error - const isAudioCodecError = fragErrorType === PlaylistLevelType.AUDIO && errorDetails === ErrorDetails.FRAG_PARSING_ERROR || data.sourceBufferName === 'audio' && (errorDetails === ErrorDetails.BUFFER_ADD_CODEC_ERROR || errorDetails === ErrorDetails.BUFFER_APPEND_ERROR); - const findAudioCodecAlternate = isAudioCodecError && levels.some(({ - audioCodec - }) => level.audioCodec !== audioCodec); - // Find alternate video codec if available on video codec error - const isVideoCodecError = data.sourceBufferName === 'video' && (errorDetails === ErrorDetails.BUFFER_ADD_CODEC_ERROR || errorDetails === ErrorDetails.BUFFER_APPEND_ERROR); - const findVideoCodecAlternate = isVideoCodecError && levels.some(({ - codecSet, - audioCodec - }) => level.codecSet !== codecSet && level.audioCodec === audioCodec); - const { - type: playlistErrorType, - groupId: playlistErrorGroupId - } = (_data$context2 = data.context) != null ? _data$context2 : {}; - for (let i = levels.length; i--;) { - const candidate = (i + loadLevel) % levels.length; - if (candidate !== loadLevel && candidate >= minAutoLevel && candidate <= maxAutoLevel && levels[candidate].loadError === 0) { - var _level$audioGroups, _level$subtitleGroups; - const levelCandidate = levels[candidate]; - // Skip level switch if GAP tag is found in next level at same position - if (errorDetails === ErrorDetails.FRAG_GAP && fragErrorType === PlaylistLevelType.MAIN && data.frag) { - const levelDetails = levels[candidate].details; - if (levelDetails) { - const fragCandidate = findFragmentByPTS(data.frag, levelDetails.fragments, data.frag.start); - if (fragCandidate != null && fragCandidate.gap) { - continue; - } - } - } else if (playlistErrorType === PlaylistContextType.AUDIO_TRACK && levelCandidate.hasAudioGroup(playlistErrorGroupId) || playlistErrorType === PlaylistContextType.SUBTITLE_TRACK && levelCandidate.hasSubtitleGroup(playlistErrorGroupId)) { - // For audio/subs playlist errors find another group ID or fallthrough to redundant fail-over - continue; - } else if (fragErrorType === PlaylistLevelType.AUDIO && (_level$audioGroups = level.audioGroups) != null && _level$audioGroups.some(groupId => levelCandidate.hasAudioGroup(groupId)) || fragErrorType === PlaylistLevelType.SUBTITLE && (_level$subtitleGroups = level.subtitleGroups) != null && _level$subtitleGroups.some(groupId => levelCandidate.hasSubtitleGroup(groupId)) || findAudioCodecAlternate && level.audioCodec === levelCandidate.audioCodec || !findAudioCodecAlternate && level.audioCodec !== levelCandidate.audioCodec || findVideoCodecAlternate && level.codecSet === levelCandidate.codecSet) { - // For video/audio/subs frag errors find another group ID or fallthrough to redundant fail-over - continue; - } - nextLevel = candidate; - break; - } - } - if (nextLevel > -1 && hls.loadLevel !== nextLevel) { - data.levelRetry = true; - this.playlistError = 0; - return { - action: NetworkErrorAction.SendAlternateToPenaltyBox, - flags: ErrorActionFlags.None, - nextAutoLevel: nextLevel - }; - } - } - // No levels to switch / Manual level selection / Level not found - // Resolve with Pathway switch, Redundant fail-over, or stay on lowest Level - return { - action: NetworkErrorAction.SendAlternateToPenaltyBox, - flags: ErrorActionFlags.MoveAllAlternatesMatchingHost - }; - } - onErrorOut(event, data) { - var _data$errorAction; - switch ((_data$errorAction = data.errorAction) == null ? void 0 : _data$errorAction.action) { - case NetworkErrorAction.DoNothing: - break; - case NetworkErrorAction.SendAlternateToPenaltyBox: - this.sendAlternateToPenaltyBox(data); - if (!data.errorAction.resolved && data.details !== ErrorDetails.FRAG_GAP) { - data.fatal = true; - } else if (/MediaSource readyState: ended/.test(data.error.message)) { - this.warn(`MediaSource ended after "${data.sourceBufferName}" sourceBuffer append error. Attempting to recover from media error.`); - this.hls.recoverMediaError(); - } - break; - case NetworkErrorAction.RetryRequest: - // handled by stream and playlist/level controllers - break; - } - if (data.fatal) { - this.hls.stopLoad(); - return; - } - } - sendAlternateToPenaltyBox(data) { - const hls = this.hls; - const errorAction = data.errorAction; - if (!errorAction) { - return; - } - const { - flags, - hdcpLevel, - nextAutoLevel - } = errorAction; - switch (flags) { - case ErrorActionFlags.None: - this.switchLevel(data, nextAutoLevel); - break; - case ErrorActionFlags.MoveAllAlternatesMatchingHDCP: - if (hdcpLevel) { - hls.maxHdcpLevel = HdcpLevels[HdcpLevels.indexOf(hdcpLevel) - 1]; - errorAction.resolved = true; - } - this.warn(`Restricting playback to HDCP-LEVEL of "${hls.maxHdcpLevel}" or lower`); - break; - } - // If not resolved by previous actions try to switch to next level - if (!errorAction.resolved) { - this.switchLevel(data, nextAutoLevel); - } - } - switchLevel(data, levelIndex) { - if (levelIndex !== undefined && data.errorAction) { - this.warn(`switching to level ${levelIndex} after ${data.details}`); - this.hls.nextAutoLevel = levelIndex; - data.errorAction.resolved = true; - // Stream controller is responsible for this but won't switch on false start - this.hls.nextLoadLevel = this.hls.nextAutoLevel; - } - } -} -function createDoNothingErrorAction(resolved) { - const errorAction = { - action: NetworkErrorAction.DoNothing, - flags: ErrorActionFlags.None - }; - if (resolved) { - errorAction.resolved = true; - } - return errorAction; -} - -class BasePlaylistController extends Logger { - constructor(hls, logPrefix) { - super(logPrefix, hls.logger); - this.hls = void 0; - this.timer = -1; - this.requestScheduled = -1; - this.canLoad = false; - this.hls = hls; - } - destroy() { - this.clearTimer(); - // @ts-ignore - this.hls = this.log = this.warn = null; - } - clearTimer() { - if (this.timer !== -1) { - self.clearTimeout(this.timer); - this.timer = -1; - } - } - startLoad() { - this.canLoad = true; - this.requestScheduled = -1; - this.loadPlaylist(); - } - stopLoad() { - this.canLoad = false; - this.clearTimer(); - } - switchParams(playlistUri, previous, current) { - const renditionReports = previous == null ? void 0 : previous.renditionReports; - if (renditionReports) { - let foundIndex = -1; - for (let i = 0; i < renditionReports.length; i++) { - const attr = renditionReports[i]; - let uri; - try { - uri = new self.URL(attr.URI, previous.url).href; - } catch (error) { - this.warn(`Could not construct new URL for Rendition Report: ${error}`); - uri = attr.URI || ''; - } - // Use exact match. Otherwise, the last partial match, if any, will be used - // (Playlist URI includes a query string that the Rendition Report does not) - if (uri === playlistUri) { - foundIndex = i; - break; - } else if (uri === playlistUri.substring(0, uri.length)) { - foundIndex = i; - } - } - if (foundIndex !== -1) { - const attr = renditionReports[foundIndex]; - const msn = parseInt(attr['LAST-MSN']) || (previous == null ? void 0 : previous.lastPartSn); - let part = parseInt(attr['LAST-PART']) || (previous == null ? void 0 : previous.lastPartIndex); - if (this.hls.config.lowLatencyMode) { - const currentGoal = Math.min(previous.age - previous.partTarget, previous.targetduration); - if (part >= 0 && currentGoal > previous.partTarget) { - part += 1; - } - } - const skip = current && getSkipValue(current); - return new HlsUrlParameters(msn, part >= 0 ? part : undefined, skip); - } - } - } - loadPlaylist(hlsUrlParameters) { - if (this.requestScheduled === -1) { - this.requestScheduled = self.performance.now(); - } - // Loading is handled by the subclasses - } - shouldLoadPlaylist(playlist) { - return this.canLoad && !!playlist && !!playlist.url && (!playlist.details || playlist.details.live); - } - shouldReloadPlaylist(playlist) { - return this.timer === -1 && this.requestScheduled === -1 && this.shouldLoadPlaylist(playlist); - } - playlistLoaded(index, data, previousDetails) { - const { - details, - stats - } = data; - - // Set last updated date-time - const now = self.performance.now(); - const elapsed = stats.loading.first ? Math.max(0, now - stats.loading.first) : 0; - details.advancedDateTime = Date.now() - elapsed; - - // shift fragment starts with timelineOffset - const timelineOffset = this.hls.config.timelineOffset; - if (timelineOffset !== details.appliedTimelineOffset) { - const offset = Math.max(timelineOffset || 0, 0); - details.appliedTimelineOffset = offset; - details.fragments.forEach(frag => { - frag.start = frag.playlistOffset + offset; - }); - } - - // if current playlist is a live playlist, arm a timer to reload it - if (details.live || previousDetails != null && previousDetails.live) { - details.reloaded(previousDetails); - if (previousDetails) { - this.log(`live playlist ${index} ${details.advanced ? 'REFRESHED ' + details.lastPartSn + '-' + details.lastPartIndex : details.updated ? 'UPDATED' : 'MISSED'}`); - } - // Merge live playlists to adjust fragment starts and fill in delta playlist skipped segments - if (previousDetails && details.fragments.length > 0) { - mergeDetails(previousDetails, details); - } - if (!this.canLoad || !details.live) { - return; - } - let deliveryDirectives; - let msn = undefined; - let part = undefined; - if (details.canBlockReload && details.endSN && details.advanced) { - // Load level with LL-HLS delivery directives - const lowLatencyMode = this.hls.config.lowLatencyMode; - const lastPartSn = details.lastPartSn; - const endSn = details.endSN; - const lastPartIndex = details.lastPartIndex; - const hasParts = lastPartIndex !== -1; - const lastPart = lastPartSn === endSn; - // When low latency mode is disabled, we'll skip part requests once the last part index is found - const nextSnStartIndex = lowLatencyMode ? 0 : lastPartIndex; - if (hasParts) { - msn = lastPart ? endSn + 1 : lastPartSn; - part = lastPart ? nextSnStartIndex : lastPartIndex + 1; - } else { - msn = endSn + 1; - } - // Low-Latency CDN Tune-in: "age" header and time since load indicates we're behind by more than one part - // Update directives to obtain the Playlist that has the estimated additional duration of media - const lastAdvanced = details.age; - const cdnAge = lastAdvanced + details.ageHeader; - let currentGoal = Math.min(cdnAge - details.partTarget, details.targetduration * 1.5); - if (currentGoal > 0) { - if (cdnAge > details.targetduration * 3) { - // Omit segment and part directives when the last response was more than 3 target durations ago, - this.log(`Playlist last advanced ${lastAdvanced.toFixed(2)}s ago. Omitting segment and part directives.`); - msn = undefined; - part = undefined; - } else if (previousDetails != null && previousDetails.tuneInGoal && cdnAge - details.partTarget > previousDetails.tuneInGoal) { - // If we attempted to get the next or latest playlist update, but currentGoal increased, - // then we either can't catchup, or the "age" header cannot be trusted. - this.warn(`CDN Tune-in goal increased from: ${previousDetails.tuneInGoal} to: ${currentGoal} with playlist age: ${details.age}`); - currentGoal = 0; - } else { - const segments = Math.floor(currentGoal / details.targetduration); - msn += segments; - if (part !== undefined) { - const parts = Math.round(currentGoal % details.targetduration / details.partTarget); - part += parts; - } - this.log(`CDN Tune-in age: ${details.ageHeader}s last advanced ${lastAdvanced.toFixed(2)}s goal: ${currentGoal} skip sn ${segments} to part ${part}`); - } - details.tuneInGoal = currentGoal; - } - deliveryDirectives = this.getDeliveryDirectives(details, data.deliveryDirectives, msn, part); - if (lowLatencyMode || !lastPart) { - this.loadPlaylist(deliveryDirectives); - return; - } - } else if (details.canBlockReload || details.canSkipUntil) { - deliveryDirectives = this.getDeliveryDirectives(details, data.deliveryDirectives, msn, part); - } - 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 && 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 if (this.requestScheduled === -1 || this.requestScheduled + reloadInterval < now) { - this.requestScheduled = now; - } else if (this.requestScheduled - now <= 0) { - 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), estimatedTimeUntilUpdate); - } else { - this.clearTimer(); - } - } - getDeliveryDirectives(details, previousDeliveryDirectives, msn, part) { - let skip = getSkipValue(details); - if (previousDeliveryDirectives != null && previousDeliveryDirectives.skip && details.deltaUpdateFailed) { - msn = previousDeliveryDirectives.msn; - part = previousDeliveryDirectives.part; - skip = HlsSkip.No; - } - return new HlsUrlParameters(msn, part, skip); - } - checkRetry(errorEvent) { - const errorDetails = errorEvent.details; - const isTimeout = isTimeoutError(errorEvent); - const errorAction = errorEvent.errorAction; - const { - action, - retryCount = 0, - retryConfig - } = errorAction || {}; - const retry = !!errorAction && !!retryConfig && (action === NetworkErrorAction.RetryRequest || !errorAction.resolved && action === NetworkErrorAction.SendAlternateToPenaltyBox); - if (retry) { - var _errorEvent$context; - this.requestScheduled = -1; - if (retryCount >= retryConfig.maxNumRetry) { - return false; - } - if (isTimeout && (_errorEvent$context = errorEvent.context) != null && _errorEvent$context.deliveryDirectives) { - // The LL-HLS request already timed out so retry immediately - this.warn(`Retrying playlist loading ${retryCount + 1}/${retryConfig.maxNumRetry} after "${errorDetails}" without delivery-directives`); - this.loadPlaylist(); - } else { - const delay = getRetryDelay(retryConfig, retryCount); - // Schedule level/track reload - this.timer = self.setTimeout(() => this.loadPlaylist(), delay); - this.warn(`Retrying playlist loading ${retryCount + 1}/${retryConfig.maxNumRetry} after "${errorDetails}" in ${delay}ms`); - } - // `levelRetry = true` used to inform other controllers that a retry is happening - errorEvent.levelRetry = true; - errorAction.resolved = true; - } - return retry; - } -} - -class LevelController extends BasePlaylistController { - constructor(hls, contentSteeringController) { - super(hls, 'level-controller'); - this._levels = []; - this._firstLevel = -1; - this._maxAutoLevel = -1; - this._startLevel = void 0; - this.currentLevel = null; - this.currentLevelIndex = -1; - this.manualLevelIndex = -1; - this.steering = void 0; - this.onParsedComplete = void 0; - this.steering = contentSteeringController; - this._registerListeners(); - } - _registerListeners() { - const { - hls - } = this; - hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); - hls.on(Events.MANIFEST_LOADED, this.onManifestLoaded, this); - hls.on(Events.LEVEL_LOADED, this.onLevelLoaded, this); - hls.on(Events.LEVELS_UPDATED, this.onLevelsUpdated, this); - hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this); - hls.on(Events.ERROR, this.onError, this); - } - _unregisterListeners() { - const { - hls - } = this; - hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); - hls.off(Events.MANIFEST_LOADED, this.onManifestLoaded, this); - hls.off(Events.LEVEL_LOADED, this.onLevelLoaded, this); - hls.off(Events.LEVELS_UPDATED, this.onLevelsUpdated, this); - hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this); - hls.off(Events.ERROR, this.onError, this); - } - destroy() { - this._unregisterListeners(); - this.steering = null; - this.resetLevels(); - super.destroy(); - } - stopLoad() { - const levels = this._levels; - - // clean up live level details to force reload them, and reset load errors - levels.forEach(level => { - level.loadError = 0; - level.fragmentError = 0; - }); - super.stopLoad(); - } - resetLevels() { - this._startLevel = undefined; - this.manualLevelIndex = -1; - this.currentLevelIndex = -1; - this.currentLevel = null; - this._levels = []; - this._maxAutoLevel = -1; - } - onManifestLoading(event, data) { - this.resetLevels(); - } - onManifestLoaded(event, data) { - const preferManagedMediaSource = this.hls.config.preferManagedMediaSource; - const levels = []; - const redundantSet = {}; - const generatePathwaySet = {}; - let resolutionFound = false; - let videoCodecFound = false; - let audioCodecFound = false; - data.levels.forEach(levelParsed => { - var _videoCodec; - const attributes = levelParsed.attrs; - let { - audioCodec, - videoCodec - } = levelParsed; - if (audioCodec) { - // Returns empty and set to undefined for 'mp4a.40.34' with fallback to 'audio/mpeg' SourceBuffer - levelParsed.audioCodec = audioCodec = getCodecCompatibleName(audioCodec, preferManagedMediaSource) || undefined; - } - if (((_videoCodec = videoCodec) == null ? void 0 : _videoCodec.indexOf('avc1')) === 0) { - videoCodec = levelParsed.videoCodec = convertAVC1ToAVCOTI(videoCodec); - } - - // only keep levels with supported audio/video codecs - const { - width, - height, - unknownCodecs - } = levelParsed; - let unknownUnsupportedCodecCount = unknownCodecs ? unknownCodecs.length : 0; - if (unknownCodecs) { - // Treat unknown codec as audio or video codec based on passing `isTypeSupported` check - // (allows for playback of any supported codec even if not indexed in utils/codecs) - for (let i = unknownUnsupportedCodecCount; i--;) { - const unknownCodec = unknownCodecs[i]; - if (this.isAudioSupported(unknownCodec)) { - levelParsed.audioCodec = audioCodec = audioCodec ? `${audioCodec},${unknownCodec}` : unknownCodec; - unknownUnsupportedCodecCount--; - sampleEntryCodesISO.audio[audioCodec.substring(0, 4)] = 2; - } else if (this.isVideoSupported(unknownCodec)) { - levelParsed.videoCodec = videoCodec = videoCodec ? `${videoCodec},${unknownCodec}` : unknownCodec; - unknownUnsupportedCodecCount--; - sampleEntryCodesISO.video[videoCodec.substring(0, 4)] = 2; - } - } - } - resolutionFound || (resolutionFound = !!(width && height)); - videoCodecFound || (videoCodecFound = !!videoCodec); - audioCodecFound || (audioCodecFound = !!audioCodec); - if (unknownUnsupportedCodecCount || audioCodec && !this.isAudioSupported(audioCodec) || videoCodec && !this.isVideoSupported(videoCodec)) { - return; - } - const { - CODECS, - 'FRAME-RATE': FRAMERATE, - 'HDCP-LEVEL': HDCP, - 'PATHWAY-ID': PATHWAY, - RESOLUTION, - 'VIDEO-RANGE': VIDEO_RANGE - } = attributes; - const contentSteeringPrefix = `${PATHWAY || '.'}-`; - const levelKey = `${contentSteeringPrefix}${levelParsed.bitrate}-${RESOLUTION}-${FRAMERATE}-${CODECS}-${VIDEO_RANGE}-${HDCP}`; - if (!redundantSet[levelKey]) { - const level = new Level(levelParsed); - redundantSet[levelKey] = level; - generatePathwaySet[levelKey] = 1; - levels.push(level); - } else if (redundantSet[levelKey].uri !== levelParsed.url && !levelParsed.attrs['PATHWAY-ID']) { - // Assign Pathway IDs to Redundant Streams (default Pathways is ".". Redundant Streams "..", "...", and so on.) - // Content Steering controller to handles Pathway fallback on error - const pathwayCount = generatePathwaySet[levelKey] += 1; - levelParsed.attrs['PATHWAY-ID'] = new Array(pathwayCount + 1).join('.'); - const level = new Level(levelParsed); - redundantSet[levelKey] = level; - levels.push(level); - } else { - redundantSet[levelKey].addGroupId('audio', attributes.AUDIO); - redundantSet[levelKey].addGroupId('text', attributes.SUBTITLES); - } - }); - this.filterAndSortMediaOptions(levels, data, resolutionFound, videoCodecFound, audioCodecFound); - } - isAudioSupported(codec) { - return areCodecsMediaSourceSupported(codec, 'audio', this.hls.config.preferManagedMediaSource); - } - isVideoSupported(codec) { - return areCodecsMediaSourceSupported(codec, 'video', this.hls.config.preferManagedMediaSource); - } - filterAndSortMediaOptions(filteredLevels, data, resolutionFound, videoCodecFound, audioCodecFound) { - let audioTracks = []; - let subtitleTracks = []; - let levels = filteredLevels; - - // remove audio-only and invalid video-range levels if we also have levels with video codecs or RESOLUTION signalled - if ((resolutionFound || videoCodecFound) && audioCodecFound) { - levels = levels.filter(({ - videoCodec, - videoRange, - width, - height - }) => (!!videoCodec || !!(width && height)) && isVideoRange(videoRange)); - } - if (levels.length === 0) { - // Dispatch error after MANIFEST_LOADED is done propagating - Promise.resolve().then(() => { - if (this.hls) { - if (data.levels.length) { - this.warn(`One or more CODECS in variant not supported: ${JSON.stringify(data.levels[0].attrs)}`); - } - const error = new Error('no level with compatible codecs found in manifest'); - this.hls.trigger(Events.ERROR, { - type: ErrorTypes.MEDIA_ERROR, - details: ErrorDetails.MANIFEST_INCOMPATIBLE_CODECS_ERROR, - fatal: true, - url: data.url, - error, - reason: error.message - }); - } - }); - return; - } - if (data.audioTracks) { - audioTracks = data.audioTracks.filter(track => !track.audioCodec || this.isAudioSupported(track.audioCodec)); - // Assign ids after filtering as array indices by group-id - assignTrackIdsByGroup(audioTracks); - } - if (data.subtitles) { - subtitleTracks = data.subtitles; - assignTrackIdsByGroup(subtitleTracks); - } - // start bitrate is the first bitrate of the manifest - const unsortedLevels = levels.slice(0); - // 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; - } - // sort on height before bitrate for cap-level-controller - if (resolutionFound && a.height !== b.height) { - return a.height - b.height; - } - if (a.frameRate !== b.frameRate) { - return a.frameRate - b.frameRate; - } - if (a.videoRange !== b.videoRange) { - return VideoRangeValues.indexOf(a.videoRange) - VideoRangeValues.indexOf(b.videoRange); - } - if (a.videoCodec !== b.videoCodec) { - const valueA = videoCodecPreferenceValue(a.videoCodec); - const valueB = videoCodecPreferenceValue(b.videoCodec); - if (valueA !== valueB) { - return valueB - valueA; - } - } - if (a.uri === b.uri && a.codecSet !== b.codecSet) { - const valueA = codecsSetSelectionPreferenceValue(a.codecSet); - const valueB = codecsSetSelectionPreferenceValue(b.codecSet); - if (valueA !== valueB) { - return valueB - valueA; - } - } - if (a.averageBitrate !== b.averageBitrate) { - return a.averageBitrate - b.averageBitrate; - } - return 0; - }); - let firstLevelInPlaylist = unsortedLevels[0]; - if (this.steering) { - levels = this.steering.filterParsedLevels(levels); - if (levels.length !== unsortedLevels.length) { - for (let i = 0; i < unsortedLevels.length; i++) { - if (unsortedLevels[i].pathwayId === levels[0].pathwayId) { - firstLevelInPlaylist = unsortedLevels[i]; - break; - } - } - } - } - this._levels = levels; - - // find index of first level in sorted levels - for (let i = 0; i < levels.length; i++) { - if (levels[i] === firstLevelInPlaylist) { - var _this$hls$userConfig; - this._firstLevel = i; - const firstLevelBitrate = firstLevelInPlaylist.bitrate; - const bandwidthEstimate = this.hls.bandwidthEstimate; - this.log(`manifest loaded, ${levels.length} level(s) found, first bitrate: ${firstLevelBitrate}`); - // Update default bwe to first variant bitrate as long it has not been configured or set - if (((_this$hls$userConfig = this.hls.userConfig) == null ? void 0 : _this$hls$userConfig.abrEwmaDefaultEstimate) === undefined) { - const startingBwEstimate = Math.min(firstLevelBitrate, this.hls.config.abrEwmaDefaultEstimateMax); - if (startingBwEstimate > bandwidthEstimate && bandwidthEstimate === this.hls.abrEwmaDefaultEstimate) { - this.hls.bandwidthEstimate = startingBwEstimate; - } - } - break; - } - } - - // Audio is only alternate if manifest include a URI along with the audio group tag, - // and this is not an audio-only stream where levels contain audio-only - const audioOnly = audioCodecFound && !videoCodecFound; - const edata = { - levels, - audioTracks, - subtitleTracks, - sessionData: data.sessionData, - sessionKeys: data.sessionKeys, - firstLevel: this._firstLevel, - stats: data.stats, - audio: audioCodecFound, - video: videoCodecFound, - altAudio: !audioOnly && audioTracks.some(t => !!t.url) - }; - this.hls.trigger(Events.MANIFEST_PARSED, edata); - - // Initiate loading after all controllers have received MANIFEST_PARSED - const { - config: { - autoStartLoad, - startPosition - }, - forceStartLoad - } = this.hls; - if (autoStartLoad || forceStartLoad) { - this.log(`${autoStartLoad ? 'auto' : 'force'} startLoad with configured startPosition ${startPosition}`); - this.hls.startLoad(startPosition); - } - } - get levels() { - if (this._levels.length === 0) { - return null; - } - return this._levels; - } - get level() { - return this.currentLevelIndex; - } - set level(newLevel) { - const levels = this._levels; - if (levels.length === 0) { - return; - } - // check if level idx is valid - if (newLevel < 0 || newLevel >= levels.length) { - // invalid level id given, trigger error - const error = new Error('invalid level idx'); - const fatal = newLevel < 0; - this.hls.trigger(Events.ERROR, { - type: ErrorTypes.OTHER_ERROR, - details: ErrorDetails.LEVEL_SWITCH_ERROR, - level: newLevel, - fatal, - error, - reason: error.message - }); - if (fatal) { - return; - } - newLevel = Math.min(newLevel, levels.length - 1); - } - const lastLevelIndex = this.currentLevelIndex; - const lastLevel = this.currentLevel; - const lastPathwayId = lastLevel ? lastLevel.attrs['PATHWAY-ID'] : undefined; - const level = levels[newLevel]; - const pathwayId = level.attrs['PATHWAY-ID']; - this.currentLevelIndex = newLevel; - this.currentLevel = level; - if (lastLevelIndex === newLevel && level.details && lastLevel && lastPathwayId === pathwayId) { - return; - } - this.log(`Switching to level ${newLevel} (${level.height ? level.height + 'p ' : ''}${level.videoRange ? level.videoRange + ' ' : ''}${level.codecSet ? level.codecSet + ' ' : ''}@${level.bitrate})${pathwayId ? ' with Pathway ' + pathwayId : ''} from level ${lastLevelIndex}${lastPathwayId ? ' with Pathway ' + lastPathwayId : ''}`); - const levelSwitchingData = { - level: newLevel, - attrs: level.attrs, - details: level.details, - bitrate: level.bitrate, - averageBitrate: level.averageBitrate, - maxBitrate: level.maxBitrate, - realBitrate: level.realBitrate, - width: level.width, - height: level.height, - codecSet: level.codecSet, - audioCodec: level.audioCodec, - videoCodec: level.videoCodec, - audioGroups: level.audioGroups, - subtitleGroups: level.subtitleGroups, - loaded: level.loaded, - loadError: level.loadError, - fragmentError: level.fragmentError, - name: level.name, - id: level.id, - uri: level.uri, - url: level.url, - urlId: 0, - audioGroupIds: level.audioGroupIds, - textGroupIds: level.textGroupIds - }; - this.hls.trigger(Events.LEVEL_SWITCHING, levelSwitchingData); - // check if we need to load playlist for this level - const levelDetails = level.details; - if (!levelDetails || levelDetails.live) { - // level not retrieved yet, or live playlist we need to (re)load it - const hlsUrlParameters = this.switchParams(level.uri, lastLevel == null ? void 0 : lastLevel.details, levelDetails); - this.loadPlaylist(hlsUrlParameters); - } - } - get manualLevel() { - return this.manualLevelIndex; - } - set manualLevel(newLevel) { - this.manualLevelIndex = newLevel; - if (this._startLevel === undefined) { - this._startLevel = newLevel; - } - if (newLevel !== -1) { - this.level = newLevel; - } - } - get firstLevel() { - return this._firstLevel; - } - set firstLevel(newLevel) { - this._firstLevel = newLevel; - } - get startLevel() { - // Setting hls.startLevel (this._startLevel) overrides config.startLevel - if (this._startLevel === undefined) { - const configStartLevel = this.hls.config.startLevel; - if (configStartLevel !== undefined) { - return configStartLevel; - } - return this.hls.firstAutoLevel; - } - return this._startLevel; - } - set startLevel(newLevel) { - this._startLevel = newLevel; - } - get pathwayPriority() { - if (this.steering) { - return this.steering.pathwayPriority; - } - return null; - } - set pathwayPriority(pathwayPriority) { - if (this.steering) { - const pathwaysList = this.steering.pathways(); - const filteredPathwayPriority = pathwayPriority.filter(pathwayId => { - return pathwaysList.indexOf(pathwayId) !== -1; - }); - if (pathwayPriority.length < 1) { - this.warn(`pathwayPriority ${pathwayPriority} should contain at least one pathway from list: ${pathwaysList}`); - return; - } - this.steering.pathwayPriority = filteredPathwayPriority; - } - } - onError(event, data) { - if (data.fatal || !data.context) { - return; - } - if (data.context.type === PlaylistContextType.LEVEL && data.context.level === this.level) { - this.checkRetry(data); - } - } - - // reset errors on the successful load of a fragment - onFragBuffered(event, { - frag - }) { - if (frag !== undefined && frag.type === PlaylistLevelType.MAIN) { - const el = frag.elementaryStreams; - if (!Object.keys(el).some(type => !!el[type])) { - return; - } - const level = this._levels[frag.level]; - if (level != null && level.loadError) { - this.log(`Resetting level error count of ${level.loadError} on frag buffered`); - level.loadError = 0; - } - } - } - onLevelLoaded(event, data) { - var _data$deliveryDirecti2; - const { - level, - details - } = data; - const curLevel = this._levels[level]; - if (!curLevel) { - var _data$deliveryDirecti; - this.warn(`Invalid level index ${level}`); - if ((_data$deliveryDirecti = data.deliveryDirectives) != null && _data$deliveryDirecti.skip) { - details.deltaUpdateFailed = true; - } - return; - } - - // only process level loaded events matching with expected level - if (level === this.currentLevelIndex) { - // reset level load error counter on successful level loaded only if there is no issues with fragments - if (curLevel.fragmentError === 0) { - curLevel.loadError = 0; - } - // Ignore matching details populated by loading a Media Playlist directly - let previousDetails = curLevel.details; - if (previousDetails === data.details && previousDetails.advanced) { - previousDetails = undefined; - } - this.playlistLoaded(level, data, previousDetails); - } else if ((_data$deliveryDirecti2 = data.deliveryDirectives) != null && _data$deliveryDirecti2.skip) { - // received a delta playlist update that cannot be merged - details.deltaUpdateFailed = true; - } - } - loadPlaylist(hlsUrlParameters) { - super.loadPlaylist(); - const currentLevelIndex = this.currentLevelIndex; - const currentLevel = this.currentLevel; - if (currentLevel && this.shouldLoadPlaylist(currentLevel)) { - let url = currentLevel.uri; - if (hlsUrlParameters) { - try { - url = hlsUrlParameters.addDirectives(url); - } catch (error) { - this.warn(`Could not construct new URL with HLS Delivery Directives: ${error}`); - } - } - const pathwayId = currentLevel.attrs['PATHWAY-ID']; - this.log(`Loading level index ${currentLevelIndex}${(hlsUrlParameters == null ? void 0 : hlsUrlParameters.msn) !== undefined ? ' at sn ' + hlsUrlParameters.msn + ' part ' + hlsUrlParameters.part : ''} with${pathwayId ? ' Pathway ' + pathwayId : ''} ${url}`); - - // console.log('Current audio track group ID:', this.hls.audioTracks[this.hls.audioTrack].groupId); - // console.log('New video quality level audio group id:', levelObject.attrs.AUDIO, level); - this.clearTimer(); - this.hls.trigger(Events.LEVEL_LOADING, { - url, - level: currentLevelIndex, - pathwayId: currentLevel.attrs['PATHWAY-ID'], - id: 0, - // Deprecated Level urlId - deliveryDirectives: hlsUrlParameters || null - }); - } - } - get nextLoadLevel() { - if (this.manualLevelIndex !== -1) { - return this.manualLevelIndex; - } else { - return this.hls.nextAutoLevel; - } - } - set nextLoadLevel(nextLevel) { - this.level = nextLevel; - if (this.manualLevelIndex === -1) { - this.hls.nextAutoLevel = nextLevel; - } - } - removeLevel(levelIndex) { - var _this$currentLevel; - const levels = this._levels.filter((level, index) => { - if (index !== levelIndex) { - return true; - } - if (this.steering) { - this.steering.removeLevel(level); - } - if (level === this.currentLevel) { - this.currentLevel = null; - this.currentLevelIndex = -1; - if (level.details) { - level.details.fragments.forEach(f => f.level = -1); - } - } - return false; - }); - reassignFragmentLevelIndexes(levels); - this._levels = levels; - if (this.currentLevelIndex > -1 && (_this$currentLevel = this.currentLevel) != null && _this$currentLevel.details) { - this.currentLevelIndex = this.currentLevel.details.fragments[0].level; - } - this.hls.trigger(Events.LEVELS_UPDATED, { - levels - }); - } - onLevelsUpdated(event, { - levels - }) { - this._levels = levels; - } - checkMaxAutoUpdated() { - const { - autoLevelCapping, - maxAutoLevel, - maxHdcpLevel - } = this.hls; - if (this._maxAutoLevel !== maxAutoLevel) { - this._maxAutoLevel = maxAutoLevel; - this.hls.trigger(Events.MAX_AUTO_LEVEL_UPDATED, { - autoLevelCapping, - levels: this.levels, - maxAutoLevel, - minAutoLevel: this.hls.minAutoLevel, - maxHdcpLevel - }); - } - } -} -function assignTrackIdsByGroup(tracks) { - const groups = {}; - tracks.forEach(track => { - const groupId = track.groupId || ''; - track.id = groups[groupId] = groups[groupId] || 0; - groups[groupId]++; - }); -} - -var FragmentState = { - NOT_LOADED: "NOT_LOADED", - APPENDING: "APPENDING", - PARTIAL: "PARTIAL", - OK: "OK" -}; -class FragmentTracker { - constructor(hls) { - this.activePartLists = Object.create(null); - this.endListFragments = Object.create(null); - this.fragments = Object.create(null); - this.timeRanges = Object.create(null); - this.bufferPadding = 0.2; - this.hls = void 0; - this.hasGaps = false; - this.hls = hls; - this._registerListeners(); - } - _registerListeners() { - const { - hls - } = this; - hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); - hls.on(Events.BUFFER_APPENDED, this.onBufferAppended, this); - hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this); - hls.on(Events.FRAG_LOADED, this.onFragLoaded, this); - } - _unregisterListeners() { - const { - hls - } = this; - hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); - hls.off(Events.BUFFER_APPENDED, this.onBufferAppended, this); - hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this); - hls.off(Events.FRAG_LOADED, this.onFragLoaded, this); - } - destroy() { - this._unregisterListeners(); - // @ts-ignore - this.fragments = - // @ts-ignore - this.activePartLists = - // @ts-ignore - this.endListFragments = this.timeRanges = null; - } - - /** - * Return a Fragment or Part with an appended range that matches the position and levelType - * Otherwise, return null - */ - getAppendedFrag(position, levelType) { - const activeParts = this.activePartLists[levelType]; - if (activeParts) { - for (let i = activeParts.length; i--;) { - const activePart = activeParts[i]; - if (!activePart) { - break; - } - const appendedPTS = activePart.end; - if (activePart.start <= position && appendedPTS !== null && position <= appendedPTS) { - return activePart; - } - } - } - return this.getBufferedFrag(position, levelType); - } - - /** - * Return a buffered Fragment that matches the position and levelType. - * A buffered Fragment is one whose loading, parsing and appending is done (completed or "partial" meaning aborted). - * If not found any Fragment, return null - */ - getBufferedFrag(position, levelType) { - return this.getFragAtPos(position, levelType, true); - } - getFragAtPos(position, levelType, buffered) { - const { - fragments - } = this; - const keys = Object.keys(fragments); - for (let i = keys.length; i--;) { - const fragmentEntity = fragments[keys[i]]; - if ((fragmentEntity == null ? void 0 : fragmentEntity.body.type) === levelType && (!buffered || fragmentEntity.buffered)) { - const frag = fragmentEntity.body; - if (frag.start <= position && position <= frag.end) { - return frag; - } - } - } - return null; - } - - /** - * Partial fragments effected by coded frame eviction will be removed - * The browser will unload parts of the buffer to free up memory for new buffer data - * Fragments will need to be reloaded when the buffer is freed up, removing partial fragments will allow them to reload(since there might be parts that are still playable) - */ - detectEvictedFragments(elementaryStream, timeRange, playlistType, appendedPart, removeAppending) { - if (this.timeRanges) { - this.timeRanges[elementaryStream] = timeRange; - } - // Check if any flagged fragments have been unloaded - // excluding anything newer than appendedPartSn - const appendedPartSn = (appendedPart == null ? void 0 : appendedPart.fragment.sn) || -1; - Object.keys(this.fragments).forEach(key => { - const fragmentEntity = this.fragments[key]; - if (!fragmentEntity) { - return; - } - if (appendedPartSn >= fragmentEntity.body.sn) { - return; - } - if (!fragmentEntity.buffered && (!fragmentEntity.loaded || removeAppending)) { - if (fragmentEntity.body.type === playlistType) { - this.removeFragment(fragmentEntity.body); - } - return; - } - const esData = fragmentEntity.range[elementaryStream]; - if (!esData) { - return; - } - if (esData.time.length === 0) { - this.removeFragment(fragmentEntity.body); - return; - } - esData.time.some(time => { - const isNotBuffered = !this.isTimeBuffered(time.startPTS, time.endPTS, timeRange); - if (isNotBuffered) { - // Unregister partial fragment as it needs to load again to be reused - this.removeFragment(fragmentEntity.body); - } - return isNotBuffered; - }); - }); - } - - /** - * Checks if the fragment passed in is loaded in the buffer properly - * Partially loaded fragments will be registered as a partial fragment - */ - detectPartialFragments(data) { - const timeRanges = this.timeRanges; - if (!timeRanges || data.frag.sn === 'initSegment') { - return; - } - const frag = data.frag; - const fragKey = getFragmentKey(frag); - const fragmentEntity = this.fragments[fragKey]; - if (!fragmentEntity || fragmentEntity.buffered && frag.gap) { - return; - } - const isFragHint = !frag.relurl; - Object.keys(timeRanges).forEach(elementaryStream => { - const streamInfo = frag.elementaryStreams[elementaryStream]; - if (!streamInfo) { - return; - } - const timeRange = timeRanges[elementaryStream]; - const partial = isFragHint || streamInfo.partial === true; - fragmentEntity.range[elementaryStream] = this.getBufferedTimes(frag, data.part, partial, timeRange); - }); - fragmentEntity.loaded = null; - if (Object.keys(fragmentEntity.range).length) { - fragmentEntity.buffered = true; - const endList = fragmentEntity.body.endList = frag.endList || fragmentEntity.body.endList; - if (endList) { - this.endListFragments[fragmentEntity.body.type] = fragmentEntity; - } - if (!isPartial(fragmentEntity)) { - // Remove older fragment parts from lookup after frag is tracked as buffered - this.removeParts(frag.sn - 1, frag.type); - } - } else { - // remove fragment if nothing was appended - this.removeFragment(fragmentEntity.body); - } - } - removeParts(snToKeep, levelType) { - const activeParts = this.activePartLists[levelType]; - if (!activeParts) { - return; - } - this.activePartLists[levelType] = activeParts.filter(part => part.fragment.sn >= snToKeep); - } - fragBuffered(frag, force) { - const fragKey = getFragmentKey(frag); - let fragmentEntity = this.fragments[fragKey]; - if (!fragmentEntity && force) { - fragmentEntity = this.fragments[fragKey] = { - body: frag, - appendedPTS: null, - loaded: null, - buffered: false, - range: Object.create(null) - }; - if (frag.gap) { - this.hasGaps = true; - } - } - if (fragmentEntity) { - fragmentEntity.loaded = null; - fragmentEntity.buffered = true; - } - } - getBufferedTimes(fragment, part, partial, timeRange) { - const buffered = { - time: [], - partial - }; - const startPTS = fragment.start; - const endPTS = fragment.end; - const minEndPTS = fragment.minEndPTS || endPTS; - const maxStartPTS = fragment.maxStartPTS || startPTS; - for (let i = 0; i < timeRange.length; i++) { - const startTime = timeRange.start(i) - this.bufferPadding; - const endTime = timeRange.end(i) + this.bufferPadding; - if (maxStartPTS >= startTime && minEndPTS <= endTime) { - // Fragment is entirely contained in buffer - // No need to check the other timeRange times since it's completely playable - buffered.time.push({ - startPTS: Math.max(startPTS, timeRange.start(i)), - endPTS: Math.min(endPTS, timeRange.end(i)) - }); - break; - } else if (startPTS < endTime && endPTS > startTime) { - const start = Math.max(startPTS, timeRange.start(i)); - const end = Math.min(endPTS, timeRange.end(i)); - if (end > start) { - buffered.partial = true; - // Check for intersection with buffer - // Get playable sections of the fragment - buffered.time.push({ - startPTS: start, - endPTS: end - }); - } - } else if (endPTS <= startTime) { - // No need to check the rest of the timeRange as it is in order - break; - } - } - return buffered; - } - - /** - * Gets the partial fragment for a certain time - */ - getPartialFragment(time) { - let bestFragment = null; - let timePadding; - let startTime; - let endTime; - let bestOverlap = 0; - const { - bufferPadding, - fragments - } = this; - Object.keys(fragments).forEach(key => { - const fragmentEntity = fragments[key]; - if (!fragmentEntity) { - return; - } - if (isPartial(fragmentEntity)) { - startTime = fragmentEntity.body.start - bufferPadding; - endTime = fragmentEntity.body.end + bufferPadding; - if (time >= startTime && time <= endTime) { - // Use the fragment that has the most padding from start and end time - timePadding = Math.min(time - startTime, endTime - time); - if (bestOverlap <= timePadding) { - bestFragment = fragmentEntity.body; - bestOverlap = timePadding; - } - } - } - }); - return bestFragment; - } - isEndListAppended(type) { - const lastFragmentEntity = this.endListFragments[type]; - return lastFragmentEntity !== undefined && (lastFragmentEntity.buffered || isPartial(lastFragmentEntity)); - } - getState(fragment) { - const fragKey = getFragmentKey(fragment); - const fragmentEntity = this.fragments[fragKey]; - if (fragmentEntity) { - if (!fragmentEntity.buffered) { - return FragmentState.APPENDING; - } else if (isPartial(fragmentEntity)) { - return FragmentState.PARTIAL; - } else { - return FragmentState.OK; - } - } - return FragmentState.NOT_LOADED; - } - isTimeBuffered(startPTS, endPTS, timeRange) { - let startTime; - let endTime; - for (let i = 0; i < timeRange.length; i++) { - startTime = timeRange.start(i) - this.bufferPadding; - endTime = timeRange.end(i) + this.bufferPadding; - if (startPTS >= startTime && endPTS <= endTime) { - return true; - } - if (endPTS <= startTime) { - // No need to check the rest of the timeRange as it is in order - return false; - } - } - return false; - } - onManifestLoading() { - this.removeAllFragments(); - } - onFragLoaded(event, data) { - // don't track initsegment (for which sn is not a number) - // don't track frags used for bitrateTest, they're irrelevant. - if (data.frag.sn === 'initSegment' || data.frag.bitrateTest) { - return; - } - const frag = data.frag; - // Fragment entity `loaded` FragLoadedData is null when loading parts - const loaded = data.part ? null : data; - const fragKey = getFragmentKey(frag); - this.fragments[fragKey] = { - body: frag, - appendedPTS: null, - loaded, - buffered: false, - range: Object.create(null) - }; - } - onBufferAppended(event, data) { - const { - frag, - part, - timeRanges, - type - } = data; - if (frag.sn === 'initSegment') { - return; - } - const playlistType = frag.type; - if (part) { - let activeParts = this.activePartLists[playlistType]; - if (!activeParts) { - this.activePartLists[playlistType] = activeParts = []; - } - activeParts.push(part); - } - // Store the latest timeRanges loaded in the buffer - this.timeRanges = timeRanges; - const timeRange = timeRanges[type]; - this.detectEvictedFragments(type, timeRange, playlistType, part); - } - onFragBuffered(event, data) { - this.detectPartialFragments(data); - } - hasFragment(fragment) { - const fragKey = getFragmentKey(fragment); - return !!this.fragments[fragKey]; - } - hasFragments(type) { - const { - fragments - } = this; - const keys = Object.keys(fragments); - if (!type) { - return keys.length > 0; - } - for (let i = keys.length; i--;) { - const fragmentEntity = fragments[keys[i]]; - if ((fragmentEntity == null ? void 0 : fragmentEntity.body.type) === type) { - return true; - } - } - return false; - } - hasParts(type) { - var _this$activePartLists; - return !!((_this$activePartLists = this.activePartLists[type]) != null && _this$activePartLists.length); - } - removeFragmentsInRange(start, end, playlistType, withGapOnly, unbufferedOnly) { - if (withGapOnly && !this.hasGaps) { - return; - } - Object.keys(this.fragments).forEach(key => { - const fragmentEntity = this.fragments[key]; - if (!fragmentEntity) { - return; - } - const frag = fragmentEntity.body; - if (frag.type !== playlistType || withGapOnly && !frag.gap) { - return; - } - if (frag.start < end && frag.end > start && (fragmentEntity.buffered || unbufferedOnly)) { - this.removeFragment(frag); - } - }); - } - removeFragment(fragment) { - const fragKey = getFragmentKey(fragment); - fragment.stats.loaded = 0; - fragment.clearElementaryStreamInfo(); - const activeParts = this.activePartLists[fragment.type]; - if (activeParts) { - const snToRemove = fragment.sn; - this.activePartLists[fragment.type] = activeParts.filter(part => part.fragment.sn !== snToRemove); - } - delete this.fragments[fragKey]; - if (fragment.endList) { - delete this.endListFragments[fragment.type]; - } - } - removeAllFragments() { - var _this$hls, _this$hls$latestLevel; - this.fragments = Object.create(null); - this.endListFragments = Object.create(null); - this.activePartLists = Object.create(null); - this.hasGaps = false; - const partlist = (_this$hls = this.hls) == null ? void 0 : (_this$hls$latestLevel = _this$hls.latestLevelDetails) == null ? void 0 : _this$hls$latestLevel.partList; - if (partlist) { - partlist.forEach(part => part.clearElementaryStreamInfo()); - } - } -} -function isPartial(fragmentEntity) { - var _fragmentEntity$range, _fragmentEntity$range2, _fragmentEntity$range3; - return fragmentEntity.buffered && (fragmentEntity.body.gap || ((_fragmentEntity$range = fragmentEntity.range.video) == null ? void 0 : _fragmentEntity$range.partial) || ((_fragmentEntity$range2 = fragmentEntity.range.audio) == null ? void 0 : _fragmentEntity$range2.partial) || ((_fragmentEntity$range3 = fragmentEntity.range.audiovideo) == null ? void 0 : _fragmentEntity$range3.partial)); -} -function getFragmentKey(fragment) { - return `${fragment.type}_${fragment.level}_${fragment.sn}`; -} - -const MIN_CHUNK_SIZE = Math.pow(2, 17); // 128kb - -class FragmentLoader { - constructor(config) { - this.config = void 0; - this.loader = null; - this.partLoadTimeout = -1; - this.config = config; - } - destroy() { - if (this.loader) { - this.loader.destroy(); - this.loader = null; - } - } - abort() { - if (this.loader) { - // Abort the loader for current fragment. Only one may load at any given time - this.loader.abort(); - } - } - load(frag, onProgress) { - const url = frag.url; - if (!url) { - return Promise.reject(new LoadError({ - type: ErrorTypes.NETWORK_ERROR, - details: ErrorDetails.FRAG_LOAD_ERROR, - fatal: false, - frag, - error: new Error(`Fragment does not have a ${url ? 'part list' : 'url'}`), - networkDetails: null - })); - } - this.abort(); - const config = this.config; - const FragmentILoader = config.fLoader; - const DefaultILoader = config.loader; - return new Promise((resolve, reject) => { - if (this.loader) { - this.loader.destroy(); - } - if (frag.gap) { - if (frag.tagList.some(tags => tags[0] === 'GAP')) { - reject(createGapLoadError(frag)); - return; - } else { - // Reset temporary treatment as GAP tag - frag.gap = false; - } - } - const loader = this.loader = FragmentILoader ? new FragmentILoader(config) : new DefaultILoader(config); - const loaderContext = createLoaderContext(frag); - frag.loader = loader; - const loadPolicy = getLoaderConfigWithoutReties(config.fragLoadPolicy.default); - const loaderConfig = { - loadPolicy, - timeout: loadPolicy.maxLoadTimeMs, - maxRetry: 0, - retryDelay: 0, - maxRetryDelay: 0, - highWaterMark: frag.sn === 'initSegment' ? Infinity : MIN_CHUNK_SIZE - }; - // Assign frag stats to the loader's stats reference - frag.stats = loader.stats; - const callbacks = { - onSuccess: (response, stats, context, networkDetails) => { - this.resetLoader(frag, loader); - let payload = response.data; - if (context.resetIV && frag.decryptdata) { - frag.decryptdata.iv = new Uint8Array(payload.slice(0, 16)); - payload = payload.slice(16); - } - resolve({ - frag, - part: null, - payload, - networkDetails - }); - }, - onError: (response, context, networkDetails, stats) => { - this.resetLoader(frag, loader); - reject(new LoadError({ - type: ErrorTypes.NETWORK_ERROR, - details: ErrorDetails.FRAG_LOAD_ERROR, - fatal: false, - frag, - response: _objectSpread2({ - url, - data: undefined - }, response), - error: new Error(`HTTP Error ${response.code} ${response.text}`), - networkDetails, - stats - })); - }, - onAbort: (stats, context, networkDetails) => { - this.resetLoader(frag, loader); - reject(new LoadError({ - type: ErrorTypes.NETWORK_ERROR, - details: ErrorDetails.INTERNAL_ABORTED, - fatal: false, - frag, - error: new Error('Aborted'), - networkDetails, - stats - })); - }, - onTimeout: (stats, context, networkDetails) => { - this.resetLoader(frag, loader); - reject(new LoadError({ - type: ErrorTypes.NETWORK_ERROR, - details: ErrorDetails.FRAG_LOAD_TIMEOUT, - fatal: false, - frag, - error: new Error(`Timeout after ${loaderConfig.timeout}ms`), - networkDetails, - stats - })); - } - }; - if (onProgress) { - callbacks.onProgress = (stats, context, data, networkDetails) => onProgress({ - frag, - part: null, - payload: data, - networkDetails - }); - } - loader.load(loaderContext, loaderConfig, callbacks); - }); - } - loadPart(frag, part, onProgress) { - this.abort(); - const config = this.config; - const FragmentILoader = config.fLoader; - const DefaultILoader = config.loader; - return new Promise((resolve, reject) => { - if (this.loader) { - this.loader.destroy(); - } - if (frag.gap || part.gap) { - reject(createGapLoadError(frag, part)); - return; - } - const loader = this.loader = FragmentILoader ? new FragmentILoader(config) : new DefaultILoader(config); - const loaderContext = createLoaderContext(frag, part); - frag.loader = loader; - // Should we define another load policy for parts? - const loadPolicy = getLoaderConfigWithoutReties(config.fragLoadPolicy.default); - const loaderConfig = { - loadPolicy, - timeout: loadPolicy.maxLoadTimeMs, - maxRetry: 0, - retryDelay: 0, - maxRetryDelay: 0, - highWaterMark: MIN_CHUNK_SIZE - }; - // Assign part stats to the loader's stats reference - part.stats = loader.stats; - loader.load(loaderContext, loaderConfig, { - onSuccess: (response, stats, context, networkDetails) => { - this.resetLoader(frag, loader); - this.updateStatsFromPart(frag, part); - const partLoadedData = { - frag, - part, - payload: response.data, - networkDetails - }; - onProgress(partLoadedData); - resolve(partLoadedData); - }, - onError: (response, context, networkDetails, stats) => { - this.resetLoader(frag, loader); - reject(new LoadError({ - type: ErrorTypes.NETWORK_ERROR, - details: ErrorDetails.FRAG_LOAD_ERROR, - fatal: false, - frag, - part, - response: _objectSpread2({ - url: loaderContext.url, - data: undefined - }, response), - error: new Error(`HTTP Error ${response.code} ${response.text}`), - networkDetails, - stats - })); - }, - onAbort: (stats, context, networkDetails) => { - frag.stats.aborted = part.stats.aborted; - this.resetLoader(frag, loader); - reject(new LoadError({ - type: ErrorTypes.NETWORK_ERROR, - details: ErrorDetails.INTERNAL_ABORTED, - fatal: false, - frag, - part, - error: new Error('Aborted'), - networkDetails, - stats - })); - }, - onTimeout: (stats, context, networkDetails) => { - this.resetLoader(frag, loader); - reject(new LoadError({ - type: ErrorTypes.NETWORK_ERROR, - details: ErrorDetails.FRAG_LOAD_TIMEOUT, - fatal: false, - frag, - part, - error: new Error(`Timeout after ${loaderConfig.timeout}ms`), - networkDetails, - stats - })); - } - }); - }); - } - updateStatsFromPart(frag, part) { - const fragStats = frag.stats; - const partStats = part.stats; - const partTotal = partStats.total; - fragStats.loaded += partStats.loaded; - if (partTotal) { - const estTotalParts = Math.round(frag.duration / part.duration); - const estLoadedParts = Math.min(Math.round(fragStats.loaded / partTotal), estTotalParts); - const estRemainingParts = estTotalParts - estLoadedParts; - const estRemainingBytes = estRemainingParts * Math.round(fragStats.loaded / estLoadedParts); - fragStats.total = fragStats.loaded + estRemainingBytes; - } else { - fragStats.total = Math.max(fragStats.loaded, fragStats.total); - } - const fragLoading = fragStats.loading; - const partLoading = partStats.loading; - if (fragLoading.start) { - // add to fragment loader latency - fragLoading.first += partLoading.first - partLoading.start; - } else { - fragLoading.start = partLoading.start; - fragLoading.first = partLoading.first; - } - fragLoading.end = partLoading.end; - } - resetLoader(frag, loader) { - frag.loader = null; - if (this.loader === loader) { - self.clearTimeout(this.partLoadTimeout); - this.loader = null; - } - loader.destroy(); - } -} -function createLoaderContext(frag, part = null) { - const segment = part || frag; - const loaderContext = { - frag, - part, - responseType: 'arraybuffer', - url: segment.url, - headers: {}, - rangeStart: 0, - rangeEnd: 0 - }; - const start = segment.byteRangeStartOffset; - const end = segment.byteRangeEndOffset; - if (isFiniteNumber(start) && isFiniteNumber(end)) { - var _frag$decryptdata; - let byteRangeStart = start; - let byteRangeEnd = end; - if (frag.sn === 'initSegment' && isMethodFullSegmentAesCbc((_frag$decryptdata = frag.decryptdata) == null ? void 0 : _frag$decryptdata.method)) { - // MAP segment encrypted with method 'AES-128' or 'AES-256' (cbc), when served with HTTP Range, - // has the unencrypted size specified in the range. - // Ref: https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-08#section-6.3.6 - const fragmentLen = end - start; - if (fragmentLen % 16) { - byteRangeEnd = end + (16 - fragmentLen % 16); - } - if (start !== 0) { - loaderContext.resetIV = true; - byteRangeStart = start - 16; - } - } - loaderContext.rangeStart = byteRangeStart; - loaderContext.rangeEnd = byteRangeEnd; - } - return loaderContext; -} -function createGapLoadError(frag, part) { - const error = new Error(`GAP ${frag.gap ? 'tag' : 'attribute'} found`); - const errorData = { - type: ErrorTypes.MEDIA_ERROR, - details: ErrorDetails.FRAG_GAP, - fatal: false, - frag, - error, - networkDetails: null - }; - if (part) { - errorData.part = part; - } - (part ? part : frag).stats.aborted = true; - return new LoadError(errorData); -} -function isMethodFullSegmentAesCbc(method) { - return method === 'AES-128' || method === 'AES-256'; -} -class LoadError extends Error { - constructor(data) { - super(data.error.message); - this.data = void 0; - this.data = data; - } -} - -class KeyLoader { - constructor(config) { - this.config = void 0; - this.keyUriToKeyInfo = {}; - this.emeController = null; - this.config = config; - } - abort(type) { - for (const uri in this.keyUriToKeyInfo) { - const loader = this.keyUriToKeyInfo[uri].loader; - if (loader) { - var _loader$context; - if (type && type !== ((_loader$context = loader.context) == null ? void 0 : _loader$context.frag.type)) { - return; - } - loader.abort(); - } - } - } - detach() { - for (const uri in this.keyUriToKeyInfo) { - const keyInfo = this.keyUriToKeyInfo[uri]; - // Remove cached EME keys on detach - if (keyInfo.mediaKeySessionContext || keyInfo.decryptdata.isCommonEncryption) { - delete this.keyUriToKeyInfo[uri]; - } - } - } - destroy() { - this.detach(); - for (const uri in this.keyUriToKeyInfo) { - const loader = this.keyUriToKeyInfo[uri].loader; - if (loader) { - loader.destroy(); - } - } - this.keyUriToKeyInfo = {}; - } - createKeyLoadError(frag, details = ErrorDetails.KEY_LOAD_ERROR, error, networkDetails, response) { - return new LoadError({ - type: ErrorTypes.NETWORK_ERROR, - details, - fatal: false, - frag, - response, - error, - networkDetails - }); - } - loadClear(loadingFrag, encryptedFragments) { - if (this.emeController && this.config.emeEnabled) { - // access key-system with nearest key on start (loaidng frag is unencrypted) - const { - sn, - cc - } = loadingFrag; - for (let i = 0; i < encryptedFragments.length; i++) { - const frag = encryptedFragments[i]; - if (cc <= frag.cc && (sn === 'initSegment' || frag.sn === 'initSegment' || sn < frag.sn)) { - this.emeController.selectKeySystemFormat(frag).then(keySystemFormat => { - frag.setKeyFormat(keySystemFormat); - }); - break; - } - } - } - } - load(frag) { - if (!frag.decryptdata && frag.encrypted && this.emeController) { - // Multiple keys, but none selected, resolve in eme-controller - return this.emeController.selectKeySystemFormat(frag).then(keySystemFormat => { - return this.loadInternal(frag, keySystemFormat); - }); - } - return this.loadInternal(frag); - } - loadInternal(frag, keySystemFormat) { - var _keyInfo, _keyInfo2; - if (keySystemFormat) { - frag.setKeyFormat(keySystemFormat); - } - const decryptdata = frag.decryptdata; - if (!decryptdata) { - const error = new Error(keySystemFormat ? `Expected frag.decryptdata to be defined after setting format ${keySystemFormat}` : 'Missing decryption data on fragment in onKeyLoading'); - return Promise.reject(this.createKeyLoadError(frag, ErrorDetails.KEY_LOAD_ERROR, error)); - } - const uri = decryptdata.uri; - if (!uri) { - return Promise.reject(this.createKeyLoadError(frag, ErrorDetails.KEY_LOAD_ERROR, new Error(`Invalid key URI: "${uri}"`))); - } - let keyInfo = this.keyUriToKeyInfo[uri]; - if ((_keyInfo = keyInfo) != null && _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 ((_keyInfo2 = keyInfo) != null && _keyInfo2.keyLoadPromise) { - var _keyInfo$mediaKeySess; - switch ((_keyInfo$mediaKeySess = keyInfo.mediaKeySessionContext) == null ? void 0 : _keyInfo$mediaKeySess.keyStatus) { - case undefined: - case 'status-pending': - case 'usable': - case 'usable-in-future': - return keyInfo.keyLoadPromise.then(keyLoadedData => { - // Return the correct fragment with updated decryptdata key and loaded keyInfo - decryptdata.key = keyLoadedData.keyInfo.decryptdata.key; - return { - frag, - keyInfo - }; - }); - } - // If we have a key session and status and it is not pending or usable, continue - // This will go back to the eme-controller for expired keys to get a new keyLoadPromise - } - - // Load the key or return the loading promise - keyInfo = this.keyUriToKeyInfo[uri] = { - decryptdata, - keyLoadPromise: null, - loader: null, - mediaKeySessionContext: null - }; - switch (decryptdata.method) { - case 'ISO-23001-7': - case 'SAMPLE-AES': - case 'SAMPLE-AES-CENC': - case 'SAMPLE-AES-CTR': - if (decryptdata.keyFormat === 'identity') { - // loadKeyHTTP handles http(s) and data URLs - return this.loadKeyHTTP(keyInfo, frag); - } - return this.loadKeyEME(keyInfo, frag); - case 'AES-128': - case 'AES-256': - case 'AES-256-CTR': - return this.loadKeyHTTP(keyInfo, frag); - default: - return Promise.reject(this.createKeyLoadError(frag, ErrorDetails.KEY_LOAD_ERROR, new Error(`Key supplied with unsupported METHOD: "${decryptdata.method}"`))); - } - } - loadKeyEME(keyInfo, frag) { - const 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, frag) { - const config = this.config; - const Loader = config.loader; - const keyLoader = new Loader(config); - frag.keyLoader = keyInfo.loader = keyLoader; - return keyInfo.keyLoadPromise = new Promise((resolve, reject) => { - const loaderContext = { - keyInfo, - frag, - responseType: 'arraybuffer', - url: keyInfo.decryptdata.uri - }; - - // maxRetry is 0 so that instead of retrying the same key on the same variant multiple times, - // key-loader will trigger an error and rely on stream-controller to handle retry logic. - // this will also align retry logic with fragment-loader - const loadPolicy = config.keyLoadPolicy.default; - const loaderConfig = { - loadPolicy, - timeout: loadPolicy.maxLoadTimeMs, - maxRetry: 0, - retryDelay: 0, - maxRetryDelay: 0 - }; - const loaderCallbacks = { - onSuccess: (response, stats, context, networkDetails) => { - const { - frag, - keyInfo, - url: uri - } = context; - if (!frag.decryptdata || keyInfo !== this.keyUriToKeyInfo[uri]) { - return reject(this.createKeyLoadError(frag, ErrorDetails.KEY_LOAD_ERROR, new Error('after key load, decryptdata unset or changed'), networkDetails)); - } - keyInfo.decryptdata.key = frag.decryptdata.key = new Uint8Array(response.data); - - // detach fragment key loader on load success - frag.keyLoader = null; - keyInfo.loader = null; - resolve({ - frag, - keyInfo - }); - }, - onError: (response, context, networkDetails, stats) => { - this.resetLoader(context); - reject(this.createKeyLoadError(frag, ErrorDetails.KEY_LOAD_ERROR, new Error(`HTTP Error ${response.code} loading key ${response.text}`), networkDetails, _objectSpread2({ - url: loaderContext.url, - data: undefined - }, response))); - }, - onTimeout: (stats, context, networkDetails) => { - this.resetLoader(context); - reject(this.createKeyLoadError(frag, ErrorDetails.KEY_LOAD_TIMEOUT, new Error('key loading timed out'), networkDetails)); - }, - onAbort: (stats, context, networkDetails) => { - this.resetLoader(context); - reject(this.createKeyLoadError(frag, ErrorDetails.INTERNAL_ABORTED, new Error('key loading aborted'), networkDetails)); - } - }; - keyLoader.load(loaderContext, loaderConfig, loaderCallbacks); - }); - } - resetLoader(context) { - const { - frag, - keyInfo, - url: uri - } = context; - const loader = keyInfo.loader; - if (frag.keyLoader === loader) { - frag.keyLoader = null; - keyInfo.loader = null; - } - delete this.keyUriToKeyInfo[uri]; - if (loader) { - loader.destroy(); - } - } -} - -/** - * @ignore - * Sub-class specialization of EventHandler base class. - * - * TaskLoop allows to schedule a task function being called (optionnaly repeatedly) on the main loop, - * scheduled asynchroneously, avoiding recursive calls in the same tick. - * - * The task itself is implemented in `doTick`. It can be requested and called for single execution - * using the `tick` method. - * - * It will be assured that the task execution method (`tick`) only gets called once per main loop "tick", - * no matter how often it gets requested for execution. Execution in further ticks will be scheduled accordingly. - * - * If further execution requests have already been scheduled on the next tick, it can be checked with `hasNextTick`, - * and cancelled with `clearNextTick`. - * - * The task can be scheduled as an interval repeatedly with a period as parameter (see `setInterval`, `clearInterval`). - * - * Sub-classes need to implement the `doTick` method which will effectively have the task execution routine. - * - * Further explanations: - * - * The baseclass has a `tick` method that will schedule the doTick call. It may be called synchroneously - * only for a stack-depth of one. On re-entrant calls, sub-sequent calls are scheduled for next main loop ticks. - * - * When the task execution (`tick` method) is called in re-entrant way this is detected and - * we are limiting the task execution per call stack to exactly one, but scheduling/post-poning further - * task processing on the next main loop iteration (also known as "next tick" in the Node/JS runtime lingo). - */ -class TaskLoop extends Logger { - constructor(label, logger) { - super(label, logger); - this._boundTick = void 0; - this._tickTimer = null; - this._tickInterval = null; - this._tickCallCount = 0; - this._boundTick = this.tick.bind(this); - } - destroy() { - this.onHandlerDestroying(); - this.onHandlerDestroyed(); - } - onHandlerDestroying() { - // clear all timers before unregistering from event bus - this.clearNextTick(); - this.clearInterval(); - } - onHandlerDestroyed() {} - hasInterval() { - return !!this._tickInterval; - } - hasNextTick() { - return !!this._tickTimer; - } - - /** - * @param millis - Interval time (ms) - * @eturns True when interval has been scheduled, false when already scheduled (no effect) - */ - setInterval(millis) { - if (!this._tickInterval) { - this._tickCallCount = 0; - this._tickInterval = self.setInterval(this._boundTick, millis); - return true; - } - return false; - } - - /** - * @returns True when interval was cleared, false when none was set (no effect) - */ - clearInterval() { - if (this._tickInterval) { - self.clearInterval(this._tickInterval); - this._tickInterval = null; - return true; - } - return false; - } - - /** - * @returns True when timeout was cleared, false when none was set (no effect) - */ - clearNextTick() { - if (this._tickTimer) { - self.clearTimeout(this._tickTimer); - this._tickTimer = null; - return true; - } - return false; - } - - /** - * Will call the subclass doTick implementation in this main loop tick - * or in the next one (via setTimeout(,0)) in case it has already been called - * in this tick (in case this is a re-entrant call). - */ - tick() { - this._tickCallCount++; - if (this._tickCallCount === 1) { - this.doTick(); - // re-entrant call to tick from previous doTick call stack - // -> schedule a call on the next main loop iteration to process this task processing request - if (this._tickCallCount > 1) { - // make sure only one timer exists at any time at max - this.tickImmediate(); - } - this._tickCallCount = 0; - } - } - tickImmediate() { - this.clearNextTick(); - this._tickTimer = self.setTimeout(this._boundTick, 0); - } - - /** - * For subclass to implement task logic - * @abstract - */ - doTick() {} -} - -/** - * Provides methods dealing with buffer length retrieval for example. - * - * In general, a helper around HTML5 MediaElement TimeRanges gathered from `buffered` property. - * - * Also @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/buffered - */ - -const noopBuffered = { - length: 0, - start: () => 0, - end: () => 0 -}; -class BufferHelper { - /** - * Return true if `media`'s buffered include `position` - */ - static isBuffered(media, position) { - if (media) { - const buffered = BufferHelper.getBuffered(media); - for (let i = buffered.length; i--;) { - if (position >= buffered.start(i) && position <= buffered.end(i)) { - return true; - } - } - } - return false; - } - static bufferInfo(media, pos, maxHoleDuration) { - if (media) { - const vbuffered = BufferHelper.getBuffered(media); - if (vbuffered.length) { - const buffered = []; - for (let i = 0; i < vbuffered.length; i++) { - buffered.push({ - start: vbuffered.start(i), - end: vbuffered.end(i) - }); - } - return BufferHelper.bufferedInfo(buffered, pos, maxHoleDuration); - } - } - return { - len: 0, - start: pos, - end: pos, - nextStart: undefined - }; - } - static bufferedInfo(buffered, pos, maxHoleDuration) { - pos = Math.max(0, pos); - // sort on buffer.start/smaller end (IE does not always return sorted buffered range) - buffered.sort((a, b) => a.start - b.start || b.end - a.end); - let buffered2 = []; - if (maxHoleDuration) { - // there might be some small holes between buffer time range - // consider that holes smaller than maxHoleDuration are irrelevant and build another - // buffer time range representations that discards those holes - for (let i = 0; i < buffered.length; i++) { - const buf2len = buffered2.length; - if (buf2len) { - const buf2end = buffered2[buf2len - 1].end; - // if small hole (value between 0 or maxHoleDuration ) or overlapping (negative) - if (buffered[i].start - buf2end < maxHoleDuration) { - // merge overlapping time ranges - // update lastRange.end only if smaller than item.end - // e.g. [ 1, 15] with [ 2,8] => [ 1,15] (no need to modify lastRange.end) - // whereas [ 1, 8] with [ 2,15] => [ 1,15] ( lastRange should switch from [1,8] to [1,15]) - if (buffered[i].end > buf2end) { - buffered2[buf2len - 1].end = buffered[i].end; - } - } else { - // big hole - buffered2.push(buffered[i]); - } - } else { - // first value - buffered2.push(buffered[i]); - } - } - } else { - buffered2 = buffered; - } - let bufferLen = 0; - - // bufferStartNext can possibly be undefined based on the conditional logic below - let bufferStartNext; - - // bufferStart and bufferEnd are buffer boundaries around current video position - let bufferStart = pos; - let bufferEnd = pos; - for (let i = 0; i < buffered2.length; i++) { - const start = buffered2[i].start; - const end = buffered2[i].end; - // logger.log('buf start/end:' + buffered.start(i) + '/' + buffered.end(i)); - if (pos + maxHoleDuration >= start && pos < end) { - // play position is inside this buffer TimeRange, retrieve end of buffer position and buffer length - bufferStart = start; - bufferEnd = end; - bufferLen = bufferEnd - pos; - } else if (pos + maxHoleDuration < start) { - bufferStartNext = start; - break; - } - } - return { - len: bufferLen, - start: bufferStart || 0, - end: bufferEnd || 0, - nextStart: bufferStartNext - }; - } - - /** - * Safe method to get buffered property. - * SourceBuffer.buffered may throw if SourceBuffer is removed from it's MediaSource - */ - static getBuffered(media) { - try { - return media.buffered || noopBuffered; - } catch (e) { - logger.log('failed to get media.buffered', e); - return noopBuffered; - } - } -} - -class ChunkMetadata { - constructor(level, sn, id, size = 0, part = -1, partial = false) { - this.level = void 0; - this.sn = void 0; - this.part = void 0; - this.id = void 0; - this.size = void 0; - this.partial = void 0; - this.transmuxing = getNewPerformanceTiming(); - this.buffering = { - audio: getNewPerformanceTiming(), - video: getNewPerformanceTiming(), - audiovideo: getNewPerformanceTiming() - }; - this.level = level; - this.sn = sn; - this.id = id; - this.size = size; - this.part = part; - this.partial = partial; - } -} -function getNewPerformanceTiming() { - return { - start: 0, - executeStart: 0, - executeEnd: 0, - end: 0 - }; -} - -function findFirstFragWithCC(fragments, cc) { - for (let i = 0, len = fragments.length; i < len; i++) { - var _fragments$i; - if (((_fragments$i = fragments[i]) == null ? void 0 : _fragments$i.cc) === cc) { - return fragments[i]; - } - } - return null; -} -function shouldAlignOnDiscontinuities(refDetails, details) { - if (refDetails) { - if (details.startCC < refDetails.endCC && details.endCC > refDetails.startCC) { - return true; - } - } - return false; -} -function adjustFragmentStart(frag, sliding) { - if (frag) { - const start = frag.start + sliding; - frag.start = frag.startPTS = start; - frag.endPTS = start + frag.duration; - } -} -function adjustSlidingStart(sliding, details) { - // Update segments - const fragments = details.fragments; - for (let i = 0, len = fragments.length; i < len; i++) { - adjustFragmentStart(fragments[i], sliding); - } - // Update LL-HLS parts at the end of the playlist - if (details.fragmentHint) { - adjustFragmentStart(details.fragmentHint, sliding); - } - details.alignedSliding = true; -} - -/** - * Using the parameters of the last level, this function computes PTS' of the new fragments so that they form a - * contiguous stream with the last fragments. - * The PTS of a fragment lets Hls.js know where it fits into a stream - by knowing every PTS, we know which fragment to - * download at any given time. PTS is normally computed when the fragment is demuxed, so taking this step saves us time - * and an extra download. - * @param lastFrag - * @param lastLevel - * @param details - */ -function alignStream(lastFrag, switchDetails, details) { - if (!switchDetails) { - return; - } - alignDiscontinuities(details, switchDetails); - if (!details.alignedSliding && switchDetails) { - // If the PTS wasn't figured out via discontinuity sequence that means there was no CC increase within the level. - // Aligning via Program Date Time should therefore be reliable, since PDT should be the same within the same - // discontinuity sequence. - alignMediaPlaylistByPDT(details, switchDetails); - } - if (!details.alignedSliding && switchDetails && !details.skippedSegments) { - // Try to align on sn so that we pick a better start fragment. - // Do not perform this on playlists with delta updates as this is only to align levels on switch - // and adjustSliding only adjusts fragments after skippedSegments. - adjustSliding(switchDetails, details, false); - } -} - -/** - * Ajust the start of fragments in `details` by the difference in time between fragments of the latest - * shared discontinuity sequence change. - * @param lastLevel - The details of the last loaded level - * @param details - The details of the new level - */ -function alignDiscontinuities(details, refDetails) { - if (!shouldAlignOnDiscontinuities(refDetails, details)) { - return; - } - const targetCC = Math.min(refDetails.endCC, details.endCC); - const refFrag = findFirstFragWithCC(refDetails.fragments, targetCC); - const frag = findFirstFragWithCC(details.fragments, targetCC); - if (!refFrag || !frag) { - return; - } - logger.log(`Aligning playlist at start of dicontinuity sequence ${targetCC}`); - const delta = refFrag.start - frag.start; - adjustSlidingStart(delta, details); -} - -/** - * Ensures appropriate time-alignment between renditions based on PDT. - * This function assumes the timelines represented in `refDetails` are accurate, including the PDTs - * for the last discontinuity sequence number shared by both playlists when present, - * and uses the "wallclock"/PDT timeline as a cross-reference to `details`, adjusting the presentation - * times/timelines of `details` accordingly. - * Given the asynchronous nature of fetches and initial loads of live `main` and audio/subtitle tracks, - * the primary purpose of this function is to ensure the "local timelines" of audio/subtitle tracks - * are aligned to the main/video timeline, using PDT as the cross-reference/"anchor" that should - * be consistent across playlists, per the HLS spec. - * @param details - The details of the rendition you'd like to time-align (e.g. an audio rendition). - * @param refDetails - The details of the reference rendition with start and PDT times for alignment. - */ -function alignMediaPlaylistByPDT(details, refDetails) { - if (!details.hasProgramDateTime || !refDetails.hasProgramDateTime) { - return; - } - const fragments = details.fragments; - const refFragments = refDetails.fragments; - if (!fragments.length || !refFragments.length) { - return; - } - - // 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. - let refFrag; - let frag; - const targetCC = Math.min(refDetails.endCC, details.endCC); - if (refDetails.startCC < targetCC && details.startCC < targetCC) { - refFrag = findFirstFragWithCC(refFragments, targetCC); - frag = findFirstFragWithCC(fragments, targetCC); - } - if (!refFrag || !frag) { - refFrag = refFragments[Math.floor(refFragments.length / 2)]; - frag = findFirstFragWithCC(fragments, refFrag.cc) || fragments[Math.floor(fragments.length / 2)]; - } - const refPDT = refFrag.programDateTime; - const targetPDT = frag.programDateTime; - if (!refPDT || !targetPDT) { - return; - } - const delta = (targetPDT - refPDT) / 1000 - (frag.start - refFrag.start); - adjustSlidingStart(delta, details); -} - -class AESCrypto { - constructor(subtle, iv, aesMode) { - this.subtle = void 0; - this.aesIV = void 0; - this.aesMode = void 0; - this.subtle = subtle; - this.aesIV = iv; - this.aesMode = aesMode; - } - decrypt(data, key) { - switch (this.aesMode) { - case DecrypterAesMode.cbc: - return this.subtle.decrypt({ - name: 'AES-CBC', - iv: this.aesIV - }, key, data); - case DecrypterAesMode.ctr: - return this.subtle.decrypt({ - name: 'AES-CTR', - counter: this.aesIV, - length: 64 - }, - //64 : NIST SP800-38A standard suggests that the counter should occupy half of the counter block - key, data); - default: - throw new Error(`[AESCrypto] invalid aes mode ${this.aesMode}`); - } - } -} - -class FastAESKey { - constructor(subtle, key, aesMode) { - this.subtle = void 0; - this.key = void 0; - this.aesMode = void 0; - this.subtle = subtle; - this.key = key; - this.aesMode = aesMode; - } - expandKey() { - const subtleAlgoName = getSubtleAlgoName(this.aesMode); - return this.subtle.importKey('raw', this.key, { - name: subtleAlgoName - }, false, ['encrypt', 'decrypt']); - } -} -function getSubtleAlgoName(aesMode) { - switch (aesMode) { - case DecrypterAesMode.cbc: - return 'AES-CBC'; - case DecrypterAesMode.ctr: - return 'AES-CTR'; - default: - throw new Error(`[FastAESKey] invalid aes mode ${aesMode}`); - } -} - -// PKCS7 -function removePadding(array) { - const outputBytes = array.byteLength; - const paddingBytes = outputBytes && new DataView(array.buffer).getUint8(outputBytes - 1); - if (paddingBytes) { - return sliceUint8(array, 0, outputBytes - paddingBytes); - } - return array; -} -class AESDecryptor { - constructor() { - this.rcon = [0x0, 0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36]; - this.subMix = [new Uint32Array(256), new Uint32Array(256), new Uint32Array(256), new Uint32Array(256)]; - this.invSubMix = [new Uint32Array(256), new Uint32Array(256), new Uint32Array(256), new Uint32Array(256)]; - this.sBox = new Uint32Array(256); - this.invSBox = new Uint32Array(256); - this.key = new Uint32Array(0); - this.ksRows = 0; - this.keySize = 0; - this.keySchedule = void 0; - this.invKeySchedule = void 0; - this.initTable(); - } - - // Using view.getUint32() also swaps the byte order. - uint8ArrayToUint32Array_(arrayBuffer) { - const view = new DataView(arrayBuffer); - const newArray = new Uint32Array(4); - for (let i = 0; i < 4; i++) { - newArray[i] = view.getUint32(i * 4); - } - return newArray; - } - initTable() { - const sBox = this.sBox; - const invSBox = this.invSBox; - const subMix = this.subMix; - const subMix0 = subMix[0]; - const subMix1 = subMix[1]; - const subMix2 = subMix[2]; - const subMix3 = subMix[3]; - const invSubMix = this.invSubMix; - const invSubMix0 = invSubMix[0]; - const invSubMix1 = invSubMix[1]; - const invSubMix2 = invSubMix[2]; - const invSubMix3 = invSubMix[3]; - const d = new Uint32Array(256); - let x = 0; - let xi = 0; - let i = 0; - for (i = 0; i < 256; i++) { - if (i < 128) { - d[i] = i << 1; - } else { - d[i] = i << 1 ^ 0x11b; - } - } - for (i = 0; i < 256; i++) { - let sx = xi ^ xi << 1 ^ xi << 2 ^ xi << 3 ^ xi << 4; - sx = sx >>> 8 ^ sx & 0xff ^ 0x63; - sBox[x] = sx; - invSBox[sx] = x; - - // Compute multiplication - const x2 = d[x]; - const x4 = d[x2]; - const x8 = d[x4]; - - // Compute sub/invSub bytes, mix columns tables - let t = d[sx] * 0x101 ^ sx * 0x1010100; - subMix0[x] = t << 24 | t >>> 8; - subMix1[x] = t << 16 | t >>> 16; - subMix2[x] = t << 8 | t >>> 24; - subMix3[x] = t; - - // Compute inv sub bytes, inv mix columns tables - t = x8 * 0x1010101 ^ x4 * 0x10001 ^ x2 * 0x101 ^ x * 0x1010100; - invSubMix0[sx] = t << 24 | t >>> 8; - invSubMix1[sx] = t << 16 | t >>> 16; - invSubMix2[sx] = t << 8 | t >>> 24; - invSubMix3[sx] = t; - - // Compute next counter - if (!x) { - x = xi = 1; - } else { - x = x2 ^ d[d[d[x8 ^ x2]]]; - xi ^= d[d[xi]]; - } - } - } - expandKey(keyBuffer) { - // convert keyBuffer to Uint32Array - const key = this.uint8ArrayToUint32Array_(keyBuffer); - let sameKey = true; - let offset = 0; - while (offset < key.length && sameKey) { - sameKey = key[offset] === this.key[offset]; - offset++; - } - if (sameKey) { - return; - } - this.key = key; - const keySize = this.keySize = key.length; - if (keySize !== 4 && keySize !== 6 && keySize !== 8) { - throw new Error('Invalid aes key size=' + keySize); - } - const ksRows = this.ksRows = (keySize + 6 + 1) * 4; - let ksRow; - let invKsRow; - const keySchedule = this.keySchedule = new Uint32Array(ksRows); - const invKeySchedule = this.invKeySchedule = new Uint32Array(ksRows); - const sbox = this.sBox; - const rcon = this.rcon; - const invSubMix = this.invSubMix; - const invSubMix0 = invSubMix[0]; - const invSubMix1 = invSubMix[1]; - const invSubMix2 = invSubMix[2]; - const invSubMix3 = invSubMix[3]; - let prev; - let t; - for (ksRow = 0; ksRow < ksRows; ksRow++) { - if (ksRow < keySize) { - prev = keySchedule[ksRow] = key[ksRow]; - continue; - } - t = prev; - if (ksRow % keySize === 0) { - // Rot word - t = t << 8 | t >>> 24; - - // Sub word - t = sbox[t >>> 24] << 24 | sbox[t >>> 16 & 0xff] << 16 | sbox[t >>> 8 & 0xff] << 8 | sbox[t & 0xff]; - - // Mix Rcon - t ^= rcon[ksRow / keySize | 0] << 24; - } else if (keySize > 6 && ksRow % keySize === 4) { - // Sub word - t = sbox[t >>> 24] << 24 | sbox[t >>> 16 & 0xff] << 16 | sbox[t >>> 8 & 0xff] << 8 | sbox[t & 0xff]; - } - keySchedule[ksRow] = prev = (keySchedule[ksRow - keySize] ^ t) >>> 0; - } - for (invKsRow = 0; invKsRow < ksRows; invKsRow++) { - ksRow = ksRows - invKsRow; - if (invKsRow & 3) { - t = keySchedule[ksRow]; - } else { - t = keySchedule[ksRow - 4]; - } - if (invKsRow < 4 || ksRow <= 4) { - invKeySchedule[invKsRow] = t; - } else { - invKeySchedule[invKsRow] = invSubMix0[sbox[t >>> 24]] ^ invSubMix1[sbox[t >>> 16 & 0xff]] ^ invSubMix2[sbox[t >>> 8 & 0xff]] ^ invSubMix3[sbox[t & 0xff]]; - } - invKeySchedule[invKsRow] = invKeySchedule[invKsRow] >>> 0; - } - } - - // Adding this as a method greatly improves performance. - networkToHostOrderSwap(word) { - return word << 24 | (word & 0xff00) << 8 | (word & 0xff0000) >> 8 | word >>> 24; - } - decrypt(inputArrayBuffer, offset, aesIV) { - const nRounds = this.keySize + 6; - const invKeySchedule = this.invKeySchedule; - const invSBOX = this.invSBox; - const invSubMix = this.invSubMix; - const invSubMix0 = invSubMix[0]; - const invSubMix1 = invSubMix[1]; - const invSubMix2 = invSubMix[2]; - const invSubMix3 = invSubMix[3]; - const initVector = this.uint8ArrayToUint32Array_(aesIV); - let initVector0 = initVector[0]; - let initVector1 = initVector[1]; - let initVector2 = initVector[2]; - let initVector3 = initVector[3]; - const inputInt32 = new Int32Array(inputArrayBuffer); - const outputInt32 = new Int32Array(inputInt32.length); - let t0, t1, t2, t3; - let s0, s1, s2, s3; - let inputWords0, inputWords1, inputWords2, inputWords3; - let ksRow, i; - const swapWord = this.networkToHostOrderSwap; - while (offset < inputInt32.length) { - inputWords0 = swapWord(inputInt32[offset]); - inputWords1 = swapWord(inputInt32[offset + 1]); - inputWords2 = swapWord(inputInt32[offset + 2]); - inputWords3 = swapWord(inputInt32[offset + 3]); - s0 = inputWords0 ^ invKeySchedule[0]; - s1 = inputWords3 ^ invKeySchedule[1]; - s2 = inputWords2 ^ invKeySchedule[2]; - s3 = inputWords1 ^ invKeySchedule[3]; - ksRow = 4; - - // Iterate through the rounds of decryption - for (i = 1; i < nRounds; i++) { - t0 = invSubMix0[s0 >>> 24] ^ invSubMix1[s1 >> 16 & 0xff] ^ invSubMix2[s2 >> 8 & 0xff] ^ invSubMix3[s3 & 0xff] ^ invKeySchedule[ksRow]; - t1 = invSubMix0[s1 >>> 24] ^ invSubMix1[s2 >> 16 & 0xff] ^ invSubMix2[s3 >> 8 & 0xff] ^ invSubMix3[s0 & 0xff] ^ invKeySchedule[ksRow + 1]; - t2 = invSubMix0[s2 >>> 24] ^ invSubMix1[s3 >> 16 & 0xff] ^ invSubMix2[s0 >> 8 & 0xff] ^ invSubMix3[s1 & 0xff] ^ invKeySchedule[ksRow + 2]; - t3 = invSubMix0[s3 >>> 24] ^ invSubMix1[s0 >> 16 & 0xff] ^ invSubMix2[s1 >> 8 & 0xff] ^ invSubMix3[s2 & 0xff] ^ invKeySchedule[ksRow + 3]; - // Update state - s0 = t0; - s1 = t1; - s2 = t2; - s3 = t3; - ksRow = ksRow + 4; - } - - // Shift rows, sub bytes, add round key - t0 = invSBOX[s0 >>> 24] << 24 ^ invSBOX[s1 >> 16 & 0xff] << 16 ^ invSBOX[s2 >> 8 & 0xff] << 8 ^ invSBOX[s3 & 0xff] ^ invKeySchedule[ksRow]; - t1 = invSBOX[s1 >>> 24] << 24 ^ invSBOX[s2 >> 16 & 0xff] << 16 ^ invSBOX[s3 >> 8 & 0xff] << 8 ^ invSBOX[s0 & 0xff] ^ invKeySchedule[ksRow + 1]; - t2 = invSBOX[s2 >>> 24] << 24 ^ invSBOX[s3 >> 16 & 0xff] << 16 ^ invSBOX[s0 >> 8 & 0xff] << 8 ^ invSBOX[s1 & 0xff] ^ invKeySchedule[ksRow + 2]; - t3 = invSBOX[s3 >>> 24] << 24 ^ invSBOX[s0 >> 16 & 0xff] << 16 ^ invSBOX[s1 >> 8 & 0xff] << 8 ^ invSBOX[s2 & 0xff] ^ invKeySchedule[ksRow + 3]; - - // Write - outputInt32[offset] = swapWord(t0 ^ initVector0); - outputInt32[offset + 1] = swapWord(t3 ^ initVector1); - outputInt32[offset + 2] = swapWord(t2 ^ initVector2); - outputInt32[offset + 3] = swapWord(t1 ^ initVector3); - - // reset initVector to last 4 unsigned int - initVector0 = inputWords0; - initVector1 = inputWords1; - initVector2 = inputWords2; - initVector3 = inputWords3; - offset = offset + 4; - } - return outputInt32.buffer; - } -} - -const CHUNK_SIZE = 16; // 16 bytes, 128 bits - -class Decrypter { - constructor(config, { - removePKCS7Padding = true - } = {}) { - this.logEnabled = true; - this.removePKCS7Padding = void 0; - this.subtle = null; - this.softwareDecrypter = null; - this.key = null; - this.fastAesKey = null; - this.remainderData = null; - this.currentIV = null; - this.currentResult = null; - this.useSoftware = void 0; - this.enableSoftwareAES = void 0; - this.enableSoftwareAES = config.enableSoftwareAES; - this.removePKCS7Padding = removePKCS7Padding; - // built in decryptor expects PKCS7 padding - if (removePKCS7Padding) { - try { - const browserCrypto = self.crypto; - if (browserCrypto) { - this.subtle = browserCrypto.subtle || browserCrypto.webkitSubtle; - } - } catch (e) { - /* no-op */ - } - } - this.useSoftware = !this.subtle; - } - destroy() { - this.subtle = null; - this.softwareDecrypter = null; - this.key = null; - this.fastAesKey = null; - this.remainderData = null; - this.currentIV = null; - this.currentResult = null; - } - isSync() { - return this.useSoftware; - } - flush() { - const { - currentResult, - remainderData - } = this; - if (!currentResult || remainderData) { - this.reset(); - return null; - } - const data = new Uint8Array(currentResult); - this.reset(); - if (this.removePKCS7Padding) { - return removePadding(data); - } - return data; - } - reset() { - this.currentResult = null; - this.currentIV = null; - this.remainderData = null; - if (this.softwareDecrypter) { - this.softwareDecrypter = null; - } - } - decrypt(data, key, iv, aesMode) { - if (this.useSoftware) { - return new Promise((resolve, reject) => { - this.softwareDecrypt(new Uint8Array(data), key, iv, aesMode); - 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, aesMode); - } - - // Software decryption is progressive. Progressive decryption may not return a result on each call. Any cached - // data is handled in the flush() call - softwareDecrypt(data, key, iv, aesMode) { - const { - currentIV, - currentResult, - remainderData - } = this; - if (aesMode !== DecrypterAesMode.cbc || key.byteLength !== 16) { - logger.warn('SoftwareDecrypt: can only handle AES-128-CBC'); - return null; - } - this.logOnce('JS AES decrypt'); - // The output is staggered during progressive parsing - the current result is cached, and emitted on the next call - // This is done in order to strip PKCS7 padding, which is found at the end of each segment. We only know we've reached - // the end on flush(), but by that time we have already received all bytes for the segment. - // Progressive decryption does not work with WebCrypto - - if (remainderData) { - data = appendUint8Array(remainderData, data); - this.remainderData = null; - } - - // Byte length must be a multiple of 16 (AES-128 = 128 bit blocks = 16 bytes) - const currentChunk = this.getValidChunk(data); - if (!currentChunk.length) { - return null; - } - if (currentIV) { - iv = currentIV; - } - let softwareDecrypter = this.softwareDecrypter; - if (!softwareDecrypter) { - softwareDecrypter = this.softwareDecrypter = new AESDecryptor(); - } - softwareDecrypter.expandKey(key); - const result = currentResult; - this.currentResult = softwareDecrypter.decrypt(currentChunk.buffer, 0, iv); - this.currentIV = sliceUint8(currentChunk, -16).buffer; - if (!result) { - return null; - } - return result; - } - webCryptoDecrypt(data, key, iv, aesMode) { - if (this.key !== key || !this.fastAesKey) { - if (!this.subtle) { - return Promise.resolve(this.onWebCryptoError(data, key, iv, aesMode)); - } - this.key = key; - this.fastAesKey = new FastAESKey(this.subtle, key, aesMode); - } - return this.fastAesKey.expandKey().then(aesKey => { - // decrypt using web crypto - if (!this.subtle) { - return Promise.reject(new Error('web crypto not initialized')); - } - this.logOnce('WebCrypto AES decrypt'); - const crypto = new AESCrypto(this.subtle, new Uint8Array(iv), aesMode); - return crypto.decrypt(data.buffer, aesKey); - }).catch(err => { - logger.warn(`[decrypter]: WebCrypto Error, disable WebCrypto API, ${err.name}: ${err.message}`); - return this.onWebCryptoError(data, key, iv, aesMode); - }); - } - onWebCryptoError(data, key, iv, aesMode) { - const enableSoftwareAES = this.enableSoftwareAES; - if (enableSoftwareAES) { - this.useSoftware = true; - this.logEnabled = true; - this.softwareDecrypt(data, key, iv, aesMode); - const decryptResult = this.flush(); - if (decryptResult) { - return decryptResult.buffer; - } - } - throw new Error('WebCrypto' + (enableSoftwareAES ? ' and softwareDecrypt' : '') + ': failed to decrypt data'); - } - getValidChunk(data) { - let currentChunk = data; - const splitPoint = data.length - data.length % CHUNK_SIZE; - if (splitPoint !== data.length) { - currentChunk = sliceUint8(data, 0, splitPoint); - this.remainderData = sliceUint8(data, splitPoint); - } - return currentChunk; - } - logOnce(msg) { - if (!this.logEnabled) { - return; - } - logger.log(`[decrypter]: ${msg}`); - this.logEnabled = false; - } -} - -/** - * TimeRanges to string helper - */ - -const TimeRanges = { - toString: function (r) { - let log = ''; - const len = r.length; - for (let i = 0; i < len; i++) { - log += `[${r.start(i).toFixed(3)}-${r.end(i).toFixed(3)}]`; - } - return log; - } -}; - -const State = { - STOPPED: 'STOPPED', - IDLE: 'IDLE', - KEY_LOADING: 'KEY_LOADING', - FRAG_LOADING: 'FRAG_LOADING', - FRAG_LOADING_WAITING_RETRY: 'FRAG_LOADING_WAITING_RETRY', - WAITING_TRACK: 'WAITING_TRACK', - PARSING: 'PARSING', - PARSED: 'PARSED', - ENDED: 'ENDED', - ERROR: 'ERROR', - WAITING_INIT_PTS: 'WAITING_INIT_PTS', - WAITING_LEVEL: 'WAITING_LEVEL' -}; -class BaseStreamController extends TaskLoop { - constructor(hls, fragmentTracker, keyLoader, logPrefix, playlistType) { - super(logPrefix, hls.logger); - this.hls = void 0; - this.fragPrevious = null; - this.fragCurrent = null; - this.fragmentTracker = void 0; - this.transmuxer = null; - this._state = State.STOPPED; - this.playlistType = void 0; - this.media = null; - this.mediaBuffer = null; - this.config = void 0; - this.bitrateTest = false; - this.lastCurrentTime = 0; - this.nextLoadPosition = 0; - this.startPosition = 0; - this.startTimeOffset = null; - this.retryDate = 0; - this.levels = null; - this.fragmentLoader = void 0; - this.keyLoader = void 0; - this.levelLastLoaded = null; - this.startFragRequested = false; - this.decrypter = void 0; - this.initPTS = []; - this.buffering = true; - this.loadingParts = false; - this.loopSn = void 0; - this.onMediaSeeking = () => { - const { - config, - fragCurrent, - media, - mediaBuffer, - state - } = this; - const currentTime = media ? media.currentTime : 0; - const bufferInfo = BufferHelper.bufferInfo(mediaBuffer ? mediaBuffer : media, currentTime, config.maxBufferHole); - this.log(`media seeking to ${isFiniteNumber(currentTime) ? currentTime.toFixed(3) : currentTime}, state: ${state}`); - if (this.state === State.ENDED) { - this.resetLoadingState(); - } 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; - // 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.fragPrevious = null; - } - } - } - if (media) { - // Remove gap fragments - this.fragmentTracker.removeFragmentsInRange(currentTime, Infinity, this.playlistType, true); - - // Don't set lastCurrentTime with backward seeks (allows for frag selection with strict tolerances) - const lastCurrentTime = this.lastCurrentTime; - if (currentTime > lastCurrentTime) { - this.lastCurrentTime = currentTime; - } - if (!this.loadingParts) { - const bufferEnd = Math.max(bufferInfo.end, currentTime); - const shouldLoadParts = this.shouldLoadParts(this.getLevelDetails(), bufferEnd); - if (shouldLoadParts) { - this.log(`LL-Part loading ON after seeking to ${currentTime.toFixed(2)} with buffer @${bufferEnd.toFixed(2)}`); - this.loadingParts = shouldLoadParts; - } - } - } - - // in case seeking occurs although no media buffered, adjust startPosition and nextLoadPosition to seek target - if (!this.hls.hasEnoughToStart && !bufferInfo.len) { - this.log(`setting startPosition to ${currentTime} because of seek before start`); - this.nextLoadPosition = this.startPosition = currentTime; - } - - // Async tick to speed up processing - this.tickImmediate(); - }; - this.onMediaEnded = () => { - // reset startPosition and lastCurrentTime to restart playback @ stream beginning - this.log(`setting startPosition to 0 because media ended`); - this.startPosition = this.lastCurrentTime = 0; - this.triggerEnded(); - }; - this.playlistType = playlistType; - this.hls = hls; - this.fragmentLoader = new FragmentLoader(hls.config); - this.keyLoader = keyLoader; - this.fragmentTracker = fragmentTracker; - this.config = hls.config; - this.decrypter = new Decrypter(hls.config); - } - registerListeners() { - const { - hls - } = this; - hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this); - hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this); - hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); - hls.on(Events.MANIFEST_LOADED, this.onManifestLoaded, this); - hls.on(Events.ERROR, this.onError, this); - } - unregisterListeners() { - const { - hls - } = this; - hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this); - hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this); - hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); - hls.off(Events.MANIFEST_LOADED, this.onManifestLoaded, this); - hls.off(Events.ERROR, this.onError, this); - } - doTick() { - this.onTickEnd(); - } - onTickEnd() {} - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - startLoad(startPosition) {} - stopLoad() { - if (this.state === State.STOPPED) { - return; - } - this.fragmentLoader.abort(); - this.keyLoader.abort(this.playlistType); - const frag = this.fragCurrent; - if (frag != null && frag.loader) { - frag.abortRequests(); - this.fragmentTracker.removeFragment(frag); - } - this.resetTransmuxer(); - this.fragCurrent = null; - this.fragPrevious = null; - this.clearInterval(); - this.clearNextTick(); - this.state = State.STOPPED; - } - get startPositionValue() { - const { - nextLoadPosition, - startPosition - } = this; - if (startPosition === -1 && nextLoadPosition) { - return nextLoadPosition; - } - return startPosition; - } - get bufferingEnabled() { - return this.buffering; - } - pauseBuffering() { - this.buffering = false; - } - resumeBuffering() { - this.buffering = true; - } - _streamEnded(bufferInfo, levelDetails) { - // If playlist is live, there is another buffered range after the current range, nothing buffered, media is detached, - // of nothing loading/loaded return false - const hasTimelineOffset = this.config.timelineOffset !== undefined; - const nextStart = bufferInfo.nextStart; - const hasSecondBufferedRange = nextStart && (!hasTimelineOffset || nextStart < levelDetails.edge); - if (levelDetails.live || hasSecondBufferedRange || !bufferInfo.end || !this.media) { - 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 != null && 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); - } - getLevelDetails() { - if (this.levels && this.levelLastLoaded !== null) { - var _this$levelLastLoaded; - return (_this$levelLastLoaded = this.levelLastLoaded) == null ? void 0 : _this$levelLastLoaded.details; - } - } - onMediaAttached(event, data) { - const media = this.media = this.mediaBuffer = data.media; - media.removeEventListener('seeking', this.onMediaSeeking); - media.removeEventListener('ended', this.onMediaEnded); - media.addEventListener('seeking', this.onMediaSeeking); - media.addEventListener('ended', this.onMediaEnded); - const config = this.config; - if (this.levels && config.autoStartLoad && this.state === State.STOPPED) { - this.startLoad(config.startPosition); - } - } - onMediaDetaching(event, data) { - const transferringMedia = !!data.transferMedia; - const media = this.media; - if (media === null) { - return; - } - if (media.ended) { - this.log('MSE detaching and video ended, reset startPosition'); - this.startPosition = this.lastCurrentTime = 0; - } - - // remove video listeners - media.removeEventListener('seeking', this.onMediaSeeking); - media.removeEventListener('ended', this.onMediaEnded); - if (this.keyLoader && !transferringMedia) { - this.keyLoader.detach(); - } - this.media = this.mediaBuffer = null; - this.loopSn = undefined; - if (transferringMedia) { - this.resetTransmuxer(); - return; - } - this.loadingParts = false; - this.fragmentTracker.removeAllFragments(); - this.stopLoad(); - } - onManifestLoading() { - this.initPTS = []; - this.levels = this.levelLastLoaded = this.fragCurrent = null; - this.lastCurrentTime = this.startPosition = 0; - this.startFragRequested = false; - } - onError(event, data) {} - triggerEnded() { - /* overridden in stream-controller */ - } - onManifestLoaded(event, data) { - this.startTimeOffset = data.startTimeOffset; - } - onHandlerDestroying() { - this.stopLoad(); - if (this.transmuxer) { - this.transmuxer.destroy(); - this.transmuxer = null; - } - super.onHandlerDestroying(); - // @ts-ignore - this.hls = this.onMediaSeeking = this.onMediaEnded = null; - } - onHandlerDestroyed() { - this.state = State.STOPPED; - if (this.fragmentLoader) { - this.fragmentLoader.destroy(); - } - if (this.keyLoader) { - this.keyLoader.destroy(); - } - if (this.decrypter) { - this.decrypter.destroy(); - } - this.hls = this.log = this.warn = this.decrypter = this.keyLoader = this.fragmentLoader = this.fragmentTracker = null; - super.onHandlerDestroyed(); - } - loadFragment(frag, level, targetBufferTime) { - const config = this.hls.config; - if (config.interstitialsController && config.enableInterstitialPlayback !== false) { - // Do not load fragments outside the buffering schedule segment - const interstitials = this.hls.interstitialsManager; - const bufferingItem = interstitials == null ? void 0 : interstitials.bufferingItem; - if (bufferingItem) { - const bufferingInterstitial = bufferingItem.event; - if (bufferingInterstitial) { - // Do not stream fragments while buffering Interstitial Events (except for overlap at the start) - if (bufferingInterstitial.appendInPlace || Math.abs(frag.start - bufferingItem.start) > 1 || bufferingItem.start === 0) { - return; - } - } else { - var _level$details; - // Limit fragment loading to media in schedule item - if (frag.end <= bufferingItem.start && ((_level$details = level.details) == null ? void 0 : _level$details.live) === false) { - // fragment ends by schedule item start - return; - } - if (frag.start > bufferingItem.end && bufferingItem.nextEvent) { - // fragment is past schedule item end - return; - } - } - } - } - this.startFragRequested = true; - this._loadFragForPlayback(frag, level, targetBufferTime); - } - _loadFragForPlayback(frag, level, targetBufferTime) { - const progressCallback = data => { - if (this.fragContextChanged(frag)) { - this.warn(`Fragment ${frag.sn}${data.part ? ' p: ' + data.part.index : ''} of level ${frag.level} was dropped during download.`); - this.fragmentTracker.removeFragment(frag); - return; - } - frag.stats.chunkCount++; - this._handleFragmentLoadProgress(data); - }; - this._doFragLoad(frag, level, targetBufferTime, progressCallback).then(data => { - if (!data) { - // if we're here we probably needed to backtrack or are waiting for more parts - return; - } - const state = this.state; - if (this.fragContextChanged(frag)) { - if (state === State.FRAG_LOADING || !this.fragCurrent && state === State.PARSING) { - this.fragmentTracker.removeFragment(frag); - this.state = State.IDLE; - } - return; - } - if ('payload' in data) { - this.log(`Loaded ${frag.type} sn: ${frag.sn} of ${this.playlistLabel()} ${frag.level}`); - this.hls.trigger(Events.FRAG_LOADED, data); - } - - // Pass through the whole payload; controllers not implementing progressive loading receive data from this callback - this._handleFragmentLoadComplete(data); - }).catch(reason => { - if (this.state === State.STOPPED || this.state === State.ERROR) { - return; - } - this.warn(`Frag error: ${(reason == null ? void 0 : reason.message) || reason}`); - this.resetFragmentLoading(frag); - }); - } - clearTrackerIfNeeded(frag) { - var _this$mediaBuffer; - const { - fragmentTracker - } = this; - const fragState = fragmentTracker.getState(frag); - if (fragState === FragmentState.APPENDING) { - // Lower the max buffer length and try again - const playlistType = frag.type; - const bufferedInfo = this.getFwdBufferInfo(this.mediaBuffer, playlistType); - const minForwardBufferLength = Math.max(frag.duration, bufferedInfo ? bufferedInfo.len : this.config.maxBufferLength); - // If backtracking, always remove from the tracker without reducing max buffer length - const backtrackFragment = this.backtrackFragment; - const backtracked = backtrackFragment ? frag.sn - backtrackFragment.sn : 0; - if (backtracked === 1 || this.reduceMaxBufferLength(minForwardBufferLength, frag.duration)) { - fragmentTracker.removeFragment(frag); - } - } else if (((_this$mediaBuffer = this.mediaBuffer) == null ? void 0 : _this$mediaBuffer.buffered.length) === 0) { - // Stop gap for bad tracker / buffer flush behavior - fragmentTracker.removeAllFragments(); - } else if (fragmentTracker.hasParts(frag.type)) { - // In low latency mode, remove fragments for which only some parts were buffered - fragmentTracker.detectPartialFragments({ - frag, - part: null, - stats: frag.stats, - id: frag.type - }); - if (fragmentTracker.getState(frag) === FragmentState.PARTIAL) { - fragmentTracker.removeFragment(frag); - } - } - } - checkLiveUpdate(details) { - if (details.updated && !details.live) { - // Live stream ended, update fragment tracker - const lastFragment = details.fragments[details.fragments.length - 1]; - this.fragmentTracker.detectPartialFragments({ - frag: lastFragment, - part: null, - stats: lastFragment.stats, - id: lastFragment.type - }); - } - if (!details.fragments[0]) { - details.deltaUpdateFailed = true; - } - } - flushMainBuffer(startOffset, endOffset, type = null) { - if (!(startOffset - endOffset)) { - return; - } - // When alternate audio is playing, the audio-stream-controller is responsible for the audio buffer. Otherwise, - // passing a null type flushes both buffers - const flushScope = { - startOffset, - endOffset, - type - }; - this.hls.trigger(Events.BUFFER_FLUSHING, flushScope); - } - _loadInitSegment(frag, level) { - this._doFragLoad(frag, level).then(data => { - if (!data || this.fragContextChanged(frag) || !this.levels) { - throw new Error('init load aborted'); - } - return data; - }).then(data => { - const { - hls - } = this; - const { - payload - } = data; - const decryptData = frag.decryptdata; - - // check to see if the payload needs to be decrypted - if (payload && payload.byteLength > 0 && decryptData != null && decryptData.key && decryptData.iv && isFullSegmentEncryption(decryptData.method)) { - const startTime = self.performance.now(); - // decrypt init segment data - return this.decrypter.decrypt(new Uint8Array(payload), decryptData.key.buffer, decryptData.iv.buffer, getAesModeFromFullSegmentMethod(decryptData.method)).catch(err => { - hls.trigger(Events.ERROR, { - type: ErrorTypes.MEDIA_ERROR, - details: ErrorDetails.FRAG_DECRYPT_ERROR, - fatal: false, - error: err, - reason: err.message, - frag - }); - throw err; - }).then(decryptedData => { - const endTime = self.performance.now(); - hls.trigger(Events.FRAG_DECRYPTED, { - frag, - payload: decryptedData, - stats: { - tstart: startTime, - tdecrypt: endTime - } - }); - data.payload = decryptedData; - return this.completeInitSegmentLoad(data); - }); - } - return this.completeInitSegmentLoad(data); - }).catch(reason => { - if (this.state === State.STOPPED || this.state === State.ERROR) { - return; - } - this.warn(reason); - this.resetFragmentLoading(frag); - }); - } - completeInitSegmentLoad(data) { - const { - levels - } = this; - if (!levels) { - throw new Error('init load aborted, missing levels'); - } - const stats = data.frag.stats; - if (this.state !== State.STOPPED) { - this.state = State.IDLE; - } - data.frag.data = new Uint8Array(data.payload); - stats.parsing.start = stats.buffering.start = self.performance.now(); - stats.parsing.end = stats.buffering.end = self.performance.now(); - this.tick(); - } - fragContextChanged(frag) { - const { - fragCurrent - } = this; - return !frag || !fragCurrent || frag.sn !== fragCurrent.sn || frag.level !== fragCurrent.level; - } - fragBufferedComplete(frag, part) { - const media = this.mediaBuffer ? this.mediaBuffer : this.media; - this.log(`Buffered ${frag.type} sn: ${frag.sn}${part ? ' part: ' + part.index : ''} of ${this.fragInfo(frag)} > buffer:${media ? TimeRanges.toString(BufferHelper.getBuffered(media)) : '(detached)'})`); - if (frag.sn !== 'initSegment') { - var _this$levels; - if (frag.type !== PlaylistLevelType.SUBTITLE) { - const el = frag.elementaryStreams; - if (!Object.keys(el).some(type => !!el[type])) { - // empty segment - this.state = State.IDLE; - return; - } - } - const level = (_this$levels = this.levels) == null ? void 0 : _this$levels[frag.level]; - if (level != null && level.fragmentError) { - this.log(`Resetting level fragment error count of ${level.fragmentError} on frag buffered`); - level.fragmentError = 0; - } - } - this.state = State.IDLE; - } - _handleFragmentLoadComplete(fragLoadedEndData) { - const { - transmuxer - } = this; - if (!transmuxer) { - return; - } - const { - frag, - part, - partsLoaded - } = fragLoadedEndData; - // If we did not load parts, or loaded all parts, we have complete (not partial) fragment data - const complete = !partsLoaded || partsLoaded.length === 0 || partsLoaded.some(fragLoaded => !fragLoaded); - const chunkMeta = new ChunkMetadata(frag.level, frag.sn, frag.stats.chunkCount + 1, 0, part ? part.index : -1, !complete); - transmuxer.flush(chunkMeta); - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _handleFragmentLoadProgress(frag) {} - _doFragLoad(frag, level, targetBufferTime = null, progressCallback) { - var _frag$decryptdata; - this.fragCurrent = frag; - const details = level == null ? void 0 : level.details; - if (!this.levels || !details) { - throw new Error(`frag load aborted, missing level${details ? '' : ' detail'}s`); - } - let keyLoadingPromise = null; - if (frag.encrypted && !((_frag$decryptdata = frag.decryptdata) != null && _frag$decryptdata.key)) { - this.log(`Loading key for ${frag.sn} of [${details.startSN}-${details.endSN}], ${this.playlistLabel()} ${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 - }); - if (this.fragCurrent === null) { - keyLoadingPromise = Promise.reject(new Error(`frag load aborted, context changed in KEY_LOADING`)); - } - } else if (!frag.encrypted && details.encryptedFragments.length) { - this.keyLoader.loadClear(frag, details.encryptedFragments); - } - const fragPrevious = this.fragPrevious; - if (frag.sn !== 'initSegment' && (!fragPrevious || frag.sn !== fragPrevious.sn)) { - const shouldLoadParts = this.shouldLoadParts(level.details, frag.end); - if (shouldLoadParts !== this.loadingParts) { - this.log(`LL-Part loading ${shouldLoadParts ? 'ON' : 'OFF'} loading sn ${fragPrevious == null ? void 0 : fragPrevious.sn}->${frag.sn}`); - this.loadingParts = shouldLoadParts; - } - } - targetBufferTime = Math.max(frag.start, targetBufferTime || 0); - if (this.loadingParts && frag.sn !== 'initSegment') { - const partList = details.partList; - if (partList && progressCallback) { - if (targetBufferTime > frag.end && details.fragmentHint) { - frag = details.fragmentHint; - } - const partIndex = this.getNextPart(partList, frag, targetBufferTime); - if (partIndex > -1) { - const part = partList[partIndex]; - this.log(`Loading part sn: ${frag.sn} p: ${part.index} cc: ${frag.cc} of playlist [${details.startSN}-${details.endSN}] parts [0-${partIndex}-${partList.length - 1}] ${this.playlistLabel()}: ${frag.level}, target: ${parseFloat(targetBufferTime.toFixed(3))}`); - this.nextLoadPosition = part.start + part.duration; - this.state = State.FRAG_LOADING; - let _result; - if (keyLoadingPromise) { - _result = keyLoadingPromise.then(keyLoadedData => { - if (!keyLoadedData || this.fragContextChanged(keyLoadedData.frag)) { - return null; - } - return this.doFragPartsLoad(frag, part, level, progressCallback); - }).catch(error => this.handleFragLoadError(error)); - } else { - _result = this.doFragPartsLoad(frag, part, level, progressCallback).catch(error => this.handleFragLoadError(error)); - } - this.hls.trigger(Events.FRAG_LOADING, { - frag, - part, - targetBufferTime - }); - if (this.fragCurrent === null) { - return Promise.reject(new Error(`frag load aborted, context changed in FRAG_LOADING parts`)); - } - return _result; - } else if (!frag.url || this.loadedEndOfParts(partList, targetBufferTime)) { - // Fragment hint has no parts - return Promise.resolve(null); - } - } - } - if (frag.sn !== 'initSegment' && this.loadingParts) { - this.log(`LL-Part loading OFF after next part miss @${targetBufferTime.toFixed(2)}`); - this.loadingParts = false; - } else if (!frag.url) { - // Selected fragment hint for part but not loading parts - return Promise.resolve(null); - } - this.log(`Loading ${frag.type} sn: ${frag.sn} of ${this.fragInfo(frag, false)}) cc: ${frag.cc} ${details ? '[' + details.startSN + '-' + details.endSN + ']' : ''}, target: ${parseFloat(targetBufferTime.toFixed(3))}`); - // Don't update nextLoadPosition for fragments which are not buffered - if (isFiniteNumber(frag.sn) && !this.bitrateTest) { - this.nextLoadPosition = frag.start + frag.duration; - } - this.state = State.FRAG_LOADING; - - // Load key before streaming fragment data - const dataOnProgress = this.config.progressive; - let result; - if (dataOnProgress && keyLoadingPromise) { - result = keyLoadingPromise.then(keyLoadedData => { - if (!keyLoadedData || this.fragContextChanged(keyLoadedData == null ? void 0 : keyLoadedData.frag)) { - return null; - } - return this.fragmentLoader.load(frag, progressCallback); - }).catch(error => this.handleFragLoadError(error)); - } else { - // load unencrypted fragment data with progress event, - // or handle fragment result after key and fragment are finished loading - result = 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)); - } - this.hls.trigger(Events.FRAG_LOADING, { - frag, - targetBufferTime - }); - if (this.fragCurrent === null) { - return Promise.reject(new Error(`frag load aborted, context changed in FRAG_LOADING`)); - } - return result; - } - doFragPartsLoad(frag, fromPart, level, progressCallback) { - return new Promise((resolve, reject) => { - var _level$details2; - const partsLoaded = []; - const initialPartList = (_level$details2 = level.details) == null ? void 0 : _level$details2.partList; - const loadPart = part => { - this.fragmentLoader.loadPart(frag, part, progressCallback).then(partLoadedData => { - partsLoaded[part.index] = partLoadedData; - const loadedPart = partLoadedData.part; - this.hls.trigger(Events.FRAG_LOADED, partLoadedData); - const nextPart = getPartWith(level.details, frag.sn, part.index + 1) || findPart(initialPartList, frag.sn, part.index + 1); - if (nextPart) { - loadPart(nextPart); - } else { - return resolve({ - frag, - part: loadedPart, - partsLoaded - }); - } - }).catch(reject); - }; - loadPart(fromPart); - }); - } - handleFragLoadError(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); - } - } else { - this.hls.trigger(Events.ERROR, { - type: ErrorTypes.OTHER_ERROR, - details: ErrorDetails.INTERNAL_EXCEPTION, - err: error, - error, - fatal: true - }); - } - return null; - } - _handleTransmuxerFlush(chunkMeta) { - const context = this.getCurrentContext(chunkMeta); - if (!context || this.state !== State.PARSING) { - if (!this.fragCurrent && this.state !== State.STOPPED && this.state !== State.ERROR) { - this.state = State.IDLE; - } - return; - } - const { - frag, - part, - level - } = context; - const now = self.performance.now(); - frag.stats.parsing.end = now; - if (part) { - part.stats.parsing.end = now; - } - // See if part loading should be disabled/enabled based on buffer and playback position. - const levelDetails = this.getLevelDetails(); - const loadingPartsAtEdge = levelDetails && frag.sn > levelDetails.endSN; - const shouldLoadParts = loadingPartsAtEdge || this.shouldLoadParts(levelDetails, frag.end); - if (shouldLoadParts !== this.loadingParts) { - this.log(`LL-Part loading ${shouldLoadParts ? 'ON' : 'OFF'} after parsing segment ending @${frag.end.toFixed(2)}`); - this.loadingParts = shouldLoadParts; - } - this.updateLevelTiming(frag, part, level, chunkMeta.partial); - } - shouldLoadParts(details, bufferEnd) { - if (this.config.lowLatencyMode) { - if (!details) { - return this.loadingParts; - } - if (details != null && details.partList) { - var _details$fragmentHint; - // Buffer must be ahead of first part + duration of parts after last segment - // and playback must be at or past segment adjacent to part list - const firstPart = details.partList[0]; - const safePartStart = firstPart.end + (((_details$fragmentHint = details.fragmentHint) == null ? void 0 : _details$fragmentHint.duration) || 0); - if (bufferEnd >= safePartStart) { - var _this$media; - const playhead = this.hls.hasEnoughToStart ? ((_this$media = this.media) == null ? void 0 : _this$media.currentTime) || this.lastCurrentTime : this.getLoadPosition(); - if (playhead > firstPart.start - firstPart.fragment.duration) { - return true; - } - } - } - } - return false; - } - getCurrentContext(chunkMeta) { - const { - levels, - fragCurrent - } = this; - const { - level: levelIndex, - sn, - part: partIndex - } = chunkMeta; - if (!(levels != null && levels[levelIndex])) { - this.warn(`Levels object was unset while buffering fragment ${sn} of level ${levelIndex}. The current chunk will not be buffered.`); - return null; - } - const level = levels[levelIndex]; - const levelDetails = level.details; - const part = partIndex > -1 ? getPartWith(levelDetails, sn, partIndex) : null; - const frag = part ? part.fragment : getFragmentWithSN(levelDetails, sn, fragCurrent); - if (!frag) { - return null; - } - if (fragCurrent && fragCurrent !== frag) { - frag.stats = fragCurrent.stats; - } - return { - frag, - part, - level - }; - } - bufferFragmentData(data, frag, part, chunkMeta, noBacktracking) { - var _buffer; - if (!data || this.state !== State.PARSING) { - return; - } - const { - data1, - data2 - } = data; - let buffer = data1; - if (data1 && data2) { - // Combine the moof + mdat so that we buffer with a single append - buffer = appendUint8Array(data1, data2); - } - if (!((_buffer = buffer) != null && _buffer.length)) { - return; - } - const segment = { - type: data.type, - frag, - part, - chunkMeta, - parent: frag.type, - data: buffer - }; - this.hls.trigger(Events.BUFFER_APPENDING, segment); - if (data.dropped && data.independent && !part) { - if (noBacktracking) { - return; - } - // Clear buffer so that we reload previous segments sequentially if required - this.flushBufferGap(frag); - } - } - flushBufferGap(frag) { - const media = this.media; - if (!media) { - return; - } - // If currentTime is not buffered, clear the back buffer so that we can backtrack as much as needed - if (!BufferHelper.isBuffered(media, media.currentTime)) { - this.flushMainBuffer(0, frag.start); - return; - } - // Remove back-buffer without interrupting playback to allow back tracking - const currentTime = media.currentTime; - const bufferInfo = BufferHelper.bufferInfo(media, currentTime, 0); - const fragDuration = frag.duration; - const segmentFraction = Math.min(this.config.maxFragLookUpTolerance * 2, fragDuration * 0.25); - const start = Math.max(Math.min(frag.start - segmentFraction, bufferInfo.end - segmentFraction), currentTime + segmentFraction); - if (frag.start - start > segmentFraction) { - this.flushMainBuffer(start, frag.start); - } - } - getFwdBufferInfo(bufferable, type) { - var _this$media2; - const pos = this.getLoadPosition(); - if (!isFiniteNumber(pos)) { - return null; - } - const backwardSeek = this.lastCurrentTime > pos; - const maxBufferHole = backwardSeek || (_this$media2 = this.media) != null && _this$media2.paused ? 0 : this.config.maxBufferHole; - return this.getFwdBufferInfoAtPos(bufferable, pos, type, maxBufferHole); - } - getFwdBufferInfoAtPos(bufferable, pos, type, maxBufferHole) { - const bufferInfo = BufferHelper.bufferInfo(bufferable, pos, maxBufferHole); - // Workaround flaw in getting forward buffer when maxBufferHole is smaller than gap at current pos - if (bufferInfo.len === 0 && bufferInfo.nextStart !== undefined) { - const bufferedFragAtPos = this.fragmentTracker.getBufferedFrag(pos, type); - if (bufferedFragAtPos && (bufferInfo.nextStart <= bufferedFragAtPos.end || bufferedFragAtPos.gap)) { - return BufferHelper.bufferInfo(bufferable, pos, Math.max(bufferInfo.nextStart, maxBufferHole)); - } - } - return bufferInfo; - } - getMaxBufferLength(levelBitrate) { - const { - config - } = this; - let maxBufLen; - if (levelBitrate) { - maxBufLen = Math.max(8 * config.maxBufferSize / levelBitrate, config.maxBufferLength); - } else { - maxBufLen = config.maxBufferLength; - } - return Math.min(maxBufLen, config.maxMaxBufferLength); - } - reduceMaxBufferLength(threshold, fragDuration) { - const config = this.config; - const minLength = Math.max(Math.min(threshold - fragDuration, config.maxBufferLength), fragDuration); - const reducedLength = Math.max(threshold - fragDuration * 3, config.maxMaxBufferLength / 2, minLength); - if (reducedLength >= minLength) { - // reduce max buffer length as it might be too high. we do this to avoid loop flushing ... - config.maxMaxBufferLength = reducedLength; - this.warn(`Reduce max buffer length to ${reducedLength}s`); - return true; - } - return false; - } - getAppendedFrag(position, playlistType = PlaylistLevelType.MAIN) { - var _this$fragmentTracker; - const fragOrPart = (_this$fragmentTracker = this.fragmentTracker) == null ? void 0 : _this$fragmentTracker.getAppendedFrag(position, playlistType); - if (fragOrPart && 'fragment' in fragOrPart) { - return fragOrPart.fragment; - } - return fragOrPart; - } - getNextFragment(pos, levelDetails) { - const fragments = levelDetails.fragments; - const fragLen = fragments.length; - if (!fragLen) { - return null; - } - - // find fragment index, contiguous with end of buffer position - const { - config - } = this; - const start = fragments[0].start; - const canLoadParts = config.lowLatencyMode && !!levelDetails.partList; - let frag = null; - if (levelDetails.live) { - const initialLiveManifestSize = config.initialLiveManifestSize; - if (fragLen < initialLiveManifestSize) { - this.warn(`Not enough fragments to start playback (have: ${fragLen}, need: ${initialLiveManifestSize})`); - return null; - } - // The real fragment start times for a live stream are only known after the PTS range for that level is known. - // In order to discover the range, we load the best matching fragment for that level and demux it. - // Do not load using live logic if the starting frag is requested - we want to use getFragmentAtPosition() so that - // we get the fragment matching that start time - if (!levelDetails.PTSKnown && !this.startFragRequested && this.startPosition === -1 || pos < start) { - var _frag; - if (canLoadParts && !this.loadingParts) { - this.log(`LL-Part loading ON for initial live fragment`); - this.loadingParts = true; - } - frag = this.getInitialLiveFragment(levelDetails, fragments); - const mainStart = this.hls.startPosition; - const liveSyncPosition = this.hls.liveSyncPosition; - const startPosition = frag ? (mainStart !== -1 ? mainStart : liveSyncPosition) || frag.start : pos; - this.log(`Setting startPosition to ${startPosition} to match initial live edge. mainStart: ${mainStart} liveSyncPosition: ${liveSyncPosition} frag.start: ${(_frag = frag) == null ? void 0 : _frag.start}`); - this.startPosition = this.nextLoadPosition = startPosition; - } - } else if (pos <= start) { - // VoD playlist: if loadPosition before start of playlist, load first fragment - frag = fragments[0]; - } - - // If we haven't run into any special cases already, just load the fragment most closely matching the requested position - if (!frag) { - const end = this.loadingParts ? levelDetails.partEnd : levelDetails.fragmentEnd; - frag = this.getFragmentAtPosition(pos, end, levelDetails); - } - return this.mapToInitFragWhenRequired(frag); - } - isLoopLoading(frag, targetBufferTime) { - const trackerState = this.fragmentTracker.getState(frag); - return (trackerState === FragmentState.OK || trackerState === FragmentState.PARTIAL && !!frag.gap) && this.nextLoadPosition > targetBufferTime; - } - getNextFragmentLoopLoading(frag, levelDetails, bufferInfo, playlistType, maxBufLen) { - let nextFragment = null; - if (frag.gap) { - nextFragment = this.getNextFragment(this.nextLoadPosition, levelDetails); - if (nextFragment && !nextFragment.gap && bufferInfo.nextStart) { - // Media buffered after GAP tags should not make the next buffer timerange exceed forward buffer length - const nextbufferInfo = this.getFwdBufferInfoAtPos(this.mediaBuffer ? this.mediaBuffer : this.media, bufferInfo.nextStart, playlistType, 0); - if (nextbufferInfo !== null && bufferInfo.len + nextbufferInfo.len >= maxBufLen) { - // Returning here might result in not finding an audio and video candiate to skip to - const sn = nextFragment.sn; - if (this.loopSn !== sn) { - this.log(`buffer full after gaps in "${playlistType}" playlist starting at sn: ${sn}`); - this.loopSn = sn; - } - return null; - } - } - } - this.loopSn = undefined; - return nextFragment; - } - mapToInitFragWhenRequired(frag) { - // If an initSegment is present, it must be buffered first - if (frag != null && frag.initSegment && !(frag != null && frag.initSegment.data) && !this.bitrateTest) { - return frag.initSegment; - } - return frag; - } - getNextPart(partList, frag, targetBufferTime) { - let nextPart = -1; - let contiguous = false; - let independentAttrOmitted = true; - for (let i = 0, len = partList.length; i < len; i++) { - const part = partList[i]; - independentAttrOmitted = independentAttrOmitted && !part.independent; - if (nextPart > -1 && targetBufferTime < part.start) { - break; - } - const loaded = part.loaded; - if (loaded) { - nextPart = -1; - } else if ((contiguous || part.independent || independentAttrOmitted) && part.fragment === frag) { - nextPart = i; - } - contiguous = loaded; - } - return nextPart; - } - loadedEndOfParts(partList, targetBufferTime) { - const lastPart = partList[partList.length - 1]; - return lastPart && targetBufferTime > lastPart.start && lastPart.loaded; - } - - /* - This method is used find the best matching first fragment for a live playlist. This fragment is used to calculate the - "sliding" of the playlist, which is its offset from the start of playback. After sliding we can compute the real - start and end times for each fragment in the playlist (after which this method will not need to be called). - */ - getInitialLiveFragment(levelDetails, fragments) { - const fragPrevious = this.fragPrevious; - let frag = null; - if (fragPrevious) { - if (levelDetails.hasProgramDateTime) { - // Prefer using PDT, because it can be accurate enough to choose the correct fragment without knowing the level sliding - this.log(`Live playlist, switching playlist, load frag with same PDT: ${fragPrevious.programDateTime}`); - frag = findFragmentByPDT(fragments, fragPrevious.endProgramDateTime, this.config.maxFragLookUpTolerance); - } - if (!frag) { - // SN does not need to be accurate between renditions, but depending on the packaging it may be so. - const targetSN = fragPrevious.sn + 1; - if (targetSN >= levelDetails.startSN && targetSN <= levelDetails.endSN) { - const fragNext = fragments[targetSN - levelDetails.startSN]; - // Ensure that we're staying within the continuity range, since PTS resets upon a new range - if (fragPrevious.cc === fragNext.cc) { - frag = fragNext; - this.log(`Live playlist, switching playlist, load frag with next SN: ${frag.sn}`); - } - } - // It's important to stay within the continuity range if available; otherwise the fragments in the playlist - // will have the wrong start times - if (!frag) { - frag = findFragWithCC(fragments, fragPrevious.cc); - if (frag) { - this.log(`Live playlist, switching playlist, load frag with same CC: ${frag.sn}`); - } - } - } - } else { - // Find a new start fragment when fragPrevious is null - const liveStart = this.hls.liveSyncPosition; - if (liveStart !== null) { - frag = this.getFragmentAtPosition(liveStart, this.bitrateTest ? levelDetails.fragmentEnd : levelDetails.edge, levelDetails); - } - } - return frag; - } - - /* - This method finds the best matching fragment given the provided position. - */ - getFragmentAtPosition(bufferEnd, end, levelDetails) { - const { - config - } = this; - let { - fragPrevious - } = this; - let { - fragments, - endSN - } = levelDetails; - const { - fragmentHint - } = levelDetails; - const { - maxFragLookUpTolerance - } = config; - const partList = levelDetails.partList; - const loadingParts = !!(this.loadingParts && partList != null && partList.length && fragmentHint); - if (loadingParts && fragmentHint && !this.bitrateTest && partList[partList.length - 1].fragment.sn === fragmentHint.sn) { - // Include incomplete fragment with parts at end - fragments = fragments.concat(fragmentHint); - endSN = fragmentHint.sn; - } - let frag; - if (bufferEnd < end) { - var _this$media3; - const backwardSeek = bufferEnd < this.lastCurrentTime; - const lookupTolerance = backwardSeek || bufferEnd > end - maxFragLookUpTolerance || (_this$media3 = this.media) != null && _this$media3.paused || !this.startFragRequested ? 0 : maxFragLookUpTolerance; - // Remove the tolerance if it would put the bufferEnd past the actual end of stream - // Uses buffer and sequence number to calculate switch segment (required if using EXT-X-DISCONTINUITY-SEQUENCE) - frag = findFragmentByPTS(fragPrevious, fragments, bufferEnd, lookupTolerance); - } else { - // reach end of playlist - frag = fragments[fragments.length - 1]; - } - 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. - const fragState = this.fragmentTracker.getState(frag); - if (fragState === FragmentState.OK || fragState === FragmentState.PARTIAL && frag.gap) { - fragPrevious = frag; - } - if (fragPrevious && frag.sn === fragPrevious.sn && (!loadingParts || partList[0].fragment.sn > frag.sn || !levelDetails.live && !loadingParts)) { - // Force the next fragment to load if the previous one was already selected. This can occasionally happen with - // non-uniform fragment durations - const sameLevel = fragPrevious && frag.level === fragPrevious.level; - if (sameLevel) { - const nextFrag = fragments[curSNIdx + 1]; - if (frag.sn < endSN && this.fragmentTracker.getState(nextFrag) !== FragmentState.OK) { - frag = nextFrag; - } else { - frag = null; - } - } - } - } - return frag; - } - alignPlaylists(details, previousDetails, switchDetails) { - // TODO: If not for `shouldAlignOnDiscontinuities` requiring fragPrevious.cc, - // this could all go in level-helper mergeDetails() - const length = details.fragments.length; - if (!length) { - this.warn(`No fragments in live playlist`); - return 0; - } - const slidingStart = details.fragmentStart; - const firstLevelLoad = !previousDetails; - const aligned = details.alignedSliding && isFiniteNumber(slidingStart); - if (firstLevelLoad || !aligned && !slidingStart) { - const { - fragPrevious - } = this; - alignStream(fragPrevious, switchDetails, details); - const alignedSlidingStart = details.fragmentStart; - this.log(`Live playlist sliding: ${alignedSlidingStart.toFixed(2)} start-sn: ${previousDetails ? previousDetails.startSN : 'na'}->${details.startSN} prev-sn: ${fragPrevious ? fragPrevious.sn : 'na'} fragments: ${length}`); - return alignedSlidingStart; - } - return slidingStart; - } - waitForCdnTuneIn(details) { - // Wait for Low-Latency CDN Tune-in to get an updated playlist - const advancePartLimit = 3; - return details.live && details.canBlockReload && details.partTarget && details.tuneInGoal > Math.max(details.partHoldBack, details.partTarget * advancePartLimit); - } - setStartPosition(details, sliding) { - // compute start position if set to -1. use it straight away if value is defined - let startPosition = this.startPosition; - if (startPosition < sliding) { - startPosition = -1; - } - if (startPosition === -1) { - // Use Playlist EXT-X-START:TIME-OFFSET when set - // Prioritize Multivariant Playlist offset so that main, audio, and subtitle stream-controller start times match - const offsetInMultivariantPlaylist = this.startTimeOffset !== null; - const startTimeOffset = offsetInMultivariantPlaylist ? this.startTimeOffset : details.startTimeOffset; - if (startTimeOffset !== null && isFiniteNumber(startTimeOffset)) { - startPosition = sliding + startTimeOffset; - if (startTimeOffset < 0) { - startPosition += details.edge; - } - startPosition = Math.min(Math.max(sliding, startPosition), sliding + details.totalduration); - this.log(`Setting startPosition to ${startPosition} for start time offset ${startTimeOffset} found in ${offsetInMultivariantPlaylist ? 'multivariant' : 'media'} playlist`); - this.startPosition = startPosition; - } else if (details.live) { - // Leave this.startPosition at -1, so that we can use `getInitialLiveFragment` logic when startPosition has - // not been specified via the config or an as an argument to startLoad (#3736). - startPosition = this.hls.liveSyncPosition || sliding; - } else { - this.log(`setting startPosition to 0 by default`); - this.startPosition = startPosition = 0; - } - this.lastCurrentTime = startPosition; - } - this.nextLoadPosition = startPosition; - } - getLoadPosition() { - const { - media - } = this; - // if we have not yet loaded any fragment, start loading from start position - let pos = 0; - if (this.hls.hasEnoughToStart && media) { - pos = media.currentTime; - } else if (this.nextLoadPosition >= 0) { - pos = this.nextLoadPosition; - } - return pos; - } - handleFragLoadAborted(frag, part) { - if (this.transmuxer && frag.sn !== 'initSegment' && frag.stats.aborted) { - this.warn(`Fragment ${frag.sn}${part ? ' part ' + part.index : ''} of level ${frag.level} was aborted`); - this.resetFragmentLoading(frag); - } - } - resetFragmentLoading(frag) { - if (!this.fragCurrent || !this.fragContextChanged(frag) && this.state !== State.FRAG_LOADING_WAITING_RETRY) { - this.state = State.IDLE; - } - } - onFragmentOrKeyLoadError(filterType, data) { - if (data.chunkMeta && !data.frag) { - const context = this.getCurrentContext(data.chunkMeta); - if (context) { - data.frag = context.frag; - } - } - const frag = data.frag; - // Handle frag error related to caller's filterType - if (!frag || frag.type !== filterType || !this.levels) { - return; - } - if (this.fragContextChanged(frag)) { - var _this$fragCurrent; - this.warn(`Frag load error must match current frag to retry ${frag.url} > ${(_this$fragCurrent = this.fragCurrent) == null ? void 0 : _this$fragCurrent.url}`); - return; - } - const gapTagEncountered = data.details === ErrorDetails.FRAG_GAP; - if (gapTagEncountered) { - this.fragmentTracker.fragBuffered(frag, true); - } - // keep retrying until the limit will be reached - const errorAction = data.errorAction; - const { - action, - retryCount = 0, - retryConfig - } = errorAction || {}; - if (errorAction && action === NetworkErrorAction.RetryRequest && retryConfig) { - this.resetStartWhenNotLoaded(this.levelLastLoaded); - const delay = getRetryDelay(retryConfig, retryCount); - this.warn(`Fragment ${frag.sn} of ${filterType} ${frag.level} errored with ${data.details}, retrying loading ${retryCount + 1}/${retryConfig.maxNumRetry} in ${delay}ms`); - errorAction.resolved = true; - this.retryDate = self.performance.now() + delay; - this.state = State.FRAG_LOADING_WAITING_RETRY; - } else if (retryConfig && errorAction) { - this.resetFragmentErrors(filterType); - if (retryCount < retryConfig.maxNumRetry) { - // Network retry is skipped when level switch is preferred - if (!gapTagEncountered && action !== NetworkErrorAction.RemoveAlternatePermanently) { - errorAction.resolved = true; - } - } else { - this.warn(`${data.details} reached or exceeded max retry (${retryCount})`); - return; - } - } else if ((errorAction == null ? void 0 : errorAction.action) === NetworkErrorAction.SendAlternateToPenaltyBox) { - this.state = State.WAITING_LEVEL; - } else { - this.state = State.ERROR; - } - // Perform next async tick sooner to speed up error action resolution - this.tickImmediate(); - } - reduceLengthAndFlushBuffer(data) { - // if in appending state - if (this.state === State.PARSING || this.state === State.PARSED) { - const frag = data.frag; - const playlistType = data.parent; - const bufferedInfo = this.getFwdBufferInfo(this.mediaBuffer, playlistType); - // 0.5 : tolerance needed as some browsers stalls playback before reaching buffered end - // reduce max buf len if current position is buffered - const buffered = bufferedInfo && bufferedInfo.len > 0.5; - if (buffered) { - this.reduceMaxBufferLength(bufferedInfo.len, (frag == null ? void 0 : frag.duration) || 10); - } - const flushBuffer = !buffered; - if (flushBuffer) { - // current position is not buffered, but browser is still complaining about buffer full error - // this happens on IE/Edge, refer to https://github.com/video-dev/hls.js/pull/708 - // in that case flush the whole audio buffer to recover - this.warn(`Buffer full error while media.currentTime is not buffered, flush ${playlistType} buffer`); - } - if (frag) { - this.fragmentTracker.removeFragment(frag); - this.nextLoadPosition = frag.start; - } - this.resetLoadingState(); - return flushBuffer; - } - return false; - } - resetFragmentErrors(filterType) { - if (filterType === PlaylistLevelType.AUDIO) { - // Reset current fragment since audio track audio is essential and may not have a fail-over track - this.fragCurrent = null; - } - // Fragment errors that result in a level switch or redundant fail-over - // should reset the stream controller state to idle - if (!this.hls.hasEnoughToStart) { - this.startFragRequested = false; - } - if (this.state !== State.STOPPED) { - this.state = State.IDLE; - } - } - afterBufferFlushed(media, bufferType, playlistType) { - if (!media) { - return; - } - // After successful buffer flushing, filter flushed fragments from bufferedFrags use mediaBuffered instead of media - // (so that we will check against video.buffered ranges in case of alt audio track) - const bufferedTimeRanges = BufferHelper.getBuffered(media); - this.fragmentTracker.detectEvictedFragments(bufferType, bufferedTimeRanges, playlistType); - if (this.state === State.ENDED) { - this.resetLoadingState(); - } - } - resetLoadingState() { - this.log('Reset loading state'); - this.fragCurrent = null; - this.fragPrevious = null; - if (this.state !== State.STOPPED) { - this.state = State.IDLE; - } - } - resetStartWhenNotLoaded(level) { - // if loadedmetadata is not set, it means that first frag request failed - // in that case, reset startFragRequested flag - if (!this.hls.hasEnoughToStart) { - this.startFragRequested = false; - const details = level ? level.details : null; - if (details != null && details.live) { - // Update the start position and return to IDLE to recover live start - this.log(`resetting startPosition for live start`); - this.startPosition = -1; - this.setStartPosition(details, details.fragmentStart); - this.resetLoadingState(); - } else { - this.nextLoadPosition = this.startPosition; - } - } - } - resetWhenMissingContext(chunkMeta) { - this.warn(`The loading context changed while buffering fragment ${chunkMeta.sn} of level ${chunkMeta.level}. This chunk will not be buffered.`); - this.removeUnbufferedFrags(); - this.resetStartWhenNotLoaded(this.levelLastLoaded); - this.resetLoadingState(); - } - removeUnbufferedFrags(start = 0) { - this.fragmentTracker.removeFragmentsInRange(start, Infinity, this.playlistType, false, true); - } - updateLevelTiming(frag, part, level, partial) { - var _this$transmuxer; - const details = level.details; - if (!details) { - this.warn('level.details undefined'); - return; - } - const parsed = Object.keys(frag.elementaryStreams).reduce((result, type) => { - const info = frag.elementaryStreams[type]; - if (info) { - const parsedDuration = info.endPTS - info.startPTS; - if (parsedDuration <= 0) { - // Destroy the transmuxer after it's next time offset failed to advance because duration was <= 0. - // The new transmuxer will be configured with a time offset matching the next fragment start, - // preventing the timeline from shifting. - this.warn(`Could not parse fragment ${frag.sn} ${type} duration reliably (${parsedDuration})`); - return result || false; - } - const drift = partial ? 0 : updateFragPTSDTS(details, frag, info.startPTS, info.endPTS, info.startDTS, info.endDTS); - this.hls.trigger(Events.LEVEL_PTS_UPDATED, { - details, - level, - drift, - type, - frag, - start: info.startPTS, - end: info.endPTS - }); - return true; - } - return result; - }, false); - if (!parsed && ((_this$transmuxer = this.transmuxer) == null ? void 0 : _this$transmuxer.error) === null) { - const error = new Error(`Found no media in fragment ${frag.sn} of level ${frag.level} resetting transmuxer to fallback to playlist timing`); - if (level.fragmentError === 0) { - // Mark and track the odd empty segment as a gap to avoid reloading - level.fragmentError++; - frag.gap = true; - this.fragmentTracker.removeFragment(frag); - this.fragmentTracker.fragBuffered(frag, true); - } - this.warn(error.message); - this.hls.trigger(Events.ERROR, { - type: ErrorTypes.MEDIA_ERROR, - details: ErrorDetails.FRAG_PARSING_ERROR, - fatal: false, - error, - frag, - reason: `Found no media in msn ${frag.sn} of level "${level.url}"` - }); - if (!this.hls) { - return; - } - this.resetTransmuxer(); - // For this error fallthrough. Marking parsed will allow advancing to next fragment. - } - this.state = State.PARSED; - this.log(`Parsed ${frag.type} sn: ${frag.sn}${part ? ' part: ' + part.index : ''} of ${this.fragInfo(frag)})`); - this.hls.trigger(Events.FRAG_PARSED, { - frag, - part - }); - } - playlistLabel() { - return this.playlistType === PlaylistLevelType.MAIN ? 'level' : 'track'; - } - fragInfo(frag, pts = true) { - var _ref, _ref2; - return `${this.playlistLabel()} ${frag.level} (frag:[${((_ref = pts ? frag.startPTS : frag.start) != null ? _ref : NaN).toFixed(3)}-${((_ref2 = pts ? frag.endPTS : frag.end) != null ? _ref2 : NaN).toFixed(3)}]`; - } - resetTransmuxer() { - var _this$transmuxer2; - (_this$transmuxer2 = this.transmuxer) == null ? void 0 : _this$transmuxer2.reset(); - } - recoverWorkerError(data) { - if (data.event === 'demuxerWorker') { - this.fragmentTracker.removeAllFragments(); - if (this.transmuxer) { - this.transmuxer.destroy(); - this.transmuxer = null; - } - this.resetStartWhenNotLoaded(this.levelLastLoaded); - this.resetLoadingState(); - } - } - set state(nextState) { - const previousState = this._state; - if (previousState !== nextState) { - this._state = nextState; - this.log(`${previousState}->${nextState}`); - } - } - get state() { - return this._state; - } -} - -function getSourceBuffer() { - return self.SourceBuffer || self.WebKitSourceBuffer; -} -function isMSESupported() { - const mediaSource = getMediaSource(); - if (!mediaSource) { - return false; - } - - // if SourceBuffer is exposed ensure its API is valid - // Older browsers do not expose SourceBuffer globally so checking SourceBuffer.prototype is impossible - const sourceBuffer = getSourceBuffer(); - return !sourceBuffer || sourceBuffer.prototype && typeof sourceBuffer.prototype.appendBuffer === 'function' && typeof sourceBuffer.prototype.remove === 'function'; -} -function isSupported() { - if (!isMSESupported()) { - return false; - } - const mediaSource = getMediaSource(); - return typeof (mediaSource == null ? void 0 : mediaSource.isTypeSupported) === 'function' && (['avc1.42E01E,mp4a.40.2', 'av01.0.01M.08', 'vp09.00.50.08'].some(codecsForVideoContainer => mediaSource.isTypeSupported(mimeTypeForCodec(codecsForVideoContainer, 'video'))) || ['mp4a.40.2', 'fLaC'].some(codecForAudioContainer => mediaSource.isTypeSupported(mimeTypeForCodec(codecForAudioContainer, 'audio')))); -} -function changeTypeSupported() { - var _sourceBuffer$prototy; - const sourceBuffer = getSourceBuffer(); - return typeof (sourceBuffer == null ? void 0 : (_sourceBuffer$prototy = sourceBuffer.prototype) == null ? void 0 : _sourceBuffer$prototy.changeType) === 'function'; -} - -const version = undefined; - -// ensure the worker ends up in the bundle -// If the worker should not be included this gets aliased to empty.js -const workerStore = {}; -function hasUMDWorker() { - return typeof __HLS_WORKER_BUNDLE__ === 'function'; -} -function injectWorker() { - const workerContext = workerStore[version]; - if (workerContext) { - workerContext.clientCount++; - return workerContext; - } - const blob = new self.Blob([`var exports={};var module={exports:exports};function define(f){f()};define.amd=true;(${__HLS_WORKER_BUNDLE__.toString()})(true);`], { - type: 'text/javascript' - }); - const objectURL = self.URL.createObjectURL(blob); - const worker = new self.Worker(objectURL); - const result = { - worker, - objectURL, - clientCount: 1 - }; - workerStore[version] = result; - return result; -} -function loadWorker(path) { - const workerContext = workerStore[path]; - if (workerContext) { - workerContext.clientCount++; - return workerContext; - } - const scriptURL = new self.URL(path, self.location.href).href; - const worker = new self.Worker(scriptURL); - const result = { - worker, - scriptURL, - clientCount: 1 - }; - workerStore[path] = result; - return result; -} -function removeWorkerFromStore(path) { - const workerContext = workerStore[path || version]; - if (workerContext) { - const clientCount = workerContext.clientCount--; - if (clientCount === 1) { - const { - worker, - objectURL - } = workerContext; - delete workerStore[path || version]; - if (objectURL) { - // revoke the Object URL that was used to create transmuxer worker, so as not to leak it - self.URL.revokeObjectURL(objectURL); - } - worker.terminate(); - } - } -} - -function dummyTrack(type = '', inputTimeScale = 90000) { - return { - type, - id: -1, - pid: -1, - inputTimeScale, - sequenceNumber: -1, - samples: [], - dropped: 0 - }; -} - -/** - * Returns any adjacent ID3 tags found in data starting at offset, as one block of data - * - * @param data - The data to search in - * @param offset - The offset at which to start searching - * - * @returns The block of data containing any ID3 tags found - * or `undefined` if no header is found at the starting offset - * - * @internal - * - * @group ID3 - */ -function getId3Data(data, offset) { - const front = offset; - let length = 0; - while (isId3Header(data, offset)) { - // ID3 header is 10 bytes - length += 10; - const size = readId3Size(data, offset + 6); - length += size; - if (isId3Footer(data, offset + 10)) { - // ID3 footer is 10 bytes - length += 10; - } - offset += length; - } - if (length > 0) { - return data.subarray(front, front + length); - } - return undefined; -} - -/** - * Read a 33 bit timestamp from an ID3 frame. - * - * @param timeStampFrame - the ID3 frame - * - * @returns The timestamp - * - * @internal - * - * @group ID3 - */ -function readId3Timestamp(timeStampFrame) { - if (timeStampFrame.data.byteLength === 8) { - const data = new Uint8Array(timeStampFrame.data); - // timestamp is 33 bit expressed as a big-endian eight-octet number, - // with the upper 31 bits set to zero. - const pts33Bit = data[3] & 0x1; - let timestamp = (data[4] << 23) + (data[5] << 15) + (data[6] << 7) + data[7]; - timestamp /= 45; - if (pts33Bit) { - timestamp += 47721858.84; - } // 2^32 / 90 - return Math.round(timestamp); - } - return undefined; -} - -/** - * Searches for the Elementary Stream timestamp found in the ID3 data chunk - * - * @param data - Block of data containing one or more ID3 tags - * - * @returns The timestamp - * - * @group ID3 - * - * @beta - */ -function getId3Timestamp(data) { - const frames = getId3Frames(data); - for (let i = 0; i < frames.length; i++) { - const frame = frames[i]; - if (isId3TimestampFrame(frame)) { - return readId3Timestamp(frame); - } - } - return undefined; -} - -/** - * Checks if the given data contains an ID3 tag. - * - * @param data - The data to check - * @param offset - The offset at which to start checking - * - * @returns `true` if an ID3 tag is found - * - * @group ID3 - * - * @beta - */ -function canParseId3(data, offset) { - return isId3Header(data, offset) && readId3Size(data, offset + 6) + 10 <= data.length - offset; -} - -class BaseAudioDemuxer { - constructor() { - this._audioTrack = void 0; - this._id3Track = void 0; - this.frameIndex = 0; - this.cachedData = null; - this.basePTS = null; - this.initPTS = null; - this.lastPTS = null; - } - resetInitSegment(initSegment, audioCodec, videoCodec, trackDuration) { - this._id3Track = { - type: 'id3', - id: 3, - pid: -1, - inputTimeScale: 90000, - sequenceNumber: 0, - samples: [], - dropped: 0 - }; - } - resetTimeStamp(deaultTimestamp) { - this.initPTS = deaultTimestamp; - this.resetContiguity(); - } - resetContiguity() { - this.basePTS = null; - this.lastPTS = null; - this.frameIndex = 0; - } - canParse(data, offset) { - return false; - } - appendFrame(track, data, offset) {} - - // feed incoming data to the front of the parsing pipeline - demux(data, timeOffset) { - if (this.cachedData) { - data = appendUint8Array(this.cachedData, data); - this.cachedData = null; - } - let id3Data = getId3Data(data, 0); - let offset = id3Data ? id3Data.length : 0; - let lastDataIndex; - const track = this._audioTrack; - const id3Track = this._id3Track; - const timestamp = id3Data ? getId3Timestamp(id3Data) : undefined; - const length = data.length; - if (this.basePTS === null || this.frameIndex === 0 && isFiniteNumber(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.lastPTS, - dts: this.lastPTS, - data: id3Data, - type: MetadataSchema.audioId3, - duration: Number.POSITIVE_INFINITY - }); - } - while (offset < length) { - if (this.canParse(data, offset)) { - const frame = this.appendFrame(track, data, offset); - if (frame) { - this.frameIndex++; - this.lastPTS = frame.sample.pts; - offset += frame.length; - lastDataIndex = offset; - } else { - offset = length; - } - } else if (canParseId3(data, offset)) { - // after a canParse, a call to getId3Data *should* always returns some data - id3Data = getId3Data(data, offset); - id3Track.samples.push({ - pts: this.lastPTS, - dts: this.lastPTS, - data: id3Data, - type: MetadataSchema.audioId3, - duration: Number.POSITIVE_INFINITY - }); - offset += id3Data.length; - lastDataIndex = offset; - } else { - offset++; - } - if (offset === length && lastDataIndex !== length) { - const partialData = sliceUint8(data, lastDataIndex); - if (this.cachedData) { - this.cachedData = appendUint8Array(this.cachedData, partialData); - } else { - this.cachedData = partialData; - } - } - } - return { - audioTrack: track, - videoTrack: dummyTrack(), - id3Track, - textTrack: dummyTrack() - }; - } - demuxSampleAes(data, keyData, timeOffset) { - return Promise.reject(new Error(`[${this}] This demuxer does not support Sample-AES decryption`)); - } - flush(timeOffset) { - // Parse cache in case of remaining frames. - const cachedData = this.cachedData; - if (cachedData) { - this.cachedData = null; - this.demux(cachedData, 0); - } - return { - audioTrack: this._audioTrack, - videoTrack: dummyTrack(), - id3Track: this._id3Track, - textTrack: dummyTrack() - }; - } - destroy() { - this.cachedData = null; - // @ts-ignore - this._audioTrack = this._id3Track = undefined; - } -} - -/** - * Initialize PTS - * <p> - * use timestamp unless it is undefined, NaN or Infinity - * </p> - */ -const initPTSFn = (timestamp, timeOffset, initPTS) => { - if (isFiniteNumber(timestamp)) { - return timestamp * 90; - } - const init90kHz = initPTS ? initPTS.baseTime * 90000 / initPTS.timescale : 0; - return timeOffset * 90000 + init90kHz; -}; - -function getAudioConfig(observer, data, offset, manifestCodec) { - const adtsSamplingRates = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350]; - const byte2 = data[offset + 2]; - const adtsSamplingIndex = byte2 >> 2 & 0xf; - if (adtsSamplingIndex > 12) { - const error = new Error(`invalid ADTS sampling index:${adtsSamplingIndex}`); - observer.emit(Events.ERROR, Events.ERROR, { - type: ErrorTypes.MEDIA_ERROR, - details: ErrorDetails.FRAG_PARSING_ERROR, - fatal: true, - error, - reason: error.message - }); - return; - } - // MPEG-4 Audio Object Type (profile_ObjectType+1) - const adtsObjectType = (byte2 >> 6 & 0x3) + 1; - const channelCount = data[offset + 3] >> 6 & 0x3 | (byte2 & 1) << 2; - const codec = 'mp4a.40.' + adtsObjectType; - /* refer to http://wiki.multimedia.cx/index.php?title=MPEG-4_Audio#Audio_Specific_Config - ISO/IEC 14496-3 - Table 1.13 — Syntax of AudioSpecificConfig() - Audio Profile / Audio Object Type - 0: Null - 1: AAC Main - 2: AAC LC (Low Complexity) - 3: AAC SSR (Scalable Sample Rate) - 4: AAC LTP (Long Term Prediction) - 5: SBR (Spectral Band Replication) - 6: AAC Scalable - sampling freq - 0: 96000 Hz - 1: 88200 Hz - 2: 64000 Hz - 3: 48000 Hz - 4: 44100 Hz - 5: 32000 Hz - 6: 24000 Hz - 7: 22050 Hz - 8: 16000 Hz - 9: 12000 Hz - 10: 11025 Hz - 11: 8000 Hz - 12: 7350 Hz - 13: Reserved - 14: Reserved - 15: frequency is written explictly - Channel Configurations - These are the channel configurations: - 0: Defined in AOT Specifc Config - 1: 1 channel: front-center - 2: 2 channels: front-left, front-right - */ - // audioObjectType = profile => profile, the MPEG-4 Audio Object Type minus 1 - const samplerate = adtsSamplingRates[adtsSamplingIndex]; - let aacSampleIndex = adtsSamplingIndex; - if (adtsObjectType === 5 || adtsObjectType === 29) { - // HE-AAC uses SBR (Spectral Band Replication) , high frequencies are constructed from low frequencies - // there is a factor 2 between frame sample rate and output sample rate - // multiply frequency by 2 (see table above, equivalent to substract 3) - aacSampleIndex -= 3; - } - const config = [adtsObjectType << 3 | (aacSampleIndex & 0x0e) >> 1, (aacSampleIndex & 0x01) << 7 | channelCount << 3]; - logger.log(`manifest codec:${manifestCodec}, parsed codec:${codec}, channels:${channelCount}, rate:${samplerate} (ADTS object type:${adtsObjectType} sampling index:${adtsSamplingIndex})`); - return { - config, - samplerate, - channelCount, - codec, - parsedCodec: codec, - manifestCodec - }; -} -function isHeaderPattern$1(data, offset) { - return data[offset] === 0xff && (data[offset + 1] & 0xf6) === 0xf0; -} -function getHeaderLength(data, offset) { - return data[offset + 1] & 0x01 ? 7 : 9; -} -function getFullFrameLength(data, offset) { - return (data[offset + 3] & 0x03) << 11 | data[offset + 4] << 3 | (data[offset + 5] & 0xe0) >>> 5; -} -function canGetFrameLength(data, offset) { - return offset + 5 < data.length; -} -function isHeader$1(data, offset) { - // Look for ADTS header | 1111 1111 | 1111 X00X | where X can be either 0 or 1 - // Layer bits (position 14 and 15) in header should be always 0 for ADTS - // More info https://wiki.multimedia.cx/index.php?title=ADTS - return offset + 1 < data.length && isHeaderPattern$1(data, offset); -} -function canParse$1(data, offset) { - return canGetFrameLength(data, offset) && isHeaderPattern$1(data, offset) && getFullFrameLength(data, offset) <= data.length - offset; -} -function probe$1(data, offset) { - // same as isHeader but we also check that ADTS frame follows last ADTS frame - // or end of data is reached - if (isHeader$1(data, offset)) { - // ADTS header Length - const headerLength = getHeaderLength(data, offset); - if (offset + headerLength >= data.length) { - return false; - } - // ADTS frame Length - const frameLength = getFullFrameLength(data, offset); - if (frameLength <= headerLength) { - return false; - } - const newOffset = offset + frameLength; - return newOffset === data.length || isHeader$1(data, newOffset); - } - return false; -} -function initTrackConfig(track, observer, data, offset, audioCodec) { - if (!track.samplerate) { - const config = getAudioConfig(observer, data, offset, audioCodec); - if (!config) { - return; - } - _extends(track, config); - } -} -function getFrameDuration(samplerate) { - return 1024 * 90000 / samplerate; -} -function parseFrameHeader(data, offset) { - // The protection skip bit tells us if we have 2 bytes of CRC data at the end of the ADTS header - const headerLength = getHeaderLength(data, offset); - if (offset + headerLength <= data.length) { - // retrieve frame size - const frameLength = getFullFrameLength(data, offset) - headerLength; - if (frameLength > 0) { - // logger.log(`AAC frame, offset/length/total/pts:${offset+headerLength}/${frameLength}/${data.byteLength}`); - return { - headerLength, - frameLength - }; - } - } -} -function appendFrame$2(track, data, offset, pts, frameIndex) { - const frameDuration = getFrameDuration(track.samplerate); - const stamp = pts + frameIndex * frameDuration; - const header = parseFrameHeader(data, offset); - let unit; - if (header) { - const { - frameLength, - headerLength - } = header; - const _length = headerLength + frameLength; - const missing = Math.max(0, offset + _length - data.length); - // logger.log(`AAC frame ${frameIndex}, pts:${stamp} length@offset/total: ${frameLength}@${offset+headerLength}/${data.byteLength} missing: ${missing}`); - if (missing) { - unit = new Uint8Array(_length - headerLength); - unit.set(data.subarray(offset + headerLength, data.length), 0); - } else { - unit = data.subarray(offset + headerLength, offset + _length); - } - const _sample = { - unit, - pts: stamp - }; - if (!missing) { - track.samples.push(_sample); - } - return { - sample: _sample, - length: _length, - missing - }; - } - // overflow incomplete header - const length = data.length - offset; - unit = new Uint8Array(length); - unit.set(data.subarray(offset, data.length), 0); - const sample = { - unit, - pts: stamp - }; - return { - sample, - length, - missing: -1 - }; -} - -/** - * MPEG parser helper - */ - -let chromeVersion$1 = null; -const BitratesMap = [32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160]; -const SamplingRateMap = [44100, 48000, 32000, 22050, 24000, 16000, 11025, 12000, 8000]; -const SamplesCoefficients = [ -// MPEG 2.5 -[0, -// Reserved -72, -// Layer3 -144, -// Layer2 -12 // Layer1 -], -// Reserved -[0, -// Reserved -0, -// Layer3 -0, -// Layer2 -0 // Layer1 -], -// MPEG 2 -[0, -// Reserved -72, -// Layer3 -144, -// Layer2 -12 // Layer1 -], -// MPEG 1 -[0, -// Reserved -144, -// Layer3 -144, -// Layer2 -12 // Layer1 -]]; -const BytesInSlot = [0, -// Reserved -1, -// Layer3 -1, -// Layer2 -4 // Layer1 -]; -function appendFrame$1(track, data, offset, pts, frameIndex) { - // Using http://www.datavoyage.com/mpgscript/mpeghdr.htm as a reference - if (offset + 24 > data.length) { - return; - } - const header = parseHeader(data, offset); - if (header && offset + header.frameLength <= data.length) { - const frameDuration = header.samplesPerFrame * 90000 / header.sampleRate; - const stamp = pts + frameIndex * frameDuration; - const sample = { - unit: data.subarray(offset, offset + header.frameLength), - pts: stamp, - dts: stamp - }; - track.config = []; - track.channelCount = header.channelCount; - track.samplerate = header.sampleRate; - track.samples.push(sample); - return { - sample, - length: header.frameLength, - missing: 0 - }; - } -} -function parseHeader(data, offset) { - const mpegVersion = data[offset + 1] >> 3 & 3; - const mpegLayer = data[offset + 1] >> 1 & 3; - const bitRateIndex = data[offset + 2] >> 4 & 15; - const sampleRateIndex = data[offset + 2] >> 2 & 3; - if (mpegVersion !== 1 && bitRateIndex !== 0 && bitRateIndex !== 15 && sampleRateIndex !== 3) { - const paddingBit = data[offset + 2] >> 1 & 1; - const channelMode = data[offset + 3] >> 6; - const columnInBitrates = mpegVersion === 3 ? 3 - mpegLayer : mpegLayer === 3 ? 3 : 4; - const bitRate = BitratesMap[columnInBitrates * 14 + bitRateIndex - 1] * 1000; - const columnInSampleRates = mpegVersion === 3 ? 0 : mpegVersion === 2 ? 1 : 2; - const sampleRate = SamplingRateMap[columnInSampleRates * 3 + sampleRateIndex]; - const channelCount = channelMode === 3 ? 1 : 2; // If bits of channel mode are `11` then it is a single channel (Mono) - const sampleCoefficient = SamplesCoefficients[mpegVersion][mpegLayer]; - const bytesInSlot = BytesInSlot[mpegLayer]; - const samplesPerFrame = sampleCoefficient * 8 * bytesInSlot; - const frameLength = Math.floor(sampleCoefficient * bitRate / sampleRate + paddingBit) * bytesInSlot; - if (chromeVersion$1 === null) { - const userAgent = navigator.userAgent || ''; - const result = userAgent.match(/Chrome\/(\d+)/i); - chromeVersion$1 = result ? parseInt(result[1]) : 0; - } - const needChromeFix = !!chromeVersion$1 && chromeVersion$1 <= 87; - if (needChromeFix && mpegLayer === 2 && bitRate >= 224000 && channelMode === 0) { - // Work around bug in Chromium by setting channelMode to dual-channel (01) instead of stereo (00) - data[offset + 3] = data[offset + 3] | 0x80; - } - return { - sampleRate, - channelCount, - frameLength, - samplesPerFrame - }; - } -} -function isHeaderPattern(data, offset) { - return data[offset] === 0xff && (data[offset + 1] & 0xe0) === 0xe0 && (data[offset + 1] & 0x06) !== 0x00; -} -function isHeader(data, offset) { - // Look for MPEG header | 1111 1111 | 111X XYZX | where X can be either 0 or 1 and Y or Z should be 1 - // Layer bits (position 14 and 15) in header should be always different from 0 (Layer I or Layer II or Layer III) - // More info http://www.mp3-tech.org/programmer/frame_header.html - return offset + 1 < data.length && isHeaderPattern(data, offset); -} -function canParse(data, offset) { - const headerSize = 4; - return isHeaderPattern(data, offset) && headerSize <= data.length - offset; -} -function probe(data, offset) { - // same as isHeader but we also check that MPEG frame follows last MPEG frame - // or end of data is reached - if (offset + 1 < data.length && isHeaderPattern(data, offset)) { - // MPEG header Length - const headerLength = 4; - // MPEG frame Length - const header = parseHeader(data, offset); - let frameLength = headerLength; - if (header != null && header.frameLength) { - frameLength = header.frameLength; - } - const newOffset = offset + frameLength; - return newOffset === data.length || isHeader(data, newOffset); - } - return false; -} - -/** - * AAC demuxer - */ -class AACDemuxer extends BaseAudioDemuxer { - constructor(observer, config) { - super(); - this.observer = void 0; - this.config = void 0; - this.observer = observer; - this.config = config; - } - resetInitSegment(initSegment, audioCodec, videoCodec, trackDuration) { - super.resetInitSegment(initSegment, audioCodec, videoCodec, trackDuration); - this._audioTrack = { - container: 'audio/adts', - type: 'audio', - id: 2, - pid: -1, - sequenceNumber: 0, - segmentCodec: 'aac', - samples: [], - manifestCodec: audioCodec, - duration: trackDuration, - inputTimeScale: 90000, - dropped: 0 - }; - } - - // Source for probe info - https://wiki.multimedia.cx/index.php?title=ADTS - static probe(data, logger) { - if (!data) { - return false; - } - - // Check for the ADTS sync word - // Look for ADTS header | 1111 1111 | 1111 X00X | where X can be either 0 or 1 - // Layer bits (position 14 and 15) in header should be always 0 for ADTS - // More info https://wiki.multimedia.cx/index.php?title=ADTS - const id3Data = getId3Data(data, 0); - let offset = (id3Data == null ? void 0 : id3Data.length) || 0; - if (probe(data, offset)) { - return false; - } - for (let length = data.length; offset < length; offset++) { - if (probe$1(data, offset)) { - logger.log('ADTS sync word found !'); - return true; - } - } - return false; - } - canParse(data, offset) { - return canParse$1(data, offset); - } - appendFrame(track, data, offset) { - initTrackConfig(track, this.observer, data, offset, track.manifestCodec); - const frame = appendFrame$2(track, data, offset, this.basePTS, this.frameIndex); - if (frame && frame.missing === 0) { - return frame; - } - } -} - -const emsgSchemePattern = /\/emsg[-/]ID3/i; -class MP4Demuxer { - constructor(observer, config) { - this.remainderData = null; - this.timeOffset = 0; - this.config = void 0; - this.videoTrack = void 0; - this.audioTrack = void 0; - this.id3Track = void 0; - this.txtTrack = void 0; - this.config = config; - } - resetTimeStamp() {} - resetInitSegment(initSegment, audioCodec, videoCodec, trackDuration) { - const videoTrack = this.videoTrack = dummyTrack('video', 1); - const audioTrack = this.audioTrack = dummyTrack('audio', 1); - const captionTrack = this.txtTrack = dummyTrack('text', 1); - this.id3Track = dummyTrack('id3', 1); - this.timeOffset = 0; - if (!(initSegment != null && initSegment.byteLength)) { - return; - } - const initData = parseInitSegment(initSegment); - if (initData.video) { - const { - id, - timescale, - codec - } = initData.video; - videoTrack.id = id; - videoTrack.timescale = captionTrack.timescale = timescale; - videoTrack.codec = codec; - } - if (initData.audio) { - const { - id, - timescale, - codec - } = initData.audio; - audioTrack.id = id; - audioTrack.timescale = timescale; - audioTrack.codec = codec; - } - captionTrack.id = RemuxerTrackIdConfig.text; - videoTrack.sampleDuration = 0; - videoTrack.duration = audioTrack.duration = trackDuration; - } - resetContiguity() { - this.remainderData = null; - } - static probe(data) { - return hasMoofData(data); - } - demux(data, timeOffset) { - this.timeOffset = timeOffset; - // Load all data into the avc track. The CMAF remuxer will look for the data in the samples object; the rest of the fields do not matter - let videoSamples = data; - const videoTrack = this.videoTrack; - const textTrack = this.txtTrack; - if (this.config.progressive) { - // Split the bytestream into two ranges: one encompassing all data up until the start of the last moof, and everything else. - // This is done to guarantee that we're sending valid data to MSE - when demuxing progressively, we have no guarantee - // that the fetch loader gives us flush moof+mdat pairs. If we push jagged data to MSE, it will throw an exception. - if (this.remainderData) { - videoSamples = appendUint8Array(this.remainderData, data); - } - const segmentedData = segmentValidRange(videoSamples); - this.remainderData = segmentedData.remainder; - videoTrack.samples = segmentedData.valid || new Uint8Array(); - } else { - videoTrack.samples = videoSamples; - } - const id3Track = this.extractID3Track(videoTrack, timeOffset); - textTrack.samples = parseSamples(timeOffset, videoTrack); - return { - videoTrack, - audioTrack: this.audioTrack, - id3Track, - textTrack: this.txtTrack - }; - } - flush() { - const timeOffset = this.timeOffset; - const videoTrack = this.videoTrack; - const textTrack = this.txtTrack; - videoTrack.samples = this.remainderData || new Uint8Array(); - this.remainderData = null; - const id3Track = this.extractID3Track(videoTrack, this.timeOffset); - textTrack.samples = parseSamples(timeOffset, videoTrack); - return { - videoTrack, - audioTrack: dummyTrack(), - id3Track, - textTrack: dummyTrack() - }; - } - extractID3Track(videoTrack, timeOffset) { - const id3Track = this.id3Track; - if (videoTrack.samples.length) { - const emsgs = findBox(videoTrack.samples, ['emsg']); - if (emsgs) { - emsgs.forEach(data => { - const emsgInfo = parseEmsg(data); - if (emsgSchemePattern.test(emsgInfo.schemeIdUri)) { - const pts = getEmsgStartTime(emsgInfo, timeOffset); - 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, - len: payload.byteLength, - dts: pts, - pts: pts, - type: MetadataSchema.emsg, - duration: duration - }); - } else if (this.config.enableEmsgKLVMetadata && emsgInfo.schemeIdUri.startsWith('urn:misb:KLV:bin:1910.1')) { - const pts = getEmsgStartTime(emsgInfo, timeOffset); - id3Track.samples.push({ - data: emsgInfo.payload, - len: emsgInfo.payload.byteLength, - dts: pts, - pts: pts, - type: MetadataSchema.misbklv, - duration: Number.POSITIVE_INFINITY - }); - } - }); - } - } - return id3Track; - } - demuxSampleAes(data, keyData, timeOffset) { - return Promise.reject(new Error('The MP4 demuxer does not support SAMPLE-AES decryption')); - } - destroy() { - // @ts-ignore - this.config = null; - this.remainderData = null; - this.videoTrack = this.audioTrack = this.id3Track = this.txtTrack = undefined; - } -} -function getEmsgStartTime(emsgInfo, timeOffset) { - return isFiniteNumber(emsgInfo.presentationTime) ? emsgInfo.presentationTime / emsgInfo.timeScale : timeOffset + emsgInfo.presentationTimeDelta / emsgInfo.timeScale; -} - -const getAudioBSID = (data, offset) => { - // check the bsid to confirm ac-3 | ec-3 - let bsid = 0; - let numBits = 5; - offset += numBits; - const temp = new Uint32Array(1); // unsigned 32 bit for temporary storage - const mask = new Uint32Array(1); // unsigned 32 bit mask value - const byte = new Uint8Array(1); // unsigned 8 bit for temporary storage - while (numBits > 0) { - byte[0] = data[offset]; - // read remaining bits, upto 8 bits at a time - const bits = Math.min(numBits, 8); - const shift = 8 - bits; - mask[0] = 0xff000000 >>> 24 + shift << shift; - temp[0] = (byte[0] & mask[0]) >> shift; - bsid = !bsid ? temp[0] : bsid << bits | temp[0]; - offset += 1; - numBits -= bits; - } - return bsid; -}; - -class AC3Demuxer extends BaseAudioDemuxer { - constructor(observer) { - super(); - this.observer = void 0; - this.observer = observer; - } - resetInitSegment(initSegment, audioCodec, videoCodec, trackDuration) { - super.resetInitSegment(initSegment, audioCodec, videoCodec, trackDuration); - this._audioTrack = { - container: 'audio/ac-3', - type: 'audio', - id: 2, - pid: -1, - sequenceNumber: 0, - segmentCodec: 'ac3', - samples: [], - manifestCodec: audioCodec, - duration: trackDuration, - inputTimeScale: 90000, - dropped: 0 - }; - } - canParse(data, offset) { - return offset + 64 < data.length; - } - appendFrame(track, data, offset) { - const frameLength = appendFrame(track, data, offset, this.basePTS, this.frameIndex); - if (frameLength !== -1) { - const sample = track.samples[track.samples.length - 1]; - return { - sample, - length: frameLength, - missing: 0 - }; - } - } - static probe(data) { - if (!data) { - return false; - } - const id3Data = getId3Data(data, 0); - if (!id3Data) { - return false; - } - - // look for the ac-3 sync bytes - const offset = id3Data.length; - if (data[offset] === 0x0b && data[offset + 1] === 0x77 && getId3Timestamp(id3Data) !== undefined && - // check the bsid to confirm ac-3 - getAudioBSID(data, offset) < 16) { - return true; - } - return false; - } -} -function appendFrame(track, data, start, pts, frameIndex) { - if (start + 8 > data.length) { - return -1; // not enough bytes left - } - if (data[start] !== 0x0b || data[start + 1] !== 0x77) { - return -1; // invalid magic - } - - // get sample rate - const samplingRateCode = data[start + 4] >> 6; - if (samplingRateCode >= 3) { - return -1; // invalid sampling rate - } - const samplingRateMap = [48000, 44100, 32000]; - const sampleRate = samplingRateMap[samplingRateCode]; - - // get frame size - const frameSizeCode = data[start + 4] & 0x3f; - const frameSizeMap = [64, 69, 96, 64, 70, 96, 80, 87, 120, 80, 88, 120, 96, 104, 144, 96, 105, 144, 112, 121, 168, 112, 122, 168, 128, 139, 192, 128, 140, 192, 160, 174, 240, 160, 175, 240, 192, 208, 288, 192, 209, 288, 224, 243, 336, 224, 244, 336, 256, 278, 384, 256, 279, 384, 320, 348, 480, 320, 349, 480, 384, 417, 576, 384, 418, 576, 448, 487, 672, 448, 488, 672, 512, 557, 768, 512, 558, 768, 640, 696, 960, 640, 697, 960, 768, 835, 1152, 768, 836, 1152, 896, 975, 1344, 896, 976, 1344, 1024, 1114, 1536, 1024, 1115, 1536, 1152, 1253, 1728, 1152, 1254, 1728, 1280, 1393, 1920, 1280, 1394, 1920]; - const frameLength = frameSizeMap[frameSizeCode * 3 + samplingRateCode] * 2; - if (start + frameLength > data.length) { - return -1; - } - - // get channel count - const channelMode = data[start + 6] >> 5; - let skipCount = 0; - if (channelMode === 2) { - skipCount += 2; - } else { - if (channelMode & 1 && channelMode !== 1) { - skipCount += 2; - } - if (channelMode & 4) { - skipCount += 2; - } - } - const lfeon = (data[start + 6] << 8 | data[start + 7]) >> 12 - skipCount & 1; - const channelsMap = [2, 1, 2, 3, 3, 4, 4, 5]; - const channelCount = channelsMap[channelMode] + lfeon; - - // build dac3 box - const bsid = data[start + 5] >> 3; - const bsmod = data[start + 5] & 7; - const config = new Uint8Array([samplingRateCode << 6 | bsid << 1 | bsmod >> 2, (bsmod & 3) << 6 | channelMode << 3 | lfeon << 2 | frameSizeCode >> 4, frameSizeCode << 4 & 0xe0]); - const frameDuration = 1536 / sampleRate * 90000; - const stamp = pts + frameIndex * frameDuration; - const unit = data.subarray(start, start + frameLength); - track.config = config; - track.channelCount = channelCount; - track.samplerate = sampleRate; - track.samples.push({ - unit, - pts: stamp - }); - return frameLength; -} - -class BaseVideoParser { - constructor() { - this.VideoSample = null; - } - createVideoSample(key, pts, dts) { - return { - key, - frame: false, - pts, - dts, - units: [], - length: 0 - }; - } - getLastNalUnit(samples) { - var _VideoSample; - let VideoSample = this.VideoSample; - let lastUnit; - // try to fallback to previous sample if current one is empty - if (!VideoSample || VideoSample.units.length === 0) { - VideoSample = samples[samples.length - 1]; - } - if ((_VideoSample = VideoSample) != null && _VideoSample.units) { - const units = VideoSample.units; - lastUnit = units[units.length - 1]; - } - return lastUnit; - } - pushAccessUnit(VideoSample, videoTrack) { - if (VideoSample.units.length && VideoSample.frame) { - // if sample does not have PTS/DTS, patch with last sample PTS/DTS - if (VideoSample.pts === undefined) { - const samples = videoTrack.samples; - const nbSamples = samples.length; - if (nbSamples) { - const lastSample = samples[nbSamples - 1]; - VideoSample.pts = lastSample.pts; - VideoSample.dts = lastSample.dts; - } else { - // dropping samples, no timestamp found - videoTrack.dropped++; - return; - } - } - videoTrack.samples.push(VideoSample); - } - } - parseNALu(track, array, endOfSegment) { - const len = array.byteLength; - let state = track.naluState || 0; - const lastState = state; - const units = []; - let i = 0; - let value; - let overflow; - let unitType; - let lastUnitStart = -1; - let lastUnitType = 0; - // logger.log('PES:' + Hex.hexDump(array)); - - if (state === -1) { - // special use case where we found 3 or 4-byte start codes exactly at the end of previous PES packet - lastUnitStart = 0; - // NALu type is value read from offset 0 - lastUnitType = this.getNALuType(array, 0); - state = 0; - i = 1; - } - while (i < len) { - value = array[i++]; - // optimization. state 0 and 1 are the predominant case. let's handle them outside of the switch/case - if (!state) { - state = value ? 0 : 1; - continue; - } - if (state === 1) { - state = value ? 0 : 2; - continue; - } - // here we have state either equal to 2 or 3 - if (!value) { - state = 3; - } else if (value === 1) { - overflow = i - state - 1; - if (lastUnitStart >= 0) { - const unit = { - data: array.subarray(lastUnitStart, overflow), - type: lastUnitType - }; - // logger.log('pushing NALU, type/size:' + unit.type + '/' + unit.data.byteLength); - units.push(unit); - } else { - // lastUnitStart is undefined => this is the first start code found in this PES packet - // first check if start code delimiter is overlapping between 2 PES packets, - // ie it started in last packet (lastState not zero) - // and ended at the beginning of this PES packet (i <= 4 - lastState) - const lastUnit = this.getLastNalUnit(track.samples); - if (lastUnit) { - if (lastState && i <= 4 - lastState) { - // start delimiter overlapping between PES packets - // strip start delimiter bytes from the end of last NAL unit - // check if lastUnit had a state different from zero - if (lastUnit.state) { - // strip last bytes - lastUnit.data = lastUnit.data.subarray(0, lastUnit.data.byteLength - lastState); - } - } - // If NAL units are not starting right at the beginning of the PES packet, push preceding data into previous NAL unit. - - if (overflow > 0) { - // logger.log('first NALU found with overflow:' + overflow); - lastUnit.data = appendUint8Array(lastUnit.data, array.subarray(0, overflow)); - lastUnit.state = 0; - } - } - } - // check if we can read unit type - if (i < len) { - unitType = this.getNALuType(array, i); - // logger.log('find NALU @ offset:' + i + ',type:' + unitType); - lastUnitStart = i; - lastUnitType = unitType; - state = 0; - } else { - // not enough byte to read unit type. let's read it on next PES parsing - state = -1; - } - } else { - state = 0; - } - } - if (lastUnitStart >= 0 && state >= 0) { - const unit = { - data: array.subarray(lastUnitStart, len), - type: lastUnitType, - state: state - }; - units.push(unit); - // logger.log('pushing NALU, type/size/state:' + unit.type + '/' + unit.data.byteLength + '/' + state); - } - // no NALu found - if (units.length === 0) { - // append pes.data to previous NAL unit - const lastUnit = this.getLastNalUnit(track.samples); - if (lastUnit) { - lastUnit.data = appendUint8Array(lastUnit.data, array); - } - } - track.naluState = state; - return units; - } -} - -/** - * Parser for exponential Golomb codes, a variable-bitwidth number encoding scheme used by h264. - */ - -class ExpGolomb { - constructor(data) { - this.data = void 0; - this.bytesAvailable = void 0; - this.word = void 0; - this.bitsAvailable = void 0; - this.data = data; - // the number of bytes left to examine in this.data - this.bytesAvailable = data.byteLength; - // the current word being examined - this.word = 0; // :uint - // the number of bits left to examine in the current word - this.bitsAvailable = 0; // :uint - } - - // ():void - loadWord() { - const data = this.data; - const bytesAvailable = this.bytesAvailable; - const position = data.byteLength - bytesAvailable; - const workingBytes = new Uint8Array(4); - const availableBytes = Math.min(4, bytesAvailable); - if (availableBytes === 0) { - throw new Error('no bytes available'); - } - workingBytes.set(data.subarray(position, position + availableBytes)); - this.word = new DataView(workingBytes.buffer).getUint32(0); - // track the amount of this.data that has been processed - this.bitsAvailable = availableBytes * 8; - this.bytesAvailable -= availableBytes; - } - - // (count:int):void - skipBits(count) { - 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; - this.bytesAvailable -= skipBytes; - this.loadWord(); - this.word <<= count; - this.bitsAvailable -= count; - } - } - - // (size:int):uint - readBits(size) { - let bits = Math.min(this.bitsAvailable, size); // :uint - const valu = this.word >>> 32 - bits; // :uint - if (size > 32) { - logger.error('Cannot read more than 32 bits at a time'); - } - this.bitsAvailable -= bits; - if (this.bitsAvailable > 0) { - this.word <<= bits; - } else if (this.bytesAvailable > 0) { - this.loadWord(); - } else { - throw new Error('no bits available'); - } - bits = size - bits; - if (bits > 0 && this.bitsAvailable) { - return valu << bits | this.readBits(bits); - } else { - return valu; - } - } - - // ():uint - skipLZ() { - let leadingZeroCount; // :uint - for (leadingZeroCount = 0; leadingZeroCount < this.bitsAvailable; ++leadingZeroCount) { - if ((this.word & 0x80000000 >>> leadingZeroCount) !== 0) { - // the first bit of working word is 1 - this.word <<= leadingZeroCount; - this.bitsAvailable -= leadingZeroCount; - return leadingZeroCount; - } - } - // we exhausted word and still have not found a 1 - this.loadWord(); - return leadingZeroCount + this.skipLZ(); - } - - // ():void - skipUEG() { - this.skipBits(1 + this.skipLZ()); - } - - // ():void - skipEG() { - this.skipBits(1 + this.skipLZ()); - } - - // ():uint - readUEG() { - const clz = this.skipLZ(); // :uint - return this.readBits(clz + 1) - 1; - } - - // ():int - readEG() { - const valu = this.readUEG(); // :int - if (0x01 & valu) { - // the number is odd if the low order bit is set - return 1 + valu >>> 1; // add 1 to make it even, and divide by 2 - } else { - return -1 * (valu >>> 1); // divide by two then make it negative - } - } - - // Some convenience functions - // :Boolean - readBoolean() { - return this.readBits(1) === 1; - } - - // ():int - readUByte() { - return this.readBits(8); - } - - // ():int - readUShort() { - return this.readBits(16); - } - - // ():int - readUInt() { - return this.readBits(32); - } -} - -class AvcVideoParser extends BaseVideoParser { - parsePES(track, textTrack, pes, endOfSegment) { - const units = this.parseNALu(track, pes.data, endOfSegment); - let VideoSample = this.VideoSample; - let push; - let spsfound = false; - // free pes.data to save up some memory - pes.data = null; - - // if new NAL units found and last sample still there, let's push ... - // this helps parsing streams with missing AUD (only do this if AUD never found) - if (VideoSample && units.length && !track.audFound) { - this.pushAccessUnit(VideoSample, track); - VideoSample = this.VideoSample = this.createVideoSample(false, pes.pts, pes.dts); - } - units.forEach(unit => { - var _VideoSample2, _VideoSample3; - switch (unit.type) { - // NDR - case 1: - { - let iskey = false; - push = true; - const data = unit.data; - // only check slice type to detect KF in case SPS found in same packet (any keyframe is preceded by SPS ...) - if (spsfound && data.length > 4) { - // retrieve slice type by parsing beginning of NAL unit (follow H264 spec, slice_header definition) to detect keyframe embedded in NDR - const sliceType = this.readSliceType(data); - // 2 : I slice, 4 : SI slice, 7 : I slice, 9: SI slice - // SI slice : A slice that is coded using intra prediction only and using quantisation of the prediction samples. - // An SI slice can be coded such that its decoded samples can be constructed identically to an SP slice. - // I slice: A slice that is not an SI slice that is decoded using intra prediction only. - // if (sliceType === 2 || sliceType === 7) { - if (sliceType === 2 || sliceType === 4 || sliceType === 7 || sliceType === 9) { - iskey = true; - } - } - if (iskey) { - var _VideoSample; - // if we have non-keyframe data already, that cannot belong to the same frame as a keyframe, so force a push - if ((_VideoSample = VideoSample) != null && _VideoSample.frame && !VideoSample.key) { - this.pushAccessUnit(VideoSample, track); - VideoSample = this.VideoSample = null; - } - } - if (!VideoSample) { - VideoSample = this.VideoSample = this.createVideoSample(true, pes.pts, pes.dts); - } - VideoSample.frame = true; - VideoSample.key = iskey; - break; - // IDR - } - case 5: - push = true; - // handle PES not starting with AUD - // if we have frame data already, that cannot belong to the same frame, so force a push - if ((_VideoSample2 = VideoSample) != null && _VideoSample2.frame && !VideoSample.key) { - this.pushAccessUnit(VideoSample, track); - VideoSample = this.VideoSample = null; - } - if (!VideoSample) { - VideoSample = this.VideoSample = this.createVideoSample(true, pes.pts, pes.dts); - } - VideoSample.key = true; - VideoSample.frame = true; - break; - // SEI - case 6: - { - push = true; - parseSEIMessageFromNALu(unit.data, 1, pes.pts, textTrack.samples); - break; - // SPS - } - case 7: - { - var _track$pixelRatio, _track$pixelRatio2; - push = true; - spsfound = true; - const sps = unit.data; - const config = this.readSPS(sps); - if (!track.sps || track.width !== config.width || track.height !== config.height || ((_track$pixelRatio = track.pixelRatio) == null ? void 0 : _track$pixelRatio[0]) !== config.pixelRatio[0] || ((_track$pixelRatio2 = track.pixelRatio) == null ? void 0 : _track$pixelRatio2[1]) !== config.pixelRatio[1]) { - track.width = config.width; - track.height = config.height; - track.pixelRatio = config.pixelRatio; - track.sps = [sps]; - const codecarray = sps.subarray(1, 4); - let codecstring = 'avc1.'; - for (let i = 0; i < 3; i++) { - let h = codecarray[i].toString(16); - if (h.length < 2) { - h = '0' + h; - } - codecstring += h; - } - track.codec = codecstring; - } - break; - } - // PPS - case 8: - push = true; - track.pps = [unit.data]; - break; - // AUD - case 9: - push = true; - track.audFound = true; - if ((_VideoSample3 = VideoSample) != null && _VideoSample3.frame) { - this.pushAccessUnit(VideoSample, track); - VideoSample = null; - } - if (!VideoSample) { - VideoSample = this.VideoSample = this.createVideoSample(false, pes.pts, pes.dts); - } - break; - // Filler Data - case 12: - push = true; - break; - default: - push = false; - break; - } - if (VideoSample && push) { - const units = VideoSample.units; - units.push(unit); - } - }); - // if last PES packet, push samples - if (endOfSegment && VideoSample) { - this.pushAccessUnit(VideoSample, track); - this.VideoSample = null; - } - } - getNALuType(data, offset) { - return data[offset] & 0x1f; - } - readSliceType(data) { - const eg = new ExpGolomb(data); - // skip NALu type - eg.readUByte(); - // discard first_mb_in_slice - eg.readUEG(); - // return slice_type - return eg.readUEG(); - } - - /** - * The scaling list is optionally transmitted as part of a sequence parameter - * set and is not relevant to transmuxing. - * @param count the number of entries in this scaling list - * @see Recommendation ITU-T H.264, Section 7.3.2.1.1.1 - */ - skipScalingList(count, reader) { - let lastScale = 8; - let nextScale = 8; - let deltaScale; - for (let j = 0; j < count; j++) { - if (nextScale !== 0) { - deltaScale = reader.readEG(); - nextScale = (lastScale + deltaScale + 256) % 256; - } - lastScale = nextScale === 0 ? lastScale : nextScale; - } - } - - /** - * Read a sequence parameter set and return some interesting video - * properties. A sequence parameter set is the H264 metadata that - * describes the properties of upcoming video frames. - * @returns an object with configuration parsed from the - * sequence parameter set, including the dimensions of the - * associated video frames. - */ - readSPS(sps) { - const eg = new ExpGolomb(sps); - let frameCropLeftOffset = 0; - let frameCropRightOffset = 0; - let frameCropTopOffset = 0; - let frameCropBottomOffset = 0; - let numRefFramesInPicOrderCntCycle; - let scalingListCount; - let i; - const readUByte = eg.readUByte.bind(eg); - const readBits = eg.readBits.bind(eg); - const readUEG = eg.readUEG.bind(eg); - const readBoolean = eg.readBoolean.bind(eg); - const skipBits = eg.skipBits.bind(eg); - const skipEG = eg.skipEG.bind(eg); - const skipUEG = eg.skipUEG.bind(eg); - const skipScalingList = this.skipScalingList.bind(this); - readUByte(); - const profileIdc = readUByte(); // profile_idc - readBits(5); // profileCompat constraint_set[0-4]_flag, u(5) - skipBits(3); // reserved_zero_3bits u(3), - readUByte(); // level_idc u(8) - skipUEG(); // seq_parameter_set_id - // some profiles have more optional data we don't need - if (profileIdc === 100 || profileIdc === 110 || profileIdc === 122 || profileIdc === 244 || profileIdc === 44 || profileIdc === 83 || profileIdc === 86 || profileIdc === 118 || profileIdc === 128) { - const chromaFormatIdc = readUEG(); - if (chromaFormatIdc === 3) { - skipBits(1); - } // separate_colour_plane_flag - - skipUEG(); // bit_depth_luma_minus8 - skipUEG(); // bit_depth_chroma_minus8 - skipBits(1); // qpprime_y_zero_transform_bypass_flag - if (readBoolean()) { - // seq_scaling_matrix_present_flag - scalingListCount = chromaFormatIdc !== 3 ? 8 : 12; - for (i = 0; i < scalingListCount; i++) { - if (readBoolean()) { - // seq_scaling_list_present_flag[ i ] - if (i < 6) { - skipScalingList(16, eg); - } else { - skipScalingList(64, eg); - } - } - } - } - } - skipUEG(); // log2_max_frame_num_minus4 - const picOrderCntType = readUEG(); - if (picOrderCntType === 0) { - readUEG(); // log2_max_pic_order_cnt_lsb_minus4 - } else if (picOrderCntType === 1) { - skipBits(1); // delta_pic_order_always_zero_flag - skipEG(); // offset_for_non_ref_pic - skipEG(); // offset_for_top_to_bottom_field - numRefFramesInPicOrderCntCycle = readUEG(); - for (i = 0; i < numRefFramesInPicOrderCntCycle; i++) { - skipEG(); - } // offset_for_ref_frame[ i ] - } - skipUEG(); // max_num_ref_frames - skipBits(1); // gaps_in_frame_num_value_allowed_flag - const picWidthInMbsMinus1 = readUEG(); - const picHeightInMapUnitsMinus1 = readUEG(); - const frameMbsOnlyFlag = readBits(1); - if (frameMbsOnlyFlag === 0) { - skipBits(1); - } // mb_adaptive_frame_field_flag - - skipBits(1); // direct_8x8_inference_flag - if (readBoolean()) { - // frame_cropping_flag - frameCropLeftOffset = readUEG(); - frameCropRightOffset = readUEG(); - frameCropTopOffset = readUEG(); - frameCropBottomOffset = readUEG(); - } - let pixelRatio = [1, 1]; - if (readBoolean()) { - // vui_parameters_present_flag - if (readBoolean()) { - // aspect_ratio_info_present_flag - const aspectRatioIdc = readUByte(); - switch (aspectRatioIdc) { - case 1: - pixelRatio = [1, 1]; - break; - case 2: - pixelRatio = [12, 11]; - break; - case 3: - pixelRatio = [10, 11]; - break; - case 4: - pixelRatio = [16, 11]; - break; - case 5: - pixelRatio = [40, 33]; - break; - case 6: - pixelRatio = [24, 11]; - break; - case 7: - pixelRatio = [20, 11]; - break; - case 8: - pixelRatio = [32, 11]; - break; - case 9: - pixelRatio = [80, 33]; - break; - case 10: - pixelRatio = [18, 11]; - break; - case 11: - pixelRatio = [15, 11]; - break; - case 12: - pixelRatio = [64, 33]; - break; - case 13: - pixelRatio = [160, 99]; - break; - case 14: - pixelRatio = [4, 3]; - break; - case 15: - pixelRatio = [3, 2]; - break; - case 16: - pixelRatio = [2, 1]; - break; - case 255: - { - pixelRatio = [readUByte() << 8 | readUByte(), readUByte() << 8 | readUByte()]; - break; - } - } - } - } - return { - width: Math.ceil((picWidthInMbsMinus1 + 1) * 16 - frameCropLeftOffset * 2 - frameCropRightOffset * 2), - height: (2 - frameMbsOnlyFlag) * (picHeightInMapUnitsMinus1 + 1) * 16 - (frameMbsOnlyFlag ? 2 : 4) * (frameCropTopOffset + frameCropBottomOffset), - pixelRatio: pixelRatio - }; - } -} - -class HevcVideoParser extends BaseVideoParser { - constructor(...args) { - super(...args); - this.initVPS = null; - } - parsePES(track, textTrack, pes, endOfSegment) { - const units = this.parseNALu(track, pes.data, endOfSegment); - let VideoSample = this.VideoSample; - let push; - let spsfound = false; - // free pes.data to save up some memory - pes.data = null; - - // if new NAL units found and last sample still there, let's push ... - // this helps parsing streams with missing AUD (only do this if AUD never found) - if (VideoSample && units.length && !track.audFound) { - this.pushAccessUnit(VideoSample, track); - VideoSample = this.VideoSample = this.createVideoSample(false, pes.pts, pes.dts); - } - units.forEach(unit => { - var _VideoSample2, _VideoSample3; - switch (unit.type) { - // NON-IDR, NON RANDOM ACCESS SLICE - case 0: - case 1: - case 2: - case 3: - case 4: - case 5: - case 6: - case 7: - case 8: - case 9: - if (!VideoSample) { - VideoSample = this.VideoSample = this.createVideoSample(false, pes.pts, pes.dts); - } - VideoSample.frame = true; - push = true; - break; - - // CRA, BLA (random access picture) - case 16: - case 17: - case 18: - case 21: - push = true; - if (spsfound) { - var _VideoSample; - // handle PES not starting with AUD - // if we have frame data already, that cannot belong to the same frame, so force a push - if ((_VideoSample = VideoSample) != null && _VideoSample.frame && !VideoSample.key) { - this.pushAccessUnit(VideoSample, track); - VideoSample = this.VideoSample = null; - } - } - if (!VideoSample) { - VideoSample = this.VideoSample = this.createVideoSample(true, pes.pts, pes.dts); - } - VideoSample.key = true; - VideoSample.frame = true; - break; - - // IDR - case 19: - case 20: - push = true; - // handle PES not starting with AUD - // if we have frame data already, that cannot belong to the same frame, so force a push - if ((_VideoSample2 = VideoSample) != null && _VideoSample2.frame && !VideoSample.key) { - this.pushAccessUnit(VideoSample, track); - VideoSample = this.VideoSample = null; - } - if (!VideoSample) { - VideoSample = this.VideoSample = this.createVideoSample(true, pes.pts, pes.dts); - } - VideoSample.key = true; - VideoSample.frame = true; - break; - - // SEI - case 39: - push = true; - parseSEIMessageFromNALu(unit.data, 2, - // NALu header size - pes.pts, textTrack.samples); - break; - - // VPS - case 32: - push = true; - if (!track.vps) { - const config = this.readVPS(unit.data); - track.params = _objectSpread2({}, config); - this.initVPS = unit.data; - } - track.vps = [unit.data]; - break; - - // SPS - case 33: - push = true; - spsfound = true; - if (typeof track.params === 'object') { - if (track.vps !== undefined && track.vps[0] !== this.initVPS && track.sps !== undefined && !this.matchSPS(track.sps[0], unit.data)) { - this.initVPS = track.vps[0]; - track.sps = track.pps = undefined; - } - if (!track.sps) { - const config = this.readSPS(unit.data); - track.width = config.width; - track.height = config.height; - track.pixelRatio = config.pixelRatio; - track.codec = config.codecString; - track.sps = []; - for (const prop in config.params) { - track.params[prop] = config.params[prop]; - } - } - if (track.vps !== undefined && track.vps[0] === this.initVPS) { - track.sps.push(unit.data); - } - } - if (!VideoSample) { - VideoSample = this.VideoSample = this.createVideoSample(true, pes.pts, pes.dts); - } - VideoSample.key = true; - break; - - // PPS - case 34: - push = true; - if (typeof track.params === 'object') { - if (!track.pps) { - track.pps = []; - const config = this.readPPS(unit.data); - for (const prop in config) { - track.params[prop] = config[prop]; - } - } - if (track.vps !== undefined && track.vps[0] === this.initVPS) { - track.pps.push(unit.data); - } - } - break; - - // ACCESS UNIT DELIMITER - case 35: - push = true; - track.audFound = true; - if ((_VideoSample3 = VideoSample) != null && _VideoSample3.frame) { - this.pushAccessUnit(VideoSample, track); - VideoSample = null; - } - if (!VideoSample) { - VideoSample = this.VideoSample = this.createVideoSample(false, pes.pts, pes.dts); - } - break; - default: - push = false; - break; - } - if (VideoSample && push) { - const units = VideoSample.units; - units.push(unit); - } - }); - // if last PES packet, push samples - if (endOfSegment && VideoSample) { - this.pushAccessUnit(VideoSample, track); - this.VideoSample = null; - } - } - getNALuType(data, offset) { - return (data[offset] & 0x7e) >>> 1; - } - ebsp2rbsp(arr) { - const dst = new Uint8Array(arr.byteLength); - let dstIdx = 0; - for (let i = 0; i < arr.byteLength; i++) { - if (i >= 2) { - // Unescape: Skip 0x03 after 00 00 - if (arr[i] === 0x03 && arr[i - 1] === 0x00 && arr[i - 2] === 0x00) { - continue; - } - } - dst[dstIdx] = arr[i]; - dstIdx++; - } - return new Uint8Array(dst.buffer, 0, dstIdx); - } - pushAccessUnit(VideoSample, videoTrack) { - super.pushAccessUnit(VideoSample, videoTrack); - if (this.initVPS) { - this.initVPS = null; // null initVPS to prevent possible track's sps/pps growth until next VPS - } - } - readVPS(vps) { - const eg = new ExpGolomb(vps); - // remove header - eg.readUByte(); - eg.readUByte(); - eg.readBits(4); // video_parameter_set_id - eg.skipBits(2); - eg.readBits(6); // max_layers_minus1 - const max_sub_layers_minus1 = eg.readBits(3); - const temporal_id_nesting_flag = eg.readBoolean(); - // ...vui fps can be here, but empty fps value is not critical for metadata - - return { - numTemporalLayers: max_sub_layers_minus1 + 1, - temporalIdNested: temporal_id_nesting_flag - }; - } - readSPS(sps) { - const eg = new ExpGolomb(this.ebsp2rbsp(sps)); - eg.readUByte(); - eg.readUByte(); - eg.readBits(4); //video_parameter_set_id - const max_sub_layers_minus1 = eg.readBits(3); - eg.readBoolean(); // temporal_id_nesting_flag - - // profile_tier_level - const general_profile_space = eg.readBits(2); - const general_tier_flag = eg.readBoolean(); - const general_profile_idc = eg.readBits(5); - const general_profile_compatibility_flags_1 = eg.readUByte(); - const general_profile_compatibility_flags_2 = eg.readUByte(); - const general_profile_compatibility_flags_3 = eg.readUByte(); - const general_profile_compatibility_flags_4 = eg.readUByte(); - const general_constraint_indicator_flags_1 = eg.readUByte(); - const general_constraint_indicator_flags_2 = eg.readUByte(); - const general_constraint_indicator_flags_3 = eg.readUByte(); - const general_constraint_indicator_flags_4 = eg.readUByte(); - const general_constraint_indicator_flags_5 = eg.readUByte(); - const general_constraint_indicator_flags_6 = eg.readUByte(); - const general_level_idc = eg.readUByte(); - const sub_layer_profile_present_flags = []; - const sub_layer_level_present_flags = []; - for (let i = 0; i < max_sub_layers_minus1; i++) { - sub_layer_profile_present_flags.push(eg.readBoolean()); - sub_layer_level_present_flags.push(eg.readBoolean()); - } - if (max_sub_layers_minus1 > 0) { - for (let i = max_sub_layers_minus1; i < 8; i++) { - eg.readBits(2); - } - } - for (let i = 0; i < max_sub_layers_minus1; i++) { - if (sub_layer_profile_present_flags[i]) { - eg.readUByte(); // sub_layer_profile_space, sub_layer_tier_flag, sub_layer_profile_idc - eg.readUByte(); - eg.readUByte(); - eg.readUByte(); - eg.readUByte(); // sub_layer_profile_compatibility_flag - eg.readUByte(); - eg.readUByte(); - eg.readUByte(); - eg.readUByte(); - eg.readUByte(); - eg.readUByte(); - } - if (sub_layer_level_present_flags[i]) { - eg.readUByte(); - } - } - eg.readUEG(); // seq_parameter_set_id - const chroma_format_idc = eg.readUEG(); - if (chroma_format_idc == 3) { - eg.skipBits(1); //separate_colour_plane_flag - } - const pic_width_in_luma_samples = eg.readUEG(); - const pic_height_in_luma_samples = eg.readUEG(); - const conformance_window_flag = eg.readBoolean(); - let pic_left_offset = 0, - pic_right_offset = 0, - pic_top_offset = 0, - pic_bottom_offset = 0; - if (conformance_window_flag) { - pic_left_offset += eg.readUEG(); - pic_right_offset += eg.readUEG(); - pic_top_offset += eg.readUEG(); - pic_bottom_offset += eg.readUEG(); - } - const bit_depth_luma_minus8 = eg.readUEG(); - const bit_depth_chroma_minus8 = eg.readUEG(); - const log2_max_pic_order_cnt_lsb_minus4 = eg.readUEG(); - const sub_layer_ordering_info_present_flag = eg.readBoolean(); - for (let i = sub_layer_ordering_info_present_flag ? 0 : max_sub_layers_minus1; i <= max_sub_layers_minus1; i++) { - eg.skipUEG(); // max_dec_pic_buffering_minus1[i] - eg.skipUEG(); // max_num_reorder_pics[i] - eg.skipUEG(); // max_latency_increase_plus1[i] - } - eg.skipUEG(); // log2_min_luma_coding_block_size_minus3 - eg.skipUEG(); // log2_diff_max_min_luma_coding_block_size - eg.skipUEG(); // log2_min_transform_block_size_minus2 - eg.skipUEG(); // log2_diff_max_min_transform_block_size - eg.skipUEG(); // max_transform_hierarchy_depth_inter - eg.skipUEG(); // max_transform_hierarchy_depth_intra - const scaling_list_enabled_flag = eg.readBoolean(); - if (scaling_list_enabled_flag) { - const sps_scaling_list_data_present_flag = eg.readBoolean(); - if (sps_scaling_list_data_present_flag) { - for (let sizeId = 0; sizeId < 4; sizeId++) { - for (let matrixId = 0; matrixId < (sizeId === 3 ? 2 : 6); matrixId++) { - const scaling_list_pred_mode_flag = eg.readBoolean(); - if (!scaling_list_pred_mode_flag) { - eg.readUEG(); // scaling_list_pred_matrix_id_delta - } else { - const coefNum = Math.min(64, 1 << 4 + (sizeId << 1)); - if (sizeId > 1) { - eg.readEG(); - } - for (let i = 0; i < coefNum; i++) { - eg.readEG(); - } - } - } - } - } - } - eg.readBoolean(); // amp_enabled_flag - eg.readBoolean(); // sample_adaptive_offset_enabled_flag - const pcm_enabled_flag = eg.readBoolean(); - if (pcm_enabled_flag) { - eg.readUByte(); - eg.skipUEG(); - eg.skipUEG(); - eg.readBoolean(); - } - const num_short_term_ref_pic_sets = eg.readUEG(); - let num_delta_pocs = 0; - for (let i = 0; i < num_short_term_ref_pic_sets; i++) { - let inter_ref_pic_set_prediction_flag = false; - if (i !== 0) { - inter_ref_pic_set_prediction_flag = eg.readBoolean(); - } - if (inter_ref_pic_set_prediction_flag) { - if (i === num_short_term_ref_pic_sets) { - eg.readUEG(); - } - eg.readBoolean(); - eg.readUEG(); - let next_num_delta_pocs = 0; - for (let j = 0; j <= num_delta_pocs; j++) { - const used_by_curr_pic_flag = eg.readBoolean(); - let use_delta_flag = false; - if (!used_by_curr_pic_flag) { - use_delta_flag = eg.readBoolean(); - } - if (used_by_curr_pic_flag || use_delta_flag) { - next_num_delta_pocs++; - } - } - num_delta_pocs = next_num_delta_pocs; - } else { - const num_negative_pics = eg.readUEG(); - const num_positive_pics = eg.readUEG(); - num_delta_pocs = num_negative_pics + num_positive_pics; - for (let j = 0; j < num_negative_pics; j++) { - eg.readUEG(); - eg.readBoolean(); - } - for (let j = 0; j < num_positive_pics; j++) { - eg.readUEG(); - eg.readBoolean(); - } - } - } - const long_term_ref_pics_present_flag = eg.readBoolean(); - if (long_term_ref_pics_present_flag) { - const num_long_term_ref_pics_sps = eg.readUEG(); - for (let i = 0; i < num_long_term_ref_pics_sps; i++) { - for (let j = 0; j < log2_max_pic_order_cnt_lsb_minus4 + 4; j++) { - eg.readBits(1); - } - eg.readBits(1); - } - } - let min_spatial_segmentation_idc = 0; - let sar_width = 1, - sar_height = 1; - let fps_fixed = true, - fps_den = 1, - fps_num = 0; - eg.readBoolean(); // sps_temporal_mvp_enabled_flag - eg.readBoolean(); // strong_intra_smoothing_enabled_flag - let default_display_window_flag = false; - const vui_parameters_present_flag = eg.readBoolean(); - if (vui_parameters_present_flag) { - const aspect_ratio_info_present_flag = eg.readBoolean(); - if (aspect_ratio_info_present_flag) { - const aspect_ratio_idc = eg.readUByte(); - const sar_width_table = [1, 12, 10, 16, 40, 24, 20, 32, 80, 18, 15, 64, 160, 4, 3, 2]; - const sar_height_table = [1, 11, 11, 11, 33, 11, 11, 11, 33, 11, 11, 33, 99, 3, 2, 1]; - if (aspect_ratio_idc > 0 && aspect_ratio_idc < 16) { - sar_width = sar_width_table[aspect_ratio_idc - 1]; - sar_height = sar_height_table[aspect_ratio_idc - 1]; - } else if (aspect_ratio_idc === 255) { - sar_width = eg.readBits(16); - sar_height = eg.readBits(16); - } - } - const overscan_info_present_flag = eg.readBoolean(); - if (overscan_info_present_flag) { - eg.readBoolean(); - } - const video_signal_type_present_flag = eg.readBoolean(); - if (video_signal_type_present_flag) { - eg.readBits(3); - eg.readBoolean(); - const colour_description_present_flag = eg.readBoolean(); - if (colour_description_present_flag) { - eg.readUByte(); - eg.readUByte(); - eg.readUByte(); - } - } - const chroma_loc_info_present_flag = eg.readBoolean(); - if (chroma_loc_info_present_flag) { - eg.readUEG(); - eg.readUEG(); - } - eg.readBoolean(); // neutral_chroma_indication_flag - eg.readBoolean(); // field_seq_flag - eg.readBoolean(); // frame_field_info_present_flag - default_display_window_flag = eg.readBoolean(); - if (default_display_window_flag) { - pic_left_offset += eg.readUEG(); - pic_right_offset += eg.readUEG(); - pic_top_offset += eg.readUEG(); - pic_bottom_offset += eg.readUEG(); - } - const vui_timing_info_present_flag = eg.readBoolean(); - if (vui_timing_info_present_flag) { - fps_den = eg.readBits(32); - fps_num = eg.readBits(32); - const vui_poc_proportional_to_timing_flag = eg.readBoolean(); - if (vui_poc_proportional_to_timing_flag) { - eg.readUEG(); - } - const vui_hrd_parameters_present_flag = eg.readBoolean(); - if (vui_hrd_parameters_present_flag) { - //const commonInfPresentFlag = true; - //if (commonInfPresentFlag) { - const nal_hrd_parameters_present_flag = eg.readBoolean(); - const vcl_hrd_parameters_present_flag = eg.readBoolean(); - let sub_pic_hrd_params_present_flag = false; - if (nal_hrd_parameters_present_flag || vcl_hrd_parameters_present_flag) { - sub_pic_hrd_params_present_flag = eg.readBoolean(); - if (sub_pic_hrd_params_present_flag) { - eg.readUByte(); - eg.readBits(5); - eg.readBoolean(); - eg.readBits(5); - } - eg.readBits(4); // bit_rate_scale - eg.readBits(4); // cpb_size_scale - if (sub_pic_hrd_params_present_flag) { - eg.readBits(4); - } - eg.readBits(5); - eg.readBits(5); - eg.readBits(5); - } - //} - for (let i = 0; i <= max_sub_layers_minus1; i++) { - fps_fixed = eg.readBoolean(); // fixed_pic_rate_general_flag - const fixed_pic_rate_within_cvs_flag = fps_fixed || eg.readBoolean(); - let low_delay_hrd_flag = false; - if (fixed_pic_rate_within_cvs_flag) { - eg.readEG(); - } else { - low_delay_hrd_flag = eg.readBoolean(); - } - const cpb_cnt = low_delay_hrd_flag ? 1 : eg.readUEG() + 1; - if (nal_hrd_parameters_present_flag) { - for (let j = 0; j < cpb_cnt; j++) { - eg.readUEG(); - eg.readUEG(); - if (sub_pic_hrd_params_present_flag) { - eg.readUEG(); - eg.readUEG(); - } - eg.skipBits(1); - } - } - if (vcl_hrd_parameters_present_flag) { - for (let j = 0; j < cpb_cnt; j++) { - eg.readUEG(); - eg.readUEG(); - if (sub_pic_hrd_params_present_flag) { - eg.readUEG(); - eg.readUEG(); - } - eg.skipBits(1); - } - } - } - } - } - const bitstream_restriction_flag = eg.readBoolean(); - if (bitstream_restriction_flag) { - eg.readBoolean(); // tiles_fixed_structure_flag - eg.readBoolean(); // motion_vectors_over_pic_boundaries_flag - eg.readBoolean(); // restricted_ref_pic_lists_flag - min_spatial_segmentation_idc = eg.readUEG(); - } - } - let width = pic_width_in_luma_samples, - height = pic_height_in_luma_samples; - if (conformance_window_flag || default_display_window_flag) { - let chroma_scale_w = 1, - chroma_scale_h = 1; - if (chroma_format_idc === 1) { - // YUV 420 - chroma_scale_w = chroma_scale_h = 2; - } else if (chroma_format_idc == 2) { - // YUV 422 - chroma_scale_w = 2; - } - width = pic_width_in_luma_samples - chroma_scale_w * pic_right_offset - chroma_scale_w * pic_left_offset; - height = pic_height_in_luma_samples - chroma_scale_h * pic_bottom_offset - chroma_scale_h * pic_top_offset; - } - const profile_space_string = general_profile_space ? ['A', 'B', 'C'][general_profile_space] : ''; - const profile_compatibility_buf = general_profile_compatibility_flags_1 << 24 | general_profile_compatibility_flags_2 << 16 | general_profile_compatibility_flags_3 << 8 | general_profile_compatibility_flags_4; - let profile_compatibility_rev = 0; - for (let i = 0; i < 32; i++) { - profile_compatibility_rev = (profile_compatibility_rev | (profile_compatibility_buf >> i & 1) << 31 - i) >>> 0; // reverse bit position (and cast as UInt32) - } - let profile_compatibility_flags_string = profile_compatibility_rev.toString(16); - if (general_profile_idc === 1 && profile_compatibility_flags_string === '2') { - profile_compatibility_flags_string = '6'; - } - const tier_flag_string = general_tier_flag ? 'H' : 'L'; - return { - codecString: `hvc1.${profile_space_string}${general_profile_idc}.${profile_compatibility_flags_string}.${tier_flag_string}${general_level_idc}.B0`, - params: { - general_tier_flag, - general_profile_idc, - general_profile_space, - general_profile_compatibility_flags: [general_profile_compatibility_flags_1, general_profile_compatibility_flags_2, general_profile_compatibility_flags_3, general_profile_compatibility_flags_4], - general_constraint_indicator_flags: [general_constraint_indicator_flags_1, general_constraint_indicator_flags_2, general_constraint_indicator_flags_3, general_constraint_indicator_flags_4, general_constraint_indicator_flags_5, general_constraint_indicator_flags_6], - general_level_idc, - bit_depth: bit_depth_luma_minus8 + 8, - bit_depth_luma_minus8, - bit_depth_chroma_minus8, - min_spatial_segmentation_idc, - chroma_format_idc: chroma_format_idc, - frame_rate: { - fixed: fps_fixed, - fps: fps_num / fps_den - } - }, - width, - height, - pixelRatio: [sar_width, sar_height] - }; - } - readPPS(pps) { - const eg = new ExpGolomb(this.ebsp2rbsp(pps)); - eg.readUByte(); - eg.readUByte(); - eg.skipUEG(); // pic_parameter_set_id - eg.skipUEG(); // seq_parameter_set_id - eg.skipBits(2); // dependent_slice_segments_enabled_flag, output_flag_present_flag - eg.skipBits(3); // num_extra_slice_header_bits - eg.skipBits(2); // sign_data_hiding_enabled_flag, cabac_init_present_flag - eg.skipUEG(); - eg.skipUEG(); - eg.skipEG(); // init_qp_minus26 - eg.skipBits(2); // constrained_intra_pred_flag, transform_skip_enabled_flag - const cu_qp_delta_enabled_flag = eg.readBoolean(); - if (cu_qp_delta_enabled_flag) { - eg.skipUEG(); - } - eg.skipEG(); // cb_qp_offset - eg.skipEG(); // cr_qp_offset - eg.skipBits(4); // pps_slice_chroma_qp_offsets_present_flag, weighted_pred_flag, weighted_bipred_flag, transquant_bypass_enabled_flag - const tiles_enabled_flag = eg.readBoolean(); - const entropy_coding_sync_enabled_flag = eg.readBoolean(); - let parallelismType = 1; // slice-based parallel decoding - if (entropy_coding_sync_enabled_flag && tiles_enabled_flag) { - parallelismType = 0; // mixed-type parallel decoding - } else if (entropy_coding_sync_enabled_flag) { - parallelismType = 3; // wavefront-based parallel decoding - } else if (tiles_enabled_flag) { - parallelismType = 2; // tile-based parallel decoding - } - return { - parallelismType - }; - } - matchSPS(sps1, sps2) { - // compare without headers and VPS related params - return String.fromCharCode.apply(null, sps1).substr(3) === String.fromCharCode.apply(null, sps2).substr(3); - } -} - -/** - * SAMPLE-AES decrypter - */ - -class SampleAesDecrypter { - constructor(observer, config, keyData) { - this.keyData = void 0; - this.decrypter = void 0; - this.keyData = keyData; - this.decrypter = new Decrypter(config, { - removePKCS7Padding: false - }); - } - decryptBuffer(encryptedData) { - return this.decrypter.decrypt(encryptedData, this.keyData.key.buffer, this.keyData.iv.buffer, DecrypterAesMode.cbc); - } - - // AAC - encrypt all full 16 bytes blocks starting from offset 16 - decryptAacSample(samples, sampleIndex, callback) { - const curUnit = samples[sampleIndex].unit; - if (curUnit.length <= 16) { - // No encrypted portion in this sample (first 16 bytes is not - // encrypted, see https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/HLS_Sample_Encryption/Encryption/Encryption.html), - return; - } - const encryptedData = curUnit.subarray(16, curUnit.length - curUnit.length % 16); - const encryptedBuffer = encryptedData.buffer.slice(encryptedData.byteOffset, encryptedData.byteOffset + encryptedData.length); - this.decryptBuffer(encryptedBuffer).then(decryptedBuffer => { - const decryptedData = new Uint8Array(decryptedBuffer); - curUnit.set(decryptedData, 16); - if (!this.decrypter.isSync()) { - this.decryptAacSamples(samples, sampleIndex + 1, callback); - } - }); - } - decryptAacSamples(samples, sampleIndex, callback) { - for (;; sampleIndex++) { - if (sampleIndex >= samples.length) { - callback(); - return; - } - if (samples[sampleIndex].unit.length < 32) { - continue; - } - this.decryptAacSample(samples, sampleIndex, callback); - if (!this.decrypter.isSync()) { - return; - } - } - } - - // AVC - encrypt one 16 bytes block out of ten, starting from offset 32 - getAvcEncryptedData(decodedData) { - const encryptedDataLen = Math.floor((decodedData.length - 48) / 160) * 16 + 16; - const encryptedData = new Int8Array(encryptedDataLen); - let outputPos = 0; - for (let inputPos = 32; inputPos < decodedData.length - 16; inputPos += 160, outputPos += 16) { - encryptedData.set(decodedData.subarray(inputPos, inputPos + 16), outputPos); - } - return encryptedData; - } - getAvcDecryptedUnit(decodedData, decryptedData) { - const uint8DecryptedData = new Uint8Array(decryptedData); - let inputPos = 0; - for (let outputPos = 32; outputPos < decodedData.length - 16; outputPos += 160, inputPos += 16) { - decodedData.set(uint8DecryptedData.subarray(inputPos, inputPos + 16), outputPos); - } - return decodedData; - } - decryptAvcSample(samples, sampleIndex, unitIndex, callback, curUnit) { - const decodedData = discardEPB(curUnit.data); - const encryptedData = this.getAvcEncryptedData(decodedData); - this.decryptBuffer(encryptedData.buffer).then(decryptedBuffer => { - curUnit.data = this.getAvcDecryptedUnit(decodedData, decryptedBuffer); - if (!this.decrypter.isSync()) { - this.decryptAvcSamples(samples, sampleIndex, unitIndex + 1, callback); - } - }); - } - decryptAvcSamples(samples, sampleIndex, unitIndex, callback) { - if (samples instanceof Uint8Array) { - throw new Error('Cannot decrypt samples of type Uint8Array'); - } - for (;; sampleIndex++, unitIndex = 0) { - if (sampleIndex >= samples.length) { - callback(); - return; - } - const curUnits = samples[sampleIndex].units; - for (;; unitIndex++) { - if (unitIndex >= curUnits.length) { - break; - } - const curUnit = curUnits[unitIndex]; - if (curUnit.data.length <= 48 || curUnit.type !== 1 && curUnit.type !== 5) { - continue; - } - this.decryptAvcSample(samples, sampleIndex, unitIndex, callback, curUnit); - if (!this.decrypter.isSync()) { - return; - } - } - } - } -} - -const PACKET_LENGTH = 188; -class TSDemuxer { - constructor(observer, config, typeSupported, logger) { - this.logger = void 0; - this.observer = void 0; - this.config = void 0; - this.typeSupported = void 0; - this.sampleAes = null; - this.pmtParsed = false; - this.audioCodec = void 0; - this.videoCodec = void 0; - this._pmtId = -1; - this._videoTrack = void 0; - this._audioTrack = void 0; - this._id3Track = void 0; - this._txtTrack = void 0; - this.aacOverFlow = null; - this.remainderData = null; - this.videoParser = void 0; - this.observer = observer; - this.config = config; - this.typeSupported = typeSupported; - this.logger = logger; - this.videoParser = null; - } - static probe(data, logger) { - 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) { - const length = data.length; - let scanwindow = Math.min(PACKET_LENGTH * 5, 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; - let packetStart = -1; - let tsPackets = 0; - for (let j = i; j < length; j += PACKET_LENGTH) { - if (data[j] === 0x47 && (length - j === PACKET_LENGTH || data[j + PACKET_LENGTH] === 0x47)) { - tsPackets++; - if (packetStart === -1) { - packetStart = j; - // First sync word found at offset, increase scan length (#5251) - if (packetStart !== 0) { - scanwindow = Math.min(packetStart + PACKET_LENGTH * 99, data.length - PACKET_LENGTH) + 1; - } - } - if (!foundPat) { - foundPat = parsePID(data, j) === 0; - } - // Sync word found at 0 with 3 packets, or found at offset least 2 packets up to scanwindow (#5501) - if (foundPat && tsPackets > 1 && (packetStart === 0 && tsPackets > 2 || j + PACKET_LENGTH > scanwindow)) { - return packetStart; - } - } else if (tsPackets) { - // Exit if sync word found, but does not contain contiguous packets - return -1; - } else { - break; - } - } - i++; - } - return -1; - } - - /** - * Creates a track model internal to demuxer used to drive remuxing input - */ - static createTrack(type, duration) { - return { - container: type === 'video' || type === 'audio' ? 'video/mp2t' : undefined, - type, - id: RemuxerTrackIdConfig[type], - pid: -1, - inputTimeScale: 90000, - sequenceNumber: 0, - samples: [], - dropped: 0, - duration: type === 'audio' ? duration : undefined - }; - } - - /** - * Initializes a new init segment on the demuxer/remuxer interface. Needed for discontinuities/track-switches (or at stream start) - * Resets all internal track instances of the demuxer. - */ - resetInitSegment(initSegment, audioCodec, videoCodec, trackDuration) { - this.pmtParsed = false; - this._pmtId = -1; - this._videoTrack = TSDemuxer.createTrack('video'); - this._videoTrack.duration = trackDuration; - this._audioTrack = TSDemuxer.createTrack('audio', trackDuration); - this._id3Track = TSDemuxer.createTrack('id3'); - this._txtTrack = TSDemuxer.createTrack('text'); - this._audioTrack.segmentCodec = 'aac'; - - // flush any partial content - this.aacOverFlow = null; - this.remainderData = null; - this.audioCodec = audioCodec; - this.videoCodec = videoCodec; - } - resetTimeStamp() {} - resetContiguity() { - const { - _audioTrack, - _videoTrack, - _id3Track - } = this; - if (_audioTrack) { - _audioTrack.pesData = null; - } - if (_videoTrack) { - _videoTrack.pesData = null; - } - if (_id3Track) { - _id3Track.pesData = null; - } - this.aacOverFlow = null; - this.remainderData = null; - } - demux(data, timeOffset, isSampleAes = false, flush = false) { - if (!isSampleAes) { - this.sampleAes = null; - } - let pes; - const videoTrack = this._videoTrack; - const audioTrack = this._audioTrack; - const id3Track = this._id3Track; - const textTrack = this._txtTrack; - let videoPid = videoTrack.pid; - let videoData = videoTrack.pesData; - let audioPid = audioTrack.pid; - let id3Pid = id3Track.pid; - let audioData = audioTrack.pesData; - let id3Data = id3Track.pesData; - let unknownPID = null; - let pmtParsed = this.pmtParsed; - let pmtId = this._pmtId; - let len = data.length; - if (this.remainderData) { - data = appendUint8Array(this.remainderData, data); - len = data.length; - this.remainderData = null; - } - if (len < PACKET_LENGTH && !flush) { - this.remainderData = data; - return { - audioTrack, - videoTrack, - id3Track, - textTrack - }; - } - const syncOffset = Math.max(0, TSDemuxer.syncOffset(data)); - len -= (len - syncOffset) % PACKET_LENGTH; - if (len < data.byteLength && !flush) { - this.remainderData = new Uint8Array(data.buffer, len, data.buffer.byteLength - len); - } - - // loop through TS packets - let tsPacketErrors = 0; - for (let start = syncOffset; start < len; start += PACKET_LENGTH) { - if (data[start] === 0x47) { - const stt = !!(data[start + 1] & 0x40); - 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. - let offset; - if (atf > 1) { - offset = start + 5 + data[start + 4]; - // continue if there is only adaptation field - if (offset === start + PACKET_LENGTH) { - continue; - } - } else { - offset = start + 4; - } - switch (pid) { - case videoPid: - if (stt) { - if (videoData && (pes = parsePES(videoData, this.logger))) { - if (this.videoParser === null) { - switch (videoTrack.segmentCodec) { - case 'avc': - this.videoParser = new AvcVideoParser(); - break; - case 'hevc': - { - this.videoParser = new HevcVideoParser(); - } - break; - } - } - if (this.videoParser !== null) { - this.videoParser.parsePES(videoTrack, textTrack, pes, false); - } - } - videoData = { - data: [], - size: 0 - }; - } - if (videoData) { - videoData.data.push(data.subarray(offset, start + PACKET_LENGTH)); - videoData.size += start + PACKET_LENGTH - offset; - } - break; - case audioPid: - if (stt) { - if (audioData && (pes = parsePES(audioData, this.logger))) { - switch (audioTrack.segmentCodec) { - case 'aac': - this.parseAACPES(audioTrack, pes); - break; - case 'mp3': - this.parseMPEGPES(audioTrack, pes); - break; - case 'ac3': - { - this.parseAC3PES(audioTrack, pes); - } - break; - } - } - audioData = { - data: [], - size: 0 - }; - } - if (audioData) { - audioData.data.push(data.subarray(offset, start + PACKET_LENGTH)); - audioData.size += start + PACKET_LENGTH - offset; - } - break; - case id3Pid: - if (stt) { - if (id3Data && (pes = parsePES(id3Data, this.logger))) { - this.parseID3PES(id3Track, pes); - } - id3Data = { - data: [], - size: 0 - }; - } - if (id3Data) { - id3Data.data.push(data.subarray(offset, start + PACKET_LENGTH)); - id3Data.size += start + PACKET_LENGTH - offset; - } - break; - case 0: - if (stt) { - offset += data[offset] + 1; - } - pmtId = this._pmtId = parsePAT(data, offset); - // this.logger.log('PMT PID:' + this._pmtId); - break; - case pmtId: - { - if (stt) { - offset += data[offset] + 1; - } - const parsedPIDs = parsePMT(data, offset, this.typeSupported, isSampleAes, this.observer, this.logger); - - // only update track id if track PID found while parsing PMT - // this is to avoid resetting the PID to -1 in case - // track PID transiently disappears from the stream - // this could happen in case of transient missing audio samples for example - // NOTE this is only the PID of the track as found in TS, - // but we are not using this for MP4 track IDs. - videoPid = parsedPIDs.videoPid; - if (videoPid > 0) { - videoTrack.pid = videoPid; - videoTrack.segmentCodec = parsedPIDs.segmentVideoCodec; - } - audioPid = parsedPIDs.audioPid; - if (audioPid > 0) { - audioTrack.pid = audioPid; - audioTrack.segmentCodec = parsedPIDs.segmentAudioCodec; - } - id3Pid = parsedPIDs.id3Pid; - if (id3Pid > 0) { - id3Track.pid = id3Pid; - } - if (unknownPID !== null && !pmtParsed) { - this.logger.warn(`MPEG-TS PMT found at ${start} after unknown PID '${unknownPID}'. Backtracking to sync byte @${syncOffset} to parse all TS packets.`); - 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 0x11: - case 0x1fff: - break; - default: - unknownPID = pid; - break; - } - } else { - tsPacketErrors++; - } - } - if (tsPacketErrors > 0) { - emitParsingError(this.observer, new Error(`Found ${tsPacketErrors} TS packet/s that do not start with 0x47`), undefined, this.logger); - } - videoTrack.pesData = videoData; - audioTrack.pesData = audioData; - id3Track.pesData = id3Data; - const demuxResult = { - audioTrack, - videoTrack, - id3Track, - textTrack - }; - if (flush) { - this.extractRemainingSamples(demuxResult); - } - return demuxResult; - } - flush() { - const { - remainderData - } = this; - this.remainderData = null; - let result; - if (remainderData) { - result = this.demux(remainderData, -1, false, true); - } else { - result = { - videoTrack: this._videoTrack, - audioTrack: this._audioTrack, - id3Track: this._id3Track, - textTrack: this._txtTrack - }; - } - this.extractRemainingSamples(result); - if (this.sampleAes) { - return this.decrypt(result, this.sampleAes); - } - return result; - } - extractRemainingSamples(demuxResult) { - const { - audioTrack, - videoTrack, - id3Track, - textTrack - } = demuxResult; - const videoData = videoTrack.pesData; - const audioData = audioTrack.pesData; - const id3Data = id3Track.pesData; - // try to parse last PES packets - let pes; - if (videoData && (pes = parsePES(videoData, this.logger))) { - if (this.videoParser === null) { - switch (videoTrack.segmentCodec) { - case 'avc': - this.videoParser = new AvcVideoParser(); - break; - case 'hevc': - { - this.videoParser = new HevcVideoParser(); - } - break; - } - } - if (this.videoParser !== null) { - this.videoParser.parsePES(videoTrack, textTrack, pes, true); - videoTrack.pesData = null; - } - } else { - // either avcData null or PES truncated, keep it for next frag parsing - videoTrack.pesData = videoData; - } - if (audioData && (pes = parsePES(audioData, this.logger))) { - switch (audioTrack.segmentCodec) { - case 'aac': - this.parseAACPES(audioTrack, pes); - break; - case 'mp3': - this.parseMPEGPES(audioTrack, pes); - break; - case 'ac3': - { - this.parseAC3PES(audioTrack, pes); - } - break; - } - audioTrack.pesData = null; - } else { - if (audioData != null && audioData.size) { - this.logger.log('last AAC PES packet truncated,might overlap between fragments'); - } - - // either audioData null or PES truncated, keep it for next frag parsing - audioTrack.pesData = audioData; - } - if (id3Data && (pes = parsePES(id3Data, this.logger))) { - this.parseID3PES(id3Track, pes); - id3Track.pesData = null; - } else { - // either id3Data null or PES truncated, keep it for next frag parsing - id3Track.pesData = id3Data; - } - } - demuxSampleAes(data, keyData, timeOffset) { - const demuxResult = this.demux(data, timeOffset, true, !this.config.progressive); - const sampleAes = this.sampleAes = new SampleAesDecrypter(this.observer, this.config, keyData); - return this.decrypt(demuxResult, sampleAes); - } - decrypt(demuxResult, sampleAes) { - return new Promise(resolve => { - const { - audioTrack, - videoTrack - } = demuxResult; - if (audioTrack.samples && audioTrack.segmentCodec === 'aac') { - sampleAes.decryptAacSamples(audioTrack.samples, 0, () => { - if (videoTrack.samples) { - sampleAes.decryptAvcSamples(videoTrack.samples, 0, 0, () => { - resolve(demuxResult); - }); - } else { - resolve(demuxResult); - } - }); - } else if (videoTrack.samples) { - sampleAes.decryptAvcSamples(videoTrack.samples, 0, 0, () => { - resolve(demuxResult); - }); - } - }); - } - destroy() { - if (this.observer) { - this.observer.removeAllListeners(); - } - // @ts-ignore - this.config = this.logger = this.observer = null; - this.aacOverFlow = this.videoParser = this.remainderData = this.sampleAes = null; - this._videoTrack = this._audioTrack = this._id3Track = this._txtTrack = undefined; - } - parseAACPES(track, pes) { - let startOffset = 0; - const aacOverFlow = this.aacOverFlow; - let data = pes.data; - if (aacOverFlow) { - this.aacOverFlow = null; - const frameMissingBytes = aacOverFlow.missing; - const sampleLength = aacOverFlow.sample.unit.byteLength; - // logger.log(`AAC: append overflowing ${sampleLength} bytes to beginning of new PES`); - if (frameMissingBytes === -1) { - data = appendUint8Array(aacOverFlow.sample.unit, data); - } else { - const frameOverflowBytes = sampleLength - frameMissingBytes; - aacOverFlow.sample.unit.set(data.subarray(0, frameMissingBytes), frameOverflowBytes); - track.samples.push(aacOverFlow.sample); - startOffset = aacOverFlow.missing; - } - } - // look for ADTS header (0xFFFx) - let offset; - let len; - for (offset = startOffset, len = data.length; offset < len - 1; offset++) { - if (isHeader$1(data, offset)) { - break; - } - } - // if ADTS header does not start straight from the beginning of the PES payload, raise an error - if (offset !== startOffset) { - let reason; - const recoverable = offset < len - 1; - if (recoverable) { - reason = `AAC PES did not start with ADTS header,offset:${offset}`; - } else { - reason = 'No ADTS header found in AAC PES'; - } - emitParsingError(this.observer, new Error(reason), recoverable, this.logger); - if (!recoverable) { - return; - } - } - initTrackConfig(track, this.observer, data, offset, this.audioCodec); - let pts; - if (pes.pts !== undefined) { - pts = pes.pts; - } else if (aacOverFlow) { - // if last AAC frame is overflowing, we should ensure timestamps are contiguous: - // first sample PTS should be equal to last sample PTS + frameDuration - const frameDuration = getFrameDuration(track.samplerate); - pts = aacOverFlow.sample.pts + frameDuration; - } else { - this.logger.warn('[tsdemuxer]: AAC PES unknown PTS'); - return; - } - - // scan for aac samples - let frameIndex = 0; - let frame; - while (offset < len) { - frame = appendFrame$2(track, data, offset, pts, frameIndex); - offset += frame.length; - if (!frame.missing) { - frameIndex++; - for (; offset < len - 1; offset++) { - if (isHeader$1(data, offset)) { - break; - } - } - } else { - this.aacOverFlow = frame; - break; - } - } - } - parseMPEGPES(track, pes) { - const data = pes.data; - const length = data.length; - let frameIndex = 0; - let offset = 0; - const pts = pes.pts; - if (pts === undefined) { - this.logger.warn('[tsdemuxer]: MPEG PES unknown PTS'); - return; - } - while (offset < length) { - if (isHeader(data, offset)) { - const frame = appendFrame$1(track, data, offset, pts, frameIndex); - if (frame) { - offset += frame.length; - frameIndex++; - } else { - // logger.log('Unable to parse Mpeg audio frame'); - break; - } - } else { - // nothing found, keep looking - offset++; - } - } - } - parseAC3PES(track, pes) { - { - const data = pes.data; - const pts = pes.pts; - if (pts === undefined) { - this.logger.warn('[tsdemuxer]: AC3 PES unknown PTS'); - return; - } - const length = data.length; - let frameIndex = 0; - let offset = 0; - let parsed; - while (offset < length && (parsed = appendFrame(track, data, offset, pts, frameIndex++)) > 0) { - offset += parsed; - } - } - } - parseID3PES(id3Track, pes) { - if (pes.pts === undefined) { - this.logger.warn('[tsdemuxer]: ID3 PES unknown PTS'); - return; - } - const id3Sample = _extends({}, pes, { - type: this._videoTrack ? MetadataSchema.emsg : MetadataSchema.audioId3, - duration: Number.POSITIVE_INFINITY - }); - id3Track.samples.push(id3Sample); - } -} -function parsePID(data, offset) { - // pid is a 13-bit field starting at the last bit of TS[1] - return ((data[offset + 1] & 0x1f) << 8) + data[offset + 2]; -} -function parsePAT(data, offset) { - // skip the PSI header and parse the first PMT entry - return (data[offset + 10] & 0x1f) << 8 | data[offset + 11]; -} -function parsePMT(data, offset, typeSupported, isSampleAes, observer, logger) { - const result = { - audioPid: -1, - videoPid: -1, - id3Pid: -1, - segmentVideoCodec: 'avc', - segmentAudioCodec: 'aac' - }; - const sectionLength = (data[offset + 1] & 0x0f) << 8 | data[offset + 2]; - const tableEnd = offset + 3 + sectionLength - 4; - // to determine where the table is, we have to figure out how - // long the program info descriptors are - const programInfoLength = (data[offset + 10] & 0x0f) << 8 | data[offset + 11]; - // advance the offset to the first entry in the mapping table - offset += 12 + programInfoLength; - while (offset < tableEnd) { - const pid = parsePID(data, offset); - const esInfoLength = (data[offset + 3] & 0x0f) << 8 | data[offset + 4]; - switch (data[offset]) { - case 0xcf: - // SAMPLE-AES AAC - if (!isSampleAes) { - logEncryptedSamplesFoundInUnencryptedStream('ADTS AAC', logger); - break; - } - /* falls through */ - case 0x0f: - // ISO/IEC 13818-7 ADTS AAC (MPEG-2 lower bit-rate audio) - // logger.log('AAC PID:' + pid); - if (result.audioPid === -1) { - result.audioPid = pid; - } - break; - - // Packetized metadata (ID3) - case 0x15: - // logger.log('ID3 PID:' + pid); - if (result.id3Pid === -1) { - result.id3Pid = pid; - } - break; - case 0xdb: - // SAMPLE-AES AVC - if (!isSampleAes) { - logEncryptedSamplesFoundInUnencryptedStream('H.264', logger); - break; - } - /* falls through */ - case 0x1b: - // ITU-T Rec. H.264 and ISO/IEC 14496-10 (lower bit-rate video) - // logger.log('AVC PID:' + pid); - if (result.videoPid === -1) { - result.videoPid = pid; - result.segmentVideoCodec = 'avc'; - } - break; - - // ISO/IEC 11172-3 (MPEG-1 audio) - // or ISO/IEC 13818-3 (MPEG-2 halved sample rate audio) - case 0x03: - case 0x04: - // logger.log('MPEG PID:' + pid); - if (!typeSupported.mpeg && !typeSupported.mp3) { - logger.log('MPEG audio found, not supported in this browser'); - } else if (result.audioPid === -1) { - result.audioPid = pid; - result.segmentAudioCodec = 'mp3'; - } - break; - case 0xc1: - // SAMPLE-AES AC3 - if (!isSampleAes) { - logEncryptedSamplesFoundInUnencryptedStream('AC-3', logger); - break; - } - /* falls through */ - case 0x81: - { - if (!typeSupported.ac3) { - logger.log('AC-3 audio found, not supported in this browser'); - } else if (result.audioPid === -1) { - result.audioPid = pid; - result.segmentAudioCodec = 'ac3'; - } - } - break; - case 0x06: - // stream_type 6 can mean a lot of different things in case of DVB. - // We need to look at the descriptors. Right now, we're only interested - // in AC-3 audio, so we do the descriptor parsing only when we don't have - // an audio PID yet. - if (result.audioPid === -1 && esInfoLength > 0) { - let parsePos = offset + 5; - let remaining = esInfoLength; - while (remaining > 2) { - const descriptorId = data[parsePos]; - switch (descriptorId) { - case 0x6a: - // DVB Descriptor for AC-3 - { - if (typeSupported.ac3 !== true) { - logger.log('AC-3 audio found, not supported in this browser for now'); - } else { - result.audioPid = pid; - result.segmentAudioCodec = 'ac3'; - } - } - break; - } - const descriptorLen = data[parsePos + 1] + 2; - parsePos += descriptorLen; - remaining -= descriptorLen; - } - } - break; - case 0xc2: // SAMPLE-AES EC3 - /* falls through */ - case 0x87: - emitParsingError(observer, new Error('Unsupported EC-3 in M2TS found'), undefined, logger); - return result; - case 0x24: - // ITU-T Rec. H.265 and ISO/IEC 23008-2 (HEVC) - { - if (result.videoPid === -1) { - result.videoPid = pid; - result.segmentVideoCodec = 'hevc'; - logger.log('HEVC in M2TS found'); - } - } - break; - } - // move to the next table entry - // skip past the elementary stream descriptors, if present - offset += esInfoLength + 5; - } - return result; -} -function emitParsingError(observer, error, levelRetry, logger) { - logger.warn(`parsing error: ${error.message}`); - observer.emit(Events.ERROR, Events.ERROR, { - type: ErrorTypes.MEDIA_ERROR, - details: ErrorDetails.FRAG_PARSING_ERROR, - fatal: false, - levelRetry, - error, - reason: error.message - }); -} -function logEncryptedSamplesFoundInUnencryptedStream(type, logger) { - logger.log(`${type} with AES-128-CBC encryption found in unencrypted stream`); -} -function parsePES(stream, logger) { - let i = 0; - let frag; - let pesLen; - let pesHdrLen; - let pesPts; - let pesDts; - const data = stream.data; - // safety check - if (!stream || stream.size === 0) { - return null; - } - - // we might need up to 19 bytes to read PES header - // if first chunk of data is less than 19 bytes, let's merge it with following ones until we get 19 bytes - // usually only one merge is needed (and this is rare ...) - while (data[0].length < 19 && data.length > 1) { - data[0] = appendUint8Array(data[0], data[1]); - data.splice(1, 1); - } - // retrieve PTS/DTS from first fragment - frag = data[0]; - const pesPrefix = (frag[0] << 16) + (frag[1] << 8) + frag[2]; - if (pesPrefix === 1) { - pesLen = (frag[4] << 8) + frag[5]; - // if PES parsed length is not zero and greater than total received length, stop parsing. PES might be truncated - // minus 6 : PES header size - if (pesLen && pesLen > stream.size - 6) { - return null; - } - const pesFlags = frag[7]; - if (pesFlags & 0xc0) { - /* PES header described here : http://dvd.sourceforge.net/dvdinfo/pes-hdr.html - as PTS / DTS is 33 bit we cannot use bitwise operator in JS, - as Bitwise operators treat their operands as a sequence of 32 bits */ - pesPts = (frag[9] & 0x0e) * 536870912 + - // 1 << 29 - (frag[10] & 0xff) * 4194304 + - // 1 << 22 - (frag[11] & 0xfe) * 16384 + - // 1 << 14 - (frag[12] & 0xff) * 128 + - // 1 << 7 - (frag[13] & 0xfe) / 2; - if (pesFlags & 0x40) { - pesDts = (frag[14] & 0x0e) * 536870912 + - // 1 << 29 - (frag[15] & 0xff) * 4194304 + - // 1 << 22 - (frag[16] & 0xfe) * 16384 + - // 1 << 14 - (frag[17] & 0xff) * 128 + - // 1 << 7 - (frag[18] & 0xfe) / 2; - if (pesPts - pesDts > 60 * 90000) { - logger.warn(`${Math.round((pesPts - pesDts) / 90000)}s delta between PTS and DTS, align them`); - pesPts = pesDts; - } - } else { - pesDts = pesPts; - } - } - pesHdrLen = frag[8]; - // 9 bytes : 6 bytes for PES header + 3 bytes for PES extension - let payloadStartOffset = pesHdrLen + 9; - if (stream.size <= payloadStartOffset) { - return null; - } - stream.size -= payloadStartOffset; - // reassemble PES packet - const pesData = new Uint8Array(stream.size); - for (let j = 0, dataLen = data.length; j < dataLen; j++) { - frag = data[j]; - let len = frag.byteLength; - if (payloadStartOffset) { - if (payloadStartOffset > len) { - // trim full frag if PES header bigger than frag - payloadStartOffset -= len; - continue; - } else { - // trim partial frag if PES header smaller than frag - frag = frag.subarray(payloadStartOffset); - len -= payloadStartOffset; - payloadStartOffset = 0; - } - } - pesData.set(frag, i); - i += len; - } - if (pesLen) { - // payload size : remove PES header + PES extension - pesLen -= pesHdrLen + 3; - } - return { - data: pesData, - pts: pesPts, - dts: pesDts, - len: pesLen - }; - } - return null; -} - -/** - * MP3 demuxer - */ -class MP3Demuxer extends BaseAudioDemuxer { - resetInitSegment(initSegment, audioCodec, videoCodec, trackDuration) { - super.resetInitSegment(initSegment, audioCodec, videoCodec, trackDuration); - this._audioTrack = { - container: 'audio/mpeg', - type: 'audio', - id: 2, - pid: -1, - sequenceNumber: 0, - segmentCodec: 'mp3', - samples: [], - manifestCodec: audioCodec, - duration: trackDuration, - inputTimeScale: 90000, - dropped: 0 - }; - } - static probe(data) { - if (!data) { - return false; - } - - // check if data contains ID3 timestamp and MPEG sync word - // Look for MPEG header | 1111 1111 | 111X XYZX | where X can be either 0 or 1 and Y or Z should be 1 - // Layer bits (position 14 and 15) in header should be always different from 0 (Layer I or Layer II or Layer III) - // More info http://www.mp3-tech.org/programmer/frame_header.html - const id3Data = getId3Data(data, 0); - let offset = (id3Data == null ? void 0 : id3Data.length) || 0; - - // Check for ac-3|ec-3 sync bytes and return false if present - if (id3Data && data[offset] === 0x0b && data[offset + 1] === 0x77 && getId3Timestamp(id3Data) !== undefined && - // check the bsid to confirm ac-3 or ec-3 (not mp3) - getAudioBSID(data, offset) <= 16) { - return false; - } - for (let length = data.length; offset < length; offset++) { - if (probe(data, offset)) { - logger.log('MPEG Audio sync word found !'); - return true; - } - } - return false; - } - canParse(data, offset) { - return canParse(data, offset); - } - appendFrame(track, data, offset) { - if (this.basePTS === null) { - return; - } - return appendFrame$1(track, data, offset, this.basePTS, this.frameIndex); - } -} - -/** - * AAC helper - */ - -class AAC { - static getSilentFrame(codec, channelCount) { - switch (codec) { - case 'mp4a.40.2': - if (channelCount === 1) { - return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x23, 0x80]); - } else if (channelCount === 2) { - return new Uint8Array([0x21, 0x00, 0x49, 0x90, 0x02, 0x19, 0x00, 0x23, 0x80]); - } else if (channelCount === 3) { - return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x8e]); - } else if (channelCount === 4) { - return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x80, 0x2c, 0x80, 0x08, 0x02, 0x38]); - } else if (channelCount === 5) { - return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x82, 0x30, 0x04, 0x99, 0x00, 0x21, 0x90, 0x02, 0x38]); - } else if (channelCount === 6) { - return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x82, 0x30, 0x04, 0x99, 0x00, 0x21, 0x90, 0x02, 0x00, 0xb2, 0x00, 0x20, 0x08, 0xe0]); - } - break; - // handle HE-AAC below (mp4a.40.5 / mp4a.40.29) - default: - if (channelCount === 1) { - // ffmpeg -y -f lavfi -i "aevalsrc=0:d=0.05" -c:a libfdk_aac -profile:a aac_he -b:a 4k output.aac && hexdump -v -e '16/1 "0x%x," "\n"' -v output.aac - return new Uint8Array([0x1, 0x40, 0x22, 0x80, 0xa3, 0x4e, 0xe6, 0x80, 0xba, 0x8, 0x0, 0x0, 0x0, 0x1c, 0x6, 0xf1, 0xc1, 0xa, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5e]); - } else if (channelCount === 2) { - // ffmpeg -y -f lavfi -i "aevalsrc=0|0:d=0.05" -c:a libfdk_aac -profile:a aac_he_v2 -b:a 4k output.aac && hexdump -v -e '16/1 "0x%x," "\n"' -v output.aac - return new Uint8Array([0x1, 0x40, 0x22, 0x80, 0xa3, 0x5e, 0xe6, 0x80, 0xba, 0x8, 0x0, 0x0, 0x0, 0x0, 0x95, 0x0, 0x6, 0xf1, 0xa1, 0xa, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5e]); - } else if (channelCount === 3) { - // ffmpeg -y -f lavfi -i "aevalsrc=0|0|0:d=0.05" -c:a libfdk_aac -profile:a aac_he_v2 -b:a 4k output.aac && hexdump -v -e '16/1 "0x%x," "\n"' -v output.aac - return new Uint8Array([0x1, 0x40, 0x22, 0x80, 0xa3, 0x5e, 0xe6, 0x80, 0xba, 0x8, 0x0, 0x0, 0x0, 0x0, 0x95, 0x0, 0x6, 0xf1, 0xa1, 0xa, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5e]); - } - break; - } - return undefined; - } -} - -/** - * Generate MP4 Box - */ - -const UINT32_MAX = Math.pow(2, 32) - 1; -class MP4 { - static init() { - MP4.types = { - avc1: [], - // codingname - avcC: [], - hvc1: [], - hvcC: [], - btrt: [], - dinf: [], - dref: [], - esds: [], - ftyp: [], - hdlr: [], - mdat: [], - mdhd: [], - mdia: [], - mfhd: [], - minf: [], - moof: [], - moov: [], - mp4a: [], - '.mp3': [], - dac3: [], - 'ac-3': [], - mvex: [], - mvhd: [], - pasp: [], - sdtp: [], - stbl: [], - stco: [], - stsc: [], - stsd: [], - stsz: [], - stts: [], - tfdt: [], - tfhd: [], - traf: [], - trak: [], - trun: [], - trex: [], - tkhd: [], - vmhd: [], - smhd: [] - }; - let i; - for (i in MP4.types) { - if (MP4.types.hasOwnProperty(i)) { - MP4.types[i] = [i.charCodeAt(0), i.charCodeAt(1), i.charCodeAt(2), i.charCodeAt(3)]; - } - } - const videoHdlr = new Uint8Array([0x00, - // version 0 - 0x00, 0x00, 0x00, - // flags - 0x00, 0x00, 0x00, 0x00, - // pre_defined - 0x76, 0x69, 0x64, 0x65, - // handler_type: 'vide' - 0x00, 0x00, 0x00, 0x00, - // reserved - 0x00, 0x00, 0x00, 0x00, - // reserved - 0x00, 0x00, 0x00, 0x00, - // reserved - 0x56, 0x69, 0x64, 0x65, 0x6f, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'VideoHandler' - ]); - const audioHdlr = new Uint8Array([0x00, - // version 0 - 0x00, 0x00, 0x00, - // flags - 0x00, 0x00, 0x00, 0x00, - // pre_defined - 0x73, 0x6f, 0x75, 0x6e, - // handler_type: 'soun' - 0x00, 0x00, 0x00, 0x00, - // reserved - 0x00, 0x00, 0x00, 0x00, - // reserved - 0x00, 0x00, 0x00, 0x00, - // reserved - 0x53, 0x6f, 0x75, 0x6e, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'SoundHandler' - ]); - MP4.HDLR_TYPES = { - video: videoHdlr, - audio: audioHdlr - }; - const dref = new Uint8Array([0x00, - // version 0 - 0x00, 0x00, 0x00, - // flags - 0x00, 0x00, 0x00, 0x01, - // entry_count - 0x00, 0x00, 0x00, 0x0c, - // entry_size - 0x75, 0x72, 0x6c, 0x20, - // 'url' type - 0x00, - // version 0 - 0x00, 0x00, 0x01 // entry_flags - ]); - const stco = new Uint8Array([0x00, - // version - 0x00, 0x00, 0x00, - // flags - 0x00, 0x00, 0x00, 0x00 // entry_count - ]); - MP4.STTS = MP4.STSC = MP4.STCO = stco; - MP4.STSZ = new Uint8Array([0x00, - // version - 0x00, 0x00, 0x00, - // flags - 0x00, 0x00, 0x00, 0x00, - // sample_size - 0x00, 0x00, 0x00, 0x00 // sample_count - ]); - MP4.VMHD = new Uint8Array([0x00, - // version - 0x00, 0x00, 0x01, - // flags - 0x00, 0x00, - // graphicsmode - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // opcolor - ]); - MP4.SMHD = new Uint8Array([0x00, - // version - 0x00, 0x00, 0x00, - // flags - 0x00, 0x00, - // balance - 0x00, 0x00 // reserved - ]); - MP4.STSD = new Uint8Array([0x00, - // version 0 - 0x00, 0x00, 0x00, - // flags - 0x00, 0x00, 0x00, 0x01]); // entry_count - - const majorBrand = new Uint8Array([105, 115, 111, 109]); // isom - const avc1Brand = new Uint8Array([97, 118, 99, 49]); // avc1 - const minorVersion = new Uint8Array([0, 0, 0, 1]); - MP4.FTYP = MP4.box(MP4.types.ftyp, majorBrand, minorVersion, majorBrand, avc1Brand); - MP4.DINF = MP4.box(MP4.types.dinf, MP4.box(MP4.types.dref, dref)); - } - static box(type, ...payload) { - let size = 8; - let i = payload.length; - const len = i; - // calculate the total size we need to allocate - 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); - // copy the payload into the result - for (i = 0, size = 8; i < len; i++) { - // copy payload[i] array @ offset size - result.set(payload[i], size); - size += payload[i].byteLength; - } - return result; - } - static hdlr(type) { - return MP4.box(MP4.types.hdlr, MP4.HDLR_TYPES[type]); - } - static mdat(data) { - return MP4.box(MP4.types.mdat, data); - } - static mdhd(timescale, duration) { - duration *= timescale; - const upperWordDuration = Math.floor(duration / (UINT32_MAX + 1)); - const lowerWordDuration = Math.floor(duration % (UINT32_MAX + 1)); - return MP4.box(MP4.types.mdhd, new Uint8Array([0x01, - // version 1 - 0x00, 0x00, 0x00, - // flags - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, - // creation_time - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, - // modification_time - timescale >> 24 & 0xff, timescale >> 16 & 0xff, timescale >> 8 & 0xff, timescale & 0xff, - // timescale - upperWordDuration >> 24, upperWordDuration >> 16 & 0xff, upperWordDuration >> 8 & 0xff, upperWordDuration & 0xff, lowerWordDuration >> 24, lowerWordDuration >> 16 & 0xff, lowerWordDuration >> 8 & 0xff, lowerWordDuration & 0xff, 0x55, 0xc4, - // 'und' language (undetermined) - 0x00, 0x00])); - } - static mdia(track) { - return MP4.box(MP4.types.mdia, MP4.mdhd(track.timescale, track.duration), MP4.hdlr(track.type), MP4.minf(track)); - } - static mfhd(sequenceNumber) { - return MP4.box(MP4.types.mfhd, new Uint8Array([0x00, 0x00, 0x00, 0x00, - // flags - sequenceNumber >> 24, sequenceNumber >> 16 & 0xff, sequenceNumber >> 8 & 0xff, sequenceNumber & 0xff // sequence_number - ])); - } - static minf(track) { - if (track.type === 'audio') { - return MP4.box(MP4.types.minf, MP4.box(MP4.types.smhd, MP4.SMHD), MP4.DINF, MP4.stbl(track)); - } else { - return MP4.box(MP4.types.minf, MP4.box(MP4.types.vmhd, MP4.VMHD), MP4.DINF, MP4.stbl(track)); - } - } - static moof(sn, baseMediaDecodeTime, track) { - return MP4.box(MP4.types.moof, MP4.mfhd(sn), MP4.traf(track, baseMediaDecodeTime)); - } - static moov(tracks) { - let i = tracks.length; - const boxes = []; - while (i--) { - boxes[i] = MP4.trak(tracks[i]); - } - return MP4.box.apply(null, [MP4.types.moov, MP4.mvhd(tracks[0].timescale, tracks[0].duration)].concat(boxes).concat(MP4.mvex(tracks))); - } - static mvex(tracks) { - let i = tracks.length; - const boxes = []; - while (i--) { - boxes[i] = MP4.trex(tracks[i]); - } - return MP4.box.apply(null, [MP4.types.mvex, ...boxes]); - } - static mvhd(timescale, duration) { - duration *= timescale; - const upperWordDuration = Math.floor(duration / (UINT32_MAX + 1)); - const lowerWordDuration = Math.floor(duration % (UINT32_MAX + 1)); - const bytes = new Uint8Array([0x01, - // version 1 - 0x00, 0x00, 0x00, - // flags - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, - // creation_time - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, - // modification_time - timescale >> 24 & 0xff, timescale >> 16 & 0xff, timescale >> 8 & 0xff, timescale & 0xff, - // timescale - upperWordDuration >> 24, upperWordDuration >> 16 & 0xff, upperWordDuration >> 8 & 0xff, upperWordDuration & 0xff, lowerWordDuration >> 24, lowerWordDuration >> 16 & 0xff, lowerWordDuration >> 8 & 0xff, lowerWordDuration & 0xff, 0x00, 0x01, 0x00, 0x00, - // 1.0 rate - 0x01, 0x00, - // 1.0 volume - 0x00, 0x00, - // reserved - 0x00, 0x00, 0x00, 0x00, - // reserved - 0x00, 0x00, 0x00, 0x00, - // reserved - 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, - // transformation: unity matrix - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // pre_defined - 0xff, 0xff, 0xff, 0xff // next_track_ID - ]); - return MP4.box(MP4.types.mvhd, bytes); - } - static sdtp(track) { - const samples = track.samples || []; - const bytes = new Uint8Array(4 + samples.length); - let i; - let flags; - // leave the full box header (4 bytes) all zero - // write the sample table - for (i = 0; i < samples.length; i++) { - flags = samples[i].flags; - bytes[i + 4] = flags.dependsOn << 4 | flags.isDependedOn << 2 | flags.hasRedundancy; - } - return MP4.box(MP4.types.sdtp, bytes); - } - static stbl(track) { - return MP4.box(MP4.types.stbl, MP4.stsd(track), MP4.box(MP4.types.stts, MP4.STTS), MP4.box(MP4.types.stsc, MP4.STSC), MP4.box(MP4.types.stsz, MP4.STSZ), MP4.box(MP4.types.stco, MP4.STCO)); - } - static avc1(track) { - let sps = []; - let pps = []; - let i; - let data; - let len; - // assemble the SPSs - - for (i = 0; i < track.sps.length; i++) { - data = track.sps[i]; - len = data.byteLength; - sps.push(len >>> 8 & 0xff); - sps.push(len & 0xff); - - // SPS - sps = sps.concat(Array.prototype.slice.call(data)); - } - - // assemble the PPSs - for (i = 0; i < track.pps.length; i++) { - data = track.pps[i]; - len = data.byteLength; - pps.push(len >>> 8 & 0xff); - pps.push(len & 0xff); - pps = pps.concat(Array.prototype.slice.call(data)); - } - const avcc = MP4.box(MP4.types.avcC, new Uint8Array([0x01, - // version - sps[3], - // profile - sps[4], - // profile compat - sps[5], - // level - 0xfc | 3, - // lengthSizeMinusOne, hard-coded to 4 bytes - 0xe0 | track.sps.length // 3bit reserved (111) + numOfSequenceParameterSets - ].concat(sps).concat([track.pps.length // numOfPictureParameterSets - ]).concat(pps))); // "PPS" - const width = track.width; - const height = track.height; - const hSpacing = track.pixelRatio[0]; - const vSpacing = track.pixelRatio[1]; - return MP4.box(MP4.types.avc1, new Uint8Array([0x00, 0x00, 0x00, - // reserved - 0x00, 0x00, 0x00, - // reserved - 0x00, 0x01, - // data_reference_index - 0x00, 0x00, - // pre_defined - 0x00, 0x00, - // reserved - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // pre_defined - width >> 8 & 0xff, width & 0xff, - // width - height >> 8 & 0xff, height & 0xff, - // height - 0x00, 0x48, 0x00, 0x00, - // horizresolution - 0x00, 0x48, 0x00, 0x00, - // vertresolution - 0x00, 0x00, 0x00, 0x00, - // reserved - 0x00, 0x01, - // frame_count - 0x12, 0x64, 0x61, 0x69, 0x6c, - // dailymotion/hls.js - 0x79, 0x6d, 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x68, 0x6c, 0x73, 0x2e, 0x6a, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // compressorname - 0x00, 0x18, - // depth = 24 - 0x11, 0x11]), - // pre_defined = -1 - avcc, MP4.box(MP4.types.btrt, new Uint8Array([0x00, 0x1c, 0x9c, 0x80, - // bufferSizeDB - 0x00, 0x2d, 0xc6, 0xc0, - // maxBitrate - 0x00, 0x2d, 0xc6, 0xc0])), - // avgBitrate - MP4.box(MP4.types.pasp, new Uint8Array([hSpacing >> 24, - // hSpacing - hSpacing >> 16 & 0xff, hSpacing >> 8 & 0xff, hSpacing & 0xff, vSpacing >> 24, - // vSpacing - vSpacing >> 16 & 0xff, vSpacing >> 8 & 0xff, vSpacing & 0xff]))); - } - static esds(track) { - const config = track.config; - return new Uint8Array([0x00, - // version 0 - 0x00, 0x00, 0x00, - // flags - - 0x03, - // descriptor_type - 0x19, - // length - - 0x00, 0x01, - // es_id - - 0x00, - // stream_priority - - 0x04, - // descriptor_type - 0x11, - // length - 0x40, - // codec : mpeg4_audio - 0x15, - // stream_type - 0x00, 0x00, 0x00, - // buffer_size - 0x00, 0x00, 0x00, 0x00, - // maxBitrate - 0x00, 0x00, 0x00, 0x00, - // avgBitrate - - 0x05, - // descriptor_type - 0x02, - // length - ...config, 0x06, 0x01, 0x02 // GASpecificConfig)); // length + audio config descriptor - ]); - } - static audioStsd(track) { - const samplerate = track.samplerate; - return new Uint8Array([0x00, 0x00, 0x00, - // reserved - 0x00, 0x00, 0x00, - // reserved - 0x00, 0x01, - // data_reference_index - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // reserved - 0x00, track.channelCount, - // channelcount - 0x00, 0x10, - // sampleSize:16bits - 0x00, 0x00, 0x00, 0x00, - // reserved2 - samplerate >> 8 & 0xff, samplerate & 0xff, - // - 0x00, 0x00]); - } - static mp4a(track) { - return MP4.box(MP4.types.mp4a, MP4.audioStsd(track), MP4.box(MP4.types.esds, MP4.esds(track))); - } - static mp3(track) { - return MP4.box(MP4.types['.mp3'], MP4.audioStsd(track)); - } - static ac3(track) { - return MP4.box(MP4.types['ac-3'], MP4.audioStsd(track), MP4.box(MP4.types.dac3, track.config)); - } - static stsd(track) { - if (track.type === 'audio') { - if (track.segmentCodec === 'mp3' && track.codec === 'mp3') { - return MP4.box(MP4.types.stsd, MP4.STSD, MP4.mp3(track)); - } - if (track.segmentCodec === 'ac3') { - return MP4.box(MP4.types.stsd, MP4.STSD, MP4.ac3(track)); - } - return MP4.box(MP4.types.stsd, MP4.STSD, MP4.mp4a(track)); - } else if (track.segmentCodec === 'avc') { - return MP4.box(MP4.types.stsd, MP4.STSD, MP4.avc1(track)); - } else { - return MP4.box(MP4.types.stsd, MP4.STSD, MP4.hvc1(track)); - } - } - static tkhd(track) { - const id = track.id; - const duration = track.duration * track.timescale; - const width = track.width; - const height = track.height; - const upperWordDuration = Math.floor(duration / (UINT32_MAX + 1)); - const lowerWordDuration = Math.floor(duration % (UINT32_MAX + 1)); - return MP4.box(MP4.types.tkhd, new Uint8Array([0x01, - // version 1 - 0x00, 0x00, 0x07, - // flags - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, - // creation_time - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, - // modification_time - id >> 24 & 0xff, id >> 16 & 0xff, id >> 8 & 0xff, id & 0xff, - // track_ID - 0x00, 0x00, 0x00, 0x00, - // reserved - upperWordDuration >> 24, upperWordDuration >> 16 & 0xff, upperWordDuration >> 8 & 0xff, upperWordDuration & 0xff, lowerWordDuration >> 24, lowerWordDuration >> 16 & 0xff, lowerWordDuration >> 8 & 0xff, lowerWordDuration & 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // reserved - 0x00, 0x00, - // layer - 0x00, 0x00, - // alternate_group - 0x00, 0x00, - // non-audio track volume - 0x00, 0x00, - // reserved - 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, - // transformation: unity matrix - width >> 8 & 0xff, width & 0xff, 0x00, 0x00, - // width - height >> 8 & 0xff, height & 0xff, 0x00, 0x00 // height - ])); - } - static traf(track, baseMediaDecodeTime) { - const sampleDependencyTable = MP4.sdtp(track); - const id = track.id; - const upperWordBaseMediaDecodeTime = Math.floor(baseMediaDecodeTime / (UINT32_MAX + 1)); - const lowerWordBaseMediaDecodeTime = Math.floor(baseMediaDecodeTime % (UINT32_MAX + 1)); - return MP4.box(MP4.types.traf, MP4.box(MP4.types.tfhd, new Uint8Array([0x00, - // version 0 - 0x00, 0x00, 0x00, - // flags - id >> 24, id >> 16 & 0xff, id >> 8 & 0xff, id & 0xff // track_ID - ])), MP4.box(MP4.types.tfdt, new Uint8Array([0x01, - // version 1 - 0x00, 0x00, 0x00, - // flags - upperWordBaseMediaDecodeTime >> 24, upperWordBaseMediaDecodeTime >> 16 & 0xff, upperWordBaseMediaDecodeTime >> 8 & 0xff, upperWordBaseMediaDecodeTime & 0xff, lowerWordBaseMediaDecodeTime >> 24, lowerWordBaseMediaDecodeTime >> 16 & 0xff, lowerWordBaseMediaDecodeTime >> 8 & 0xff, lowerWordBaseMediaDecodeTime & 0xff])), MP4.trun(track, sampleDependencyTable.length + 16 + - // tfhd - 20 + - // tfdt - 8 + - // traf header - 16 + - // mfhd - 8 + - // moof header - 8), - // mdat header - sampleDependencyTable); - } - - /** - * Generate a track box. - * @param track a track definition - */ - static trak(track) { - track.duration = track.duration || 0xffffffff; - return MP4.box(MP4.types.trak, MP4.tkhd(track), MP4.mdia(track)); - } - static trex(track) { - const id = track.id; - return MP4.box(MP4.types.trex, new Uint8Array([0x00, - // version 0 - 0x00, 0x00, 0x00, - // flags - id >> 24, id >> 16 & 0xff, id >> 8 & 0xff, id & 0xff, - // track_ID - 0x00, 0x00, 0x00, 0x01, - // default_sample_description_index - 0x00, 0x00, 0x00, 0x00, - // default_sample_duration - 0x00, 0x00, 0x00, 0x00, - // default_sample_size - 0x00, 0x01, 0x00, 0x01 // default_sample_flags - ])); - } - static trun(track, offset) { - const samples = track.samples || []; - const len = samples.length; - const arraylen = 12 + 16 * len; - const array = new Uint8Array(arraylen); - let i; - let sample; - let duration; - let size; - let flags; - let cts; - offset += 8 + arraylen; - array.set([track.type === 'video' ? 0x01 : 0x00, - // version 1 for video with signed-int sample_composition_time_offset - 0x00, 0x0f, 0x01, - // flags - len >>> 24 & 0xff, len >>> 16 & 0xff, len >>> 8 & 0xff, len & 0xff, - // sample_count - offset >>> 24 & 0xff, offset >>> 16 & 0xff, offset >>> 8 & 0xff, offset & 0xff // data_offset - ], 0); - for (i = 0; i < len; i++) { - sample = samples[i]; - duration = sample.duration; - size = sample.size; - flags = sample.flags; - cts = sample.cts; - array.set([duration >>> 24 & 0xff, duration >>> 16 & 0xff, duration >>> 8 & 0xff, duration & 0xff, - // sample_duration - size >>> 24 & 0xff, size >>> 16 & 0xff, size >>> 8 & 0xff, size & 0xff, - // sample_size - flags.isLeading << 2 | flags.dependsOn, flags.isDependedOn << 6 | flags.hasRedundancy << 4 | flags.paddingValue << 1 | flags.isNonSync, flags.degradPrio & 0xf0 << 8, flags.degradPrio & 0x0f, - // sample_flags - cts >>> 24 & 0xff, cts >>> 16 & 0xff, cts >>> 8 & 0xff, cts & 0xff // sample_composition_time_offset - ], 12 + 16 * i); - } - return MP4.box(MP4.types.trun, array); - } - static initSegment(tracks) { - if (!MP4.types) { - MP4.init(); - } - const movie = MP4.moov(tracks); - const result = appendUint8Array(MP4.FTYP, movie); - return result; - } - static hvc1(track) { - const ps = track.params; - const units = [track.vps, track.sps, track.pps]; - const NALuLengthSize = 4; - const config = new Uint8Array([0x01, ps.general_profile_space << 6 | (ps.general_tier_flag ? 32 : 0) | ps.general_profile_idc, ps.general_profile_compatibility_flags[0], ps.general_profile_compatibility_flags[1], ps.general_profile_compatibility_flags[2], ps.general_profile_compatibility_flags[3], ps.general_constraint_indicator_flags[0], ps.general_constraint_indicator_flags[1], ps.general_constraint_indicator_flags[2], ps.general_constraint_indicator_flags[3], ps.general_constraint_indicator_flags[4], ps.general_constraint_indicator_flags[5], ps.general_level_idc, 240 | ps.min_spatial_segmentation_idc >> 8, 255 & ps.min_spatial_segmentation_idc, 252 | ps.parallelismType, 252 | ps.chroma_format_idc, 248 | ps.bit_depth_luma_minus8, 248 | ps.bit_depth_chroma_minus8, 0x00, parseInt(ps.frame_rate.fps), NALuLengthSize - 1 | ps.temporal_id_nested << 2 | ps.num_temporal_layers << 3 | (ps.frame_rate.fixed ? 64 : 0), units.length]); - - // compute hvcC size in bytes - let length = config.length; - for (let i = 0; i < units.length; i += 1) { - length += 3; - for (let j = 0; j < units[i].length; j += 1) { - length += 2 + units[i][j].length; - } - } - const hvcC = new Uint8Array(length); - hvcC.set(config, 0); - length = config.length; - // append parameter set units: one vps, one or more sps and pps - const iMax = units.length - 1; - for (let i = 0; i < units.length; i += 1) { - hvcC.set(new Uint8Array([32 + i | (i === iMax ? 128 : 0), 0x00, units[i].length]), length); - length += 3; - for (let j = 0; j < units[i].length; j += 1) { - hvcC.set(new Uint8Array([units[i][j].length >> 8, units[i][j].length & 255]), length); - length += 2; - hvcC.set(units[i][j], length); - length += units[i][j].length; - } - } - const hvcc = MP4.box(MP4.types.hvcC, hvcC); - const width = track.width; - const height = track.height; - const hSpacing = track.pixelRatio[0]; - const vSpacing = track.pixelRatio[1]; - return MP4.box(MP4.types.hvc1, new Uint8Array([0x00, 0x00, 0x00, - // reserved - 0x00, 0x00, 0x00, - // reserved - 0x00, 0x01, - // data_reference_index - 0x00, 0x00, - // pre_defined - 0x00, 0x00, - // reserved - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // pre_defined - width >> 8 & 0xff, width & 0xff, - // width - height >> 8 & 0xff, height & 0xff, - // height - 0x00, 0x48, 0x00, 0x00, - // horizresolution - 0x00, 0x48, 0x00, 0x00, - // vertresolution - 0x00, 0x00, 0x00, 0x00, - // reserved - 0x00, 0x01, - // frame_count - 0x12, 0x64, 0x61, 0x69, 0x6c, - // dailymotion/hls.js - 0x79, 0x6d, 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x68, 0x6c, 0x73, 0x2e, 0x6a, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // compressorname - 0x00, 0x18, - // depth = 24 - 0x11, 0x11]), - // pre_defined = -1 - hvcc, MP4.box(MP4.types.btrt, new Uint8Array([0x00, 0x1c, 0x9c, 0x80, - // bufferSizeDB - 0x00, 0x2d, 0xc6, 0xc0, - // maxBitrate - 0x00, 0x2d, 0xc6, 0xc0])), - // avgBitrate - MP4.box(MP4.types.pasp, new Uint8Array([hSpacing >> 24, - // hSpacing - hSpacing >> 16 & 0xff, hSpacing >> 8 & 0xff, hSpacing & 0xff, vSpacing >> 24, - // vSpacing - vSpacing >> 16 & 0xff, vSpacing >> 8 & 0xff, vSpacing & 0xff]))); - } -} -MP4.types = void 0; -MP4.HDLR_TYPES = void 0; -MP4.STTS = void 0; -MP4.STSC = void 0; -MP4.STCO = void 0; -MP4.STSZ = void 0; -MP4.VMHD = void 0; -MP4.SMHD = void 0; -MP4.STSD = void 0; -MP4.FTYP = void 0; -MP4.DINF = void 0; - -const MPEG_TS_CLOCK_FREQ_HZ = 90000; -function toTimescaleFromBase(baseTime, destScale, srcBase = 1, round = false) { - const result = baseTime * destScale * srcBase; // equivalent to `(value * scale) / (1 / base)` - return round ? Math.round(result) : result; -} -function toTimescaleFromScale(baseTime, destScale, srcScale = 1, round = false) { - return toTimescaleFromBase(baseTime, destScale, 1 / srcScale, round); -} -function toMsFromMpegTsClock(baseTime, round = false) { - return toTimescaleFromBase(baseTime, 1000, 1 / MPEG_TS_CLOCK_FREQ_HZ, round); -} -function toMpegTsClockFromTimescale(baseTime, srcScale = 1) { - return toTimescaleFromBase(baseTime, MPEG_TS_CLOCK_FREQ_HZ, 1 / srcScale); -} - -const MAX_SILENT_FRAME_DURATION = 10 * 1000; // 10 seconds -const AAC_SAMPLES_PER_FRAME = 1024; -const MPEG_AUDIO_SAMPLE_PER_FRAME = 1152; -const AC3_SAMPLES_PER_FRAME = 1536; -let chromeVersion = null; -let safariWebkitVersion = null; -class MP4Remuxer { - constructor(observer, config, typeSupported, logger) { - this.logger = void 0; - this.observer = void 0; - this.config = void 0; - this.typeSupported = void 0; - this.ISGenerated = false; - this._initPTS = null; - this._initDTS = null; - this.nextAvcDts = null; - this.nextAudioPts = null; - this.videoSampleDuration = null; - this.isAudioContiguous = false; - this.isVideoContiguous = false; - this.videoTrackConfig = void 0; - this.observer = observer; - this.config = config; - this.typeSupported = typeSupported; - this.logger = logger; - this.ISGenerated = false; - if (chromeVersion === null) { - const userAgent = navigator.userAgent || ''; - const result = userAgent.match(/Chrome\/(\d+)/i); - chromeVersion = result ? parseInt(result[1]) : 0; - } - if (safariWebkitVersion === null) { - const result = navigator.userAgent.match(/Safari\/(\d+)/i); - safariWebkitVersion = result ? parseInt(result[1]) : 0; - } - } - destroy() { - // @ts-ignore - this.config = this.videoTrackConfig = this._initPTS = this._initDTS = null; - } - resetTimeStamp(defaultTimeStamp) { - this.logger.log('[mp4-remuxer]: initPTS & initDTS reset'); - this._initPTS = this._initDTS = defaultTimeStamp; - } - resetNextTimestamp() { - this.logger.log('[mp4-remuxer]: reset next timestamp'); - this.isVideoContiguous = false; - this.isAudioContiguous = false; - } - resetInitSegment() { - this.logger.log('[mp4-remuxer]: ISGenerated flag reset'); - this.ISGenerated = false; - this.videoTrackConfig = undefined; - } - getVideoStartPts(videoSamples) { - let rolloverDetected = false; - const startPTS = videoSamples.reduce((minPTS, sample) => { - const delta = sample.pts - minPTS; - if (delta < -4294967296) { - // 2^32, see PTSNormalize for reasoning, but we're hitting a rollover here, and we don't want that to impact the timeOffset calculation - rolloverDetected = true; - return normalizePts(minPTS, sample.pts); - } else if (delta > 0) { - return minPTS; - } else { - return sample.pts; - } - }, videoSamples[0].pts); - if (rolloverDetected) { - this.logger.debug('PTS rollover detected'); - } - return startPTS; - } - remux(audioTrack, videoTrack, id3Track, textTrack, timeOffset, accurateTimeOffset, flush, playlistType) { - let video; - let audio; - let initSegment; - let text; - let id3; - let independent; - let audioTimeOffset = timeOffset; - let videoTimeOffset = timeOffset; - - // If we're remuxing audio and video progressively, wait until we've received enough samples for each track before proceeding. - // This is done to synchronize the audio and video streams. We know if the current segment will have samples if the "pid" - // parameter is greater than -1. The pid is set when the PMT is parsed, which contains the tracks list. - // However, if the initSegment has already been generated, or we've reached the end of a segment (flush), - // then we can remux one track without waiting for the other. - const hasAudio = audioTrack.pid > -1; - const hasVideo = videoTrack.pid > -1; - const length = videoTrack.samples.length; - const enoughAudioSamples = audioTrack.samples.length > 0; - const enoughVideoSamples = flush && length > 0 || length > 1; - const canRemuxAvc = (!hasAudio || enoughAudioSamples) && (!hasVideo || enoughVideoSamples) || this.ISGenerated || flush; - if (canRemuxAvc) { - if (this.ISGenerated) { - var _videoTrack$pixelRati, _config$pixelRatio, _videoTrack$pixelRati2, _config$pixelRatio2; - const config = this.videoTrackConfig; - if (config && (videoTrack.width !== config.width || videoTrack.height !== config.height || ((_videoTrack$pixelRati = videoTrack.pixelRatio) == null ? void 0 : _videoTrack$pixelRati[0]) !== ((_config$pixelRatio = config.pixelRatio) == null ? void 0 : _config$pixelRatio[0]) || ((_videoTrack$pixelRati2 = videoTrack.pixelRatio) == null ? void 0 : _videoTrack$pixelRati2[1]) !== ((_config$pixelRatio2 = config.pixelRatio) == null ? void 0 : _config$pixelRatio2[1])) || !config && enoughVideoSamples || this.nextAudioPts === null && enoughAudioSamples) { - this.resetInitSegment(); - } - } - if (!this.ISGenerated) { - initSegment = this.generateIS(audioTrack, videoTrack, timeOffset, accurateTimeOffset); - } - const isVideoContiguous = this.isVideoContiguous; - let firstKeyFrameIndex = -1; - let firstKeyFramePTS; - if (enoughVideoSamples) { - firstKeyFrameIndex = findKeyframeIndex(videoTrack.samples); - if (!isVideoContiguous && this.config.forceKeyFrameOnDiscontinuity) { - independent = true; - if (firstKeyFrameIndex > 0) { - this.logger.warn(`[mp4-remuxer]: Dropped ${firstKeyFrameIndex} out of ${length} video samples due to a missing keyframe`); - const startPTS = this.getVideoStartPts(videoTrack.samples); - videoTrack.samples = videoTrack.samples.slice(firstKeyFrameIndex); - videoTrack.dropped += firstKeyFrameIndex; - videoTimeOffset += (videoTrack.samples[0].pts - startPTS) / videoTrack.inputTimeScale; - firstKeyFramePTS = videoTimeOffset; - } else if (firstKeyFrameIndex === -1) { - this.logger.warn(`[mp4-remuxer]: No keyframe found out of ${length} video samples`); - independent = false; - } - } - } - if (this.ISGenerated) { - if (enoughAudioSamples && enoughVideoSamples) { - // timeOffset is expected to be the offset of the first timestamp of this fragment (first DTS) - // if first audio DTS is not aligned with first video DTS then we need to take that into account - // when providing timeOffset to remuxAudio / remuxVideo. if we don't do that, there might be a permanent / small - // drift between audio and video streams - const startPTS = this.getVideoStartPts(videoTrack.samples); - const tsDelta = normalizePts(audioTrack.samples[0].pts, startPTS) - startPTS; - const audiovideoTimestampDelta = tsDelta / videoTrack.inputTimeScale; - audioTimeOffset += Math.max(0, audiovideoTimestampDelta); - videoTimeOffset += Math.max(0, -audiovideoTimestampDelta); - } - - // Purposefully remuxing audio before video, so that remuxVideo can use nextAudioPts, which is calculated in remuxAudio. - if (enoughAudioSamples) { - // if initSegment was generated without audio samples, regenerate it again - if (!audioTrack.samplerate) { - this.logger.warn('[mp4-remuxer]: regenerate InitSegment as audio detected'); - initSegment = this.generateIS(audioTrack, videoTrack, timeOffset, accurateTimeOffset); - } - audio = this.remuxAudio(audioTrack, audioTimeOffset, this.isAudioContiguous, accurateTimeOffset, hasVideo || enoughVideoSamples || playlistType === PlaylistLevelType.AUDIO ? videoTimeOffset : undefined); - if (enoughVideoSamples) { - const audioTrackLength = audio ? audio.endPTS - audio.startPTS : 0; - // if initSegment was generated without video samples, regenerate it again - if (!videoTrack.inputTimeScale) { - this.logger.warn('[mp4-remuxer]: regenerate InitSegment as video detected'); - initSegment = this.generateIS(audioTrack, videoTrack, timeOffset, accurateTimeOffset); - } - video = this.remuxVideo(videoTrack, videoTimeOffset, isVideoContiguous, audioTrackLength); - } - } else if (enoughVideoSamples) { - video = this.remuxVideo(videoTrack, videoTimeOffset, isVideoContiguous, 0); - } - if (video) { - video.firstKeyFrame = firstKeyFrameIndex; - video.independent = firstKeyFrameIndex !== -1; - video.firstKeyFramePTS = firstKeyFramePTS; - } - } - } - - // Allow ID3 and text to remux, even if more audio/video samples are required - if (this.ISGenerated && this._initPTS && this._initDTS) { - if (id3Track.samples.length) { - id3 = flushTextTrackMetadataCueSamples(id3Track, timeOffset, this._initPTS, this._initDTS); - } - if (textTrack.samples.length) { - text = flushTextTrackUserdataCueSamples(textTrack, timeOffset, this._initPTS); - } - } - return { - audio, - video, - initSegment, - independent, - text, - id3 - }; - } - generateIS(audioTrack, videoTrack, timeOffset, accurateTimeOffset) { - const audioSamples = audioTrack.samples; - const videoSamples = videoTrack.samples; - const typeSupported = this.typeSupported; - const tracks = {}; - const _initPTS = this._initPTS; - let computePTSDTS = !_initPTS || accurateTimeOffset; - let container = 'audio/mp4'; - let initPTS; - let initDTS; - let timescale; - if (computePTSDTS) { - initPTS = initDTS = Infinity; - } - if (audioTrack.config && audioSamples.length) { - // let's use audio sampling rate as MP4 time scale. - // rationale is that there is a integer nb of audio frames per audio sample (1024 for AAC) - // using audio sampling rate here helps having an integer MP4 frame duration - // this avoids potential rounding issue and AV sync issue - audioTrack.timescale = audioTrack.samplerate; - switch (audioTrack.segmentCodec) { - case 'mp3': - if (typeSupported.mpeg) { - // Chrome and Safari - container = 'audio/mpeg'; - audioTrack.codec = ''; - } else if (typeSupported.mp3) { - // Firefox - audioTrack.codec = 'mp3'; - } - break; - case 'ac3': - audioTrack.codec = 'ac-3'; - break; - } - tracks.audio = { - id: 'audio', - container: container, - codec: audioTrack.codec, - initSegment: audioTrack.segmentCodec === 'mp3' && typeSupported.mpeg ? new Uint8Array(0) : MP4.initSegment([audioTrack]), - metadata: { - channelCount: audioTrack.channelCount - } - }; - if (computePTSDTS) { - timescale = audioTrack.inputTimeScale; - if (!_initPTS || timescale !== _initPTS.timescale) { - // remember first PTS of this demuxing context. for audio, PTS = DTS - initPTS = initDTS = audioSamples[0].pts - Math.round(timescale * timeOffset); - } else { - computePTSDTS = false; - } - } - } - if (videoTrack.sps && videoTrack.pps && videoSamples.length) { - // let's use input time scale as MP4 video timescale - // we use input time scale straight away to avoid rounding issues on frame duration / cts computation - videoTrack.timescale = videoTrack.inputTimeScale; - tracks.video = { - id: 'main', - container: 'video/mp4', - codec: videoTrack.codec, - initSegment: MP4.initSegment([videoTrack]), - metadata: { - width: videoTrack.width, - height: videoTrack.height - } - }; - if (computePTSDTS) { - timescale = videoTrack.inputTimeScale; - if (!_initPTS || timescale !== _initPTS.timescale) { - const startPTS = this.getVideoStartPts(videoSamples); - const startOffset = Math.round(timescale * timeOffset); - initDTS = Math.min(initDTS, normalizePts(videoSamples[0].dts, startPTS) - startOffset); - initPTS = Math.min(initPTS, startPTS - startOffset); - } else { - computePTSDTS = false; - } - } - this.videoTrackConfig = { - width: videoTrack.width, - height: videoTrack.height, - pixelRatio: videoTrack.pixelRatio - }; - } - if (Object.keys(tracks).length) { - this.ISGenerated = true; - if (computePTSDTS) { - this._initPTS = { - baseTime: initPTS, - timescale: timescale - }; - this._initDTS = { - baseTime: initDTS, - timescale: timescale - }; - } else { - initPTS = timescale = undefined; - } - return { - tracks, - initPTS, - timescale - }; - } - } - remuxVideo(track, timeOffset, contiguous, audioTrackLength) { - const timeScale = track.inputTimeScale; - const inputSamples = track.samples; - const outputSamples = []; - const nbSamples = inputSamples.length; - const initPTS = this._initPTS; - let nextAvcDts = this.nextAvcDts; - let offset = 8; - let mp4SampleDuration = this.videoSampleDuration; - let firstDTS; - let lastDTS; - let minPTS = Number.POSITIVE_INFINITY; - let maxPTS = Number.NEGATIVE_INFINITY; - let sortSamples = false; - - // if parsed fragment is contiguous with last one, let's use last DTS value as reference - if (!contiguous || nextAvcDts === null) { - const pts = timeOffset * timeScale; - const cts = inputSamples[0].pts - normalizePts(inputSamples[0].dts, inputSamples[0].pts); - if (chromeVersion && nextAvcDts !== null && Math.abs(pts - cts - nextAvcDts) < 15000) { - // treat as contigous to adjust samples that would otherwise produce video buffer gaps in Chrome - contiguous = true; - } else { - // if not contiguous, let's use target timeOffset - nextAvcDts = pts - cts; - } - } - - // PTS is coded on 33bits, and can loop from -2^32 to 2^32 - // PTSNormalize will make PTS/DTS value monotonic, we use last known DTS value as reference value - const initTime = initPTS.baseTime * timeScale / initPTS.timescale; - for (let i = 0; i < nbSamples; i++) { - const sample = inputSamples[i]; - sample.pts = normalizePts(sample.pts - initTime, nextAvcDts); - sample.dts = normalizePts(sample.dts - initTime, nextAvcDts); - if (sample.dts < inputSamples[i > 0 ? i - 1 : i].dts) { - sortSamples = true; - } - } - - // sort video samples by DTS then PTS then demux id order - if (sortSamples) { - inputSamples.sort(function (a, b) { - const deltadts = a.dts - b.dts; - const deltapts = a.pts - b.pts; - return deltadts || deltapts; - }); - } - - // Get first/last DTS - firstDTS = inputSamples[0].dts; - lastDTS = inputSamples[inputSamples.length - 1].dts; - - // Sample duration (as expected by trun MP4 boxes), should be the delta between sample DTS - // set this constant duration as being the avg delta between consecutive DTS. - const inputDuration = lastDTS - firstDTS; - const averageSampleDuration = inputDuration ? Math.round(inputDuration / (nbSamples - 1)) : mp4SampleDuration || track.inputTimeScale / 30; - - // if fragment are contiguous, detect hole/overlapping between fragments - if (contiguous) { - // check timestamp continuity across consecutive fragments (this is to remove inter-fragment gap/hole) - const delta = firstDTS - nextAvcDts; - const foundHole = delta > averageSampleDuration; - const foundOverlap = delta < -1; - if (foundHole || foundOverlap) { - if (foundHole) { - this.logger.warn(`${(track.segmentCodec || '').toUpperCase()}: ${toMsFromMpegTsClock(delta, true)} ms (${delta}dts) hole between fragments detected at ${timeOffset.toFixed(3)}`); - } else { - this.logger.warn(`${(track.segmentCodec || '').toUpperCase()}: ${toMsFromMpegTsClock(-delta, true)} ms (${delta}dts) overlapping between fragments detected at ${timeOffset.toFixed(3)}`); - } - if (!foundOverlap || nextAvcDts >= inputSamples[0].pts || chromeVersion) { - firstDTS = nextAvcDts; - const firstPTS = inputSamples[0].pts - delta; - if (foundHole) { - inputSamples[0].dts = firstDTS; - inputSamples[0].pts = firstPTS; - } else { - let isPTSOrderRetained = true; - for (let i = 0; i < inputSamples.length; i++) { - if (inputSamples[i].dts > firstPTS && isPTSOrderRetained) { - break; - } - const prevPTS = inputSamples[i].pts; - inputSamples[i].dts -= delta; - inputSamples[i].pts -= delta; - - // check to see if this sample's PTS order has changed - // relative to the next one - if (i < inputSamples.length - 1) { - const nextSamplePTS = inputSamples[i + 1].pts; - const currentSamplePTS = inputSamples[i].pts; - const currentOrder = nextSamplePTS <= currentSamplePTS; - const prevOrder = nextSamplePTS <= prevPTS; - isPTSOrderRetained = currentOrder == prevOrder; - } - } - } - this.logger.log(`Video: Initial PTS/DTS adjusted: ${toMsFromMpegTsClock(firstPTS, true)}/${toMsFromMpegTsClock(firstDTS, true)}, delta: ${toMsFromMpegTsClock(delta, true)} ms`); - } - } - } - firstDTS = Math.max(0, firstDTS); - let nbNalu = 0; - let naluLen = 0; - let dtsStep = firstDTS; - for (let i = 0; i < nbSamples; i++) { - // compute total/avc sample length and nb of NAL units - const sample = inputSamples[i]; - const units = sample.units; - const nbUnits = units.length; - let sampleLen = 0; - for (let j = 0; j < nbUnits; j++) { - sampleLen += units[j].data.length; - } - naluLen += sampleLen; - nbNalu += nbUnits; - sample.length = sampleLen; - - // ensure sample monotonic DTS - if (sample.dts < dtsStep) { - sample.dts = dtsStep; - dtsStep += averageSampleDuration / 4 | 0 || 1; - } else { - dtsStep = sample.dts; - } - minPTS = Math.min(sample.pts, minPTS); - maxPTS = Math.max(sample.pts, maxPTS); - } - lastDTS = inputSamples[nbSamples - 1].dts; - - /* concatenate the video data and construct the mdat in place - (need 8 more bytes to fill length and mpdat type) */ - const mdatSize = naluLen + 4 * nbNalu + 8; - let mdat; - try { - mdat = new Uint8Array(mdatSize); - } catch (err) { - this.observer.emit(Events.ERROR, Events.ERROR, { - type: ErrorTypes.MUX_ERROR, - details: ErrorDetails.REMUX_ALLOC_ERROR, - fatal: false, - error: err, - bytes: mdatSize, - reason: `fail allocating video mdat ${mdatSize}` - }); - return; - } - const view = new DataView(mdat.buffer); - view.setUint32(0, mdatSize); - mdat.set(MP4.types.mdat, 4); - let stretchedLastFrame = false; - let minDtsDelta = Number.POSITIVE_INFINITY; - let minPtsDelta = Number.POSITIVE_INFINITY; - let maxDtsDelta = Number.NEGATIVE_INFINITY; - let maxPtsDelta = Number.NEGATIVE_INFINITY; - for (let i = 0; i < nbSamples; i++) { - const VideoSample = inputSamples[i]; - const VideoSampleUnits = VideoSample.units; - let mp4SampleLength = 0; - // convert NALU bitstream to MP4 format (prepend NALU with size field) - for (let j = 0, nbUnits = VideoSampleUnits.length; j < nbUnits; j++) { - const unit = VideoSampleUnits[j]; - const unitData = unit.data; - const unitDataLen = unit.data.byteLength; - view.setUint32(offset, unitDataLen); - offset += 4; - mdat.set(unitData, offset); - offset += unitDataLen; - mp4SampleLength += 4 + unitDataLen; - } - - // expected sample duration is the Decoding Timestamp diff of consecutive samples - let ptsDelta; - if (i < nbSamples - 1) { - mp4SampleDuration = inputSamples[i + 1].dts - VideoSample.dts; - ptsDelta = inputSamples[i + 1].pts - VideoSample.pts; - } else { - const config = this.config; - const lastFrameDuration = i > 0 ? VideoSample.dts - inputSamples[i - 1].dts : averageSampleDuration; - ptsDelta = i > 0 ? VideoSample.pts - inputSamples[i - 1].pts : averageSampleDuration; - if (config.stretchShortVideoTrack && this.nextAudioPts !== null) { - // In some cases, a segment's audio track duration may exceed the video track duration. - // Since we've already remuxed audio, and we know how long the audio track is, we look to - // see if the delta to the next segment is longer than maxBufferHole. - // If so, playback would potentially get stuck, so we artificially inflate - // the duration of the last frame to minimize any potential gap between segments. - const gapTolerance = Math.floor(config.maxBufferHole * timeScale); - const deltaToFrameEnd = (audioTrackLength ? minPTS + audioTrackLength * timeScale : this.nextAudioPts) - VideoSample.pts; - if (deltaToFrameEnd > gapTolerance) { - // We subtract lastFrameDuration from deltaToFrameEnd to try to prevent any video - // frame overlap. maxBufferHole should be >> lastFrameDuration anyway. - mp4SampleDuration = deltaToFrameEnd - lastFrameDuration; - if (mp4SampleDuration < 0) { - mp4SampleDuration = lastFrameDuration; - } else { - stretchedLastFrame = true; - } - this.logger.log(`[mp4-remuxer]: It is approximately ${deltaToFrameEnd / 90} ms to the next segment; using duration ${mp4SampleDuration / 90} ms for the last video frame.`); - } else { - mp4SampleDuration = lastFrameDuration; - } - } else { - mp4SampleDuration = lastFrameDuration; - } - } - const compositionTimeOffset = Math.round(VideoSample.pts - VideoSample.dts); - minDtsDelta = Math.min(minDtsDelta, mp4SampleDuration); - maxDtsDelta = Math.max(maxDtsDelta, mp4SampleDuration); - minPtsDelta = Math.min(minPtsDelta, ptsDelta); - maxPtsDelta = Math.max(maxPtsDelta, ptsDelta); - outputSamples.push(new Mp4Sample(VideoSample.key, mp4SampleDuration, mp4SampleLength, compositionTimeOffset)); - } - if (outputSamples.length) { - if (chromeVersion) { - if (chromeVersion < 70) { - // Chrome workaround, mark first sample as being a Random Access Point (keyframe) to avoid sourcebuffer append issue - // https://code.google.com/p/chromium/issues/detail?id=229412 - const flags = outputSamples[0].flags; - flags.dependsOn = 2; - flags.isNonSync = 0; - } - } else if (safariWebkitVersion) { - // Fix for "CNN special report, with CC" in test-streams (Safari browser only) - // Ignore DTS when frame durations are irregular. Safari MSE does not handle this leading to gaps. - if (maxPtsDelta - minPtsDelta < maxDtsDelta - minDtsDelta && averageSampleDuration / maxDtsDelta < 0.025 && outputSamples[0].cts === 0) { - this.logger.warn('Found irregular gaps in sample duration. Using PTS instead of DTS to determine MP4 sample duration.'); - let dts = firstDTS; - for (let i = 0, len = outputSamples.length; i < len; i++) { - const nextDts = dts + outputSamples[i].duration; - const pts = dts + outputSamples[i].cts; - if (i < len - 1) { - const nextPts = nextDts + outputSamples[i + 1].cts; - outputSamples[i].duration = nextPts - pts; - } else { - outputSamples[i].duration = i ? outputSamples[i - 1].duration : averageSampleDuration; - } - outputSamples[i].cts = 0; - dts = nextDts; - } - } - } - } - // next AVC/HEVC sample DTS should be equal to last sample DTS + last sample duration (in PES timescale) - mp4SampleDuration = stretchedLastFrame || !mp4SampleDuration ? averageSampleDuration : mp4SampleDuration; - this.nextAvcDts = nextAvcDts = lastDTS + mp4SampleDuration; - this.videoSampleDuration = mp4SampleDuration; - this.isVideoContiguous = true; - const moof = MP4.moof(track.sequenceNumber++, firstDTS, _extends({}, track, { - samples: outputSamples - })); - const type = 'video'; - const data = { - data1: moof, - data2: mdat, - startPTS: minPTS / timeScale, - endPTS: (maxPTS + mp4SampleDuration) / timeScale, - startDTS: firstDTS / timeScale, - endDTS: nextAvcDts / timeScale, - type, - hasAudio: false, - hasVideo: true, - nb: outputSamples.length, - dropped: track.dropped - }; - track.samples = []; - track.dropped = 0; - return data; - } - getSamplesPerFrame(track) { - switch (track.segmentCodec) { - case 'mp3': - return MPEG_AUDIO_SAMPLE_PER_FRAME; - case 'ac3': - return AC3_SAMPLES_PER_FRAME; - default: - return AAC_SAMPLES_PER_FRAME; - } - } - remuxAudio(track, timeOffset, contiguous, accurateTimeOffset, videoTimeOffset) { - const inputTimeScale = track.inputTimeScale; - const mp4timeScale = track.samplerate ? track.samplerate : inputTimeScale; - const scaleFactor = inputTimeScale / mp4timeScale; - const mp4SampleDuration = this.getSamplesPerFrame(track); - const inputSampleDuration = mp4SampleDuration * scaleFactor; - const initPTS = this._initPTS; - const rawMPEG = track.segmentCodec === 'mp3' && this.typeSupported.mpeg; - const outputSamples = []; - const alignedWithVideo = videoTimeOffset !== undefined; - let inputSamples = track.samples; - let offset = rawMPEG ? 0 : 8; - let nextAudioPts = this.nextAudioPts || -1; - - // window.audioSamples ? window.audioSamples.push(inputSamples.map(s => s.pts)) : (window.audioSamples = [inputSamples.map(s => s.pts)]); - - // for audio samples, also consider consecutive fragments as being contiguous (even if a level switch occurs), - // for sake of clarity: - // consecutive fragments are frags with - // - less than 100ms gaps between new time offset (if accurate) and next expected PTS OR - // - less than 20 audio frames distance - // contiguous fragments are consecutive fragments from same quality level (same level, new SN = old SN + 1) - // this helps ensuring audio continuity - // and this also avoids audio glitches/cut when switching quality, or reporting wrong duration on first audio frame - const timeOffsetMpegTS = timeOffset * inputTimeScale; - const initTime = initPTS.baseTime * inputTimeScale / initPTS.timescale; - this.isAudioContiguous = contiguous = contiguous || inputSamples.length && nextAudioPts > 0 && (accurateTimeOffset && Math.abs(timeOffsetMpegTS - nextAudioPts) < 9000 || Math.abs(normalizePts(inputSamples[0].pts - initTime, timeOffsetMpegTS) - nextAudioPts) < 20 * inputSampleDuration); - - // compute normalized PTS - inputSamples.forEach(function (sample) { - sample.pts = normalizePts(sample.pts - initTime, timeOffsetMpegTS); - }); - if (!contiguous || nextAudioPts < 0) { - // filter out sample with negative PTS that are not playable anyway - // if we don't remove these negative samples, they will shift all audio samples forward. - // leading to audio overlap between current / next fragment - inputSamples = inputSamples.filter(sample => sample.pts >= 0); - - // in case all samples have negative PTS, and have been filtered out, return now - if (!inputSamples.length) { - return; - } - if (videoTimeOffset === 0) { - // Set the start to 0 to match video so that start gaps larger than inputSampleDuration are filled with silence - nextAudioPts = 0; - } else if (accurateTimeOffset && !alignedWithVideo) { - // When not seeking, not live, and LevelDetails.PTSKnown, use fragment start as predicted next audio PTS - nextAudioPts = Math.max(0, timeOffsetMpegTS); - } else { - // if frags are not contiguous and if we cant trust time offset, let's use first sample PTS as next audio PTS - nextAudioPts = inputSamples[0].pts; - } - } - - // If the audio track is missing samples, the frames seem to get "left-shifted" within the - // resulting mp4 segment, causing sync issues and leaving gaps at the end of the audio segment. - // In an effort to prevent this from happening, we inject frames here where there are gaps. - // When possible, we inject a silent frame; when that's not possible, we duplicate the last - // frame. - - if (track.segmentCodec === 'aac') { - const maxAudioFramesDrift = this.config.maxAudioFramesDrift; - for (let i = 0, nextPts = nextAudioPts; i < inputSamples.length; i++) { - // First, let's see how far off this frame is from where we expect it to be - const sample = inputSamples[i]; - const pts = sample.pts; - const delta = pts - nextPts; - const duration = Math.abs(1000 * delta / inputTimeScale); - - // When remuxing with video, if we're overlapping by more than a duration, drop this sample to stay in sync - if (delta <= -maxAudioFramesDrift * inputSampleDuration && alignedWithVideo) { - if (i === 0) { - this.logger.warn(`Audio frame @ ${(pts / inputTimeScale).toFixed(3)}s overlaps nextAudioPts by ${Math.round(1000 * delta / inputTimeScale)} ms.`); - this.nextAudioPts = nextAudioPts = nextPts = pts; - } - } // eslint-disable-line brace-style - - // Insert missing frames if: - // 1: We're more than maxAudioFramesDrift frame away - // 2: Not more than MAX_SILENT_FRAME_DURATION away - // 3: currentTime (aka nextPtsNorm) is not 0 - // 4: remuxing with video (videoTimeOffset !== undefined) - else if (delta >= maxAudioFramesDrift * inputSampleDuration && duration < MAX_SILENT_FRAME_DURATION && alignedWithVideo) { - let missing = Math.round(delta / inputSampleDuration); - // Adjust nextPts so that silent samples are aligned with media pts. This will prevent media samples from - // later being shifted if nextPts is based on timeOffset and delta is not a multiple of inputSampleDuration. - nextPts = pts - missing * inputSampleDuration; - if (nextPts < 0) { - missing--; - nextPts += inputSampleDuration; - } - if (i === 0) { - this.nextAudioPts = nextAudioPts = nextPts; - } - this.logger.warn(`[mp4-remuxer]: Injecting ${missing} audio frame @ ${(nextPts / inputTimeScale).toFixed(3)}s due to ${Math.round(1000 * delta / inputTimeScale)} ms gap.`); - for (let j = 0; j < missing; j++) { - const newStamp = Math.max(nextPts, 0); - let fillFrame = AAC.getSilentFrame(track.parsedCodec || track.manifestCodec || track.codec, track.channelCount); - if (!fillFrame) { - this.logger.log('[mp4-remuxer]: Unable to get silent frame for given audio codec; duplicating last frame instead.'); - fillFrame = sample.unit.subarray(); - } - inputSamples.splice(i, 0, { - unit: fillFrame, - pts: newStamp - }); - nextPts += inputSampleDuration; - i++; - } - } - sample.pts = nextPts; - nextPts += inputSampleDuration; - } - } - let firstPTS = null; - let lastPTS = null; - let mdat; - let mdatSize = 0; - let sampleLength = inputSamples.length; - while (sampleLength--) { - mdatSize += inputSamples[sampleLength].unit.byteLength; - } - for (let j = 0, _nbSamples = inputSamples.length; j < _nbSamples; j++) { - const audioSample = inputSamples[j]; - const unit = audioSample.unit; - let pts = audioSample.pts; - if (lastPTS !== null) { - // If we have more than one sample, set the duration of the sample to the "real" duration; the PTS diff with - // the previous sample - const prevSample = outputSamples[j - 1]; - prevSample.duration = Math.round((pts - lastPTS) / scaleFactor); - } else { - if (contiguous && track.segmentCodec === 'aac') { - // set PTS/DTS to expected PTS/DTS - pts = nextAudioPts; - } - // remember first PTS of our audioSamples - firstPTS = pts; - if (mdatSize > 0) { - /* concatenate the audio data and construct the mdat in place - (need 8 more bytes to fill length and mdat type) */ - mdatSize += offset; - try { - mdat = new Uint8Array(mdatSize); - } catch (err) { - this.observer.emit(Events.ERROR, Events.ERROR, { - type: ErrorTypes.MUX_ERROR, - details: ErrorDetails.REMUX_ALLOC_ERROR, - fatal: false, - error: err, - bytes: mdatSize, - reason: `fail allocating audio mdat ${mdatSize}` - }); - return; - } - if (!rawMPEG) { - const view = new DataView(mdat.buffer); - view.setUint32(0, mdatSize); - mdat.set(MP4.types.mdat, 4); - } - } else { - // no audio samples - return; - } - } - mdat.set(unit, offset); - const unitLen = unit.byteLength; - offset += unitLen; - // Default the sample's duration to the computed mp4SampleDuration, which will either be 1024 for AAC or 1152 for MPEG - // In the case that we have 1 sample, this will be the duration. If we have more than one sample, the duration - // becomes the PTS diff with the previous sample - outputSamples.push(new Mp4Sample(true, mp4SampleDuration, unitLen, 0)); - lastPTS = pts; - } - - // We could end up with no audio samples if all input samples were overlapping with the previously remuxed ones - const nbSamples = outputSamples.length; - if (!nbSamples) { - return; - } - - // The next audio sample PTS should be equal to last sample PTS + duration - const lastSample = outputSamples[outputSamples.length - 1]; - this.nextAudioPts = nextAudioPts = lastPTS + scaleFactor * lastSample.duration; - - // Set the track samples from inputSamples to outputSamples before remuxing - const moof = rawMPEG ? new Uint8Array(0) : MP4.moof(track.sequenceNumber++, firstPTS / scaleFactor, _extends({}, track, { - samples: outputSamples - })); - - // Clear the track samples. This also clears the samples array in the demuxer, since the reference is shared - track.samples = []; - const start = firstPTS / inputTimeScale; - const end = nextAudioPts / inputTimeScale; - const type = 'audio'; - const audioData = { - data1: moof, - data2: mdat, - startPTS: start, - endPTS: end, - startDTS: start, - endDTS: end, - type, - hasAudio: true, - hasVideo: false, - nb: nbSamples - }; - this.isAudioContiguous = true; - return audioData; - } -} -function normalizePts(value, reference) { - let offset; - if (reference === null) { - return value; - } - if (reference < value) { - // - 2^33 - offset = -8589934592; - } else { - // + 2^33 - offset = 8589934592; - } - /* PTS is 33bit (from 0 to 2^33 -1) - if diff between value and reference is bigger than half of the amplitude (2^32) then it means that - PTS looping occured. fill the gap */ - while (Math.abs(value - reference) > 4294967296) { - value += offset; - } - return value; -} -function findKeyframeIndex(samples) { - for (let i = 0; i < samples.length; i++) { - if (samples[i].key) { - return i; - } - } - return -1; -} -function flushTextTrackMetadataCueSamples(track, timeOffset, initPTS, initDTS) { - const length = track.samples.length; - if (!length) { - return; - } - const inputTimeScale = track.inputTimeScale; - for (let index = 0; index < length; index++) { - const sample = track.samples[index]; - // setting id3 pts, dts to relative time - // using this._initPTS and this._initDTS to calculate relative time - sample.pts = normalizePts(sample.pts - initPTS.baseTime * inputTimeScale / initPTS.timescale, timeOffset * inputTimeScale) / inputTimeScale; - sample.dts = normalizePts(sample.dts - initDTS.baseTime * inputTimeScale / initDTS.timescale, timeOffset * inputTimeScale) / inputTimeScale; - } - const samples = track.samples; - track.samples = []; - return { - samples - }; -} -function flushTextTrackUserdataCueSamples(track, timeOffset, initPTS) { - const length = track.samples.length; - if (!length) { - return; - } - const inputTimeScale = track.inputTimeScale; - for (let index = 0; index < length; index++) { - const sample = track.samples[index]; - // setting text pts, dts to relative time - // using this._initPTS and this._initDTS to calculate relative time - sample.pts = normalizePts(sample.pts - initPTS.baseTime * inputTimeScale / initPTS.timescale, timeOffset * inputTimeScale) / inputTimeScale; - } - track.samples.sort((a, b) => a.pts - b.pts); - const samples = track.samples; - track.samples = []; - return { - samples - }; -} -class Mp4Sample { - constructor(isKeyframe, duration, size, cts) { - this.size = void 0; - this.duration = void 0; - this.cts = void 0; - this.flags = void 0; - this.duration = duration; - this.size = size; - this.cts = cts; - this.flags = { - isLeading: 0, - isDependedOn: 0, - hasRedundancy: 0, - degradPrio: 0, - dependsOn: isKeyframe ? 2 : 1, - isNonSync: isKeyframe ? 0 : 1 - }; - } -} - -class PassThroughRemuxer { - constructor(observer, config, typeSupported, logger) { - this.logger = void 0; - this.emitInitSegment = false; - this.audioCodec = void 0; - this.videoCodec = void 0; - this.initData = void 0; - this.initPTS = null; - this.initTracks = void 0; - this.lastEndTime = null; - this.logger = logger; - } - destroy() {} - resetTimeStamp(defaultInitPTS) { - this.initPTS = defaultInitPTS; - this.lastEndTime = null; - } - resetNextTimestamp() { - this.lastEndTime = null; - } - resetInitSegment(initSegment, audioCodec, videoCodec, decryptdata) { - this.audioCodec = audioCodec; - this.videoCodec = videoCodec; - this.generateInitSegment(patchEncyptionData(initSegment, decryptdata)); - this.emitInitSegment = true; - } - generateInitSegment(initSegment) { - let { - audioCodec, - videoCodec - } = this; - if (!(initSegment != null && initSegment.byteLength)) { - this.initTracks = undefined; - this.initData = undefined; - return; - } - const initData = this.initData = parseInitSegment(initSegment); - - // Get codec from initSegment - if (initData.audio) { - audioCodec = getParsedTrackCodec(initData.audio, ElementaryStreamTypes.AUDIO); - } - if (initData.video) { - videoCodec = getParsedTrackCodec(initData.video, ElementaryStreamTypes.VIDEO); - } - const tracks = {}; - if (initData.audio && initData.video) { - tracks.audiovideo = { - container: 'video/mp4', - codec: audioCodec + ',' + videoCodec, - initSegment, - id: 'main' - }; - } else if (initData.audio) { - tracks.audio = { - container: 'audio/mp4', - codec: audioCodec, - initSegment, - id: 'audio' - }; - } else if (initData.video) { - tracks.video = { - container: 'video/mp4', - codec: videoCodec, - initSegment, - id: 'main' - }; - } else { - this.logger.warn('[passthrough-remuxer.ts]: initSegment does not contain moov or trak boxes.'); - } - this.initTracks = tracks; - } - remux(audioTrack, videoTrack, id3Track, textTrack, timeOffset, accurateTimeOffset) { - var _initData, _initData2; - let { - initPTS, - lastEndTime - } = this; - const result = { - audio: undefined, - video: undefined, - text: textTrack, - id3: id3Track, - initSegment: undefined - }; - - // If we haven't yet set a lastEndDTS, or it was reset, set it to the provided timeOffset. We want to use the - // lastEndDTS over timeOffset whenever possible; during progressive playback, the media source will not update - // the media duration (which is what timeOffset is provided as) before we need to process the next chunk. - if (!isFiniteNumber(lastEndTime)) { - lastEndTime = this.lastEndTime = timeOffset || 0; - } - - // The binary segment data is added to the videoTrack in the mp4demuxer. We don't check to see if the data is only - // audio or video (or both); adding it to video was an arbitrary choice. - const data = videoTrack.samples; - if (!(data != null && data.length)) { - return result; - } - const initSegment = { - initPTS: undefined, - timescale: 1 - }; - let initData = this.initData; - if (!((_initData = initData) != null && _initData.length)) { - this.generateInitSegment(data); - initData = this.initData; - } - if (!((_initData2 = initData) != null && _initData2.length)) { - // We can't remux if the initSegment could not be generated - this.logger.warn('[passthrough-remuxer.ts]: Failed to generate initSegment.'); - return result; - } - if (this.emitInitSegment) { - initSegment.tracks = this.initTracks; - this.emitInitSegment = false; - } - const duration = getDuration(data, initData); - const startDTS = getStartDTS(initData, data); - const decodeTime = startDTS === null ? timeOffset : startDTS; - if (isInvalidInitPts(initPTS, decodeTime, timeOffset, duration) || initSegment.timescale !== initPTS.timescale && accurateTimeOffset) { - initSegment.initPTS = decodeTime - timeOffset; - if (initPTS && initPTS.timescale === 1) { - this.logger.warn(`Adjusting initPTS @${timeOffset} from ${initPTS.baseTime / initPTS.timescale} to ${initSegment.initPTS}`); - } - this.initPTS = initPTS = { - baseTime: initSegment.initPTS, - timescale: 1 - }; - } - const startTime = audioTrack ? decodeTime - initPTS.baseTime / initPTS.timescale : lastEndTime; - const endTime = startTime + duration; - offsetStartDTS(initData, data, initPTS.baseTime / initPTS.timescale); - if (duration > 0) { - this.lastEndTime = endTime; - } else { - this.logger.warn('Duration parsed from mp4 should be greater than zero'); - this.resetNextTimestamp(); - } - const hasAudio = !!initData.audio; - const hasVideo = !!initData.video; - let type = ''; - if (hasAudio) { - type += 'audio'; - } - if (hasVideo) { - type += 'video'; - } - const track = { - data1: data, - startPTS: startTime, - startDTS: startTime, - endPTS: endTime, - endDTS: endTime, - type, - hasAudio, - hasVideo, - nb: 1, - dropped: 0 - }; - result.audio = track.type === 'audio' ? track : undefined; - result.video = track.type !== 'audio' ? track : undefined; - result.initSegment = initSegment; - result.id3 = flushTextTrackMetadataCueSamples(id3Track, timeOffset, initPTS, initPTS); - if (textTrack.samples.length) { - result.text = flushTextTrackUserdataCueSamples(textTrack, timeOffset, initPTS); - } - return result; - } -} -function isInvalidInitPts(initPTS, startDTS, timeOffset, duration) { - if (initPTS === null) { - return true; - } - // InitPTS is invalid when distance from program would be more than segment duration or a minimum of one second - const minDuration = Math.max(duration, 1); - const startTime = startDTS - initPTS.baseTime / initPTS.timescale; - return Math.abs(startTime - timeOffset) > minDuration; -} -function getParsedTrackCodec(track, type) { - const parsedCodec = track == null ? void 0 : track.codec; - if (parsedCodec && parsedCodec.length > 4) { - return parsedCodec; - } - if (type === ElementaryStreamTypes.AUDIO) { - if (parsedCodec === 'ec-3' || parsedCodec === 'ac-3' || parsedCodec === 'alac') { - return parsedCodec; - } - if (parsedCodec === 'fLaC' || parsedCodec === 'Opus') { - // Opting not to get `preferManagedMediaSource` from player config for isSupported() check for simplicity - const preferManagedMediaSource = false; - return getCodecCompatibleName(parsedCodec, preferManagedMediaSource); - } - logger.warn(`Unhandled audio codec "${parsedCodec}" in mp4 MAP`); - return parsedCodec || 'mp4a'; - } - // Provide defaults based on codec type - // This allows for some playback of some fmp4 playlists without CODECS defined in manifest - logger.warn(`Unhandled video codec "${parsedCodec}" in mp4 MAP`); - return parsedCodec || 'avc1'; -} - -let now; -// performance.now() not available on WebWorker, at least on Safari Desktop -try { - now = self.performance.now.bind(self.performance); -} catch (err) { - now = Date.now; -} -const muxConfig = [{ - demux: MP4Demuxer, - remux: PassThroughRemuxer -}, { - demux: TSDemuxer, - remux: MP4Remuxer -}, { - demux: AACDemuxer, - remux: MP4Remuxer -}, { - demux: MP3Demuxer, - remux: MP4Remuxer -}]; -{ - muxConfig.splice(2, 0, { - demux: AC3Demuxer, - remux: MP4Remuxer - }); -} -class Transmuxer { - constructor(observer, typeSupported, config, vendor, id, logger) { - this.asyncResult = false; - this.logger = void 0; - this.observer = void 0; - this.typeSupported = void 0; - this.config = void 0; - this.id = void 0; - this.demuxer = void 0; - this.remuxer = void 0; - this.decrypter = void 0; - this.probe = void 0; - this.decryptionPromise = null; - this.transmuxConfig = void 0; - this.currentTransmuxState = void 0; - this.observer = observer; - this.typeSupported = typeSupported; - this.config = config; - this.id = id; - this.logger = logger; - } - configure(transmuxConfig) { - this.transmuxConfig = transmuxConfig; - if (this.decrypter) { - this.decrypter.reset(); - } - } - push(data, decryptdata, chunkMeta, state) { - const stats = chunkMeta.transmuxing; - stats.executeStart = now(); - let uintData = new Uint8Array(data); - const { - currentTransmuxState, - transmuxConfig - } = this; - if (state) { - this.currentTransmuxState = state; - } - const { - contiguous, - discontinuity, - trackSwitch, - accurateTimeOffset, - timeOffset, - initSegmentChange - } = state || currentTransmuxState; - const { - audioCodec, - videoCodec, - defaultInitPts, - duration, - initSegmentData - } = transmuxConfig; - const keyData = getEncryptionType(uintData, decryptdata); - if (keyData && isFullSegmentEncryption(keyData.method)) { - const decrypter = this.getDecrypter(); - const aesMode = getAesModeFromFullSegmentMethod(keyData.method); - - // Software decryption is synchronous; webCrypto is not - 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 - let decryptedData = decrypter.softwareDecrypt(uintData, keyData.key.buffer, keyData.iv.buffer, aesMode); - // 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); - } - uintData = new Uint8Array(decryptedData); - } else { - this.asyncResult = true; - this.decryptionPromise = decrypter.webCryptoDecrypt(uintData, keyData.key.buffer, keyData.iv.buffer, aesMode).then(decryptedData => { - // Calling push here is important; if flush() is called while this is still resolving, this ensures that - // the decrypted data has been transmuxed - const result = this.push(decryptedData, null, chunkMeta); - this.decryptionPromise = null; - return result; - }); - return this.decryptionPromise; - } - } - const resetMuxers = this.needsProbing(discontinuity, trackSwitch); - if (resetMuxers) { - const error = this.configureTransmuxer(uintData); - if (error) { - this.logger.warn(`[transmuxer] ${error.message}`); - this.observer.emit(Events.ERROR, Events.ERROR, { - type: ErrorTypes.MEDIA_ERROR, - details: ErrorDetails.FRAG_PARSING_ERROR, - fatal: false, - error, - reason: error.message - }); - stats.executeEnd = now(); - return emptyResult(chunkMeta); - } - } - 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(uintData, keyData, timeOffset, accurateTimeOffset, chunkMeta); - this.asyncResult = isPromise(result); - const currentState = this.currentTransmuxState; - currentState.contiguous = true; - currentState.discontinuity = false; - currentState.trackSwitch = false; - stats.executeEnd = now(); - return result; - } - - // Due to data caching, flush calls can produce more than one TransmuxerResult (hence the Array type) - flush(chunkMeta) { - const stats = chunkMeta.transmuxing; - stats.executeStart = now(); - const { - decrypter, - currentTransmuxState, - decryptionPromise - } = this; - if (decryptionPromise) { - this.asyncResult = true; - // Upon resolution, the decryption promise calls push() and returns its TransmuxerResult up the stack. Therefore - // only flushing is required for async decryption - return decryptionPromise.then(() => { - return this.flush(chunkMeta); - }); - } - const transmuxResults = []; - const { - timeOffset - } = currentTransmuxState; - if (decrypter) { - // The decrypter may have data cached, which needs to be demuxed. In this case we'll have two TransmuxResults - // This happens in the case that we receive only 1 push call for a segment (either for non-progressive downloads, - // or for progressive downloads with small segments) - const decryptedData = decrypter.flush(); - if (decryptedData) { - // Push always returns a TransmuxerResult if decryptdata is null - transmuxResults.push(this.push(decryptedData, null, chunkMeta)); - } - } - const { - demuxer, - remuxer - } = this; - if (!demuxer || !remuxer) { - // If probing failed, then Hls.js has been given content its not able to handle - stats.executeEnd = now(); - const emptyResults = [emptyResult(chunkMeta)]; - if (this.asyncResult) { - return Promise.resolve(emptyResults); - } - return emptyResults; - } - const demuxResultOrPromise = demuxer.flush(timeOffset); - if (isPromise(demuxResultOrPromise)) { - this.asyncResult = true; - // Decrypt final SAMPLE-AES samples - return demuxResultOrPromise.then(demuxResult => { - this.flushRemux(transmuxResults, demuxResult, chunkMeta); - return transmuxResults; - }); - } - this.flushRemux(transmuxResults, demuxResultOrPromise, chunkMeta); - if (this.asyncResult) { - return Promise.resolve(transmuxResults); - } - return transmuxResults; - } - flushRemux(transmuxResults, demuxResult, chunkMeta) { - const { - audioTrack, - videoTrack, - id3Track, - textTrack - } = demuxResult; - const { - accurateTimeOffset, - timeOffset - } = this.currentTransmuxState; - this.logger.log(`[transmuxer.ts]: Flushed ${this.id} sn: ${chunkMeta.sn}${chunkMeta.part > -1 ? ' p: ' + chunkMeta.part : ''} of ${this.id === PlaylistLevelType.MAIN ? 'level' : 'track'} ${chunkMeta.level}`); - const remuxResult = this.remuxer.remux(audioTrack, videoTrack, id3Track, textTrack, timeOffset, accurateTimeOffset, true, this.id); - transmuxResults.push({ - remuxResult, - chunkMeta - }); - chunkMeta.transmuxing.executeEnd = now(); - } - resetInitialTimestamp(defaultInitPts) { - const { - demuxer, - remuxer - } = this; - if (!demuxer || !remuxer) { - return; - } - demuxer.resetTimeStamp(defaultInitPts); - remuxer.resetTimeStamp(defaultInitPts); - } - resetContiguity() { - const { - demuxer, - remuxer - } = this; - if (!demuxer || !remuxer) { - return; - } - demuxer.resetContiguity(); - remuxer.resetNextTimestamp(); - } - resetInitSegment(initSegmentData, audioCodec, videoCodec, trackDuration, decryptdata) { - const { - demuxer, - remuxer - } = this; - if (!demuxer || !remuxer) { - return; - } - demuxer.resetInitSegment(initSegmentData, audioCodec, videoCodec, trackDuration); - remuxer.resetInitSegment(initSegmentData, audioCodec, videoCodec, decryptdata); - } - destroy() { - if (this.demuxer) { - this.demuxer.destroy(); - this.demuxer = undefined; - } - if (this.remuxer) { - this.remuxer.destroy(); - this.remuxer = undefined; - } - } - transmux(data, keyData, timeOffset, accurateTimeOffset, chunkMeta) { - let result; - if (keyData && keyData.method === 'SAMPLE-AES') { - result = this.transmuxSampleAes(data, keyData, timeOffset, accurateTimeOffset, chunkMeta); - } else { - result = this.transmuxUnencrypted(data, timeOffset, accurateTimeOffset, chunkMeta); - } - return result; - } - transmuxUnencrypted(data, timeOffset, accurateTimeOffset, chunkMeta) { - const { - audioTrack, - videoTrack, - id3Track, - textTrack - } = this.demuxer.demux(data, timeOffset, false, !this.config.progressive); - const remuxResult = this.remuxer.remux(audioTrack, videoTrack, id3Track, textTrack, timeOffset, accurateTimeOffset, false, this.id); - return { - remuxResult, - chunkMeta - }; - } - transmuxSampleAes(data, decryptData, timeOffset, accurateTimeOffset, chunkMeta) { - return this.demuxer.demuxSampleAes(data, decryptData, timeOffset).then(demuxResult => { - const remuxResult = this.remuxer.remux(demuxResult.audioTrack, demuxResult.videoTrack, demuxResult.id3Track, demuxResult.textTrack, timeOffset, accurateTimeOffset, false, this.id); - return { - remuxResult, - chunkMeta - }; - }); - } - configureTransmuxer(data) { - const { - config, - observer, - typeSupported - } = this; - // probe for content type - let mux; - for (let i = 0, len = muxConfig.length; i < len; i++) { - var _muxConfig$i$demux; - if ((_muxConfig$i$demux = muxConfig[i].demux) != null && _muxConfig$i$demux.probe(data, this.logger)) { - mux = muxConfig[i]; - break; - } - } - if (!mux) { - return new Error('Failed to find demuxer by probing fragment data'); - } - // so let's check that current remuxer and demuxer are still valid - const demuxer = this.demuxer; - const remuxer = this.remuxer; - const Remuxer = mux.remux; - const Demuxer = mux.demux; - if (!remuxer || !(remuxer instanceof Remuxer)) { - this.remuxer = new Remuxer(observer, config, typeSupported, this.logger); - } - if (!demuxer || !(demuxer instanceof Demuxer)) { - this.demuxer = new Demuxer(observer, config, typeSupported, this.logger); - this.probe = Demuxer.probe; - } - } - needsProbing(discontinuity, trackSwitch) { - // 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; - } - getDecrypter() { - let decrypter = this.decrypter; - if (!decrypter) { - decrypter = this.decrypter = new Decrypter(this.config); - } - return decrypter; - } -} -function getEncryptionType(data, decryptData) { - let encryptionType = null; - if (data.byteLength > 0 && (decryptData == null ? void 0 : decryptData.key) != null && decryptData.iv !== null && decryptData.method != null) { - encryptionType = decryptData; - } - return encryptionType; -} -const emptyResult = chunkMeta => ({ - remuxResult: {}, - chunkMeta -}); -function isPromise(p) { - return 'then' in p && p.then instanceof Function; -} -class TransmuxConfig { - constructor(audioCodec, videoCodec, initSegmentData, duration, defaultInitPts) { - this.audioCodec = void 0; - this.videoCodec = void 0; - this.initSegmentData = void 0; - this.duration = void 0; - this.defaultInitPts = void 0; - this.audioCodec = audioCodec; - this.videoCodec = videoCodec; - this.initSegmentData = initSegmentData; - this.duration = duration; - this.defaultInitPts = defaultInitPts || null; - } -} -class TransmuxState { - constructor(discontinuity, contiguous, accurateTimeOffset, trackSwitch, timeOffset, initSegmentChange) { - this.discontinuity = void 0; - this.contiguous = void 0; - this.accurateTimeOffset = void 0; - this.trackSwitch = void 0; - this.timeOffset = void 0; - this.initSegmentChange = void 0; - this.discontinuity = discontinuity; - this.contiguous = contiguous; - this.accurateTimeOffset = accurateTimeOffset; - this.trackSwitch = trackSwitch; - this.timeOffset = timeOffset; - this.initSegmentChange = initSegmentChange; - } -} - -var eventemitter3 = {exports: {}}; - -(function (module) { - - var has = Object.prototype.hasOwnProperty - , prefix = '~'; - - /** - * Constructor to create a storage for our `EE` objects. - * An `Events` instance is a plain object whose properties are event names. - * - * @constructor - * @private - */ - function Events() {} - - // - // We try to not inherit from `Object.prototype`. In some engines creating an - // instance in this way is faster than calling `Object.create(null)` directly. - // If `Object.create(null)` is not supported we prefix the event names with a - // character to make sure that the built-in object properties are not - // overridden or used as an attack vector. - // - if (Object.create) { - Events.prototype = Object.create(null); - - // - // This hack is needed because the `__proto__` property is still inherited in - // some old browsers like Android 4, iPhone 5.1, Opera 11 and Safari 5. - // - if (!new Events().__proto__) prefix = false; - } - - /** - * Representation of a single event listener. - * - * @param {Function} fn The listener function. - * @param {*} context The context to invoke the listener with. - * @param {Boolean} [once=false] Specify if the listener is a one-time listener. - * @constructor - * @private - */ - function EE(fn, context, once) { - this.fn = fn; - this.context = context; - this.once = once || false; - } - - /** - * Add a listener for a given event. - * - * @param {EventEmitter} emitter Reference to the `EventEmitter` instance. - * @param {(String|Symbol)} event The event name. - * @param {Function} fn The listener function. - * @param {*} context The context to invoke the listener with. - * @param {Boolean} once Specify if the listener is a one-time listener. - * @returns {EventEmitter} - * @private - */ - function addListener(emitter, event, fn, context, once) { - if (typeof fn !== 'function') { - throw new TypeError('The listener must be a function'); - } - - var listener = new EE(fn, context || emitter, once) - , evt = prefix ? prefix + event : event; - - if (!emitter._events[evt]) emitter._events[evt] = listener, emitter._eventsCount++; - else if (!emitter._events[evt].fn) emitter._events[evt].push(listener); - else emitter._events[evt] = [emitter._events[evt], listener]; - - return emitter; - } - - /** - * Clear event by name. - * - * @param {EventEmitter} emitter Reference to the `EventEmitter` instance. - * @param {(String|Symbol)} evt The Event name. - * @private - */ - function clearEvent(emitter, evt) { - if (--emitter._eventsCount === 0) emitter._events = new Events(); - else delete emitter._events[evt]; - } - - /** - * Minimal `EventEmitter` interface that is molded against the Node.js - * `EventEmitter` interface. - * - * @constructor - * @public - */ - function EventEmitter() { - this._events = new Events(); - this._eventsCount = 0; - } - - /** - * Return an array listing the events for which the emitter has registered - * listeners. - * - * @returns {Array} - * @public - */ - EventEmitter.prototype.eventNames = function eventNames() { - var names = [] - , events - , name; - - if (this._eventsCount === 0) return names; - - for (name in (events = this._events)) { - if (has.call(events, name)) names.push(prefix ? name.slice(1) : name); - } - - if (Object.getOwnPropertySymbols) { - return names.concat(Object.getOwnPropertySymbols(events)); - } - - return names; - }; - - /** - * Return the listeners registered for a given event. - * - * @param {(String|Symbol)} event The event name. - * @returns {Array} The registered listeners. - * @public - */ - EventEmitter.prototype.listeners = function listeners(event) { - var evt = prefix ? prefix + event : event - , handlers = this._events[evt]; - - if (!handlers) return []; - if (handlers.fn) return [handlers.fn]; - - for (var i = 0, l = handlers.length, ee = new Array(l); i < l; i++) { - ee[i] = handlers[i].fn; - } - - return ee; - }; - - /** - * Return the number of listeners listening to a given event. - * - * @param {(String|Symbol)} event The event name. - * @returns {Number} The number of listeners. - * @public - */ - EventEmitter.prototype.listenerCount = function listenerCount(event) { - var evt = prefix ? prefix + event : event - , listeners = this._events[evt]; - - if (!listeners) return 0; - if (listeners.fn) return 1; - return listeners.length; - }; - - /** - * Calls each of the listeners registered for a given event. - * - * @param {(String|Symbol)} event The event name. - * @returns {Boolean} `true` if the event had listeners, else `false`. - * @public - */ - EventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) { - var evt = prefix ? prefix + event : event; - - if (!this._events[evt]) return false; - - var listeners = this._events[evt] - , len = arguments.length - , args - , i; - - if (listeners.fn) { - if (listeners.once) this.removeListener(event, listeners.fn, undefined, true); - - switch (len) { - case 1: return listeners.fn.call(listeners.context), true; - case 2: return listeners.fn.call(listeners.context, a1), true; - case 3: return listeners.fn.call(listeners.context, a1, a2), true; - case 4: return listeners.fn.call(listeners.context, a1, a2, a3), true; - case 5: return listeners.fn.call(listeners.context, a1, a2, a3, a4), true; - case 6: return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true; - } - - for (i = 1, args = new Array(len -1); i < len; i++) { - args[i - 1] = arguments[i]; - } - - listeners.fn.apply(listeners.context, args); - } else { - var length = listeners.length - , j; - - for (i = 0; i < length; i++) { - if (listeners[i].once) this.removeListener(event, listeners[i].fn, undefined, true); - - switch (len) { - case 1: listeners[i].fn.call(listeners[i].context); break; - case 2: listeners[i].fn.call(listeners[i].context, a1); break; - case 3: listeners[i].fn.call(listeners[i].context, a1, a2); break; - case 4: listeners[i].fn.call(listeners[i].context, a1, a2, a3); break; - default: - if (!args) for (j = 1, args = new Array(len -1); j < len; j++) { - args[j - 1] = arguments[j]; - } - - listeners[i].fn.apply(listeners[i].context, args); - } - } - } - - return true; - }; - - /** - * Add a listener for a given event. - * - * @param {(String|Symbol)} event The event name. - * @param {Function} fn The listener function. - * @param {*} [context=this] The context to invoke the listener with. - * @returns {EventEmitter} `this`. - * @public - */ - EventEmitter.prototype.on = function on(event, fn, context) { - return addListener(this, event, fn, context, false); - }; - - /** - * Add a one-time listener for a given event. - * - * @param {(String|Symbol)} event The event name. - * @param {Function} fn The listener function. - * @param {*} [context=this] The context to invoke the listener with. - * @returns {EventEmitter} `this`. - * @public - */ - EventEmitter.prototype.once = function once(event, fn, context) { - return addListener(this, event, fn, context, true); - }; - - /** - * Remove the listeners of a given event. - * - * @param {(String|Symbol)} event The event name. - * @param {Function} fn Only remove the listeners that match this function. - * @param {*} context Only remove the listeners that have this context. - * @param {Boolean} once Only remove one-time listeners. - * @returns {EventEmitter} `this`. - * @public - */ - EventEmitter.prototype.removeListener = function removeListener(event, fn, context, once) { - var evt = prefix ? prefix + event : event; - - if (!this._events[evt]) return this; - if (!fn) { - clearEvent(this, evt); - return this; - } - - var listeners = this._events[evt]; - - if (listeners.fn) { - if ( - listeners.fn === fn && - (!once || listeners.once) && - (!context || listeners.context === context) - ) { - clearEvent(this, evt); - } - } else { - for (var i = 0, events = [], length = listeners.length; i < length; i++) { - if ( - listeners[i].fn !== fn || - (once && !listeners[i].once) || - (context && listeners[i].context !== context) - ) { - events.push(listeners[i]); - } - } - - // - // Reset the array, or remove it completely if we have no more listeners. - // - if (events.length) this._events[evt] = events.length === 1 ? events[0] : events; - else clearEvent(this, evt); - } - - return this; - }; - - /** - * Remove all listeners, or those of the specified event. - * - * @param {(String|Symbol)} [event] The event name. - * @returns {EventEmitter} `this`. - * @public - */ - EventEmitter.prototype.removeAllListeners = function removeAllListeners(event) { - var evt; - - if (event) { - evt = prefix ? prefix + event : event; - if (this._events[evt]) clearEvent(this, evt); - } else { - this._events = new Events(); - this._eventsCount = 0; - } - - return this; - }; - - // - // Alias methods names because people roll like that. - // - EventEmitter.prototype.off = EventEmitter.prototype.removeListener; - EventEmitter.prototype.addListener = EventEmitter.prototype.on; - - // - // Expose the prefix. - // - EventEmitter.prefixed = prefix; - - // - // Allow `EventEmitter` to be imported as module namespace. - // - EventEmitter.EventEmitter = EventEmitter; - - // - // Expose the module. - // - { - module.exports = EventEmitter; - } -} (eventemitter3)); - -var eventemitter3Exports = eventemitter3.exports; -var EventEmitter = /*@__PURE__*/getDefaultExportFromCjs(eventemitter3Exports); - -let transmuxerInstanceCount = 0; -class TransmuxerInterface { - constructor(_hls, id, onTransmuxComplete, onFlush) { - this.error = null; - this.hls = void 0; - this.id = void 0; - this.instanceNo = transmuxerInstanceCount++; - this.observer = void 0; - this.frag = null; - this.part = null; - this.useWorker = void 0; - this.workerContext = null; - this.transmuxer = null; - this.onTransmuxComplete = void 0; - this.onFlush = void 0; - this.onWorkerMessage = event => { - const data = event.data; - const hls = this.hls; - if (!hls || !(data != null && data.event) || data.instanceNo !== this.instanceNo) { - return; - } - switch (data.event) { - case 'init': - { - var _this$workerContext; - const objectURL = (_this$workerContext = this.workerContext) == null ? void 0 : _this$workerContext.objectURL; - if (objectURL) { - // revoke the Object URL that was used to create transmuxer worker, so as not to leak it - self.URL.revokeObjectURL(objectURL); - } - break; - } - case 'transmuxComplete': - { - this.handleTransmuxComplete(data.data); - break; - } - case 'flush': - { - this.onFlush(data.data); - break; - } - - // pass logs from the worker thread to the main logger - case 'workerLog': - { - if (hls.logger[data.data.logType]) { - hls.logger[data.data.logType](data.data.message); - } - break; - } - default: - { - data.data = data.data || {}; - data.data.frag = this.frag; - data.data.part = this.part; - data.data.id = this.id; - hls.trigger(data.event, data.data); - break; - } - } - }; - this.onWorkerError = event => { - if (!this.hls) { - return; - } - const error = new Error(`${event.message} (${event.filename}:${event.lineno})`); - this.hls.config.enableWorker = false; - this.hls.logger.warn(`Error in "${this.id}" Web Worker, fallback to inline`); - this.hls.trigger(Events.ERROR, { - type: ErrorTypes.OTHER_ERROR, - details: ErrorDetails.INTERNAL_EXCEPTION, - fatal: false, - event: 'demuxerWorker', - error - }); - }; - const config = _hls.config; - this.hls = _hls; - this.id = id; - this.useWorker = !!config.enableWorker; - this.onTransmuxComplete = onTransmuxComplete; - this.onFlush = onFlush; - const forwardMessage = (ev, data) => { - data = data || {}; - data.frag = this.frag || undefined; - if (ev === Events.ERROR) { - data = data; - data.parent = this.id; - data.part = this.part; - this.error = data.error; - } - this.hls.trigger(ev, data); - }; - - // forward events to main thread - this.observer = new EventEmitter(); - this.observer.on(Events.FRAG_DECRYPTED, forwardMessage); - this.observer.on(Events.ERROR, forwardMessage); - const m2tsTypeSupported = getM2TSSupportedAudioTypes(config.preferManagedMediaSource); - if (this.useWorker && typeof Worker !== 'undefined') { - const logger = this.hls.logger; - const canCreateWorker = config.workerPath || hasUMDWorker(); - if (canCreateWorker) { - try { - if (config.workerPath) { - logger.log(`loading Web Worker ${config.workerPath} for "${id}"`); - this.workerContext = loadWorker(config.workerPath); - } else { - logger.log(`injecting Web Worker for "${id}"`); - this.workerContext = injectWorker(); - } - const { - worker - } = this.workerContext; - worker.addEventListener('message', this.onWorkerMessage); - worker.addEventListener('error', this.onWorkerError); - worker.postMessage({ - instanceNo: this.instanceNo, - cmd: 'init', - typeSupported: m2tsTypeSupported, - id, - config: JSON.stringify(config) - }); - } catch (err) { - logger.warn(`Error setting up "${id}" Web Worker, fallback to inline`, err); - this.terminateWorker(); - this.error = null; - this.transmuxer = new Transmuxer(this.observer, m2tsTypeSupported, config, '', id, _hls.logger); - } - return; - } - } - this.transmuxer = new Transmuxer(this.observer, m2tsTypeSupported, config, '', id, _hls.logger); - } - reset() { - this.frag = null; - this.part = null; - if (this.workerContext) { - const instanceNo = this.instanceNo; - this.instanceNo = transmuxerInstanceCount++; - const config = this.hls.config; - const m2tsTypeSupported = getM2TSSupportedAudioTypes(config.preferManagedMediaSource); - this.workerContext.worker.postMessage({ - instanceNo: this.instanceNo, - cmd: 'reset', - resetNo: instanceNo, - typeSupported: m2tsTypeSupported, - id: this.id, - config: JSON.stringify(config) - }); - } - } - terminateWorker() { - if (this.workerContext) { - const { - worker - } = this.workerContext; - this.workerContext = null; - worker.removeEventListener('message', this.onWorkerMessage); - worker.removeEventListener('error', this.onWorkerError); - removeWorkerFromStore(this.hls.config.workerPath); - } - } - destroy() { - if (this.workerContext) { - this.terminateWorker(); - // @ts-ignore - this.onWorkerMessage = this.onWorkerError = null; - } else { - const transmuxer = this.transmuxer; - if (transmuxer) { - transmuxer.destroy(); - this.transmuxer = null; - } - } - const observer = this.observer; - if (observer) { - observer.removeAllListeners(); - } - this.frag = null; - this.part = null; - // @ts-ignore - this.observer = null; - // @ts-ignore - this.hls = null; - } - push(data, initSegmentData, audioCodec, videoCodec, frag, part, duration, accurateTimeOffset, chunkMeta, defaultInitPTS) { - var _frag$initSegment, _lastFrag$initSegment; - chunkMeta.transmuxing.start = self.performance.now(); - const { - instanceNo, - transmuxer - } = 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; - const discontinuity = !(lastFrag && frag.cc === lastFrag.cc); - const trackSwitch = !(lastFrag && chunkMeta.level === lastFrag.level); - const snDiff = lastFrag ? chunkMeta.sn - lastFrag.sn : -1; - const partDiff = this.part ? chunkMeta.part - this.part.index : -1; - const progressive = snDiff === 0 && chunkMeta.id > 1 && chunkMeta.id === (lastFrag == null ? void 0 : lastFrag.stats.chunkCount); - const contiguous = !trackSwitch && (snDiff === 1 || snDiff === 0 && (partDiff === 1 || progressive && partDiff <= 0)); - const now = self.performance.now(); - if (trackSwitch || snDiff || frag.stats.parsing.start === 0) { - frag.stats.parsing.start = now; - } - if (part && (partDiff || !contiguous)) { - part.stats.parsing.start = now; - } - const initSegmentChange = !(lastFrag && ((_frag$initSegment = frag.initSegment) == null ? void 0 : _frag$initSegment.url) === ((_lastFrag$initSegment = lastFrag.initSegment) == null ? void 0 : _lastFrag$initSegment.url)); - const state = new TransmuxState(discontinuity, contiguous, accurateTimeOffset, trackSwitch, timeOffset, initSegmentChange); - if (!contiguous || discontinuity || initSegmentChange) { - this.hls.logger.log(`[transmuxer-interface, ${frag.type}]: Starting new transmux session for sn: ${chunkMeta.sn} p: ${chunkMeta.part} level: ${chunkMeta.level} id: ${chunkMeta.id} - discontinuity: ${discontinuity} - trackSwitch: ${trackSwitch} - contiguous: ${contiguous} - accurateTimeOffset: ${accurateTimeOffset} - timeOffset: ${timeOffset} - initSegmentChange: ${initSegmentChange}`); - const config = new TransmuxConfig(audioCodec, videoCodec, initSegmentData, duration, defaultInitPTS); - this.configureTransmuxer(config); - } - this.frag = frag; - this.part = part; - - // Frags with sn of 'initSegment' are not transmuxed - if (this.workerContext) { - // post fragment payload as transferable objects for ArrayBuffer (no copy) - this.workerContext.worker.postMessage({ - instanceNo, - cmd: 'demux', - data, - decryptdata, - chunkMeta, - state - }, data instanceof ArrayBuffer ? [data] : []); - } else if (transmuxer) { - const transmuxResult = transmuxer.push(data, decryptdata, chunkMeta, state); - if (isPromise(transmuxResult)) { - transmuxResult.then(data => { - this.handleTransmuxComplete(data); - }).catch(error => { - this.transmuxerError(error, chunkMeta, 'transmuxer-interface push error'); - }); - } else { - this.handleTransmuxComplete(transmuxResult); - } - } - } - flush(chunkMeta) { - chunkMeta.transmuxing.start = self.performance.now(); - const { - instanceNo, - transmuxer - } = this; - if (this.workerContext) { - this.workerContext.worker.postMessage({ - instanceNo, - cmd: 'flush', - chunkMeta - }); - } else if (transmuxer) { - const transmuxResult = transmuxer.flush(chunkMeta); - if (isPromise(transmuxResult)) { - transmuxResult.then(data => { - this.handleFlushResult(data, chunkMeta); - }).catch(error => { - this.transmuxerError(error, chunkMeta, 'transmuxer-interface flush error'); - }); - } else { - this.handleFlushResult(transmuxResult, chunkMeta); - } - } - } - transmuxerError(error, chunkMeta, reason) { - if (!this.hls) { - return; - } - this.error = error; - this.hls.trigger(Events.ERROR, { - type: ErrorTypes.MEDIA_ERROR, - details: ErrorDetails.FRAG_PARSING_ERROR, - chunkMeta, - frag: this.frag || undefined, - part: this.part || undefined, - fatal: false, - error, - err: error, - reason - }); - } - handleFlushResult(results, chunkMeta) { - results.forEach(result => { - this.handleTransmuxComplete(result); - }); - this.onFlush(chunkMeta); - } - configureTransmuxer(config) { - const { - instanceNo, - transmuxer - } = this; - if (this.workerContext) { - this.workerContext.worker.postMessage({ - instanceNo, - cmd: 'configure', - config - }); - } else if (transmuxer) { - transmuxer.configure(config); - } - } - handleTransmuxComplete(result) { - result.chunkMeta.transmuxing.end = self.performance.now(); - this.onTransmuxComplete(result); - } -} - -const STALL_MINIMUM_DURATION_MS = 250; -const MAX_START_GAP_JUMP = 2.0; -const SKIP_BUFFER_HOLE_STEP_SECONDS = 0.1; -const SKIP_BUFFER_RANGE_START = 0.05; -class GapController extends Logger { - constructor(config, media, fragmentTracker, hls) { - super('gap-controller', hls.logger); - this.config = void 0; - this.media = null; - this.fragmentTracker = void 0; - this.hls = void 0; - this.nudgeRetry = 0; - this.stallReported = false; - this.stalled = null; - this.moved = false; - this.seeking = false; - this.ended = 0; - this.config = config; - this.media = media; - this.fragmentTracker = fragmentTracker; - this.hls = hls; - } - destroy() { - this.media = null; - // @ts-ignore - this.hls = this.fragmentTracker = null; - } - - /** - * Checks if the playhead is stuck within a gap, and if so, attempts to free it. - * A gap is an unbuffered range between two buffered ranges (or the start and the first buffered range). - * - * @param lastCurrentTime - Previously read playhead position - */ - poll(lastCurrentTime, activeFrag, levelDetails, state) { - const { - config, - media, - stalled - } = this; - if (media === null) { - return; - } - const { - currentTime, - seeking - } = media; - const seeked = this.seeking && !seeking; - const beginSeek = !this.seeking && seeking; - this.seeking = seeking; - - // The playhead is moving, no-op - if (currentTime !== lastCurrentTime) { - if (lastCurrentTime) { - this.ended = 0; - } - this.moved = true; - if (!seeking) { - this.nudgeRetry = 0; - } - if (stalled !== null) { - // The playhead is now moving, but was previously stalled - if (this.stallReported) { - const _stalledDuration = self.performance.now() - stalled; - this.warn(`playback not stuck anymore @${currentTime}, after ${Math.round(_stalledDuration)}ms`); - this.stallReported = false; - } - this.stalled = null; - } - return; - } - - // Clear stalled state when beginning or finishing seeking so that we don't report stalls coming out of a seek - if (beginSeek || seeked) { - this.stalled = null; - return; - } - - // The playhead should not be moving - if (media.paused && !seeking || media.ended || media.playbackRate === 0 || !BufferHelper.getBuffered(media).length) { - // Fire MEDIA_ENDED to workaround event not being dispatched by browser - if (!this.ended && media.ended) { - this.ended = currentTime || 1; - this.hls.trigger(Events.MEDIA_ENDED, { - stalled: false - }); - } - this.nudgeRetry = 0; - return; - } - const bufferInfo = BufferHelper.bufferInfo(media, currentTime, 0); - const nextStart = bufferInfo.nextStart || 0; - if (seeking) { - // Waiting for seeking in a buffered range to complete - const hasEnoughBuffer = bufferInfo.len > MAX_START_GAP_JUMP; - // Next buffered range is too far ahead to jump to while still seeking - const noBufferGap = !nextStart || activeFrag && activeFrag.start <= currentTime || nextStart - currentTime > MAX_START_GAP_JUMP && !this.fragmentTracker.getPartialFragment(currentTime); - if (hasEnoughBuffer || noBufferGap) { - return; - } - // Reset moved state when seeking to a point in or before a gap - this.moved = false; - } - - // Skip start gaps if we haven't played, but the last poll detected the start of a stall - // The addition poll gives the browser a chance to jump the gap for us - if (!this.moved && this.stalled !== null) { - // There is no playable buffer (seeked, waiting for buffer) - const isBuffered = bufferInfo.len > 0; - if (!isBuffered && !nextStart) { - return; - } - // Jump start gaps within jump threshold - const startJump = Math.max(nextStart, bufferInfo.start || 0) - currentTime; - - // When joining a live stream with audio tracks, account for live playlist window sliding by allowing - // a larger jump over start gaps caused by the audio-stream-controller buffering a start fragment - // that begins over 1 target duration after the video start position. - const isLive = !!(levelDetails != null && levelDetails.live); - const maxStartGapJump = isLive ? levelDetails.targetduration * 2 : MAX_START_GAP_JUMP; - const partialOrGap = this.fragmentTracker.getPartialFragment(currentTime); - if (startJump > 0 && (startJump <= maxStartGapJump || partialOrGap)) { - if (!media.paused) { - this._trySkipBufferHole(partialOrGap); - } - return; - } - } - - // Start tracking stall time - const tnow = self.performance.now(); - if (stalled === null) { - this.stalled = tnow; - return; - } - const stalledDuration = tnow - stalled; - if (!seeking && stalledDuration >= STALL_MINIMUM_DURATION_MS) { - // Dispatch MEDIA_ENDED when media.ended/ended event is not signalled at end of stream - if (state === State.ENDED && !(levelDetails != null && levelDetails.live) && Math.abs(currentTime - ((levelDetails == null ? void 0 : levelDetails.edge) || 0)) < 1) { - if (stalledDuration < 1000 || this.ended) { - return; - } - this.ended = currentTime || 1; - this.hls.trigger(Events.MEDIA_ENDED, { - stalled: true - }); - return; - } - // Report stalling after trying to fix - this._reportStall(bufferInfo); - if (!this.media) { - return; - } - } - const bufferedWithHoles = BufferHelper.bufferInfo(media, currentTime, config.maxBufferHole); - this._tryFixBufferStall(bufferedWithHoles, stalledDuration); - } - - /** - * Detects and attempts to fix known buffer stalling issues. - * @param bufferInfo - The properties of the current buffer. - * @param stalledDurationMs - The amount of time Hls.js has been stalling for. - * @private - */ - _tryFixBufferStall(bufferInfo, stalledDurationMs) { - const { - config, - fragmentTracker, - media - } = this; - if (media === null) { - return; - } - const currentTime = media.currentTime; - const partial = fragmentTracker.getPartialFragment(currentTime); - if (partial) { - // Try to skip over the buffer hole caused by a partial fragment - // This method isn't limited by the size of the gap between buffered ranges - const targetTime = this._trySkipBufferHole(partial); - // we return here in this case, meaning - // the branch below only executes when we haven't seeked to a new position - if (targetTime || !this.media) { - return; - } - } - - // if we haven't had to skip over a buffer hole of a partial fragment - // we may just have to "nudge" the playlist as the browser decoding/rendering engine - // needs to cross some sort of threshold covering all source-buffers content - // to start playing properly. - if ((bufferInfo.len > config.maxBufferHole || bufferInfo.nextStart && bufferInfo.nextStart - currentTime < config.maxBufferHole) && stalledDurationMs > config.highBufferWatchdogPeriod * 1000) { - this.warn('Trying to nudge playhead over buffer-hole'); - // Try to nudge currentTime over a buffer hole if we've been stalling for the configured amount of seconds - // We only try to jump the hole if it's under the configured size - // Reset stalled so to rearm watchdog timer - this.stalled = null; - this._tryNudgeBuffer(); - } - } - - /** - * Triggers a BUFFER_STALLED_ERROR event, but only once per stall period. - * @param bufferLen - The playhead distance from the end of the current buffer segment. - * @private - */ - _reportStall(bufferInfo) { - const { - hls, - media, - stallReported - } = this; - if (!stallReported && media) { - // Report stalled error once - this.stallReported = true; - const error = new Error(`Playback stalling at @${media.currentTime} due to low buffer (${JSON.stringify(bufferInfo)})`); - this.warn(error.message); - hls.trigger(Events.ERROR, { - type: ErrorTypes.MEDIA_ERROR, - details: ErrorDetails.BUFFER_STALLED_ERROR, - fatal: false, - error, - buffer: bufferInfo.len - }); - } - } - - /** - * Attempts to fix buffer stalls by jumping over known gaps caused by partial fragments - * @param partial - The partial fragment found at the current time (where playback is stalling). - * @private - */ - _trySkipBufferHole(partial) { - const { - config, - hls, - media - } = this; - if (media === null) { - return 0; - } - - // Check if currentTime is between unbuffered regions of partial fragments - const currentTime = media.currentTime; - const bufferInfo = BufferHelper.bufferInfo(media, currentTime, 0); - const startTime = currentTime < bufferInfo.start ? bufferInfo.start : bufferInfo.nextStart; - if (startTime) { - const bufferStarved = bufferInfo.len <= config.maxBufferHole; - const waiting = bufferInfo.len > 0 && bufferInfo.len < 1 && media.readyState < 3; - const gapLength = startTime - currentTime; - if (gapLength > 0 && (bufferStarved || waiting)) { - // Only allow large gaps to be skipped if it is a start gap, or all fragments in skip range are partial - if (gapLength > config.maxBufferHole) { - const { - fragmentTracker - } = this; - let startGap = false; - if (currentTime === 0) { - const startFrag = fragmentTracker.getAppendedFrag(0, PlaylistLevelType.MAIN); - if (startFrag && startTime < startFrag.end) { - startGap = true; - } - } - if (!startGap) { - const startProvisioned = partial || fragmentTracker.getAppendedFrag(currentTime, PlaylistLevelType.MAIN); - if (startProvisioned) { - let moreToLoad = false; - let pos = startProvisioned.end; - while (pos < startTime) { - const provisioned = fragmentTracker.getPartialFragment(pos); - if (provisioned) { - pos += provisioned.duration; - } else { - moreToLoad = true; - break; - } - } - if (moreToLoad) { - return 0; - } - } - } - } - const targetTime = Math.max(startTime + SKIP_BUFFER_RANGE_START, currentTime + SKIP_BUFFER_HOLE_STEP_SECONDS); - this.warn(`skipping hole, adjusting currentTime from ${currentTime} to ${targetTime}`); - this.moved = true; - this.stalled = null; - media.currentTime = targetTime; - if (partial && !partial.gap) { - const error = new Error(`fragment loaded with buffer holes, seeking from ${currentTime} to ${targetTime}`); - hls.trigger(Events.ERROR, { - type: ErrorTypes.MEDIA_ERROR, - details: ErrorDetails.BUFFER_SEEK_OVER_HOLE, - fatal: false, - error, - reason: error.message, - frag: partial - }); - } - return targetTime; - } - } - return 0; - } - - /** - * Attempts to fix buffer stalls by advancing the mediaElement's current time by a small amount. - * @private - */ - _tryNudgeBuffer() { - const { - config, - hls, - media, - nudgeRetry - } = this; - if (media === null) { - return; - } - const currentTime = media.currentTime; - this.nudgeRetry++; - if (nudgeRetry < config.nudgeMaxRetry) { - const targetTime = currentTime + (nudgeRetry + 1) * config.nudgeOffset; - // playback stalled in buffered area ... let's nudge currentTime to try to overcome this - const error = new Error(`Nudging 'currentTime' from ${currentTime} to ${targetTime}`); - this.warn(error.message); - media.currentTime = targetTime; - hls.trigger(Events.ERROR, { - type: ErrorTypes.MEDIA_ERROR, - details: ErrorDetails.BUFFER_NUDGE_ON_STALL, - error, - fatal: false - }); - } else { - const error = new Error(`Playhead still not moving while enough data buffered @${currentTime} after ${config.nudgeMaxRetry} nudges`); - this.error(error.message); - hls.trigger(Events.ERROR, { - type: ErrorTypes.MEDIA_ERROR, - details: ErrorDetails.BUFFER_STALLED_ERROR, - error, - fatal: true - }); - } - } -} - -const TICK_INTERVAL$2 = 100; // how often to tick in ms - -class StreamController extends BaseStreamController { - constructor(hls, fragmentTracker, keyLoader) { - super(hls, fragmentTracker, keyLoader, 'stream-controller', PlaylistLevelType.MAIN); - this.audioCodecSwap = false; - this.gapController = null; - this.level = -1; - this._forceStartLoad = false; - this._hasEnoughToStart = false; - this.altAudio = false; - this.audioOnly = false; - this.fragPlaying = null; - this.fragLastKbps = 0; - this.couldBacktrack = false; - this.backtrackFragment = null; - this.audioCodecSwitch = false; - this.videoBuffer = null; - this.onMediaPlaying = () => { - // tick to speed up FRAG_CHANGED triggering - const gapController = this.gapController; - if (gapController) { - gapController.ended = 0; - } - this.tick(); - }; - this.onMediaSeeked = () => { - const media = this.media; - const currentTime = media ? media.currentTime : null; - if (isFiniteNumber(currentTime)) { - this.log(`Media seeked to ${currentTime.toFixed(3)}`); - } - - // If seeked was issued before buffer was appended do not tick immediately - const bufferInfo = this.getMainFwdBufferInfo(); - if (bufferInfo === null || bufferInfo.len === 0) { - this.warn(`Main forward buffer length on "seeked" event ${bufferInfo ? bufferInfo.len : 'empty'})`); - return; - } - - // tick to speed up FRAG_CHANGED triggering - this.tick(); - }; - this.registerListeners(); - } - registerListeners() { - super.registerListeners(); - const { - hls - } = this; - hls.on(Events.MANIFEST_PARSED, this.onManifestParsed, this); - hls.on(Events.LEVEL_LOADING, this.onLevelLoading, this); - hls.on(Events.LEVEL_LOADED, this.onLevelLoaded, this); - hls.on(Events.FRAG_LOAD_EMERGENCY_ABORTED, this.onFragLoadEmergencyAborted, this); - hls.on(Events.AUDIO_TRACK_SWITCHING, this.onAudioTrackSwitching, this); - hls.on(Events.AUDIO_TRACK_SWITCHED, this.onAudioTrackSwitched, this); - hls.on(Events.BUFFER_CREATED, this.onBufferCreated, this); - hls.on(Events.BUFFER_FLUSHED, this.onBufferFlushed, this); - hls.on(Events.LEVELS_UPDATED, this.onLevelsUpdated, this); - hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this); - } - unregisterListeners() { - super.unregisterListeners(); - const { - hls - } = this; - hls.off(Events.MANIFEST_PARSED, this.onManifestParsed, this); - hls.off(Events.LEVEL_LOADED, this.onLevelLoaded, this); - hls.off(Events.FRAG_LOAD_EMERGENCY_ABORTED, this.onFragLoadEmergencyAborted, this); - hls.off(Events.AUDIO_TRACK_SWITCHING, this.onAudioTrackSwitching, this); - hls.off(Events.AUDIO_TRACK_SWITCHED, this.onAudioTrackSwitched, this); - hls.off(Events.BUFFER_CREATED, this.onBufferCreated, this); - hls.off(Events.BUFFER_FLUSHED, this.onBufferFlushed, this); - hls.off(Events.LEVELS_UPDATED, this.onLevelsUpdated, this); - hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this); - } - onHandlerDestroying() { - // @ts-ignore - this.onMediaPlaying = this.onMediaSeeked = null; - this.unregisterListeners(); - super.onHandlerDestroying(); - } - startLoad(startPosition, skipSeekToStartPosition) { - if (this.levels) { - const { - lastCurrentTime, - hls - } = this; - this.stopLoad(); - this.setInterval(TICK_INTERVAL$2); - this.level = -1; - if (!this.startFragRequested) { - // determine load level - let startLevel = hls.startLevel; - if (startLevel === -1) { - if (hls.config.testBandwidth && this.levels.length > 1) { - // -1 : guess start Level by doing a bitrate test by loading first fragment of lowest quality level - startLevel = 0; - this.bitrateTest = true; - } else { - startLevel = hls.firstAutoLevel; - } - } - // set new level to playlist loader : this will trigger start level load - // hls.nextLoadLevel remains until it is set to a new value or until a new frag is successfully loaded - hls.nextLoadLevel = startLevel; - this.level = hls.loadLevel; - this._hasEnoughToStart = false; - } - // if startPosition undefined but lastCurrentTime set, set startPosition to last currentTime - if (lastCurrentTime > 0 && startPosition === -1) { - this.log(`Override startPosition with lastCurrentTime @${lastCurrentTime.toFixed(3)}`); - startPosition = lastCurrentTime; - } - this.state = State.IDLE; - this.nextLoadPosition = this.lastCurrentTime = startPosition; - this.startPosition = skipSeekToStartPosition ? -1 : startPosition; - this.tick(); - } else { - this._forceStartLoad = true; - this.state = State.STOPPED; - } - } - stopLoad() { - this._forceStartLoad = false; - super.stopLoad(); - } - doTick() { - switch (this.state) { - case State.WAITING_LEVEL: - { - const { - levels, - level - } = this; - const currentLevel = levels == null ? void 0 : levels[level]; - const details = currentLevel == null ? void 0 : currentLevel.details; - if (details && (!details.live || this.levelLastLoaded === currentLevel)) { - if (this.waitForCdnTuneIn(details)) { - break; - } - this.state = State.IDLE; - break; - } else if (this.hls.nextLoadLevel !== this.level) { - this.state = State.IDLE; - break; - } - break; - } - case State.FRAG_LOADING_WAITING_RETRY: - { - var _this$media; - const now = self.performance.now(); - const retryDate = this.retryDate; - // if current time is gt than retryDate, or if media seeking let's switch to IDLE state to retry loading - if (!retryDate || now >= retryDate || (_this$media = this.media) != null && _this$media.seeking) { - const { - levels, - level - } = this; - const currentLevel = levels == null ? void 0 : levels[level]; - this.resetStartWhenNotLoaded(currentLevel || null); - this.state = State.IDLE; - } - } - break; - } - if (this.state === State.IDLE) { - this.doTickIdle(); - } - this.onTickEnd(); - } - onTickEnd() { - super.onTickEnd(); - this.checkBuffer(); - this.checkFragmentChanged(); - } - doTickIdle() { - const { - hls, - levelLastLoaded, - levels, - media - } = this; - - // if start level not parsed yet OR - // if video not attached AND start fragment already requested OR start frag prefetch not enabled - // exit loop, as we either need more info (level not parsed) or we need media to be attached to load new fragment - if (levelLastLoaded === null || !media && (this.startFragRequested || !hls.config.startFragPrefetch)) { - return; - } - - // If the "main" level is audio-only but we are loading an alternate track in the same group, do not load anything - if (this.altAudio && this.audioOnly) { - return; - } - const level = this.buffering ? hls.nextLoadLevel : hls.loadLevel; - if (!(levels != null && levels[level])) { - return; - } - 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 = {}; - if (this.altAudio) { - data.type = 'video'; - } - this.hls.trigger(Events.BUFFER_EOS, data); - this.state = State.ENDED; - return; - } - if (!this.buffering) { - return; - } - - // set next load level : this will trigger a playlist load if needed - if (hls.loadLevel !== level && hls.manualLevel === -1) { - this.log(`Adapting to level ${level} from level ${this.level}`); - } - this.level = hls.nextLoadLevel = level; - const levelDetails = levelInfo.details; - // if level info not retrieved yet, switch state and wait for level retrieval - // if live playlist, ensure that new playlist has been refreshed to avoid loading/try to load - // a useless and outdated fragment (that might even introduce load error if it is already out of the live playlist) - if (!levelDetails || this.state === State.WAITING_LEVEL || levelDetails.live && this.levelLastLoaded !== levelInfo) { - this.level = level; - this.state = State.WAITING_LEVEL; - 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 - const maxBufLen = this.getMaxBufferLength(levelInfo.maxBitrate); - - // Stay idle if we are still with buffer margins - if (bufferLen >= maxBufLen) { - return; - } - if (this.backtrackFragment && this.backtrackFragment.start > bufferInfo.end) { - this.backtrackFragment = null; - } - const targetBufferTime = this.backtrackFragment ? this.backtrackFragment.start : bufferInfo.end; - let frag = this.getNextFragment(targetBufferTime, levelDetails); - // Avoid backtracking by loading an earlier segment in streams with segments that do not start with a key frame (flagged by `couldBacktrack`) - if (this.couldBacktrack && !this.fragPrevious && frag && frag.sn !== 'initSegment' && this.fragmentTracker.getState(frag) !== FragmentState.OK) { - var _this$backtrackFragme; - const backtrackSn = ((_this$backtrackFragme = this.backtrackFragment) != null ? _this$backtrackFragme : frag).sn; - const fragIdx = backtrackSn - levelDetails.startSN; - const backtrackFrag = levelDetails.fragments[fragIdx - 1]; - if (backtrackFrag && frag.cc === backtrackFrag.cc) { - frag = backtrackFrag; - this.fragmentTracker.removeFragment(backtrackFrag); - } - } else if (this.backtrackFragment && bufferInfo.len) { - this.backtrackFragment = null; - } - // Avoid loop loading by using nextLoadPosition set for backtracking and skipping consecutive GAP tags - if (frag && this.isLoopLoading(frag, targetBufferTime)) { - const gapStart = frag.gap; - if (!gapStart) { - // Cleanup the fragment tracker before trying to find the next unbuffered fragment - const type = this.audioOnly && !this.altAudio ? ElementaryStreamTypes.AUDIO : ElementaryStreamTypes.VIDEO; - const mediaBuffer = (type === ElementaryStreamTypes.VIDEO ? this.videoBuffer : this.mediaBuffer) || this.media; - if (mediaBuffer) { - this.afterBufferFlushed(mediaBuffer, type, PlaylistLevelType.MAIN); - } - } - frag = this.getNextFragmentLoopLoading(frag, levelDetails, bufferInfo, PlaylistLevelType.MAIN, maxBufLen); - } - if (!frag) { - return; - } - if (frag.initSegment && !frag.initSegment.data && !this.bitrateTest) { - frag = frag.initSegment; - } - this.loadFragment(frag, levelInfo, targetBufferTime); - } - loadFragment(frag, level, targetBufferTime) { - // Check if fragment is not loaded - const fragState = this.fragmentTracker.getState(frag); - if (fragState === FragmentState.NOT_LOADED || fragState === FragmentState.PARTIAL) { - if (frag.sn === 'initSegment') { - this._loadInitSegment(frag, level); - } 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, level); - } else { - super.loadFragment(frag, level, targetBufferTime); - } - } else { - this.clearTrackerIfNeeded(frag); - } - } - getBufferedFrag(position) { - return this.fragmentTracker.getBufferedFrag(position, PlaylistLevelType.MAIN); - } - followingBufferedFrag(frag) { - if (frag) { - // try to get range of next fragment (500ms after this range) - return this.getBufferedFrag(frag.end + 0.5); - } - return null; - } - - /* - on immediate level switch : - - pause playback if playing - - cancel any pending load request - - and trigger a buffer flush - */ - immediateLevelSwitch() { - this.abortCurrentFrag(); - this.flushMainBuffer(0, Number.POSITIVE_INFINITY); - } - - /** - * try to switch ASAP without breaking video playback: - * in order to ensure smooth but quick level switching, - * we need to find the next flushable buffer range - * we should take into account new segment fetch time - */ - nextLevelSwitch() { - const { - levels, - media - } = this; - // ensure that media is defined and that metadata are available (to retrieve currentTime) - if (media != null && media.readyState) { - let fetchdelay; - const fragPlayingCurrent = this.getAppendedFrag(media.currentTime); - if (fragPlayingCurrent && fragPlayingCurrent.start > 1) { - // flush buffer preceding current fragment (flush until current fragment start offset) - // minus 1s to avoid video freezing, that could happen if we flush keyframe of current video ... - this.flushMainBuffer(0, fragPlayingCurrent.start - 1); - } - const levelDetails = this.getLevelDetails(); - if (levelDetails != null && levelDetails.live) { - const bufferInfo = this.getMainFwdBufferInfo(); - // Do not flush in live stream with low buffer - if (!bufferInfo || bufferInfo.len < levelDetails.targetduration * 2) { - return; - } - } - if (!media.paused && levels) { - // add a safety delay of 1s - const nextLevelId = this.hls.nextLoadLevel; - const nextLevel = levels[nextLevelId]; - const fragLastKbps = this.fragLastKbps; - if (fragLastKbps && this.fragCurrent) { - fetchdelay = this.fragCurrent.duration * nextLevel.maxBitrate / (1000 * fragLastKbps) + 1; - } else { - fetchdelay = 0; - } - } else { - fetchdelay = 0; - } - // this.log('fetchdelay:'+fetchdelay); - // find buffer range that will be reached once new fragment will be fetched - const bufferedFrag = this.getBufferedFrag(media.currentTime + fetchdelay); - if (bufferedFrag) { - // we can flush buffer range following this one without stalling playback - const nextBufferedFrag = this.followingBufferedFrag(bufferedFrag); - if (nextBufferedFrag) { - // if we are here, we can also cancel any loading/demuxing in progress, as they are useless - this.abortCurrentFrag(); - // start flush position is in next buffered frag. Leave some padding for non-independent segments and smoother playback. - const maxStart = nextBufferedFrag.maxStartPTS ? nextBufferedFrag.maxStartPTS : nextBufferedFrag.start; - const fragDuration = nextBufferedFrag.duration; - const startPts = Math.max(bufferedFrag.end, maxStart + Math.min(Math.max(fragDuration - this.config.maxFragLookUpTolerance, fragDuration * (this.couldBacktrack ? 0.5 : 0.125)), fragDuration * (this.couldBacktrack ? 0.75 : 0.25))); - this.flushMainBuffer(startPts, Number.POSITIVE_INFINITY); - } - } - } - } - abortCurrentFrag() { - const fragCurrent = this.fragCurrent; - this.fragCurrent = null; - this.backtrackFragment = null; - if (fragCurrent) { - fragCurrent.abortRequests(); - this.fragmentTracker.removeFragment(fragCurrent); - } - switch (this.state) { - case State.KEY_LOADING: - case State.FRAG_LOADING: - case State.FRAG_LOADING_WAITING_RETRY: - case State.PARSING: - case State.PARSED: - this.state = State.IDLE; - break; - } - this.nextLoadPosition = this.getLoadPosition(); - } - flushMainBuffer(startOffset, endOffset) { - super.flushMainBuffer(startOffset, endOffset, this.altAudio ? 'video' : null); - } - onMediaAttached(event, data) { - super.onMediaAttached(event, data); - const media = data.media; - media.removeEventListener('playing', this.onMediaPlaying); - media.removeEventListener('seeked', this.onMediaSeeked); - media.addEventListener('playing', this.onMediaPlaying); - media.addEventListener('seeked', this.onMediaSeeked); - this.gapController = new GapController(this.config, media, this.fragmentTracker, this.hls); - } - onMediaDetaching(event, data) { - const { - media - } = this; - if (media) { - media.removeEventListener('playing', this.onMediaPlaying); - media.removeEventListener('seeked', this.onMediaSeeked); - } - this.videoBuffer = null; - this.fragPlaying = null; - if (this.gapController) { - this.gapController.destroy(); - this.gapController = null; - } - super.onMediaDetaching(event, data); - const transferringMedia = !!data.transferMedia; - if (transferringMedia) { - return; - } - this._hasEnoughToStart = false; - } - triggerEnded() { - const gapController = this.gapController; - if (gapController) { - var _this$media2; - if (gapController.ended) { - return; - } - gapController.ended = ((_this$media2 = this.media) == null ? void 0 : _this$media2.currentTime) || 1; - } - this.hls.trigger(Events.MEDIA_ENDED, { - stalled: false - }); - } - onManifestLoading() { - super.onManifestLoading(); - // reset buffer on manifest loading - this.log('Trigger BUFFER_RESET'); - this.hls.trigger(Events.BUFFER_RESET, undefined); - this.couldBacktrack = false; - this.fragLastKbps = 0; - this.fragPlaying = this.backtrackFragment = null; - this.altAudio = this.audioOnly = false; - } - onManifestParsed(event, data) { - // detect if we have different kind of audio codecs used amongst playlists - let aac = false; - let heaac = false; - data.levels.forEach(level => { - const codec = level.audioCodec; - if (codec) { - aac = aac || codec.indexOf('mp4a.40.2') !== -1; - heaac = heaac || codec.indexOf('mp4a.40.5') !== -1; - } - }); - this.audioCodecSwitch = aac && heaac && !changeTypeSupported(); - if (this.audioCodecSwitch) { - this.log('Both AAC/HE-AAC audio found in levels; declaring level codec as HE-AAC'); - } - this.levels = data.levels; - this.startFragRequested = false; - } - onLevelLoading(event, data) { - const { - levels - } = this; - if (!levels || this.state !== State.IDLE) { - return; - } - const level = levels[data.level]; - if (!level.details || level.details.live && this.levelLastLoaded !== level || this.waitForCdnTuneIn(level.details)) { - this.state = State.WAITING_LEVEL; - } - } - onLevelLoaded(event, data) { - var _curLevel$details; - const { - levels, - startFragRequested - } = this; - const newLevelId = data.level; - const newDetails = data.details; - const duration = newDetails.totalduration; - if (!levels) { - this.warn(`Levels were reset while loading level ${newLevelId}`); - return; - } - this.log(`Level ${newLevelId} loaded [${newDetails.startSN},${newDetails.endSN}]${newDetails.lastPartSn ? `[part-${newDetails.lastPartSn}-${newDetails.lastPartIndex}]` : ''}, cc [${newDetails.startCC}, ${newDetails.endCC}] duration:${duration}`); - const curLevel = levels[newLevelId]; - const fragCurrent = this.fragCurrent; - if (fragCurrent && (this.state === State.FRAG_LOADING || this.state === State.FRAG_LOADING_WAITING_RETRY)) { - if (fragCurrent.level !== data.level && fragCurrent.loader) { - this.abortCurrentFrag(); - } - } - let sliding = 0; - if (newDetails.live || (_curLevel$details = curLevel.details) != null && _curLevel$details.live) { - var _this$levelLastLoaded; - this.checkLiveUpdate(newDetails); - if (newDetails.deltaUpdateFailed) { - return; - } - sliding = this.alignPlaylists(newDetails, curLevel.details, (_this$levelLastLoaded = this.levelLastLoaded) == null ? void 0 : _this$levelLastLoaded.details); - } - // override level info - curLevel.details = newDetails; - this.levelLastLoaded = curLevel; - if (!startFragRequested) { - this.setStartPosition(newDetails, sliding); - } - this.hls.trigger(Events.LEVEL_UPDATED, { - details: newDetails, - level: newLevelId - }); - - // only switch back to IDLE state if we were waiting for level to start downloading a new fragment - if (this.state === State.WAITING_LEVEL) { - if (this.waitForCdnTuneIn(newDetails)) { - // Wait for Low-Latency CDN Tune-in - return; - } - this.state = State.IDLE; - } - if (startFragRequested && newDetails.live) { - this.synchronizeToLiveEdge(newDetails); - } - - // trigger handler right now - this.tick(); - } - synchronizeToLiveEdge(levelDetails) { - const { - config, - media - } = this; - if (!media) { - return; - } - const liveSyncPosition = this.hls.liveSyncPosition; - const currentTime = media.currentTime; - const start = levelDetails.fragmentStart; - const end = levelDetails.edge; - const withinSlidingWindow = currentTime >= start - config.maxFragLookUpTolerance && currentTime <= end; - // Continue if we can seek forward to sync position or if current time is outside of sliding window - if (liveSyncPosition !== null && media.duration > liveSyncPosition && (currentTime < liveSyncPosition || !withinSlidingWindow)) { - // Continue if buffer is starving or if current time is behind max latency - const maxLatency = config.liveMaxLatencyDuration !== undefined ? config.liveMaxLatencyDuration : config.liveMaxLatencyDurationCount * levelDetails.targetduration; - if (!withinSlidingWindow && media.readyState < 4 || currentTime < end - maxLatency) { - if (!this._hasEnoughToStart) { - this.nextLoadPosition = liveSyncPosition; - } - // Only seek if ready and there is not a significant forward buffer available for playback - if (media.readyState) { - this.warn(`Playback: ${currentTime.toFixed(3)} is located too far from the end of live sliding playlist: ${end}, reset currentTime to : ${liveSyncPosition.toFixed(3)}`); - media.currentTime = liveSyncPosition; - } - } - } - } - _handleFragmentLoadProgress(data) { - var _frag$initSegment; - const frag = data.frag; - const { - part, - payload - } = data; - const { - levels - } = this; - if (!levels) { - this.warn(`Levels were reset while fragment load was in progress. Fragment ${frag.sn} of level ${frag.level} will not be buffered`); - return; - } - const currentLevel = levels[frag.level]; - if (!currentLevel) { - this.warn(`Level ${frag.level} not found on progress`); - return; - } - const details = currentLevel.details; - if (!details) { - this.warn(`Dropping fragment ${frag.sn} of level ${frag.level} after level details were reset`); - this.fragmentTracker.removeFragment(frag); - return; - } - const videoCodec = currentLevel.videoCodec; - - // time Offset is accurate if level PTS is known, or if playlist is not sliding (not live) - const accurateTimeOffset = details.PTSKnown || !details.live; - const initSegmentData = (_frag$initSegment = frag.initSegment) == null ? void 0 : _frag$initSegment.data; - const audioCodec = this._getAudioCodec(currentLevel); - - // transmux the MPEG-TS data to ISO-BMFF segments - // this.log(`Transmuxing ${frag.sn} of [${details.startSN} ,${details.endSN}],level ${frag.level}, cc ${frag.cc}`); - const transmuxer = this.transmuxer = this.transmuxer || new TransmuxerInterface(this.hls, PlaylistLevelType.MAIN, this._handleTransmuxComplete.bind(this), this._handleTransmuxerFlush.bind(this)); - const partIndex = part ? part.index : -1; - const partial = partIndex !== -1; - const chunkMeta = new ChunkMetadata(frag.level, frag.sn, frag.stats.chunkCount, payload.byteLength, partIndex, partial); - const initPTS = this.initPTS[frag.cc]; - transmuxer.push(payload, initSegmentData, audioCodec, videoCodec, frag, part, details.totalduration, accurateTimeOffset, chunkMeta, initPTS); - } - onAudioTrackSwitching(event, data) { - // if any URL found on new audio track, it is an alternate audio track - const fromAltAudio = this.altAudio; - const altAudio = !!data.url; - // if we switch on main audio, ensure that main fragment scheduling is synced with media.buffered - // don't do anything if we switch to alt audio: audio stream controller is handling it. - // we will just have to change buffer scheduling on audioTrackSwitched - if (!altAudio) { - if (this.mediaBuffer !== this.media) { - this.log('Switching on main audio, use media.buffered to schedule main fragment loading'); - 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) { - this.log('Switching to main audio track, cancel main fragment load'); - fragCurrent.abortRequests(); - this.fragmentTracker.removeFragment(fragCurrent); - } - // destroy transmuxer to force init segment generation (following audio switch) - this.resetTransmuxer(); - // switch to IDLE state to load new fragment - this.resetLoadingState(); - } else if (this.audioOnly) { - // Reset audio transmuxer so when switching back to main audio we're not still appending where we left off - this.resetTransmuxer(); - } - const hls = this.hls; - // If switching from alt to main audio, flush all audio and trigger track switched - if (fromAltAudio) { - hls.trigger(Events.BUFFER_FLUSHING, { - startOffset: 0, - endOffset: Number.POSITIVE_INFINITY, - type: null - }); - this.fragmentTracker.removeAllFragments(); - } - hls.trigger(Events.AUDIO_TRACK_SWITCHED, data); - } - } - onAudioTrackSwitched(event, data) { - const trackId = data.id; - const altAudio = !!this.hls.audioTracks[trackId].url; - if (altAudio) { - const videoBuffer = this.videoBuffer; - // if we switched on alternate audio, ensure that main fragment scheduling is synced with video sourcebuffer buffered - if (videoBuffer && this.mediaBuffer !== videoBuffer) { - this.log('Switching on alternate audio, use video.buffered to schedule main fragment loading'); - this.mediaBuffer = videoBuffer; - } - } - this.altAudio = altAudio; - this.tick(); - } - onBufferCreated(event, data) { - const tracks = data.tracks; - let mediaTrack; - let name; - let alternate = false; - for (const type in tracks) { - const track = tracks[type]; - if (track.id === 'main') { - name = type; - mediaTrack = track; - // keep video source buffer reference - if (type === 'video') { - const videoTrack = tracks[type]; - if (videoTrack) { - this.videoBuffer = videoTrack.buffer; - } - } - } else { - alternate = true; - } - } - if (alternate && mediaTrack) { - this.log(`Alternate track found, use ${name}.buffered to schedule main fragment loading`); - this.mediaBuffer = mediaTrack.buffer; - } else { - this.mediaBuffer = this.media; - } - } - onFragBuffered(event, data) { - const { - frag, - part - } = data; - const bufferedMainFragment = frag.type === PlaylistLevelType.MAIN; - if (bufferedMainFragment) { - if (this.fragContextChanged(frag)) { - // If a level switch was requested while a fragment was buffering, it will emit the FRAG_BUFFERED event upon completion - // Avoid setting state back to IDLE, since that will interfere with a level switch - this.warn(`Fragment ${frag.sn}${part ? ' p: ' + part.index : ''} of level ${frag.level} finished buffering, but was aborted. state: ${this.state}`); - if (this.state === State.PARSED) { - this.state = State.IDLE; - } - return; - } - const stats = part ? part.stats : frag.stats; - this.fragLastKbps = Math.round(8 * stats.total / (stats.buffering.end - stats.loading.first)); - if (frag.sn !== 'initSegment') { - this.fragPrevious = frag; - } - this.fragBufferedComplete(frag, part); - } - const media = this.media; - if (!media) { - return; - } - if (!this._hasEnoughToStart && media.buffered.length) { - this._hasEnoughToStart = true; - this.seekToStartPos(); - } - if (bufferedMainFragment) { - this.tick(); - } - } - get hasEnoughToStart() { - return this._hasEnoughToStart; - } - onError(event, data) { - var _data$context; - if (data.fatal) { - this.state = State.ERROR; - return; - } - switch (data.details) { - case ErrorDetails.FRAG_GAP: - case ErrorDetails.FRAG_PARSING_ERROR: - case ErrorDetails.FRAG_DECRYPT_ERROR: - case ErrorDetails.FRAG_LOAD_ERROR: - case ErrorDetails.FRAG_LOAD_TIMEOUT: - case ErrorDetails.KEY_LOAD_ERROR: - case ErrorDetails.KEY_LOAD_TIMEOUT: - this.onFragmentOrKeyLoadError(PlaylistLevelType.MAIN, data); - break; - case ErrorDetails.LEVEL_LOAD_ERROR: - case ErrorDetails.LEVEL_LOAD_TIMEOUT: - case ErrorDetails.LEVEL_PARSING_ERROR: - // in case of non fatal error while loading level, if level controller is not retrying to load level, switch back to IDLE - if (!data.levelRetry && this.state === State.WAITING_LEVEL && ((_data$context = data.context) == null ? void 0 : _data$context.type) === PlaylistContextType.LEVEL) { - this.state = State.IDLE; - } - break; - case ErrorDetails.BUFFER_APPEND_ERROR: - case ErrorDetails.BUFFER_FULL_ERROR: - if (!data.parent || data.parent !== 'main') { - return; - } - if (data.details === ErrorDetails.BUFFER_APPEND_ERROR) { - this.resetLoadingState(); - return; - } - if (this.reduceLengthAndFlushBuffer(data)) { - this.flushMainBuffer(0, Number.POSITIVE_INFINITY); - } - break; - case ErrorDetails.INTERNAL_EXCEPTION: - this.recoverWorkerError(data); - break; - } - } - - // Checks the health of the buffer and attempts to resolve playback stalls. - checkBuffer() { - const { - media, - gapController - } = this; - if (!media || !gapController || !media.readyState) { - // Exit early if we don't have media or if the media hasn't buffered anything yet (readyState 0) - return; - } - if (this._hasEnoughToStart || !BufferHelper.getBuffered(media).length) { - // Resolve gaps using the main buffer, whose ranges are the intersections of the A/V sourcebuffers - const state = this.state; - const activeFrag = state !== State.IDLE ? this.fragCurrent : null; - const levelDetails = this.getLevelDetails(); - gapController.poll(this.lastCurrentTime, activeFrag, levelDetails, state); - } - this.lastCurrentTime = media.currentTime; - } - onFragLoadEmergencyAborted() { - this.state = State.IDLE; - // if loadedmetadata is not set, it means that we are emergency switch down on first frag - // in that case, reset startFragRequested flag - if (!this._hasEnoughToStart) { - this.startFragRequested = false; - this.nextLoadPosition = this.lastCurrentTime; - } - this.tickImmediate(); - } - onBufferFlushed(event, { - type - }) { - if (type !== ElementaryStreamTypes.AUDIO || this.audioOnly && !this.altAudio) { - const mediaBuffer = (type === ElementaryStreamTypes.VIDEO ? this.videoBuffer : this.mediaBuffer) || this.media; - this.afterBufferFlushed(mediaBuffer, type, PlaylistLevelType.MAIN); - this.tick(); - } - } - onLevelsUpdated(event, data) { - if (this.level > -1 && this.fragCurrent) { - this.level = this.fragCurrent.level; - } - this.levels = data.levels; - } - swapAudioCodec() { - this.audioCodecSwap = !this.audioCodecSwap; - } - - /** - * Seeks to the set startPosition if not equal to the mediaElement's current time. - */ - seekToStartPos() { - const { - media - } = this; - if (!media) { - return; - } - const currentTime = media.currentTime; - let startPosition = this.startPosition; - // only adjust currentTime if different from startPosition or if startPosition not buffered - // at that stage, there should be only one buffered range, as we reach that code after first fragment has been buffered - if (startPosition >= 0) { - if (media.seeking) { - this.log(`could not seek to ${startPosition}, already seeking at ${currentTime}`); - return; - } - - // Offset start position by timeline offset - const details = this.getLevelDetails(); - const configuredTimelineOffset = this.config.timelineOffset; - if (configuredTimelineOffset && startPosition) { - startPosition += (details == null ? void 0 : details.appliedTimelineOffset) || configuredTimelineOffset; - } - const buffered = BufferHelper.getBuffered(media); - const bufferStart = buffered.length ? buffered.start(0) : 0; - const delta = bufferStart - startPosition; - const skipTolerance = Math.max(this.config.maxBufferHole, this.config.maxFragLookUpTolerance); - if (delta > 0 && (delta < skipTolerance || this.loadingParts && delta < 2 * ((details == null ? void 0 : details.partTarget) || 0))) { - this.log(`adjusting start position by ${delta} to match buffer start`); - startPosition += delta; - this.startPosition = startPosition; - } - if (currentTime < startPosition) { - this.log(`seek to target start position ${startPosition} from current time ${currentTime} buffer start ${bufferStart}`); - media.currentTime = startPosition; - } - } - } - _getAudioCodec(currentLevel) { - let audioCodec = this.config.defaultAudioCodec || currentLevel.audioCodec; - if (this.audioCodecSwap && audioCodec) { - this.log('Swapping audio codec'); - if (audioCodec.indexOf('mp4a.40.5') !== -1) { - audioCodec = 'mp4a.40.2'; - } else { - audioCodec = 'mp4a.40.5'; - } - } - return audioCodec; - } - _loadBitrateTestFrag(frag, level) { - frag.bitrateTest = true; - this._doFragLoad(frag, level).then(data => { - const { - hls - } = this; - if (!data || this.fragContextChanged(frag)) { - return; - } - level.fragmentError = 0; - this.state = State.IDLE; - this.startFragRequested = false; - this.bitrateTest = false; - const stats = frag.stats; - // Bitrate tests fragments are neither parsed nor buffered - stats.parsing.start = stats.parsing.end = stats.buffering.start = stats.buffering.end = self.performance.now(); - hls.trigger(Events.FRAG_LOADED, data); - frag.bitrateTest = false; - }); - } - _handleTransmuxComplete(transmuxResult) { - var _id3$samples; - const id = this.playlistType; - const { - hls - } = this; - const { - remuxResult, - chunkMeta - } = transmuxResult; - const context = this.getCurrentContext(chunkMeta); - if (!context) { - this.resetWhenMissingContext(chunkMeta); - return; - } - const { - frag, - part, - level - } = context; - const { - video, - text, - id3, - initSegment - } = remuxResult; - const { - details - } = level; - // The audio-stream-controller handles audio buffering if Hls.js is playing an alternate audio track - const audio = this.altAudio ? undefined : remuxResult.audio; - - // Check if the current fragment has been aborted. We check this by first seeing if we're still playing the current level. - // If we are, subsequently check if the currently loading fragment (fragCurrent) has changed. - if (this.fragContextChanged(frag)) { - this.fragmentTracker.removeFragment(frag); - return; - } - this.state = State.PARSING; - if (initSegment) { - if (initSegment != null && initSegment.tracks) { - const mapFragment = frag.initSegment || frag; - this._bufferInitSegment(level, initSegment.tracks, mapFragment, chunkMeta); - hls.trigger(Events.FRAG_PARSING_INIT_SEGMENT, { - frag: mapFragment, - id, - tracks: initSegment.tracks - }); - } - - // This would be nice if Number.isFinite acted as a typeguard, but it doesn't. See: https://github.com/Microsoft/TypeScript/issues/10038 - const initPTS = initSegment.initPTS; - const timescale = initSegment.timescale; - if (isFiniteNumber(initPTS)) { - this.initPTS[frag.cc] = { - baseTime: initPTS, - timescale - }; - hls.trigger(Events.INIT_PTS_FOUND, { - frag, - id, - initPTS, - timescale - }); - } - } - - // Avoid buffering if backtracking this fragment - if (video && details) { - const prevFrag = details.fragments[frag.sn - 1 - details.startSN]; - const isFirstFragment = frag.sn === details.startSN; - const isFirstInDiscontinuity = !prevFrag || frag.cc > prevFrag.cc; - if (remuxResult.independent !== false) { - const { - startPTS, - endPTS, - startDTS, - endDTS - } = video; - if (part) { - part.elementaryStreams[video.type] = { - startPTS, - endPTS, - startDTS, - endDTS - }; - } else { - if (video.firstKeyFrame && video.independent && chunkMeta.id === 1 && !isFirstInDiscontinuity) { - this.couldBacktrack = true; - } - if (video.dropped && video.independent) { - // Backtrack if dropped frames create a gap after currentTime - - const bufferInfo = this.getMainFwdBufferInfo(); - const targetBufferTime = (bufferInfo ? bufferInfo.end : this.getLoadPosition()) + this.config.maxBufferHole; - const startTime = video.firstKeyFramePTS ? video.firstKeyFramePTS : startPTS; - if (!isFirstFragment && targetBufferTime < startTime - this.config.maxBufferHole && !isFirstInDiscontinuity) { - this.backtrack(frag); - return; - } else if (isFirstInDiscontinuity) { - // Mark segment with a gap to avoid loop loading - frag.gap = true; - } - // Set video stream start to fragment start so that truncated samples do not distort the timeline, and mark it partial - frag.setElementaryStreamInfo(video.type, frag.start, endPTS, frag.start, endDTS, true); - } else if (isFirstFragment && startPTS - (details.appliedTimelineOffset || 0) > MAX_START_GAP_JUMP) { - // Mark segment with a gap to skip large start gap - frag.gap = true; - } - } - frag.setElementaryStreamInfo(video.type, startPTS, endPTS, startDTS, endDTS); - if (this.backtrackFragment) { - this.backtrackFragment = frag; - } - this.bufferFragmentData(video, frag, part, chunkMeta, isFirstFragment || isFirstInDiscontinuity); - } else if (isFirstFragment || isFirstInDiscontinuity) { - // Mark segment with a gap to avoid loop loading - frag.gap = true; - } else { - this.backtrack(frag); - return; - } - } - if (audio) { - const { - startPTS, - endPTS, - startDTS, - endDTS - } = audio; - if (part) { - part.elementaryStreams[ElementaryStreamTypes.AUDIO] = { - startPTS, - endPTS, - startDTS, - endDTS - }; - } - frag.setElementaryStreamInfo(ElementaryStreamTypes.AUDIO, startPTS, endPTS, startDTS, endDTS); - this.bufferFragmentData(audio, frag, part, chunkMeta); - } - if (details && id3 != null && (_id3$samples = id3.samples) != null && _id3$samples.length) { - const emittedID3 = { - id, - frag, - details, - samples: id3.samples - }; - hls.trigger(Events.FRAG_PARSING_METADATA, emittedID3); - } - if (details && text) { - const emittedText = { - id, - frag, - details, - samples: text.samples - }; - hls.trigger(Events.FRAG_PARSING_USERDATA, emittedText); - } - } - _bufferInitSegment(currentLevel, tracks, frag, chunkMeta) { - if (this.state !== State.PARSING) { - return; - } - this.audioOnly = !!tracks.audio && !tracks.video; - - // if audio track is expected to come from audio stream controller, discard any coming from main - if (this.altAudio && !this.audioOnly) { - delete tracks.audio; - } - // include levelCodec in audio and video tracks - const { - audio, - video, - audiovideo - } = tracks; - if (audio) { - let audioCodec = pickMostCompleteCodecName(audio.codec, currentLevel.audioCodec); - // Add level and profile to make up for passthrough-remuxer not being able to parse full codec - // (logger warning "Unhandled audio codec...") - if (audioCodec === 'mp4a') { - audioCodec = 'mp4a.40.5'; - } - // Handle `audioCodecSwitch` - const ua = navigator.userAgent.toLowerCase(); - if (this.audioCodecSwitch) { - if (audioCodec) { - if (audioCodec.indexOf('mp4a.40.5') !== -1) { - audioCodec = 'mp4a.40.2'; - } else { - audioCodec = 'mp4a.40.5'; - } - } - // In the case that AAC and HE-AAC audio codecs are signalled in manifest, - // force HE-AAC, as it seems that most browsers prefers it. - // don't force HE-AAC if mono stream, or in Firefox - const audioMetadata = audio.metadata; - if (audioMetadata && 'channelCount' in audioMetadata && (audioMetadata.channelCount || 1) !== 1 && ua.indexOf('firefox') === -1) { - audioCodec = 'mp4a.40.5'; - } - } - // HE-AAC is broken on Android, always signal audio codec as AAC even if variant manifest states otherwise - if (audioCodec && audioCodec.indexOf('mp4a.40.5') !== -1 && ua.indexOf('android') !== -1 && audio.container !== 'audio/mpeg') { - // Exclude mpeg audio - audioCodec = 'mp4a.40.2'; - this.log(`Android: force audio codec to ${audioCodec}`); - } - if (currentLevel.audioCodec && currentLevel.audioCodec !== audioCodec) { - this.log(`Swapping manifest audio codec "${currentLevel.audioCodec}" for "${audioCodec}"`); - } - audio.levelCodec = audioCodec; - audio.id = 'main'; - this.log(`Init audio buffer, container:${audio.container}, codecs[selected/level/parsed]=[${audioCodec || ''}/${currentLevel.audioCodec || ''}/${audio.codec}]`); - delete tracks.audiovideo; - } - if (video) { - video.levelCodec = currentLevel.videoCodec; - video.id = 'main'; - const parsedVideoCodec = video.codec; - if ((parsedVideoCodec == null ? void 0 : parsedVideoCodec.length) === 4) { - // Make up for passthrough-remuxer not being able to parse full codec - // (logger warning "Unhandled video codec...") - switch (parsedVideoCodec) { - case 'hvc1': - case 'hev1': - video.codec = 'hvc1.1.6.L120.90'; - break; - case 'av01': - video.codec = 'av01.0.04M.08'; - break; - case 'avc1': - video.codec = 'avc1.42e01e'; - break; - } - } - this.log(`Init video buffer, container:${video.container}, codecs[level/parsed]=[${currentLevel.videoCodec || ''}/${parsedVideoCodec}${video.codec !== parsedVideoCodec ? ' parsed-corrected=' + video.codec : ''}}]`); - delete tracks.audiovideo; - } - if (audiovideo) { - this.log(`Init audiovideo buffer, container:${audiovideo.container}, codecs[level/parsed]=[${currentLevel.codecs}/${audiovideo.codec}]`); - delete tracks.video; - delete tracks.audio; - } - const trackTypes = Object.keys(tracks); - if (trackTypes.length) { - this.hls.trigger(Events.BUFFER_CODECS, tracks); - if (!this.hls) { - // Exit after fatal tracks error - return; - } - // loop through tracks that are going to be provided to bufferController - trackTypes.forEach(trackName => { - const track = tracks[trackName]; - const initSegment = track.initSegment; - if (initSegment != null && initSegment.byteLength) { - this.hls.trigger(Events.BUFFER_APPENDING, { - type: trackName, - data: initSegment, - frag, - part: null, - chunkMeta, - parent: frag.type - }); - } - }); - } - // trigger handler right now - this.tickImmediate(); - } - getMainFwdBufferInfo() { - return this.getFwdBufferInfo(this.mediaBuffer ? this.mediaBuffer : this.media, PlaylistLevelType.MAIN); - } - get maxBufferLength() { - const { - levels, - level - } = this; - const levelInfo = levels == null ? void 0 : levels[level]; - if (!levelInfo) { - return this.config.maxBufferLength; - } - return this.getMaxBufferLength(levelInfo.maxBitrate); - } - backtrack(frag) { - this.couldBacktrack = true; - // Causes findFragments to backtrack through fragments to find the keyframe - this.backtrackFragment = frag; - this.resetTransmuxer(); - this.flushBufferGap(frag); - this.fragmentTracker.removeFragment(frag); - this.fragPrevious = null; - this.nextLoadPosition = frag.start; - this.state = State.IDLE; - } - checkFragmentChanged() { - const video = this.media; - let fragPlayingCurrent = null; - if (video && video.readyState > 1 && video.seeking === false) { - const currentTime = video.currentTime; - /* if video element is in seeked state, currentTime can only increase. - (assuming that playback rate is positive ...) - As sometimes currentTime jumps back to zero after a - media decode error, check this, to avoid seeking back to - wrong position after a media decode error - */ - - if (BufferHelper.isBuffered(video, currentTime)) { - fragPlayingCurrent = this.getAppendedFrag(currentTime); - } else if (BufferHelper.isBuffered(video, currentTime + 0.1)) { - /* ensure that FRAG_CHANGED event is triggered at startup, - when first video frame is displayed and playback is paused. - add a tolerance of 100ms, in case current position is not buffered, - check if current pos+100ms is buffered and use that buffer range - for FRAG_CHANGED event reporting */ - fragPlayingCurrent = this.getAppendedFrag(currentTime + 0.1); - } - if (fragPlayingCurrent) { - this.backtrackFragment = null; - const fragPlaying = this.fragPlaying; - const fragCurrentLevel = fragPlayingCurrent.level; - if (!fragPlaying || fragPlayingCurrent.sn !== fragPlaying.sn || fragPlaying.level !== fragCurrentLevel) { - this.fragPlaying = fragPlayingCurrent; - this.hls.trigger(Events.FRAG_CHANGED, { - frag: fragPlayingCurrent - }); - if (!fragPlaying || fragPlaying.level !== fragCurrentLevel) { - this.hls.trigger(Events.LEVEL_SWITCHED, { - level: fragCurrentLevel - }); - } - } - } - } - } - get nextLevel() { - const frag = this.nextBufferedFrag; - if (frag) { - return frag.level; - } - return -1; - } - get currentFrag() { - var _this$media3; - if (this.fragPlaying) { - return this.fragPlaying; - } - const currentTime = ((_this$media3 = this.media) == null ? void 0 : _this$media3.currentTime) || this.lastCurrentTime; - if (isFiniteNumber(currentTime)) { - return this.getAppendedFrag(currentTime); - } - return null; - } - get currentProgramDateTime() { - var _this$media4; - const currentTime = ((_this$media4 = this.media) == null ? void 0 : _this$media4.currentTime) || this.lastCurrentTime; - if (isFiniteNumber(currentTime)) { - const details = this.getLevelDetails(); - const frag = this.currentFrag || (details ? findFragmentByPTS(null, details.fragments, currentTime) : null); - if (frag) { - const programDateTime = frag.programDateTime; - if (programDateTime !== null) { - const epocMs = programDateTime + (currentTime - frag.start) * 1000; - return new Date(epocMs); - } - } - } - return null; - } - get currentLevel() { - const frag = this.currentFrag; - if (frag) { - return frag.level; - } - return -1; - } - get nextBufferedFrag() { - const frag = this.currentFrag; - if (frag) { - return this.followingBufferedFrag(frag); - } - return null; - } - get forceStartLoad() { - return this._forceStartLoad; - } -} - -/* - * compute an Exponential Weighted moving average - * - https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average - * - heavily inspired from shaka-player - */ - -class EWMA { - // About half of the estimated value will be from the last |halfLife| samples by weight. - constructor(halfLife, estimate = 0, weight = 0) { - this.halfLife = void 0; - this.alpha_ = void 0; - this.estimate_ = void 0; - this.totalWeight_ = void 0; - this.halfLife = halfLife; - // Larger values of alpha expire historical data more slowly. - this.alpha_ = halfLife ? Math.exp(Math.log(0.5) / halfLife) : 0; - this.estimate_ = estimate; - this.totalWeight_ = weight; - } - sample(weight, value) { - const adjAlpha = Math.pow(this.alpha_, weight); - this.estimate_ = value * (1 - adjAlpha) + adjAlpha * this.estimate_; - this.totalWeight_ += weight; - } - getTotalWeight() { - return this.totalWeight_; - } - getEstimate() { - if (this.alpha_) { - const zeroFactor = 1 - Math.pow(this.alpha_, this.totalWeight_); - if (zeroFactor) { - return this.estimate_ / zeroFactor; - } - } - return this.estimate_; - } -} - -/* - * EWMA Bandwidth Estimator - * - heavily inspired from shaka-player - * Tracks bandwidth samples and estimates available bandwidth. - * Based on the minimum of two exponentially-weighted moving averages with - * different half-lives. - */ - -class EwmaBandWidthEstimator { - constructor(slow, fast, defaultEstimate, defaultTTFB = 100) { - this.defaultEstimate_ = void 0; - this.minWeight_ = void 0; - this.minDelayMs_ = void 0; - this.slow_ = void 0; - this.fast_ = void 0; - this.defaultTTFB_ = void 0; - this.ttfb_ = void 0; - this.defaultEstimate_ = defaultEstimate; - this.minWeight_ = 0.001; - this.minDelayMs_ = 50; - this.slow_ = new EWMA(slow); - this.fast_ = new EWMA(fast); - this.defaultTTFB_ = defaultTTFB; - this.ttfb_ = new EWMA(slow); - } - update(slow, fast) { - const { - slow_, - fast_, - ttfb_ - } = this; - if (slow_.halfLife !== slow) { - this.slow_ = new EWMA(slow, slow_.getEstimate(), slow_.getTotalWeight()); - } - if (fast_.halfLife !== fast) { - this.fast_ = new EWMA(fast, fast_.getEstimate(), fast_.getTotalWeight()); - } - if (ttfb_.halfLife !== slow) { - this.ttfb_ = new EWMA(slow, ttfb_.getEstimate(), ttfb_.getTotalWeight()); - } - } - sample(durationMs, numBytes) { - durationMs = Math.max(durationMs, this.minDelayMs_); - const numBits = 8 * numBytes; - // weight is duration in seconds - const durationS = durationMs / 1000; - // value is bandwidth in bits/s - const bandwidthInBps = numBits / durationS; - this.fast_.sample(durationS, bandwidthInBps); - this.slow_.sample(durationS, bandwidthInBps); - } - sampleTTFB(ttfb) { - // weight is frequency curve applied to TTFB in seconds - // (longer times have less weight with expected input under 1 second) - const seconds = ttfb / 1000; - const weight = Math.sqrt(2) * Math.exp(-Math.pow(seconds, 2) / 2); - this.ttfb_.sample(weight, Math.max(ttfb, 5)); - } - canEstimate() { - return this.fast_.getTotalWeight() >= this.minWeight_; - } - getEstimate() { - if (this.canEstimate()) { - // console.log('slow estimate:'+ Math.round(this.slow_.getEstimate())); - // console.log('fast estimate:'+ Math.round(this.fast_.getEstimate())); - // Take the minimum of these two estimates. This should have the effect of - // adapting down quickly, but up more slowly. - return Math.min(this.fast_.getEstimate(), this.slow_.getEstimate()); - } else { - return this.defaultEstimate_; - } - } - getEstimateTTFB() { - if (this.ttfb_.getTotalWeight() >= this.minWeight_) { - return this.ttfb_.getEstimate(); - } else { - return this.defaultTTFB_; - } - } - get defaultEstimate() { - return this.defaultEstimate_; - } - destroy() {} -} - -const SUPPORTED_INFO_DEFAULT = { - supported: true, - configurations: [], - decodingInfoResults: [{ - supported: true, - powerEfficient: true, - smooth: true - }] -}; -const SUPPORTED_INFO_CACHE = {}; -function requiresMediaCapabilitiesDecodingInfo(level, audioTracksByGroup, currentVideoRange, currentFrameRate, currentBw, audioPreference) { - // Only test support when configuration is exceeds minimum options - const audioGroups = level.audioCodec ? level.audioGroups : null; - const audioCodecPreference = audioPreference == null ? void 0 : audioPreference.audioCodec; - const channelsPreference = audioPreference == null ? void 0 : audioPreference.channels; - const maxChannels = channelsPreference ? parseInt(channelsPreference) : audioCodecPreference ? Infinity : 2; - let audioChannels = null; - if (audioGroups != null && audioGroups.length) { - try { - if (audioGroups.length === 1 && audioGroups[0]) { - audioChannels = audioTracksByGroup.groups[audioGroups[0]].channels; - } else { - audioChannels = audioGroups.reduce((acc, groupId) => { - if (groupId) { - const audioTrackGroup = audioTracksByGroup.groups[groupId]; - if (!audioTrackGroup) { - throw new Error(`Audio track group ${groupId} not found`); - } - // Sum all channel key values - Object.keys(audioTrackGroup.channels).forEach(key => { - acc[key] = (acc[key] || 0) + audioTrackGroup.channels[key]; - }); - } - return acc; - }, { - 2: 0 - }); - } - } catch (error) { - return true; - } - } - return level.videoCodec !== undefined && (level.width > 1920 && level.height > 1088 || level.height > 1920 && level.width > 1088 || level.frameRate > Math.max(currentFrameRate, 30) || level.videoRange !== 'SDR' && level.videoRange !== currentVideoRange || level.bitrate > Math.max(currentBw, 8e6)) || !!audioChannels && isFiniteNumber(maxChannels) && Object.keys(audioChannels).some(channels => parseInt(channels) > maxChannels); -} -function getMediaDecodingInfoPromise(level, audioTracksByGroup, mediaCapabilities) { - const videoCodecs = level.videoCodec; - const audioCodecs = level.audioCodec; - if (!videoCodecs && !audioCodecs || !mediaCapabilities) { - return Promise.resolve(SUPPORTED_INFO_DEFAULT); - } - const configurations = []; - if (videoCodecs) { - const baseVideoConfiguration = { - width: level.width, - height: level.height, - bitrate: Math.ceil(Math.max(level.bitrate * 0.9, level.averageBitrate)), - // Assume a framerate of 30fps since MediaCapabilities will not accept Level default of 0. - framerate: level.frameRate || 30 - }; - const videoRange = level.videoRange; - if (videoRange !== 'SDR') { - baseVideoConfiguration.transferFunction = videoRange.toLowerCase(); - } - configurations.push.apply(configurations, videoCodecs.split(',').map(videoCodec => ({ - type: 'media-source', - video: _objectSpread2(_objectSpread2({}, baseVideoConfiguration), {}, { - contentType: mimeTypeForCodec(fillInMissingAV01Params(videoCodec), 'video') - }) - }))); - } - if (audioCodecs && level.audioGroups) { - level.audioGroups.forEach(audioGroupId => { - var _audioTracksByGroup$g; - if (!audioGroupId) { - return; - } - (_audioTracksByGroup$g = audioTracksByGroup.groups[audioGroupId]) == null ? void 0 : _audioTracksByGroup$g.tracks.forEach(audioTrack => { - if (audioTrack.groupId === audioGroupId) { - const channels = audioTrack.channels || ''; - const channelsNumber = parseFloat(channels); - if (isFiniteNumber(channelsNumber) && channelsNumber > 2) { - configurations.push.apply(configurations, audioCodecs.split(',').map(audioCodec => ({ - type: 'media-source', - audio: { - contentType: mimeTypeForCodec(audioCodec, 'audio'), - channels: '' + channelsNumber - // spatialRendering: - // audioCodec === 'ec-3' && channels.indexOf('JOC'), - } - }))); - } - } - }); - }); - } - return Promise.all(configurations.map(configuration => { - // Cache MediaCapabilities promises - const decodingInfoKey = getMediaDecodingInfoKey(configuration); - return SUPPORTED_INFO_CACHE[decodingInfoKey] || (SUPPORTED_INFO_CACHE[decodingInfoKey] = mediaCapabilities.decodingInfo(configuration)); - })).then(decodingInfoResults => ({ - supported: !decodingInfoResults.some(info => !info.supported), - configurations, - decodingInfoResults - })).catch(error => ({ - supported: false, - configurations, - decodingInfoResults: [], - error - })); -} -function getMediaDecodingInfoKey(config) { - const { - audio, - video - } = config; - const mediaConfig = video || audio; - if (mediaConfig) { - const codec = mediaConfig.contentType.split('"')[1]; - if (video) { - return `r${video.height}x${video.width}f${Math.ceil(video.framerate)}${video.transferFunction || 'sd'}_${codec}_${Math.ceil(video.bitrate / 1e5)}`; - } - if (audio) { - return `c${audio.channels}${audio.spatialRendering ? 's' : 'n'}_${codec}`; - } - } - return ''; -} - -/** - * @returns Whether we can detect and validate HDR capability within the window context - */ -function isHdrSupported() { - if (typeof matchMedia === 'function') { - const mediaQueryList = matchMedia('(dynamic-range: high)'); - const badQuery = matchMedia('bad query'); - if (mediaQueryList.media !== badQuery.media) { - return mediaQueryList.matches === true; - } - } - return false; -} - -/** - * Sanitizes inputs to return the active video selection options for HDR/SDR. - * When both inputs are null: - * - * `{ preferHDR: false, allowedVideoRanges: [] }` - * - * When `currentVideoRange` non-null, maintain the active range: - * - * `{ preferHDR: currentVideoRange !== 'SDR', allowedVideoRanges: [currentVideoRange] }` - * - * When VideoSelectionOption non-null: - * - * - Allow all video ranges if `allowedVideoRanges` unspecified. - * - If `preferHDR` is non-null use the value to filter `allowedVideoRanges`. - * - Else check window for HDR support and set `preferHDR` to the result. - * - * @param currentVideoRange - * @param videoPreference - */ -function getVideoSelectionOptions(currentVideoRange, videoPreference) { - let preferHDR = false; - let allowedVideoRanges = []; - if (currentVideoRange) { - preferHDR = currentVideoRange !== 'SDR'; - allowedVideoRanges = [currentVideoRange]; - } - if (videoPreference) { - allowedVideoRanges = videoPreference.allowedVideoRanges || VideoRangeValues.slice(0); - const allowAutoPreferHDR = allowedVideoRanges.join('') !== 'SDR' && !videoPreference.videoCodec; - preferHDR = videoPreference.preferHDR !== undefined ? videoPreference.preferHDR : allowAutoPreferHDR && isHdrSupported(); - if (!preferHDR) { - allowedVideoRanges = ['SDR']; - } - } - return { - preferHDR, - allowedVideoRanges - }; -} - -function getStartCodecTier(codecTiers, currentVideoRange, currentBw, audioPreference, videoPreference) { - const codecSets = Object.keys(codecTiers); - const channelsPreference = audioPreference == null ? void 0 : audioPreference.channels; - const audioCodecPreference = audioPreference == null ? void 0 : audioPreference.audioCodec; - const videoCodecPreference = videoPreference == null ? void 0 : videoPreference.videoCodec; - const preferStereo = channelsPreference && parseInt(channelsPreference) === 2; - // Use first level set to determine stereo, and minimum resolution and framerate - let hasStereo = false; - let hasCurrentVideoRange = false; - let minHeight = Infinity; - let minFramerate = Infinity; - let minBitrate = Infinity; - let minIndex = Infinity; - let selectedScore = 0; - let videoRanges = []; - const { - preferHDR, - allowedVideoRanges - } = getVideoSelectionOptions(currentVideoRange, videoPreference); - for (let i = codecSets.length; i--;) { - const tier = codecTiers[codecSets[i]]; - hasStereo || (hasStereo = tier.channels[2] > 0); - minHeight = Math.min(minHeight, tier.minHeight); - minFramerate = Math.min(minFramerate, tier.minFramerate); - minBitrate = Math.min(minBitrate, tier.minBitrate); - const matchingVideoRanges = allowedVideoRanges.filter(range => tier.videoRanges[range] > 0); - if (matchingVideoRanges.length > 0) { - hasCurrentVideoRange = true; - } - } - minHeight = isFiniteNumber(minHeight) ? minHeight : 0; - minFramerate = isFiniteNumber(minFramerate) ? minFramerate : 0; - const maxHeight = Math.max(1080, minHeight); - const maxFramerate = Math.max(30, minFramerate); - minBitrate = isFiniteNumber(minBitrate) ? minBitrate : currentBw; - currentBw = Math.max(minBitrate, currentBw); - // If there are no variants with matching preference, set currentVideoRange to undefined - if (!hasCurrentVideoRange) { - currentVideoRange = undefined; - } - const hasMultipleSets = codecSets.length > 1; - const codecSet = codecSets.reduce((selected, candidate) => { - // Remove candiates which do not meet bitrate, default audio, stereo or channels preference, 1080p or lower, 30fps or lower, or SDR/HDR selection if present - const candidateTier = codecTiers[candidate]; - if (candidate === selected) { - return selected; - } - videoRanges = hasCurrentVideoRange ? allowedVideoRanges.filter(range => candidateTier.videoRanges[range] > 0) : []; - if (hasMultipleSets) { - if (candidateTier.minBitrate > currentBw) { - logStartCodecCandidateIgnored(candidate, `min bitrate of ${candidateTier.minBitrate} > current estimate of ${currentBw}`); - return selected; - } - if (!candidateTier.hasDefaultAudio) { - logStartCodecCandidateIgnored(candidate, `no renditions with default or auto-select sound found`); - return selected; - } - if (audioCodecPreference && candidate.indexOf(audioCodecPreference.substring(0, 4)) % 5 !== 0) { - logStartCodecCandidateIgnored(candidate, `audio codec preference "${audioCodecPreference}" not found`); - return selected; - } - if (channelsPreference && !preferStereo) { - if (!candidateTier.channels[channelsPreference]) { - logStartCodecCandidateIgnored(candidate, `no renditions with ${channelsPreference} channel sound found (channels options: ${Object.keys(candidateTier.channels)})`); - return selected; - } - } else if ((!audioCodecPreference || preferStereo) && hasStereo && candidateTier.channels['2'] === 0) { - logStartCodecCandidateIgnored(candidate, `no renditions with stereo sound found`); - return selected; - } - if (candidateTier.minHeight > maxHeight) { - logStartCodecCandidateIgnored(candidate, `min resolution of ${candidateTier.minHeight} > maximum of ${maxHeight}`); - return selected; - } - if (candidateTier.minFramerate > maxFramerate) { - logStartCodecCandidateIgnored(candidate, `min framerate of ${candidateTier.minFramerate} > maximum of ${maxFramerate}`); - return selected; - } - if (!videoRanges.some(range => candidateTier.videoRanges[range] > 0)) { - logStartCodecCandidateIgnored(candidate, `no variants with VIDEO-RANGE of ${JSON.stringify(videoRanges)} found`); - return selected; - } - if (videoCodecPreference && candidate.indexOf(videoCodecPreference.substring(0, 4)) % 5 !== 0) { - logStartCodecCandidateIgnored(candidate, `video codec preference "${videoCodecPreference}" not found`); - return selected; - } - if (candidateTier.maxScore < selectedScore) { - logStartCodecCandidateIgnored(candidate, `max score of ${candidateTier.maxScore} < selected max of ${selectedScore}`); - return selected; - } - } - // Remove candiates with less preferred codecs or more errors - if (selected && (codecsSetSelectionPreferenceValue(candidate) >= codecsSetSelectionPreferenceValue(selected) || candidateTier.fragmentError > codecTiers[selected].fragmentError)) { - return selected; - } - minIndex = candidateTier.minIndex; - selectedScore = candidateTier.maxScore; - return candidate; - }, undefined); - return { - codecSet, - videoRanges, - preferHDR, - minFramerate, - minBitrate, - minIndex - }; -} -function logStartCodecCandidateIgnored(codeSet, reason) { - logger.log(`[abr] start candidates with "${codeSet}" ignored because ${reason}`); -} -function getAudioTracksByGroup(allAudioTracks) { - return allAudioTracks.reduce((audioTracksByGroup, track) => { - let trackGroup = audioTracksByGroup.groups[track.groupId]; - if (!trackGroup) { - trackGroup = audioTracksByGroup.groups[track.groupId] = { - tracks: [], - channels: { - 2: 0 - }, - hasDefault: false, - hasAutoSelect: false - }; - } - trackGroup.tracks.push(track); - const channelsKey = track.channels || '2'; - trackGroup.channels[channelsKey] = (trackGroup.channels[channelsKey] || 0) + 1; - trackGroup.hasDefault = trackGroup.hasDefault || track.default; - trackGroup.hasAutoSelect = trackGroup.hasAutoSelect || track.autoselect; - if (trackGroup.hasDefault) { - audioTracksByGroup.hasDefaultAudio = true; - } - if (trackGroup.hasAutoSelect) { - audioTracksByGroup.hasAutoSelectAudio = true; - } - return audioTracksByGroup; - }, { - hasDefaultAudio: false, - hasAutoSelectAudio: false, - groups: {} - }); -} -function getCodecTiers(levels, audioTracksByGroup, minAutoLevel, maxAutoLevel) { - return levels.slice(minAutoLevel, maxAutoLevel + 1).reduce((tiers, level, index) => { - if (!level.codecSet) { - return tiers; - } - const audioGroups = level.audioGroups; - let tier = tiers[level.codecSet]; - if (!tier) { - tiers[level.codecSet] = tier = { - minBitrate: Infinity, - minHeight: Infinity, - minFramerate: Infinity, - minIndex: index, - maxScore: 0, - videoRanges: { - SDR: 0 - }, - channels: { - '2': 0 - }, - hasDefaultAudio: !audioGroups, - fragmentError: 0 - }; - } - tier.minBitrate = Math.min(tier.minBitrate, level.bitrate); - const lesserWidthOrHeight = Math.min(level.height, level.width); - tier.minHeight = Math.min(tier.minHeight, lesserWidthOrHeight); - tier.minFramerate = Math.min(tier.minFramerate, level.frameRate); - tier.minIndex = Math.min(tier.minIndex, index); - tier.maxScore = Math.max(tier.maxScore, level.score); - tier.fragmentError += level.fragmentError; - tier.videoRanges[level.videoRange] = (tier.videoRanges[level.videoRange] || 0) + 1; - if (audioGroups) { - audioGroups.forEach(audioGroupId => { - if (!audioGroupId) { - return; - } - const audioGroup = audioTracksByGroup.groups[audioGroupId]; - if (!audioGroup) { - return; - } - // Default audio is any group with DEFAULT=YES, or if missing then any group with AUTOSELECT=YES, or all variants - tier.hasDefaultAudio = tier.hasDefaultAudio || audioTracksByGroup.hasDefaultAudio ? audioGroup.hasDefault : audioGroup.hasAutoSelect || !audioTracksByGroup.hasDefaultAudio && !audioTracksByGroup.hasAutoSelectAudio; - Object.keys(audioGroup.channels).forEach(channels => { - tier.channels[channels] = (tier.channels[channels] || 0) + audioGroup.channels[channels]; - }); - }); - } - return tiers; - }, {}); -} -function getBasicSelectionOption(option) { - if (!option) { - return option; - } - const { - lang, - assocLang, - characteristics, - channels, - audioCodec - } = option; - return { - lang, - assocLang, - characteristics, - channels, - audioCodec - }; -} -function findMatchingOption(option, tracks, matchPredicate) { - if ('attrs' in option) { - const index = tracks.indexOf(option); - if (index !== -1) { - return index; - } - } - for (let i = 0; i < tracks.length; i++) { - const track = tracks[i]; - if (matchesOption(option, track, matchPredicate)) { - return i; - } - } - return -1; -} -function matchesOption(option, track, matchPredicate) { - const { - groupId, - name, - lang, - assocLang, - characteristics, - default: isDefault - } = option; - const forced = option.forced; - return (groupId === undefined || track.groupId === groupId) && (name === undefined || track.name === name) && (lang === undefined || languagesMatch(lang, track.lang)) && (lang === undefined || track.assocLang === assocLang) && (isDefault === undefined || track.default === isDefault) && (forced === undefined || track.forced === forced) && (characteristics === undefined || characteristicsMatch(characteristics, track.characteristics)) && (matchPredicate === undefined || matchPredicate(option, track)); -} -function languagesMatch(languageA, languageB = '--') { - if (languageA.length === languageB.length) { - return languageA === languageB; - } - return languageA.startsWith(languageB) || languageB.startsWith(languageA); -} -function characteristicsMatch(characteristicsA, characteristicsB = '') { - const arrA = characteristicsA.split(','); - const arrB = characteristicsB.split(','); - // Expects each item to be unique: - return arrA.length === arrB.length && !arrA.some(el => arrB.indexOf(el) === -1); -} -function audioMatchPredicate(option, track) { - const { - audioCodec, - channels - } = option; - return (audioCodec === undefined || (track.audioCodec || '').substring(0, 4) === audioCodec.substring(0, 4)) && (channels === undefined || channels === (track.channels || '2')); -} -function findClosestLevelWithAudioGroup(option, levels, allAudioTracks, searchIndex, matchPredicate) { - const currentLevel = levels[searchIndex]; - // Are there variants with same URI as current level? - // If so, find a match that does not require any level URI change - const variants = levels.reduce((variantMap, level, index) => { - const uri = level.uri; - const renditions = variantMap[uri] || (variantMap[uri] = []); - renditions.push(index); - return variantMap; - }, {}); - const renditions = variants[currentLevel.uri]; - if (renditions.length > 1) { - searchIndex = Math.max.apply(Math, renditions); - } - // Find best match - const currentVideoRange = currentLevel.videoRange; - const currentFrameRate = currentLevel.frameRate; - const currentVideoCodec = currentLevel.codecSet.substring(0, 4); - const matchingVideo = searchDownAndUpList(levels, searchIndex, level => { - if (level.videoRange !== currentVideoRange || level.frameRate !== currentFrameRate || level.codecSet.substring(0, 4) !== currentVideoCodec) { - return false; - } - const audioGroups = level.audioGroups; - const tracks = allAudioTracks.filter(track => !audioGroups || audioGroups.indexOf(track.groupId) !== -1); - return findMatchingOption(option, tracks, matchPredicate) > -1; - }); - if (matchingVideo > -1) { - return matchingVideo; - } - return searchDownAndUpList(levels, searchIndex, level => { - const audioGroups = level.audioGroups; - const tracks = allAudioTracks.filter(track => !audioGroups || audioGroups.indexOf(track.groupId) !== -1); - return findMatchingOption(option, tracks, matchPredicate) > -1; - }); -} -function searchDownAndUpList(arr, searchIndex, predicate) { - for (let i = searchIndex; i; i--) { - if (predicate(arr[i])) { - return i; - } - } - for (let i = searchIndex + 1; i < arr.length; i++) { - if (predicate(arr[i])) { - return i; - } - } - return -1; -} - -class AbrController extends Logger { - constructor(_hls) { - super('abr', _hls.logger); - this.hls = void 0; - this.lastLevelLoadSec = 0; - this.lastLoadedFragLevel = -1; - this.firstSelection = -1; - this._nextAutoLevel = -1; - this.nextAutoLevelKey = ''; - this.audioTracksByGroup = null; - this.codecTiers = null; - this.timer = -1; - this.fragCurrent = null; - this.partCurrent = null; - this.bitrateTestDelay = 0; - this.bwEstimator = void 0; - /* - This method monitors the download rate of the current fragment, and will downswitch if that fragment will not load - quickly enough to prevent underbuffering - */ - this._abandonRulesCheck = () => { - const { - fragCurrent: frag, - partCurrent: part, - hls - } = this; - const { - autoLevelEnabled, - media - } = hls; - if (!frag || !media) { - return; - } - const now = performance.now(); - const stats = part ? part.stats : frag.stats; - const duration = part ? part.duration : frag.duration; - const timeLoading = now - stats.loading.start; - const minAutoLevel = hls.minAutoLevel; - // 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 <= minAutoLevel) { - this.clearTimer(); - // reset forced auto level value so that next level will be selected - this._nextAutoLevel = -1; - return; - } - - // This check only runs if we're in ABR mode and actually playing - if (!autoLevelEnabled || media.paused || !media.playbackRate || !media.readyState) { - return; - } - const bufferInfo = hls.mainForwardBufferInfo; - if (bufferInfo === null) { - return; - } - const ttfbEstimate = this.bwEstimator.getEstimateTTFB(); - const playbackRate = Math.abs(media.playbackRate); - // To maintain stable adaptive playback, only begin monitoring frag loading after half or more of its playback duration has passed - if (timeLoading <= Math.max(ttfbEstimate, 1000 * (duration / (playbackRate * 2)))) { - return; - } - - // bufferStarvationDelay is an estimate of the amount time (in seconds) it will take to exhaust the buffer - const bufferStarvationDelay = bufferInfo.len / playbackRate; - const ttfb = stats.loading.first ? stats.loading.first - stats.loading.start : -1; - const loadedFirstByte = stats.loaded && ttfb > -1; - const bwEstimate = this.getBwEstimate(); - const levels = hls.levels; - const level = levels[frag.level]; - const expectedLen = stats.total || Math.max(stats.loaded, Math.round(duration * level.averageBitrate / 8)); - let timeStreaming = loadedFirstByte ? timeLoading - ttfb : timeLoading; - if (timeStreaming < 1 && loadedFirstByte) { - timeStreaming = Math.min(timeLoading, stats.loaded * 8 / bwEstimate); - } - const loadRate = loadedFirstByte ? stats.loaded * 1000 / timeStreaming : 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 + ttfbEstimate / 1000; - // Only downswitch if the time to finish loading the current fragment is greater than the amount of buffer left - if (fragLoadedDelay <= bufferStarvationDelay) { - return; - } - const bwe = loadRate ? loadRate * 8 : bwEstimate; - let fragLevelNextLoadedDelay = Number.POSITIVE_INFINITY; - let nextLoadLevel; - // Iterate through lower level and try to find the largest one that avoids rebuffering - for (nextLoadLevel = frag.level - 1; nextLoadLevel > minAutoLevel; nextLoadLevel--) { - // compute time to load next fragment at lower level - // 8 = bits per byte (bps/Bps) - const levelNextBitrate = levels[nextLoadLevel].maxBitrate; - fragLevelNextLoadedDelay = this.getTimeToLoadFrag(ttfbEstimate / 1000, bwe, duration * levelNextBitrate, !levels[nextLoadLevel].details); - if (fragLevelNextLoadedDelay < bufferStarvationDelay) { - break; - } - } - // Only emergency switch down if it takes less time to load a new fragment at lowest level instead of continuing - // to load the current one - if (fragLevelNextLoadedDelay >= fragLoadedDelay) { - return; - } - - // if estimated load time of new segment is completely unreasonable, ignore and do not emergency switch down - if (fragLevelNextLoadedDelay > duration * 10) { - return; - } - hls.nextLoadLevel = hls.nextAutoLevel = nextLoadLevel; - if (loadedFirstByte) { - // If there has been loading progress, sample bandwidth using loading time offset by minimum TTFB time - this.bwEstimator.sample(timeLoading - Math.min(ttfbEstimate, ttfb), stats.loaded); - } else { - // If there has been no loading progress, sample TTFB - this.bwEstimator.sampleTTFB(timeLoading); - } - const nextLoadLevelBitrate = levels[nextLoadLevel].maxBitrate; - if (this.getBwEstimate() * this.hls.config.abrBandWidthUpFactor > nextLoadLevelBitrate) { - this.resetEstimator(nextLoadLevelBitrate); - } - this.clearTimer(); - this.warn(`Fragment ${frag.sn}${part ? ' part ' + part.index : ''} of level ${frag.level} is loading too slowly; - Time to underbuffer: ${bufferStarvationDelay.toFixed(3)} s - Estimated load time for current fragment: ${fragLoadedDelay.toFixed(3)} s - Estimated load time for down switch fragment: ${fragLevelNextLoadedDelay.toFixed(3)} s - TTFB estimate: ${ttfb | 0} ms - Current BW estimate: ${isFiniteNumber(bwEstimate) ? bwEstimate | 0 : 'Unknown'} bps - New BW estimate: ${this.getBwEstimate() | 0} bps - Switching to level ${nextLoadLevel} @ ${nextLoadLevelBitrate | 0} bps`); - hls.trigger(Events.FRAG_LOAD_EMERGENCY_ABORTED, { - frag, - part, - stats - }); - }; - this.hls = _hls; - this.bwEstimator = this.initEstimator(); - this.registerListeners(); - } - resetEstimator(abrEwmaDefaultEstimate) { - if (abrEwmaDefaultEstimate) { - this.log(`setting initial bwe to ${abrEwmaDefaultEstimate}`); - this.hls.config.abrEwmaDefaultEstimate = abrEwmaDefaultEstimate; - } - this.firstSelection = -1; - this.bwEstimator = this.initEstimator(); - } - initEstimator() { - const config = this.hls.config; - return new EwmaBandWidthEstimator(config.abrEwmaSlowVoD, config.abrEwmaFastVoD, config.abrEwmaDefaultEstimate); - } - registerListeners() { - const { - hls - } = this; - hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); - hls.on(Events.FRAG_LOADING, this.onFragLoading, this); - hls.on(Events.FRAG_LOADED, this.onFragLoaded, this); - hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this); - hls.on(Events.LEVEL_SWITCHING, this.onLevelSwitching, this); - hls.on(Events.LEVEL_LOADED, this.onLevelLoaded, this); - hls.on(Events.LEVELS_UPDATED, this.onLevelsUpdated, this); - hls.on(Events.MAX_AUTO_LEVEL_UPDATED, this.onMaxAutoLevelUpdated, this); - hls.on(Events.ERROR, this.onError, this); - } - unregisterListeners() { - const { - hls - } = this; - if (!hls) { - return; - } - hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); - hls.off(Events.FRAG_LOADING, this.onFragLoading, this); - hls.off(Events.FRAG_LOADED, this.onFragLoaded, this); - hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this); - hls.off(Events.LEVEL_SWITCHING, this.onLevelSwitching, this); - hls.off(Events.LEVEL_LOADED, this.onLevelLoaded, this); - hls.off(Events.LEVELS_UPDATED, this.onLevelsUpdated, this); - hls.off(Events.MAX_AUTO_LEVEL_UPDATED, this.onMaxAutoLevelUpdated, this); - hls.off(Events.ERROR, this.onError, this); - } - destroy() { - this.unregisterListeners(); - this.clearTimer(); - // @ts-ignore - this.hls = this._abandonRulesCheck = null; - this.fragCurrent = this.partCurrent = null; - } - onManifestLoading(event, data) { - this.lastLoadedFragLevel = -1; - this.firstSelection = -1; - this.lastLevelLoadSec = 0; - this.fragCurrent = this.partCurrent = null; - this.onLevelsUpdated(); - this.clearTimer(); - } - onLevelsUpdated() { - if (this.lastLoadedFragLevel > -1 && this.fragCurrent) { - this.lastLoadedFragLevel = this.fragCurrent.level; - } - this._nextAutoLevel = -1; - this.onMaxAutoLevelUpdated(); - this.codecTiers = null; - this.audioTracksByGroup = null; - } - onMaxAutoLevelUpdated() { - this.firstSelection = -1; - this.nextAutoLevelKey = ''; - } - onFragLoading(event, data) { - const frag = data.frag; - if (this.ignoreFragment(frag)) { - return; - } - if (!frag.bitrateTest) { - var _data$part; - this.fragCurrent = frag; - this.partCurrent = (_data$part = data.part) != null ? _data$part : null; - } - this.clearTimer(); - this.timer = self.setInterval(this._abandonRulesCheck, 100); - } - onLevelSwitching(event, data) { - this.clearTimer(); - } - onError(event, data) { - if (data.fatal) { - return; - } - switch (data.details) { - case ErrorDetails.BUFFER_ADD_CODEC_ERROR: - case ErrorDetails.BUFFER_APPEND_ERROR: - // Reset last loaded level so that a new selection can be made after calling recoverMediaError - this.lastLoadedFragLevel = -1; - this.firstSelection = -1; - break; - case ErrorDetails.FRAG_LOAD_TIMEOUT: - { - const frag = data.frag; - const { - fragCurrent, - partCurrent: part - } = this; - if (frag && fragCurrent && frag.sn === fragCurrent.sn && frag.level === fragCurrent.level) { - const now = performance.now(); - const stats = part ? part.stats : frag.stats; - const timeLoading = now - stats.loading.start; - const ttfb = stats.loading.first ? stats.loading.first - stats.loading.start : -1; - const loadedFirstByte = stats.loaded && ttfb > -1; - if (loadedFirstByte) { - const ttfbEstimate = this.bwEstimator.getEstimateTTFB(); - this.bwEstimator.sample(timeLoading - Math.min(ttfbEstimate, ttfb), stats.loaded); - } else { - this.bwEstimator.sampleTTFB(timeLoading); - } - } - break; - } - } - } - getTimeToLoadFrag(timeToFirstByteSec, bandwidth, fragSizeBits, isSwitch) { - const fragLoadSec = timeToFirstByteSec + fragSizeBits / bandwidth; - const playlistLoadSec = isSwitch ? this.lastLevelLoadSec : 0; - return fragLoadSec + playlistLoadSec; - } - onLevelLoaded(event, data) { - const config = this.hls.config; - const { - loading - } = data.stats; - const timeLoadingMs = loading.end - loading.start; - if (isFiniteNumber(timeLoadingMs)) { - this.lastLevelLoadSec = timeLoadingMs / 1000; - } - if (data.details.live) { - this.bwEstimator.update(config.abrEwmaSlowLive, config.abrEwmaFastLive); - } else { - this.bwEstimator.update(config.abrEwmaSlowVoD, config.abrEwmaFastVoD); - } - } - onFragLoaded(event, { - frag, - part - }) { - const stats = part ? part.stats : frag.stats; - if (frag.type === PlaylistLevelType.MAIN) { - this.bwEstimator.sampleTTFB(stats.loading.first - stats.loading.start); - } - if (this.ignoreFragment(frag)) { - return; - } - // stop monitoring bw once frag loaded - this.clearTimer(); - // reset forced auto level value so that next level will be selected - if (frag.level === this._nextAutoLevel) { - this._nextAutoLevel = -1; - } - this.firstSelection = -1; - - // compute level average bitrate - if (this.hls.config.abrMaxWithRealBitrate) { - const duration = part ? part.duration : frag.duration; - const level = this.hls.levels[frag.level]; - const loadedBytes = (level.loaded ? level.loaded.bytes : 0) + stats.loaded; - const loadedDuration = (level.loaded ? level.loaded.duration : 0) + duration; - level.loaded = { - bytes: loadedBytes, - duration: loadedDuration - }; - level.realBitrate = Math.round(8 * loadedBytes / loadedDuration); - } - if (frag.bitrateTest) { - const fragBufferedData = { - stats, - frag, - part, - id: frag.type - }; - this.onFragBuffered(Events.FRAG_BUFFERED, fragBufferedData); - frag.bitrateTest = false; - } else { - // store level id after successful fragment load for playback - this.lastLoadedFragLevel = frag.level; - } - } - onFragBuffered(event, data) { - const { - frag, - part - } = data; - const stats = part != null && part.stats.loaded ? part.stats : frag.stats; - if (stats.aborted) { - return; - } - if (this.ignoreFragment(frag)) { - return; - } - // Use the difference between parsing and request instead of buffering and request to compute fragLoadingProcessing; - // rationale is that buffer appending only happens once media is attached. This can happen when config.startFragPrefetch - // is used. If we used buffering in that case, our BW estimate sample will be very large. - const processingMs = stats.parsing.end - stats.loading.start - Math.min(stats.loading.first - stats.loading.start, this.bwEstimator.getEstimateTTFB()); - this.bwEstimator.sample(processingMs, stats.loaded); - stats.bwEstimate = this.getBwEstimate(); - if (frag.bitrateTest) { - this.bitrateTestDelay = processingMs / 1000; - } else { - this.bitrateTestDelay = 0; - } - } - ignoreFragment(frag) { - // Only count non-alt-audio frags which were actually buffered in our BW calculations - return frag.type !== PlaylistLevelType.MAIN || frag.sn === 'initSegment'; - } - clearTimer() { - if (this.timer > -1) { - self.clearInterval(this.timer); - this.timer = -1; - } - } - get firstAutoLevel() { - const { - maxAutoLevel, - minAutoLevel - } = this.hls; - const bwEstimate = this.getBwEstimate(); - const maxStartDelay = this.hls.config.maxStarvationDelay; - const abrAutoLevel = this.findBestLevel(bwEstimate, minAutoLevel, maxAutoLevel, 0, maxStartDelay, 1, 1); - if (abrAutoLevel > -1) { - return abrAutoLevel; - } - const firstLevel = this.hls.firstLevel; - const clamped = Math.min(Math.max(firstLevel, minAutoLevel), maxAutoLevel); - this.warn(`Could not find best starting auto level. Defaulting to first in playlist ${firstLevel} clamped to ${clamped}`); - return clamped; - } - get forcedAutoLevel() { - if (this.nextAutoLevelKey) { - return -1; - } - return this._nextAutoLevel; - } - - // return next auto level - get nextAutoLevel() { - const forcedAutoLevel = this.forcedAutoLevel; - const bwEstimator = this.bwEstimator; - const useEstimate = bwEstimator.canEstimate(); - const loadedFirstFrag = this.lastLoadedFragLevel > -1; - // in case next auto level has been forced, and bw not available or not reliable, return forced value - if (forcedAutoLevel !== -1 && (!useEstimate || !loadedFirstFrag || this.nextAutoLevelKey === this.getAutoLevelKey())) { - return forcedAutoLevel; - } - - // compute next level using ABR logic - const nextABRAutoLevel = useEstimate && loadedFirstFrag ? this.getNextABRAutoLevel() : this.firstAutoLevel; - - // use forced auto level while it hasn't errored more than ABR selection - if (forcedAutoLevel !== -1) { - const levels = this.hls.levels; - if (levels.length > Math.max(forcedAutoLevel, nextABRAutoLevel) && levels[forcedAutoLevel].loadError <= levels[nextABRAutoLevel].loadError) { - return forcedAutoLevel; - } - } - - // save result until state has changed - this._nextAutoLevel = nextABRAutoLevel; - this.nextAutoLevelKey = this.getAutoLevelKey(); - return nextABRAutoLevel; - } - getAutoLevelKey() { - return `${this.getBwEstimate()}_${this.getStarvationDelay().toFixed(2)}`; - } - getNextABRAutoLevel() { - const { - fragCurrent, - partCurrent, - hls - } = this; - if (hls.levels.length <= 1) { - return hls.loadLevel; - } - const { - maxAutoLevel, - config, - minAutoLevel - } = hls; - const currentFragDuration = partCurrent ? partCurrent.duration : fragCurrent ? fragCurrent.duration : 0; - const avgbw = this.getBwEstimate(); - // bufferStarvationDelay is the wall-clock time left until the playback buffer is exhausted. - const bufferStarvationDelay = this.getStarvationDelay(); - let bwFactor = config.abrBandWidthFactor; - let bwUpFactor = config.abrBandWidthUpFactor; - - // First, look to see if we can find a level matching with our avg bandwidth AND that could also guarantee no rebuffering at all - if (bufferStarvationDelay) { - const _bestLevel = this.findBestLevel(avgbw, minAutoLevel, maxAutoLevel, bufferStarvationDelay, 0, bwFactor, bwUpFactor); - if (_bestLevel >= 0) { - return _bestLevel; - } - } - // not possible to get rid of rebuffering... try to find level that will guarantee less than maxStarvationDelay of rebuffering - let maxStarvationDelay = currentFragDuration ? Math.min(currentFragDuration, config.maxStarvationDelay) : config.maxStarvationDelay; - if (!bufferStarvationDelay) { - // in case buffer is empty, let's check if previous fragment was loaded to perform a bitrate test - const bitrateTestDelay = this.bitrateTestDelay; - if (bitrateTestDelay) { - // if it is the case, then we need to adjust our max starvation delay using maxLoadingDelay config value - // max video loading delay used in automatic start level selection : - // in that mode ABR controller will ensure that video loading time (ie the time to fetch the first fragment at lowest quality level + - // the time to fetch the fragment at the appropriate quality level is less than ```maxLoadingDelay``` ) - // cap maxLoadingDelay and ensure it is not bigger 'than bitrate test' frag duration - const maxLoadingDelay = currentFragDuration ? Math.min(currentFragDuration, config.maxLoadingDelay) : config.maxLoadingDelay; - maxStarvationDelay = maxLoadingDelay - bitrateTestDelay; - this.info(`bitrate test took ${Math.round(1000 * bitrateTestDelay)}ms, set first fragment max fetchDuration to ${Math.round(1000 * maxStarvationDelay)} ms`); - // don't use conservative factor on bitrate test - bwFactor = bwUpFactor = 1; - } - } - const bestLevel = this.findBestLevel(avgbw, minAutoLevel, maxAutoLevel, bufferStarvationDelay, maxStarvationDelay, bwFactor, bwUpFactor); - this.info(`${bufferStarvationDelay ? 'rebuffering expected' : 'buffer is empty'}, optimal quality level ${bestLevel}`); - if (bestLevel > -1) { - return bestLevel; - } - // If no matching level found, see if min auto level would be a better option - const minLevel = hls.levels[minAutoLevel]; - const autoLevel = hls.levels[hls.loadLevel]; - if ((minLevel == null ? void 0 : minLevel.bitrate) < (autoLevel == null ? void 0 : autoLevel.bitrate)) { - return minAutoLevel; - } - // or if bitrate is not lower, continue to use loadLevel - return hls.loadLevel; - } - getStarvationDelay() { - const hls = this.hls; - const media = hls.media; - if (!media) { - return Infinity; - } - // 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. - const playbackRate = media && media.playbackRate !== 0 ? Math.abs(media.playbackRate) : 1.0; - const bufferInfo = hls.mainForwardBufferInfo; - return (bufferInfo ? bufferInfo.len : 0) / playbackRate; - } - getBwEstimate() { - return this.bwEstimator.canEstimate() ? this.bwEstimator.getEstimate() : this.hls.config.abrEwmaDefaultEstimate; - } - findBestLevel(currentBw, minAutoLevel, maxAutoLevel, bufferStarvationDelay, maxStarvationDelay, bwFactor, bwUpFactor) { - var _level$details; - const maxFetchDuration = bufferStarvationDelay + maxStarvationDelay; - const lastLoadedFragLevel = this.lastLoadedFragLevel; - const selectionBaseLevel = lastLoadedFragLevel === -1 ? this.hls.firstLevel : lastLoadedFragLevel; - const { - fragCurrent, - partCurrent - } = this; - const { - levels, - allAudioTracks, - loadLevel, - config - } = this.hls; - if (levels.length === 1) { - return 0; - } - const level = levels[selectionBaseLevel]; - const live = !!(level != null && (_level$details = level.details) != null && _level$details.live); - const firstSelection = loadLevel === -1 || lastLoadedFragLevel === -1; - let currentCodecSet; - let currentVideoRange = 'SDR'; - let currentFrameRate = (level == null ? void 0 : level.frameRate) || 0; - const { - audioPreference, - videoPreference - } = config; - const audioTracksByGroup = this.audioTracksByGroup || (this.audioTracksByGroup = getAudioTracksByGroup(allAudioTracks)); - let minStartIndex = -1; - if (firstSelection) { - if (this.firstSelection !== -1) { - return this.firstSelection; - } - const codecTiers = this.codecTiers || (this.codecTiers = getCodecTiers(levels, audioTracksByGroup, minAutoLevel, maxAutoLevel)); - const startTier = getStartCodecTier(codecTiers, currentVideoRange, currentBw, audioPreference, videoPreference); - const { - codecSet, - videoRanges, - minFramerate, - minBitrate, - minIndex, - preferHDR - } = startTier; - minStartIndex = minIndex; - currentCodecSet = codecSet; - currentVideoRange = preferHDR ? videoRanges[videoRanges.length - 1] : videoRanges[0]; - currentFrameRate = minFramerate; - currentBw = Math.max(currentBw, minBitrate); - this.log(`picked start tier ${JSON.stringify(startTier)}`); - } else { - currentCodecSet = level == null ? void 0 : level.codecSet; - currentVideoRange = level == null ? void 0 : level.videoRange; - } - const currentFragDuration = partCurrent ? partCurrent.duration : fragCurrent ? fragCurrent.duration : 0; - const ttfbEstimateSec = this.bwEstimator.getEstimateTTFB() / 1000; - const levelsSkipped = []; - for (let i = maxAutoLevel; i >= minAutoLevel; i--) { - var _levelInfo$supportedR; - const levelInfo = levels[i]; - const upSwitch = i > selectionBaseLevel; - if (!levelInfo) { - continue; - } - if (config.useMediaCapabilities && !levelInfo.supportedResult && !levelInfo.supportedPromise) { - const mediaCapabilities = navigator.mediaCapabilities; - if (typeof (mediaCapabilities == null ? void 0 : mediaCapabilities.decodingInfo) === 'function' && requiresMediaCapabilitiesDecodingInfo(levelInfo, audioTracksByGroup, currentVideoRange, currentFrameRate, currentBw, audioPreference)) { - levelInfo.supportedPromise = getMediaDecodingInfoPromise(levelInfo, audioTracksByGroup, mediaCapabilities); - levelInfo.supportedPromise.then(decodingInfo => { - if (!this.hls) { - return; - } - levelInfo.supportedResult = decodingInfo; - const levels = this.hls.levels; - const index = levels.indexOf(levelInfo); - if (decodingInfo.error) { - this.warn(`MediaCapabilities decodingInfo error: "${decodingInfo.error}" for level ${index} ${JSON.stringify(decodingInfo)}`); - } else if (!decodingInfo.supported) { - this.warn(`Unsupported MediaCapabilities decodingInfo result for level ${index} ${JSON.stringify(decodingInfo)}`); - if (index > -1 && levels.length > 1) { - this.log(`Removing unsupported level ${index}`); - this.hls.removeLevel(index); - } - } - }); - } else { - levelInfo.supportedResult = SUPPORTED_INFO_DEFAULT; - } - } - - // skip candidates which change codec-family or video-range, - // and which decrease or increase frame-rate for up and down-switch respectfully - if (currentCodecSet && levelInfo.codecSet !== currentCodecSet || currentVideoRange && levelInfo.videoRange !== currentVideoRange || upSwitch && currentFrameRate > levelInfo.frameRate || !upSwitch && currentFrameRate > 0 && currentFrameRate < levelInfo.frameRate || levelInfo.supportedResult && !((_levelInfo$supportedR = levelInfo.supportedResult.decodingInfoResults) != null && _levelInfo$supportedR[0].smooth)) { - if (!firstSelection || i !== minStartIndex) { - levelsSkipped.push(i); - continue; - } - } - const levelDetails = levelInfo.details; - const avgDuration = (partCurrent ? levelDetails == null ? void 0 : levelDetails.partTarget : levelDetails == null ? void 0 : levelDetails.averagetargetduration) || currentFragDuration; - let adjustedbw; - // follow algorithm captured from stagefright : - // https://android.googlesource.com/platform/frameworks/av/+/master/media/libstagefright/httplive/LiveSession.cpp - // Pick the highest bandwidth stream below or equal to estimated bandwidth. - // consider only 80% of the available bandwidth, but if we are switching up, - // be even more conservative (70%) to avoid overestimating and immediately - // switching back. - if (!upSwitch) { - adjustedbw = bwFactor * currentBw; - } else { - adjustedbw = bwUpFactor * currentBw; - } - - // Use average bitrate when starvation delay (buffer length) is gt or eq two segment durations and rebuffering is not expected (maxStarvationDelay > 0) - const bitrate = currentFragDuration && bufferStarvationDelay >= currentFragDuration * 2 && maxStarvationDelay === 0 ? levels[i].averageBitrate : levels[i].maxBitrate; - const fetchDuration = this.getTimeToLoadFrag(ttfbEstimateSec, adjustedbw, bitrate * avgDuration, levelDetails === undefined); - const canSwitchWithinTolerance = - // if adjusted bw is greater than level bitrate AND - adjustedbw >= bitrate && ( - // no level change, or new level has no error history - i === lastLoadedFragLevel || levelInfo.loadError === 0 && levelInfo.fragmentError === 0) && ( - // 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 <= ttfbEstimateSec || !isFiniteNumber(fetchDuration) || live && !this.bitrateTestDelay || fetchDuration < maxFetchDuration); - if (canSwitchWithinTolerance) { - const forcedAutoLevel = this.forcedAutoLevel; - if (i !== loadLevel && (forcedAutoLevel === -1 || forcedAutoLevel !== loadLevel)) { - if (levelsSkipped.length) { - this.trace(`Skipped level(s) ${levelsSkipped.join(',')} of ${maxAutoLevel} max with CODECS and VIDEO-RANGE:"${levels[levelsSkipped[0]].codecs}" ${levels[levelsSkipped[0]].videoRange}; not compatible with "${level.codecs}" ${currentVideoRange}`); - } - this.info(`switch candidate:${selectionBaseLevel}->${i} adjustedbw(${Math.round(adjustedbw)})-bitrate=${Math.round(adjustedbw - bitrate)} ttfb:${ttfbEstimateSec.toFixed(1)} avgDuration:${avgDuration.toFixed(1)} maxFetchDuration:${maxFetchDuration.toFixed(1)} fetchDuration:${fetchDuration.toFixed(1)} firstSelection:${firstSelection} codecSet:${levelInfo.codecSet} videoRange:${levelInfo.videoRange} hls.loadLevel:${loadLevel}`); - } - if (firstSelection) { - this.firstSelection = i; - } - // as we are looping from highest to lowest, this will return the best achievable quality level - return i; - } - } - // not enough time budget even with quality level 0 ... rebuffering might happen - return -1; - } - set nextAutoLevel(nextLevel) { - const { - maxAutoLevel, - minAutoLevel - } = this.hls; - const value = Math.min(Math.max(nextLevel, minAutoLevel), maxAutoLevel); - if (this._nextAutoLevel !== value) { - this.nextAutoLevelKey = ''; - this._nextAutoLevel = value; - } - } -} - -class ChunkCache { - constructor() { - this.chunks = []; - this.dataLength = 0; - } - push(chunk) { - this.chunks.push(chunk); - this.dataLength += chunk.length; - } - flush() { - const { - chunks, - dataLength - } = this; - let result; - if (!chunks.length) { - return new Uint8Array(0); - } else if (chunks.length === 1) { - result = chunks[0]; - } else { - result = concatUint8Arrays(chunks, dataLength); - } - this.reset(); - return result; - } - reset() { - this.chunks.length = 0; - this.dataLength = 0; - } -} -function concatUint8Arrays(chunks, dataLength) { - const result = new Uint8Array(dataLength); - let offset = 0; - for (let i = 0; i < chunks.length; i++) { - const chunk = chunks[i]; - result.set(chunk, offset); - offset += chunk.length; - } - return result; -} - -function subtitleOptionsIdentical(trackList1, trackList2) { - if (trackList1.length !== trackList2.length) { - return false; - } - for (let i = 0; i < trackList1.length; i++) { - if (!mediaAttributesIdentical(trackList1[i].attrs, trackList2[i].attrs)) { - return false; - } - } - return true; -} -function mediaAttributesIdentical(attrs1, attrs2, customAttributes) { - // Media options with the same rendition ID must be bit identical - const stableRenditionId = attrs1['STABLE-RENDITION-ID']; - if (stableRenditionId && !customAttributes) { - return stableRenditionId === attrs2['STABLE-RENDITION-ID']; - } - // When rendition ID is not present, compare attributes - return !(customAttributes || ['LANGUAGE', 'NAME', 'CHARACTERISTICS', 'AUTOSELECT', 'DEFAULT', 'FORCED', 'ASSOC-LANGUAGE']).some(subtitleAttribute => attrs1[subtitleAttribute] !== attrs2[subtitleAttribute]); -} -function subtitleTrackMatchesTextTrack(subtitleTrack, textTrack) { - return textTrack.label.toLowerCase() === subtitleTrack.name.toLowerCase() && (!textTrack.language || textTrack.language.toLowerCase() === (subtitleTrack.lang || '').toLowerCase()); -} - -const TICK_INTERVAL$1 = 100; // how often to tick in ms - -class AudioStreamController extends BaseStreamController { - constructor(hls, fragmentTracker, keyLoader) { - super(hls, fragmentTracker, keyLoader, 'audio-stream-controller', PlaylistLevelType.AUDIO); - this.videoAnchor = null; - this.mainFragLoading = null; - this.bufferedTrack = null; - this.switchingTrack = null; - this.trackId = -1; - this.waitingData = null; - this.mainDetails = null; - this.flushing = false; - this.bufferFlushed = false; - this.cachedTrackLoadedData = null; - this.registerListeners(); - } - onHandlerDestroying() { - this.unregisterListeners(); - super.onHandlerDestroying(); - this.mainDetails = null; - this.bufferedTrack = null; - this.switchingTrack = null; - } - registerListeners() { - super.registerListeners(); - const { - hls - } = this; - hls.on(Events.LEVEL_LOADED, this.onLevelLoaded, this); - hls.on(Events.AUDIO_TRACKS_UPDATED, this.onAudioTracksUpdated, this); - hls.on(Events.AUDIO_TRACK_SWITCHING, this.onAudioTrackSwitching, this); - hls.on(Events.AUDIO_TRACK_LOADED, this.onAudioTrackLoaded, this); - hls.on(Events.BUFFER_RESET, this.onBufferReset, this); - hls.on(Events.BUFFER_CREATED, this.onBufferCreated, this); - hls.on(Events.BUFFER_FLUSHING, this.onBufferFlushing, this); - hls.on(Events.BUFFER_FLUSHED, this.onBufferFlushed, this); - hls.on(Events.INIT_PTS_FOUND, this.onInitPtsFound, this); - hls.on(Events.FRAG_LOADING, this.onFragLoading, this); - hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this); - } - unregisterListeners() { - const { - hls - } = this; - if (!hls) { - return; - } - super.unregisterListeners(); - hls.off(Events.LEVEL_LOADED, this.onLevelLoaded, this); - hls.off(Events.AUDIO_TRACKS_UPDATED, this.onAudioTracksUpdated, this); - hls.off(Events.AUDIO_TRACK_SWITCHING, this.onAudioTrackSwitching, this); - hls.off(Events.AUDIO_TRACK_LOADED, this.onAudioTrackLoaded, this); - hls.off(Events.BUFFER_RESET, this.onBufferReset, this); - hls.off(Events.BUFFER_CREATED, this.onBufferCreated, this); - hls.off(Events.BUFFER_FLUSHING, this.onBufferFlushing, this); - hls.off(Events.BUFFER_FLUSHED, this.onBufferFlushed, this); - hls.off(Events.INIT_PTS_FOUND, this.onInitPtsFound, this); - hls.on(Events.FRAG_LOADING, this.onFragLoading, this); - hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this); - } - - // INIT_PTS_FOUND is triggered when the video track parsed in the stream-controller has a new PTS value - onInitPtsFound(event, { - frag, - id, - initPTS, - timescale - }) { - // Always update the new INIT PTS - // Can change due level switch - if (id === PlaylistLevelType.MAIN) { - const cc = frag.cc; - const inFlightFrag = this.fragCurrent; - this.initPTS[cc] = { - baseTime: initPTS, - timescale - }; - this.log(`InitPTS for cc: ${cc} found from main: ${initPTS}/${timescale}`); - this.videoAnchor = frag; - // If we are waiting, tick immediately to unblock audio fragment transmuxing - if (this.state === State.WAITING_INIT_PTS) { - const waitingData = this.waitingData; - if (!waitingData && !this.loadingParts || waitingData && waitingData.frag.cc !== cc) { - this.nextLoadPosition = this.findSyncFrag(frag).start; - } - this.tick(); - } else if (!this.hls.hasEnoughToStart && inFlightFrag && inFlightFrag.cc !== cc) { - this.startFragRequested = false; - this.nextLoadPosition = this.findSyncFrag(frag).start; - inFlightFrag.abortRequests(); - this.resetLoadingState(); - } else if (this.state === State.IDLE) { - this.tick(); - } - } - } - findSyncFrag(mainFrag) { - const trackDetails = this.getLevelDetails(); - const cc = mainFrag.cc; - return findNearestWithCC(trackDetails, cc, mainFrag) || trackDetails && findFragWithCC(trackDetails.fragments, cc) || mainFrag; - } - startLoad(startPosition) { - if (!this.levels) { - this.startPosition = startPosition; - this.state = State.STOPPED; - return; - } - const lastCurrentTime = this.lastCurrentTime; - this.stopLoad(); - this.setInterval(TICK_INTERVAL$1); - if (lastCurrentTime > 0 && startPosition === -1) { - this.log(`Override startPosition with lastCurrentTime @${lastCurrentTime.toFixed(3)}`); - startPosition = lastCurrentTime; - this.state = State.IDLE; - } else { - this.state = State.WAITING_TRACK; - } - this.nextLoadPosition = this.startPosition = this.lastCurrentTime = startPosition; - this.tick(); - } - doTick() { - switch (this.state) { - case State.IDLE: - this.doTickIdle(); - break; - case State.WAITING_TRACK: - { - var _levels$trackId; - const { - levels, - trackId - } = this; - const details = levels == null ? void 0 : (_levels$trackId = levels[trackId]) == null ? void 0 : _levels$trackId.details; - if (details) { - if (this.waitForCdnTuneIn(details)) { - break; - } - this.state = State.WAITING_INIT_PTS; - } - break; - } - case State.FRAG_LOADING_WAITING_RETRY: - { - var _this$media; - const now = performance.now(); - const retryDate = this.retryDate; - // if current time is gt than retryDate, or if media seeking let's switch to IDLE state to retry loading - if (!retryDate || now >= retryDate || (_this$media = this.media) != null && _this$media.seeking) { - const { - levels, - trackId - } = this; - this.log('RetryDate reached, switch back to IDLE state'); - this.resetStartWhenNotLoaded((levels == null ? void 0 : levels[trackId]) || null); - this.state = State.IDLE; - } - break; - } - case State.WAITING_INIT_PTS: - { - // Ensure we don't get stuck in the WAITING_INIT_PTS state if the waiting frag CC doesn't match any initPTS - const waitingData = this.waitingData; - if (waitingData) { - const { - frag, - part, - cache, - complete - } = waitingData; - const videoAnchor = this.videoAnchor; - if (this.initPTS[frag.cc] !== undefined) { - this.waitingData = null; - this.state = State.FRAG_LOADING; - const payload = cache.flush(); - const data = { - frag, - part, - payload, - networkDetails: null - }; - this._handleFragmentLoadProgress(data); - if (complete) { - super._handleFragmentLoadComplete(data); - } - } else if (videoAnchor && videoAnchor.cc !== waitingData.frag.cc) { - // Drop waiting fragment if videoTrackCC has changed since waitingFragment was set and initPTS was not found - this.log(`Waiting fragment cc (${frag.cc}) cancelled because video is at cc ${videoAnchor.cc}`); - this.nextLoadPosition = this.findSyncFrag(videoAnchor).start; - this.clearWaitingFragment(); - } - } else if (this.state !== State.STOPPED) { - this.state = State.IDLE; - } - } - } - this.onTickEnd(); - } - clearWaitingFragment() { - const waitingData = this.waitingData; - if (waitingData) { - if (!this.hls.hasEnoughToStart) { - // Load overlapping fragment on start when discontinuity start times are not aligned - this.startFragRequested = false; - } - this.fragmentTracker.removeFragment(waitingData.frag); - this.waitingData = null; - if (this.state !== State.STOPPED) { - this.state = State.IDLE; - } - } - } - resetLoadingState() { - this.clearWaitingFragment(); - super.resetLoadingState(); - } - onTickEnd() { - const { - media - } = this; - if (!(media != null && media.readyState)) { - // Exit early if we don't have media or if the media hasn't buffered anything yet (readyState 0) - return; - } - this.lastCurrentTime = media.currentTime; - } - doTickIdle() { - var _this$mainFragLoading; - const { - hls, - levels, - media, - trackId - } = this; - const config = hls.config; - - // 1. if buffering is suspended - // 2. if video not attached AND - // start fragment already requested OR start frag prefetch not enabled - // 3. if tracks or track not loaded and selected - // then exit loop - // => if media not attached but start frag prefetch is enabled and start frag not requested yet, we will not exit loop - if (!this.buffering || !media && (this.startFragRequested || !config.startFragPrefetch) || !(levels != null && levels[trackId])) { - return; - } - const levelInfo = levels[trackId]; - const trackDetails = levelInfo.details; - if (!trackDetails || trackDetails.live && this.levelLastLoaded !== levelInfo || this.waitForCdnTuneIn(trackDetails)) { - this.state = State.WAITING_TRACK; - return; - } - const bufferable = this.mediaBuffer ? this.mediaBuffer : this.media; - if (this.bufferFlushed && bufferable) { - this.bufferFlushed = false; - this.afterBufferFlushed(bufferable, ElementaryStreamTypes.AUDIO, PlaylistLevelType.AUDIO); - } - const bufferInfo = this.getFwdBufferInfo(bufferable, PlaylistLevelType.AUDIO); - if (bufferInfo === null) { - return; - } - const { - bufferedTrack, - switchingTrack - } = this; - if (!switchingTrack && this._streamEnded(bufferInfo, trackDetails)) { - hls.trigger(Events.BUFFER_EOS, { - type: 'audio' - }); - this.state = State.ENDED; - return; - } - const bufferLen = bufferInfo.len; - const maxBufLen = hls.maxBufferLength; - const fragments = trackDetails.fragments; - const start = fragments[0].start; - const loadPosition = this.getLoadPosition(); - let targetBufferTime = this.flushing ? loadPosition : bufferInfo.end; - if (switchingTrack && media) { - const pos = loadPosition; - // STABLE - if (bufferedTrack && !mediaAttributesIdentical(switchingTrack.attrs, bufferedTrack.attrs)) { - targetBufferTime = pos; - } - // if currentTime (pos) is less than alt audio playlist start time, it means that alt audio is ahead of currentTime - if (trackDetails.PTSKnown && pos < start) { - // if everything is buffered from pos to start or if audio buffer upfront, let's seek to start - if (bufferInfo.end > start || bufferInfo.nextStart) { - this.log('Alt audio track ahead of main track, seek to start of alt audio track'); - media.currentTime = start + 0.05; - } - } - } - - // if buffer length is less than maxBufLen, or near the end, find a fragment to load - if (bufferLen >= maxBufLen && !switchingTrack && targetBufferTime < fragments[fragments.length - 1].start) { - return; - } - let frag = this.getNextFragment(targetBufferTime, trackDetails); - // Avoid loop loading by using nextLoadPosition set for backtracking and skipping consecutive GAP tags - if (frag && this.isLoopLoading(frag, targetBufferTime)) { - frag = this.getNextFragmentLoopLoading(frag, trackDetails, bufferInfo, PlaylistLevelType.MAIN, maxBufLen); - } - if (!frag) { - this.bufferFlushed = true; - return; - } - - // Request audio segments up to one fragment ahead of main stream-controller - const mainFragLoading = (_this$mainFragLoading = this.mainFragLoading) == null ? void 0 : _this$mainFragLoading.frag; - if (this.startFragRequested && mainFragLoading && mainFragLoading.sn !== 'initSegment' && frag.sn !== 'initSegment' && !frag.endList && (!trackDetails.live || !this.loadingParts && targetBufferTime < this.hls.liveSyncPosition)) { - let mainFrag = mainFragLoading; - if (frag.start > mainFrag.end) { - // Get buffered frag at target position from tracker (loaded out of sequence) - const mainFragAtPos = this.fragmentTracker.getFragAtPos(targetBufferTime, PlaylistLevelType.MAIN); - if (mainFragAtPos && mainFragAtPos.end > mainFragLoading.end) { - mainFrag = mainFragAtPos; - this.mainFragLoading = { - frag: mainFragAtPos, - targetBufferTime: null - }; - } - } - const atBufferSyncLimit = frag.start > mainFrag.end; - if (atBufferSyncLimit) { - return; - } - } - this.loadFragment(frag, levelInfo, targetBufferTime); - } - onMediaDetaching(event, data) { - this.bufferFlushed = this.flushing = false; - super.onMediaDetaching(event, data); - } - onAudioTracksUpdated(event, { - audioTracks - }) { - // Reset tranxmuxer is essential for large context switches (Content Steering) - this.resetTransmuxer(); - this.levels = audioTracks.map(mediaPlaylist => new Level(mediaPlaylist)); - } - onAudioTrackSwitching(event, data) { - // if any URL found on new audio track, it is an alternate audio track - const altAudio = !!data.url; - this.trackId = data.id; - const { - fragCurrent - } = this; - if (fragCurrent) { - fragCurrent.abortRequests(); - this.removeUnbufferedFrags(fragCurrent.start); - } - this.resetLoadingState(); - - // should we switch tracks ? - if (altAudio) { - this.switchingTrack = data; - // main audio track are handled by stream-controller, just do something if switching to alt audio track - this.flushAudioIfNeeded(data); - if (this.state !== State.STOPPED) { - // switching to audio track, start timer if not already started - this.setInterval(TICK_INTERVAL$1); - this.state = State.IDLE; - this.tick(); - } - } else { - // destroy useless transmuxer when switching audio to main - this.resetTransmuxer(); - this.switchingTrack = null; - this.bufferedTrack = data; - this.clearInterval(); - } - } - onManifestLoading() { - super.onManifestLoading(); - this.bufferFlushed = this.flushing = false; - this.mainDetails = this.waitingData = this.videoAnchor = this.bufferedTrack = this.cachedTrackLoadedData = this.switchingTrack = null; - this.trackId = -1; - } - onLevelLoaded(event, data) { - this.mainDetails = data.details; - if (this.cachedTrackLoadedData !== null) { - this.hls.trigger(Events.AUDIO_TRACK_LOADED, this.cachedTrackLoadedData); - this.cachedTrackLoadedData = null; - } - } - onAudioTrackLoaded(event, data) { - var _track$details; - if (this.mainDetails == null) { - this.cachedTrackLoadedData = data; - return; - } - const { - levels - } = this; - const { - details: newDetails, - id: trackId - } = data; - if (!levels) { - this.warn(`Audio tracks were reset while loading level ${trackId}`); - return; - } - this.log(`Audio track ${trackId} loaded [${newDetails.startSN},${newDetails.endSN}]${newDetails.lastPartSn ? `[part-${newDetails.lastPartSn}-${newDetails.lastPartIndex}]` : ''},duration:${newDetails.totalduration}`); - const track = levels[trackId]; - let sliding = 0; - if (newDetails.live || (_track$details = track.details) != null && _track$details.live) { - this.checkLiveUpdate(newDetails); - const mainDetails = this.mainDetails; - if (newDetails.deltaUpdateFailed || !mainDetails) { - return; - } - if (!track.details && newDetails.hasProgramDateTime && mainDetails.hasProgramDateTime) { - // Make sure our audio rendition is aligned with the "main" rendition, using - // pdt as our reference times. - alignMediaPlaylistByPDT(newDetails, mainDetails); - sliding = newDetails.fragmentStart; - } else { - var _this$levelLastLoaded; - sliding = this.alignPlaylists(newDetails, track.details, (_this$levelLastLoaded = this.levelLastLoaded) == null ? void 0 : _this$levelLastLoaded.details); - } - } - track.details = newDetails; - this.levelLastLoaded = track; - - // compute start position if we are aligned with the main playlist - if (!this.startFragRequested && (this.mainDetails || !newDetails.live)) { - this.setStartPosition(this.mainDetails || newDetails, sliding); - } - this.hls.trigger(Events.AUDIO_TRACK_UPDATED, { - details: newDetails, - id: trackId, - groupId: data.groupId - }); - - // only switch back to IDLE state if we were waiting for track to start downloading a new fragment - if (this.state === State.WAITING_TRACK && !this.waitForCdnTuneIn(newDetails)) { - this.state = State.IDLE; - } - - // trigger handler right now - this.tick(); - } - _handleFragmentLoadProgress(data) { - var _frag$initSegment; - const frag = data.frag; - const { - part, - payload - } = data; - const { - config, - trackId, - levels - } = this; - if (!levels) { - this.warn(`Audio tracks were reset while fragment load was in progress. Fragment ${frag.sn} of level ${frag.level} will not be buffered`); - return; - } - const track = levels[trackId]; - if (!track) { - this.warn('Audio track is undefined on fragment load progress'); - return; - } - const details = track.details; - if (!details) { - this.warn('Audio track details undefined on fragment load progress'); - this.removeUnbufferedFrags(frag.start); - return; - } - const audioCodec = config.defaultAudioCodec || track.audioCodec || 'mp4a.40.2'; - let transmuxer = this.transmuxer; - if (!transmuxer) { - transmuxer = this.transmuxer = new TransmuxerInterface(this.hls, PlaylistLevelType.AUDIO, this._handleTransmuxComplete.bind(this), this._handleTransmuxerFlush.bind(this)); - } - - // Check if we have video initPTS - // If not we need to wait for it - const initPTS = this.initPTS[frag.cc]; - const initSegmentData = (_frag$initSegment = frag.initSegment) == null ? void 0 : _frag$initSegment.data; - if (initPTS !== undefined) { - // this.log(`Transmuxing ${sn} of [${details.startSN} ,${details.endSN}],track ${trackId}`); - // time Offset is accurate if level PTS is known, or if playlist is not sliding (not live) - const accurateTimeOffset = false; // details.PTSKnown || !details.live; - const partIndex = part ? part.index : -1; - const partial = partIndex !== -1; - const chunkMeta = new ChunkMetadata(frag.level, frag.sn, frag.stats.chunkCount, payload.byteLength, partIndex, partial); - transmuxer.push(payload, initSegmentData, audioCodec, '', frag, part, details.totalduration, accurateTimeOffset, chunkMeta, initPTS); - } else { - this.log(`Unknown video PTS for cc ${frag.cc}, waiting for video PTS before demuxing audio frag ${frag.sn} of [${details.startSN} ,${details.endSN}],track ${trackId}`); - const { - cache - } = this.waitingData = this.waitingData || { - frag, - part, - cache: new ChunkCache(), - complete: false - }; - cache.push(new Uint8Array(payload)); - this.state = State.WAITING_INIT_PTS; - } - } - _handleFragmentLoadComplete(fragLoadedData) { - if (this.waitingData) { - this.waitingData.complete = true; - return; - } - super._handleFragmentLoadComplete(fragLoadedData); - } - onBufferReset(/* event: Events.BUFFER_RESET */ - ) { - // reset reference to sourcebuffers - this.mediaBuffer = null; - } - onBufferCreated(event, data) { - this.bufferFlushed = this.flushing = false; - const audioTrack = data.tracks.audio; - if (audioTrack) { - this.mediaBuffer = audioTrack.buffer || null; - } - } - onFragLoading(event, data) { - if (data.frag.type === PlaylistLevelType.MAIN && data.frag.sn !== 'initSegment') { - this.mainFragLoading = data; - if (this.state === State.IDLE) { - this.tick(); - } - } - } - onFragBuffered(event, data) { - const { - frag, - part - } = data; - if (frag.type !== PlaylistLevelType.AUDIO) { - return; - } - if (this.fragContextChanged(frag)) { - // If a level switch was requested while a fragment was buffering, it will emit the FRAG_BUFFERED event upon completion - // Avoid setting state back to IDLE or concluding the audio switch; otherwise, the switched-to track will not buffer - this.warn(`Fragment ${frag.sn}${part ? ' p: ' + part.index : ''} of level ${frag.level} finished buffering, but was aborted. state: ${this.state}, audioSwitch: ${this.switchingTrack ? this.switchingTrack.name : 'false'}`); - return; - } - if (frag.sn !== 'initSegment') { - this.fragPrevious = frag; - const track = this.switchingTrack; - if (track) { - this.bufferedTrack = track; - this.switchingTrack = null; - this.hls.trigger(Events.AUDIO_TRACK_SWITCHED, _objectSpread2({}, track)); - } - } - this.fragBufferedComplete(frag, part); - if (this.media) { - this.tick(); - } - } - onError(event, data) { - var _data$context; - if (data.fatal) { - this.state = State.ERROR; - return; - } - switch (data.details) { - case ErrorDetails.FRAG_GAP: - case ErrorDetails.FRAG_PARSING_ERROR: - case ErrorDetails.FRAG_DECRYPT_ERROR: - case ErrorDetails.FRAG_LOAD_ERROR: - case ErrorDetails.FRAG_LOAD_TIMEOUT: - case ErrorDetails.KEY_LOAD_ERROR: - case ErrorDetails.KEY_LOAD_TIMEOUT: - this.onFragmentOrKeyLoadError(PlaylistLevelType.AUDIO, data); - break; - case ErrorDetails.AUDIO_TRACK_LOAD_ERROR: - case ErrorDetails.AUDIO_TRACK_LOAD_TIMEOUT: - case ErrorDetails.LEVEL_PARSING_ERROR: - // in case of non fatal error while loading track, if not retrying to load track, switch back to IDLE - if (!data.levelRetry && this.state === State.WAITING_TRACK && ((_data$context = data.context) == null ? void 0 : _data$context.type) === PlaylistContextType.AUDIO_TRACK) { - this.state = State.IDLE; - } - break; - case ErrorDetails.BUFFER_APPEND_ERROR: - case ErrorDetails.BUFFER_FULL_ERROR: - if (!data.parent || data.parent !== 'audio') { - return; - } - if (data.details === ErrorDetails.BUFFER_APPEND_ERROR) { - this.resetLoadingState(); - return; - } - if (this.reduceLengthAndFlushBuffer(data)) { - this.bufferedTrack = null; - super.flushMainBuffer(0, Number.POSITIVE_INFINITY, 'audio'); - } - break; - case ErrorDetails.INTERNAL_EXCEPTION: - this.recoverWorkerError(data); - break; - } - } - onBufferFlushing(event, { - type - }) { - if (type !== ElementaryStreamTypes.VIDEO) { - this.flushing = true; - } - } - onBufferFlushed(event, { - type - }) { - if (type !== ElementaryStreamTypes.VIDEO) { - this.flushing = false; - this.bufferFlushed = true; - if (this.state === State.ENDED) { - this.state = State.IDLE; - } - const mediaBuffer = this.mediaBuffer || this.media; - if (mediaBuffer) { - this.afterBufferFlushed(mediaBuffer, type, PlaylistLevelType.AUDIO); - this.tick(); - } - } - } - _handleTransmuxComplete(transmuxResult) { - var _id3$samples; - const id = 'audio'; - const { - hls - } = this; - const { - remuxResult, - chunkMeta - } = transmuxResult; - const context = this.getCurrentContext(chunkMeta); - if (!context) { - this.resetWhenMissingContext(chunkMeta); - return; - } - const { - frag, - part, - level - } = context; - const { - details - } = level; - const { - audio, - text, - id3, - initSegment - } = remuxResult; - - // Check if the current fragment has been aborted. We check this by first seeing if we're still playing the current level. - // If we are, subsequently check if the currently loading fragment (fragCurrent) has changed. - if (this.fragContextChanged(frag) || !details) { - this.fragmentTracker.removeFragment(frag); - return; - } - this.state = State.PARSING; - if (this.switchingTrack && audio) { - this.completeAudioSwitch(this.switchingTrack); - } - if (initSegment != null && initSegment.tracks) { - const mapFragment = frag.initSegment || frag; - this._bufferInitSegment(level, initSegment.tracks, mapFragment, chunkMeta); - hls.trigger(Events.FRAG_PARSING_INIT_SEGMENT, { - frag: mapFragment, - id, - tracks: initSegment.tracks - }); - // Only flush audio from old audio tracks when PTS is known on new audio track - } - if (audio) { - const { - startPTS, - endPTS, - startDTS, - endDTS - } = audio; - if (part) { - part.elementaryStreams[ElementaryStreamTypes.AUDIO] = { - startPTS, - endPTS, - startDTS, - endDTS - }; - } - frag.setElementaryStreamInfo(ElementaryStreamTypes.AUDIO, startPTS, endPTS, startDTS, endDTS); - this.bufferFragmentData(audio, frag, part, chunkMeta); - } - if (id3 != null && (_id3$samples = id3.samples) != null && _id3$samples.length) { - const emittedID3 = _extends({ - id, - frag, - details - }, id3); - hls.trigger(Events.FRAG_PARSING_METADATA, emittedID3); - } - if (text) { - const emittedText = _extends({ - id, - frag, - details - }, text); - hls.trigger(Events.FRAG_PARSING_USERDATA, emittedText); - } - } - _bufferInitSegment(currentLevel, tracks, frag, chunkMeta) { - if (this.state !== State.PARSING) { - return; - } - // delete any video track found on audio transmuxer - if (tracks.video) { - delete tracks.video; - } - if (tracks.audiovideo) { - delete tracks.audiovideo; - } - - // include levelCodec in audio and video tracks - if (!tracks.audio) { - return; - } - const track = tracks.audio; - track.id = 'audio'; - const variantAudioCodecs = currentLevel.audioCodec; - this.log(`Init audio buffer, container:${track.container}, codecs[level/parsed]=[${variantAudioCodecs}/${track.codec}]`); - // SourceBuffer will use track.levelCodec if defined - if (variantAudioCodecs && variantAudioCodecs.split(',').length === 1) { - track.levelCodec = variantAudioCodecs; - } - this.hls.trigger(Events.BUFFER_CODECS, tracks); - const initSegment = track.initSegment; - if (initSegment != null && initSegment.byteLength) { - const segment = { - type: 'audio', - frag, - part: null, - chunkMeta, - parent: frag.type, - data: initSegment - }; - this.hls.trigger(Events.BUFFER_APPENDING, segment); - } - // trigger handler right now - this.tickImmediate(); - } - loadFragment(frag, track, targetBufferTime) { - // only load if fragment is not loaded or if in audio switch - const fragState = this.fragmentTracker.getState(frag); - - // we force a frag loading in audio switch as fragment tracker might not have evicted previous frags in case of quick audio switch - if (this.switchingTrack || fragState === FragmentState.NOT_LOADED || fragState === FragmentState.PARTIAL) { - var _track$details2; - if (frag.sn === 'initSegment') { - this._loadInitSegment(frag, track); - } else if ((_track$details2 = track.details) != null && _track$details2.live && !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}`); - this.state = State.WAITING_INIT_PTS; - const mainDetails = this.mainDetails; - if (mainDetails && mainDetails.fragmentStart !== track.details.fragmentStart) { - alignMediaPlaylistByPDT(track.details, mainDetails); - } - } else { - super.loadFragment(frag, track, targetBufferTime); - } - } else { - this.clearTrackerIfNeeded(frag); - } - } - flushAudioIfNeeded(switchingTrack) { - const { - media, - bufferedTrack - } = this; - const bufferedAttributes = bufferedTrack == null ? void 0 : bufferedTrack.attrs; - const switchAttributes = switchingTrack.attrs; - if (media && bufferedAttributes && (bufferedAttributes.CHANNELS !== switchAttributes.CHANNELS || bufferedTrack.name !== switchingTrack.name || bufferedTrack.lang !== switchingTrack.lang)) { - this.log('Switching audio track : flushing all audio'); - super.flushMainBuffer(0, Number.POSITIVE_INFINITY, 'audio'); - this.bufferedTrack = null; - } - } - completeAudioSwitch(switchingTrack) { - const { - hls - } = this; - this.flushAudioIfNeeded(switchingTrack); - this.bufferedTrack = switchingTrack; - this.switchingTrack = null; - hls.trigger(Events.AUDIO_TRACK_SWITCHED, _objectSpread2({}, switchingTrack)); - } -} - -class AudioTrackController extends BasePlaylistController { - constructor(hls) { - super(hls, 'audio-track-controller'); - this.tracks = []; - this.groupIds = null; - this.tracksInGroup = []; - this.trackId = -1; - this.currentTrack = null; - this.selectDefaultTrack = true; - this.registerListeners(); - } - registerListeners() { - const { - hls - } = this; - hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); - hls.on(Events.MANIFEST_PARSED, this.onManifestParsed, this); - hls.on(Events.LEVEL_LOADING, this.onLevelLoading, this); - hls.on(Events.LEVEL_SWITCHING, this.onLevelSwitching, this); - hls.on(Events.AUDIO_TRACK_LOADED, this.onAudioTrackLoaded, this); - hls.on(Events.ERROR, this.onError, this); - } - unregisterListeners() { - const { - hls - } = this; - hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); - hls.off(Events.MANIFEST_PARSED, this.onManifestParsed, this); - hls.off(Events.LEVEL_LOADING, this.onLevelLoading, this); - hls.off(Events.LEVEL_SWITCHING, this.onLevelSwitching, this); - hls.off(Events.AUDIO_TRACK_LOADED, this.onAudioTrackLoaded, this); - hls.off(Events.ERROR, this.onError, this); - } - destroy() { - this.unregisterListeners(); - this.tracks.length = 0; - this.tracksInGroup.length = 0; - this.currentTrack = null; - super.destroy(); - } - onManifestLoading() { - this.tracks = []; - this.tracksInGroup = []; - this.groupIds = null; - this.currentTrack = null; - this.trackId = -1; - this.selectDefaultTrack = true; - } - onManifestParsed(event, data) { - this.tracks = data.audioTracks || []; - } - onAudioTrackLoaded(event, data) { - const { - id, - groupId, - details - } = data; - const trackInActiveGroup = this.tracksInGroup[id]; - if (!trackInActiveGroup || trackInActiveGroup.groupId !== groupId) { - this.warn(`Audio track with id:${id} and group:${groupId} not found in active group ${trackInActiveGroup == null ? void 0 : trackInActiveGroup.groupId}`); - return; - } - const curDetails = trackInActiveGroup.details; - trackInActiveGroup.details = data.details; - this.log(`Audio track ${id} "${trackInActiveGroup.name}" lang:${trackInActiveGroup.lang} group:${groupId} loaded [${details.startSN}-${details.endSN}]`); - if (id === this.trackId) { - this.playlistLoaded(id, data, curDetails); - } - } - onLevelLoading(event, data) { - this.switchLevel(data.level); - } - onLevelSwitching(event, data) { - this.switchLevel(data.level); - } - switchLevel(levelIndex) { - const levelInfo = this.hls.levels[levelIndex]; - if (!levelInfo) { - return; - } - const audioGroups = levelInfo.audioGroups || null; - const currentGroups = this.groupIds; - let currentTrack = this.currentTrack; - if (!audioGroups || (currentGroups == null ? void 0 : currentGroups.length) !== (audioGroups == null ? void 0 : audioGroups.length) || audioGroups != null && audioGroups.some(groupId => (currentGroups == null ? void 0 : currentGroups.indexOf(groupId)) === -1)) { - this.groupIds = audioGroups; - this.trackId = -1; - this.currentTrack = null; - const audioTracks = this.tracks.filter(track => !audioGroups || audioGroups.indexOf(track.groupId) !== -1); - if (audioTracks.length) { - // Disable selectDefaultTrack if there are no default tracks - if (this.selectDefaultTrack && !audioTracks.some(track => track.default)) { - this.selectDefaultTrack = false; - } - // track.id should match hls.audioTracks index - audioTracks.forEach((track, i) => { - track.id = i; - }); - } else if (!currentTrack && !this.tracksInGroup.length) { - // Do not dispatch AUDIO_TRACKS_UPDATED when there were and are no tracks - return; - } - this.tracksInGroup = audioTracks; - - // Find preferred track - const audioPreference = this.hls.config.audioPreference; - if (!currentTrack && audioPreference) { - const groupIndex = findMatchingOption(audioPreference, audioTracks, audioMatchPredicate); - if (groupIndex > -1) { - currentTrack = audioTracks[groupIndex]; - } else { - const allIndex = findMatchingOption(audioPreference, this.tracks); - currentTrack = this.tracks[allIndex]; - } - } - - // Select initial track - let trackId = this.findTrackId(currentTrack); - if (trackId === -1 && currentTrack) { - trackId = this.findTrackId(null); - } - - // Dispatch events and load track if needed - const audioTracksUpdated = { - audioTracks - }; - this.log(`Updating audio tracks, ${audioTracks.length} track(s) found in group(s): ${audioGroups == null ? void 0 : audioGroups.join(',')}`); - this.hls.trigger(Events.AUDIO_TRACKS_UPDATED, audioTracksUpdated); - const selectedTrackId = this.trackId; - if (trackId !== -1 && selectedTrackId === -1) { - this.setAudioTrack(trackId); - } else if (audioTracks.length && selectedTrackId === -1) { - var _this$groupIds; - const error = new Error(`No audio track selected for current audio group-ID(s): ${(_this$groupIds = this.groupIds) == null ? void 0 : _this$groupIds.join(',')} track count: ${audioTracks.length}`); - this.warn(error.message); - this.hls.trigger(Events.ERROR, { - type: ErrorTypes.MEDIA_ERROR, - details: ErrorDetails.AUDIO_TRACK_LOAD_ERROR, - fatal: true, - error - }); - } - } else if (this.shouldReloadPlaylist(currentTrack)) { - // Retry playlist loading if no playlist is or has been loaded yet - this.setAudioTrack(this.trackId); - } - } - onError(event, data) { - if (data.fatal || !data.context) { - return; - } - if (data.context.type === PlaylistContextType.AUDIO_TRACK && data.context.id === this.trackId && (!this.groupIds || this.groupIds.indexOf(data.context.groupId) !== -1)) { - this.requestScheduled = -1; - this.checkRetry(data); - } - } - get allAudioTracks() { - return this.tracks; - } - get audioTracks() { - return this.tracksInGroup; - } - get audioTrack() { - return this.trackId; - } - set audioTrack(newId) { - // If audio track is selected from API then don't choose from the manifest default track - this.selectDefaultTrack = false; - this.setAudioTrack(newId); - } - setAudioOption(audioOption) { - const hls = this.hls; - hls.config.audioPreference = audioOption; - if (audioOption) { - const allAudioTracks = this.allAudioTracks; - this.selectDefaultTrack = false; - if (allAudioTracks.length) { - // First see if current option matches (no switch op) - const currentTrack = this.currentTrack; - if (currentTrack && matchesOption(audioOption, currentTrack, audioMatchPredicate)) { - return currentTrack; - } - // Find option in available tracks (tracksInGroup) - const groupIndex = findMatchingOption(audioOption, this.tracksInGroup, audioMatchPredicate); - if (groupIndex > -1) { - const track = this.tracksInGroup[groupIndex]; - this.setAudioTrack(groupIndex); - return track; - } else if (currentTrack) { - // Find option in nearest level audio group - let searchIndex = hls.loadLevel; - if (searchIndex === -1) { - searchIndex = hls.firstAutoLevel; - } - const switchIndex = findClosestLevelWithAudioGroup(audioOption, hls.levels, allAudioTracks, searchIndex, audioMatchPredicate); - if (switchIndex === -1) { - // could not find matching variant - return null; - } - // and switch level to acheive the audio group switch - hls.nextLoadLevel = switchIndex; - } - if (audioOption.channels || audioOption.audioCodec) { - // Could not find a match with codec / channels predicate - // Find a match without channels or codec - const withoutCodecAndChannelsMatch = findMatchingOption(audioOption, allAudioTracks); - if (withoutCodecAndChannelsMatch > -1) { - return allAudioTracks[withoutCodecAndChannelsMatch]; - } - } - } - } - return null; - } - setAudioTrack(newId) { - const tracks = this.tracksInGroup; - - // check if level idx is valid - if (newId < 0 || newId >= tracks.length) { - this.warn(`Invalid audio track id: ${newId}`); - return; - } - - // stopping live reloading timer if any - this.clearTimer(); - this.selectDefaultTrack = false; - const lastTrack = this.currentTrack; - const track = tracks[newId]; - const trackLoaded = track.details && !track.details.live; - if (newId === this.trackId && track === lastTrack && trackLoaded) { - return; - } - this.log(`Switching to audio-track ${newId} "${track.name}" lang:${track.lang} group:${track.groupId} channels:${track.channels}`); - this.trackId = newId; - this.currentTrack = track; - this.hls.trigger(Events.AUDIO_TRACK_SWITCHING, _objectSpread2({}, track)); - // Do not reload track unless live - if (trackLoaded) { - return; - } - const hlsUrlParameters = this.switchParams(track.url, lastTrack == null ? void 0 : lastTrack.details, track.details); - this.loadPlaylist(hlsUrlParameters); - } - findTrackId(currentTrack) { - const audioTracks = this.tracksInGroup; - for (let i = 0; i < audioTracks.length; i++) { - const track = audioTracks[i]; - if (this.selectDefaultTrack && !track.default) { - continue; - } - if (!currentTrack || matchesOption(currentTrack, track, audioMatchPredicate)) { - return i; - } - } - if (currentTrack) { - const { - name, - lang, - assocLang, - characteristics, - audioCodec, - channels - } = currentTrack; - for (let i = 0; i < audioTracks.length; i++) { - const track = audioTracks[i]; - if (matchesOption({ - name, - lang, - assocLang, - characteristics, - audioCodec, - channels - }, track, audioMatchPredicate)) { - return i; - } - } - for (let i = 0; i < audioTracks.length; i++) { - const track = audioTracks[i]; - if (mediaAttributesIdentical(currentTrack.attrs, track.attrs, ['LANGUAGE', 'ASSOC-LANGUAGE', 'CHARACTERISTICS'])) { - return i; - } - } - for (let i = 0; i < audioTracks.length; i++) { - const track = audioTracks[i]; - if (mediaAttributesIdentical(currentTrack.attrs, track.attrs, ['LANGUAGE'])) { - return i; - } - } - } - return -1; - } - loadPlaylist(hlsUrlParameters) { - const audioTrack = this.currentTrack; - if (this.shouldLoadPlaylist(audioTrack) && audioTrack) { - super.loadPlaylist(); - const id = audioTrack.id; - const groupId = audioTrack.groupId; - let url = audioTrack.url; - if (hlsUrlParameters) { - try { - url = hlsUrlParameters.addDirectives(url); - } catch (error) { - this.warn(`Could not construct new URL with HLS Delivery Directives: ${error}`); - } - } - // track not retrieved yet, or live playlist we need to (re)load it - this.log(`loading audio-track playlist ${id} "${audioTrack.name}" lang:${audioTrack.lang} group:${groupId}`); - this.clearTimer(); - this.hls.trigger(Events.AUDIO_TRACK_LOADING, { - url, - id, - groupId, - deliveryDirectives: hlsUrlParameters || null - }); - } - } -} - -const TICK_INTERVAL = 500; // how often to tick in ms - -class SubtitleStreamController extends BaseStreamController { - constructor(hls, fragmentTracker, keyLoader) { - super(hls, fragmentTracker, keyLoader, 'subtitle-stream-controller', PlaylistLevelType.SUBTITLE); - this.currentTrackId = -1; - this.tracksBuffered = []; - this.mainDetails = null; - this.registerListeners(); - } - onHandlerDestroying() { - this.unregisterListeners(); - super.onHandlerDestroying(); - this.mainDetails = null; - } - registerListeners() { - super.registerListeners(); - const { - hls - } = this; - hls.on(Events.LEVEL_LOADED, this.onLevelLoaded, this); - hls.on(Events.SUBTITLE_TRACKS_UPDATED, this.onSubtitleTracksUpdated, this); - hls.on(Events.SUBTITLE_TRACK_SWITCH, this.onSubtitleTrackSwitch, this); - 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); - } - unregisterListeners() { - super.unregisterListeners(); - const { - hls - } = this; - hls.off(Events.LEVEL_LOADED, this.onLevelLoaded, this); - hls.off(Events.SUBTITLE_TRACKS_UPDATED, this.onSubtitleTracksUpdated, this); - hls.off(Events.SUBTITLE_TRACK_SWITCH, this.onSubtitleTrackSwitch, this); - 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); - } - startLoad(startPosition) { - this.stopLoad(); - this.state = State.IDLE; - this.setInterval(TICK_INTERVAL); - this.nextLoadPosition = this.startPosition = this.lastCurrentTime = startPosition; - this.tick(); - } - onManifestLoading() { - super.onManifestLoading(); - this.mainDetails = null; - } - onMediaDetaching(event, data) { - this.tracksBuffered = []; - super.onMediaDetaching(event, data); - } - onLevelLoaded(event, data) { - this.mainDetails = data.details; - } - onSubtitleFragProcessed(event, data) { - const { - frag, - success - } = data; - if (frag.sn !== 'initSegment') { - this.fragPrevious = frag; - } - this.state = State.IDLE; - if (!success) { - return; - } - const buffered = this.tracksBuffered[this.currentTrackId]; - if (!buffered) { - return; - } - - // Create/update a buffered array matching the interface used by BufferHelper.bufferedInfo - // so we can re-use the logic used to detect how much has been buffered - let timeRange; - const fragStart = frag.start; - for (let i = 0; i < buffered.length; i++) { - if (fragStart >= buffered[i].start && fragStart <= buffered[i].end) { - timeRange = buffered[i]; - break; - } - } - const fragEnd = frag.start + frag.duration; - if (timeRange) { - timeRange.end = fragEnd; - } else { - timeRange = { - start: fragStart, - end: fragEnd - }; - buffered.push(timeRange); - } - this.fragmentTracker.fragBuffered(frag); - this.fragBufferedComplete(frag, null); - if (this.media) { - this.tick(); - } - } - onBufferFlushing(event, data) { - const { - startOffset, - endOffset - } = data; - if (startOffset === 0 && endOffset !== Number.POSITIVE_INFINITY) { - const endOffsetSubtitles = endOffset - 1; - if (endOffsetSubtitles <= 0) { - return; - } - data.endOffsetSubtitles = Math.max(0, endOffsetSubtitles); - this.tracksBuffered.forEach(buffered => { - for (let i = 0; i < buffered.length;) { - if (buffered[i].end <= endOffsetSubtitles) { - buffered.shift(); - continue; - } else if (buffered[i].start < endOffsetSubtitles) { - buffered[i].start = endOffsetSubtitles; - } else { - break; - } - i++; - } - }); - this.fragmentTracker.removeFragmentsInRange(startOffset, endOffsetSubtitles, PlaylistLevelType.SUBTITLE); - } - } - - // If something goes wrong, proceed to next frag, if we were processing one. - onError(event, data) { - const frag = data.frag; - if ((frag == null ? void 0 : frag.type) === PlaylistLevelType.SUBTITLE) { - if (data.details === ErrorDetails.FRAG_GAP) { - this.fragmentTracker.fragBuffered(frag, true); - } - if (this.fragCurrent) { - this.fragCurrent.abortRequests(); - } - if (this.state !== State.STOPPED) { - this.state = State.IDLE; - } - } - } - - // Got all new subtitle levels. - onSubtitleTracksUpdated(event, { - subtitleTracks - }) { - if (this.levels && subtitleOptionsIdentical(this.levels, subtitleTracks)) { - this.levels = subtitleTracks.map(mediaPlaylist => new Level(mediaPlaylist)); - return; - } - this.tracksBuffered = []; - this.levels = subtitleTracks.map(mediaPlaylist => { - const level = new Level(mediaPlaylist); - this.tracksBuffered[level.id] = []; - return level; - }); - this.fragmentTracker.removeFragmentsInRange(0, Number.POSITIVE_INFINITY, PlaylistLevelType.SUBTITLE); - this.fragPrevious = null; - this.mediaBuffer = null; - } - onSubtitleTrackSwitch(event, data) { - var _this$levels; - this.currentTrackId = data.id; - if (!((_this$levels = this.levels) != null && _this$levels.length) || this.currentTrackId === -1) { - this.clearInterval(); - return; - } - - // Check if track has the necessary details to load fragments - const currentTrack = this.levels[this.currentTrackId]; - if (currentTrack != null && currentTrack.details) { - this.mediaBuffer = this.mediaBufferTimeRanges; - } else { - this.mediaBuffer = null; - } - if (currentTrack && this.state !== State.STOPPED) { - this.setInterval(TICK_INTERVAL); - } - } - - // Got a new set of subtitle fragments. - onSubtitleTrackLoaded(event, data) { - var _track$details; - const { - currentTrackId, - levels - } = this; - const { - details: newDetails, - id: trackId - } = data; - if (!levels) { - this.warn(`Subtitle tracks were reset while loading level ${trackId}`); - return; - } - const track = levels[trackId]; - if (trackId >= levels.length || !track) { - return; - } - this.log(`Subtitle track ${trackId} loaded [${newDetails.startSN},${newDetails.endSN}]${newDetails.lastPartSn ? `[part-${newDetails.lastPartSn}-${newDetails.lastPartIndex}]` : ''},duration:${newDetails.totalduration}`); - this.mediaBuffer = this.mediaBufferTimeRanges; - let sliding = 0; - if (newDetails.live || (_track$details = track.details) != null && _track$details.live) { - const mainDetails = this.mainDetails; - if (newDetails.deltaUpdateFailed || !mainDetails) { - return; - } - const mainSlidingStartFragment = mainDetails.fragments[0]; - if (!track.details) { - if (newDetails.hasProgramDateTime && mainDetails.hasProgramDateTime) { - alignMediaPlaylistByPDT(newDetails, mainDetails); - sliding = newDetails.fragmentStart; - } else if (mainSlidingStartFragment) { - // line up live playlist with main so that fragments in range are loaded - sliding = mainSlidingStartFragment.start; - addSliding(newDetails, sliding); - } - } else { - var _this$levelLastLoaded; - sliding = this.alignPlaylists(newDetails, track.details, (_this$levelLastLoaded = this.levelLastLoaded) == null ? void 0 : _this$levelLastLoaded.details); - if (sliding === 0 && mainSlidingStartFragment) { - // realign with main when there is no overlap with last refresh - sliding = mainSlidingStartFragment.start; - addSliding(newDetails, sliding); - } - } - } - track.details = newDetails; - this.levelLastLoaded = track; - if (trackId !== currentTrackId) { - return; - } - this.hls.trigger(Events.SUBTITLE_TRACK_UPDATED, { - details: newDetails, - id: trackId, - groupId: data.groupId - }); - - // trigger handler right now - this.tick(); - - // If playlist is misaligned because of bad PDT or drift, delete details to resync with main on reload - if (newDetails.live && !this.fragCurrent && this.media && this.state === State.IDLE) { - const foundFrag = findFragmentByPTS(null, newDetails.fragments, this.media.currentTime, 0); - if (!foundFrag) { - this.warn('Subtitle playlist not aligned with playback'); - track.details = undefined; - } - } - } - _handleFragmentLoadComplete(fragLoadedData) { - const { - frag, - payload - } = fragLoadedData; - const decryptData = frag.decryptdata; - const hls = this.hls; - if (this.fragContextChanged(frag)) { - return; - } - // check to see if the payload needs to be decrypted - if (payload && payload.byteLength > 0 && decryptData != null && decryptData.key && decryptData.iv && isFullSegmentEncryption(decryptData.method)) { - const startTime = performance.now(); - // decrypt the subtitles - this.decrypter.decrypt(new Uint8Array(payload), decryptData.key.buffer, decryptData.iv.buffer, getAesModeFromFullSegmentMethod(decryptData.method)).catch(err => { - hls.trigger(Events.ERROR, { - type: ErrorTypes.MEDIA_ERROR, - details: ErrorDetails.FRAG_DECRYPT_ERROR, - fatal: false, - error: err, - reason: err.message, - frag - }); - throw err; - }).then(decryptedData => { - const endTime = performance.now(); - hls.trigger(Events.FRAG_DECRYPTED, { - frag, - payload: decryptedData, - stats: { - tstart: startTime, - tdecrypt: endTime - } - }); - }).catch(err => { - this.warn(`${err.name}: ${err.message}`); - this.state = State.IDLE; - }); - } - } - doTick() { - if (!this.media) { - this.state = State.IDLE; - return; - } - if (this.state === State.IDLE) { - const { - currentTrackId, - levels - } = this; - const track = levels == null ? void 0 : levels[currentTrackId]; - if (!track || !levels.length || !track.details) { - return; - } - const { - config - } = this; - const currentTime = this.getLoadPosition(); - const bufferedInfo = BufferHelper.bufferedInfo(this.tracksBuffered[this.currentTrackId] || [], currentTime, config.maxBufferHole); - const { - end: targetBufferTime, - len: bufferLen - } = bufferedInfo; - const trackDetails = track.details; - const maxBufLen = this.hls.maxBufferLength + trackDetails.levelTargetDuration; - if (bufferLen > maxBufLen) { - return; - } - const fragments = trackDetails.fragments; - const fragLen = fragments.length; - const end = trackDetails.edge; - let foundFrag = null; - const fragPrevious = this.fragPrevious; - if (targetBufferTime < end) { - const tolerance = config.maxFragLookUpTolerance; - const lookupTolerance = targetBufferTime > end - tolerance ? 0 : tolerance; - foundFrag = findFragmentByPTS(fragPrevious, fragments, Math.max(fragments[0].start, targetBufferTime), lookupTolerance); - if (!foundFrag && fragPrevious && fragPrevious.start < fragments[0].start) { - foundFrag = fragments[0]; - } - } else { - foundFrag = fragments[fragLen - 1]; - } - if (!foundFrag) { - return; - } - foundFrag = this.mapToInitFragWhenRequired(foundFrag); - if (foundFrag.sn !== 'initSegment') { - // Load earlier fragment in same discontinuity to make up for misaligned playlists and cues that extend beyond end of segment - const curSNIdx = foundFrag.sn - trackDetails.startSN; - const prevFrag = fragments[curSNIdx - 1]; - if (prevFrag && prevFrag.cc === foundFrag.cc && this.fragmentTracker.getState(prevFrag) === FragmentState.NOT_LOADED) { - foundFrag = prevFrag; - } - } - if (this.fragmentTracker.getState(foundFrag) === FragmentState.NOT_LOADED) { - // only load if fragment is not loaded - this.loadFragment(foundFrag, track, targetBufferTime); - } - } - } - loadFragment(frag, level, targetBufferTime) { - if (frag.sn === 'initSegment') { - this._loadInitSegment(frag, level); - } else { - super.loadFragment(frag, level, targetBufferTime); - } - } - get mediaBufferTimeRanges() { - return new BufferableInstance(this.tracksBuffered[this.currentTrackId] || []); - } -} -class BufferableInstance { - constructor(timeranges) { - this.buffered = void 0; - const getRange = (name, index, length) => { - index = index >>> 0; - if (index > length - 1) { - throw new DOMException(`Failed to execute '${name}' on 'TimeRanges': The index provided (${index}) is greater than the maximum bound (${length})`); - } - return timeranges[index][name]; - }; - this.buffered = { - get length() { - return timeranges.length; - }, - end(index) { - return getRange('end', index, timeranges.length); - }, - start(index) { - return getRange('start', index, timeranges.length); - } - }; - } -} - -class SubtitleTrackController extends BasePlaylistController { - constructor(hls) { - super(hls, 'subtitle-track-controller'); - this.media = null; - this.tracks = []; - this.groupIds = null; - this.tracksInGroup = []; - this.trackId = -1; - this.currentTrack = null; - this.selectDefaultTrack = true; - this.queuedDefaultTrack = -1; - this.useTextTrackPolling = false; - this.subtitlePollingInterval = -1; - this._subtitleDisplay = true; - this.asyncPollTrackChange = () => this.pollTrackChange(0); - this.onTextTracksChanged = () => { - if (!this.useTextTrackPolling) { - self.clearInterval(this.subtitlePollingInterval); - } - // Media is undefined when switching streams via loadSource() - if (!this.media || !this.hls.config.renderTextTracksNatively) { - return; - } - let textTrack = null; - const tracks = filterSubtitleTracks(this.media.textTracks); - for (let i = 0; i < tracks.length; i++) { - if (tracks[i].mode === 'hidden') { - // Do not break in case there is a following track with showing. - textTrack = tracks[i]; - } else if (tracks[i].mode === 'showing') { - textTrack = tracks[i]; - break; - } - } - - // Find internal track index for TextTrack - const trackId = this.findTrackForTextTrack(textTrack); - if (this.subtitleTrack !== trackId) { - this.setSubtitleTrack(trackId); - } - }; - this.registerListeners(); - } - destroy() { - this.unregisterListeners(); - this.tracks.length = 0; - this.tracksInGroup.length = 0; - this.currentTrack = null; - // @ts-ignore - this.onTextTracksChanged = this.asyncPollTrackChange = null; - super.destroy(); - } - get subtitleDisplay() { - return this._subtitleDisplay; - } - set subtitleDisplay(value) { - this._subtitleDisplay = value; - if (this.trackId > -1) { - this.toggleTrackModes(); - } - } - registerListeners() { - const { - hls - } = this; - hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this); - hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this); - hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); - hls.on(Events.MANIFEST_PARSED, this.onManifestParsed, this); - hls.on(Events.LEVEL_LOADING, this.onLevelLoading, this); - hls.on(Events.LEVEL_SWITCHING, this.onLevelSwitching, this); - hls.on(Events.SUBTITLE_TRACK_LOADED, this.onSubtitleTrackLoaded, this); - hls.on(Events.ERROR, this.onError, this); - } - unregisterListeners() { - const { - hls - } = this; - hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this); - hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this); - hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); - hls.off(Events.MANIFEST_PARSED, this.onManifestParsed, this); - hls.off(Events.LEVEL_LOADING, this.onLevelLoading, this); - hls.off(Events.LEVEL_SWITCHING, this.onLevelSwitching, this); - hls.off(Events.SUBTITLE_TRACK_LOADED, this.onSubtitleTrackLoaded, this); - hls.off(Events.ERROR, this.onError, this); - } - - // Listen for subtitle track change, then extract the current track ID. - onMediaAttached(event, data) { - this.media = data.media; - if (!this.media) { - return; - } - if (this.queuedDefaultTrack > -1) { - this.subtitleTrack = this.queuedDefaultTrack; - this.queuedDefaultTrack = -1; - } - this.useTextTrackPolling = !(this.media.textTracks && 'onchange' in this.media.textTracks); - if (this.useTextTrackPolling) { - this.pollTrackChange(500); - } else { - this.media.textTracks.addEventListener('change', this.asyncPollTrackChange); - } - } - pollTrackChange(timeout) { - self.clearInterval(this.subtitlePollingInterval); - this.subtitlePollingInterval = self.setInterval(this.onTextTracksChanged, timeout); - } - onMediaDetaching(event, data) { - const media = this.media; - if (!media) { - return; - } - const transferringMedia = !!data.transferMedia; - self.clearInterval(this.subtitlePollingInterval); - if (!this.useTextTrackPolling) { - media.textTracks.removeEventListener('change', this.asyncPollTrackChange); - } - if (this.trackId > -1) { - this.queuedDefaultTrack = this.trackId; - } - - // Disable all subtitle tracks before detachment so when reattached only tracks in that content are enabled. - this.subtitleTrack = -1; - this.media = null; - if (transferringMedia) { - return; - } - const textTracks = filterSubtitleTracks(media.textTracks); - // Clear loaded cues on media detachment from tracks - textTracks.forEach(track => { - clearCurrentCues(track); - }); - } - onManifestLoading() { - this.tracks = []; - this.groupIds = null; - this.tracksInGroup = []; - this.trackId = -1; - this.currentTrack = null; - this.selectDefaultTrack = true; - } - - // Fired whenever a new manifest is loaded. - onManifestParsed(event, data) { - this.tracks = data.subtitleTracks; - } - onSubtitleTrackLoaded(event, data) { - const { - id, - groupId, - details - } = data; - const trackInActiveGroup = this.tracksInGroup[id]; - if (!trackInActiveGroup || trackInActiveGroup.groupId !== groupId) { - this.warn(`Subtitle track with id:${id} and group:${groupId} not found in active group ${trackInActiveGroup == null ? void 0 : trackInActiveGroup.groupId}`); - return; - } - const curDetails = trackInActiveGroup.details; - trackInActiveGroup.details = data.details; - this.log(`Subtitle track ${id} "${trackInActiveGroup.name}" lang:${trackInActiveGroup.lang} group:${groupId} loaded [${details.startSN}-${details.endSN}]`); - if (id === this.trackId) { - this.playlistLoaded(id, data, curDetails); - } - } - onLevelLoading(event, data) { - this.switchLevel(data.level); - } - onLevelSwitching(event, data) { - this.switchLevel(data.level); - } - switchLevel(levelIndex) { - const levelInfo = this.hls.levels[levelIndex]; - if (!levelInfo) { - return; - } - const subtitleGroups = levelInfo.subtitleGroups || null; - const currentGroups = this.groupIds; - let currentTrack = this.currentTrack; - if (!subtitleGroups || (currentGroups == null ? void 0 : currentGroups.length) !== (subtitleGroups == null ? void 0 : subtitleGroups.length) || subtitleGroups != null && subtitleGroups.some(groupId => (currentGroups == null ? void 0 : currentGroups.indexOf(groupId)) === -1)) { - this.groupIds = subtitleGroups; - this.trackId = -1; - this.currentTrack = null; - const subtitleTracks = this.tracks.filter(track => !subtitleGroups || subtitleGroups.indexOf(track.groupId) !== -1); - if (subtitleTracks.length) { - // Disable selectDefaultTrack if there are no default tracks - if (this.selectDefaultTrack && !subtitleTracks.some(track => track.default)) { - this.selectDefaultTrack = false; - } - // track.id should match hls.audioTracks index - subtitleTracks.forEach((track, i) => { - track.id = i; - }); - } else if (!currentTrack && !this.tracksInGroup.length) { - // Do not dispatch SUBTITLE_TRACKS_UPDATED when there were and are no tracks - return; - } - this.tracksInGroup = subtitleTracks; - - // Find preferred track - const subtitlePreference = this.hls.config.subtitlePreference; - if (!currentTrack && subtitlePreference) { - this.selectDefaultTrack = false; - const groupIndex = findMatchingOption(subtitlePreference, subtitleTracks); - if (groupIndex > -1) { - currentTrack = subtitleTracks[groupIndex]; - } else { - const allIndex = findMatchingOption(subtitlePreference, this.tracks); - currentTrack = this.tracks[allIndex]; - } - } - - // Select initial track - let trackId = this.findTrackId(currentTrack); - if (trackId === -1 && currentTrack) { - trackId = this.findTrackId(null); - } - - // Dispatch events and load track if needed - const subtitleTracksUpdated = { - subtitleTracks - }; - this.log(`Updating subtitle tracks, ${subtitleTracks.length} track(s) found in "${subtitleGroups == null ? void 0 : subtitleGroups.join(',')}" group-id`); - this.hls.trigger(Events.SUBTITLE_TRACKS_UPDATED, subtitleTracksUpdated); - if (trackId !== -1 && this.trackId === -1) { - this.setSubtitleTrack(trackId); - } - } else if (this.shouldReloadPlaylist(currentTrack)) { - // Retry playlist loading if no playlist is or has been loaded yet - this.setSubtitleTrack(this.trackId); - } - } - findTrackId(currentTrack) { - const tracks = this.tracksInGroup; - const selectDefault = this.selectDefaultTrack; - for (let i = 0; i < tracks.length; i++) { - const track = tracks[i]; - if (selectDefault && !track.default || !selectDefault && !currentTrack) { - continue; - } - if (!currentTrack || matchesOption(track, currentTrack)) { - return i; - } - } - if (currentTrack) { - for (let i = 0; i < tracks.length; i++) { - const track = tracks[i]; - if (mediaAttributesIdentical(currentTrack.attrs, track.attrs, ['LANGUAGE', 'ASSOC-LANGUAGE', 'CHARACTERISTICS'])) { - return i; - } - } - for (let i = 0; i < tracks.length; i++) { - const track = tracks[i]; - if (mediaAttributesIdentical(currentTrack.attrs, track.attrs, ['LANGUAGE'])) { - return i; - } - } - } - return -1; - } - findTrackForTextTrack(textTrack) { - if (textTrack) { - const tracks = this.tracksInGroup; - for (let i = 0; i < tracks.length; i++) { - const track = tracks[i]; - if (subtitleTrackMatchesTextTrack(track, textTrack)) { - return i; - } - } - } - return -1; - } - onError(event, data) { - if (data.fatal || !data.context) { - return; - } - if (data.context.type === PlaylistContextType.SUBTITLE_TRACK && data.context.id === this.trackId && (!this.groupIds || this.groupIds.indexOf(data.context.groupId) !== -1)) { - this.checkRetry(data); - } - } - get allSubtitleTracks() { - return this.tracks; - } - - /** get alternate subtitle tracks list from playlist **/ - get subtitleTracks() { - return this.tracksInGroup; - } - - /** get/set index of the selected subtitle track (based on index in subtitle track lists) **/ - get subtitleTrack() { - return this.trackId; - } - set subtitleTrack(newId) { - this.selectDefaultTrack = false; - this.setSubtitleTrack(newId); - } - setSubtitleOption(subtitleOption) { - this.hls.config.subtitlePreference = subtitleOption; - if (subtitleOption) { - if (subtitleOption.id === -1) { - this.setSubtitleTrack(-1); - return null; - } - const allSubtitleTracks = this.allSubtitleTracks; - this.selectDefaultTrack = false; - if (allSubtitleTracks.length) { - // First see if current option matches (no switch op) - const currentTrack = this.currentTrack; - if (currentTrack && matchesOption(subtitleOption, currentTrack)) { - return currentTrack; - } - // Find option in current group - const groupIndex = findMatchingOption(subtitleOption, this.tracksInGroup); - if (groupIndex > -1) { - const track = this.tracksInGroup[groupIndex]; - this.setSubtitleTrack(groupIndex); - return track; - } else if (currentTrack) { - // If this is not the initial selection return null - // option should have matched one in active group - return null; - } else { - // Find the option in all tracks for initial selection - const allIndex = findMatchingOption(subtitleOption, allSubtitleTracks); - if (allIndex > -1) { - return allSubtitleTracks[allIndex]; - } - } - } - } - return null; - } - loadPlaylist(hlsUrlParameters) { - super.loadPlaylist(); - const currentTrack = this.currentTrack; - if (this.shouldLoadPlaylist(currentTrack) && currentTrack) { - const id = currentTrack.id; - const groupId = currentTrack.groupId; - let url = currentTrack.url; - if (hlsUrlParameters) { - try { - url = hlsUrlParameters.addDirectives(url); - } catch (error) { - this.warn(`Could not construct new URL with HLS Delivery Directives: ${error}`); - } - } - this.log(`Loading subtitle playlist for id ${id}`); - this.hls.trigger(Events.SUBTITLE_TRACK_LOADING, { - url, - id, - groupId, - deliveryDirectives: hlsUrlParameters || null - }); - } - } - - /** - * Disables the old subtitleTrack and sets current mode on the next subtitleTrack. - * This operates on the DOM textTracks. - * A value of -1 will disable all subtitle tracks. - */ - toggleTrackModes() { - const { - media - } = this; - if (!media) { - return; - } - const textTracks = filterSubtitleTracks(media.textTracks); - const currentTrack = this.currentTrack; - let nextTrack; - if (currentTrack) { - nextTrack = textTracks.filter(textTrack => subtitleTrackMatchesTextTrack(currentTrack, textTrack))[0]; - if (!nextTrack) { - this.warn(`Unable to find subtitle TextTrack with name "${currentTrack.name}" and language "${currentTrack.lang}"`); - } - } - [].slice.call(textTracks).forEach(track => { - if (track.mode !== 'disabled' && track !== nextTrack) { - track.mode = 'disabled'; - } - }); - if (nextTrack) { - const mode = this.subtitleDisplay ? 'showing' : 'hidden'; - if (nextTrack.mode !== mode) { - nextTrack.mode = mode; - } - } - } - - /** - * This method is responsible for validating the subtitle index and periodically reloading if live. - * Dispatches the SUBTITLE_TRACK_SWITCH event, which instructs the subtitle-stream-controller to load the selected track. - */ - setSubtitleTrack(newId) { - const tracks = this.tracksInGroup; - - // setting this.subtitleTrack will trigger internal logic - // if media has not been attached yet, it will fail - // we keep a reference to the default track id - // and we'll set subtitleTrack when onMediaAttached is triggered - if (!this.media) { - this.queuedDefaultTrack = newId; - return; - } - - // exit if track id as already set or invalid - if (newId < -1 || newId >= tracks.length || !isFiniteNumber(newId)) { - this.warn(`Invalid subtitle track id: ${newId}`); - return; - } - - // stopping live reloading timer if any - this.clearTimer(); - this.selectDefaultTrack = false; - const lastTrack = this.currentTrack; - const track = tracks[newId] || null; - this.trackId = newId; - this.currentTrack = track; - this.toggleTrackModes(); - if (!track) { - // switch to -1 - this.hls.trigger(Events.SUBTITLE_TRACK_SWITCH, { - id: newId - }); - return; - } - const trackLoaded = !!track.details && !track.details.live; - if (newId === this.trackId && track === lastTrack && trackLoaded) { - return; - } - this.log(`Switching to subtitle-track ${newId}` + (track ? ` "${track.name}" lang:${track.lang} group:${track.groupId}` : '')); - const { - id, - groupId = '', - name, - type, - url - } = track; - this.hls.trigger(Events.SUBTITLE_TRACK_SWITCH, { - id, - groupId, - name, - type, - url - }); - const hlsUrlParameters = this.switchParams(track.url, lastTrack == null ? void 0 : lastTrack.details, track.details); - this.loadPlaylist(hlsUrlParameters); - } -} - -class BufferOperationQueue { - constructor(sourceBufferReference) { - this.tracks = void 0; - this.queues = { - video: [], - audio: [], - audiovideo: [] - }; - this.tracks = sourceBufferReference; - } - destroy() { - this.tracks = this.queues = null; - } - append(operation, type, pending) { - if (this.queues === null || this.tracks === null) { - return; - } - const queue = this.queues[type]; - queue.push(operation); - if (queue.length === 1 && !pending) { - this.executeNext(type); - } - } - appendBlocker(type) { - return new Promise(resolve => { - const operation = { - label: 'async-blocker', - execute: resolve, - onStart: () => {}, - onComplete: () => {}, - onError: () => {} - }; - this.append(operation, type); - }); - } - prependBlocker(type) { - return new Promise(resolve => { - if (this.queues) { - const operation = { - label: 'async-blocker-prepend', - execute: resolve, - onStart: () => {}, - onComplete: () => {}, - onError: () => {} - }; - this.queues[type].unshift(operation); - } - }); - } - removeBlockers() { - if (this.queues === null) { - return; - } - [this.queues.video, this.queues.audio, this.queues.audiovideo].forEach(queue => { - var _queue$; - const label = (_queue$ = queue[0]) == null ? void 0 : _queue$.label; - if (label === 'async-blocker' || label === 'async-blocker-prepend') { - queue[0].execute(); - queue.splice(0, 1); - } - }); - } - unblockAudio(op) { - if (this.queues === null) { - return; - } - const queue = this.queues.audio; - if (queue[0] === op) { - this.shiftAndExecuteNext('audio'); - } - } - executeNext(type) { - if (this.queues === null || this.tracks === null) { - return; - } - const queue = this.queues[type]; - if (queue.length) { - const operation = queue[0]; - try { - // Operations are expected to result in an 'updateend' event being fired. If not, the queue will lock. Operations - // which do not end with this event must call _onSBUpdateEnd manually - operation.execute(); - } catch (error) { - var _this$tracks$type; - operation.onError(error); - if (this.queues === null || this.tracks === null) { - return; - } - - // Only shift the current operation off, otherwise the updateend handler will do this for us - const sb = (_this$tracks$type = this.tracks[type]) == null ? void 0 : _this$tracks$type.buffer; - if (!(sb != null && sb.updating)) { - this.shiftAndExecuteNext(type); - } - } - } - } - shiftAndExecuteNext(type) { - if (this.queues === null) { - return; - } - this.queues[type].shift(); - this.executeNext(type); - } - current(type) { - var _this$queues; - return ((_this$queues = this.queues) == null ? void 0 : _this$queues[type][0]) || null; - } - toString() { - const { - queues, - tracks - } = this; - if (queues === null || tracks === null) { - return `<destroyed>`; - } - return ` -${this.list('video')} -${this.list('audio')} -${this.list('audiovideo')}}`; - } - list(type) { - var _this$queues2, _this$tracks; - return (_this$queues2 = this.queues) != null && _this$queues2[type] || (_this$tracks = this.tracks) != null && _this$tracks[type] ? `${type}: (${this.listSbInfo(type)}) ${this.listOps(type)}` : ''; - } - listSbInfo(type) { - var _this$tracks2; - const track = (_this$tracks2 = this.tracks) == null ? void 0 : _this$tracks2[type]; - const sb = track == null ? void 0 : track.buffer; - if (!sb) { - return 'none'; - } - return `SourceBuffer${sb.updating ? ' updating' : ''}${track.ended ? ' ended' : ''}${track.ending ? ' ending' : ''}`; - } - listOps(type) { - var _this$queues3; - return ((_this$queues3 = this.queues) == null ? void 0 : _this$queues3[type].map(op => op.label).join(', ')) || ''; - } -} - -const VIDEO_CODEC_PROFILE_REPLACE = /(avc[1234]|hvc1|hev1|dvh[1e]|vp09|av01)(?:\.[^.,]+)+/; -const TRACK_REMOVED_ERROR_NAME = 'HlsJsTrackRemovedError'; -class HlsJsTrackRemovedError extends Error { - constructor(message) { - super(message); - this.name = TRACK_REMOVED_ERROR_NAME; - } -} -class BufferController extends Logger { - constructor(hls, fragmentTracker) { - super('buffer-controller', hls.logger); - this.hls = void 0; - this.fragmentTracker = void 0; - // The level details used to determine duration, target-duration and live - this.details = null; - // cache the self generated object url to detect hijack of video tag - this._objectUrl = null; - // A queue of buffer operations which require the SourceBuffer to not be updating upon execution - this.operationQueue = null; - // The total number track codecs expected before any sourceBuffers are created (2: audio and video or 1: audiovideo | audio | video) - this.bufferCodecEventsTotal = 0; - // A reference to the attached media element - this.media = null; - // A reference to the active media source - this.mediaSource = null; - // Last MP3 audio chunk appended - this.lastMpegAudioChunk = null; - // Audio fragment blocked from appending until corresponding video appends or context changes - this.blockedAudioAppend = null; - // Keep track of video append position for unblocking audio - this.lastVideoAppendEnd = 0; - // Whether or not to use ManagedMediaSource API and append source element to media element. - this.appendSource = void 0; - // Transferred MediaSource information used to detmerine if duration end endstream may be appended - this.transferData = void 0; - // Directives used to override default MediaSource handling - this.overrides = void 0; - // Error counters - this.appendErrors = { - audio: 0, - video: 0, - audiovideo: 0 - }; - // Record of required or created buffers by type. SourceBuffer is stored in Track.buffer once created. - this.tracks = {}; - // Array of SourceBuffer type and SourceBuffer (or null). One entry per TrackSet in this.tracks. - this.sourceBuffers = [[null, null], [null, null]]; - this._onEndStreaming = event => { - var _this$mediaSource; - if (!this.hls) { - return; - } - if (((_this$mediaSource = this.mediaSource) == null ? void 0 : _this$mediaSource.readyState) !== 'open') { - return; - } - this.hls.pauseBuffering(); - }; - this._onStartStreaming = event => { - if (!this.hls) { - return; - } - this.hls.resumeBuffering(); - }; - // Keep as arrow functions so that we can directly reference these functions directly as event listeners - this._onMediaSourceOpen = e => { - const { - media, - mediaSource - } = this; - if (e) { - this.log('Media source opened'); - } - if (!media || !mediaSource) { - return; - } - media.removeEventListener('emptied', this._onMediaEmptied); - this.updateDuration(); - this.hls.trigger(Events.MEDIA_ATTACHED, { - media, - mediaSource: mediaSource - }); - - // once received, don't listen anymore to sourceopen event - mediaSource.removeEventListener('sourceopen', this._onMediaSourceOpen); - if (this.mediaSource !== null) { - this.checkPendingTracks(); - } - }; - this._onMediaSourceClose = () => { - this.log('Media source closed'); - }; - this._onMediaSourceEnded = () => { - this.log('Media source ended'); - }; - this._onMediaEmptied = () => { - const { - mediaSrc, - _objectUrl - } = this; - if (mediaSrc !== _objectUrl) { - this.error(`Media element src was set while attaching MediaSource (${_objectUrl} > ${mediaSrc})`); - } - }; - this.hls = hls; - this.fragmentTracker = fragmentTracker; - this.appendSource = isManagedMediaSource(getMediaSource(hls.config.preferManagedMediaSource)); - this.initTracks(); - this.registerListeners(); - } - hasSourceTypes() { - return Object.keys(this.tracks).length > 0; - } - destroy() { - this.unregisterListeners(); - this.details = null; - this.lastMpegAudioChunk = this.blockedAudioAppend = null; - this.transferData = this.overrides = undefined; - if (this.operationQueue) { - this.operationQueue.destroy(); - this.operationQueue = null; - } - // @ts-ignore - this.hls = this.fragmentTracker = null; - // @ts-ignore - this._onMediaSourceOpen = this._onMediaSourceClose = null; - // @ts-ignore - this._onMediaSourceEnded = null; - // @ts-ignore - this._onStartStreaming = this._onEndStreaming = null; - } - registerListeners() { - const { - hls - } = this; - hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); - hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this); - hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); - hls.on(Events.MANIFEST_PARSED, this.onManifestParsed, this); - hls.on(Events.BUFFER_RESET, this.onBufferReset, this); - hls.on(Events.BUFFER_APPENDING, this.onBufferAppending, this); - hls.on(Events.BUFFER_CODECS, this.onBufferCodecs, this); - hls.on(Events.BUFFER_EOS, this.onBufferEos, this); - hls.on(Events.BUFFER_FLUSHING, this.onBufferFlushing, this); - hls.on(Events.LEVEL_UPDATED, this.onLevelUpdated, this); - hls.on(Events.FRAG_PARSED, this.onFragParsed, this); - hls.on(Events.FRAG_CHANGED, this.onFragChanged, this); - hls.on(Events.ERROR, this.onError, this); - } - unregisterListeners() { - const { - hls - } = this; - hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); - hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this); - hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); - hls.off(Events.MANIFEST_PARSED, this.onManifestParsed, this); - hls.off(Events.BUFFER_RESET, this.onBufferReset, this); - hls.off(Events.BUFFER_APPENDING, this.onBufferAppending, this); - hls.off(Events.BUFFER_CODECS, this.onBufferCodecs, this); - hls.off(Events.BUFFER_EOS, this.onBufferEos, this); - hls.off(Events.BUFFER_FLUSHING, this.onBufferFlushing, this); - hls.off(Events.LEVEL_UPDATED, this.onLevelUpdated, this); - hls.off(Events.FRAG_PARSED, this.onFragParsed, this); - hls.off(Events.FRAG_CHANGED, this.onFragChanged, this); - hls.off(Events.ERROR, this.onError, this); - } - transferMedia() { - const { - media, - mediaSource - } = this; - if (!media) { - return null; - } - const tracks = {}; - if (this.operationQueue) { - const updating = this.isUpdating(); - if (!updating) { - this.operationQueue.removeBlockers(); - } - const queued = this.isQueued(); - if (updating || queued) { - this.warn(`Transfering MediaSource with${queued ? ' operations in queue' : ''}${updating ? ' updating SourceBuffer(s)' : ''} ${this.operationQueue}`); - } - this.operationQueue.destroy(); - } - const transferData = this.transferData; - if (!this.sourceBufferCount && transferData && transferData.mediaSource === mediaSource) { - _extends(tracks, transferData.tracks); - } else { - this.sourceBuffers.forEach(tuple => { - const [type] = tuple; - if (type) { - tracks[type] = _extends({}, this.tracks[type]); - this.removeBuffer(type); - } - tuple[0] = tuple[1] = null; - }); - } - return { - media, - mediaSource, - tracks - }; - } - initTracks() { - const tracks = {}; - this.sourceBuffers = [[null, null], [null, null]]; - this.tracks = tracks; - this.resetQueue(); - this.resetAppendErrors(); - this.lastMpegAudioChunk = this.blockedAudioAppend = null; - this.lastVideoAppendEnd = 0; - } - onManifestLoading() { - this.bufferCodecEventsTotal = 0; - this.details = null; - } - onManifestParsed(event, data) { - var _this$transferData; - // in case of alt audio 2 BUFFER_CODECS events will be triggered, one per stream controller - // sourcebuffers will be created all at once when the expected nb of tracks will be reached - // in case alt audio is not used, only one BUFFER_CODEC event will be fired from main stream controller - // it will contain the expected nb of source buffers, no need to compute it - let codecEvents = 2; - if (data.audio && !data.video || !data.altAudio || !true) { - codecEvents = 1; - } - this.bufferCodecEventsTotal = codecEvents; - this.log(`${codecEvents} bufferCodec event(s) expected.`); - if ((_this$transferData = this.transferData) != null && _this$transferData.mediaSource && this.sourceBufferCount && codecEvents) { - this.bufferCreated(); - } - } - onMediaAttaching(event, data) { - const media = this.media = data.media; - const MediaSource = getMediaSource(this.appendSource); - this.transferData = this.overrides = undefined; - if (media && MediaSource) { - const transferringMedia = !!data.mediaSource; - if (transferringMedia || data.overrides) { - this.transferData = data; - this.overrides = data.overrides; - } - const ms = this.mediaSource = data.mediaSource || new MediaSource(); - this.assignMediaSource(ms); - if (transferringMedia) { - this._objectUrl = media.src; - this.attachTransferred(); - } else { - // cache the locally generated object url - const objectUrl = this._objectUrl = self.URL.createObjectURL(ms); - // link video and media Source - if (this.appendSource) { - try { - media.removeAttribute('src'); - // ManagedMediaSource will not open without disableRemotePlayback set to false or source alternatives - const MMS = self.ManagedMediaSource; - media.disableRemotePlayback = media.disableRemotePlayback || MMS && ms instanceof MMS; - removeSourceChildren(media); - addSource(media, objectUrl); - media.load(); - } catch (error) { - media.src = objectUrl; - } - } else { - media.src = objectUrl; - } - } - media.addEventListener('emptied', this._onMediaEmptied); - } - } - assignMediaSource(ms) { - var _this$transferData2, _ms$constructor; - this.log(`${((_this$transferData2 = this.transferData) == null ? void 0 : _this$transferData2.mediaSource) === ms ? 'transferred' : 'created'} media source: ${(_ms$constructor = ms.constructor) == null ? void 0 : _ms$constructor.name}`); - // MediaSource listeners are arrow functions with a lexical scope, and do not need to be bound - ms.addEventListener('sourceopen', this._onMediaSourceOpen); - ms.addEventListener('sourceended', this._onMediaSourceEnded); - ms.addEventListener('sourceclose', this._onMediaSourceClose); - if (this.appendSource) { - ms.addEventListener('startstreaming', this._onStartStreaming); - ms.addEventListener('endstreaming', this._onEndStreaming); - } - } - attachTransferred() { - const media = this.media; - const data = this.transferData; - if (!data || !media) { - return; - } - const requiredTracks = this.tracks; - const transferredTracks = data.tracks; - const trackNames = transferredTracks ? Object.keys(transferredTracks) : null; - const trackCount = trackNames ? trackNames.length : 0; - const mediaSourceOpenCallback = () => { - if (this.media) { - var _this$mediaSource2; - const readyState = (_this$mediaSource2 = this.mediaSource) == null ? void 0 : _this$mediaSource2.readyState; - if (readyState === 'open' || readyState === 'ended') { - this._onMediaSourceOpen(); - } - } - }; - if (transferredTracks && trackNames && trackCount) { - if (!this.tracksReady) { - // Wait for CODECS event(s) - this.hls.config.startFragPrefetch = true; - this.log(`attachTransferred: waiting for SourceBuffer track info`); - return; - } - this.log(`attachTransferred: (bufferCodecEventsTotal ${this.bufferCodecEventsTotal}) -required tracks: ${JSON.stringify(requiredTracks, (key, value) => key === 'initSegment' ? undefined : value)}; -transfer tracks: ${JSON.stringify(transferredTracks, (key, value) => key === 'initSegment' ? undefined : value)}}`); - if (!isCompatibleTrackChange(transferredTracks, requiredTracks)) { - // destroy attaching media source - data.mediaSource = null; - data.tracks = undefined; - const currentTime = media.currentTime; - const details = this.details; - const startTime = Math.max(currentTime, (details == null ? void 0 : details.fragments[0].start) || 0); - if (startTime - currentTime > 1) { - this.log(`attachTransferred: waiting for playback to reach new tracks start time ${currentTime} -> ${startTime}`); - return; - } - this.warn(`attachTransferred: resetting MediaSource for incompatible tracks ("${Object.keys(transferredTracks)}"->"${Object.keys(requiredTracks)}") start time: ${startTime} currentTime: ${currentTime}`); - this.onMediaDetaching(Events.MEDIA_DETACHING, {}); - this.onMediaAttaching(Events.MEDIA_ATTACHING, data); - media.currentTime = startTime; - return; - } - this.transferData = undefined; - trackNames.forEach(trackName => { - const type = trackName; - const track = transferredTracks[type]; - if (track) { - const sb = track.buffer; - if (sb) { - // Purge fragment tracker of ejected segments for existing buffer - const fragmentTracker = this.fragmentTracker; - const playlistType = track.id; - if (fragmentTracker.hasFragments(playlistType) || fragmentTracker.hasParts(playlistType)) { - const bufferedTimeRanges = BufferHelper.getBuffered(sb); - fragmentTracker.detectEvictedFragments(type, bufferedTimeRanges, playlistType, null, true); - } - // Transfer SourceBuffer - const sbIndex = sourceBufferNameToIndex(type); - const sbTuple = [type, sb]; - this.sourceBuffers[sbIndex] = sbTuple; - if (sb.updating && this.operationQueue) { - this.operationQueue.prependBlocker(type); - } - this.trackSourceBuffer(type, track); - } - } - }); - mediaSourceOpenCallback(); - this.bufferCreated(); - } else { - this.log(`attachTransferred: MediaSource w/o SourceBuffers`); - mediaSourceOpenCallback(); - } - } - onMediaDetaching(event, data) { - const transferringMedia = !!data.transferMedia; - this.transferData = this.overrides = undefined; - const { - media, - mediaSource, - _objectUrl - } = this; - if (mediaSource) { - this.log(`media source ${transferringMedia ? 'transferring' : 'detaching'}`); - if (transferringMedia) { - // Detach SourceBuffers without removing from MediaSource - // and leave `tracks` (required SourceBuffers configuration) - this.sourceBuffers.forEach(([type]) => { - if (type) { - this.removeBuffer(type); - } - }); - this.resetQueue(); - } else { - if (mediaSource.readyState === 'open') { - try { - const sourceBuffers = mediaSource.sourceBuffers; - for (let i = sourceBuffers.length; i--;) { - sourceBuffers[i].abort(); - mediaSource.removeSourceBuffer(sourceBuffers[i]); - } - // endOfStream could trigger exception if any sourcebuffer is in updating state - // we don't really care about checking sourcebuffer state here, - // as we are anyway detaching the MediaSource - // let's just avoid this exception to propagate - mediaSource.endOfStream(); - } catch (err) { - this.warn(`onMediaDetaching: ${err.message} while calling endOfStream`); - } - } - // Clean up the SourceBuffers by invoking onBufferReset - if (this.sourceBufferCount) { - this.onBufferReset(); - } - } - mediaSource.removeEventListener('sourceopen', this._onMediaSourceOpen); - mediaSource.removeEventListener('sourceended', this._onMediaSourceEnded); - mediaSource.removeEventListener('sourceclose', this._onMediaSourceClose); - if (this.appendSource) { - mediaSource.removeEventListener('startstreaming', this._onStartStreaming); - mediaSource.removeEventListener('endstreaming', this._onEndStreaming); - } - this.mediaSource = null; - this._objectUrl = null; - } - - // 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 (!transferringMedia) { - if (_objectUrl) { - self.URL.revokeObjectURL(_objectUrl); - } - - // clean up video tag src only if it's our own url. some external libraries might - // hijack the video tag and change its 'src' without destroying the Hls instance first - if (this.mediaSrc === _objectUrl) { - media.removeAttribute('src'); - if (this.appendSource) { - removeSourceChildren(media); - } - media.load(); - } else { - this.warn('media|source.src was changed by a third party - skip cleanup'); - } - } - this.media = null; - } - this.hls.trigger(Events.MEDIA_DETACHED, data); - } - onBufferReset() { - this.sourceBuffers.forEach(([type]) => { - if (type) { - this.resetBuffer(type); - } - }); - this.initTracks(); - } - resetBuffer(type) { - var _this$tracks$type; - const sb = (_this$tracks$type = this.tracks[type]) == null ? void 0 : _this$tracks$type.buffer; - this.removeBuffer(type); - if (sb) { - try { - var _this$mediaSource3; - if ((_this$mediaSource3 = this.mediaSource) != null && _this$mediaSource3.sourceBuffers.length) { - this.mediaSource.removeSourceBuffer(sb); - } - } catch (err) { - this.warn(`onBufferReset ${type}`, err); - } - } - delete this.tracks[type]; - } - removeBuffer(type) { - this.removeBufferListeners(type); - this.sourceBuffers[sourceBufferNameToIndex(type)] = [null, null]; - const track = this.tracks[type]; - if (track) { - track.buffer = undefined; - } - } - resetQueue() { - if (this.operationQueue) { - this.operationQueue.destroy(); - } - this.operationQueue = new BufferOperationQueue(this.tracks); - } - onBufferCodecs(event, data) { - const tracks = this.tracks; - const trackNames = Object.keys(data); - this.log(`BUFFER_CODECS: "${trackNames}" (current SB count ${this.sourceBufferCount})`); - const unmuxedToMuxed = 'audiovideo' in data && (tracks.audio || tracks.video) || tracks.audiovideo && ('audio' in data || 'video' in data); - const muxedToUnmuxed = !unmuxedToMuxed && this.sourceBufferCount && this.media && trackNames.some(sbName => !tracks[sbName]); - if (unmuxedToMuxed || muxedToUnmuxed) { - this.warn(`Unsupported transition between "${Object.keys(tracks)}" and "${trackNames}" SourceBuffers`); - // Do not add incompatible track ('audiovideo' <-> 'video'/'audio'). - // Allow following onBufferAppending handle to trigger BUFFER_APPEND_ERROR. - // This will either be resolved by level switch or could be handled with recoverMediaError(). - return; - } - trackNames.forEach(trackName => { - var _this$transferData3, _this$transferData3$t, _trackCodec; - const parsedTrack = data[trackName]; - const { - id, - codec, - levelCodec, - container, - metadata - } = parsedTrack; - let track = tracks[trackName]; - const transferredTrack = (_this$transferData3 = this.transferData) == null ? void 0 : (_this$transferData3$t = _this$transferData3.tracks) == null ? void 0 : _this$transferData3$t[trackName]; - const sbTrack = transferredTrack != null && transferredTrack.buffer ? transferredTrack : track; - const sbCodec = (sbTrack == null ? void 0 : sbTrack.pendingCodec) || (sbTrack == null ? void 0 : sbTrack.codec); - const trackLevelCodec = sbTrack == null ? void 0 : sbTrack.levelCodec; - const forceChangeType = !sbTrack || !!this.hls.config.assetPlayerId; - if (!track) { - track = tracks[trackName] = { - buffer: undefined, - listeners: [], - codec, - container, - levelCodec, - metadata, - id - }; - } - // check if SourceBuffer codec needs to change - const currentCodecFull = pickMostCompleteCodecName(sbCodec, trackLevelCodec); - const currentCodec = currentCodecFull == null ? void 0 : currentCodecFull.replace(VIDEO_CODEC_PROFILE_REPLACE, '$1'); - let trackCodec = pickMostCompleteCodecName(codec, levelCodec); - const nextCodec = (_trackCodec = trackCodec) == null ? void 0 : _trackCodec.replace(VIDEO_CODEC_PROFILE_REPLACE, '$1'); - if (trackCodec && (currentCodec !== nextCodec || forceChangeType)) { - if (trackName.slice(0, 5) === 'audio') { - trackCodec = getCodecCompatibleName(trackCodec, this.appendSource); - } - this.log(`switching codec ${sbCodec} to ${trackCodec}`); - if (trackCodec !== (track.pendingCodec || track.codec)) { - track.pendingCodec = trackCodec; - } - track.container = container; - this.appendChangeType(trackName, container, trackCodec); - } - }); - if (this.tracksReady || this.sourceBufferCount) { - data.tracks = this.sourceBufferTracks; - } - - // if sourcebuffers already created, do nothing ... - if (this.sourceBufferCount) { - return; - } - if (this.mediaSource !== null && this.mediaSource.readyState === 'open') { - this.checkPendingTracks(); - } - } - get sourceBufferTracks() { - return Object.keys(this.tracks).reduce((baseTracks, type) => { - const track = this.tracks[type]; - baseTracks[type] = { - id: track.id, - container: track.container, - codec: track.codec, - levelCodec: track.levelCodec - }; - return baseTracks; - }, {}); - } - appendChangeType(type, container, codec) { - const mimeType = `${container};codecs=${codec}`; - const operation = { - label: `change-type=${mimeType}`, - execute: () => { - const track = this.tracks[type]; - if (track) { - const sb = track.buffer; - if (sb != null && sb.changeType) { - this.log(`changing ${type} sourceBuffer type to ${mimeType}`); - sb.changeType(mimeType); - track.codec = codec; - track.container = container; - } - } - this.shiftAndExecuteNext(type); - }, - onStart: () => {}, - onComplete: () => {}, - onError: error => { - this.warn(`Failed to change ${type} SourceBuffer type`, error); - } - }; - this.append(operation, type, this.isPending(this.tracks[type])); - } - blockAudio(partOrFrag) { - var _this$fragmentTracker; - const pStart = partOrFrag.start; - const pTime = pStart + partOrFrag.duration * 0.05; - const atGap = ((_this$fragmentTracker = this.fragmentTracker.getAppendedFrag(pStart, PlaylistLevelType.MAIN)) == null ? void 0 : _this$fragmentTracker.gap) === true; - if (atGap) { - return; - } - const op = { - label: 'block-audio', - execute: () => { - var _this$fragmentTracker2; - const videoTrack = this.tracks.video; - if (this.lastVideoAppendEnd > pTime || videoTrack != null && videoTrack.buffer && BufferHelper.isBuffered(videoTrack.buffer, pTime) || ((_this$fragmentTracker2 = this.fragmentTracker.getAppendedFrag(pTime, PlaylistLevelType.MAIN)) == null ? void 0 : _this$fragmentTracker2.gap) === true) { - this.blockedAudioAppend = null; - this.shiftAndExecuteNext('audio'); - } - }, - onStart: () => {}, - onComplete: () => {}, - onError: error => { - this.warn('Error executing block-audio operation', error); - } - }; - this.blockedAudioAppend = { - op, - frag: partOrFrag - }; - this.append(op, 'audio', true); - } - unblockAudio() { - const { - blockedAudioAppend, - operationQueue - } = this; - if (blockedAudioAppend && operationQueue) { - this.blockedAudioAppend = null; - operationQueue.unblockAudio(blockedAudioAppend.op); - } - } - onBufferAppending(event, eventData) { - const { - tracks - } = this; - const { - data, - type, - parent, - frag, - part, - chunkMeta - } = eventData; - const chunkStats = chunkMeta.buffering[type]; - const sn = frag.sn; - const bufferAppendingStart = self.performance.now(); - chunkStats.start = bufferAppendingStart; - const fragBuffering = frag.stats.buffering; - const partBuffering = part ? part.stats.buffering : null; - if (fragBuffering.start === 0) { - fragBuffering.start = bufferAppendingStart; - } - if (partBuffering && partBuffering.start === 0) { - partBuffering.start = bufferAppendingStart; - } - - // TODO: Only update timestampOffset when audio/mpeg fragment or part is not contiguous with previously appended - // Adjusting `SourceBuffer.timestampOffset` (desired point in the timeline where the next frames should be appended) - // in Chrome browser when we detect MPEG audio container and time delta between level PTS and `SourceBuffer.timestampOffset` - // 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; - let checkTimestampOffset = false; - if (type === 'audio' && (audioTrack == null ? void 0 : audioTrack.container) === 'audio/mpeg') { - checkTimestampOffset = !this.lastMpegAudioChunk || chunkMeta.id === 1 || this.lastMpegAudioChunk.sn !== chunkMeta.sn; - this.lastMpegAudioChunk = chunkMeta; - } - - // Block audio append until overlapping video append - const videoTrack = this.tracks.video; - const videoSb = videoTrack == null ? void 0 : videoTrack.buffer; - if (videoSb && sn !== 'initSegment') { - const partOrFrag = part || frag; - const blockedAudioAppend = this.blockedAudioAppend; - if (type === 'audio' && parent !== 'main' && !this.blockedAudioAppend) { - const pStart = partOrFrag.start; - const pTime = pStart + partOrFrag.duration * 0.05; - const vbuffered = videoSb.buffered; - const vappending = this.currentOp('video'); - if (!vbuffered.length && !vappending) { - // wait for video before appending audio - this.blockAudio(partOrFrag); - } else if (!vappending && !BufferHelper.isBuffered(videoSb, pTime) && this.lastVideoAppendEnd < pTime) { - // audio is ahead of video - this.blockAudio(partOrFrag); - } - } else if (type === 'video') { - const videoAppendEnd = partOrFrag.end; - if (blockedAudioAppend) { - const audioStart = blockedAudioAppend.frag.start; - if (videoAppendEnd > audioStart || videoAppendEnd < this.lastVideoAppendEnd || BufferHelper.isBuffered(videoSb, audioStart)) { - this.unblockAudio(); - } - } - this.lastVideoAppendEnd = videoAppendEnd; - } - } - const fragStart = (part || frag).start; - const operation = { - label: `append-${type}`, - execute: () => { - chunkStats.executeStart = self.performance.now(); - if (checkTimestampOffset) { - const track = this.tracks[type]; - if (track) { - const sb = track.buffer; - if (sb) { - const delta = fragStart - sb.timestampOffset; - if (Math.abs(delta) >= 0.1) { - this.log(`Updating audio SourceBuffer timestampOffset to ${fragStart} (delta: ${delta}) sn: ${sn})`); - sb.timestampOffset = fragStart; - } - } - } - } - this.appendExecutor(data, type); - }, - onStart: () => { - // logger.debug(`[buffer-controller]: ${type} SourceBuffer updatestart`); - }, - onComplete: () => { - // logger.debug(`[buffer-controller]: ${type} SourceBuffer updateend`); - const end = self.performance.now(); - chunkStats.executeEnd = chunkStats.end = end; - if (fragBuffering.first === 0) { - fragBuffering.first = end; - } - if (partBuffering && partBuffering.first === 0) { - partBuffering.first = end; - } - const timeRanges = {}; - this.sourceBuffers.forEach(([type, sb]) => { - if (type) { - timeRanges[type] = BufferHelper.getBuffered(sb); - } - }); - this.appendErrors[type] = 0; - if (type === 'audio' || type === 'video') { - this.appendErrors.audiovideo = 0; - } else { - this.appendErrors.audio = 0; - this.appendErrors.video = 0; - } - this.hls.trigger(Events.BUFFER_APPENDED, { - type, - frag, - part, - chunkMeta, - parent: frag.type, - timeRanges - }); - }, - onError: error => { - var _this$mediaSource4, _this$media; - // in case any error occured while appending, put back segment in segments table - const event = { - type: ErrorTypes.MEDIA_ERROR, - parent: frag.type, - details: ErrorDetails.BUFFER_APPEND_ERROR, - sourceBufferName: type, - frag, - part, - chunkMeta, - error, - err: error, - fatal: false - }; - if (error.code === DOMException.QUOTA_EXCEEDED_ERR) { - // QuotaExceededError: http://www.w3.org/TR/html5/infrastructure.html#quotaexceedederror - // let's stop appending any segments, and report BUFFER_FULL_ERROR error - event.details = ErrorDetails.BUFFER_FULL_ERROR; - } else if (error.code === DOMException.INVALID_STATE_ERR && ((_this$mediaSource4 = this.mediaSource) == null ? void 0 : _this$mediaSource4.readyState) === 'open' && !((_this$media = this.media) != null && _this$media.error)) { - // Allow retry for "Failed to execute 'appendBuffer' on 'SourceBuffer': This SourceBuffer is still processing" errors - event.errorAction = createDoNothingErrorAction(true); - } else if (error.name === TRACK_REMOVED_ERROR_NAME) { - // Do nothing if sourceBuffers were removed (media is detached and append was not aborted) - if (this.sourceBufferCount === 0) { - event.errorAction = createDoNothingErrorAction(true); - } else { - ++this.appendErrors[type]; - } - } else { - const appendErrorCount = ++this.appendErrors[type]; - /* with UHD content, we could get loop of quota exceeded error until - browser is able to evict some data from sourcebuffer. Retrying can help recover. - */ - this.warn(`Failed ${appendErrorCount}/${this.hls.config.appendErrorMaxRetry} times to append segment in "${type}" sourceBuffer`); - if (appendErrorCount >= this.hls.config.appendErrorMaxRetry) { - event.fatal = true; - } - } - this.hls.trigger(Events.ERROR, event); - } - }; - this.append(operation, type, this.isPending(this.tracks[type])); - } - getFlushOp(type, start, end) { - this.log(`queuing "${type}" remove ${start}-${end}`); - return { - label: 'remove', - execute: () => { - this.removeExecutor(type, start, end); - }, - onStart: () => { - // logger.debug(`[buffer-controller]: Started flushing ${data.startOffset} -> ${data.endOffset} for ${type} Source Buffer`); - }, - onComplete: () => { - // logger.debug(`[buffer-controller]: Finished flushing ${data.startOffset} -> ${data.endOffset} for ${type} Source Buffer`); - this.hls.trigger(Events.BUFFER_FLUSHED, { - type - }); - }, - onError: error => { - this.warn(`Failed to remove ${start}-${end} from "${type}" SourceBuffer`, error); - } - }; - } - onBufferFlushing(event, data) { - const { - type, - startOffset, - endOffset - } = data; - if (type) { - this.append(this.getFlushOp(type, startOffset, endOffset), type); - } else { - this.sourceBuffers.forEach(([type]) => { - if (type) { - this.append(this.getFlushOp(type, startOffset, endOffset), type); - } - }); - } - } - onFragParsed(event, data) { - const { - frag, - part - } = data; - const buffersAppendedTo = []; - const elementaryStreams = part ? part.elementaryStreams : frag.elementaryStreams; - if (elementaryStreams[ElementaryStreamTypes.AUDIOVIDEO]) { - buffersAppendedTo.push('audiovideo'); - } else { - if (elementaryStreams[ElementaryStreamTypes.AUDIO]) { - buffersAppendedTo.push('audio'); - } - if (elementaryStreams[ElementaryStreamTypes.VIDEO]) { - buffersAppendedTo.push('video'); - } - } - const onUnblocked = () => { - const now = self.performance.now(); - frag.stats.buffering.end = now; - if (part) { - part.stats.buffering.end = now; - } - const stats = part ? part.stats : frag.stats; - this.hls.trigger(Events.FRAG_BUFFERED, { - frag, - part, - stats, - id: frag.type - }); - }; - if (buffersAppendedTo.length === 0) { - this.warn(`Fragments must have at least one ElementaryStreamType set. type: ${frag.type} level: ${frag.level} sn: ${frag.sn}`); - } - this.blockBuffers(onUnblocked, buffersAppendedTo); - } - onFragChanged(event, data) { - this.trimBuffers(); - } - get bufferedToEnd() { - return this.sourceBufferCount > 0 && !this.sourceBuffers.some(([type]) => { - var _this$tracks$type2, _this$tracks$type3; - return type && (!((_this$tracks$type2 = this.tracks[type]) != null && _this$tracks$type2.ended) || ((_this$tracks$type3 = this.tracks[type]) == null ? void 0 : _this$tracks$type3.ending)); - }); - } - - // on BUFFER_EOS mark matching sourcebuffer(s) as "ending" and "ended" and queue endOfStream after remaining operations(s) - // an undefined data.type will mark all buffers as EOS. - onBufferEos(event, data) { - var _this$overrides; - this.sourceBuffers.forEach(([type]) => { - if (type) { - const track = this.tracks[type]; - if (!data.type || data.type === type) { - track.ending = true; - if (!track.ended) { - track.ended = true; - this.log(`${type} buffer reached EOS`); - } - } - } - }); - const allowEndOfStream = ((_this$overrides = this.overrides) == null ? void 0 : _this$overrides.endOfStream) !== false; - const allTracksEnding = this.sourceBufferCount > 0 && !this.sourceBuffers.some(([type]) => { - var _this$tracks$type4; - return type && !((_this$tracks$type4 = this.tracks[type]) != null && _this$tracks$type4.ended); - }); - if (allTracksEnding) { - this.log(`Queueing EOS`); - this.blockUntilOpen(() => { - this.sourceBuffers.forEach(([type]) => { - if (type !== null) { - const track = this.tracks[type]; - if (track) { - track.ending = false; - } - } - }); - if (allowEndOfStream) { - const { - mediaSource - } = this; - if (!mediaSource || mediaSource.readyState !== 'open') { - if (mediaSource) { - this.log(`Could not call mediaSource.endOfStream(). mediaSource.readyState: ${mediaSource.readyState}`); - } - return; - } - this.log(`Calling mediaSource.endOfStream()`); - // Allow this to throw and be caught by the enqueueing function - mediaSource.endOfStream(); - } - this.hls.trigger(Events.BUFFERED_TO_END, undefined); - }); - } - } - onLevelUpdated(event, { - details - }) { - if (!details.fragments.length) { - return; - } - this.details = details; - this.updateDuration(); - } - updateDuration() { - const durationAndRange = this.getDurationAndRange(); - if (!durationAndRange) { - return; - } - this.blockUntilOpen(() => this.updateMediaSource(durationAndRange)); - } - onError(event, data) { - if (data.details === ErrorDetails.BUFFER_APPEND_ERROR && data.frag) { - var _data$errorAction; - const nextAutoLevel = (_data$errorAction = data.errorAction) == null ? void 0 : _data$errorAction.nextAutoLevel; - if (isFiniteNumber(nextAutoLevel) && nextAutoLevel !== data.frag.level) { - this.resetAppendErrors(); - } - } - } - resetAppendErrors() { - this.appendErrors = { - audio: 0, - video: 0, - audiovideo: 0 - }; - } - trimBuffers() { - const { - hls, - details, - media - } = this; - if (!media || details === null) { - return; - } - if (!this.sourceBufferCount) { - return; - } - const config = hls.config; - const currentTime = media.currentTime; - const targetDuration = details.levelTargetDuration; - - // Support for deprecated liveBackBufferLength - const backBufferLength = details.live && config.liveBackBufferLength !== null ? config.liveBackBufferLength : config.backBufferLength; - if (isFiniteNumber(backBufferLength) && backBufferLength > 0) { - const maxBackBufferLength = Math.max(backBufferLength, targetDuration); - const targetBackBufferPosition = Math.floor(currentTime / targetDuration) * targetDuration - maxBackBufferLength; - this.flushBackBuffer(currentTime, targetDuration, targetBackBufferPosition); - } - if (isFiniteNumber(config.frontBufferFlushThreshold) && config.frontBufferFlushThreshold > 0) { - const frontBufferLength = Math.max(config.maxBufferLength, config.frontBufferFlushThreshold); - const maxFrontBufferLength = Math.max(frontBufferLength, targetDuration); - const targetFrontBufferPosition = Math.floor(currentTime / targetDuration) * targetDuration + maxFrontBufferLength; - this.flushFrontBuffer(currentTime, targetDuration, targetFrontBufferPosition); - } - } - flushBackBuffer(currentTime, targetDuration, targetBackBufferPosition) { - this.sourceBuffers.forEach(([type, sb]) => { - if (sb) { - const buffered = BufferHelper.getBuffered(sb); - // when target buffer start exceeds actual buffer start - if (buffered.length > 0 && targetBackBufferPosition > buffered.start(0)) { - var _this$details; - this.hls.trigger(Events.BACK_BUFFER_REACHED, { - bufferEnd: targetBackBufferPosition - }); - - // Support for deprecated event: - const track = this.tracks[type]; - if ((_this$details = this.details) != null && _this$details.live) { - this.hls.trigger(Events.LIVE_BACK_BUFFER_REACHED, { - bufferEnd: targetBackBufferPosition - }); - } else if (track != null && track.ended && buffered.end(buffered.length - 1) - currentTime < targetDuration * 2) { - this.log(`Cannot flush ${type} back buffer while SourceBuffer is in ended state`); - return; - } - this.hls.trigger(Events.BUFFER_FLUSHING, { - startOffset: 0, - endOffset: targetBackBufferPosition, - type - }); - } - } - }); - } - flushFrontBuffer(currentTime, targetDuration, targetFrontBufferPosition) { - this.sourceBuffers.forEach(([type, sb]) => { - if (sb) { - const buffered = BufferHelper.getBuffered(sb); - const numBufferedRanges = buffered.length; - // The buffer is either empty or contiguous - if (numBufferedRanges < 2) { - return; - } - const bufferStart = buffered.start(numBufferedRanges - 1); - const bufferEnd = buffered.end(numBufferedRanges - 1); - const track = this.tracks[type]; - // No flush if we can tolerate the current buffer length or the current buffer range we would flush is contiguous with current position - if (targetFrontBufferPosition > bufferStart || currentTime >= bufferStart && currentTime <= bufferEnd) { - return; - } else if (track != null && track.ended && currentTime - bufferEnd < 2 * targetDuration) { - this.log(`Cannot flush ${type} front buffer while SourceBuffer is in ended state`); - return; - } - this.hls.trigger(Events.BUFFER_FLUSHING, { - startOffset: bufferStart, - endOffset: Infinity, - type - }); - } - }); - } - - /** - * Update Media Source duration to current level duration or override to Infinity if configuration parameter - * 'liveDurationInfinity` is set to `true` - * More details: https://github.com/video-dev/hls.js/issues/355 - */ - getDurationAndRange() { - var _this$overrides2; - const { - details, - mediaSource - } = this; - if (!details || !this.media || (mediaSource == null ? void 0 : mediaSource.readyState) !== 'open') { - return null; - } - const playlistEnd = details.edge; - if (details.live && this.hls.config.liveDurationInfinity) { - // Override duration to Infinity - mediaSource.duration = Infinity; - const len = details.fragments.length; - if (len && details.live && !!mediaSource.setLiveSeekableRange) { - const start = Math.max(0, details.fragmentStart); - const end = Math.max(start, playlistEnd); - return { - duration: Infinity, - start, - end - }; - } - return { - duration: Infinity - }; - } - const overrideDuration = (_this$overrides2 = this.overrides) == null ? void 0 : _this$overrides2.duration; - if (overrideDuration) { - return { - duration: overrideDuration - }; - } - const mediaDuration = this.media.duration; - const msDuration = isFiniteNumber(mediaSource.duration) ? mediaSource.duration : 0; - if (playlistEnd > msDuration && playlistEnd > mediaDuration || !isFiniteNumber(mediaDuration)) { - return { - duration: playlistEnd - }; - } - return null; - } - updateMediaSource({ - duration, - start, - end - }) { - const mediaSource = this.mediaSource; - if (!this.media || !mediaSource || mediaSource.readyState !== 'open') { - return; - } - if (mediaSource.duration !== duration) { - if (isFiniteNumber(duration)) { - this.log(`Updating MediaSource duration to ${duration.toFixed(3)}`); - } - mediaSource.duration = duration; - } - if (start !== undefined && end !== undefined) { - this.log(`MediaSource duration is set to ${mediaSource.duration}. Setting seekable range to ${start}-${end}.`); - mediaSource.setLiveSeekableRange(start, end); - } - } - get tracksReady() { - const pendingTrackCount = this.pendingTrackCount; - return pendingTrackCount > 0 && (pendingTrackCount >= this.bufferCodecEventsTotal || this.isPending(this.tracks.audiovideo)); - } - checkPendingTracks() { - const { - bufferCodecEventsTotal, - pendingTrackCount, - tracks - } = this; - this.log(`checkPendingTracks (pending: ${pendingTrackCount} codec events expected: ${bufferCodecEventsTotal}) ${JSON.stringify(tracks)}`); - // Check if we've received all of the expected bufferCodec events. When none remain, create all the sourceBuffers at once. - // This is important because the MSE spec allows implementations to throw QuotaExceededErrors if creating new sourceBuffers after - // data has been appended to existing ones. - // 2 tracks is the max (one for audio, one for video). If we've reach this max go ahead and create the buffers. - if (this.tracksReady) { - var _this$transferData4; - const transferredTracks = (_this$transferData4 = this.transferData) == null ? void 0 : _this$transferData4.tracks; - if (transferredTracks && Object.keys(transferredTracks).length) { - this.attachTransferred(); - } else { - // ok, let's create them now ! - this.createSourceBuffers(); - this.bufferCreated(); - } - } - } - bufferCreated() { - if (this.sourceBufferCount) { - const tracks = {}; - this.sourceBuffers.forEach(([type, buffer]) => { - if (type) { - const track = this.tracks[type]; - tracks[type] = { - buffer, - container: track.container, - codec: track.codec, - levelCodec: track.levelCodec, - id: track.id, - metadata: track.metadata - }; - } - }); - this.hls.trigger(Events.BUFFER_CREATED, { - tracks - }); - this.log(`SourceBuffers created. Running queue: ${this.operationQueue}`); - this.sourceBuffers.forEach(([type]) => { - this.executeNext(type); - }); - } else { - const error = new Error('could not create source buffer for media codec(s)'); - this.hls.trigger(Events.ERROR, { - type: ErrorTypes.MEDIA_ERROR, - details: ErrorDetails.BUFFER_INCOMPATIBLE_CODECS_ERROR, - fatal: true, - error, - reason: error.message - }); - } - } - createSourceBuffers() { - const { - tracks, - sourceBuffers, - mediaSource - } = this; - if (!mediaSource) { - throw new Error('createSourceBuffers called when mediaSource was null'); - } - for (const trackName in tracks) { - const type = trackName; - const track = tracks[type]; - if (this.isPending(track)) { - const codec = this.getTrackCodec(track, type); - const mimeType = `${track.container};codecs=${codec}`; - track.codec = codec; - this.log(`creating sourceBuffer(${mimeType})${this.currentOp(type) ? ' Queued' : ''} ${JSON.stringify(track)}`); - try { - const sb = mediaSource.addSourceBuffer(mimeType); - const sbIndex = sourceBufferNameToIndex(type); - const sbTuple = [type, sb]; - sourceBuffers[sbIndex] = sbTuple; - track.buffer = sb; - } catch (error) { - this.error(`error while trying to add sourceBuffer: ${error.message}`); - this.hls.trigger(Events.ERROR, { - type: ErrorTypes.MEDIA_ERROR, - details: ErrorDetails.BUFFER_ADD_CODEC_ERROR, - fatal: false, - error, - sourceBufferName: type, - mimeType: mimeType - }); - break; - } - this.trackSourceBuffer(type, track); - } - } - } - getTrackCodec(track, trackName) { - const codec = pickMostCompleteCodecName(track.codec, track.levelCodec); - if (codec) { - if (trackName.slice(0, 5) === 'audio') { - return getCodecCompatibleName(codec, this.appendSource); - } - return codec; - } - return ''; - } - trackSourceBuffer(type, track) { - const buffer = track.buffer; - if (!buffer) { - return; - } - const codec = this.getTrackCodec(track, type); - this.tracks[type] = { - buffer, - codec, - container: track.container, - levelCodec: track.levelCodec, - metadata: track.metadata, - id: track.id, - listeners: [] - }; - this.addBufferListener(type, 'updatestart', this.onSBUpdateStart); - this.addBufferListener(type, 'updateend', this.onSBUpdateEnd); - this.addBufferListener(type, 'error', this.onSBUpdateError); - // ManagedSourceBuffer bufferedchange event - if (this.appendSource) { - this.addBufferListener(type, 'bufferedchange', (type, event) => { - // If media was ejected check for a change. Added ranges are redundant with changes on 'updateend' event. - const removedRanges = event.removedRanges; - if (removedRanges != null && removedRanges.length) { - this.hls.trigger(Events.BUFFER_FLUSHED, { - type: type - }); - } - }); - } - } - get mediaSrc() { - var _this$media2, _this$media2$querySel; - const media = ((_this$media2 = this.media) == null ? void 0 : (_this$media2$querySel = _this$media2.querySelector) == null ? void 0 : _this$media2$querySel.call(_this$media2, 'source')) || this.media; - return media == null ? void 0 : media.src; - } - onSBUpdateStart(type) { - const operation = this.currentOp(type); - if (!operation) { - return; - } - operation.onStart(); - } - onSBUpdateEnd(type) { - var _this$mediaSource5; - if (((_this$mediaSource5 = this.mediaSource) == null ? void 0 : _this$mediaSource5.readyState) === 'closed') { - this.resetBuffer(type); - return; - } - const operation = this.currentOp(type); - if (!operation) { - return; - } - operation.onComplete(); - this.shiftAndExecuteNext(type); - } - onSBUpdateError(type, event) { - var _this$mediaSource6; - const error = new Error(`${type} SourceBuffer error. MediaSource readyState: ${(_this$mediaSource6 = this.mediaSource) == null ? void 0 : _this$mediaSource6.readyState}`); - this.error(`${error}`, event); - // according to http://www.w3.org/TR/media-source/#sourcebuffer-append-error - // SourceBuffer errors are not necessarily fatal; if so, the HTMLMediaElement will fire an error event - this.hls.trigger(Events.ERROR, { - type: ErrorTypes.MEDIA_ERROR, - details: ErrorDetails.BUFFER_APPENDING_ERROR, - sourceBufferName: type, - error, - fatal: false - }); - // updateend is always fired after error, so we'll allow that to shift the current operation off of the queue - const operation = this.currentOp(type); - if (operation) { - operation.onError(error); - } - } - - // This method must result in an updateend event; if remove is not called, onSBUpdateEnd must be called manually - removeExecutor(type, startOffset, endOffset) { - const { - media, - mediaSource - } = this; - const track = this.tracks[type]; - const sb = track == null ? void 0 : track.buffer; - if (!media || !mediaSource || !sb) { - this.warn(`Attempting to remove from the ${type} SourceBuffer, but it does not exist`); - this.shiftAndExecuteNext(type); - return; - } - const mediaDuration = isFiniteNumber(media.duration) ? media.duration : Infinity; - const msDuration = isFiniteNumber(mediaSource.duration) ? mediaSource.duration : Infinity; - const removeStart = Math.max(0, startOffset); - const removeEnd = Math.min(endOffset, mediaDuration, msDuration); - if (removeEnd > removeStart && (!track.ending || track.ended)) { - track.ended = false; - this.log(`Removing [${removeStart},${removeEnd}] from the ${type} SourceBuffer`); - sb.remove(removeStart, removeEnd); - } else { - // Cycle the queue - this.shiftAndExecuteNext(type); - } - } - - // This method must result in an updateend event; if append is not called, onSBUpdateEnd must be called manually - appendExecutor(data, type) { - const track = this.tracks[type]; - const sb = track == null ? void 0 : track.buffer; - if (!sb) { - throw new HlsJsTrackRemovedError(`Attempting to append to the ${type} SourceBuffer, but it does not exist`); - } - track.ending = false; - track.ended = false; - sb.appendBuffer(data); - } - blockUntilOpen(callback) { - if (this.isUpdating() || this.isQueued()) { - this.blockBuffers(callback); - } else { - callback(); - } - } - isUpdating() { - return this.sourceBuffers.some(([type, sb]) => type && sb.updating); - } - isQueued() { - return this.sourceBuffers.some(([type]) => type && !!this.currentOp(type)); - } - isPending(track) { - return !!track && !track.buffer; - } - - // Enqueues an operation to each SourceBuffer queue which, upon execution, resolves a promise. When all promises - // resolve, the onUnblocked function is executed. Functions calling this method do not need to unblock the queue - // upon completion, since we already do it here - blockBuffers(onUnblocked, bufferNames = this.sourceBufferTypes) { - if (!bufferNames.length) { - this.log('Blocking operation requested, but no SourceBuffers exist'); - Promise.resolve().then(onUnblocked); - return; - } - const { - operationQueue - } = this; - - // logger.debug(`[buffer-controller]: Blocking ${buffers} SourceBuffer`); - const blockingOperations = bufferNames.map(type => this.appendBlocker(type)); - const audioBlocked = bufferNames.length > 1 && !!this.blockedAudioAppend; - if (audioBlocked) { - this.unblockAudio(); - } - Promise.all(blockingOperations).then(result => { - if (operationQueue !== this.operationQueue) { - return; - } - // logger.debug(`[buffer-controller]: Blocking operation resolved; unblocking ${buffers} SourceBuffer`); - onUnblocked(); - this.stepOperationQueue(bufferNames); - }); - } - stepOperationQueue(bufferNames) { - bufferNames.forEach(type => { - var _this$tracks$type5; - const sb = (_this$tracks$type5 = this.tracks[type]) == null ? void 0 : _this$tracks$type5.buffer; - // Only cycle the queue if the SB is not updating. There's a bug in Chrome which sets the SB updating flag to - // true when changing the MediaSource duration (https://bugs.chromium.org/p/chromium/issues/detail?id=959359&can=2&q=mediasource%20duration) - // While this is a workaround, it's probably useful to have around - if (!sb || sb.updating) { - return; - } - this.shiftAndExecuteNext(type); - }); - } - append(operation, type, pending) { - if (this.operationQueue) { - this.operationQueue.append(operation, type, pending); - } - } - appendBlocker(type) { - if (this.operationQueue) { - return this.operationQueue.appendBlocker(type); - } - } - currentOp(type) { - if (this.operationQueue) { - return this.operationQueue.current(type); - } - return null; - } - executeNext(type) { - if (type && this.operationQueue) { - this.operationQueue.executeNext(type); - } - } - shiftAndExecuteNext(type) { - if (this.operationQueue) { - this.operationQueue.shiftAndExecuteNext(type); - } - } - get pendingTrackCount() { - return Object.keys(this.tracks).reduce((acc, type) => acc + (this.isPending(this.tracks[type]) ? 1 : 0), 0); - } - get sourceBufferCount() { - return this.sourceBuffers.reduce((acc, [type]) => acc + (type ? 1 : 0), 0); - } - get sourceBufferTypes() { - return this.sourceBuffers.map(([type]) => type).filter(type => !!type); - } - addBufferListener(type, event, fn) { - const track = this.tracks[type]; - if (!track) { - return; - } - const buffer = track.buffer; - if (!buffer) { - return; - } - const listener = fn.bind(this, type); - track.listeners.push({ - event, - listener - }); - buffer.addEventListener(event, listener); - } - removeBufferListeners(type) { - const track = this.tracks[type]; - if (!track) { - return; - } - const buffer = track.buffer; - if (!buffer) { - return; - } - track.listeners.forEach(l => { - buffer.removeEventListener(l.event, l.listener); - }); - track.listeners.length = 0; - } -} -function removeSourceChildren(node) { - const sourceChildren = node.querySelectorAll('source'); - [].slice.call(sourceChildren).forEach(source => { - node.removeChild(source); - }); -} -function addSource(media, url) { - const source = self.document.createElement('source'); - source.type = 'video/mp4'; - source.src = url; - media.appendChild(source); -} -function sourceBufferNameToIndex(type) { - return type === 'audio' ? 1 : 0; -} - -/** - * - * This code was ported from the dash.js project at: - * https://github.com/Dash-Industry-Forum/dash.js/blob/development/externals/cea608-parser.js - * https://github.com/Dash-Industry-Forum/dash.js/commit/8269b26a761e0853bb21d78780ed945144ecdd4d#diff-71bc295a2d6b6b7093a1d3290d53a4b2 - * - * The original copyright appears below: - * - * The copyright in this software is being made available under the BSD License, - * included below. This software may be subject to other third party and contributor - * rights, including patent rights, and no such rights are granted under this license. - * - * Copyright (c) 2015-2016, DASH Industry Forum. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * 2. Neither the name of Dash Industry Forum nor the names of its - * contributors may be used to endorse or promote products derived from this software - * without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -/** - * Exceptions from regular ASCII. CodePoints are mapped to UTF-16 codes - */ - -const specialCea608CharsCodes = { - 0x2a: 0xe1, - // lowercase a, acute accent - 0x5c: 0xe9, - // lowercase e, acute accent - 0x5e: 0xed, - // lowercase i, acute accent - 0x5f: 0xf3, - // lowercase o, acute accent - 0x60: 0xfa, - // lowercase u, acute accent - 0x7b: 0xe7, - // lowercase c with cedilla - 0x7c: 0xf7, - // division symbol - 0x7d: 0xd1, - // uppercase N tilde - 0x7e: 0xf1, - // lowercase n tilde - 0x7f: 0x2588, - // Full block - // THIS BLOCK INCLUDES THE 16 EXTENDED (TWO-BYTE) LINE 21 CHARACTERS - // THAT COME FROM HI BYTE=0x11 AND LOW BETWEEN 0x30 AND 0x3F - // THIS MEANS THAT \x50 MUST BE ADDED TO THE VALUES - 0x80: 0xae, - // Registered symbol (R) - 0x81: 0xb0, - // degree sign - 0x82: 0xbd, - // 1/2 symbol - 0x83: 0xbf, - // Inverted (open) question mark - 0x84: 0x2122, - // Trademark symbol (TM) - 0x85: 0xa2, - // Cents symbol - 0x86: 0xa3, - // Pounds sterling - 0x87: 0x266a, - // Music 8'th note - 0x88: 0xe0, - // lowercase a, grave accent - 0x89: 0x20, - // transparent space (regular) - 0x8a: 0xe8, - // lowercase e, grave accent - 0x8b: 0xe2, - // lowercase a, circumflex accent - 0x8c: 0xea, - // lowercase e, circumflex accent - 0x8d: 0xee, - // lowercase i, circumflex accent - 0x8e: 0xf4, - // lowercase o, circumflex accent - 0x8f: 0xfb, - // lowercase u, circumflex accent - // THIS BLOCK INCLUDES THE 32 EXTENDED (TWO-BYTE) LINE 21 CHARACTERS - // THAT COME FROM HI BYTE=0x12 AND LOW BETWEEN 0x20 AND 0x3F - 0x90: 0xc1, - // capital letter A with acute - 0x91: 0xc9, - // capital letter E with acute - 0x92: 0xd3, - // capital letter O with acute - 0x93: 0xda, - // capital letter U with acute - 0x94: 0xdc, - // capital letter U with diaresis - 0x95: 0xfc, - // lowercase letter U with diaeresis - 0x96: 0x2018, - // opening single quote - 0x97: 0xa1, - // inverted exclamation mark - 0x98: 0x2a, - // asterisk - 0x99: 0x2019, - // closing single quote - 0x9a: 0x2501, - // box drawings heavy horizontal - 0x9b: 0xa9, - // copyright sign - 0x9c: 0x2120, - // Service mark - 0x9d: 0x2022, - // (round) bullet - 0x9e: 0x201c, - // Left double quotation mark - 0x9f: 0x201d, - // Right double quotation mark - 0xa0: 0xc0, - // uppercase A, grave accent - 0xa1: 0xc2, - // uppercase A, circumflex - 0xa2: 0xc7, - // uppercase C with cedilla - 0xa3: 0xc8, - // uppercase E, grave accent - 0xa4: 0xca, - // uppercase E, circumflex - 0xa5: 0xcb, - // capital letter E with diaresis - 0xa6: 0xeb, - // lowercase letter e with diaresis - 0xa7: 0xce, - // uppercase I, circumflex - 0xa8: 0xcf, - // uppercase I, with diaresis - 0xa9: 0xef, - // lowercase i, with diaresis - 0xaa: 0xd4, - // uppercase O, circumflex - 0xab: 0xd9, - // uppercase U, grave accent - 0xac: 0xf9, - // lowercase u, grave accent - 0xad: 0xdb, - // uppercase U, circumflex - 0xae: 0xab, - // left-pointing double angle quotation mark - 0xaf: 0xbb, - // right-pointing double angle quotation mark - // THIS BLOCK INCLUDES THE 32 EXTENDED (TWO-BYTE) LINE 21 CHARACTERS - // THAT COME FROM HI BYTE=0x13 AND LOW BETWEEN 0x20 AND 0x3F - 0xb0: 0xc3, - // Uppercase A, tilde - 0xb1: 0xe3, - // Lowercase a, tilde - 0xb2: 0xcd, - // Uppercase I, acute accent - 0xb3: 0xcc, - // Uppercase I, grave accent - 0xb4: 0xec, - // Lowercase i, grave accent - 0xb5: 0xd2, - // Uppercase O, grave accent - 0xb6: 0xf2, - // Lowercase o, grave accent - 0xb7: 0xd5, - // Uppercase O, tilde - 0xb8: 0xf5, - // Lowercase o, tilde - 0xb9: 0x7b, - // Open curly brace - 0xba: 0x7d, - // Closing curly brace - 0xbb: 0x5c, - // Backslash - 0xbc: 0x5e, - // Caret - 0xbd: 0x5f, - // Underscore - 0xbe: 0x7c, - // Pipe (vertical line) - 0xbf: 0x223c, - // Tilde operator - 0xc0: 0xc4, - // Uppercase A, umlaut - 0xc1: 0xe4, - // Lowercase A, umlaut - 0xc2: 0xd6, - // Uppercase O, umlaut - 0xc3: 0xf6, - // Lowercase o, umlaut - 0xc4: 0xdf, - // Esszett (sharp S) - 0xc5: 0xa5, - // Yen symbol - 0xc6: 0xa4, - // Generic currency sign - 0xc7: 0x2503, - // Box drawings heavy vertical - 0xc8: 0xc5, - // Uppercase A, ring - 0xc9: 0xe5, - // Lowercase A, ring - 0xca: 0xd8, - // Uppercase O, stroke - 0xcb: 0xf8, - // Lowercase o, strok - 0xcc: 0x250f, - // Box drawings heavy down and right - 0xcd: 0x2513, - // Box drawings heavy down and left - 0xce: 0x2517, - // Box drawings heavy up and right - 0xcf: 0x251b // Box drawings heavy up and left -}; - -/** - * Utils - */ -const getCharForByte = byte => String.fromCharCode(specialCea608CharsCodes[byte] || byte); -const NR_ROWS = 15; -const NR_COLS = 100; -// Tables to look up row from PAC data -const rowsLowCh1 = { - 0x11: 1, - 0x12: 3, - 0x15: 5, - 0x16: 7, - 0x17: 9, - 0x10: 11, - 0x13: 12, - 0x14: 14 -}; -const rowsHighCh1 = { - 0x11: 2, - 0x12: 4, - 0x15: 6, - 0x16: 8, - 0x17: 10, - 0x13: 13, - 0x14: 15 -}; -const rowsLowCh2 = { - 0x19: 1, - 0x1a: 3, - 0x1d: 5, - 0x1e: 7, - 0x1f: 9, - 0x18: 11, - 0x1b: 12, - 0x1c: 14 -}; -const rowsHighCh2 = { - 0x19: 2, - 0x1a: 4, - 0x1d: 6, - 0x1e: 8, - 0x1f: 10, - 0x1b: 13, - 0x1c: 15 -}; -const backgroundColors = ['white', 'green', 'blue', 'cyan', 'red', 'yellow', 'magenta', 'black', 'transparent']; -class CaptionsLogger { - constructor() { - this.time = null; - this.verboseLevel = 0; - } - log(severity, msg) { - if (this.verboseLevel >= severity) { - const m = typeof msg === 'function' ? msg() : msg; - logger.log(`${this.time} [${severity}] ${m}`); - } - } -} -const numArrayToHexArray = function numArrayToHexArray(numArray) { - const hexArray = []; - for (let j = 0; j < numArray.length; j++) { - hexArray.push(numArray[j].toString(16)); - } - return hexArray; -}; -class PenState { - constructor() { - this.foreground = 'white'; - this.underline = false; - this.italics = false; - this.background = 'black'; - this.flash = false; - } - reset() { - this.foreground = 'white'; - this.underline = false; - this.italics = false; - this.background = 'black'; - this.flash = false; - } - setStyles(styles) { - const attribs = ['foreground', 'underline', 'italics', 'background', 'flash']; - for (let i = 0; i < attribs.length; i++) { - const style = attribs[i]; - if (styles.hasOwnProperty(style)) { - this[style] = styles[style]; - } - } - } - isDefault() { - return this.foreground === 'white' && !this.underline && !this.italics && this.background === 'black' && !this.flash; - } - equals(other) { - return this.foreground === other.foreground && this.underline === other.underline && this.italics === other.italics && this.background === other.background && this.flash === other.flash; - } - copy(newPenState) { - this.foreground = newPenState.foreground; - this.underline = newPenState.underline; - this.italics = newPenState.italics; - this.background = newPenState.background; - this.flash = newPenState.flash; - } - toString() { - return 'color=' + this.foreground + ', underline=' + this.underline + ', italics=' + this.italics + ', background=' + this.background + ', flash=' + this.flash; - } -} - -/** - * Unicode character with styling and background. - * @constructor - */ -class StyledUnicodeChar { - constructor() { - this.uchar = ' '; - this.penState = new PenState(); - } - reset() { - this.uchar = ' '; - this.penState.reset(); - } - setChar(uchar, newPenState) { - this.uchar = uchar; - this.penState.copy(newPenState); - } - setPenState(newPenState) { - this.penState.copy(newPenState); - } - equals(other) { - return this.uchar === other.uchar && this.penState.equals(other.penState); - } - copy(newChar) { - this.uchar = newChar.uchar; - this.penState.copy(newChar.penState); - } - isEmpty() { - return this.uchar === ' ' && this.penState.isDefault(); - } -} - -/** - * CEA-608 row consisting of NR_COLS instances of StyledUnicodeChar. - * @constructor - */ -class Row { - constructor(logger) { - this.chars = []; - this.pos = 0; - this.currPenState = new PenState(); - this.cueStartTime = null; - this.logger = void 0; - for (let i = 0; i < NR_COLS; i++) { - this.chars.push(new StyledUnicodeChar()); - } - this.logger = logger; - } - equals(other) { - for (let i = 0; i < NR_COLS; i++) { - if (!this.chars[i].equals(other.chars[i])) { - return false; - } - } - return true; - } - copy(other) { - for (let i = 0; i < NR_COLS; i++) { - this.chars[i].copy(other.chars[i]); - } - } - isEmpty() { - let empty = true; - for (let i = 0; i < NR_COLS; i++) { - if (!this.chars[i].isEmpty()) { - empty = false; - break; - } - } - return empty; - } - - /** - * Set the cursor to a valid column. - */ - setCursor(absPos) { - if (this.pos !== absPos) { - this.pos = absPos; - } - if (this.pos < 0) { - this.logger.log(3, 'Negative cursor position ' + this.pos); - this.pos = 0; - } else if (this.pos > NR_COLS) { - this.logger.log(3, 'Too large cursor position ' + this.pos); - this.pos = NR_COLS; - } - } - - /** - * Move the cursor relative to current position. - */ - moveCursor(relPos) { - const newPos = this.pos + relPos; - if (relPos > 1) { - for (let i = this.pos + 1; i < newPos + 1; i++) { - this.chars[i].setPenState(this.currPenState); - } - } - this.setCursor(newPos); - } - - /** - * Backspace, move one step back and clear character. - */ - backSpace() { - this.moveCursor(-1); - this.chars[this.pos].setChar(' ', this.currPenState); - } - insertChar(byte) { - if (byte >= 0x90) { - // Extended char - this.backSpace(); - } - const char = getCharForByte(byte); - if (this.pos >= NR_COLS) { - this.logger.log(0, () => 'Cannot insert ' + byte.toString(16) + ' (' + char + ') at position ' + this.pos + '. Skipping it!'); - return; - } - this.chars[this.pos].setChar(char, this.currPenState); - this.moveCursor(1); - } - clearFromPos(startPos) { - let i; - for (i = startPos; i < NR_COLS; i++) { - this.chars[i].reset(); - } - } - clear() { - this.clearFromPos(0); - this.pos = 0; - this.currPenState.reset(); - } - clearToEndOfRow() { - this.clearFromPos(this.pos); - } - getTextString() { - const chars = []; - let empty = true; - for (let i = 0; i < NR_COLS; i++) { - const char = this.chars[i].uchar; - if (char !== ' ') { - empty = false; - } - chars.push(char); - } - if (empty) { - return ''; - } else { - return chars.join(''); - } - } - setPenStyles(styles) { - this.currPenState.setStyles(styles); - const currChar = this.chars[this.pos]; - currChar.setPenState(this.currPenState); - } -} - -/** - * Keep a CEA-608 screen of 32x15 styled characters - * @constructor - */ -class CaptionScreen { - constructor(logger) { - this.rows = []; - this.currRow = NR_ROWS - 1; - this.nrRollUpRows = null; - this.lastOutputScreen = null; - this.logger = void 0; - for (let i = 0; i < NR_ROWS; i++) { - this.rows.push(new Row(logger)); - } - this.logger = logger; - } - reset() { - for (let i = 0; i < NR_ROWS; i++) { - this.rows[i].clear(); - } - this.currRow = NR_ROWS - 1; - } - equals(other) { - let equal = true; - for (let i = 0; i < NR_ROWS; i++) { - if (!this.rows[i].equals(other.rows[i])) { - equal = false; - break; - } - } - return equal; - } - copy(other) { - for (let i = 0; i < NR_ROWS; i++) { - this.rows[i].copy(other.rows[i]); - } - } - isEmpty() { - let empty = true; - for (let i = 0; i < NR_ROWS; i++) { - if (!this.rows[i].isEmpty()) { - empty = false; - break; - } - } - return empty; - } - backSpace() { - const row = this.rows[this.currRow]; - row.backSpace(); - } - clearToEndOfRow() { - const row = this.rows[this.currRow]; - row.clearToEndOfRow(); - } - - /** - * Insert a character (without styling) in the current row. - */ - insertChar(char) { - const row = this.rows[this.currRow]; - row.insertChar(char); - } - setPen(styles) { - const row = this.rows[this.currRow]; - row.setPenStyles(styles); - } - moveCursor(relPos) { - const row = this.rows[this.currRow]; - row.moveCursor(relPos); - } - setCursor(absPos) { - this.logger.log(2, 'setCursor: ' + absPos); - const row = this.rows[this.currRow]; - row.setCursor(absPos); - } - setPAC(pacData) { - this.logger.log(2, () => 'pacData = ' + JSON.stringify(pacData)); - let newRow = pacData.row - 1; - if (this.nrRollUpRows && newRow < this.nrRollUpRows - 1) { - newRow = this.nrRollUpRows - 1; - } - - // Make sure this only affects Roll-up Captions by checking this.nrRollUpRows - if (this.nrRollUpRows && this.currRow !== newRow) { - // clear all rows first - for (let i = 0; i < NR_ROWS; i++) { - this.rows[i].clear(); - } - - // Copy this.nrRollUpRows rows from lastOutputScreen and place it in the newRow location - // topRowIndex - the start of rows to copy (inclusive index) - const topRowIndex = this.currRow + 1 - this.nrRollUpRows; - // We only copy if the last position was already shown. - // We use the cueStartTime value to check this. - const lastOutputScreen = this.lastOutputScreen; - if (lastOutputScreen) { - const prevLineTime = lastOutputScreen.rows[topRowIndex].cueStartTime; - const time = this.logger.time; - if (prevLineTime !== null && time !== null && prevLineTime < time) { - for (let i = 0; i < this.nrRollUpRows; i++) { - this.rows[newRow - this.nrRollUpRows + i + 1].copy(lastOutputScreen.rows[topRowIndex + i]); - } - } - } - } - this.currRow = newRow; - const row = this.rows[this.currRow]; - if (pacData.indent !== null) { - const indent = pacData.indent; - const prevPos = Math.max(indent - 1, 0); - row.setCursor(pacData.indent); - pacData.color = row.chars[prevPos].penState.foreground; - } - const styles = { - foreground: pacData.color, - underline: pacData.underline, - italics: pacData.italics, - background: 'black', - flash: false - }; - this.setPen(styles); - } - - /** - * Set background/extra foreground, but first do back_space, and then insert space (backwards compatibility). - */ - setBkgData(bkgData) { - this.logger.log(2, () => 'bkgData = ' + JSON.stringify(bkgData)); - this.backSpace(); - this.setPen(bkgData); - this.insertChar(0x20); // Space - } - setRollUpRows(nrRows) { - this.nrRollUpRows = nrRows; - } - rollUp() { - if (this.nrRollUpRows === null) { - this.logger.log(3, 'roll_up but nrRollUpRows not set yet'); - return; // Not properly setup - } - this.logger.log(1, () => this.getDisplayText()); - const topRowIndex = this.currRow + 1 - this.nrRollUpRows; - const topRow = this.rows.splice(topRowIndex, 1)[0]; - topRow.clear(); - this.rows.splice(this.currRow, 0, topRow); - this.logger.log(2, 'Rolling up'); - // this.logger.log(VerboseLevel.TEXT, this.get_display_text()) - } - - /** - * Get all non-empty rows with as unicode text. - */ - getDisplayText(asOneRow) { - asOneRow = asOneRow || false; - const displayText = []; - let text = ''; - let rowNr = -1; - for (let i = 0; i < NR_ROWS; i++) { - const rowText = this.rows[i].getTextString(); - if (rowText) { - rowNr = i + 1; - if (asOneRow) { - displayText.push('Row ' + rowNr + ": '" + rowText + "'"); - } else { - displayText.push(rowText.trim()); - } - } - } - if (displayText.length > 0) { - if (asOneRow) { - text = '[' + displayText.join(' | ') + ']'; - } else { - text = displayText.join('\n'); - } - } - return text; - } - getTextAndFormat() { - return this.rows; - } -} - -// var modes = ['MODE_ROLL-UP', 'MODE_POP-ON', 'MODE_PAINT-ON', 'MODE_TEXT']; - -class Cea608Channel { - constructor(channelNumber, outputFilter, logger) { - this.chNr = void 0; - this.outputFilter = void 0; - this.mode = void 0; - this.verbose = void 0; - this.displayedMemory = void 0; - this.nonDisplayedMemory = void 0; - this.lastOutputScreen = void 0; - this.currRollUpRow = void 0; - this.writeScreen = void 0; - this.cueStartTime = void 0; - this.logger = void 0; - this.chNr = channelNumber; - this.outputFilter = outputFilter; - this.mode = null; - this.verbose = 0; - this.displayedMemory = new CaptionScreen(logger); - this.nonDisplayedMemory = new CaptionScreen(logger); - this.lastOutputScreen = new CaptionScreen(logger); - this.currRollUpRow = this.displayedMemory.rows[NR_ROWS - 1]; - this.writeScreen = this.displayedMemory; - this.mode = null; - this.cueStartTime = null; // Keeps track of where a cue started. - this.logger = logger; - } - reset() { - this.mode = null; - this.displayedMemory.reset(); - this.nonDisplayedMemory.reset(); - this.lastOutputScreen.reset(); - this.outputFilter.reset(); - this.currRollUpRow = this.displayedMemory.rows[NR_ROWS - 1]; - this.writeScreen = this.displayedMemory; - this.mode = null; - this.cueStartTime = null; - } - getHandler() { - return this.outputFilter; - } - setHandler(newHandler) { - this.outputFilter = newHandler; - } - setPAC(pacData) { - this.writeScreen.setPAC(pacData); - } - setBkgData(bkgData) { - this.writeScreen.setBkgData(bkgData); - } - setMode(newMode) { - if (newMode === this.mode) { - return; - } - this.mode = newMode; - this.logger.log(2, () => 'MODE=' + newMode); - if (this.mode === 'MODE_POP-ON') { - this.writeScreen = this.nonDisplayedMemory; - } else { - this.writeScreen = this.displayedMemory; - this.writeScreen.reset(); - } - if (this.mode !== 'MODE_ROLL-UP') { - this.displayedMemory.nrRollUpRows = null; - this.nonDisplayedMemory.nrRollUpRows = null; - } - this.mode = newMode; - } - insertChars(chars) { - for (let i = 0; i < chars.length; i++) { - this.writeScreen.insertChar(chars[i]); - } - const screen = this.writeScreen === this.displayedMemory ? 'DISP' : 'NON_DISP'; - this.logger.log(2, () => screen + ': ' + this.writeScreen.getDisplayText(true)); - if (this.mode === 'MODE_PAINT-ON' || this.mode === 'MODE_ROLL-UP') { - this.logger.log(1, () => 'DISPLAYED: ' + this.displayedMemory.getDisplayText(true)); - this.outputDataUpdate(); - } - } - ccRCL() { - // Resume Caption Loading (switch mode to Pop On) - this.logger.log(2, 'RCL - Resume Caption Loading'); - this.setMode('MODE_POP-ON'); - } - ccBS() { - // BackSpace - this.logger.log(2, 'BS - BackSpace'); - if (this.mode === 'MODE_TEXT') { - return; - } - this.writeScreen.backSpace(); - if (this.writeScreen === this.displayedMemory) { - this.outputDataUpdate(); - } - } - ccAOF() { - // Reserved (formerly Alarm Off) - } - ccAON() { - // Reserved (formerly Alarm On) - } - ccDER() { - // Delete to End of Row - this.logger.log(2, 'DER- Delete to End of Row'); - this.writeScreen.clearToEndOfRow(); - this.outputDataUpdate(); - } - ccRU(nrRows) { - // Roll-Up Captions-2,3,or 4 Rows - this.logger.log(2, 'RU(' + nrRows + ') - Roll Up'); - this.writeScreen = this.displayedMemory; - this.setMode('MODE_ROLL-UP'); - this.writeScreen.setRollUpRows(nrRows); - } - ccFON() { - // Flash On - this.logger.log(2, 'FON - Flash On'); - this.writeScreen.setPen({ - flash: true - }); - } - ccRDC() { - // Resume Direct Captioning (switch mode to PaintOn) - this.logger.log(2, 'RDC - Resume Direct Captioning'); - this.setMode('MODE_PAINT-ON'); - } - ccTR() { - // Text Restart in text mode (not supported, however) - this.logger.log(2, 'TR'); - this.setMode('MODE_TEXT'); - } - ccRTD() { - // Resume Text Display in Text mode (not supported, however) - this.logger.log(2, 'RTD'); - this.setMode('MODE_TEXT'); - } - ccEDM() { - // Erase Displayed Memory - this.logger.log(2, 'EDM - Erase Displayed Memory'); - this.displayedMemory.reset(); - this.outputDataUpdate(true); - } - ccCR() { - // Carriage Return - this.logger.log(2, 'CR - Carriage Return'); - this.writeScreen.rollUp(); - this.outputDataUpdate(true); - } - ccENM() { - // Erase Non-Displayed Memory - this.logger.log(2, 'ENM - Erase Non-displayed Memory'); - this.nonDisplayedMemory.reset(); - } - ccEOC() { - // End of Caption (Flip Memories) - this.logger.log(2, 'EOC - End Of Caption'); - if (this.mode === 'MODE_POP-ON') { - const tmp = this.displayedMemory; - this.displayedMemory = this.nonDisplayedMemory; - this.nonDisplayedMemory = tmp; - this.writeScreen = this.nonDisplayedMemory; - this.logger.log(1, () => 'DISP: ' + this.displayedMemory.getDisplayText()); - } - this.outputDataUpdate(true); - } - ccTO(nrCols) { - // Tab Offset 1,2, or 3 columns - this.logger.log(2, 'TO(' + nrCols + ') - Tab Offset'); - this.writeScreen.moveCursor(nrCols); - } - ccMIDROW(secondByte) { - // Parse MIDROW command - const styles = { - flash: false - }; - styles.underline = secondByte % 2 === 1; - styles.italics = secondByte >= 0x2e; - if (!styles.italics) { - const colorIndex = Math.floor(secondByte / 2) - 0x10; - const colors = ['white', 'green', 'blue', 'cyan', 'red', 'yellow', 'magenta']; - styles.foreground = colors[colorIndex]; - } else { - styles.foreground = 'white'; - } - this.logger.log(2, 'MIDROW: ' + JSON.stringify(styles)); - this.writeScreen.setPen(styles); - } - outputDataUpdate(dispatch = false) { - const time = this.logger.time; - if (time === null) { - return; - } - if (this.outputFilter) { - if (this.cueStartTime === null && !this.displayedMemory.isEmpty()) { - // Start of a new cue - this.cueStartTime = time; - } else { - if (!this.displayedMemory.equals(this.lastOutputScreen)) { - this.outputFilter.newCue(this.cueStartTime, time, this.lastOutputScreen); - if (dispatch && this.outputFilter.dispatchCue) { - this.outputFilter.dispatchCue(); - } - this.cueStartTime = this.displayedMemory.isEmpty() ? null : time; - } - } - this.lastOutputScreen.copy(this.displayedMemory); - } - } - cueSplitAtTime(t) { - if (this.outputFilter) { - if (!this.displayedMemory.isEmpty()) { - if (this.outputFilter.newCue) { - this.outputFilter.newCue(this.cueStartTime, t, this.displayedMemory); - } - this.cueStartTime = t; - } - } - } -} - -// Will be 1 or 2 when parsing captions - -class Cea608Parser { - constructor(field, out1, out2) { - this.channels = void 0; - this.currentChannel = 0; - this.cmdHistory = createCmdHistory(); - this.logger = void 0; - const logger = this.logger = new CaptionsLogger(); - this.channels = [null, new Cea608Channel(field, out1, logger), new Cea608Channel(field + 1, out2, logger)]; - } - getHandler(channel) { - return this.channels[channel].getHandler(); - } - setHandler(channel, newHandler) { - this.channels[channel].setHandler(newHandler); - } - - /** - * Add data for time t in forms of list of bytes (unsigned ints). The bytes are treated as pairs. - */ - addData(time, byteList) { - this.logger.time = time; - for (let i = 0; i < byteList.length; i += 2) { - const a = byteList[i] & 0x7f; - const b = byteList[i + 1] & 0x7f; - let cmdFound = false; - let charsFound = null; - if (a === 0 && b === 0) { - continue; - } else { - this.logger.log(3, () => '[' + numArrayToHexArray([byteList[i], byteList[i + 1]]) + '] -> (' + numArrayToHexArray([a, b]) + ')'); - } - const cmdHistory = this.cmdHistory; - const isControlCode = a >= 0x10 && a <= 0x1f; - if (isControlCode) { - // Skip redundant control codes - if (hasCmdRepeated(a, b, cmdHistory)) { - setLastCmd(null, null, cmdHistory); - this.logger.log(3, () => 'Repeated command (' + numArrayToHexArray([a, b]) + ') is dropped'); - continue; - } - setLastCmd(a, b, this.cmdHistory); - cmdFound = this.parseCmd(a, b); - if (!cmdFound) { - cmdFound = this.parseMidrow(a, b); - } - if (!cmdFound) { - cmdFound = this.parsePAC(a, b); - } - if (!cmdFound) { - cmdFound = this.parseBackgroundAttributes(a, b); - } - } else { - setLastCmd(null, null, cmdHistory); - } - if (!cmdFound) { - charsFound = this.parseChars(a, b); - if (charsFound) { - const currChNr = this.currentChannel; - if (currChNr && currChNr > 0) { - const channel = this.channels[currChNr]; - channel.insertChars(charsFound); - } else { - this.logger.log(2, 'No channel found yet. TEXT-MODE?'); - } - } - } - if (!cmdFound && !charsFound) { - this.logger.log(2, () => "Couldn't parse cleaned data " + numArrayToHexArray([a, b]) + ' orig: ' + numArrayToHexArray([byteList[i], byteList[i + 1]])); - } - } - } - - /** - * Parse Command. - * @returns True if a command was found - */ - parseCmd(a, b) { - const cond1 = (a === 0x14 || a === 0x1c || a === 0x15 || a === 0x1d) && b >= 0x20 && b <= 0x2f; - const cond2 = (a === 0x17 || a === 0x1f) && b >= 0x21 && b <= 0x23; - if (!(cond1 || cond2)) { - return false; - } - const chNr = a === 0x14 || a === 0x15 || a === 0x17 ? 1 : 2; - const channel = this.channels[chNr]; - if (a === 0x14 || a === 0x15 || a === 0x1c || a === 0x1d) { - if (b === 0x20) { - channel.ccRCL(); - } else if (b === 0x21) { - channel.ccBS(); - } else if (b === 0x22) { - channel.ccAOF(); - } else if (b === 0x23) { - channel.ccAON(); - } else if (b === 0x24) { - channel.ccDER(); - } else if (b === 0x25) { - channel.ccRU(2); - } else if (b === 0x26) { - channel.ccRU(3); - } else if (b === 0x27) { - channel.ccRU(4); - } else if (b === 0x28) { - channel.ccFON(); - } else if (b === 0x29) { - channel.ccRDC(); - } else if (b === 0x2a) { - channel.ccTR(); - } else if (b === 0x2b) { - channel.ccRTD(); - } else if (b === 0x2c) { - channel.ccEDM(); - } else if (b === 0x2d) { - channel.ccCR(); - } else if (b === 0x2e) { - channel.ccENM(); - } else if (b === 0x2f) { - channel.ccEOC(); - } - } else { - // a == 0x17 || a == 0x1F - channel.ccTO(b - 0x20); - } - this.currentChannel = chNr; - return true; - } - - /** - * Parse midrow styling command - */ - parseMidrow(a, b) { - let chNr = 0; - if ((a === 0x11 || a === 0x19) && b >= 0x20 && b <= 0x2f) { - if (a === 0x11) { - chNr = 1; - } else { - chNr = 2; - } - if (chNr !== this.currentChannel) { - this.logger.log(0, 'Mismatch channel in midrow parsing'); - return false; - } - const channel = this.channels[chNr]; - if (!channel) { - return false; - } - channel.ccMIDROW(b); - this.logger.log(3, () => 'MIDROW (' + numArrayToHexArray([a, b]) + ')'); - return true; - } - return false; - } - - /** - * Parse Preable Access Codes (Table 53). - * @returns {Boolean} Tells if PAC found - */ - parsePAC(a, b) { - let row; - const case1 = (a >= 0x11 && a <= 0x17 || a >= 0x19 && a <= 0x1f) && b >= 0x40 && b <= 0x7f; - const case2 = (a === 0x10 || a === 0x18) && b >= 0x40 && b <= 0x5f; - if (!(case1 || case2)) { - return false; - } - const chNr = a <= 0x17 ? 1 : 2; - if (b >= 0x40 && b <= 0x5f) { - row = chNr === 1 ? rowsLowCh1[a] : rowsLowCh2[a]; - } else { - // 0x60 <= b <= 0x7F - row = chNr === 1 ? rowsHighCh1[a] : rowsHighCh2[a]; - } - const channel = this.channels[chNr]; - if (!channel) { - return false; - } - channel.setPAC(this.interpretPAC(row, b)); - this.currentChannel = chNr; - return true; - } - - /** - * Interpret the second byte of the pac, and return the information. - * @returns pacData with style parameters - */ - interpretPAC(row, byte) { - let pacIndex; - const pacData = { - color: null, - italics: false, - indent: null, - underline: false, - row: row - }; - if (byte > 0x5f) { - pacIndex = byte - 0x60; - } else { - pacIndex = byte - 0x40; - } - pacData.underline = (pacIndex & 1) === 1; - if (pacIndex <= 0xd) { - pacData.color = ['white', 'green', 'blue', 'cyan', 'red', 'yellow', 'magenta', 'white'][Math.floor(pacIndex / 2)]; - } else if (pacIndex <= 0xf) { - pacData.italics = true; - pacData.color = 'white'; - } else { - pacData.indent = Math.floor((pacIndex - 0x10) / 2) * 4; - } - return pacData; // Note that row has zero offset. The spec uses 1. - } - - /** - * Parse characters. - * @returns An array with 1 to 2 codes corresponding to chars, if found. null otherwise. - */ - parseChars(a, b) { - let channelNr; - let charCodes = null; - let charCode1 = null; - if (a >= 0x19) { - channelNr = 2; - charCode1 = a - 8; - } else { - channelNr = 1; - charCode1 = a; - } - if (charCode1 >= 0x11 && charCode1 <= 0x13) { - // Special character - let oneCode; - if (charCode1 === 0x11) { - oneCode = b + 0x50; - } else if (charCode1 === 0x12) { - oneCode = b + 0x70; - } else { - oneCode = b + 0x90; - } - this.logger.log(2, () => "Special char '" + getCharForByte(oneCode) + "' in channel " + channelNr); - charCodes = [oneCode]; - } else if (a >= 0x20 && a <= 0x7f) { - charCodes = b === 0 ? [a] : [a, b]; - } - if (charCodes) { - this.logger.log(3, () => 'Char codes = ' + numArrayToHexArray(charCodes).join(',')); - } - return charCodes; - } - - /** - * Parse extended background attributes as well as new foreground color black. - * @returns True if background attributes are found - */ - parseBackgroundAttributes(a, b) { - const case1 = (a === 0x10 || a === 0x18) && b >= 0x20 && b <= 0x2f; - const case2 = (a === 0x17 || a === 0x1f) && b >= 0x2d && b <= 0x2f; - if (!(case1 || case2)) { - return false; - } - let index; - const bkgData = {}; - if (a === 0x10 || a === 0x18) { - index = Math.floor((b - 0x20) / 2); - bkgData.background = backgroundColors[index]; - if (b % 2 === 1) { - bkgData.background = bkgData.background + '_semi'; - } - } else if (b === 0x2d) { - bkgData.background = 'transparent'; - } else { - bkgData.foreground = 'black'; - if (b === 0x2f) { - bkgData.underline = true; - } - } - const chNr = a <= 0x17 ? 1 : 2; - const channel = this.channels[chNr]; - channel.setBkgData(bkgData); - return true; - } - - /** - * Reset state of parser and its channels. - */ - reset() { - for (let i = 0; i < Object.keys(this.channels).length; i++) { - const channel = this.channels[i]; - if (channel) { - channel.reset(); - } - } - setLastCmd(null, null, this.cmdHistory); - } - - /** - * Trigger the generation of a cue, and the start of a new one if displayScreens are not empty. - */ - cueSplitAtTime(t) { - for (let i = 0; i < this.channels.length; i++) { - const channel = this.channels[i]; - if (channel) { - channel.cueSplitAtTime(t); - } - } - } -} -function setLastCmd(a, b, cmdHistory) { - cmdHistory.a = a; - cmdHistory.b = b; -} -function hasCmdRepeated(a, b, cmdHistory) { - return cmdHistory.a === a && cmdHistory.b === b; -} -function createCmdHistory() { - return { - a: null, - b: null - }; -} - -class OutputFilter { - constructor(timelineController, trackName) { - this.timelineController = void 0; - this.cueRanges = []; - this.trackName = void 0; - this.startTime = null; - this.endTime = null; - this.screen = null; - this.timelineController = timelineController; - this.trackName = trackName; - } - dispatchCue() { - if (this.startTime === null) { - return; - } - this.timelineController.addCues(this.trackName, this.startTime, this.endTime, this.screen, this.cueRanges); - this.startTime = null; - } - newCue(startTime, endTime, screen) { - if (this.startTime === null || this.startTime > startTime) { - this.startTime = startTime; - } - this.endTime = endTime; - this.screen = screen; - this.timelineController.createCaptionsTrack(this.trackName); - } - reset() { - this.cueRanges = []; - this.startTime = null; - } -} - -/** - * Copyright 2013 vtt.js Contributors - * - * Licensed under the Apache License, Version 2.0 (the 'License'); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an 'AS IS' BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -var VTTCue = (function () { - if (optionalSelf != null && optionalSelf.VTTCue) { - return self.VTTCue; - } - const AllowedDirections = ['', 'lr', 'rl']; - const AllowedAlignments = ['start', 'middle', 'end', 'left', 'right']; - function isAllowedValue(allowed, value) { - if (typeof value !== 'string') { - return false; - } - // necessary for assuring the generic conforms to the Array interface - if (!Array.isArray(allowed)) { - return false; - } - // reset the type so that the next narrowing works well - const lcValue = value.toLowerCase(); - // use the allow list to narrow the type to a specific subset of strings - if (~allowed.indexOf(lcValue)) { - return lcValue; - } - return false; - } - function findDirectionSetting(value) { - return isAllowedValue(AllowedDirections, value); - } - function findAlignSetting(value) { - return isAllowedValue(AllowedAlignments, value); - } - function extend(obj, ...rest) { - let i = 1; - for (; i < arguments.length; i++) { - const cobj = arguments[i]; - for (const p in cobj) { - obj[p] = cobj[p]; - } - } - return obj; - } - function VTTCue(startTime, endTime, text) { - const cue = this; - const baseObj = { - enumerable: true - }; - /** - * Shim implementation specific properties. These properties are not in - * the spec. - */ - - // Lets us know when the VTTCue's data has changed in such a way that we need - // to recompute its display state. This lets us compute its display state - // lazily. - cue.hasBeenReset = false; - - /** - * VTTCue and TextTrackCue properties - * http://dev.w3.org/html5/webvtt/#vttcue-interface - */ - - let _id = ''; - let _pauseOnExit = false; - let _startTime = startTime; - let _endTime = endTime; - let _text = text; - let _region = null; - let _vertical = ''; - let _snapToLines = true; - let _line = 'auto'; - let _lineAlign = 'start'; - let _position = 50; - let _positionAlign = 'middle'; - let _size = 50; - let _align = 'middle'; - Object.defineProperty(cue, 'id', extend({}, baseObj, { - get: function () { - return _id; - }, - set: function (value) { - _id = '' + value; - } - })); - Object.defineProperty(cue, 'pauseOnExit', extend({}, baseObj, { - get: function () { - return _pauseOnExit; - }, - set: function (value) { - _pauseOnExit = !!value; - } - })); - Object.defineProperty(cue, 'startTime', extend({}, baseObj, { - get: function () { - return _startTime; - }, - set: function (value) { - if (typeof value !== 'number') { - throw new TypeError('Start time must be set to a number.'); - } - _startTime = value; - this.hasBeenReset = true; - } - })); - Object.defineProperty(cue, 'endTime', extend({}, baseObj, { - get: function () { - return _endTime; - }, - set: function (value) { - if (typeof value !== 'number') { - throw new TypeError('End time must be set to a number.'); - } - _endTime = value; - this.hasBeenReset = true; - } - })); - Object.defineProperty(cue, 'text', extend({}, baseObj, { - get: function () { - return _text; - }, - set: function (value) { - _text = '' + value; - this.hasBeenReset = true; - } - })); - - // todo: implement VTTRegion polyfill? - Object.defineProperty(cue, 'region', extend({}, baseObj, { - get: function () { - return _region; - }, - set: function (value) { - _region = value; - this.hasBeenReset = true; - } - })); - Object.defineProperty(cue, 'vertical', extend({}, baseObj, { - get: function () { - return _vertical; - }, - set: function (value) { - const setting = findDirectionSetting(value); - // Have to check for false because the setting an be an empty string. - if (setting === false) { - throw new SyntaxError('An invalid or illegal string was specified.'); - } - _vertical = setting; - this.hasBeenReset = true; - } - })); - Object.defineProperty(cue, 'snapToLines', extend({}, baseObj, { - get: function () { - return _snapToLines; - }, - set: function (value) { - _snapToLines = !!value; - this.hasBeenReset = true; - } - })); - Object.defineProperty(cue, 'line', extend({}, baseObj, { - get: function () { - return _line; - }, - set: function (value) { - if (typeof value !== 'number' && value !== 'auto') { - throw new SyntaxError('An invalid number or illegal string was specified.'); - } - _line = value; - this.hasBeenReset = true; - } - })); - Object.defineProperty(cue, 'lineAlign', extend({}, baseObj, { - get: function () { - return _lineAlign; - }, - set: function (value) { - const setting = findAlignSetting(value); - if (!setting) { - throw new SyntaxError('An invalid or illegal string was specified.'); - } - _lineAlign = setting; - this.hasBeenReset = true; - } - })); - Object.defineProperty(cue, 'position', extend({}, baseObj, { - get: function () { - return _position; - }, - set: function (value) { - if (value < 0 || value > 100) { - throw new Error('Position must be between 0 and 100.'); - } - _position = value; - this.hasBeenReset = true; - } - })); - Object.defineProperty(cue, 'positionAlign', extend({}, baseObj, { - get: function () { - return _positionAlign; - }, - set: function (value) { - const setting = findAlignSetting(value); - if (!setting) { - throw new SyntaxError('An invalid or illegal string was specified.'); - } - _positionAlign = setting; - this.hasBeenReset = true; - } - })); - Object.defineProperty(cue, 'size', extend({}, baseObj, { - get: function () { - return _size; - }, - set: function (value) { - if (value < 0 || value > 100) { - throw new Error('Size must be between 0 and 100.'); - } - _size = value; - this.hasBeenReset = true; - } - })); - Object.defineProperty(cue, 'align', extend({}, baseObj, { - get: function () { - return _align; - }, - set: function (value) { - const setting = findAlignSetting(value); - if (!setting) { - throw new SyntaxError('An invalid or illegal string was specified.'); - } - _align = setting; - this.hasBeenReset = true; - } - })); - - /** - * Other <track> spec defined properties - */ - - // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-video-element.html#text-track-cue-display-state - cue.displayState = undefined; - } - - /** - * VTTCue methods - */ - - VTTCue.prototype.getCueAsHTML = function () { - // Assume WebVTT.convertCueToDOMTree is on the global. - const WebVTT = self.WebVTT; - return WebVTT.convertCueToDOMTree(self, this.text); - }; - // this is a polyfill hack - return VTTCue; -})(); - -/* - * Source: https://github.com/mozilla/vtt.js/blob/master/dist/vtt.js - */ - -class StringDecoder { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - decode(data, options) { - if (!data) { - return ''; - } - if (typeof data !== 'string') { - throw new Error('Error - expected string data.'); - } - return decodeURIComponent(encodeURIComponent(data)); - } -} - -// Try to parse input as a time stamp. -function parseTimeStamp(input) { - function computeSeconds(h, m, s, f) { - return (h | 0) * 3600 + (m | 0) * 60 + (s | 0) + parseFloat(f || 0); - } - const m = input.match(/^(?:(\d+):)?(\d{2}):(\d{2})(\.\d+)?/); - if (!m) { - return null; - } - if (parseFloat(m[2]) > 59) { - // Timestamp takes the form of [hours]:[minutes].[milliseconds] - // First position is hours as it's over 59. - return computeSeconds(m[2], m[3], 0, m[4]); - } - // Timestamp takes the form of [hours (optional)]:[minutes]:[seconds].[milliseconds] - return computeSeconds(m[1], m[2], m[3], m[4]); -} - -// A settings object holds key/value pairs and will ignore anything but the first -// assignment to a specific key. -class Settings { - constructor() { - this.values = Object.create(null); - } - // Only accept the first assignment to any key. - set(k, v) { - if (!this.get(k) && v !== '') { - this.values[k] = v; - } - } - // Return the value for a key, or a default value. - // If 'defaultKey' is passed then 'dflt' is assumed to be an object with - // a number of possible default values as properties where 'defaultKey' is - // the key of the property that will be chosen; otherwise it's assumed to be - // a single value. - get(k, dflt, defaultKey) { - if (defaultKey) { - return this.has(k) ? this.values[k] : dflt[defaultKey]; - } - return this.has(k) ? this.values[k] : dflt; - } - // Check whether we have a value for a key. - has(k) { - return k in this.values; - } - // Accept a setting if its one of the given alternatives. - alt(k, v, a) { - for (let n = 0; n < a.length; ++n) { - if (v === a[n]) { - this.set(k, v); - break; - } - } - } - // Accept a setting if its a valid (signed) integer. - integer(k, v) { - if (/^-?\d+$/.test(v)) { - // integer - this.set(k, parseInt(v, 10)); - } - } - // Accept a setting if its a valid percentage. - percent(k, v) { - if (/^([\d]{1,3})(\.[\d]*)?%$/.test(v)) { - const percent = parseFloat(v); - if (percent >= 0 && percent <= 100) { - this.set(k, percent); - return true; - } - } - return false; - } -} - -// Helper function to parse input into groups separated by 'groupDelim', and -// interpret each group as a key/value pair separated by 'keyValueDelim'. -function parseOptions(input, callback, keyValueDelim, groupDelim) { - const groups = groupDelim ? input.split(groupDelim) : [input]; - for (const i in groups) { - if (typeof groups[i] !== 'string') { - continue; - } - const kv = groups[i].split(keyValueDelim); - if (kv.length !== 2) { - continue; - } - const k = kv[0]; - const v = kv[1]; - callback(k, v); - } -} -const defaults = new VTTCue(0, 0, ''); -// 'middle' was changed to 'center' in the spec: https://github.com/w3c/webvtt/pull/244 -// Safari doesn't yet support this change, but FF and Chrome do. -const center = defaults.align === 'middle' ? 'middle' : 'center'; -function parseCue(input, cue, regionList) { - // Remember the original input if we need to throw an error. - const oInput = input; - // 4.1 WebVTT timestamp - function consumeTimeStamp() { - const ts = parseTimeStamp(input); - if (ts === null) { - throw new Error('Malformed timestamp: ' + oInput); - } - - // Remove time stamp from input. - input = input.replace(/^[^\sa-zA-Z-]+/, ''); - return ts; - } - - // 4.4.2 WebVTT cue settings - function consumeCueSettings(input, cue) { - const settings = new Settings(); - parseOptions(input, function (k, v) { - let vals; - switch (k) { - case 'region': - // Find the last region we parsed with the same region id. - for (let i = regionList.length - 1; i >= 0; i--) { - if (regionList[i].id === v) { - settings.set(k, regionList[i].region); - break; - } - } - break; - case 'vertical': - settings.alt(k, v, ['rl', 'lr']); - break; - case 'line': - vals = v.split(','); - settings.integer(k, vals[0]); - if (settings.percent(k, vals[0])) { - settings.set('snapToLines', false); - } - settings.alt(k, vals[0], ['auto']); - if (vals.length === 2) { - settings.alt('lineAlign', vals[1], ['start', center, 'end']); - } - break; - case 'position': - vals = v.split(','); - settings.percent(k, vals[0]); - if (vals.length === 2) { - settings.alt('positionAlign', vals[1], ['start', center, 'end', 'line-left', 'line-right', 'auto']); - } - break; - case 'size': - settings.percent(k, v); - break; - case 'align': - settings.alt(k, v, ['start', center, 'end', 'left', 'right']); - break; - } - }, /:/, /\s/); - - // Apply default values for any missing fields. - cue.region = settings.get('region', null); - cue.vertical = settings.get('vertical', ''); - let line = settings.get('line', 'auto'); - if (line === 'auto' && defaults.line === -1) { - // set numeric line number for Safari - line = -1; - } - cue.line = line; - cue.lineAlign = settings.get('lineAlign', 'start'); - cue.snapToLines = settings.get('snapToLines', true); - cue.size = settings.get('size', 100); - cue.align = settings.get('align', center); - let position = settings.get('position', 'auto'); - if (position === 'auto' && defaults.position === 50) { - // set numeric position for Safari - position = cue.align === 'start' || cue.align === 'left' ? 0 : cue.align === 'end' || cue.align === 'right' ? 100 : 50; - } - cue.position = position; - } - function skipWhitespace() { - input = input.replace(/^\s+/, ''); - } - - // 4.1 WebVTT cue timings. - skipWhitespace(); - cue.startTime = consumeTimeStamp(); // (1) collect cue start time - skipWhitespace(); - if (input.slice(0, 3) !== '-->') { - // (3) next characters must match '-->' - throw new Error("Malformed time stamp (time stamps must be separated by '-->'): " + oInput); - } - input = input.slice(3); - skipWhitespace(); - cue.endTime = consumeTimeStamp(); // (5) collect cue end time - - // 4.1 WebVTT cue settings list. - skipWhitespace(); - consumeCueSettings(input, cue); -} -function fixLineBreaks(input) { - return input.replace(/<br(?: \/)?>/gi, '\n'); -} -class VTTParser { - constructor() { - this.state = 'INITIAL'; - this.buffer = ''; - this.decoder = new StringDecoder(); - this.regionList = []; - this.cue = null; - this.oncue = void 0; - this.onparsingerror = void 0; - this.onflush = void 0; - } - parse(data) { - const _this = this; - - // If there is no data then we won't decode it, but will just try to parse - // whatever is in buffer already. This may occur in circumstances, for - // example when flush() is called. - if (data) { - // Try to decode the data that we received. - _this.buffer += _this.decoder.decode(data, { - stream: true - }); - } - function collectNextLine() { - let buffer = _this.buffer; - let pos = 0; - buffer = fixLineBreaks(buffer); - while (pos < buffer.length && buffer[pos] !== '\r' && buffer[pos] !== '\n') { - ++pos; - } - const line = buffer.slice(0, pos); - // Advance the buffer early in case we fail below. - if (buffer[pos] === '\r') { - ++pos; - } - if (buffer[pos] === '\n') { - ++pos; - } - _this.buffer = buffer.slice(pos); - return line; - } - - // 3.2 WebVTT metadata header syntax - function parseHeader(input) { - parseOptions(input, function (k, v) { - // switch (k) { - // case 'region': - // 3.3 WebVTT region metadata header syntax - // console.log('parse region', v); - // parseRegion(v); - // break; - // } - }, /:/); - } - - // 5.1 WebVTT file parsing. - try { - let line = ''; - if (_this.state === 'INITIAL') { - // We can't start parsing until we have the first line. - if (!/\r\n|\n/.test(_this.buffer)) { - return this; - } - line = collectNextLine(); - // strip of UTF-8 BOM if any - // https://en.wikipedia.org/wiki/Byte_order_mark#UTF-8 - const m = line.match(/^()?WEBVTT([ \t].*)?$/); - if (!(m != null && m[0])) { - throw new Error('Malformed WebVTT signature.'); - } - _this.state = 'HEADER'; - } - let alreadyCollectedLine = false; - while (_this.buffer) { - // We can't parse a line until we have the full line. - if (!/\r\n|\n/.test(_this.buffer)) { - return this; - } - if (!alreadyCollectedLine) { - line = collectNextLine(); - } else { - alreadyCollectedLine = false; - } - switch (_this.state) { - case 'HEADER': - // 13-18 - Allow a header (metadata) under the WEBVTT line. - if (/:/.test(line)) { - parseHeader(line); - } else if (!line) { - // An empty line terminates the header and starts the body (cues). - _this.state = 'ID'; - } - continue; - case 'NOTE': - // Ignore NOTE blocks. - if (!line) { - _this.state = 'ID'; - } - continue; - case 'ID': - // Check for the start of NOTE blocks. - if (/^NOTE($|[ \t])/.test(line)) { - _this.state = 'NOTE'; - break; - } - // 19-29 - Allow any number of line terminators, then initialize new cue values. - if (!line) { - continue; - } - _this.cue = new VTTCue(0, 0, ''); - _this.state = 'CUE'; - // 30-39 - Check if self line contains an optional identifier or timing data. - if (line.indexOf('-->') === -1) { - _this.cue.id = line; - continue; - } - // Process line as start of a cue. - /* falls through */ - case 'CUE': - // 40 - Collect cue timings and settings. - if (!_this.cue) { - _this.state = 'BADCUE'; - continue; - } - try { - parseCue(line, _this.cue, _this.regionList); - } catch (e) { - // In case of an error ignore rest of the cue. - _this.cue = null; - _this.state = 'BADCUE'; - continue; - } - _this.state = 'CUETEXT'; - continue; - case 'CUETEXT': - { - const hasSubstring = line.indexOf('-->') !== -1; - // 34 - If we have an empty line then report the cue. - // 35 - If we have the special substring '-->' then report the cue, - // but do not collect the line as we need to process the current - // one as a new cue. - if (!line || hasSubstring && (alreadyCollectedLine = true)) { - // We are done parsing self cue. - if (_this.oncue && _this.cue) { - _this.oncue(_this.cue); - } - _this.cue = null; - _this.state = 'ID'; - continue; - } - if (_this.cue === null) { - continue; - } - if (_this.cue.text) { - _this.cue.text += '\n'; - } - _this.cue.text += line; - } - continue; - case 'BADCUE': - // 54-62 - Collect and discard the remaining cue. - if (!line) { - _this.state = 'ID'; - } - } - } - } catch (e) { - // If we are currently parsing a cue, report what we have. - if (_this.state === 'CUETEXT' && _this.cue && _this.oncue) { - _this.oncue(_this.cue); - } - _this.cue = null; - // Enter BADWEBVTT state if header was not parsed correctly otherwise - // another exception occurred so enter BADCUE state. - _this.state = _this.state === 'INITIAL' ? 'BADWEBVTT' : 'BADCUE'; - } - return this; - } - flush() { - const _this = this; - try { - // Finish decoding the stream. - // _this.buffer += _this.decoder.decode(); - // Synthesize the end of the current cue or region. - if (_this.cue || _this.state === 'HEADER') { - _this.buffer += '\n\n'; - _this.parse(); - } - // If we've flushed, parsed, and we're still on the INITIAL state then - // that means we don't have enough of the stream to parse the first - // line. - if (_this.state === 'INITIAL' || _this.state === 'BADWEBVTT') { - throw new Error('Malformed WebVTT signature.'); - } - } catch (e) { - if (_this.onparsingerror) { - _this.onparsingerror(e); - } - } - if (_this.onflush) { - _this.onflush(); - } - return this; - } -} - -// From https://github.com/darkskyapp/string-hash -function hash(text) { - let hash = 5381; - let i = text.length; - while (i) { - hash = hash * 33 ^ text.charCodeAt(--i); - } - return (hash >>> 0).toString(); -} - -const LINEBREAKS = /\r\n|\n\r|\n|\r/g; - -// String.prototype.startsWith is not supported in IE11 -const startsWith = function startsWith(inputString, searchString, position = 0) { - return inputString.slice(position, position + searchString.length) === searchString; -}; -const cueString2millis = function cueString2millis(timeString) { - let ts = parseInt(timeString.slice(-3)); - const secs = parseInt(timeString.slice(-6, -4)); - const mins = parseInt(timeString.slice(-9, -7)); - const hours = timeString.length > 9 ? parseInt(timeString.substring(0, timeString.indexOf(':'))) : 0; - if (!isFiniteNumber(ts) || !isFiniteNumber(secs) || !isFiniteNumber(mins) || !isFiniteNumber(hours)) { - throw Error(`Malformed X-TIMESTAMP-MAP: Local:${timeString}`); - } - ts += 1000 * secs; - ts += 60 * 1000 * mins; - ts += 60 * 60 * 1000 * hours; - return ts; -}; - -// Create a unique hash id for a cue based on start/end times and text. -// This helps timeline-controller to avoid showing repeated captions. -function generateCueId(startTime, endTime, text) { - return hash(startTime.toString()) + hash(endTime.toString()) + hash(text); -} -const calculateOffset = function calculateOffset(vttCCs, cc, presentationTime) { - let currCC = vttCCs[cc]; - let prevCC = vttCCs[currCC.prevCC]; - - // This is the first discontinuity or cues have been processed since the last discontinuity - // Offset = current discontinuity time - if (!prevCC || !prevCC.new && currCC.new) { - vttCCs.ccOffset = vttCCs.presentationOffset = currCC.start; - currCC.new = false; - return; - } - - // There have been discontinuities since cues were last parsed. - // Offset = time elapsed - while ((_prevCC = prevCC) != null && _prevCC.new) { - var _prevCC; - vttCCs.ccOffset += currCC.start - prevCC.start; - currCC.new = false; - currCC = prevCC; - prevCC = vttCCs[currCC.prevCC]; - } - vttCCs.presentationOffset = presentationTime; -}; -function parseWebVTT(vttByteArray, initPTS, vttCCs, cc, timeOffset, callBack, errorCallBack) { - const parser = new VTTParser(); - // Convert byteArray into string, replacing any somewhat exotic linefeeds with "\n", then split on that character. - // Uint8Array.prototype.reduce is not implemented in IE11 - const vttLines = utf8ArrayToStr(new Uint8Array(vttByteArray)).trim().replace(LINEBREAKS, '\n').split('\n'); - const cues = []; - const init90kHz = initPTS ? toMpegTsClockFromTimescale(initPTS.baseTime, initPTS.timescale) : 0; - let cueTime = '00:00.000'; - let timestampMapMPEGTS = 0; - let timestampMapLOCAL = 0; - let parsingError; - let inHeader = true; - parser.oncue = function (cue) { - // Adjust cue timing; clamp cues to start no earlier than - and drop cues that don't end after - 0 on timeline. - const currCC = vttCCs[cc]; - let cueOffset = vttCCs.ccOffset; - - // Calculate subtitle PTS offset - const webVttMpegTsMapOffset = (timestampMapMPEGTS - init90kHz) / 90000; - - // Update offsets for new discontinuities - if (currCC != null && currCC.new) { - if (timestampMapLOCAL !== undefined) { - // When local time is provided, offset = discontinuity start time - local time - cueOffset = vttCCs.ccOffset = currCC.start; - } else { - calculateOffset(vttCCs, cc, webVttMpegTsMapOffset); - } - } - if (webVttMpegTsMapOffset) { - if (!initPTS) { - parsingError = new Error('Missing initPTS for VTT MPEGTS'); - return; - } - // If we have MPEGTS, offset = presentation time + discontinuity offset - cueOffset = webVttMpegTsMapOffset - vttCCs.presentationOffset; - } - const duration = cue.endTime - cue.startTime; - const startTime = normalizePts((cue.startTime + cueOffset - timestampMapLOCAL) * 90000, timeOffset * 90000) / 90000; - cue.startTime = Math.max(startTime, 0); - cue.endTime = Math.max(startTime + duration, 0); - - //trim trailing webvtt block whitespaces - const text = cue.text.trim(); - - // Fix encoding of special characters - cue.text = decodeURIComponent(encodeURIComponent(text)); - - // If the cue was not assigned an id from the VTT file (line above the content), create one. - if (!cue.id) { - cue.id = generateCueId(cue.startTime, cue.endTime, text); - } - if (cue.endTime > 0) { - cues.push(cue); - } - }; - parser.onparsingerror = function (error) { - parsingError = error; - }; - parser.onflush = function () { - if (parsingError) { - errorCallBack(parsingError); - return; - } - callBack(cues); - }; - - // Go through contents line by line. - vttLines.forEach(line => { - if (inHeader) { - // Look for X-TIMESTAMP-MAP in header. - if (startsWith(line, 'X-TIMESTAMP-MAP=')) { - // Once found, no more are allowed anyway, so stop searching. - inHeader = false; - // Extract LOCAL and MPEGTS. - line.slice(16).split(',').forEach(timestamp => { - if (startsWith(timestamp, 'LOCAL:')) { - cueTime = timestamp.slice(6); - } else if (startsWith(timestamp, 'MPEGTS:')) { - timestampMapMPEGTS = parseInt(timestamp.slice(7)); - } - }); - try { - // Convert cue time to seconds - timestampMapLOCAL = cueString2millis(cueTime) / 1000; - } catch (error) { - parsingError = error; - } - // Return without parsing X-TIMESTAMP-MAP line. - return; - } else if (line === '') { - inHeader = false; - } - } - // Parse line by default. - parser.parse(line + '\n'); - }); - parser.flush(); -} - -const IMSC1_CODEC = 'stpp.ttml.im1t'; - -// Time format: h:m:s:frames(.subframes) -const HMSF_REGEX = /^(\d{2,}):(\d{2}):(\d{2}):(\d{2})\.?(\d+)?$/; - -// Time format: hours, minutes, seconds, milliseconds, frames, ticks -const TIME_UNIT_REGEX = /^(\d*(?:\.\d*)?)(h|m|s|ms|f|t)$/; -const textAlignToLineAlign = { - left: 'start', - center: 'center', - right: 'end', - start: 'start', - end: 'end' -}; -function parseIMSC1(payload, initPTS, callBack, errorCallBack) { - const results = findBox(new Uint8Array(payload), ['mdat']); - if (results.length === 0) { - errorCallBack(new Error('Could not parse IMSC1 mdat')); - return; - } - const ttmlList = results.map(mdat => utf8ArrayToStr(mdat)); - const syncTime = toTimescaleFromScale(initPTS.baseTime, 1, initPTS.timescale); - try { - ttmlList.forEach(ttml => callBack(parseTTML(ttml, syncTime))); - } catch (error) { - errorCallBack(error); - } -} -function parseTTML(ttml, syncTime) { - const parser = new DOMParser(); - const xmlDoc = parser.parseFromString(ttml, 'text/xml'); - const tt = xmlDoc.getElementsByTagName('tt')[0]; - if (!tt) { - throw new Error('Invalid ttml'); - } - const defaultRateInfo = { - frameRate: 30, - subFrameRate: 1, - frameRateMultiplier: 0, - tickRate: 0 - }; - const rateInfo = Object.keys(defaultRateInfo).reduce((result, key) => { - result[key] = tt.getAttribute(`ttp:${key}`) || defaultRateInfo[key]; - return result; - }, {}); - const trim = tt.getAttribute('xml:space') !== 'preserve'; - const styleElements = collectionToDictionary(getElementCollection(tt, 'styling', 'style')); - const regionElements = collectionToDictionary(getElementCollection(tt, 'layout', 'region')); - const cueElements = getElementCollection(tt, 'body', '[begin]'); - return [].map.call(cueElements, cueElement => { - const cueText = getTextContent(cueElement, trim); - if (!cueText || !cueElement.hasAttribute('begin')) { - return null; - } - const startTime = parseTtmlTime(cueElement.getAttribute('begin'), rateInfo); - const duration = parseTtmlTime(cueElement.getAttribute('dur'), rateInfo); - let endTime = parseTtmlTime(cueElement.getAttribute('end'), rateInfo); - if (startTime === null) { - throw timestampParsingError(cueElement); - } - if (endTime === null) { - if (duration === null) { - throw timestampParsingError(cueElement); - } - endTime = startTime + duration; - } - const cue = new VTTCue(startTime - syncTime, endTime - syncTime, cueText); - cue.id = generateCueId(cue.startTime, cue.endTime, cue.text); - const region = regionElements[cueElement.getAttribute('region')]; - const style = styleElements[cueElement.getAttribute('style')]; - - // Apply styles to cue - const styles = getTtmlStyles(region, style, styleElements); - const { - textAlign - } = styles; - if (textAlign) { - // cue.positionAlign not settable in FF~2016 - const lineAlign = textAlignToLineAlign[textAlign]; - if (lineAlign) { - cue.lineAlign = lineAlign; - } - cue.align = textAlign; - } - _extends(cue, styles); - return cue; - }).filter(cue => cue !== null); -} -function getElementCollection(fromElement, parentName, childName) { - const parent = fromElement.getElementsByTagName(parentName)[0]; - if (parent) { - return [].slice.call(parent.querySelectorAll(childName)); - } - return []; -} -function collectionToDictionary(elementsWithId) { - return elementsWithId.reduce((dict, element) => { - const id = element.getAttribute('xml:id'); - if (id) { - dict[id] = element; - } - return dict; - }, {}); -} -function getTextContent(element, trim) { - return [].slice.call(element.childNodes).reduce((str, node, i) => { - var _node$childNodes; - if (node.nodeName === 'br' && i) { - return str + '\n'; - } - if ((_node$childNodes = node.childNodes) != null && _node$childNodes.length) { - return getTextContent(node, trim); - } else if (trim) { - return str + node.textContent.trim().replace(/\s+/g, ' '); - } - return str + node.textContent; - }, ''); -} -function getTtmlStyles(region, style, styleElements) { - const ttsNs = 'http://www.w3.org/ns/ttml#styling'; - let regionStyle = null; - const styleAttributes = ['displayAlign', 'textAlign', 'color', 'backgroundColor', 'fontSize', 'fontFamily' - // 'fontWeight', - // 'lineHeight', - // 'wrapOption', - // 'fontStyle', - // 'direction', - // 'writingMode' - ]; - const regionStyleName = region != null && region.hasAttribute('style') ? region.getAttribute('style') : null; - if (regionStyleName && styleElements.hasOwnProperty(regionStyleName)) { - regionStyle = styleElements[regionStyleName]; - } - return styleAttributes.reduce((styles, name) => { - const value = getAttributeNS(style, ttsNs, name) || getAttributeNS(region, ttsNs, name) || getAttributeNS(regionStyle, ttsNs, name); - if (value) { - styles[name] = value; - } - return styles; - }, {}); -} -function getAttributeNS(element, ns, name) { - if (!element) { - return null; - } - return element.hasAttributeNS(ns, name) ? element.getAttributeNS(ns, name) : null; -} -function timestampParsingError(node) { - return new Error(`Could not parse ttml timestamp ${node}`); -} -function parseTtmlTime(timeAttributeValue, rateInfo) { - if (!timeAttributeValue) { - return null; - } - let seconds = parseTimeStamp(timeAttributeValue); - if (seconds === null) { - if (HMSF_REGEX.test(timeAttributeValue)) { - seconds = parseHoursMinutesSecondsFrames(timeAttributeValue, rateInfo); - } else if (TIME_UNIT_REGEX.test(timeAttributeValue)) { - seconds = parseTimeUnits(timeAttributeValue, rateInfo); - } - } - return seconds; -} -function parseHoursMinutesSecondsFrames(timeAttributeValue, rateInfo) { - const m = HMSF_REGEX.exec(timeAttributeValue); - const frames = (m[4] | 0) + (m[5] | 0) / rateInfo.subFrameRate; - return (m[1] | 0) * 3600 + (m[2] | 0) * 60 + (m[3] | 0) + frames / rateInfo.frameRate; -} -function parseTimeUnits(timeAttributeValue, rateInfo) { - const m = TIME_UNIT_REGEX.exec(timeAttributeValue); - const value = Number(m[1]); - const unit = m[2]; - switch (unit) { - case 'h': - return value * 3600; - case 'm': - return value * 60; - case 'ms': - return value * 1000; - case 'f': - return value / rateInfo.frameRate; - case 't': - return value / rateInfo.tickRate; - } - return value; -} - -class TimelineController { - constructor(hls) { - this.hls = void 0; - this.media = null; - this.config = void 0; - this.enabled = true; - this.Cues = void 0; - this.textTracks = []; - this.tracks = []; - this.initPTS = []; - this.unparsedVttFrags = []; - this.captionsTracks = {}; - this.nonNativeCaptionsTracks = {}; - this.cea608Parser1 = void 0; - this.cea608Parser2 = void 0; - this.lastCc = -1; - // Last video (CEA-608) fragment CC - this.lastSn = -1; - // Last video (CEA-608) fragment MSN - this.lastPartIndex = -1; - // Last video (CEA-608) fragment Part Index - this.prevCC = -1; - // Last subtitle fragment CC - this.vttCCs = newVTTCCs(); - this.captionsProperties = void 0; - this.hls = hls; - this.config = hls.config; - this.Cues = hls.config.cueHandler; - this.captionsProperties = { - textTrack1: { - label: this.config.captionsTextTrack1Label, - languageCode: this.config.captionsTextTrack1LanguageCode - }, - textTrack2: { - label: this.config.captionsTextTrack2Label, - languageCode: this.config.captionsTextTrack2LanguageCode - }, - textTrack3: { - label: this.config.captionsTextTrack3Label, - languageCode: this.config.captionsTextTrack3LanguageCode - }, - textTrack4: { - label: this.config.captionsTextTrack4Label, - languageCode: this.config.captionsTextTrack4LanguageCode - } - }; - hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); - hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this); - hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); - hls.on(Events.MANIFEST_LOADED, this.onManifestLoaded, this); - hls.on(Events.SUBTITLE_TRACKS_UPDATED, this.onSubtitleTracksUpdated, this); - hls.on(Events.FRAG_LOADING, this.onFragLoading, this); - hls.on(Events.FRAG_LOADED, this.onFragLoaded, this); - hls.on(Events.FRAG_PARSING_USERDATA, this.onFragParsingUserdata, this); - hls.on(Events.FRAG_DECRYPTED, this.onFragDecrypted, this); - hls.on(Events.INIT_PTS_FOUND, this.onInitPtsFound, this); - hls.on(Events.SUBTITLE_TRACKS_CLEARED, this.onSubtitleTracksCleared, this); - hls.on(Events.BUFFER_FLUSHING, this.onBufferFlushing, this); - } - destroy() { - const { - hls - } = this; - hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); - hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this); - hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); - hls.off(Events.MANIFEST_LOADED, this.onManifestLoaded, this); - hls.off(Events.SUBTITLE_TRACKS_UPDATED, this.onSubtitleTracksUpdated, this); - hls.off(Events.FRAG_LOADING, this.onFragLoading, this); - hls.off(Events.FRAG_LOADED, this.onFragLoaded, this); - hls.off(Events.FRAG_PARSING_USERDATA, this.onFragParsingUserdata, this); - hls.off(Events.FRAG_DECRYPTED, this.onFragDecrypted, this); - hls.off(Events.INIT_PTS_FOUND, this.onInitPtsFound, this); - hls.off(Events.SUBTITLE_TRACKS_CLEARED, this.onSubtitleTracksCleared, this); - hls.off(Events.BUFFER_FLUSHING, this.onBufferFlushing, this); - // @ts-ignore - this.hls = this.config = this.media = null; - this.cea608Parser1 = this.cea608Parser2 = undefined; - } - initCea608Parsers() { - const channel1 = new OutputFilter(this, 'textTrack1'); - const channel2 = new OutputFilter(this, 'textTrack2'); - const channel3 = new OutputFilter(this, 'textTrack3'); - const channel4 = new OutputFilter(this, 'textTrack4'); - this.cea608Parser1 = new Cea608Parser(1, channel1, channel2); - this.cea608Parser2 = new Cea608Parser(3, channel3, channel4); - } - addCues(trackName, startTime, endTime, screen, cueRanges) { - // skip cues which overlap more than 50% with previously parsed time ranges - let merged = false; - for (let i = cueRanges.length; i--;) { - const cueRange = cueRanges[i]; - const overlap = intersection(cueRange[0], cueRange[1], startTime, endTime); - if (overlap >= 0) { - cueRange[0] = Math.min(cueRange[0], startTime); - cueRange[1] = Math.max(cueRange[1], endTime); - merged = true; - if (overlap / (endTime - startTime) > 0.5) { - return; - } - } - } - if (!merged) { - cueRanges.push([startTime, endTime]); - } - if (this.config.renderTextTracksNatively) { - const track = this.captionsTracks[trackName]; - this.Cues.newCue(track, startTime, endTime, screen); - } else { - const cues = this.Cues.newCue(null, startTime, endTime, screen); - this.hls.trigger(Events.CUES_PARSED, { - type: 'captions', - cues, - track: trackName - }); - } - } - - // Triggered when an initial PTS is found; used for synchronisation of WebVTT. - onInitPtsFound(event, { - frag, - id, - initPTS, - timescale - }) { - const { - unparsedVttFrags - } = this; - if (id === PlaylistLevelType.MAIN) { - this.initPTS[frag.cc] = { - baseTime: initPTS, - timescale - }; - } - - // Due to asynchronous processing, initial PTS may arrive later than the first VTT fragments are loaded. - // Parse any unparsed fragments upon receiving the initial PTS. - if (unparsedVttFrags.length) { - this.unparsedVttFrags = []; - unparsedVttFrags.forEach(frag => { - this.onFragLoaded(Events.FRAG_LOADED, frag); - }); - } - } - getExistingTrack(label, language) { - const { - media - } = this; - if (media) { - for (let i = 0; i < media.textTracks.length; i++) { - const textTrack = media.textTracks[i]; - if (canReuseVttTextTrack(textTrack, { - name: label, - lang: language, - characteristics: 'transcribes-spoken-dialog,describes-music-and-sound', - attrs: {} - })) { - return textTrack; - } - } - } - return null; - } - createCaptionsTrack(trackName) { - if (this.config.renderTextTracksNatively) { - this.createNativeTrack(trackName); - } else { - this.createNonNativeTrack(trackName); - } - } - createNativeTrack(trackName) { - if (this.captionsTracks[trackName]) { - return; - } - const { - captionsProperties, - captionsTracks, - media - } = this; - const { - label, - languageCode - } = captionsProperties[trackName]; - // Enable reuse of existing text track. - const existingTrack = this.getExistingTrack(label, languageCode); - if (!existingTrack) { - const textTrack = this.createTextTrack('captions', label, languageCode); - if (textTrack) { - // Set a special property on the track so we know it's managed by Hls.js - textTrack[trackName] = true; - captionsTracks[trackName] = textTrack; - } - } else { - captionsTracks[trackName] = existingTrack; - clearCurrentCues(captionsTracks[trackName]); - sendAddTrackEvent(captionsTracks[trackName], media); - } - } - createNonNativeTrack(trackName) { - if (this.nonNativeCaptionsTracks[trackName]) { - return; - } - // Create a list of a single track for the provider to consume - const trackProperties = this.captionsProperties[trackName]; - if (!trackProperties) { - return; - } - const label = trackProperties.label; - const track = { - _id: trackName, - label, - kind: 'captions', - default: trackProperties.media ? !!trackProperties.media.default : false, - closedCaptions: trackProperties.media - }; - this.nonNativeCaptionsTracks[trackName] = track; - this.hls.trigger(Events.NON_NATIVE_TEXT_TRACKS_FOUND, { - tracks: [track] - }); - } - createTextTrack(kind, label, lang) { - const media = this.media; - if (!media) { - return; - } - return media.addTextTrack(kind, label, lang); - } - onMediaAttaching(event, data) { - this.media = data.media; - if (!data.mediaSource) { - this._cleanTracks(); - } - } - onMediaDetaching(event, data) { - const transferringMedia = !!data.transferMedia; - this.media = null; - if (transferringMedia) { - return; - } - const { - captionsTracks - } = this; - Object.keys(captionsTracks).forEach(trackName => { - clearCurrentCues(captionsTracks[trackName]); - delete captionsTracks[trackName]; - }); - this.nonNativeCaptionsTracks = {}; - } - onManifestLoading() { - // Detect discontinuity in video fragment (CEA-608) parsing - this.lastCc = -1; - this.lastSn = -1; - this.lastPartIndex = -1; - // Detect discontinuity in subtitle manifests - this.prevCC = -1; - this.vttCCs = newVTTCCs(); - // Reset tracks - this._cleanTracks(); - this.tracks = []; - this.captionsTracks = {}; - this.nonNativeCaptionsTracks = {}; - this.textTracks = []; - this.unparsedVttFrags = []; - this.initPTS = []; - if (this.cea608Parser1 && this.cea608Parser2) { - this.cea608Parser1.reset(); - this.cea608Parser2.reset(); - } - } - _cleanTracks() { - // clear outdated subtitles - const { - media - } = this; - if (!media) { - return; - } - const textTracks = media.textTracks; - if (textTracks) { - for (let i = 0; i < textTracks.length; i++) { - clearCurrentCues(textTracks[i]); - } - } - } - onSubtitleTracksUpdated(event, data) { - const tracks = data.subtitleTracks || []; - const hasIMSC1 = tracks.some(track => track.textCodec === IMSC1_CODEC); - if (this.config.enableWebVTT || hasIMSC1 && this.config.enableIMSC1) { - const listIsIdentical = subtitleOptionsIdentical(this.tracks, tracks); - if (listIsIdentical) { - this.tracks = tracks; - return; - } - this.textTracks = []; - this.tracks = tracks; - if (this.config.renderTextTracksNatively) { - const media = this.media; - const inUseTracks = media ? filterSubtitleTracks(media.textTracks) : null; - this.tracks.forEach((track, index) => { - // Reuse tracks with the same label and lang, but do not reuse 608/708 tracks - let textTrack; - if (inUseTracks) { - let inUseTrack = null; - for (let i = 0; i < inUseTracks.length; i++) { - if (inUseTracks[i] && canReuseVttTextTrack(inUseTracks[i], track)) { - inUseTrack = inUseTracks[i]; - inUseTracks[i] = null; - break; - } - } - if (inUseTrack) { - textTrack = inUseTrack; - } - } - if (textTrack) { - clearCurrentCues(textTrack); - } else { - const textTrackKind = captionsOrSubtitlesFromCharacteristics(track); - textTrack = this.createTextTrack(textTrackKind, track.name, track.lang); - if (textTrack) { - textTrack.mode = 'disabled'; - } - } - if (textTrack) { - this.textTracks.push(textTrack); - } - }); - // Warn when video element has captions or subtitle TextTracks carried over from another source - if (inUseTracks != null && inUseTracks.length) { - const unusedTextTracks = inUseTracks.filter(t => t !== null).map(t => t.label); - if (unusedTextTracks.length) { - this.hls.logger.warn(`Media element contains unused subtitle tracks: ${unusedTextTracks.join(', ')}. Replace media element for each source to clear TextTracks and captions menu.`); - } - } - } else if (this.tracks.length) { - // Create a list of tracks for the provider to consume - const tracksList = this.tracks.map(track => { - return { - label: track.name, - kind: track.type.toLowerCase(), - default: track.default, - subtitleTrack: track - }; - }); - this.hls.trigger(Events.NON_NATIVE_TEXT_TRACKS_FOUND, { - tracks: tracksList - }); - } - } - } - onManifestLoaded(event, data) { - if (this.config.enableCEA708Captions && data.captions) { - data.captions.forEach(captionsTrack => { - const instreamIdMatch = /(?:CC|SERVICE)([1-4])/.exec(captionsTrack.instreamId); - if (!instreamIdMatch) { - return; - } - const trackName = `textTrack${instreamIdMatch[1]}`; - const trackProperties = this.captionsProperties[trackName]; - if (!trackProperties) { - return; - } - trackProperties.label = captionsTrack.name; - if (captionsTrack.lang) { - // optional attribute - trackProperties.languageCode = captionsTrack.lang; - } - trackProperties.media = captionsTrack; - }); - } - } - closedCaptionsForLevel(frag) { - const level = this.hls.levels[frag.level]; - return level == null ? void 0 : level.attrs['CLOSED-CAPTIONS']; - } - onFragLoading(event, data) { - // if this frag isn't contiguous, clear the parser so cues with bad start/end times aren't added to the textTrack - if (this.enabled && data.frag.type === PlaylistLevelType.MAIN) { - var _data$part$index, _data$part; - const { - cea608Parser1, - cea608Parser2, - lastSn - } = this; - const { - cc, - sn - } = data.frag; - const partIndex = (_data$part$index = (_data$part = data.part) == null ? void 0 : _data$part.index) != null ? _data$part$index : -1; - if (cea608Parser1 && cea608Parser2) { - if (sn !== lastSn + 1 || sn === lastSn && partIndex !== this.lastPartIndex + 1 || cc !== this.lastCc) { - cea608Parser1.reset(); - cea608Parser2.reset(); - } - } - this.lastCc = cc; - this.lastSn = sn; - this.lastPartIndex = partIndex; - } - } - onFragLoaded(event, data) { - const { - frag, - payload - } = data; - if (frag.type === PlaylistLevelType.SUBTITLE) { - // If fragment is subtitle type, parse as WebVTT. - if (payload.byteLength) { - const decryptData = frag.decryptdata; - // 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.encrypted || decrypted) { - const trackPlaylistMedia = this.tracks[frag.level]; - const vttCCs = this.vttCCs; - if (!vttCCs[frag.cc]) { - vttCCs[frag.cc] = { - start: frag.start, - prevCC: this.prevCC, - new: true - }; - this.prevCC = frag.cc; - } - if (trackPlaylistMedia && trackPlaylistMedia.textCodec === IMSC1_CODEC) { - this._parseIMSC1(frag, payload); - } else { - this._parseVTTs(data); - } - } - } else { - // In case there is no payload, finish unsuccessfully. - this.hls.trigger(Events.SUBTITLE_FRAG_PROCESSED, { - success: false, - frag, - error: new Error('Empty subtitle payload') - }); - } - } - } - _parseIMSC1(frag, payload) { - const hls = this.hls; - parseIMSC1(payload, this.initPTS[frag.cc], cues => { - this._appendCues(cues, frag.level); - hls.trigger(Events.SUBTITLE_FRAG_PROCESSED, { - success: true, - frag: frag - }); - }, error => { - hls.logger.log(`Failed to parse IMSC1: ${error}`); - hls.trigger(Events.SUBTITLE_FRAG_PROCESSED, { - success: false, - frag: frag, - error - }); - }); - } - _parseVTTs(data) { - var _frag$initSegment; - const { - frag, - payload - } = data; - // We need an initial synchronisation PTS. Store fragments as long as none has arrived - const { - initPTS, - unparsedVttFrags - } = this; - const maxAvCC = initPTS.length - 1; - if (!initPTS[frag.cc] && maxAvCC === -1) { - unparsedVttFrags.push(data); - return; - } - const hls = this.hls; - // Parse the WebVTT file contents. - const payloadWebVTT = (_frag$initSegment = frag.initSegment) != null && _frag$initSegment.data ? appendUint8Array(frag.initSegment.data, new Uint8Array(payload)) : payload; - parseWebVTT(payloadWebVTT, this.initPTS[frag.cc], this.vttCCs, frag.cc, frag.start, cues => { - this._appendCues(cues, frag.level); - hls.trigger(Events.SUBTITLE_FRAG_PROCESSED, { - success: true, - frag: frag - }); - }, error => { - const missingInitPTS = error.message === 'Missing initPTS for VTT MPEGTS'; - if (missingInitPTS) { - unparsedVttFrags.push(data); - } else { - this._fallbackToIMSC1(frag, payload); - } - // Something went wrong while parsing. Trigger event with success false. - hls.logger.log(`Failed to parse VTT cue: ${error}`); - if (missingInitPTS && maxAvCC > frag.cc) { - return; - } - hls.trigger(Events.SUBTITLE_FRAG_PROCESSED, { - success: false, - frag: frag, - error - }); - }); - } - _fallbackToIMSC1(frag, payload) { - // If textCodec is unknown, try parsing as IMSC1. Set textCodec based on the result - const trackPlaylistMedia = this.tracks[frag.level]; - if (!trackPlaylistMedia.textCodec) { - parseIMSC1(payload, this.initPTS[frag.cc], () => { - trackPlaylistMedia.textCodec = IMSC1_CODEC; - this._parseIMSC1(frag, payload); - }, () => { - trackPlaylistMedia.textCodec = 'wvtt'; - }); - } - } - _appendCues(cues, fragLevel) { - const hls = this.hls; - if (this.config.renderTextTracksNatively) { - const textTrack = this.textTracks[fragLevel]; - // WebVTTParser.parse is an async method and if the currently selected text track mode is set to "disabled" - // before parsing is done then don't try to access currentTrack.cues.getCueById as cues will be null - // and trying to access getCueById method of cues will throw an exception - // Because we check if the mode is disabled, we can force check `cues` below. They can't be null. - if (!textTrack || textTrack.mode === 'disabled') { - return; - } - cues.forEach(cue => addCueToTrack(textTrack, cue)); - } else { - const currentTrack = this.tracks[fragLevel]; - if (!currentTrack) { - return; - } - const track = currentTrack.default ? 'default' : 'subtitles' + fragLevel; - hls.trigger(Events.CUES_PARSED, { - type: 'subtitles', - cues, - track - }); - } - } - onFragDecrypted(event, data) { - const { - frag - } = data; - if (frag.type === PlaylistLevelType.SUBTITLE) { - this.onFragLoaded(Events.FRAG_LOADED, data); - } - } - onSubtitleTracksCleared() { - this.tracks = []; - this.captionsTracks = {}; - } - onFragParsingUserdata(event, data) { - if (!this.enabled || !this.config.enableCEA708Captions) { - 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 < samples.length; i++) { - const ccBytes = samples[i].bytes; - if (ccBytes) { - if (!this.cea608Parser1) { - this.initCea608Parsers(); - } - const ccdatas = this.extractCea608Data(ccBytes); - this.cea608Parser1.addData(samples[i].pts, ccdatas[0]); - this.cea608Parser2.addData(samples[i].pts, ccdatas[1]); - } - } - } - onBufferFlushing(event, { - startOffset, - endOffset, - endOffsetSubtitles, - type - }) { - const { - media - } = this; - if (!media || media.currentTime < endOffset) { - return; - } - // Clear 608 caption cues from the captions TextTracks when the video back buffer is flushed - // Forward cues are never removed because we can loose streamed 608 content from recent fragments - if (!type || type === 'video') { - const { - captionsTracks - } = this; - Object.keys(captionsTracks).forEach(trackName => removeCuesInRange(captionsTracks[trackName], startOffset, endOffset)); - } - if (this.config.renderTextTracksNatively) { - // Clear VTT/IMSC1 subtitle cues from the subtitle TextTracks when the back buffer is flushed - if (startOffset === 0 && endOffsetSubtitles !== undefined) { - const { - textTracks - } = this; - Object.keys(textTracks).forEach(trackName => removeCuesInRange(textTracks[trackName], startOffset, endOffsetSubtitles)); - } - } - } - extractCea608Data(byteArray) { - const actualCCBytes = [[], []]; - const count = byteArray[0] & 0x1f; - let position = 2; - for (let j = 0; j < count; j++) { - const tmpByte = byteArray[position++]; - const ccbyte1 = 0x7f & byteArray[position++]; - const ccbyte2 = 0x7f & byteArray[position++]; - if (ccbyte1 === 0 && ccbyte2 === 0) { - continue; - } - const ccValid = (0x04 & tmpByte) !== 0; // Support all four channels - if (ccValid) { - const ccType = 0x03 & tmpByte; - if (0x00 /* CEA608 field1*/ === ccType || 0x01 /* CEA608 field2*/ === ccType) { - // Exclude CEA708 CC data. - actualCCBytes[ccType].push(ccbyte1); - actualCCBytes[ccType].push(ccbyte2); - } - } - } - return actualCCBytes; - } -} -function captionsOrSubtitlesFromCharacteristics(track) { - if (track.characteristics) { - if (/transcribes-spoken-dialog/gi.test(track.characteristics) && /describes-music-and-sound/gi.test(track.characteristics)) { - return 'captions'; - } - } - return 'subtitles'; -} -function canReuseVttTextTrack(inUseTrack, manifestTrack) { - return !!inUseTrack && inUseTrack.kind === captionsOrSubtitlesFromCharacteristics(manifestTrack) && subtitleTrackMatchesTextTrack(manifestTrack, inUseTrack); -} -function intersection(x1, x2, y1, y2) { - return Math.min(x2, y2) - Math.max(x1, y1); -} -function newVTTCCs() { - return { - ccOffset: 0, - presentationOffset: 0, - 0: { - start: 0, - prevCC: -1, - new: true - } - }; -} - -class CapLevelController { - constructor(hls) { - this.hls = void 0; - this.autoLevelCapping = void 0; - this.firstLevel = void 0; - this.media = void 0; - this.restrictedLevels = void 0; - this.timer = void 0; - this.clientRect = void 0; - this.streamController = void 0; - this.hls = hls; - this.autoLevelCapping = Number.POSITIVE_INFINITY; - this.firstLevel = -1; - this.media = null; - this.restrictedLevels = []; - this.timer = undefined; - this.clientRect = null; - this.registerListeners(); - } - setStreamController(streamController) { - this.streamController = streamController; - } - destroy() { - if (this.hls) { - this.unregisterListener(); - } - if (this.timer) { - this.stopCapping(); - } - this.media = null; - this.clientRect = null; - // @ts-ignore - this.hls = this.streamController = null; - } - registerListeners() { - const { - hls - } = this; - hls.on(Events.FPS_DROP_LEVEL_CAPPING, this.onFpsDropLevelCapping, this); - hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); - hls.on(Events.MANIFEST_PARSED, this.onManifestParsed, this); - hls.on(Events.LEVELS_UPDATED, this.onLevelsUpdated, this); - hls.on(Events.BUFFER_CODECS, this.onBufferCodecs, this); - hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this); - } - unregisterListener() { - const { - hls - } = this; - hls.off(Events.FPS_DROP_LEVEL_CAPPING, this.onFpsDropLevelCapping, this); - hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); - hls.off(Events.MANIFEST_PARSED, this.onManifestParsed, this); - hls.off(Events.LEVELS_UPDATED, this.onLevelsUpdated, this); - hls.off(Events.BUFFER_CODECS, this.onBufferCodecs, this); - hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this); - } - onFpsDropLevelCapping(event, data) { - // Don't add a restricted level more than once - const level = this.hls.levels[data.droppedLevel]; - if (this.isLevelAllowed(level)) { - this.restrictedLevels.push({ - bitrate: level.bitrate, - height: level.height, - width: level.width - }); - } - } - onMediaAttaching(event, data) { - this.media = data.media instanceof HTMLVideoElement ? data.media : null; - this.clientRect = null; - if (this.timer && this.hls.levels.length) { - this.detectPlayerSize(); - } - } - onManifestParsed(event, data) { - const hls = this.hls; - this.restrictedLevels = []; - this.firstLevel = data.firstLevel; - if (hls.config.capLevelToPlayerSize && data.video) { - // Start capping immediately if the manifest has signaled video codecs - this.startCapping(); - } - } - onLevelsUpdated(event, data) { - if (this.timer && isFiniteNumber(this.autoLevelCapping)) { - this.detectPlayerSize(); - } - } - - // Only activate capping when playing a video stream; otherwise, multi-bitrate audio-only streams will be restricted - // to the first level - onBufferCodecs(event, data) { - const hls = this.hls; - if (hls.config.capLevelToPlayerSize && data.video) { - // If the manifest did not signal a video codec capping has been deferred until we're certain video is present - this.startCapping(); - } - } - onMediaDetaching() { - this.stopCapping(); - this.media = null; - } - detectPlayerSize() { - if (this.media) { - if (this.mediaHeight <= 0 || this.mediaWidth <= 0) { - this.clientRect = null; - return; - } - const levels = this.hls.levels; - if (levels.length) { - const hls = this.hls; - const maxLevel = this.getMaxLevel(levels.length - 1); - if (maxLevel !== this.autoLevelCapping) { - hls.logger.log(`Setting autoLevelCapping to ${maxLevel}: ${levels[maxLevel].height}p@${levels[maxLevel].bitrate} for media ${this.mediaWidth}x${this.mediaHeight}`); - } - hls.autoLevelCapping = maxLevel; - if (hls.autoLevelEnabled && hls.autoLevelCapping > this.autoLevelCapping && this.streamController) { - // if auto level capping has a higher value for the previous one, flush the buffer using nextLevelSwitch - // usually happen when the user go to the fullscreen mode. - this.streamController.nextLevelSwitch(); - } - this.autoLevelCapping = hls.autoLevelCapping; - } - } - } - - /* - * returns level should be the one with the dimensions equal or greater than the media (player) dimensions (so the video will be downscaled) - */ - getMaxLevel(capLevelIndex) { - const levels = this.hls.levels; - if (!levels.length) { - return -1; - } - const validLevels = levels.filter((level, index) => this.isLevelAllowed(level) && index <= capLevelIndex); - this.clientRect = null; - return CapLevelController.getMaxLevelByMediaSize(validLevels, this.mediaWidth, this.mediaHeight); - } - startCapping() { - if (this.timer) { - // Don't reset capping if started twice; this can happen if the manifest signals a video codec - return; - } - this.autoLevelCapping = Number.POSITIVE_INFINITY; - self.clearInterval(this.timer); - this.timer = self.setInterval(this.detectPlayerSize.bind(this), 1000); - this.detectPlayerSize(); - } - stopCapping() { - this.restrictedLevels = []; - this.firstLevel = -1; - this.autoLevelCapping = Number.POSITIVE_INFINITY; - if (this.timer) { - self.clearInterval(this.timer); - this.timer = undefined; - } - } - getDimensions() { - if (this.clientRect) { - return this.clientRect; - } - const media = this.media; - const boundsRect = { - width: 0, - height: 0 - }; - if (media) { - const clientRect = media.getBoundingClientRect(); - boundsRect.width = clientRect.width; - boundsRect.height = clientRect.height; - if (!boundsRect.width && !boundsRect.height) { - // When the media element has no width or height (equivalent to not being in the DOM), - // then use its width and height attributes (media.width, media.height) - boundsRect.width = clientRect.right - clientRect.left || media.width || 0; - boundsRect.height = clientRect.bottom - clientRect.top || media.height || 0; - } - } - this.clientRect = boundsRect; - return boundsRect; - } - get mediaWidth() { - return this.getDimensions().width * this.contentScaleFactor; - } - get mediaHeight() { - return this.getDimensions().height * this.contentScaleFactor; - } - get contentScaleFactor() { - let pixelRatio = 1; - if (!this.hls.config.ignoreDevicePixelRatio) { - try { - pixelRatio = self.devicePixelRatio; - } catch (e) { - /* no-op */ - } - } - return pixelRatio; - } - isLevelAllowed(level) { - const restrictedLevels = this.restrictedLevels; - return !restrictedLevels.some(restrictedLevel => { - return level.bitrate === restrictedLevel.bitrate && level.width === restrictedLevel.width && level.height === restrictedLevel.height; - }); - } - static getMaxLevelByMediaSize(levels, width, height) { - if (!(levels != null && levels.length)) { - return -1; - } - - // Levels can have the same dimensions but differing bandwidths - since levels are ordered, we can look to the next - // to determine whether we've chosen the greatest bandwidth for the media's dimensions - const atGreatestBandwidth = (curLevel, nextLevel) => { - if (!nextLevel) { - return true; - } - return curLevel.width !== nextLevel.width || curLevel.height !== nextLevel.height; - }; - - // If we run through the loop without breaking, the media's dimensions are greater than every level, so default to - // the max level - let maxLevelIndex = levels.length - 1; - // Prevent changes in aspect-ratio from causing capping to toggle back and forth - const squareSize = Math.max(width, height); - for (let i = 0; i < levels.length; i += 1) { - const level = levels[i]; - if ((level.width >= squareSize || level.height >= squareSize) && atGreatestBandwidth(level, levels[i + 1])) { - maxLevelIndex = i; - break; - } - } - return maxLevelIndex; - } -} - -class FPSController { - constructor(hls) { - this.hls = void 0; - this.isVideoPlaybackQualityAvailable = false; - this.timer = void 0; - this.media = null; - this.lastTime = void 0; - this.lastDroppedFrames = 0; - this.lastDecodedFrames = 0; - // stream controller must be provided as a dependency! - this.streamController = void 0; - this.hls = hls; - this.registerListeners(); - } - setStreamController(streamController) { - this.streamController = streamController; - } - registerListeners() { - this.hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); - this.hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this); - } - unregisterListeners() { - this.hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); - this.hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this); - } - destroy() { - if (this.timer) { - clearInterval(this.timer); - } - this.unregisterListeners(); - this.isVideoPlaybackQualityAvailable = false; - this.media = null; - } - onMediaAttaching(event, data) { - const config = this.hls.config; - if (config.capLevelOnFPSDrop) { - const media = data.media instanceof self.HTMLVideoElement ? data.media : null; - this.media = media; - if (media && typeof media.getVideoPlaybackQuality === 'function') { - this.isVideoPlaybackQualityAvailable = true; - } - self.clearInterval(this.timer); - this.timer = self.setInterval(this.checkFPSInterval.bind(this), config.fpsDroppedMonitoringPeriod); - } - } - onMediaDetaching() { - this.media = null; - } - checkFPS(video, decodedFrames, droppedFrames) { - const currentTime = performance.now(); - if (decodedFrames) { - if (this.lastTime) { - const currentPeriod = currentTime - this.lastTime; - const currentDropped = droppedFrames - this.lastDroppedFrames; - const currentDecoded = decodedFrames - this.lastDecodedFrames; - const droppedFPS = 1000 * currentDropped / currentPeriod; - const hls = this.hls; - hls.trigger(Events.FPS_DROP, { - currentDropped: currentDropped, - currentDecoded: currentDecoded, - totalDroppedFrames: droppedFrames - }); - if (droppedFPS > 0) { - // hls.logger.log('checkFPS : droppedFPS/decodedFPS:' + droppedFPS/(1000 * currentDecoded / currentPeriod)); - if (currentDropped > hls.config.fpsDroppedMonitoringThreshold * currentDecoded) { - let currentLevel = hls.currentLevel; - hls.logger.warn('drop FPS ratio greater than max allowed value for currentLevel: ' + currentLevel); - if (currentLevel > 0 && (hls.autoLevelCapping === -1 || hls.autoLevelCapping >= currentLevel)) { - currentLevel = currentLevel - 1; - hls.trigger(Events.FPS_DROP_LEVEL_CAPPING, { - level: currentLevel, - droppedLevel: hls.currentLevel - }); - hls.autoLevelCapping = currentLevel; - this.streamController.nextLevelSwitch(); - } - } - } - } - this.lastTime = currentTime; - this.lastDroppedFrames = droppedFrames; - this.lastDecodedFrames = decodedFrames; - } - } - checkFPSInterval() { - const video = this.media; - if (video) { - if (this.isVideoPlaybackQualityAvailable) { - const videoPlaybackQuality = video.getVideoPlaybackQuality(); - this.checkFPS(video, videoPlaybackQuality.totalVideoFrames, videoPlaybackQuality.droppedVideoFrames); - } else { - // HTMLVideoElement doesn't include the webkit types - this.checkFPS(video, video.webkitDecodedFrameCount, video.webkitDroppedFrameCount); - } - } - } -} - -/** - * Controller to deal with encrypted media extensions (EME) - * @see https://developer.mozilla.org/en-US/docs/Web/API/Encrypted_Media_Extensions_API - * - * @class - * @constructor - */ -class EMEController extends Logger { - constructor(hls) { - super('eme', hls.logger); - this.hls = void 0; - this.config = void 0; - this.media = null; - this.keyFormatPromise = null; - this.keySystemAccessPromises = {}; - this._requestLicenseFailureCount = 0; - this.mediaKeySessions = []; - this.keyIdToKeySessionPromise = {}; - this.setMediaKeysQueue = EMEController.CDMCleanupPromise ? [EMEController.CDMCleanupPromise] : []; - this.onMediaEncrypted = event => { - const { - initDataType, - initData - } = event; - const logMessage = `"${event.type}" event: init data type: "${initDataType}"`; - this.debug(logMessage); - - // Ignore event when initData is null - if (initData === null) { - return; - } - let keyId; - let keySystemDomain; - if (initDataType === 'sinf' && this.getLicenseServerUrl(KeySystems.FAIRPLAY)) { - // Match sinf keyId to playlist skd://keyId= - const json = bin2str(new Uint8Array(initData)); - try { - const sinf = base64Decode(JSON.parse(json).sinf); - const tenc = parseSinf(new Uint8Array(sinf)); - if (!tenc) { - throw new Error(`'schm' box missing or not cbcs/cenc with schi > tenc`); - } - keyId = tenc.subarray(8, 24); - keySystemDomain = KeySystems.FAIRPLAY; - } catch (error) { - this.warn(`${logMessage} Failed to parse sinf: ${error}`); - return; - } - } else if (this.getLicenseServerUrl(KeySystems.WIDEVINE)) { - // Support Widevine clear-lead key-session creation (otherwise depend on playlist keys) - const psshResults = parseMultiPssh(initData); - - // TODO: If using keySystemAccessPromises we might want to wait until one is resolved - let keySystems = Object.keys(this.keySystemAccessPromises); - if (!keySystems.length) { - keySystems = getKeySystemsForConfig(this.config); - } - const psshInfo = psshResults.filter(pssh => { - const keySystem = pssh.systemId ? keySystemIdToKeySystemDomain(pssh.systemId) : null; - return keySystem ? keySystems.indexOf(keySystem) > -1 : false; - })[0]; - if (!psshInfo) { - if (psshResults.length === 0 || psshResults.some(pssh => !pssh.systemId)) { - this.warn(`${logMessage} contains incomplete or invalid pssh data`); - } else { - this.log(`ignoring ${logMessage} for ${psshResults.map(pssh => keySystemIdToKeySystemDomain(pssh.systemId)).join(',')} pssh data in favor of playlist keys`); - } - return; - } - keySystemDomain = keySystemIdToKeySystemDomain(psshInfo.systemId); - if (psshInfo.version === 0 && psshInfo.data) { - if (keySystemDomain === KeySystems.WIDEVINE) { - const offset = psshInfo.data.length - 22; - keyId = psshInfo.data.subarray(offset, offset + 16); - } else if (keySystemDomain === KeySystems.PLAYREADY) { - keyId = parsePlayReadyWRM(psshInfo.data); - } - } - } - if (!keySystemDomain || !keyId) { - return; - } - const keyIdHex = Hex.hexDump(keyId); - const { - keyIdToKeySessionPromise, - mediaKeySessions - } = this; - let keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex]; - for (let i = 0; i < mediaKeySessions.length; i++) { - // Match playlist key - const keyContext = mediaKeySessions[i]; - const decryptdata = keyContext.decryptdata; - if (!decryptdata.keyId) { - continue; - } - const oldKeyIdHex = Hex.hexDump(decryptdata.keyId); - if (keyIdHex === oldKeyIdHex || decryptdata.uri.replace(/-/g, '').indexOf(keyIdHex) !== -1) { - keySessionContextPromise = keyIdToKeySessionPromise[oldKeyIdHex]; - if (decryptdata.pssh) { - break; - } - delete keyIdToKeySessionPromise[oldKeyIdHex]; - decryptdata.pssh = new Uint8Array(initData); - decryptdata.keyId = keyId; - keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex] = keySessionContextPromise.then(() => { - return this.generateRequestWithPreferredKeySession(keyContext, initDataType, initData, 'encrypted-event-key-match'); - }); - keySessionContextPromise.catch(error => this.handleError(error)); - break; - } - } - if (!keySessionContextPromise) { - // Clear-lead key (not encountered in playlist) - keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex] = this.getKeySystemSelectionPromise([keySystemDomain]).then(({ - keySystem, - mediaKeys - }) => { - var _keySystemToKeySystem; - this.throwIfDestroyed(); - const decryptdata = new LevelKey('ISO-23001-7', keyIdHex, (_keySystemToKeySystem = keySystemDomainToKeySystemFormat(keySystem)) != null ? _keySystemToKeySystem : ''); - decryptdata.pssh = new Uint8Array(initData); - decryptdata.keyId = keyId; - return this.attemptSetMediaKeys(keySystem, mediaKeys).then(() => { - this.throwIfDestroyed(); - const keySessionContext = this.createMediaKeySessionContext({ - decryptdata, - keySystem, - mediaKeys - }); - return this.generateRequestWithPreferredKeySession(keySessionContext, initDataType, initData, 'encrypted-event-no-match'); - }); - }); - keySessionContextPromise.catch(error => this.handleError(error)); - } - }; - this.onWaitingForKey = event => { - this.log(`"${event.type}" event`); - }; - this.hls = hls; - this.config = hls.config; - this.registerListeners(); - } - destroy() { - this.unregisterListeners(); - this.onMediaDetached(); - // Remove any references that could be held in config options or callbacks - const config = this.config; - config.requestMediaKeySystemAccessFunc = null; - config.licenseXhrSetup = config.licenseResponseCallback = undefined; - config.drmSystems = config.drmSystemOptions = {}; - // @ts-ignore - this.hls = this.config = this.keyIdToKeySessionPromise = null; - // @ts-ignore - this.onMediaEncrypted = this.onWaitingForKey = null; - } - registerListeners() { - this.hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this); - this.hls.on(Events.MEDIA_DETACHED, this.onMediaDetached, this); - this.hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); - this.hls.on(Events.MANIFEST_LOADED, this.onManifestLoaded, this); - } - unregisterListeners() { - this.hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this); - this.hls.off(Events.MEDIA_DETACHED, this.onMediaDetached, this); - this.hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); - this.hls.off(Events.MANIFEST_LOADED, this.onManifestLoaded, this); - } - getLicenseServerUrl(keySystem) { - const { - drmSystems, - widevineLicenseUrl - } = this.config; - const keySystemConfiguration = drmSystems[keySystem]; - if (keySystemConfiguration) { - return keySystemConfiguration.licenseUrl; - } - - // For backward compatibility - if (keySystem === KeySystems.WIDEVINE && widevineLicenseUrl) { - return widevineLicenseUrl; - } - } - getLicenseServerUrlOrThrow(keySystem) { - const url = this.getLicenseServerUrl(keySystem); - if (url === undefined) { - throw new Error(`no license server URL configured for key-system "${keySystem}"`); - } - return url; - } - getServerCertificateUrl(keySystem) { - const { - drmSystems - } = this.config; - const keySystemConfiguration = drmSystems[keySystem]; - if (keySystemConfiguration) { - return keySystemConfiguration.serverCertificateUrl; - } else { - this.log(`No Server Certificate in config.drmSystems["${keySystem}"]`); - } - } - attemptKeySystemAccess(keySystemsToAttempt) { - const levels = this.hls.levels; - const uniqueCodec = (value, i, a) => !!value && a.indexOf(value) === i; - const audioCodecs = levels.map(level => level.audioCodec).filter(uniqueCodec); - const videoCodecs = levels.map(level => level.videoCodec).filter(uniqueCodec); - if (audioCodecs.length + videoCodecs.length === 0) { - videoCodecs.push('avc1.42e01e'); - } - return new Promise((resolve, reject) => { - const attempt = keySystems => { - const keySystem = keySystems.shift(); - this.getMediaKeysPromise(keySystem, audioCodecs, videoCodecs).then(mediaKeys => resolve({ - keySystem, - mediaKeys - })).catch(error => { - if (keySystems.length) { - attempt(keySystems); - } else if (error instanceof EMEKeyError) { - reject(error); - } else { - reject(new EMEKeyError({ - type: ErrorTypes.KEY_SYSTEM_ERROR, - details: ErrorDetails.KEY_SYSTEM_NO_ACCESS, - error, - fatal: true - }, error.message)); - } - }); - }; - attempt(keySystemsToAttempt); - }); - } - requestMediaKeySystemAccess(keySystem, supportedConfigurations) { - const { - requestMediaKeySystemAccessFunc - } = this.config; - if (!(typeof requestMediaKeySystemAccessFunc === 'function')) { - let errMessage = `Configured requestMediaKeySystemAccess is not a function ${requestMediaKeySystemAccessFunc}`; - if (requestMediaKeySystemAccess === null && self.location.protocol === 'http:') { - errMessage = `navigator.requestMediaKeySystemAccess is not available over insecure protocol ${location.protocol}`; - } - return Promise.reject(new Error(errMessage)); - } - return requestMediaKeySystemAccessFunc(keySystem, supportedConfigurations); - } - getMediaKeysPromise(keySystem, audioCodecs, videoCodecs) { - // This can throw, but is caught in event handler callpath - const mediaKeySystemConfigs = getSupportedMediaKeySystemConfigurations(keySystem, audioCodecs, videoCodecs, this.config.drmSystemOptions); - const keySystemAccessPromises = this.keySystemAccessPromises[keySystem]; - let keySystemAccess = keySystemAccessPromises == null ? void 0 : keySystemAccessPromises.keySystemAccess; - if (!keySystemAccess) { - this.log(`Requesting encrypted media "${keySystem}" key-system access with config: ${JSON.stringify(mediaKeySystemConfigs)}`); - keySystemAccess = this.requestMediaKeySystemAccess(keySystem, mediaKeySystemConfigs); - const _keySystemAccessPromises = this.keySystemAccessPromises[keySystem] = { - keySystemAccess - }; - keySystemAccess.catch(error => { - this.log(`Failed to obtain access to key-system "${keySystem}": ${error}`); - }); - return keySystemAccess.then(mediaKeySystemAccess => { - this.log(`Access for key-system "${mediaKeySystemAccess.keySystem}" obtained`); - const certificateRequest = this.fetchServerCertificate(keySystem); - this.log(`Create media-keys for "${keySystem}"`); - _keySystemAccessPromises.mediaKeys = mediaKeySystemAccess.createMediaKeys().then(mediaKeys => { - this.log(`Media-keys created for "${keySystem}"`); - return certificateRequest.then(certificate => { - if (certificate) { - return this.setMediaKeysServerCertificate(mediaKeys, keySystem, certificate); - } - return mediaKeys; - }); - }); - _keySystemAccessPromises.mediaKeys.catch(error => { - this.error(`Failed to create media-keys for "${keySystem}"}: ${error}`); - }); - return _keySystemAccessPromises.mediaKeys; - }); - } - return keySystemAccess.then(() => keySystemAccessPromises.mediaKeys); - } - createMediaKeySessionContext({ - decryptdata, - keySystem, - mediaKeys - }) { - this.log(`Creating key-system session "${keySystem}" keyId: ${Hex.hexDump(decryptdata.keyId || [])}`); - const mediaKeysSession = mediaKeys.createSession(); - const mediaKeySessionContext = { - decryptdata, - keySystem, - mediaKeys, - mediaKeysSession, - keyStatus: 'status-pending' - }; - this.mediaKeySessions.push(mediaKeySessionContext); - return mediaKeySessionContext; - } - renewKeySession(mediaKeySessionContext) { - const decryptdata = mediaKeySessionContext.decryptdata; - if (decryptdata.pssh) { - const keySessionContext = this.createMediaKeySessionContext(mediaKeySessionContext); - const keyId = this.getKeyIdString(decryptdata); - const scheme = 'cenc'; - this.keyIdToKeySessionPromise[keyId] = this.generateRequestWithPreferredKeySession(keySessionContext, scheme, decryptdata.pssh, 'expired'); - } else { - this.warn(`Could not renew expired session. Missing pssh initData.`); - } - this.removeSession(mediaKeySessionContext); - } - getKeyIdString(decryptdata) { - if (!decryptdata) { - throw new Error('Could not read keyId of undefined decryptdata'); - } - if (decryptdata.keyId === null) { - throw new Error('keyId is null'); - } - return Hex.hexDump(decryptdata.keyId); - } - updateKeySession(mediaKeySessionContext, data) { - var _mediaKeySessionConte; - const keySession = mediaKeySessionContext.mediaKeysSession; - this.log(`Updating key-session "${keySession.sessionId}" for keyID ${Hex.hexDump(((_mediaKeySessionConte = mediaKeySessionContext.decryptdata) == null ? void 0 : _mediaKeySessionConte.keyId) || [])} - } (data length: ${data ? data.byteLength : data})`); - return keySession.update(data); - } - selectKeySystemFormat(frag) { - const keyFormats = Object.keys(frag.levelkeys || {}); - if (!this.keyFormatPromise) { - this.log(`Selecting key-system from fragment (sn: ${frag.sn} ${frag.type}: ${frag.level}) key formats ${keyFormats.join(', ')}`); - this.keyFormatPromise = this.getKeyFormatPromise(keyFormats); - } - return this.keyFormatPromise; - } - getKeyFormatPromise(keyFormats) { - return new Promise((resolve, reject) => { - const keySystemsInConfig = getKeySystemsForConfig(this.config); - const keySystemsToAttempt = keyFormats.map(keySystemFormatToKeySystemDomain).filter(value => !!value && keySystemsInConfig.indexOf(value) !== -1); - return this.getKeySystemSelectionPromise(keySystemsToAttempt).then(({ - keySystem - }) => { - const keySystemFormat = keySystemDomainToKeySystemFormat(keySystem); - if (keySystemFormat) { - resolve(keySystemFormat); - } else { - reject(new Error(`Unable to find format for key-system "${keySystem}"`)); - } - }).catch(reject); - }); - } - loadKey(data) { - const decryptdata = data.keyInfo.decryptdata; - const keyId = this.getKeyIdString(decryptdata); - const keyDetails = `(keyId: ${keyId} format: "${decryptdata.keyFormat}" method: ${decryptdata.method} uri: ${decryptdata.uri})`; - this.log(`Starting session for key ${keyDetails}`); - let keyContextPromise = this.keyIdToKeySessionPromise[keyId]; - if (!keyContextPromise) { - keyContextPromise = this.getKeySystemForKeyPromise(decryptdata).then(({ - keySystem, - mediaKeys - }) => { - this.throwIfDestroyed(); - this.log(`Handle encrypted media sn: ${data.frag.sn} ${data.frag.type}: ${data.frag.level} using key ${keyDetails}`); - return this.attemptSetMediaKeys(keySystem, mediaKeys).then(() => { - this.throwIfDestroyed(); - return this.createMediaKeySessionContext({ - keySystem, - mediaKeys, - decryptdata - }); - }); - }); - const keySessionContextPromise = this.keyIdToKeySessionPromise[keyId] = keyContextPromise.then(keySessionContext => { - const scheme = 'cenc'; - return this.generateRequestWithPreferredKeySession(keySessionContext, scheme, decryptdata.pssh, 'playlist-key'); - }); - keySessionContextPromise.catch(error => this.handleError(error)); - } - return keyContextPromise; - } - throwIfDestroyed(message = 'Invalid state') { - if (!this.hls) { - throw new Error('invalid state'); - } - } - handleError(error) { - if (!this.hls) { - return; - } - this.error(error.message); - if (error instanceof EMEKeyError) { - this.hls.trigger(Events.ERROR, error.data); - } else { - this.hls.trigger(Events.ERROR, { - type: ErrorTypes.KEY_SYSTEM_ERROR, - details: ErrorDetails.KEY_SYSTEM_NO_KEYS, - error, - fatal: true - }); - } - } - getKeySystemForKeyPromise(decryptdata) { - const keyId = this.getKeyIdString(decryptdata); - const mediaKeySessionContext = this.keyIdToKeySessionPromise[keyId]; - if (!mediaKeySessionContext) { - const keySystem = keySystemFormatToKeySystemDomain(decryptdata.keyFormat); - const keySystemsToAttempt = keySystem ? [keySystem] : getKeySystemsForConfig(this.config); - return this.attemptKeySystemAccess(keySystemsToAttempt); - } - return mediaKeySessionContext; - } - getKeySystemSelectionPromise(keySystemsToAttempt) { - if (!keySystemsToAttempt.length) { - keySystemsToAttempt = getKeySystemsForConfig(this.config); - } - if (keySystemsToAttempt.length === 0) { - throw new EMEKeyError({ - type: ErrorTypes.KEY_SYSTEM_ERROR, - details: ErrorDetails.KEY_SYSTEM_NO_CONFIGURED_LICENSE, - fatal: true - }, `Missing key-system license configuration options ${JSON.stringify({ - drmSystems: this.config.drmSystems - })}`); - } - return this.attemptKeySystemAccess(keySystemsToAttempt); - } - attemptSetMediaKeys(keySystem, mediaKeys) { - const queue = this.setMediaKeysQueue.slice(); - this.log(`Setting media-keys for "${keySystem}"`); - // Only one setMediaKeys() can run at one time, and multiple setMediaKeys() operations - // can be queued for execution for multiple key sessions. - const setMediaKeysPromise = Promise.all(queue).then(() => { - if (!this.media) { - throw new Error('Attempted to set mediaKeys without media element attached'); - } - return this.media.setMediaKeys(mediaKeys); - }); - this.setMediaKeysQueue.push(setMediaKeysPromise); - return setMediaKeysPromise.then(() => { - this.log(`Media-keys set for "${keySystem}"`); - queue.push(setMediaKeysPromise); - this.setMediaKeysQueue = this.setMediaKeysQueue.filter(p => queue.indexOf(p) === -1); - }); - } - generateRequestWithPreferredKeySession(context, initDataType, initData, reason) { - var _this$config$drmSyste, _this$config$drmSyste2; - const generateRequestFilter = (_this$config$drmSyste = this.config.drmSystems) == null ? void 0 : (_this$config$drmSyste2 = _this$config$drmSyste[context.keySystem]) == null ? void 0 : _this$config$drmSyste2.generateRequest; - if (generateRequestFilter) { - try { - const mappedInitData = generateRequestFilter.call(this.hls, initDataType, initData, context); - if (!mappedInitData) { - throw new Error('Invalid response from configured generateRequest filter'); - } - initDataType = mappedInitData.initDataType; - initData = context.decryptdata.pssh = mappedInitData.initData ? new Uint8Array(mappedInitData.initData) : null; - } catch (error) { - var _this$hls; - this.warn(error.message); - if ((_this$hls = this.hls) != null && _this$hls.config.debug) { - throw error; - } - } - } - if (initData === null) { - this.log(`Skipping key-session request for "${reason}" (no initData)`); - return Promise.resolve(context); - } - const keyId = this.getKeyIdString(context.decryptdata); - this.log(`Generating key-session request for "${reason}": ${keyId} (init data type: ${initDataType} length: ${initData ? initData.byteLength : null})`); - const licenseStatus = new EventEmitter(); - const onmessage = context._onmessage = event => { - const keySession = context.mediaKeysSession; - if (!keySession) { - licenseStatus.emit('error', new Error('invalid state')); - return; - } - const { - messageType, - message - } = event; - this.log(`"${messageType}" message event for session "${keySession.sessionId}" message size: ${message.byteLength}`); - if (messageType === 'license-request' || messageType === 'license-renewal') { - this.renewLicense(context, message).catch(error => { - if (licenseStatus.eventNames().length) { - licenseStatus.emit('error', error); - } else { - this.handleError(error); - } - }); - } else if (messageType === 'license-release') { - if (context.keySystem === KeySystems.FAIRPLAY) { - this.updateKeySession(context, strToUtf8array('acknowledged')); - this.removeSession(context); - } - } else { - this.warn(`unhandled media key message type "${messageType}"`); - } - }; - const onkeystatuseschange = context._onkeystatuseschange = event => { - const keySession = context.mediaKeysSession; - if (!keySession) { - licenseStatus.emit('error', new Error('invalid state')); - return; - } - this.onKeyStatusChange(context); - const keyStatus = context.keyStatus; - licenseStatus.emit('keyStatus', keyStatus); - if (keyStatus === 'expired') { - this.warn(`${context.keySystem} expired for key ${keyId}`); - this.renewKeySession(context); - } - }; - context.mediaKeysSession.addEventListener('message', onmessage); - context.mediaKeysSession.addEventListener('keystatuseschange', onkeystatuseschange); - const keyUsablePromise = new Promise((resolve, reject) => { - licenseStatus.on('error', reject); - licenseStatus.on('keyStatus', keyStatus => { - if (keyStatus.startsWith('usable')) { - resolve(); - } else if (keyStatus === 'output-restricted') { - reject(new EMEKeyError({ - type: ErrorTypes.KEY_SYSTEM_ERROR, - details: ErrorDetails.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED, - fatal: false - }, 'HDCP level output restricted')); - } else if (keyStatus === 'internal-error') { - reject(new EMEKeyError({ - type: ErrorTypes.KEY_SYSTEM_ERROR, - details: ErrorDetails.KEY_SYSTEM_STATUS_INTERNAL_ERROR, - fatal: true - }, `key status changed to "${keyStatus}"`)); - } else if (keyStatus === 'expired') { - reject(new Error('key expired while generating request')); - } else { - this.warn(`unhandled key status change "${keyStatus}"`); - } - }); - }); - return context.mediaKeysSession.generateRequest(initDataType, initData).then(() => { - var _context$mediaKeysSes; - this.log(`Request generated for key-session "${(_context$mediaKeysSes = context.mediaKeysSession) == null ? void 0 : _context$mediaKeysSes.sessionId}" keyId: ${keyId}`); - }).catch(error => { - throw new EMEKeyError({ - type: ErrorTypes.KEY_SYSTEM_ERROR, - details: ErrorDetails.KEY_SYSTEM_NO_SESSION, - error, - fatal: false - }, `Error generating key-session request: ${error}`); - }).then(() => keyUsablePromise).catch(error => { - licenseStatus.removeAllListeners(); - this.removeSession(context); - throw error; - }).then(() => { - licenseStatus.removeAllListeners(); - return context; - }); - } - onKeyStatusChange(mediaKeySessionContext) { - mediaKeySessionContext.mediaKeysSession.keyStatuses.forEach((status, keyId) => { - this.log(`key status change "${status}" for keyStatuses keyId: ${Hex.hexDump('buffer' in keyId ? new Uint8Array(keyId.buffer, keyId.byteOffset, keyId.byteLength) : new Uint8Array(keyId))} session keyId: ${Hex.hexDump(new Uint8Array(mediaKeySessionContext.decryptdata.keyId || []))} uri: ${mediaKeySessionContext.decryptdata.uri}`); - mediaKeySessionContext.keyStatus = status; - }); - } - fetchServerCertificate(keySystem) { - const config = this.config; - const Loader = config.loader; - const certLoader = new Loader(config); - const url = this.getServerCertificateUrl(keySystem); - if (!url) { - return Promise.resolve(); - } - this.log(`Fetching server certificate for "${keySystem}"`); - return new Promise((resolve, reject) => { - const loaderContext = { - responseType: 'arraybuffer', - url - }; - const loadPolicy = config.certLoadPolicy.default; - const loaderConfig = { - loadPolicy, - timeout: loadPolicy.maxLoadTimeMs, - maxRetry: 0, - retryDelay: 0, - maxRetryDelay: 0 - }; - const loaderCallbacks = { - onSuccess: (response, stats, context, networkDetails) => { - resolve(response.data); - }, - onError: (response, contex, networkDetails, stats) => { - reject(new EMEKeyError({ - type: ErrorTypes.KEY_SYSTEM_ERROR, - details: ErrorDetails.KEY_SYSTEM_SERVER_CERTIFICATE_REQUEST_FAILED, - fatal: true, - networkDetails, - response: _objectSpread2({ - url: loaderContext.url, - data: undefined - }, response) - }, `"${keySystem}" certificate request failed (${url}). Status: ${response.code} (${response.text})`)); - }, - onTimeout: (stats, context, networkDetails) => { - reject(new EMEKeyError({ - type: ErrorTypes.KEY_SYSTEM_ERROR, - details: ErrorDetails.KEY_SYSTEM_SERVER_CERTIFICATE_REQUEST_FAILED, - fatal: true, - networkDetails, - response: { - url: loaderContext.url, - data: undefined - } - }, `"${keySystem}" certificate request timed out (${url})`)); - }, - onAbort: (stats, context, networkDetails) => { - reject(new Error('aborted')); - } - }; - certLoader.load(loaderContext, loaderConfig, loaderCallbacks); - }); - } - setMediaKeysServerCertificate(mediaKeys, keySystem, cert) { - return new Promise((resolve, reject) => { - mediaKeys.setServerCertificate(cert).then(success => { - this.log(`setServerCertificate ${success ? 'success' : 'not supported by CDM'} (${cert == null ? void 0 : cert.byteLength}) on "${keySystem}"`); - resolve(mediaKeys); - }).catch(error => { - reject(new EMEKeyError({ - type: ErrorTypes.KEY_SYSTEM_ERROR, - details: ErrorDetails.KEY_SYSTEM_SERVER_CERTIFICATE_UPDATE_FAILED, - error, - fatal: true - }, error.message)); - }); - }); - } - renewLicense(context, keyMessage) { - return this.requestLicense(context, new Uint8Array(keyMessage)).then(data => { - return this.updateKeySession(context, new Uint8Array(data)).catch(error => { - throw new EMEKeyError({ - type: ErrorTypes.KEY_SYSTEM_ERROR, - details: ErrorDetails.KEY_SYSTEM_SESSION_UPDATE_FAILED, - error, - fatal: true - }, error.message); - }); - }); - } - unpackPlayReadyKeyMessage(xhr, licenseChallenge) { - // On Edge, the raw license message is UTF-16-encoded XML. We need - // to unpack the Challenge element (base64-encoded string containing the - // actual license request) and any HttpHeader elements (sent as request - // headers). - // For PlayReady CDMs, we need to dig the Challenge out of the XML. - const xmlString = String.fromCharCode.apply(null, new Uint16Array(licenseChallenge.buffer)); - if (!xmlString.includes('PlayReadyKeyMessage')) { - // This does not appear to be a wrapped message as on Edge. Some - // clients do not need this unwrapping, so we will assume this is one of - // them. Note that "xml" at this point probably looks like random - // garbage, since we interpreted UTF-8 as UTF-16. - xhr.setRequestHeader('Content-Type', 'text/xml; charset=utf-8'); - return licenseChallenge; - } - const keyMessageXml = new DOMParser().parseFromString(xmlString, 'application/xml'); - // Set request headers. - const headers = keyMessageXml.querySelectorAll('HttpHeader'); - if (headers.length > 0) { - let header; - for (let i = 0, len = headers.length; i < len; i++) { - var _header$querySelector, _header$querySelector2; - header = headers[i]; - const name = (_header$querySelector = header.querySelector('name')) == null ? void 0 : _header$querySelector.textContent; - const value = (_header$querySelector2 = header.querySelector('value')) == null ? void 0 : _header$querySelector2.textContent; - if (name && value) { - xhr.setRequestHeader(name, value); - } - } - } - const challengeElement = keyMessageXml.querySelector('Challenge'); - const challengeText = challengeElement == null ? void 0 : challengeElement.textContent; - if (!challengeText) { - throw new Error(`Cannot find <Challenge> in key message`); - } - return strToUtf8array(atob(challengeText)); - } - setupLicenseXHR(xhr, url, keysListItem, licenseChallenge) { - const licenseXhrSetup = this.config.licenseXhrSetup; - if (!licenseXhrSetup) { - xhr.open('POST', url, true); - return Promise.resolve({ - xhr, - licenseChallenge - }); - } - return Promise.resolve().then(() => { - if (!keysListItem.decryptdata) { - throw new Error('Key removed'); - } - return licenseXhrSetup.call(this.hls, xhr, url, keysListItem, licenseChallenge); - }).catch(error => { - if (!keysListItem.decryptdata) { - // Key session removed. Cancel license request. - throw error; - } - // let's try to open before running setup - xhr.open('POST', url, true); - return licenseXhrSetup.call(this.hls, xhr, url, keysListItem, licenseChallenge); - }).then(licenseXhrSetupResult => { - // if licenseXhrSetup did not yet call open, let's do it now - if (!xhr.readyState) { - xhr.open('POST', url, true); - } - const finalLicenseChallenge = licenseXhrSetupResult ? licenseXhrSetupResult : licenseChallenge; - return { - xhr, - licenseChallenge: finalLicenseChallenge - }; - }); - } - requestLicense(keySessionContext, licenseChallenge) { - const keyLoadPolicy = this.config.keyLoadPolicy.default; - return new Promise((resolve, reject) => { - const url = this.getLicenseServerUrlOrThrow(keySessionContext.keySystem); - this.log(`Sending license request to URL: ${url}`); - const xhr = new XMLHttpRequest(); - xhr.responseType = 'arraybuffer'; - xhr.onreadystatechange = () => { - if (!this.hls || !keySessionContext.mediaKeysSession) { - return reject(new Error('invalid state')); - } - if (xhr.readyState === 4) { - if (xhr.status === 200) { - this._requestLicenseFailureCount = 0; - let data = xhr.response; - this.log(`License received ${data instanceof ArrayBuffer ? data.byteLength : data}`); - const licenseResponseCallback = this.config.licenseResponseCallback; - if (licenseResponseCallback) { - try { - data = licenseResponseCallback.call(this.hls, xhr, url, keySessionContext); - } catch (error) { - this.error(error); - } - } - resolve(data); - } else { - const retryConfig = keyLoadPolicy.errorRetry; - const maxNumRetry = retryConfig ? retryConfig.maxNumRetry : 0; - this._requestLicenseFailureCount++; - if (this._requestLicenseFailureCount > maxNumRetry || xhr.status >= 400 && xhr.status < 500) { - reject(new EMEKeyError({ - type: ErrorTypes.KEY_SYSTEM_ERROR, - details: ErrorDetails.KEY_SYSTEM_LICENSE_REQUEST_FAILED, - fatal: true, - networkDetails: xhr, - response: { - url, - data: undefined, - code: xhr.status, - text: xhr.statusText - } - }, `License Request XHR failed (${url}). Status: ${xhr.status} (${xhr.statusText})`)); - } else { - const attemptsLeft = maxNumRetry - this._requestLicenseFailureCount + 1; - this.warn(`Retrying license request, ${attemptsLeft} attempts left`); - this.requestLicense(keySessionContext, licenseChallenge).then(resolve, reject); - } - } - } - }; - if (keySessionContext.licenseXhr && keySessionContext.licenseXhr.readyState !== XMLHttpRequest.DONE) { - keySessionContext.licenseXhr.abort(); - } - keySessionContext.licenseXhr = xhr; - this.setupLicenseXHR(xhr, url, keySessionContext, licenseChallenge).then(({ - xhr, - licenseChallenge - }) => { - if (keySessionContext.keySystem == KeySystems.PLAYREADY) { - licenseChallenge = this.unpackPlayReadyKeyMessage(xhr, licenseChallenge); - } - xhr.send(licenseChallenge); - }); - }); - } - onMediaAttached(event, data) { - if (!this.config.emeEnabled) { - return; - } - const media = data.media; - - // keep reference of media - this.media = media; - media.removeEventListener('encrypted', this.onMediaEncrypted); - media.removeEventListener('waitingforkey', this.onWaitingForKey); - media.addEventListener('encrypted', this.onMediaEncrypted); - media.addEventListener('waitingforkey', this.onWaitingForKey); - } - onMediaDetached() { - var _media$setMediaKeys; - const media = this.media; - const mediaKeysList = this.mediaKeySessions; - if (media) { - media.removeEventListener('encrypted', this.onMediaEncrypted); - media.removeEventListener('waitingforkey', this.onWaitingForKey); - this.media = null; - } - this._requestLicenseFailureCount = 0; - this.setMediaKeysQueue = []; - this.mediaKeySessions = []; - this.keyIdToKeySessionPromise = {}; - LevelKey.clearKeyUriToKeyIdMap(); - - // Close all sessions and remove media keys from the video element. - const keySessionCount = mediaKeysList.length; - EMEController.CDMCleanupPromise = Promise.all(mediaKeysList.map(mediaKeySessionContext => this.removeSession(mediaKeySessionContext)).concat(media == null ? void 0 : (_media$setMediaKeys = media.setMediaKeys(null)) == null ? void 0 : _media$setMediaKeys.catch(error => { - this.log(`Could not clear media keys: ${error}`); - }))).then(() => { - if (keySessionCount) { - this.log('finished closing key sessions and clearing media keys'); - mediaKeysList.length = 0; - } - }).catch(error => { - this.log(`Could not close sessions and clear media keys: ${error}`); - }); - } - onManifestLoading() { - this.keyFormatPromise = null; - } - onManifestLoaded(event, { - sessionKeys - }) { - if (!sessionKeys || !this.config.emeEnabled) { - return; - } - if (!this.keyFormatPromise) { - const keyFormats = sessionKeys.reduce((formats, sessionKey) => { - if (formats.indexOf(sessionKey.keyFormat) === -1) { - formats.push(sessionKey.keyFormat); - } - return formats; - }, []); - this.log(`Selecting key-system from session-keys ${keyFormats.join(', ')}`); - this.keyFormatPromise = this.getKeyFormatPromise(keyFormats); - } - } - removeSession(mediaKeySessionContext) { - const { - mediaKeysSession, - licenseXhr - } = mediaKeySessionContext; - if (mediaKeysSession) { - this.log(`Remove licenses and keys and close session ${mediaKeysSession.sessionId}`); - if (mediaKeySessionContext._onmessage) { - mediaKeysSession.removeEventListener('message', mediaKeySessionContext._onmessage); - mediaKeySessionContext._onmessage = undefined; - } - if (mediaKeySessionContext._onkeystatuseschange) { - mediaKeysSession.removeEventListener('keystatuseschange', mediaKeySessionContext._onkeystatuseschange); - mediaKeySessionContext._onkeystatuseschange = undefined; - } - if (licenseXhr && licenseXhr.readyState !== XMLHttpRequest.DONE) { - licenseXhr.abort(); - } - mediaKeySessionContext.mediaKeysSession = mediaKeySessionContext.decryptdata = mediaKeySessionContext.licenseXhr = undefined; - const index = this.mediaKeySessions.indexOf(mediaKeySessionContext); - if (index > -1) { - this.mediaKeySessions.splice(index, 1); - } - return mediaKeysSession.remove().catch(error => { - this.log(`Could not remove session: ${error}`); - }).then(() => { - return mediaKeysSession.close(); - }).catch(error => { - this.log(`Could not close session: ${error}`); - }); - } - } -} -EMEController.CDMCleanupPromise = void 0; -class EMEKeyError extends Error { - constructor(data, message) { - super(message); - this.data = void 0; - data.error || (data.error = new Error(message)); - this.data = data; - data.err = data.error; - } -} - -/** - * Common Media Client Data Object Type - * - * @group CMCD - * - * @beta - */ -var CmcdObjectType; -(function (CmcdObjectType) { - /** - * text file, such as a manifest or playlist - */ - CmcdObjectType["MANIFEST"] = "m"; - /** - * audio only - */ - CmcdObjectType["AUDIO"] = "a"; - /** - * video only - */ - CmcdObjectType["VIDEO"] = "v"; - /** - * muxed audio and video - */ - CmcdObjectType["MUXED"] = "av"; - /** - * init segment - */ - CmcdObjectType["INIT"] = "i"; - /** - * caption or subtitle - */ - CmcdObjectType["CAPTION"] = "c"; - /** - * ISOBMFF timed text track - */ - CmcdObjectType["TIMED_TEXT"] = "tt"; - /** - * cryptographic key, license or certificate. - */ - CmcdObjectType["KEY"] = "k"; - /** - * other - */ - CmcdObjectType["OTHER"] = "o"; -})(CmcdObjectType || (CmcdObjectType = {})); - -/** - * Common Media Client Data Streaming Format - * - * @group CMCD - * - * @beta - */ -var CmcdStreamingFormat; -(function (CmcdStreamingFormat) { - /** - * MPEG DASH - */ - CmcdStreamingFormat["DASH"] = "d"; - /** - * HTTP Live Streaming (HLS) - */ - CmcdStreamingFormat["HLS"] = "h"; - /** - * Smooth Streaming - */ - CmcdStreamingFormat["SMOOTH"] = "s"; - /** - * Other - */ - CmcdStreamingFormat["OTHER"] = "o"; -})(CmcdStreamingFormat || (CmcdStreamingFormat = {})); - -/** - * CMCD header fields. - * - * @group CMCD - * - * @beta - */ -var CmcdHeaderField; -(function (CmcdHeaderField) { - /** - * keys whose values vary with the object being requested. - */ - CmcdHeaderField["OBJECT"] = "CMCD-Object"; - /** - * keys whose values vary with each request. - */ - CmcdHeaderField["REQUEST"] = "CMCD-Request"; - /** - * keys whose values are expected to be invariant over the life of the session. - */ - CmcdHeaderField["SESSION"] = "CMCD-Session"; - /** - * keys whose values do not vary with every request or object. - */ - CmcdHeaderField["STATUS"] = "CMCD-Status"; -})(CmcdHeaderField || (CmcdHeaderField = {})); - -/** - * The map of CMCD header fields to official CMCD keys. - * - * @internal - * - * @group CMCD - */ -const CmcdHeaderMap = { - [CmcdHeaderField.OBJECT]: ['br', 'd', 'ot', 'tb'], - [CmcdHeaderField.REQUEST]: ['bl', 'dl', 'mtp', 'nor', 'nrr', 'su'], - [CmcdHeaderField.SESSION]: ['cid', 'pr', 'sf', 'sid', 'st', 'v'], - [CmcdHeaderField.STATUS]: ['bs', 'rtp'] -}; - -/** - * Structured Field Item - * - * @group Structured Field - * - * @beta - */ -class SfItem { - constructor(value, params) { - if (Array.isArray(value)) { - value = value.map(v => v instanceof SfItem ? v : new SfItem(v)); - } - this.value = value; - this.params = params; - } -} - -const DICT = 'Dict'; - -function format(value) { - if (Array.isArray(value)) { - return JSON.stringify(value); - } - if (value instanceof Map) { - return 'Map{}'; - } - if (value instanceof Set) { - return 'Set{}'; - } - if (typeof value === 'object') { - return JSON.stringify(value); - } - return String(value); -} -function throwError(action, src, type, cause) { - return new Error(`failed to ${action} "${format(src)}" as ${type}`, { - cause - }); -} - -function serializeError(src, type, cause) { - return throwError('serialize', src, type, cause); -} - -/** - * A class to represent structured field tokens when `Symbol` is not available. - * - * @group Structured Field - * - * @beta - */ -class SfToken { - constructor(description) { - this.description = description; - } -} - -const BARE_ITEM = 'Bare Item'; - -const BOOLEAN = 'Boolean'; - -// 4.1.9. Serializing a Boolean -// -// Given a Boolean as input_boolean, return an ASCII string suitable for -// use in a HTTP field value. -// -// 1. If input_boolean is not a boolean, fail serialization. -// -// 2. Let output be an empty string. -// -// 3. Append "?" to output. -// -// 4. If input_boolean is true, append "1" to output. -// -// 5. If input_boolean is false, append "0" to output. -// -// 6. Return output. -function serializeBoolean(value) { - if (typeof value !== 'boolean') { - throw serializeError(value, BOOLEAN); - } - return value ? '?1' : '?0'; -} - -const BYTES = 'Byte Sequence'; - -// 4.1.8. Serializing a Byte Sequence -// -// Given a Byte Sequence as input_bytes, return an ASCII string suitable -// for use in a HTTP field value. -// -// 1. If input_bytes is not a sequence of bytes, fail serialization. -// -// 2. Let output be an empty string. -// -// 3. Append ":" to output. -// -// 4. Append the result of base64-encoding input_bytes as per -// [RFC4648], Section 4, taking account of the requirements below. -// -// 5. Append ":" to output. -// -// 6. Return output. -// -// The encoded data is required to be padded with "=", as per [RFC4648], -// Section 3.2. -// -// Likewise, encoded data SHOULD have pad bits set to zero, as per -// [RFC4648], Section 3.5, unless it is not possible to do so due to -// implementation constraints. -function serializeByteSequence(value) { - if (ArrayBuffer.isView(value) === false) { - throw serializeError(value, BYTES); - } - return `:${base64encode(value)}:`; -} - -const INTEGER = 'Integer'; - -function isInvalidInt(value) { - return value < -999999999999999 || 999999999999999 < value; -} - -// 4.1.4. Serializing an Integer -// -// Given an Integer as input_integer, return an ASCII string suitable -// for use in a HTTP field value. -// -// 1. If input_integer is not an integer in the range of -// -999,999,999,999,999 to 999,999,999,999,999 inclusive, fail -// serialization. -// -// 2. Let output be an empty string. -// -// 3. If input_integer is less than (but not equal to) 0, append "-" to -// output. -// -// 4. Append input_integer's numeric value represented in base 10 using -// only decimal digits to output. -// -// 5. Return output. -function serializeInteger(value) { - if (isInvalidInt(value)) { - throw serializeError(value, INTEGER); - } - return value.toString(); -} - -// 4.1.10. Serializing a Date -// -// Given a Date as input_integer, return an ASCII string suitable for -// use in an HTTP field value. -// 1. Let output be "@". -// 2. Append to output the result of running Serializing an Integer -// with input_date (Section 4.1.4). -// 3. Return output. -function serializeDate(value) { - return `@${serializeInteger(value.getTime() / 1000)}`; -} - -const DECIMAL = 'Decimal'; - -// 4.1.5. Serializing a Decimal -// -// Given a decimal number as input_decimal, return an ASCII string -// suitable for use in a HTTP field value. -// -// 1. If input_decimal is not a decimal number, fail serialization. -// -// 2. If input_decimal has more than three significant digits to the -// right of the decimal point, round it to three decimal places, -// rounding the final digit to the nearest value, or to the even -// value if it is equidistant. -// -// 3. If input_decimal has more than 12 significant digits to the left -// of the decimal point after rounding, fail serialization. -// -// 4. Let output be an empty string. -// -// 5. If input_decimal is less than (but not equal to) 0, append "-" -// to output. -// -// 6. Append input_decimal's integer component represented in base 10 -// (using only decimal digits) to output; if it is zero, append -// "0". -// -// 7. Append "." to output. -// -// 8. If input_decimal's fractional component is zero, append "0" to -// output. -// -// 9. Otherwise, append the significant digits of input_decimal's -// fractional component represented in base 10 (using only decimal -// digits) to output. -// -// 10. Return output. -function serializeDecimal(value) { - const roundedValue = roundToEven(value, 3); // round to 3 decimal places - if (Math.floor(Math.abs(roundedValue)).toString().length > 12) { - throw serializeError(value, DECIMAL); - } - const stringValue = roundedValue.toString(); - return stringValue.includes('.') ? stringValue : `${stringValue}.0`; -} - -const STRING = 'String'; - -const STRING_REGEX = /[\x00-\x1f\x7f]+/; // eslint-disable-line no-control-regex - -// 4.1.6. Serializing a String -// -// Given a String as input_string, return an ASCII string suitable for -// use in a HTTP field value. -// -// 1. Convert input_string into a sequence of ASCII characters; if -// conversion fails, fail serialization. -// -// 2. If input_string contains characters in the range %x00-1f or %x7f -// (i.e., not in VCHAR or SP), fail serialization. -// -// 3. Let output be the string DQUOTE. -// -// 4. For each character char in input_string: -// -// 1. If char is "\" or DQUOTE: -// -// 1. Append "\" to output. -// -// 2. Append char to output. -// -// 5. Append DQUOTE to output. -// -// 6. Return output. -function serializeString(value) { - if (STRING_REGEX.test(value)) { - throw serializeError(value, STRING); - } - return `"${value.replace(/\\/g, `\\\\`).replace(/"/g, `\\"`)}"`; -} - -/** - * Converts a symbol to a string. - * - * @param symbol - The symbol to convert. - * - * @returns The string representation of the symbol. - * - * @internal - */ -function symbolToStr(symbol) { - return symbol.description || symbol.toString().slice(7, -1); -} - -const TOKEN = 'Token'; - -function serializeToken(token) { - const value = symbolToStr(token); - if (/^([a-zA-Z*])([!#$%&'*+\-.^_`|~\w:/]*)$/.test(value) === false) { - throw serializeError(value, TOKEN); - } - return value; -} - -// 4.1.3.1. Serializing a Bare Item -// -// Given an Item as input_item, return an ASCII string suitable for use -// in a HTTP field value. -// -// 1. If input_item is an Integer, return the result of running -// Serializing an Integer (Section 4.1.4) with input_item. -// -// 2. If input_item is a Decimal, return the result of running -// Serializing a Decimal (Section 4.1.5) with input_item. -// -// 3. If input_item is a String, return the result of running -// Serializing a String (Section 4.1.6) with input_item. -// -// 4. If input_item is a Token, return the result of running -// Serializing a Token (Section 4.1.7) with input_item. -// -// 5. If input_item is a Boolean, return the result of running -// Serializing a Boolean (Section 4.1.9) with input_item. -// -// 6. If input_item is a Byte Sequence, return the result of running -// Serializing a Byte Sequence (Section 4.1.8) with input_item. -// -// 7. If input_item is a Date, return the result of running Serializing -// a Date (Section 4.1.10) with input_item. -// -// 8. Otherwise, fail serialization. -function serializeBareItem(value) { - switch (typeof value) { - case 'number': - if (!isFiniteNumber(value)) { - throw serializeError(value, BARE_ITEM); - } - if (Number.isInteger(value)) { - return serializeInteger(value); - } - return serializeDecimal(value); - case 'string': - return serializeString(value); - case 'symbol': - return serializeToken(value); - case 'boolean': - return serializeBoolean(value); - case 'object': - if (value instanceof Date) { - return serializeDate(value); - } - if (value instanceof Uint8Array) { - return serializeByteSequence(value); - } - if (value instanceof SfToken) { - return serializeToken(value); - } - default: - // fail - throw serializeError(value, BARE_ITEM); - } -} - -const KEY = 'Key'; - -// 4.1.1.3. Serializing a Key -// -// Given a key as input_key, return an ASCII string suitable for use in -// a HTTP field value. -// -// 1. Convert input_key into a sequence of ASCII characters; if -// conversion fails, fail serialization. -// -// 2. If input_key contains characters not in lcalpha, DIGIT, "_", "-", -// ".", or "*" fail serialization. -// -// 3. If the first character of input_key is not lcalpha or "*", fail -// serialization. -// -// 4. Let output be an empty string. -// -// 5. Append input_key to output. -// -// 6. Return output. -function serializeKey(value) { - if (/^[a-z*][a-z0-9\-_.*]*$/.test(value) === false) { - throw serializeError(value, KEY); - } - return value; -} - -// 4.1.1.2. Serializing Parameters -// -// Given an ordered Dictionary as input_parameters (each member having a -// param_name and a param_value), return an ASCII string suitable for -// use in a HTTP field value. -// -// 1. Let output be an empty string. -// -// 2. For each param_name with a value of param_value in -// input_parameters: -// -// 1. Append ";" to output. -// -// 2. Append the result of running Serializing a Key -// (Section 4.1.1.3) with param_name to output. -// -// 3. If param_value is not Boolean true: -// -// 1. Append "=" to output. -// -// 2. Append the result of running Serializing a bare Item -// (Section 4.1.3.1) with param_value to output. -// -// 3. Return output. -function serializeParams(params) { - if (params == null) { - return ''; - } - return Object.entries(params).map(([key, value]) => { - if (value === true) { - return `;${serializeKey(key)}`; // omit true - } - return `;${serializeKey(key)}=${serializeBareItem(value)}`; - }).join(''); -} - -// 4.1.3. Serializing an Item -// -// Given an Item as bare_item and Parameters as item_parameters, return -// an ASCII string suitable for use in a HTTP field value. -// -// 1. Let output be an empty string. -// -// 2. Append the result of running Serializing a Bare Item -// Section 4.1.3.1 with bare_item to output. -// -// 3. Append the result of running Serializing Parameters -// Section 4.1.1.2 with item_parameters to output. -// -// 4. Return output. -function serializeItem(value) { - if (value instanceof SfItem) { - return `${serializeBareItem(value.value)}${serializeParams(value.params)}`; - } else { - return serializeBareItem(value); - } -} - -// 4.1.1.1. Serializing an Inner List -// -// Given an array of (member_value, parameters) tuples as inner_list, -// and parameters as list_parameters, return an ASCII string suitable -// for use in a HTTP field value. -// -// 1. Let output be the string "(". -// -// 2. For each (member_value, parameters) of inner_list: -// -// 1. Append the result of running Serializing an Item -// (Section 4.1.3) with (member_value, parameters) to output. -// -// 2. If more values remain in inner_list, append a single SP to -// output. -// -// 3. Append ")" to output. -// -// 4. Append the result of running Serializing Parameters -// (Section 4.1.1.2) with list_parameters to output. -// -// 5. Return output. -function serializeInnerList(value) { - return `(${value.value.map(serializeItem).join(' ')})${serializeParams(value.params)}`; -} - -// 4.1.2. Serializing a Dictionary -// -// Given an ordered Dictionary as input_dictionary (each member having a -// member_name and a tuple value of (member_value, parameters)), return -// an ASCII string suitable for use in a HTTP field value. -// -// 1. Let output be an empty string. -// -// 2. For each member_name with a value of (member_value, parameters) -// in input_dictionary: -// -// 1. Append the result of running Serializing a Key -// (Section 4.1.1.3) with member's member_name to output. -// -// 2. If member_value is Boolean true: -// -// 1. Append the result of running Serializing Parameters -// (Section 4.1.1.2) with parameters to output. -// -// 3. Otherwise: -// -// 1. Append "=" to output. -// -// 2. If member_value is an array, append the result of running -// Serializing an Inner List (Section 4.1.1.1) with -// (member_value, parameters) to output. -// -// 3. Otherwise, append the result of running Serializing an -// Item (Section 4.1.3) with (member_value, parameters) to -// output. -// -// 4. If more members remain in input_dictionary: -// -// 1. Append "," to output. -// -// 2. Append a single SP to output. -// -// 3. Return output. -function serializeDict(dict, options = { - whitespace: true -}) { - if (typeof dict !== 'object') { - throw serializeError(dict, DICT); - } - const entries = dict instanceof Map ? dict.entries() : Object.entries(dict); - const optionalWhiteSpace = (options === null || options === void 0 ? void 0 : options.whitespace) ? ' ' : ''; - return Array.from(entries).map(([key, item]) => { - if (item instanceof SfItem === false) { - item = new SfItem(item); - } - let output = serializeKey(key); - if (item.value === true) { - output += serializeParams(item.params); - } else { - output += '='; - if (Array.isArray(item.value)) { - output += serializeInnerList(item); - } else { - output += serializeItem(item); - } - } - return output; - }).join(`,${optionalWhiteSpace}`); -} - -/** - * Encode an object into a structured field dictionary - * - * @param value - The structured field dictionary to encode - * @param options - Encoding options - * - * @returns The structured field string - * - * @group Structured Field - * - * @beta - */ -function encodeSfDict(value, options) { - return serializeDict(value, options); -} - -/** - * Checks if the given key is a token field. - * - * @param key - The key to check. - * - * @returns `true` if the key is a token field. - * - * @internal - */ -function isTokenField(key) { - return key === 'ot' || key === 'sf' || key === 'st'; -} - -/** - * Checks if the given value is valid - * - * @param value - The value to check. - * - * @returns `true` if the key is a value is valid. - * - * @internal - */ -function isValid(value) { - if (typeof value === 'number') { - return isFiniteNumber(value); - } - return value != null && value !== '' && value !== false; -} - -const toRounded = value => Math.round(value); -const toUrlSafe = (value, options) => { - if (options === null || options === void 0 ? void 0 : options.baseUrl) { - value = urlToRelativePath(value, options.baseUrl); - } - return encodeURIComponent(value); -}; -const toHundred = value => toRounded(value / 100) * 100; -/** - * The default formatters for CMCD values. - * - * @group CMCD - * - * @beta - */ -const CmcdFormatters = { - /** - * Bitrate (kbps) rounded integer - */ - br: toRounded, - /** - * Duration (milliseconds) rounded integer - */ - d: toRounded, - /** - * Buffer Length (milliseconds) rounded nearest 100ms - */ - bl: toHundred, - /** - * Deadline (milliseconds) rounded nearest 100ms - */ - dl: toHundred, - /** - * Measured Throughput (kbps) rounded nearest 100kbps - */ - mtp: toHundred, - /** - * Next Object Request URL encoded - */ - nor: toUrlSafe, - /** - * Requested maximum throughput (kbps) rounded nearest 100kbps - */ - rtp: toHundred, - /** - * Top Bitrate (kbps) rounded integer - */ - tb: toRounded -}; - -/** - * Internal CMCD processing function. - * - * @param obj - The CMCD object to process. - * @param map - The mapping function to use. - * @param options - Options for encoding. - * - * @internal - * - * @group CMCD - */ -function processCmcd(obj, options) { - const results = {}; - if (obj == null || typeof obj !== 'object') { - return results; - } - const keys = Object.keys(obj).sort(); - const formatters = _extends({}, CmcdFormatters, options === null || options === void 0 ? void 0 : options.formatters); - const filter = options === null || options === void 0 ? void 0 : options.filter; - keys.forEach(key => { - if (filter === null || filter === void 0 ? void 0 : filter(key)) { - return; - } - let value = obj[key]; - const formatter = formatters[key]; - if (formatter) { - value = formatter(value, options); - } - // Version should only be reported if not equal to 1. - if (key === 'v' && value === 1) { - return; - } - // Playback rate should only be sent if not equal to 1. - if (key == 'pr' && value === 1) { - return; - } - // ignore invalid values - if (!isValid(value)) { - return; - } - if (isTokenField(key) && typeof value === 'string') { - value = new SfToken(value); - } - results[key] = value; - }); - return results; -} - -/** - * Encode a CMCD object to a string. - * - * @param cmcd - The CMCD object to encode. - * @param options - Options for encoding. - * - * @returns The encoded CMCD string. - * - * @group CMCD - * - * @beta - */ -function encodeCmcd(cmcd, options = {}) { - if (!cmcd) { - return ''; - } - return encodeSfDict(processCmcd(cmcd, options), _extends({ - whitespace: false - }, options)); -} - -/** - * Convert a CMCD data object to request headers - * - * @param cmcd - The CMCD data object to convert. - * @param options - Options for encoding the CMCD object. - * - * @returns The CMCD header shards. - * - * @group CMCD - * - * @beta - */ -function toCmcdHeaders(cmcd, options = {}) { - const result = {}; - if (!cmcd) { - return result; - } - const entries = Object.entries(cmcd); - const headerMap = Object.entries(CmcdHeaderMap).concat(Object.entries((options === null || options === void 0 ? void 0 : options.customHeaderMap) || {})); - const shards = entries.reduce((acc, entry) => { - var _a, _b; - const [key, value] = entry; - const field = ((_a = headerMap.find(entry => entry[1].includes(key))) === null || _a === void 0 ? void 0 : _a[0]) || CmcdHeaderField.REQUEST; - (_b = acc[field]) !== null && _b !== void 0 ? _b : acc[field] = {}; - acc[field][key] = value; - return acc; - }, {}); - return Object.entries(shards).reduce((acc, [field, value]) => { - acc[field] = encodeCmcd(value, options); - return acc; - }, result); -} - -/** - * Append CMCD query args to a header object. - * - * @param headers - The headers to append to. - * @param cmcd - The CMCD object to append. - * @param options - Encode options. - * - * @returns The headers with the CMCD header shards appended. - * - * @group CMCD - * - * @beta - */ -function appendCmcdHeaders(headers, cmcd, options) { - return _extends(headers, toCmcdHeaders(cmcd, options)); -} - -/** - * CMCD parameter name. - * - * @group CMCD - * - * @beta - */ -const CMCD_PARAM = 'CMCD'; - -/** - * Convert a CMCD data object to a query arg. - * - * @param cmcd - The CMCD object to convert. - * @param options - Options for encoding the CMCD object. - * - * @returns The CMCD query arg. - * - * @group CMCD - * - * @beta - */ -function toCmcdQuery(cmcd, options = {}) { - if (!cmcd) { - return ''; - } - const params = encodeCmcd(cmcd, options); - return `${CMCD_PARAM}=${encodeURIComponent(params)}`; -} - -const REGEX = /CMCD=[^&#]+/; -/** - * Append CMCD query args to a URL. - * - * @param url - The URL to append to. - * @param cmcd - The CMCD object to append. - * @param options - Options for encoding the CMCD object. - * - * @returns The URL with the CMCD query args appended. - * - * @group CMCD - * - * @beta - */ -function appendCmcdQuery(url, cmcd, options) { - // TODO: Replace with URLSearchParams once we drop Safari < 10.1 & Chrome < 49 support. - // https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams - const query = toCmcdQuery(cmcd, options); - if (!query) { - return url; - } - if (REGEX.test(url)) { - return url.replace(REGEX, query); - } - const separator = url.includes('?') ? '&' : '?'; - return `${url}${separator}${query}`; -} - -/** - * Controller to deal with Common Media Client Data (CMCD) - * @see https://cdn.cta.tech/cta/media/media/resources/standards/pdfs/cta-5004-final.pdf - */ -class CMCDController { - constructor(hls) { - this.hls = void 0; - this.config = void 0; - this.media = void 0; - this.sid = void 0; - this.cid = void 0; - this.useHeaders = false; - this.includeKeys = void 0; - this.initialized = false; - this.starved = false; - this.buffering = true; - this.audioBuffer = void 0; - this.videoBuffer = void 0; - this.onWaiting = () => { - if (this.initialized) { - this.starved = true; - } - this.buffering = true; - }; - this.onPlaying = () => { - if (!this.initialized) { - this.initialized = true; - } - this.buffering = false; - }; - /** - * Apply CMCD data to a manifest request. - */ - this.applyPlaylistData = context => { - try { - this.apply(context, { - ot: CmcdObjectType.MANIFEST, - su: !this.initialized - }); - } catch (error) { - this.hls.logger.warn('Could not generate manifest CMCD data.', error); - } - }; - /** - * Apply CMCD data to a segment request - */ - this.applyFragmentData = context => { - try { - const { - frag, - part - } = context; - const level = this.hls.levels[frag.level]; - const ot = this.getObjectType(frag); - const data = { - d: (part || frag).duration * 1000, - ot - }; - if (ot === CmcdObjectType.VIDEO || ot === CmcdObjectType.AUDIO || ot == CmcdObjectType.MUXED) { - data.br = level.bitrate / 1000; - data.tb = this.getTopBandwidth(ot) / 1000; - data.bl = this.getBufferLength(ot); - } - const next = part ? this.getNextPart(part) : this.getNextFrag(frag); - if (next != null && next.url && next.url !== frag.url) { - data.nor = next.url; - } - this.apply(context, data); - } catch (error) { - this.hls.logger.warn('Could not generate segment CMCD data.', error); - } - }; - this.hls = hls; - const config = this.config = hls.config; - const { - cmcd - } = config; - if (cmcd != null) { - config.pLoader = this.createPlaylistLoader(); - config.fLoader = this.createFragmentLoader(); - this.sid = cmcd.sessionId || hls.sessionId; - this.cid = cmcd.contentId; - this.useHeaders = cmcd.useHeaders === true; - this.includeKeys = cmcd.includeKeys; - this.registerListeners(); - } - } - registerListeners() { - const hls = this.hls; - hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this); - hls.on(Events.MEDIA_DETACHED, this.onMediaDetached, this); - hls.on(Events.BUFFER_CREATED, this.onBufferCreated, this); - } - unregisterListeners() { - const hls = this.hls; - hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this); - hls.off(Events.MEDIA_DETACHED, this.onMediaDetached, this); - hls.off(Events.BUFFER_CREATED, this.onBufferCreated, this); - } - destroy() { - this.unregisterListeners(); - this.onMediaDetached(); - - // @ts-ignore - this.hls = this.config = this.audioBuffer = this.videoBuffer = null; - // @ts-ignore - this.onWaiting = this.onPlaying = this.media = null; - } - onMediaAttached(event, data) { - this.media = data.media; - this.media.addEventListener('waiting', this.onWaiting); - this.media.addEventListener('playing', this.onPlaying); - } - onMediaDetached() { - if (!this.media) { - return; - } - this.media.removeEventListener('waiting', this.onWaiting); - this.media.removeEventListener('playing', this.onPlaying); - - // @ts-ignore - this.media = null; - } - onBufferCreated(event, data) { - var _data$tracks$audio, _data$tracks$video; - this.audioBuffer = (_data$tracks$audio = data.tracks.audio) == null ? void 0 : _data$tracks$audio.buffer; - this.videoBuffer = (_data$tracks$video = data.tracks.video) == null ? void 0 : _data$tracks$video.buffer; - } - /** - * Create baseline CMCD data - */ - createData() { - var _this$media; - return { - v: 1, - sf: CmcdStreamingFormat.HLS, - sid: this.sid, - cid: this.cid, - pr: (_this$media = this.media) == null ? void 0 : _this$media.playbackRate, - mtp: this.hls.bandwidthEstimate / 1000 - }; - } - - /** - * Apply CMCD data to a request. - */ - apply(context, data = {}) { - // apply baseline data - _extends(data, this.createData()); - const isVideo = data.ot === CmcdObjectType.INIT || data.ot === CmcdObjectType.VIDEO || data.ot === CmcdObjectType.MUXED; - if (this.starved && isVideo) { - data.bs = true; - data.su = true; - this.starved = false; - } - if (data.su == null) { - data.su = this.buffering; - } - - // TODO: Implement rtp, nrr, dl - - const { - includeKeys - } = this; - if (includeKeys) { - data = Object.keys(data).reduce((acc, key) => { - includeKeys.includes(key) && (acc[key] = data[key]); - return acc; - }, {}); - } - const options = { - baseUrl: context.url - }; - if (this.useHeaders) { - if (!context.headers) { - context.headers = {}; - } - appendCmcdHeaders(context.headers, data, options); - } else { - context.url = appendCmcdQuery(context.url, data, options); - } - } - getNextFrag(fragment) { - var _this$hls$levels$frag; - const levelDetails = (_this$hls$levels$frag = this.hls.levels[fragment.level]) == null ? void 0 : _this$hls$levels$frag.details; - if (levelDetails) { - const index = fragment.sn - levelDetails.startSN; - return levelDetails.fragments[index + 1]; - } - return undefined; - } - getNextPart(part) { - var _this$hls$levels$frag2, _this$hls$levels$frag3; - const { - index, - fragment - } = part; - const partList = (_this$hls$levels$frag2 = this.hls.levels[fragment.level]) == null ? void 0 : (_this$hls$levels$frag3 = _this$hls$levels$frag2.details) == null ? void 0 : _this$hls$levels$frag3.partList; - if (partList) { - const { - sn - } = fragment; - for (let i = partList.length - 1; i >= 0; i--) { - const p = partList[i]; - if (p.index === index && p.fragment.sn === sn) { - return partList[i + 1]; - } - } - } - return undefined; - } - - /** - * The CMCD object type. - */ - getObjectType(fragment) { - const { - type - } = fragment; - if (type === 'subtitle') { - return CmcdObjectType.TIMED_TEXT; - } - if (fragment.sn === 'initSegment') { - return CmcdObjectType.INIT; - } - if (type === 'audio') { - return CmcdObjectType.AUDIO; - } - if (type === 'main') { - if (!this.hls.audioTracks.length) { - return CmcdObjectType.MUXED; - } - return CmcdObjectType.VIDEO; - } - return undefined; - } - - /** - * Get the highest bitrate. - */ - getTopBandwidth(type) { - let bitrate = 0; - let levels; - const hls = this.hls; - if (type === CmcdObjectType.AUDIO) { - levels = hls.audioTracks; - } else { - const max = hls.maxAutoLevel; - const len = max > -1 ? max + 1 : hls.levels.length; - levels = hls.levels.slice(0, len); - } - for (const level of levels) { - if (level.bitrate > bitrate) { - bitrate = level.bitrate; - } - } - return bitrate > 0 ? bitrate : NaN; - } - - /** - * Get the buffer length for a media type in milliseconds - */ - getBufferLength(type) { - const media = this.media; - const buffer = type === CmcdObjectType.AUDIO ? this.audioBuffer : this.videoBuffer; - if (!buffer || !media) { - return NaN; - } - const info = BufferHelper.bufferInfo(buffer, media.currentTime, this.config.maxBufferHole); - return info.len * 1000; - } - - /** - * Create a playlist loader - */ - createPlaylistLoader() { - const { - pLoader - } = this.config; - const apply = this.applyPlaylistData; - const Ctor = pLoader || this.config.loader; - return class CmcdPlaylistLoader { - constructor(config) { - this.loader = void 0; - this.loader = new Ctor(config); - } - get stats() { - return this.loader.stats; - } - get context() { - return this.loader.context; - } - destroy() { - this.loader.destroy(); - } - abort() { - this.loader.abort(); - } - load(context, config, callbacks) { - apply(context); - this.loader.load(context, config, callbacks); - } - }; - } - - /** - * Create a playlist loader - */ - createFragmentLoader() { - const { - fLoader - } = this.config; - const apply = this.applyFragmentData; - const Ctor = fLoader || this.config.loader; - return class CmcdFragmentLoader { - constructor(config) { - this.loader = void 0; - this.loader = new Ctor(config); - } - get stats() { - return this.loader.stats; - } - get context() { - return this.loader.context; - } - destroy() { - this.loader.destroy(); - } - abort() { - this.loader.abort(); - } - load(context, config, callbacks) { - apply(context); - this.loader.load(context, config, callbacks); - } - }; - } -} - -const PATHWAY_PENALTY_DURATION_MS = 300000; -class ContentSteeringController extends Logger { - constructor(hls) { - super('content-steering', hls.logger); - this.hls = void 0; - this.loader = null; - this.uri = null; - this.pathwayId = '.'; - this._pathwayPriority = null; - this.timeToLoad = 300; - this.reloadTimer = -1; - this.updated = 0; - this.started = false; - this.enabled = true; - this.levels = null; - this.audioTracks = null; - this.subtitleTracks = null; - this.penalizedPathways = {}; - this.hls = hls; - this.registerListeners(); - } - registerListeners() { - const hls = this.hls; - hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); - hls.on(Events.MANIFEST_LOADED, this.onManifestLoaded, this); - hls.on(Events.MANIFEST_PARSED, this.onManifestParsed, this); - hls.on(Events.ERROR, this.onError, this); - } - unregisterListeners() { - const hls = this.hls; - if (!hls) { - return; - } - hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); - hls.off(Events.MANIFEST_LOADED, this.onManifestLoaded, this); - hls.off(Events.MANIFEST_PARSED, this.onManifestParsed, this); - hls.off(Events.ERROR, this.onError, this); - } - pathways() { - return (this.levels || []).reduce((pathways, level) => { - if (pathways.indexOf(level.pathwayId) === -1) { - pathways.push(level.pathwayId); - } - return pathways; - }, []); - } - get pathwayPriority() { - return this._pathwayPriority; - } - set pathwayPriority(pathwayPriority) { - this.updatePathwayPriority(pathwayPriority); - } - startLoad() { - this.started = true; - this.clearTimeout(); - if (this.enabled && this.uri) { - if (this.updated) { - const ttl = this.timeToLoad * 1000 - (performance.now() - this.updated); - if (ttl > 0) { - this.scheduleRefresh(this.uri, ttl); - return; - } - } - this.loadSteeringManifest(this.uri); - } - } - stopLoad() { - this.started = false; - if (this.loader) { - this.loader.destroy(); - this.loader = null; - } - this.clearTimeout(); - } - clearTimeout() { - if (this.reloadTimer !== -1) { - self.clearTimeout(this.reloadTimer); - this.reloadTimer = -1; - } - } - destroy() { - this.unregisterListeners(); - this.stopLoad(); - // @ts-ignore - this.hls = null; - this.levels = this.audioTracks = this.subtitleTracks = null; - } - removeLevel(levelToRemove) { - const levels = this.levels; - if (levels) { - this.levels = levels.filter(level => level !== levelToRemove); - } - } - onManifestLoading() { - this.stopLoad(); - this.enabled = true; - this.timeToLoad = 300; - this.updated = 0; - this.uri = null; - this.pathwayId = '.'; - this.levels = this.audioTracks = this.subtitleTracks = null; - } - onManifestLoaded(event, data) { - const { - contentSteering - } = data; - if (contentSteering === null) { - return; - } - this.pathwayId = contentSteering.pathwayId; - this.uri = contentSteering.uri; - if (this.started) { - this.startLoad(); - } - } - onManifestParsed(event, data) { - this.audioTracks = data.audioTracks; - this.subtitleTracks = data.subtitleTracks; - } - onError(event, data) { - const { - errorAction - } = data; - if ((errorAction == null ? void 0 : errorAction.action) === NetworkErrorAction.SendAlternateToPenaltyBox && errorAction.flags === ErrorActionFlags.MoveAllAlternatesMatchingHost) { - const levels = this.levels; - let pathwayPriority = this._pathwayPriority; - let errorPathway = this.pathwayId; - if (data.context) { - const { - groupId, - pathwayId, - type - } = data.context; - if (groupId && levels) { - errorPathway = this.getPathwayForGroupId(groupId, type, errorPathway); - } else if (pathwayId) { - errorPathway = pathwayId; - } - } - if (!(errorPathway in this.penalizedPathways)) { - this.penalizedPathways[errorPathway] = performance.now(); - } - if (!pathwayPriority && levels) { - // If PATHWAY-PRIORITY was not provided, list pathways for error handling - pathwayPriority = this.pathways(); - } - if (pathwayPriority && pathwayPriority.length > 1) { - this.updatePathwayPriority(pathwayPriority); - errorAction.resolved = this.pathwayId !== errorPathway; - } - if (!errorAction.resolved) { - this.warn(`Could not resolve ${data.details} ("${data.error.message}") with content-steering for Pathway: ${errorPathway} levels: ${levels ? levels.length : levels} priorities: ${JSON.stringify(pathwayPriority)} penalized: ${JSON.stringify(this.penalizedPathways)}`); - } - } - } - filterParsedLevels(levels) { - // Filter levels to only include those that are in the initial pathway - this.levels = levels; - let pathwayLevels = this.getLevelsForPathway(this.pathwayId); - if (pathwayLevels.length === 0) { - const pathwayId = levels[0].pathwayId; - this.log(`No levels found in Pathway ${this.pathwayId}. Setting initial Pathway to "${pathwayId}"`); - pathwayLevels = this.getLevelsForPathway(pathwayId); - this.pathwayId = pathwayId; - } - if (pathwayLevels.length !== levels.length) { - this.log(`Found ${pathwayLevels.length}/${levels.length} levels in Pathway "${this.pathwayId}"`); - } - return pathwayLevels; - } - getLevelsForPathway(pathwayId) { - if (this.levels === null) { - return []; - } - return this.levels.filter(level => pathwayId === level.pathwayId); - } - updatePathwayPriority(pathwayPriority) { - this._pathwayPriority = pathwayPriority; - let levels; - - // Evaluate if we should remove the pathway from the penalized list - const penalizedPathways = this.penalizedPathways; - const now = performance.now(); - Object.keys(penalizedPathways).forEach(pathwayId => { - if (now - penalizedPathways[pathwayId] > PATHWAY_PENALTY_DURATION_MS) { - delete penalizedPathways[pathwayId]; - } - }); - for (let i = 0; i < pathwayPriority.length; i++) { - const pathwayId = pathwayPriority[i]; - if (pathwayId in penalizedPathways) { - continue; - } - if (pathwayId === this.pathwayId) { - return; - } - const selectedIndex = this.hls.nextLoadLevel; - const selectedLevel = this.hls.levels[selectedIndex]; - levels = this.getLevelsForPathway(pathwayId); - if (levels.length > 0) { - this.log(`Setting Pathway to "${pathwayId}"`); - this.pathwayId = pathwayId; - reassignFragmentLevelIndexes(levels); - this.hls.trigger(Events.LEVELS_UPDATED, { - levels - }); - // Set LevelController's level to trigger LEVEL_SWITCHING which loads playlist if needed - const levelAfterChange = this.hls.levels[selectedIndex]; - if (selectedLevel && levelAfterChange && this.levels) { - if (levelAfterChange.attrs['STABLE-VARIANT-ID'] !== selectedLevel.attrs['STABLE-VARIANT-ID'] && levelAfterChange.bitrate !== selectedLevel.bitrate) { - this.log(`Unstable Pathways change from bitrate ${selectedLevel.bitrate} to ${levelAfterChange.bitrate}`); - } - this.hls.nextLoadLevel = selectedIndex; - } - break; - } - } - } - getPathwayForGroupId(groupId, type, defaultPathway) { - const levels = this.getLevelsForPathway(defaultPathway).concat(this.levels || []); - for (let i = 0; i < levels.length; i++) { - if (type === PlaylistContextType.AUDIO_TRACK && levels[i].hasAudioGroup(groupId) || type === PlaylistContextType.SUBTITLE_TRACK && levels[i].hasSubtitleGroup(groupId)) { - return levels[i].pathwayId; - } - } - return defaultPathway; - } - clonePathways(pathwayClones) { - const levels = this.levels; - if (!levels) { - return; - } - const audioGroupCloneMap = {}; - const subtitleGroupCloneMap = {}; - pathwayClones.forEach(pathwayClone => { - const { - ID: cloneId, - 'BASE-ID': baseId, - 'URI-REPLACEMENT': uriReplacement - } = pathwayClone; - if (levels.some(level => level.pathwayId === cloneId)) { - return; - } - const clonedVariants = this.getLevelsForPathway(baseId).map(baseLevel => { - const attributes = new AttrList(baseLevel.attrs); - attributes['PATHWAY-ID'] = cloneId; - const clonedAudioGroupId = attributes.AUDIO && `${attributes.AUDIO}_clone_${cloneId}`; - const clonedSubtitleGroupId = attributes.SUBTITLES && `${attributes.SUBTITLES}_clone_${cloneId}`; - if (clonedAudioGroupId) { - audioGroupCloneMap[attributes.AUDIO] = clonedAudioGroupId; - attributes.AUDIO = clonedAudioGroupId; - } - if (clonedSubtitleGroupId) { - subtitleGroupCloneMap[attributes.SUBTITLES] = clonedSubtitleGroupId; - attributes.SUBTITLES = clonedSubtitleGroupId; - } - const url = performUriReplacement(baseLevel.uri, attributes['STABLE-VARIANT-ID'], 'PER-VARIANT-URIS', uriReplacement); - const clonedLevel = new Level({ - attrs: attributes, - audioCodec: baseLevel.audioCodec, - bitrate: baseLevel.bitrate, - height: baseLevel.height, - name: baseLevel.name, - url, - videoCodec: baseLevel.videoCodec, - width: baseLevel.width - }); - if (baseLevel.audioGroups) { - for (let i = 1; i < baseLevel.audioGroups.length; i++) { - clonedLevel.addGroupId('audio', `${baseLevel.audioGroups[i]}_clone_${cloneId}`); - } - } - if (baseLevel.subtitleGroups) { - for (let i = 1; i < baseLevel.subtitleGroups.length; i++) { - clonedLevel.addGroupId('text', `${baseLevel.subtitleGroups[i]}_clone_${cloneId}`); - } - } - return clonedLevel; - }); - levels.push(...clonedVariants); - cloneRenditionGroups(this.audioTracks, audioGroupCloneMap, uriReplacement, cloneId); - cloneRenditionGroups(this.subtitleTracks, subtitleGroupCloneMap, uriReplacement, cloneId); - }); - } - loadSteeringManifest(uri) { - const config = this.hls.config; - const Loader = config.loader; - if (this.loader) { - this.loader.destroy(); - } - this.loader = new Loader(config); - let url; - try { - url = new self.URL(uri); - } catch (error) { - this.enabled = false; - this.log(`Failed to parse Steering Manifest URI: ${uri}`); - return; - } - if (url.protocol !== 'data:') { - const throughput = (this.hls.bandwidthEstimate || config.abrEwmaDefaultEstimate) | 0; - url.searchParams.set('_HLS_pathway', this.pathwayId); - url.searchParams.set('_HLS_throughput', '' + throughput); - } - const context = { - responseType: 'json', - url: url.href - }; - const loadPolicy = config.steeringManifestLoadPolicy.default; - const legacyRetryCompatibility = loadPolicy.errorRetry || loadPolicy.timeoutRetry || {}; - const loaderConfig = { - loadPolicy, - timeout: loadPolicy.maxLoadTimeMs, - maxRetry: legacyRetryCompatibility.maxNumRetry || 0, - retryDelay: legacyRetryCompatibility.retryDelayMs || 0, - maxRetryDelay: legacyRetryCompatibility.maxRetryDelayMs || 0 - }; - const callbacks = { - onSuccess: (response, stats, context, networkDetails) => { - this.log(`Loaded steering manifest: "${url}"`); - const steeringData = response.data; - if ((steeringData == null ? void 0 : steeringData.VERSION) !== 1) { - this.log(`Steering VERSION ${steeringData.VERSION} not supported!`); - return; - } - this.updated = performance.now(); - this.timeToLoad = steeringData.TTL; - const { - 'RELOAD-URI': reloadUri, - 'PATHWAY-CLONES': pathwayClones, - 'PATHWAY-PRIORITY': pathwayPriority - } = steeringData; - if (reloadUri) { - try { - this.uri = new self.URL(reloadUri, url).href; - } catch (error) { - this.enabled = false; - this.log(`Failed to parse Steering Manifest RELOAD-URI: ${reloadUri}`); - return; - } - } - this.scheduleRefresh(this.uri || context.url); - if (pathwayClones) { - this.clonePathways(pathwayClones); - } - const loadedSteeringData = { - steeringManifest: steeringData, - url: url.toString() - }; - this.hls.trigger(Events.STEERING_MANIFEST_LOADED, loadedSteeringData); - if (pathwayPriority) { - this.updatePathwayPriority(pathwayPriority); - } - }, - onError: (error, context, networkDetails, stats) => { - this.log(`Error loading steering manifest: ${error.code} ${error.text} (${context.url})`); - this.stopLoad(); - if (error.code === 410) { - this.enabled = false; - this.log(`Steering manifest ${context.url} no longer available`); - return; - } - let ttl = this.timeToLoad * 1000; - if (error.code === 429) { - const loader = this.loader; - if (typeof (loader == null ? void 0 : loader.getResponseHeader) === 'function') { - const retryAfter = loader.getResponseHeader('Retry-After'); - if (retryAfter) { - ttl = parseFloat(retryAfter) * 1000; - } - } - this.log(`Steering manifest ${context.url} rate limited`); - return; - } - this.scheduleRefresh(this.uri || context.url, ttl); - }, - onTimeout: (stats, context, networkDetails) => { - this.log(`Timeout loading steering manifest (${context.url})`); - this.scheduleRefresh(this.uri || context.url); - } - }; - this.log(`Requesting steering manifest: ${url}`); - this.loader.load(context, loaderConfig, callbacks); - } - scheduleRefresh(uri, ttlMs = this.timeToLoad * 1000) { - this.clearTimeout(); - this.reloadTimer = self.setTimeout(() => { - var _this$hls; - const media = (_this$hls = this.hls) == null ? void 0 : _this$hls.media; - if (media && !media.ended) { - this.loadSteeringManifest(uri); - return; - } - this.scheduleRefresh(uri, this.timeToLoad * 1000); - }, ttlMs); - } -} -function cloneRenditionGroups(tracks, groupCloneMap, uriReplacement, cloneId) { - if (!tracks) { - return; - } - Object.keys(groupCloneMap).forEach(audioGroupId => { - const clonedTracks = tracks.filter(track => track.groupId === audioGroupId).map(track => { - const clonedTrack = _extends({}, track); - clonedTrack.details = undefined; - clonedTrack.attrs = new AttrList(clonedTrack.attrs); - clonedTrack.url = clonedTrack.attrs.URI = performUriReplacement(track.url, track.attrs['STABLE-RENDITION-ID'], 'PER-RENDITION-URIS', uriReplacement); - clonedTrack.groupId = clonedTrack.attrs['GROUP-ID'] = groupCloneMap[audioGroupId]; - clonedTrack.attrs['PATHWAY-ID'] = cloneId; - return clonedTrack; - }); - tracks.push(...clonedTracks); - }); -} -function performUriReplacement(uri, stableId, perOptionKey, uriReplacement) { - const { - HOST: host, - PARAMS: params, - [perOptionKey]: perOptionUris - } = uriReplacement; - let perVariantUri; - if (stableId) { - perVariantUri = perOptionUris == null ? void 0 : perOptionUris[stableId]; - if (perVariantUri) { - uri = perVariantUri; - } - } - const url = new self.URL(uri); - if (host && !perVariantUri) { - url.host = host; - } - if (params) { - Object.keys(params).sort().forEach(key => { - if (key) { - url.searchParams.set(key, params[key]); - } - }); - } - return url.href; -} - -const ALIGNED_END_THRESHOLD_SECONDS = 0.02; // 0.1 // 0.2 - -let TimelineOccupancy = /*#__PURE__*/function (TimelineOccupancy) { - TimelineOccupancy[TimelineOccupancy["Point"] = 0] = "Point"; - TimelineOccupancy[TimelineOccupancy["Range"] = 1] = "Range"; - return TimelineOccupancy; -}({}); -function generateAssetIdentifier(interstitial, uri, assetListIndex) { - return `${interstitial.identifier}-${assetListIndex + 1}-${hash(uri)}`; -} -class InterstitialEvent { - constructor(dateRange, base) { - this.base = void 0; - this._duration = null; - this._timelineStart = null; - this.appendInPlaceDisabled = void 0; - this.appendInPlaceStarted = void 0; - this.dateRange = void 0; - this.hasPlayed = false; - this.cumulativeDuration = 0; - this.resumeOffset = NaN; - this.playoutLimit = NaN; - this.restrictions = { - skip: false, - jump: false - }; - this.snapOptions = { - out: false, - in: false - }; - this.assetList = []; - this.assetListLoader = void 0; - this.assetListResponse = null; - this.resumeAnchor = void 0; - this.error = void 0; - this.base = base; - this.dateRange = dateRange; - this.setDateRange(dateRange); - } - setDateRange(dateRange) { - this.dateRange = dateRange; - this.resumeOffset = dateRange.attr.optionalFloat('X-RESUME-OFFSET', this.resumeOffset); - this.playoutLimit = dateRange.attr.optionalFloat('X-PLAYOUT-LIMIT', this.playoutLimit); - this.restrictions = dateRange.attr.enumeratedStringList('X-RESTRICT', this.restrictions); - this.snapOptions = dateRange.attr.enumeratedStringList('X-SNAP', this.snapOptions); - } - reset() { - var _this$assetListLoader; - (_this$assetListLoader = this.assetListLoader) == null ? void 0 : _this$assetListLoader.destroy(); - this.assetListLoader = this.error = undefined; - } - isAssetPastPlayoutLimit(assetIndex) { - if (assetIndex >= this.assetList.length) { - return true; - } - const playoutLimit = this.playoutLimit; - if (assetIndex <= 0 || isNaN(playoutLimit)) { - return false; - } - const assetOffset = this.assetList[assetIndex].startOffset; - return assetOffset > playoutLimit; - } - findAssetIndex(asset) { - const index = this.assetList.indexOf(asset); - return index; - } - get identifier() { - return this.dateRange.id; - } - get startDate() { - return this.dateRange.startDate; - } - get startTime() { - // Primary media timeline start time - const startTime = this.dateRange.startTime; - if (this.snapOptions.out) { - const frag = this.dateRange.tagAnchor; - if (frag) { - return getSnapToFragmentTime(startTime, frag); - } - } - return startTime; - } - get startOffset() { - return this.cue.pre ? 0 : this.startTime; - } - get resumptionOffset() { - const resumeOffset = this.resumeOffset; - const offset = isFiniteNumber(resumeOffset) ? resumeOffset : this.duration; - return this.cumulativeDuration + offset; - } - get resumeTime() { - // Primary media timeline resumption time - const resumeTime = this.startOffset + this.resumptionOffset; - if (this.snapOptions.in) { - const frag = this.resumeAnchor; - if (frag) { - return getSnapToFragmentTime(resumeTime, frag); - } - } - return resumeTime; - } - get appendInPlace() { - if (this.appendInPlaceDisabled) { - return false; - } - if (!this.cue.once && !this.cue.pre && ( - // preroll starts at startPosition before startPosition is known (live) - this.startTime === 0 || this.snapOptions.out) && (isNaN(this.playoutLimit) && isNaN(this.resumeOffset) || this.resumeOffset && this.duration && Math.abs(this.resumeOffset - this.duration) < ALIGNED_END_THRESHOLD_SECONDS)) { - return true; - } - return false; - } - set appendInPlace(value) { - if (this.appendInPlaceStarted) { - return; - } - this.appendInPlaceDisabled = !value; - } - - // Extended timeline start time - get timelineStart() { - if (this._timelineStart !== null) { - return this._timelineStart; - } - return this.startTime; - } - set timelineStart(value) { - this._timelineStart = value; - } - get duration() { - const playoutLimit = this.playoutLimit; - let duration; - if (this._duration) { - duration = this._duration; - } else if (this.dateRange.duration) { - duration = this.dateRange.duration; - } else { - duration = this.dateRange.plannedDuration || 0; - } - if (!isNaN(playoutLimit) && playoutLimit < duration) { - duration = playoutLimit; - } - return duration; - } - set duration(value) { - this._duration = value; - } - get cue() { - return this.dateRange.cue; - } - get timelineOccupancy() { - if (this.dateRange.attr['X-TIMELINE-OCCUPIES'] === 'RANGE') { - return TimelineOccupancy.Range; - } - return TimelineOccupancy.Point; - } - get supplementsPrimary() { - return this.dateRange.attr['X-TIMELINE-STYLE'] === 'PRIMARY'; - } - get contentMayVary() { - return this.dateRange.attr['X-CONTENT-MAY-VARY'] !== 'NO'; - } - get assetUrl() { - return this.dateRange.attr['X-ASSET-URI']; - } - get assetListUrl() { - return this.dateRange.attr['X-ASSET-LIST']; - } - get baseUrl() { - return this.base.url; - } - toString() { - return eventToString(this); - } -} -function getSnapToFragmentTime(time, frag) { - return time - frag.start < frag.duration / 2 && !(Math.abs(time - frag.end) < ALIGNED_END_THRESHOLD_SECONDS) ? frag.start : frag.end; -} -function getInterstitialUrl(uri, sessionId, baseUrl) { - const url = new self.URL(uri, baseUrl); - if (url.protocol !== 'data:') { - url.searchParams.set('_HLS_primary_id', sessionId); - } - return url; -} -function eventToString(interstitial) { - return `["${interstitial.identifier}" ${interstitial.cue.pre ? '<pre>' : interstitial.cue.post ? '<post>' : ''}${interstitial.timelineStart.toFixed(2)}-${interstitial.resumeTime.toFixed(2)}]`; -} -function eventAssetToString(asset) { - const start = asset.timelineStart; - const duration = asset.duration || 0; - return `["${asset.identifier}" ${start.toFixed(2)}-${(start + duration).toFixed(2)}]`; -} - -const ABUTTING_THRESHOLD_SECONDS = 0.033; -class InterstitialsSchedule { - constructor(onScheduleUpdate) { - this.onScheduleUpdate = void 0; - this.eventMap = {}; - this.events = null; - this.items = null; - this.durations = { - primary: 0, - playout: 0, - integrated: 0 - }; - this.onScheduleUpdate = onScheduleUpdate; - } - destroy() { - this.reset(); - // @ts-ignore - this.onScheduleUpdate = null; - } - reset() { - this.eventMap = {}; - this.setDurations(0, 0, 0); - if (this.events) { - this.events.forEach(interstitial => interstitial.reset()); - } - this.events = this.items = null; - } - get duration() { - const items = this.items; - return items ? items[items.length - 1].end : 0; - } - get length() { - return this.items ? this.items.length : 0; - } - getEvent(identifier) { - return identifier ? this.eventMap[identifier] || null : null; - } - hasEvent(identifier) { - return identifier in this.eventMap; - } - findItemIndex(item, time) { - if (item.event) { - // Find Event Item - return this.findEventIndex(item.event.identifier); - } - // Find Primary Item - let index = -1; - if (item.nextEvent) { - index = this.findEventIndex(item.nextEvent.identifier) - 1; - } else if (item.previousEvent) { - index = this.findEventIndex(item.previousEvent.identifier) + 1; - } - const items = this.items; - if (items) { - if (!items[index]) { - if (time === undefined) { - time = item.start; - } - index = this.findItemIndexAtTime(time); - } - // Only return index of a Primary Item - while (index >= 0 && (_items$index = items[index]) != null && _items$index.event) { - var _items$index; - index--; - } - } - return index; - } - findItemIndexAtTime(timelinePos, timelineType) { - const items = this.items; - if (items) { - for (let i = 0; i < items.length; i++) { - let timeRange = items[i]; - if (timelineType && timelineType !== 'primary') { - timeRange = timeRange[timelineType]; - } - if (timelinePos === timeRange.start || timelinePos > timeRange.start && timelinePos < timeRange.end) { - return i; - } - } - } - return -1; - } - findJumpRestrictedIndex(startIndex, endIndex) { - const items = this.items; - if (items) { - for (let i = startIndex; i <= endIndex; i++) { - if (!items[i]) { - break; - } - const event = items[i].event; - if (event != null && event.restrictions.jump && !event.appendInPlace) { - return i; - } - } - } - return -1; - } - findEventIndex(identifier) { - const items = this.items; - if (items) { - for (let i = items.length; i--;) { - var _items$i$event; - if (((_items$i$event = items[i].event) == null ? void 0 : _items$i$event.identifier) === identifier) { - return i; - } - } - } - return -1; - } - findAssetIndex(event, timelinePos) { - const assetList = event.assetList; - const length = assetList.length; - if (length > 1) { - for (let i = 0; i < length; i++) { - const asset = assetList[i]; - if (!asset.error) { - const timelineStart = asset.timelineStart; - if (timelinePos === timelineStart || timelinePos > timelineStart && timelinePos < timelineStart + (asset.duration || 0)) { - return i; - } - } - } - } - return 0; - } - get assetIdAtEnd() { - var _this$items, _this$items2; - const interstitialAtEnd = (_this$items = this.items) == null ? void 0 : (_this$items2 = _this$items[this.length - 1]) == null ? void 0 : _this$items2.event; - if (interstitialAtEnd) { - const assetList = interstitialAtEnd.assetList; - const assetAtEnd = assetList[assetList.length - 1]; - if (assetAtEnd) { - return assetAtEnd.identifier; - } - } - return null; - } - parseInterstitialDateRanges(mediaSelection) { - const details = mediaSelection.main.details; - const { - dateRanges - } = details; - const previousInterstitialEvents = this.events; - const interstitialEvents = this.parseDateRanges(dateRanges, { - url: details.url - }); - const ids = Object.keys(dateRanges); - const removedInterstitials = previousInterstitialEvents ? previousInterstitialEvents.filter(event => !ids.includes(event.identifier)) : []; - if (interstitialEvents.length) { - // pre-rolls, post-rolls, and events with the same start time are played in playlist tag order - // all other events are ordered by start time - interstitialEvents.sort((a, b) => { - const aPre = a.cue.pre; - const aPost = a.cue.post; - const bPre = b.cue.pre; - const bPost = b.cue.post; - if (aPre && !bPre) { - return -1; - } - if (bPre && !aPre) { - return 1; - } - if (aPost && !bPost) { - return 1; - } - if (bPost && !aPost) { - return -1; - } - if (!aPre && !bPre && !aPost && !bPost) { - const startA = a.startTime; - const startB = b.startTime; - if (startA !== startB) { - return startA - startB; - } - } - return a.dateRange.tagOrder - b.dateRange.tagOrder; - }); - } - this.events = interstitialEvents; - - // Clear removed DateRanges from buffered list (kills playback of active Interstitials) - removedInterstitials.forEach(interstitial => { - this.removeEvent(interstitial); - }); - this.updateSchedule(mediaSelection, removedInterstitials); - } - updateSchedule(mediaSelection, removedInterstitials = []) { - const events = this.events || []; - if (events.length || removedInterstitials.length || this.length < 2) { - const currentItems = this.items; - const updatedItems = this.parseSchedule(events, mediaSelection); - const updated = removedInterstitials.length || (currentItems == null ? void 0 : currentItems.length) !== updatedItems.length || updatedItems.some((item, i) => { - return Math.abs(item.playout.start - currentItems[i].playout.start) > 0.005 || Math.abs(item.playout.end - currentItems[i].playout.end) > 0.005; - }); - if (updated) { - this.items = updatedItems; - // call interstitials-controller onScheduleUpdated() - this.onScheduleUpdate(removedInterstitials, currentItems); - } - } - } - parseDateRanges(dateRanges, baseData) { - const interstitialEvents = []; - const ids = Object.keys(dateRanges); - for (let i = 0; i < ids.length; i++) { - const id = ids[i]; - const dateRange = dateRanges[id]; - if (dateRange.isInterstitial) { - let interstitial = this.eventMap[id]; - if (interstitial) { - // Update InterstitialEvent already parsed and mapped - // This retains already loaded duration and loaded asset list info - interstitial.setDateRange(dateRange); - } else { - interstitial = new InterstitialEvent(dateRange, baseData); - this.eventMap[id] = interstitial; - } - interstitialEvents.push(interstitial); - } - } - return interstitialEvents; - } - parseSchedule(interstitialEvents, mediaSelection) { - const schedule = []; - const details = mediaSelection.main.details; - const primaryDuration = details.live ? Infinity : details.edge; - let playoutDuration = 0; - - // Filter events that have errored from the schedule (Primary fallback) - interstitialEvents = interstitialEvents.filter(event => !event.error && !(event.cue.once && event.hasPlayed)); - if (interstitialEvents.length) { - // Update Schedule - this.resolveOffsets(interstitialEvents, mediaSelection); - - // Populate Schedule with Interstitial Event and Primary Segment Items - let primaryPosition = 0; - let integratedTime = 0; - interstitialEvents.forEach((interstitial, i) => { - const preroll = interstitial.cue.pre; - const postroll = interstitial.cue.post; - const previousEvent = interstitialEvents[i - 1] || null; - const appendInPlace = interstitial.appendInPlace; - const eventStart = postroll ? primaryDuration : interstitial.startOffset; - const interstitialDuration = interstitial.duration; - const timelineDuration = interstitial.timelineOccupancy === TimelineOccupancy.Range ? interstitialDuration : 0; - const resumptionOffset = interstitial.resumptionOffset; - const inSameStartTimeSequence = (previousEvent == null ? void 0 : previousEvent.startTime) === eventStart; - const start = eventStart + interstitial.cumulativeDuration; - let end = appendInPlace ? start + interstitialDuration : eventStart + resumptionOffset; - if (preroll || !postroll && eventStart <= 0) { - // preroll or in-progress midroll - const integratedStart = integratedTime; - integratedTime += timelineDuration; - interstitial.timelineStart = start; - const playoutStart = playoutDuration; - playoutDuration += interstitialDuration; - schedule.push({ - event: interstitial, - start, - end, - playout: { - start: playoutStart, - end: playoutDuration - }, - integrated: { - start: integratedStart, - end: integratedTime - } - }); - } else if (eventStart <= primaryDuration) { - if (!inSameStartTimeSequence) { - const segmentDuration = eventStart - primaryPosition; - // Do not schedule a primary segment if interstitials are abutting by less than ABUTTING_THRESHOLD_SECONDS - if (segmentDuration > ABUTTING_THRESHOLD_SECONDS) { - // primary segment - const timelineStart = primaryPosition; - const _integratedStart = integratedTime; - integratedTime += segmentDuration; - const _playoutStart = playoutDuration; - playoutDuration += segmentDuration; - const primarySegment = { - previousEvent: interstitialEvents[i - 1] || null, - nextEvent: interstitial, - start: timelineStart, - end: timelineStart + segmentDuration, - playout: { - start: _playoutStart, - end: playoutDuration - }, - integrated: { - start: _integratedStart, - end: integratedTime - } - }; - schedule.push(primarySegment); - } else if (segmentDuration > 0 && previousEvent) { - // Add previous event `resumeTime` (based on duration or resumeOffset) so that it ends aligned with this one - previousEvent.cumulativeDuration += segmentDuration; - schedule[schedule.length - 1].end = eventStart; - } - } - // midroll / postroll - if (postroll) { - end = start; - } - interstitial.timelineStart = start; - const integratedStart = integratedTime; - integratedTime += timelineDuration; - const playoutStart = playoutDuration; - playoutDuration += interstitialDuration; - schedule.push({ - event: interstitial, - start, - end, - playout: { - start: playoutStart, - end: playoutDuration - }, - integrated: { - start: integratedStart, - end: integratedTime - } - }); - } else { - // Interstitial starts after end of primary VOD - not included in schedule - return; - } - const resumeTime = interstitial.resumeTime; - if (postroll || resumeTime > primaryDuration) { - primaryPosition = primaryDuration; - } else { - primaryPosition = resumeTime; - } - }); - if (primaryPosition < primaryDuration) { - var _schedule; - // last primary segment - const timelineStart = primaryPosition; - const integratedStart = integratedTime; - const segmentDuration = primaryDuration - primaryPosition; - integratedTime += segmentDuration; - const playoutStart = playoutDuration; - playoutDuration += segmentDuration; - schedule.push({ - previousEvent: ((_schedule = schedule[schedule.length - 1]) == null ? void 0 : _schedule.event) || null, - nextEvent: null, - start: primaryPosition, - end: timelineStart + segmentDuration, - playout: { - start: playoutStart, - end: playoutDuration - }, - integrated: { - start: integratedStart, - end: integratedTime - } - }); - } - this.setDurations(primaryDuration, playoutDuration, integratedTime); - } else { - // no interstials - schedule is one primary segment - const start = 0; - schedule.push({ - previousEvent: null, - nextEvent: null, - start, - end: primaryDuration, - playout: { - start, - end: primaryDuration - }, - integrated: { - start, - end: primaryDuration - } - }); - this.setDurations(primaryDuration, primaryDuration, primaryDuration); - } - return schedule; - } - setDurations(primary, playout, integrated) { - this.durations = { - primary, - playout, - integrated - }; - } - resolveOffsets(interstitialEvents, mediaSelection) { - const details = mediaSelection.main.details; - const primaryDuration = details.live ? Infinity : details.edge; - - // First resolve cumulative resumption offsets for Interstitials that start at the same DateTime - let cumulativeDuration = 0; - let lastScheduledStart = -1; - interstitialEvents.forEach((interstitial, i) => { - const preroll = interstitial.cue.pre; - const postroll = interstitial.cue.post; - const eventStart = preroll ? 0 : postroll ? primaryDuration : interstitial.startTime; - this.updateAssetDurations(interstitial); - - // X-RESUME-OFFSET values of interstitials scheduled at the same time are cumulative - const inSameStartTimeSequence = lastScheduledStart === eventStart; - if (inSameStartTimeSequence) { - interstitial.cumulativeDuration = cumulativeDuration; - } else { - cumulativeDuration = 0; - lastScheduledStart = eventStart; - } - if (!postroll && interstitial.snapOptions.in) { - // FIXME: Include audio playlist in snapping - interstitial.resumeAnchor = findFragmentByPTS(null, details.fragments, interstitial.startOffset + interstitial.resumptionOffset, 0, 0) || undefined; - } - // Check if primary fragments align with resumption offset and disable appendInPlace if they do not - if (interstitial.appendInPlace && !interstitial.appendInPlaceStarted) { - const alignedSegmentStart = this.primaryCanResumeInPlaceAt(interstitial, mediaSelection); - if (!alignedSegmentStart) { - interstitial.appendInPlace = false; - } - } - if (!interstitial.appendInPlace) { - // abutting Interstitials must use the same MediaSource strategy, this applies to all whether or not they are back to back: - for (let j = i - 1; i--;) { - const timeBetween = interstitialEvents[j + 1].startTime - interstitialEvents[j].resumeTime; - if (timeBetween < ABUTTING_THRESHOLD_SECONDS) { - interstitialEvents[j].appendInPlace = false; - } - } - } - // Update cumulativeDuration for next abutting interstitial with the same start date - const resumeOffset = isFiniteNumber(interstitial.resumeOffset) ? interstitial.resumeOffset : interstitial.duration; - cumulativeDuration += resumeOffset; - }); - } - primaryCanResumeInPlaceAt(interstitial, mediaSelection) { - const resumeTime = interstitial.resumeTime; - const resumesInPlaceAt = interstitial.startTime + interstitial.resumptionOffset; - if (Math.abs(resumeTime - resumesInPlaceAt) > ALIGNED_END_THRESHOLD_SECONDS) { - logger.log(`Interstitial resumption ${resumeTime} not aligned with estimated timeline end ${resumesInPlaceAt}`); - return false; - } - if (!mediaSelection) { - logger.log(`Interstitial resumption ${resumeTime} can not be aligned with media (none selected)`); - return false; - } - return !Object.keys(mediaSelection).some(playlistType => { - const details = mediaSelection[playlistType].details; - const playlistEnd = details.edge; - if (resumeTime > playlistEnd) { - logger.log(`Interstitial resumption ${resumeTime} past ${playlistType} playlist end ${playlistEnd}`); - return true; - } - const startFragment = findFragmentByPTS(null, details.fragments, resumeTime); - if (!startFragment) { - logger.log(`Interstitial resumption ${resumeTime} does not overlap with any fragments in ${playlistType} playlist`); - return true; - } - const alignedWithSegment = Math.abs(startFragment.start - resumeTime) < ALIGNED_END_THRESHOLD_SECONDS || Math.abs(startFragment.end - resumeTime) < ALIGNED_END_THRESHOLD_SECONDS; - if (!alignedWithSegment) { - logger.log(`Interstitial resumption ${resumeTime} does not overlap with fragment in ${playlistType} playlist (${startFragment.start}-${startFragment.end})`); - return true; - } - return false; - }); - } - updateAssetDurations(interstitial) { - const eventStart = interstitial.timelineStart; - let sumDuration = 0; - let hasUnknownDuration = false; - let hasErrors = false; - interstitial.assetList.forEach((asset, i) => { - const timelineStart = eventStart + sumDuration; - asset.startOffset = sumDuration; - asset.timelineStart = timelineStart; - hasUnknownDuration || (hasUnknownDuration = asset.duration === null); - hasErrors || (hasErrors = !!asset.error); - const duration = asset.error ? 0 : asset.duration || 0; - sumDuration += duration; - }); - // Use the sum of known durations when it is greater than the stated duration - if (hasUnknownDuration && !hasErrors) { - interstitial.duration = Math.max(sumDuration, interstitial.duration); - } else { - interstitial.duration = sumDuration; - } - } - removeEvent(interstitial) { - interstitial.reset(); - delete this.eventMap[interstitial.identifier]; - } -} -function segmentToString(segment) { - return `[${segment.event ? '"' + segment.event.identifier + '"' : 'primary'}: ${segment.start.toFixed(2)}-${segment.end.toFixed(2)}]`; -} - -class AssetListLoader { - constructor(hls) { - this.hls = void 0; - this.hls = hls; - } - destroy() { - // @ts-ignore - this.hls = null; - } - loadAssetList(interstitial, liveStartPosition) { - const assetListUrl = interstitial.assetListUrl; - let url; - try { - url = getInterstitialUrl(assetListUrl, this.hls.sessionId, interstitial.baseUrl); - } catch (error) { - const errorData = this.assignAssetListError(interstitial, ErrorDetails.ASSET_LIST_LOAD_ERROR, error, assetListUrl); - this.hls.trigger(Events.ERROR, errorData); - return; - } - if (liveStartPosition && !(interstitial.cue.pre || interstitial.cue.post) && url.protocol !== 'data:') { - const startOffset = liveStartPosition - interstitial.startTime; - if (startOffset > 0) { - url.searchParams.set('_HLS_start_offset', '' + Math.round(startOffset * 1000) / 1000); - } - } - const config = this.hls.config; - const Loader = config.loader; - const loader = new Loader(config); - const context = { - responseType: 'json', - url: url.href - }; - const loadPolicy = config.interstitialAssetListLoadPolicy.default; - const loaderConfig = { - loadPolicy, - timeout: loadPolicy.maxLoadTimeMs, - maxRetry: 0, - retryDelay: 0, - maxRetryDelay: 0 - }; - const callbacks = { - onSuccess: (response, stats, context, networkDetails) => { - const assetListResponse = response.data; - const assets = assetListResponse == null ? void 0 : assetListResponse.ASSETS; - if (!Array.isArray(assets)) { - const errorData = this.assignAssetListError(interstitial, ErrorDetails.ASSET_LIST_PARSING_ERROR, new Error(`Invalid interstitial asset list`), context.url, stats, networkDetails); - this.hls.trigger(Events.ERROR, errorData); - return; - } - interstitial.assetListResponse = assetListResponse; - this.hls.trigger(Events.ASSET_LIST_LOADED, { - event: interstitial, - assetListResponse, - networkDetails - }); - }, - onError: (error, context, networkDetails, stats) => { - const errorData = this.assignAssetListError(interstitial, ErrorDetails.ASSET_LIST_LOAD_ERROR, new Error(`Error loading X-ASSET-LIST: HTTP status ${error.code} ${error.text} (${context.url})`), context.url, stats, networkDetails); - this.hls.trigger(Events.ERROR, errorData); - }, - onTimeout: (stats, context, networkDetails) => { - const errorData = this.assignAssetListError(interstitial, ErrorDetails.ASSET_LIST_LOAD_TIMEOUT, new Error(`Timeout loading X-ASSET-LIST (${context.url})`), context.url, stats, networkDetails); - this.hls.trigger(Events.ERROR, errorData); - } - }; - loader.load(context, loaderConfig, callbacks); - this.hls.trigger(Events.ASSET_LIST_LOADING, { - event: interstitial - }); - return loader; - } - assignAssetListError(interstitial, details, error, url, stats, networkDetails) { - interstitial.error = error; - return { - type: ErrorTypes.NETWORK_ERROR, - details, - fatal: false, - interstitial, - url, - error, - networkDetails, - stats - }; - } -} - -class HlsAssetPlayer { - constructor(HlsPlayerClass, userConfig, _interstitial, assetItem) { - this.hls = void 0; - this.interstitial = void 0; - this.assetItem = void 0; - this.tracks = null; - this.hasDetails = false; - this.mediaAttached = null; - this.playoutOffset = 0; - this.checkPlayout = () => { - const interstitial = this.interstitial; - const playoutLimit = interstitial.playoutLimit; - if (this.playoutOffset + this.currentTime >= playoutLimit) { - this.hls.trigger(Events.PLAYOUT_LIMIT_REACHED, {}); - } - }; - const hls = this.hls = new HlsPlayerClass(userConfig); - this.interstitial = _interstitial; - this.assetItem = assetItem; - let uri = assetItem.uri; - try { - uri = getInterstitialUrl(uri, hls.sessionId).href; - } catch (error) { - // Ignore error parsing ASSET_URI or adding _HLS_primary_id to it. The - // issue should surface as an INTERSTITIAL_ASSET_ERROR loading the asset. - } - hls.loadSource(uri); - const detailsLoaded = () => { - this.hasDetails = true; - }; - hls.once(Events.LEVEL_LOADED, detailsLoaded); - hls.once(Events.AUDIO_TRACK_LOADED, detailsLoaded); - hls.once(Events.SUBTITLE_TRACK_LOADED, detailsLoaded); - hls.on(Events.MEDIA_ATTACHING, (name, { - media - }) => { - this.removeMediaListeners(); - this.mediaAttached = media; - const event = this.interstitial; - if (event.playoutLimit) { - var _event$assetList$even; - this.playoutOffset = ((_event$assetList$even = event.assetList[event.assetList.indexOf(assetItem)]) == null ? void 0 : _event$assetList$even.startOffset) || 0; - media.addEventListener('timeupdate', this.checkPlayout); - } - }); - } - get destroyed() { - var _this$hls; - return !((_this$hls = this.hls) != null && _this$hls.userConfig); - } - get assetId() { - return this.assetItem.identifier; - } - get interstitialId() { - return this.assetItem.parentIdentifier; - } - get media() { - return this.hls.media; - } - get bufferedEnd() { - const media = this.media || this.mediaAttached; - if (!media) { - return 0; - } - const bufferInfo = BufferHelper.bufferInfo(media, media.currentTime, 0.001); - return this.getAssetTime(bufferInfo.end); - } - get currentTime() { - const media = this.media || this.mediaAttached; - if (!media) { - return 0; - } - return this.getAssetTime(media.currentTime); - } - get duration() { - var _this$assetItem; - const duration = (_this$assetItem = this.assetItem) == null ? void 0 : _this$assetItem.duration; - if (!duration) { - return 0; - } - return duration; - } - get remaining() { - const duration = this.duration; - if (!duration) { - return 0; - } - return Math.max(0, duration - this.currentTime); - } - get timelineOffset() { - return this.hls.config.timelineOffset || 0; - } - set timelineOffset(value) { - const timelineOffset = this.timelineOffset; - if (value !== timelineOffset) { - const diff = value - timelineOffset; - if (Math.abs(diff) > 1 / 90000) { - if (this.hasDetails) { - throw new Error(`Cannot set timelineOffset after playlists are loaded`); - } - this.hls.config.timelineOffset = value; - } - } - } - getAssetTime(time) { - const timelineOffset = this.timelineOffset; - const duration = this.duration; - return Math.min(Math.max(0, time - timelineOffset), duration); - } - removeMediaListeners() { - const media = this.mediaAttached; - if (media) { - media.removeEventListener('timeupdate', this.checkPlayout); - } - } - destroy() { - this.removeMediaListeners(); - this.hls.destroy(); - // @ts-ignore - this.hls = this.interstitial = null; - // @ts-ignore - this.tracks = this.mediaAttached = this.checkPlayout = null; - } - attachMedia(data) { - this.hls.attachMedia(data); - } - detachMedia() { - this.removeMediaListeners(); - this.hls.detachMedia(); - } - resumeBuffering() { - this.hls.resumeBuffering(); - } - pauseBuffering() { - this.hls.pauseBuffering(); - } - transferMedia() { - return this.hls.transferMedia(); - } - on(event, listener, context) { - this.hls.on(event, listener); - } - once(event, listener, context) { - this.hls.once(event, listener); - } - off(event, listener, context) { - this.hls.off(event, listener); - } - toString() { - return `HlsAssetPlayer: ${eventAssetToString(this.assetItem)} ${this.hls.sessionId} ${this.interstitial.appendInPlace ? 'append-in-place' : ''}`; - } -} - -class InterstitialsController extends Logger { - constructor(hls, HlsPlayerClass) { - super('interstitials', hls.logger); - this.HlsPlayerClass = void 0; - this.hls = void 0; - this.assetListLoader = void 0; - // Last updated LevelDetails - this.mediaSelection = null; - this.altSelection = null; - // Media and MediaSource/SourceBuffers - this.media = null; - this.detachedData = null; - this.requiredTracks = null; - // Public Interface for Interstitial playback state and control - this.manager = null; - // Interstitial Asset Players - this.playerQueue = []; - // Timeline position tracking - this.bufferedPos = -1; - this.timelinePos = -1; - // Schedule - this.schedule = void 0; - // Schedule playback and buffering state - this.playingItem = null; - this.bufferingItem = null; - this.waitingItem = null; - this.playingAsset = null; - this.bufferingAsset = null; - this.shouldPlay = false; - this.onPlay = () => { - this.shouldPlay = true; - }; - this.onSeeking = () => { - const currentTime = this.currentTime; - if (currentTime === undefined || this.playbackDisabled) { - return; - } - const diff = currentTime - this.timelinePos; - const roundingError = Math.abs(diff) < 1 / 705600000; // one flick - if (roundingError) { - return; - } - const backwardSeek = diff <= -0.01; - this.timelinePos = currentTime; - this.bufferedPos = currentTime; - this.checkBuffer(); - - // Check if seeking out of an item - const playingItem = this.playingItem; - if (!playingItem) { - return; - } - if (backwardSeek && currentTime < playingItem.start || currentTime >= playingItem.end) { - var _this$media; - const scheduleIndex = this.schedule.findItemIndexAtTime(this.timelinePos); - if (!this.isInterstitial(playingItem) && (_this$media = this.media) != null && _this$media.paused) { - this.shouldPlay = false; - } - if (!backwardSeek) { - // check if an Interstitial between the current item and target item has an X-RESTRICT JUMP restriction - const playingIndex = this.findItemIndex(playingItem); - if (scheduleIndex > playingIndex) { - const jumpIndex = this.schedule.findJumpRestrictedIndex(playingIndex + 1, scheduleIndex); - if (jumpIndex > playingIndex) { - this.setSchedulePosition(jumpIndex); - return; - } - } - } - this.setSchedulePosition(scheduleIndex); - return; - } - // Check if seeking out of an asset (assumes same item following above check) - const playingAsset = this.playingAsset; - if (!playingAsset) { - // restart Interstitial at end - if (this.playingLastItem && this.isInterstitial(playingItem)) { - const restartAsset = playingItem.event.assetList[0]; - if (restartAsset) { - this.playingItem = null; - this.setScheduleToAssetAtTime(currentTime, restartAsset); - } - } - return; - } - const start = playingAsset.timelineStart; - const duration = playingAsset.duration || 0; - if (backwardSeek && currentTime < start || currentTime >= start + duration) { - this.setScheduleToAssetAtTime(currentTime, playingAsset); - } - }; - this.onTimeupdate = () => { - const currentTime = this.currentTime; - if (currentTime === undefined || this.playbackDisabled) { - return; - } - - // Only allow timeupdate to advance primary position, seeking is used for jumping back - // this prevents primaryPos from being reset to 0 after re-attach - if (currentTime > this.timelinePos) { - if (currentTime > this.bufferedPos) { - this.checkBuffer(); - } - this.timelinePos = currentTime; - } else { - return; - } - - // Check if playback has entered the next item - const playingItem = this.playingItem; - if (!playingItem || this.playingLastItem) { - return; - } - if (currentTime >= playingItem.end) { - this.timelinePos = playingItem.end; - const playingIndex = this.findItemIndex(playingItem); - this.setSchedulePosition(playingIndex + 1); - } - // Check if playback has entered the next asset - const playingAsset = this.playingAsset; - if (!playingAsset) { - return; - } - const end = playingAsset.timelineStart + (playingAsset.duration || 0); - if (currentTime >= end) { - this.setScheduleToAssetAtTime(currentTime, playingAsset); - } - }; - // Schedule update callback - this.onScheduleUpdate = (removedInterstitials, previousItems) => { - const schedule = this.schedule; - const playingItem = this.playingItem; - const interstitialEvents = schedule.events || []; - const scheduleItems = schedule.items || []; - const durations = schedule.durations; - const removedIds = removedInterstitials.map(interstitial => interstitial.identifier); - const interstitialsUpdated = !!(interstitialEvents.length || removedIds.length); - if (interstitialsUpdated) { - if (this.hls.config.interstitialAppendInPlace === false) { - interstitialEvents.forEach(event => event.appendInPlace = false); - } - this.log(`INTERSTITIALS_UPDATED (${interstitialEvents.length}): ${interstitialEvents} -Schedule: ${scheduleItems.map(seg => segmentToString(seg))}`); - } - if (removedIds.length) { - this.log(`Removed events ${removedIds}`); - } - if (this.isInterstitial(playingItem) && removedIds.includes(playingItem.event.identifier)) { - this.warn(`Interstitial "${playingItem.event.identifier}" removed while playing`); - } - this.playerQueue.forEach(player => { - if (player.interstitial.appendInPlace) { - const timelineStart = player.assetItem.timelineStart; - const diff = player.timelineOffset - timelineStart; - if (diff) { - try { - player.timelineOffset = timelineStart; - } catch (e) { - if (Math.abs(diff) > ALIGNED_END_THRESHOLD_SECONDS) { - this.warn(`${e} ("${player.assetId}" ${player.timelineOffset}->${timelineStart})`); - } - } - } - } - }); - - // Update schedule item references - // Do not change Interstitial playingItem - used for INTERSTITIAL_ASSET_ENDED and INTERSTITIAL_ENDED - if (playingItem && !playingItem.event) { - this.playingItem = this.updateItem(playingItem, this.timelinePos); - } - // Do not change Interstitial bufferingItem - used for transfering media element or source - const bufferingItem = this.bufferingItem; - if (bufferingItem) { - if (!bufferingItem.event) { - this.bufferingItem = this.updateItem(bufferingItem, this.bufferedPos); - } else if (!this.updateItem(bufferingItem)) { - // Interstitial removed from schedule (Live -> VOD or other scenario where Start Date is outside the range of VOD Playlist) - this.bufferingItem = null; - this.clearInterstitial(bufferingItem.event); - } - } - // Clear waitingItem if it has been removed from the schedule - this.waitingItem = this.updateItem(this.waitingItem); - removedInterstitials.forEach(interstitial => { - interstitial.assetList.forEach(asset => { - this.clearAssetPlayer(asset.identifier); - }); - }); - if (interstitialsUpdated || previousItems) { - this.hls.trigger(Events.INTERSTITIALS_UPDATED, { - events: interstitialEvents.slice(0), - schedule: scheduleItems.slice(0), - durations, - removedIds - }); - - // Check is buffered to new Interstitial event boundary - // (Live update publishes Interstitial with new segment) - this.checkBuffer(); - } - }; - this.hls = hls; - this.HlsPlayerClass = HlsPlayerClass; - this.assetListLoader = new AssetListLoader(hls); - this.schedule = new InterstitialsSchedule(this.onScheduleUpdate); - this.registerListeners(); - } - registerListeners() { - const hls = this.hls; - hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); - hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this); - hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this); - hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); - hls.on(Events.LEVEL_UPDATED, this.onLevelUpdated, this); - hls.on(Events.AUDIO_TRACK_SWITCHING, this.onAudioTrackSwitching, this); - hls.on(Events.AUDIO_TRACK_UPDATED, this.onAudioTrackUpdated, this); - hls.on(Events.SUBTITLE_TRACK_SWITCH, this.onSubtitleTrackSwitch, this); - hls.on(Events.SUBTITLE_TRACK_UPDATED, this.onSubtitleTrackUpdated, this); - hls.on(Events.ASSET_LIST_LOADED, this.onAssetListLoaded, this); - hls.on(Events.BUFFER_APPENDED, this.onBufferAppended, this); - hls.on(Events.BUFFER_FLUSHED, this.onBufferFlushed, this); - hls.on(Events.BUFFERED_TO_END, this.onBufferedToEnd, this); - hls.on(Events.MEDIA_ENDED, this.onMediaEnded, this); - hls.on(Events.ERROR, this.onError, this); - hls.on(Events.DESTROYING, this.onDestroying, this); - } - unregisterListeners() { - const hls = this.hls; - if (!hls) { - return; - } - hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); - hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this); - hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this); - hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); - hls.off(Events.LEVEL_UPDATED, this.onLevelUpdated, this); - hls.off(Events.AUDIO_TRACK_SWITCHING, this.onAudioTrackSwitching, this); - hls.off(Events.AUDIO_TRACK_UPDATED, this.onAudioTrackUpdated, this); - hls.off(Events.SUBTITLE_TRACK_SWITCH, this.onSubtitleTrackSwitch, this); - hls.off(Events.SUBTITLE_TRACK_UPDATED, this.onSubtitleTrackUpdated, this); - hls.off(Events.ASSET_LIST_LOADED, this.onAssetListLoaded, this); - hls.off(Events.BUFFER_CODECS, this.onBufferCodecs, this); - hls.off(Events.BUFFER_APPENDED, this.onBufferAppended, this); - hls.off(Events.BUFFER_FLUSHED, this.onBufferFlushed, this); - hls.off(Events.BUFFERED_TO_END, this.onBufferedToEnd, this); - hls.off(Events.MEDIA_ENDED, this.onMediaEnded, this); - hls.off(Events.ERROR, this.onError, this); - hls.off(Events.DESTROYING, this.onDestroying, this); - } - startLoad() { - // TODO: startLoad - check for waitingItem and retry by resetting schedule - this.resumeBuffering(); - } - stopLoad() { - // TODO: stopLoad - stop all scheule.events[].assetListLoader?.abort() then delete the loaders - this.pauseBuffering(); - } - resumeBuffering() { - this.playerQueue.forEach(player => player.resumeBuffering()); - } - pauseBuffering() { - this.playerQueue.forEach(player => player.pauseBuffering()); - } - destroy() { - this.unregisterListeners(); - this.stopLoad(); - if (this.assetListLoader) { - this.assetListLoader.destroy(); - } - this.emptyPlayerQueue(); - this.clearScheduleState(); - if (this.schedule) { - this.schedule.destroy(); - } - this.media = this.detachedData = this.mediaSelection = this.requiredTracks = this.altSelection = this.manager = null; - // @ts-ignore - this.hls = this.HlsPlayerClass = this.schedule = this.log = null; - // @ts-ignore - this.assetListLoader = null; - // @ts-ignore - this.onPlay = this.onSeeking = this.onTimeupdate = null; - // @ts-ignore - this.onScheduleUpdate = null; - } - onDestroying() { - const media = this.primaryMedia; - if (media) { - this.removeMediaListeners(media); - } - } - removeMediaListeners(media) { - media.removeEventListener('play', this.onPlay); - media.removeEventListener('seeking', this.onSeeking); - media.removeEventListener('timeupdate', this.onTimeupdate); - } - onMediaAttaching(event, data) { - const media = this.media = data.media; - this.removeMediaListeners(media); - media.addEventListener('seeking', this.onSeeking); - media.addEventListener('timeupdate', this.onTimeupdate); - media.addEventListener('play', this.onPlay); - } - onMediaAttached(event, data) { - const playingItem = this.playingItem; - const detachedMedia = this.detachedData; - this.detachedData = null; - if (playingItem === null) { - this.checkStart(); - } else if (!detachedMedia) { - // Resume schedule after detached externally - this.clearScheduleState(); - const playingIndex = this.findItemIndex(playingItem); - this.setSchedulePosition(playingIndex); - } - } - clearScheduleState() { - this.playingItem = this.bufferingItem = this.waitingItem = this.playingAsset = this.bufferingAsset = null; - } - onMediaDetaching(event, data) { - const transferringMedia = !!data.transferMedia; - const media = this.media; - this.media = null; - if (transferringMedia) { - return; - } - if (media) { - this.removeMediaListeners(media); - } - // If detachMedia is called while in an Interstitial, detach the asset player as well and reset the schedule position - if (this.detachedData) { - const player = this.getBufferingPlayer(); - if (player) { - this.playingAsset = null; - this.bufferingAsset = null; - this.bufferingItem = null; - this.waitingItem = null; - this.detachedData = null; - player.detachMedia(); - } - this.shouldPlay = false; - } - } - get interstitialsManager() { - if (!this.manager) { - if (!this.hls || !this.schedule.events) { - return null; - } - const c = this; - const effectiveBufferingItem = () => c.bufferingItem || c.waitingItem; - const effectivePlayingItem = () => c.playingItem || c.waitingItem; - const getAssetPlayer = asset => asset ? c.getAssetPlayer(asset.identifier) : asset; - const getMappedTime = (item, timelineType, asset, controllerField, assetPlayerField) => { - if (item) { - let time = item[timelineType].start; - const interstitial = item.event; - if (interstitial) { - if (timelineType === 'playout' || interstitial.timelineOccupancy !== TimelineOccupancy.Point) { - const assetPlayer = getAssetPlayer(asset); - if ((assetPlayer == null ? void 0 : assetPlayer.interstitial) === interstitial) { - time += assetPlayer.assetItem.startOffset + assetPlayer[assetPlayerField]; - } - } - } else { - const value = controllerField === 'bufferedPos' ? getBufferedEnd() : c[controllerField]; - time += value - item.start; - } - return time; - } - return 0; - }; - const findMappedTime = (primaryTime, timelineType) => { - if (primaryTime !== 0 && timelineType !== 'primary' && c.schedule.length) { - var _c$schedule$items; - const index = c.schedule.findItemIndexAtTime(primaryTime); - const item = (_c$schedule$items = c.schedule.items) == null ? void 0 : _c$schedule$items[index]; - if (item) { - const diff = item[timelineType].start - item.start; - return primaryTime + diff; - } - } - return primaryTime; - }; - const getBufferedEnd = () => { - const value = c.bufferedPos; - if (value === Number.MAX_VALUE) { - return getMappedDuration('primary'); - } - return value; - }; - const getMappedDuration = timelineType => { - var _c$primaryDetails; - if ((_c$primaryDetails = c.primaryDetails) != null && _c$primaryDetails.live) { - // return end of last event item or playlist - return c.primaryDetails.edge; - } - return c.schedule.durations[timelineType]; - }; - const seekTo = (time, timelineType) => { - var _item$event, _c$schedule$items2; - const item = effectivePlayingItem(); - if (item != null && (_item$event = item.event) != null && _item$event.restrictions.skip) { - return; - } - c.log(`seek to ${time} "${timelineType}"`); - const playingItem = effectivePlayingItem(); - const targetIndex = c.schedule.findItemIndexAtTime(time, timelineType); - const targetItem = (_c$schedule$items2 = c.schedule.items) == null ? void 0 : _c$schedule$items2[targetIndex]; - const playingInterstitial = playingItem == null ? void 0 : playingItem.event; - const appendInPlace = playingInterstitial == null ? void 0 : playingInterstitial.appendInPlace; - const seekInItem = playingItem && c.itemsMatch(playingItem, targetItem); - if (playingItem && (appendInPlace || seekInItem)) { - // seek in asset player or primary media (appendInPlace) - const assetPlayer = getAssetPlayer(c.playingAsset); - const media = (assetPlayer == null ? void 0 : assetPlayer.media) || c.hls.media; - if (media) { - const currentTime = timelineType === 'primary' ? media.currentTime : getMappedTime(playingItem, timelineType, c.playingAsset, 'timelinePos', 'currentTime'); - const diff = time - currentTime; - const seekToTime = media.currentTime + diff; - if (seekToTime >= 0 && (!assetPlayer || appendInPlace || seekToTime <= assetPlayer.duration)) { - media.currentTime = seekToTime; - return; - } - } - } - // seek out of item or asset - if (targetItem) { - let seekToTime = time; - if (timelineType !== 'primary') { - const primarySegmentStart = targetItem[timelineType].start; - const diff = time - primarySegmentStart; - seekToTime = targetItem.start + diff; - } - const targetIsPrimary = !c.isInterstitial(targetItem); - if (!c.isInterstitial(playingItem) && (targetIsPrimary || targetItem.event.appendInPlace)) { - const media = c.hls.media; - if (media) { - media.currentTime = seekToTime; - } - } else if (playingItem) { - // check if an Interstitial between the current item and target item has an X-RESTRICT JUMP restriction - const playingIndex = c.findItemIndex(playingItem); - if (targetIndex > playingIndex) { - const jumpIndex = c.schedule.findJumpRestrictedIndex(playingIndex + 1, targetIndex); - if (jumpIndex > playingIndex) { - c.setSchedulePosition(jumpIndex); - return; - } - } - let assetIndex = 0; - if (targetIsPrimary) { - c.timelinePos = seekToTime; - } else { - var _targetItem$event; - const assetList = targetItem == null ? void 0 : (_targetItem$event = targetItem.event) == null ? void 0 : _targetItem$event.assetList; - if (assetList) { - const eventTime = time - (targetItem[timelineType] || targetItem).start; - for (let i = assetList.length; i--;) { - const asset = assetList[i]; - if (asset.duration && eventTime >= asset.startOffset && eventTime < asset.startOffset + asset.duration) { - assetIndex = i; - break; - } - } - } - } - c.setSchedulePosition(targetIndex, assetIndex); - } - } - }; - this.manager = { - get events() { - var _c$schedule, _c$schedule$events; - return ((_c$schedule = c.schedule) == null ? void 0 : (_c$schedule$events = _c$schedule.events) == null ? void 0 : _c$schedule$events.slice(0)) || []; - }, - get schedule() { - var _c$schedule2, _c$schedule2$items; - return ((_c$schedule2 = c.schedule) == null ? void 0 : (_c$schedule2$items = _c$schedule2.items) == null ? void 0 : _c$schedule2$items.slice(0)) || []; - }, - get playerQueue() { - return c.playerQueue.slice(0); - }, - get bufferingPlayer() { - return c.getBufferingPlayer(); - }, - get bufferingAsset() { - return c.bufferingAsset; - }, - get bufferingItem() { - return c.bufferingItem; - }, - get playingAsset() { - return c.playingAsset; - }, - get playingItem() { - return c.playingItem; - }, - get bufferingIndex() { - const item = effectiveBufferingItem(); - return c.findItemIndex(item); - }, - get playingIndex() { - const item = effectivePlayingItem(); - return c.findItemIndex(item); - }, - get waitingIndex() { - return c.findItemIndex(c.waitingItem); - }, - primary: { - get bufferedEnd() { - return getBufferedEnd(); - }, - get currentTime() { - const timelinePos = c.timelinePos; - return timelinePos > 0 ? timelinePos : 0; - }, - get duration() { - return getMappedDuration('primary'); - }, - get seekableStart() { - var _c$primaryDetails2; - return ((_c$primaryDetails2 = c.primaryDetails) == null ? void 0 : _c$primaryDetails2.fragmentStart) || 0; - }, - seekTo: time => seekTo(time, 'primary') - }, - playout: { - get bufferedEnd() { - return getMappedTime(effectiveBufferingItem(), 'playout', c.bufferingAsset, 'bufferedPos', 'bufferedEnd'); - }, - get currentTime() { - return getMappedTime(effectivePlayingItem(), 'playout', c.playingAsset, 'timelinePos', 'currentTime'); - }, - get duration() { - return getMappedDuration('playout'); - }, - get seekableStart() { - var _c$primaryDetails3; - return findMappedTime(((_c$primaryDetails3 = c.primaryDetails) == null ? void 0 : _c$primaryDetails3.fragmentStart) || 0, 'playout'); - }, - seekTo: time => seekTo(time, 'playout') - }, - integrated: { - get bufferedEnd() { - return getMappedTime(effectiveBufferingItem(), 'integrated', c.bufferingAsset, 'bufferedPos', 'bufferedEnd'); - }, - get currentTime() { - return getMappedTime(effectivePlayingItem(), 'integrated', c.playingAsset, 'timelinePos', 'currentTime'); - }, - get duration() { - return getMappedDuration('integrated'); - }, - get seekableStart() { - var _c$primaryDetails4; - return findMappedTime(((_c$primaryDetails4 = c.primaryDetails) == null ? void 0 : _c$primaryDetails4.fragmentStart) || 0, 'integrated'); - }, - seekTo: time => seekTo(time, 'integrated') - }, - skip: () => { - const item = effectivePlayingItem(); - const event = item == null ? void 0 : item.event; - if (event && !event.restrictions.skip) { - const index = c.findItemIndex(item); - if (event.appendInPlace) { - const time = item.playout.start + item.event.duration; - seekTo(time + 0.001, 'playout'); - } else { - c.advanceAfterAssetEnded(event, index, Infinity); - } - } - } - }; - } - return this.manager; - } - - // Schedule getters - get playingLastItem() { - var _this$schedule; - const playingItem = this.playingItem; - if (!this.playbackStarted || !playingItem) { - return false; - } - const items = (_this$schedule = this.schedule) == null ? void 0 : _this$schedule.items; - return this.itemsMatch(playingItem, items ? items[items.length - 1] : null); - } - get playbackStarted() { - return this.playingItem !== null; - } - - // Media getters and event callbacks - get currentTime() { - var _this$bufferingItem, _this$bufferingItem$e, _media; - if (this.mediaSelection === null) { - // Do not advance before schedule is known - return undefined; - } - // Ignore currentTime when detached for Interstitial playback with source reset - const queuedForPlayback = this.waitingItem || this.playingItem; - if (this.isInterstitial(queuedForPlayback) && !queuedForPlayback.event.appendInPlace) { - return undefined; - } - let media = this.media; - if (!media && (_this$bufferingItem = this.bufferingItem) != null && (_this$bufferingItem$e = _this$bufferingItem.event) != null && _this$bufferingItem$e.appendInPlace) { - // Observe detached media currentTime when appending in place - media = this.primaryMedia; - } - const currentTime = (_media = media) == null ? void 0 : _media.currentTime; - if (currentTime === undefined || !isFiniteNumber(currentTime)) { - return undefined; - } - return currentTime; - } - get primaryMedia() { - var _this$detachedData; - return this.media || ((_this$detachedData = this.detachedData) == null ? void 0 : _this$detachedData.media) || null; - } - isInterstitial(item) { - return !!(item != null && item.event); - } - retreiveMediaSource(assetId, toSegment) { - const player = this.getAssetPlayer(assetId); - if (player) { - this.transferMediaFromPlayer(player, toSegment); - } - } - transferMediaFromPlayer(player, toSegment) { - const appendInPlace = player.interstitial.appendInPlace; - const playerMedia = player.media; - if (appendInPlace && playerMedia === this.primaryMedia) { - if (!toSegment || this.isInterstitial(toSegment) && !toSegment.event.appendInPlace) { - // MediaSource cannot be transfered back to an Interstitial that requires a source reset - // no-op when toSegment is undefined - if (toSegment && playerMedia) { - this.detachedData = { - media: playerMedia - }; - } - return; - } - const attachMediaSourceData = player.transferMedia(); - this.log(`transfer MediaSource from ${player} ${JSON.stringify(attachMediaSourceData)}`); - this.bufferingAsset = null; - this.detachedData = attachMediaSourceData; - } else if (toSegment && playerMedia) { - this.shouldPlay || (this.shouldPlay = !playerMedia.paused); - } - } - transferMediaTo(player, media) { - var _this$detachedData2, _attachMediaSourceDat; - let attachMediaSourceData = null; - const primaryPlayer = this.hls; - const isAssetPlayer = player !== primaryPlayer; - const appendInPlace = isAssetPlayer && player.interstitial.appendInPlace; - const detachedMediaSource = (_this$detachedData2 = this.detachedData) == null ? void 0 : _this$detachedData2.mediaSource; - let logFromSource; - if (primaryPlayer.media && appendInPlace) { - attachMediaSourceData = primaryPlayer.transferMedia(); - this.detachedData = attachMediaSourceData; - logFromSource = `Primary`; - } else if (detachedMediaSource) { - const bufferingPlayer = this.getBufferingPlayer(); - if (bufferingPlayer) { - attachMediaSourceData = bufferingPlayer.transferMedia(); - } - logFromSource = `${bufferingPlayer}`; - } else { - logFromSource = `<unknown>`; - } - this.log(`transferring to ${isAssetPlayer ? player : 'Primary'} -MediaSource ${JSON.stringify(attachMediaSourceData)} from ${logFromSource}`); - if (!attachMediaSourceData) { - if (detachedMediaSource) { - attachMediaSourceData = this.detachedData; - this.log(`using detachedData: MediaSource ${JSON.stringify(attachMediaSourceData)}`); - } else if (!this.detachedData) { - this.warn(`missing MediaSource (detachedData)!`); - this.hls.detachMedia(); - this.detachedData = { - media - }; - } - } - const transferring = attachMediaSourceData && 'mediaSource' in attachMediaSourceData && ((_attachMediaSourceDat = attachMediaSourceData.mediaSource) == null ? void 0 : _attachMediaSourceDat.readyState) !== 'closed'; - const dataToAttach = transferring && attachMediaSourceData ? attachMediaSourceData : media; - this.log(`${transferring ? 'transfering MediaSource' : 'attaching media'} to ${isAssetPlayer ? player : 'Primary'}`); - if (dataToAttach === attachMediaSourceData) { - const isAssetAtEndOfSchedule = isAssetPlayer && player.assetId === this.schedule.assetIdAtEnd; - // Prevent asset players from marking EoS on transferred MediaSource - dataToAttach.overrides = { - duration: this.schedule.duration, - endOfStream: !isAssetPlayer || isAssetAtEndOfSchedule, - cueRemoval: false - }; - } - player.attachMedia(dataToAttach); - } - // Scheduling methods - checkStart() { - const schedule = this.schedule; - const interstitialEvents = schedule.events; - if (!interstitialEvents || this.playbackDisabled) { - return; - } - // Check buffered to pre-roll - if (this.bufferedPos === -1) { - this.bufferedPos = 0; - } - // Start stepping through schedule when playback begins for the first time and we have a pre-roll - const timelinePos = this.timelinePos; - const waitingItem = this.waitingItem; - if (timelinePos === -1) { - const startPosition = this.hls.startPosition; - this.timelinePos = startPosition; - if (interstitialEvents.length && interstitialEvents[0].cue.pre) { - const index = schedule.findEventIndex(interstitialEvents[0].identifier); - this.setSchedulePosition(index); - } else if (startPosition >= 0 || !this.primaryLive) { - const start = this.timelinePos = startPosition > 0 ? startPosition : 0; - const index = schedule.findItemIndexAtTime(start); - this.setSchedulePosition(index); - } - } else if (waitingItem && !this.playingItem) { - const index = schedule.findItemIndex(waitingItem); - this.setSchedulePosition(index); - } - } - advanceAfterAssetEnded(interstitial, index, assetListIndex) { - const nextAssetIndex = assetListIndex + 1; - if (!interstitial.isAssetPastPlayoutLimit(nextAssetIndex) && !interstitial.assetList[nextAssetIndex].error) { - // Advance to next asset list item - this.setSchedulePosition(index, nextAssetIndex); - } else { - // Advance to next schedule segment - // check if we've reached the end of the program - const scheduleItems = this.schedule.items; - if (scheduleItems) { - const nextIndex = index + 1; - const scheduleLength = scheduleItems.length; - if (nextIndex >= scheduleLength) { - this.setSchedulePosition(-1); - return; - } - this.setSchedulePosition(nextIndex); - } - } - } - setScheduleToAssetAtTime(time, playingAsset) { - const schedule = this.schedule; - const parentIdentifier = playingAsset.parentIdentifier; - const interstitial = schedule.getEvent(parentIdentifier); - if (interstitial) { - const itemIndex = schedule.findEventIndex(parentIdentifier); - const assetListIndex = schedule.findAssetIndex(interstitial, time); - this.setSchedulePosition(itemIndex, assetListIndex); - } - } - setSchedulePosition(index, assetListIndex) { - const scheduleItems = this.schedule.items; - if (!scheduleItems || this.playbackDisabled) { - return; - } - this.log(`setSchedulePosition ${index}, ${assetListIndex}`); - const scheduledItem = index >= 0 ? scheduleItems[index] : null; - const media = this.primaryMedia; - // Cleanup current item / asset - const currentItem = this.playingItem; - const playingLastItem = this.playingLastItem; - if (this.isInterstitial(currentItem)) { - var _interstitial$assetLi; - const interstitial = currentItem.event; - const playingAsset = this.playingAsset; - const assetId = playingAsset == null ? void 0 : playingAsset.identifier; - const player = assetId ? this.getAssetPlayer(assetId) : null; - if (player && assetId && (!this.eventItemsMatch(currentItem, scheduledItem) || assetListIndex !== undefined && assetId !== ((_interstitial$assetLi = interstitial.assetList) == null ? void 0 : _interstitial$assetLi[assetListIndex].identifier))) { - this.playingAsset = null; - const _assetListIndex = interstitial.findAssetIndex(playingAsset); - this.log(`INTERSTITIAL_ASSET_ENDED ${_assetListIndex + 1}/${interstitial.assetList.length} ${eventAssetToString(playingAsset)}`); - this.hls.trigger(Events.INTERSTITIAL_ASSET_ENDED, { - asset: playingAsset, - assetListIndex: _assetListIndex, - event: interstitial, - schedule: scheduleItems.slice(0), - scheduleIndex: index, - player - }); - this.retreiveMediaSource(assetId, scheduledItem); - if (player.media && !this.detachedData) { - player.detachMedia(); - } - this.clearAssetPlayer(assetId, scheduledItem); - } - if (!this.eventItemsMatch(currentItem, scheduledItem)) { - this.playingItem = null; - this.log(`INTERSTITIAL_ENDED ${interstitial} ${segmentToString(currentItem)}`); - interstitial.hasPlayed = true; - this.hls.trigger(Events.INTERSTITIAL_ENDED, { - event: interstitial, - schedule: scheduleItems.slice(0), - scheduleIndex: index - }); - // Exiting an Interstitial - this.clearInterstitial(interstitial, scheduledItem); - if (interstitial.cue.once) { - this.updateSchedule(); - if (scheduledItem) { - const updatedIndex = this.schedule.findItemIndex(scheduledItem); - this.setSchedulePosition(updatedIndex, assetListIndex); - } - return; - } - } - } - // Cleanup out of range Interstitials - const playerQueue = this.playerQueue; - if (playerQueue.length) { - playerQueue.forEach(player => { - const interstitial = player.interstitial; - const queuedIndex = this.schedule.findEventIndex(interstitial.identifier); - if (queuedIndex < index || queuedIndex > index + 1) { - this.clearInterstitial(interstitial, scheduledItem); - } - }); - } - // Setup scheduled item - if (this.isInterstitial(scheduledItem)) { - this.timelinePos = Math.min(Math.max(this.timelinePos, scheduledItem.start), scheduledItem.end); - // Handle Interstitial - const interstitial = scheduledItem.event; - // find asset index - if (assetListIndex === undefined) { - assetListIndex = this.schedule.findAssetIndex(interstitial, this.timelinePos); - } - // Ensure Interstitial is enqueued - const waitingItem = this.waitingItem; - let player = this.preloadAssets(interstitial, assetListIndex); - if (!player) { - this.setBufferingItem(scheduledItem); - } - if (!this.eventItemsMatch(scheduledItem, currentItem || waitingItem)) { - this.waitingItem = scheduledItem; - this.log(`INTERSTITIAL_STARTED ${segmentToString(scheduledItem)} ${interstitial.appendInPlace ? 'append in place' : ''}`); - this.hls.trigger(Events.INTERSTITIAL_STARTED, { - event: interstitial, - schedule: scheduleItems.slice(0), - scheduleIndex: index - }); - } - const assetListLength = interstitial.assetList.length; - if (assetListLength === 0 && !interstitial.assetListResponse) { - // Waiting at end of primary content segment - // Expect setSchedulePosition to be called again once ASSET-LIST is loaded - this.log(`Waiting for ASSET-LIST to complete loading ${interstitial}`); - return; - } - if (interstitial.assetListLoader) { - interstitial.assetListLoader.destroy(); - interstitial.assetListLoader = undefined; - } - if (!media) { - this.log(`Waiting for attachMedia to start Interstitial ${interstitial}`); - return; - } - // Update schedule and asset list position now that it can start - this.waitingItem = null; - this.playingItem = scheduledItem; - - // If asset-list is empty or missing asset index, advance to next item - const assetItem = interstitial.assetList[assetListIndex]; - if (!assetItem) { - const nextItem = scheduleItems[index + 1]; - const _media2 = this.media; - if (nextItem && _media2 && !this.isInterstitial(nextItem) && _media2.currentTime < nextItem.start) { - _media2.currentTime = this.timelinePos = nextItem.start; - } - this.advanceAfterAssetEnded(interstitial, index, assetListIndex || 0); - return; - } - - // Start Interstitial Playback - if (!player) { - player = this.getAssetPlayer(assetItem.identifier); - } - if (player === null || player.destroyed) { - this.warn(`asset ${assetListIndex + 1}/${assetListLength} player destroyed ${interstitial}`); - player = this.createAssetPlayer(interstitial, assetItem, assetListIndex); - } - if (!this.eventItemsMatch(scheduledItem, this.bufferingItem)) { - if (interstitial.appendInPlace && this.isAssetBuffered(assetItem)) { - return; - } - } - this.startAssetPlayer(player, assetListIndex, scheduleItems, index, media); - if (this.shouldPlay) { - var _player$media; - (_player$media = player.media) == null ? void 0 : _player$media.play(); - } - } else if (scheduledItem !== null) { - this.resumePrimary(scheduledItem, index); - if (this.shouldPlay) { - var _this$hls$media; - (_this$hls$media = this.hls.media) == null ? void 0 : _this$hls$media.play(); - } - } else if (playingLastItem && this.isInterstitial(currentItem)) { - // Maintain playingItem state at end of schedule (setSchedulePosition(-1) called to end program) - // this allows onSeeking handler to update schedule position - this.playingItem = currentItem; - if (!currentItem.event.appendInPlace) { - // Media must be re-attached to resume primary schedule if not sharing source - this.attachPrimary(this.schedule.durations.primary, null); - } - } - } - get playbackDisabled() { - return this.hls.config.enableInterstitialPlayback === false; - } - get primaryDetails() { - var _this$mediaSelection, _this$mediaSelection$; - return (_this$mediaSelection = this.mediaSelection) == null ? void 0 : (_this$mediaSelection$ = _this$mediaSelection.main) == null ? void 0 : _this$mediaSelection$.details; - } - get primaryLive() { - var _this$primaryDetails; - return !!((_this$primaryDetails = this.primaryDetails) != null && _this$primaryDetails.live); - } - resumePrimary(scheduledItem, index) { - var _this$detachedData3; - this.playingItem = scheduledItem; - this.playingAsset = null; - this.waitingItem = null; - this.bufferedToItem(scheduledItem); - this.log(`resuming ${segmentToString(scheduledItem)}`); - if (!((_this$detachedData3 = this.detachedData) != null && _this$detachedData3.mediaSource)) { - let timelinePos = this.timelinePos; - if (timelinePos < scheduledItem.start || timelinePos >= scheduledItem.end) { - timelinePos = this.getPrimaryResumption(scheduledItem, index); - this.timelinePos = timelinePos; - } - this.attachPrimary(timelinePos, scheduledItem); - } - const scheduleItems = this.schedule.items; - if (!scheduleItems) { - return; - } - this.log(`resumed ${segmentToString(scheduledItem)}`); - this.hls.trigger(Events.INTERSTITIALS_PRIMARY_RESUMED, { - schedule: scheduleItems.slice(0), - scheduleIndex: index - }); - this.checkBuffer(); - } - getPrimaryResumption(scheduledItem, index) { - const itemStart = scheduledItem.start; - if (this.primaryLive) { - const details = this.primaryDetails; - if (index === 0) { - return this.hls.startPosition; - } else if (details && (itemStart < details.fragmentStart || itemStart > details.edge)) { - return this.hls.liveSyncPosition || -1; - } - } - return itemStart; - } - isAssetBuffered(asset) { - const player = this.getAssetPlayer(asset.identifier); - if (player != null && player.hls) { - return player.hls.bufferedToEnd; - } - const bufferInfo = BufferHelper.bufferInfo(this.primaryMedia, this.timelinePos, 0); - return bufferInfo.end + 1 >= asset.timelineStart + (asset.duration || 0); - } - attachPrimary(timelinePos, item, skipSeekToStartPosition) { - if (item) { - this.setBufferingItem(item); - } else { - this.bufferingItem = null; - } - this.bufferingAsset = null; - const media = this.primaryMedia; - if (!media) { - return; - } - const hls = this.hls; - if (hls.media) { - this.checkBuffer(); - } else { - this.transferMediaTo(hls, media); - if (skipSeekToStartPosition) { - hls.startLoad(timelinePos, skipSeekToStartPosition); - } - } - if (!skipSeekToStartPosition) { - // Set primary position to resume time - this.timelinePos = timelinePos; - hls.startLoad(timelinePos, skipSeekToStartPosition); - } - } - - // HLS.js event callbacks - onManifestLoading() { - this.stopLoad(); - this.schedule.reset(); - this.emptyPlayerQueue(); - this.clearScheduleState(); - this.bufferedPos = this.timelinePos = -1; - this.mediaSelection = this.altSelection = this.manager = this.requiredTracks = null; - // BUFFER_CODECS listener added here for buffer-controller to handle it first where it adds tracks - this.hls.off(Events.BUFFER_CODECS, this.onBufferCodecs, this); - this.hls.on(Events.BUFFER_CODECS, this.onBufferCodecs, this); - } - onLevelUpdated(event, data) { - const main = this.hls.levels[data.level]; - const currentSelection = _objectSpread2(_objectSpread2({}, this.mediaSelection || this.altSelection), {}, { - main - }); - this.mediaSelection = currentSelection; - this.schedule.parseInterstitialDateRanges(currentSelection); - if (!this.playingItem && this.schedule.items) { - this.checkStart(); - } - } - onAudioTrackUpdated(event, data) { - const audio = this.hls.audioTracks[data.id]; - const previousSelection = this.mediaSelection; - if (!previousSelection) { - this.altSelection = _objectSpread2(_objectSpread2({}, this.altSelection), {}, { - audio - }); - return; - } - const currentSelection = _objectSpread2(_objectSpread2({}, previousSelection), {}, { - audio - }); - this.mediaSelection = currentSelection; - } - onSubtitleTrackUpdated(event, data) { - const subtitles = this.hls.subtitleTracks[data.id]; - const previousSelection = this.mediaSelection; - if (!previousSelection) { - this.altSelection = _objectSpread2(_objectSpread2({}, this.altSelection), {}, { - subtitles - }); - return; - } - const currentSelection = _objectSpread2(_objectSpread2({}, previousSelection), {}, { - subtitles - }); - this.mediaSelection = currentSelection; - } - onAudioTrackSwitching(event, data) { - const audioOption = getBasicSelectionOption(data); - this.playerQueue.forEach(player => player.hls.setAudioOption(data) || player.hls.setAudioOption(audioOption)); - } - onSubtitleTrackSwitch(event, data) { - const subtitleOption = getBasicSelectionOption(data); - this.playerQueue.forEach(player => player.hls.setSubtitleOption(data) || data.id !== -1 && player.hls.setSubtitleOption(subtitleOption)); - } - onBufferCodecs(event, data) { - const requiredTracks = data.tracks; - if (requiredTracks) { - this.requiredTracks = requiredTracks; - } - } - onBufferAppended(event, data) { - this.checkBuffer(); - } - onBufferFlushed(event, data) { - const { - playingItem - } = this; - if (playingItem && playingItem !== this.bufferingItem && !this.isInterstitial(playingItem)) { - const timelinePos = this.timelinePos; - this.bufferedPos = timelinePos; - this.setBufferingItem(playingItem); - } - } - onBufferedToEnd(event) { - // Buffered to post-roll - const interstitialEvents = this.schedule.events; - if (this.bufferedPos < Number.MAX_VALUE && interstitialEvents) { - for (let i = 0; i < interstitialEvents.length; i++) { - const interstitial = interstitialEvents[i]; - if (interstitial.cue.post) { - var _this$schedule$items; - const scheduleIndex = this.schedule.findEventIndex(interstitial.identifier); - const item = (_this$schedule$items = this.schedule.items) == null ? void 0 : _this$schedule$items[scheduleIndex]; - if (this.isInterstitial(item) && this.eventItemsMatch(item, this.bufferingItem)) { - this.bufferedToItem(item, 0); - } - break; - } - } - this.bufferedPos = Number.MAX_VALUE; - } - } - onMediaEnded(event) { - const playingItem = this.playingItem; - if (!this.playingLastItem && playingItem) { - const playingIndex = this.findItemIndex(playingItem); - this.setSchedulePosition(playingIndex + 1); - } - } - updateItem(previousItem, time) { - // find item in this.schedule.items; - const items = this.schedule.items; - if (previousItem && items) { - const index = this.findItemIndex(previousItem, time); - return items[index] || null; - } - return null; - } - itemsMatch(a, b) { - var _a$nextEvent, _b$nextEvent; - return !!b && (a === b || a.event && b.event && this.eventItemsMatch(a, b) || !a.event && !b.event && ((_a$nextEvent = a.nextEvent) == null ? void 0 : _a$nextEvent.identifier) === ((_b$nextEvent = b.nextEvent) == null ? void 0 : _b$nextEvent.identifier)); - } - eventItemsMatch(a, b) { - var _b$event; - return !!b && (a === b || a.event.identifier === ((_b$event = b.event) == null ? void 0 : _b$event.identifier)); - } - findItemIndex(item, time) { - return item ? this.schedule.findItemIndex(item, time) : -1; - } - updateSchedule() { - const mediaSelection = this.mediaSelection; - if (!mediaSelection) { - return; - } - this.schedule.updateSchedule(mediaSelection, []); - } - - // Schedule buffer control - checkBuffer() { - const items = this.schedule.items; - if (!items) { - return; - } - // Find when combined forward buffer change reaches next schedule segment - const bufferInfo = BufferHelper.bufferInfo(this.primaryMedia, this.timelinePos, 0); - this.updateBufferedPos(bufferInfo.end, items, bufferInfo.len === 0); - } - updateBufferedPos(bufferEnd, items, bufferIsEmpty) { - const schedule = this.schedule; - const bufferingItem = this.bufferingItem; - if (this.bufferedPos > bufferEnd) { - return; - } - if (items.length === 1 && this.itemsMatch(items[0], bufferingItem)) { - this.bufferedPos = bufferEnd; - return; - } - const playingItem = this.playingItem; - const playingIndex = this.findItemIndex(playingItem); - let bufferEndIndex = schedule.findItemIndexAtTime(bufferEnd); - if (this.bufferedPos < bufferEnd) { - var _bufferingItem$event; - const bufferingIndex = this.findItemIndex(bufferingItem); - const nextToBufferIndex = Math.min(bufferingIndex + 1, items.length - 1); - const nextItemToBuffer = items[nextToBufferIndex]; - if (bufferEndIndex === -1 && bufferingItem && bufferEnd >= bufferingItem.end) { - bufferEndIndex = nextToBufferIndex; - } - if (nextToBufferIndex - playingIndex > 1 && (bufferingItem == null ? void 0 : (_bufferingItem$event = bufferingItem.event) == null ? void 0 : _bufferingItem$event.appendInPlace) === false) { - // do not advance buffering item past Interstitial that requires source reset - return; - } - this.bufferedPos = bufferEnd; - if (bufferEndIndex > bufferingIndex && bufferEndIndex > playingIndex) { - this.bufferedToItem(nextItemToBuffer); - } else { - // allow more time than distance from edge for assets to load - const details = this.primaryDetails; - if (this.primaryLive && details && bufferEnd > details.edge - details.targetduration && nextItemToBuffer.start < details.edge + this.hls.config.interstitialLiveLookAhead && this.isInterstitial(nextItemToBuffer)) { - this.preloadAssets(nextItemToBuffer.event, 0); - } - } - } else if (bufferIsEmpty && playingItem && bufferingItem !== playingItem && bufferEndIndex === playingIndex) { - this.bufferedToItem(playingItem); - } - } - setBufferingItem(item) { - const bufferingLast = this.bufferingItem; - const schedule = this.schedule; - const { - items, - events - } = schedule; - if (items && events && (!bufferingLast || schedule.findItemIndex(bufferingLast) !== schedule.findItemIndex(item))) { - const isInterstitial = this.isInterstitial(item); - const bufferingPlayer = this.getBufferingPlayer(); - const timeRemaining = bufferingPlayer ? bufferingPlayer.remaining : bufferingLast ? bufferingLast.end - this.timelinePos : 0; - this.log(`buffered to boundary ${segmentToString(item)}` + (bufferingLast ? ` (${timeRemaining.toFixed(2)} remaining)` : '')); - this.bufferingItem = item; - this.bufferedPos = item.start; - if (!this.playbackDisabled) { - if (isInterstitial) { - // primary fragment loading will exit early in base-stream-controller while `bufferingItem` is set to an Interstitial block - this.playerQueue.forEach(player => player.resumeBuffering()); - } else { - this.hls.resumeBuffering(); - this.playerQueue.forEach(player => player.pauseBuffering()); - } - } - this.hls.trigger(Events.INTERSTITIALS_BUFFERED_TO_BOUNDARY, { - events: events.slice(0), - schedule: items.slice(0), - bufferingIndex: this.findItemIndex(item), - playingIndex: this.findItemIndex(this.playingItem) - }); - } - return bufferingLast; - } - bufferedToItem(item, assetListIndex = 0) { - const bufferingLast = this.setBufferingItem(item); - if (this.playbackDisabled) { - return; - } - if (this.isInterstitial(item)) { - // Ensure asset list is loaded - this.bufferedToEvent(item, assetListIndex); - } else if (bufferingLast !== null) { - // If primary player is detached, it is also stopped, restart loading at primary position - this.bufferingAsset = null; - const detachedData = this.detachedData; - if (detachedData) { - if (detachedData.mediaSource) { - const skipSeekToStartPosition = true; - this.attachPrimary(item.start, item, skipSeekToStartPosition); - } else { - this.preloadPrimary(item); - } - } else { - // If not detached seek to resumption point - this.preloadPrimary(item); - } - } - } - preloadPrimary(item) { - const index = this.findItemIndex(item); - const timelinePos = this.getPrimaryResumption(item, index); - this.hls.startLoad(timelinePos); - } - bufferedToEvent(item, assetListIndex) { - const interstitial = item.event; - const neverLoaded = interstitial.assetList.length === 0 && !interstitial.assetListLoader; - const playOnce = interstitial.cue.once; - if (neverLoaded || !playOnce) { - // Buffered to Interstitial boundary - const player = this.preloadAssets(interstitial, assetListIndex); - if (player != null && player.interstitial.appendInPlace) { - // If we have a player and asset list info, start buffering - const assetItem = interstitial.assetList[assetListIndex]; - const media = this.primaryMedia; - if (assetItem && media) { - this.bufferAssetPlayer(player, media); - } - } - } - } - preloadAssets(interstitial, assetListIndex) { - const assetListLength = interstitial.assetList.length; - const neverLoaded = assetListLength === 0 && !interstitial.assetListLoader; - const playOnce = interstitial.cue.once; - if (neverLoaded) { - this.log(`Load interstitial asset ${assetListIndex + 1}/${assetListLength} ${interstitial}`); - const timelineStart = interstitial.timelineStart; - if (interstitial.appendInPlace) { - this.flushFrontBuffer(timelineStart); - } - const uri = interstitial.assetUrl; - if (uri) { - return this.createAsset(interstitial, 0, 0, timelineStart, interstitial.duration, uri); - } - let liveStartPosition = 0; - if (!this.playingItem && this.primaryLive) { - liveStartPosition = this.hls.startPosition; - if (liveStartPosition === -1) { - liveStartPosition = this.hls.liveSyncPosition || 0; - } - } - const assetListLoader = this.assetListLoader.loadAssetList(interstitial, liveStartPosition); - if (assetListLoader) { - interstitial.assetListLoader = assetListLoader; - } - } else if (!playOnce && assetListLength) { - // Re-buffered to Interstitial boundary, re-create asset player(s) - for (let i = assetListIndex; i < assetListLength; i++) { - const asset = interstitial.assetList[i]; - const playerIndex = this.getAssetPlayerQueueIndex(asset.identifier); - if ((playerIndex === -1 || this.playerQueue[playerIndex].destroyed) && !asset.error) { - this.createAssetPlayer(interstitial, asset, i); - } - } - return this.getAssetPlayer(interstitial.assetList[assetListIndex].identifier); - } - return null; - } - flushFrontBuffer(startOffset) { - // Force queued flushing of all buffers - const requiredTracks = this.requiredTracks; - if (!requiredTracks) { - return; - } - const sourceBufferNames = Object.keys(requiredTracks); - sourceBufferNames.forEach(type => { - this.hls.trigger(Events.BUFFER_FLUSHING, { - startOffset, - endOffset: Infinity, - type - }); - }); - } - - // Interstitial Asset Player control - getAssetPlayerQueueIndex(assetId) { - const playerQueue = this.playerQueue; - for (let i = 0; i < playerQueue.length; i++) { - if (assetId === playerQueue[i].assetId) { - return i; - } - } - return -1; - } - getAssetPlayer(assetId) { - const index = this.getAssetPlayerQueueIndex(assetId); - return this.playerQueue[index] || null; - } - getBufferingPlayer() { - const { - playerQueue, - primaryMedia - } = this; - if (primaryMedia) { - for (let i = 0; i < playerQueue.length; i++) { - if (playerQueue[i].media === primaryMedia) { - return playerQueue[i]; - } - } - } - return null; - } - createAsset(interstitial, assetListIndex, startOffset, timelineStart, duration, uri) { - const assetItem = { - parentIdentifier: interstitial.identifier, - identifier: generateAssetIdentifier(interstitial, uri, assetListIndex), - duration, - startOffset, - timelineStart, - uri - }; - return this.createAssetPlayer(interstitial, assetItem, assetListIndex); - } - createAssetPlayer(interstitial, assetItem, assetListIndex) { - this.log(`create HLSAssetPlayer for ${eventAssetToString(assetItem)}`); - const primary = this.hls; - const userConfig = primary.userConfig; - let videoPreference = userConfig.videoPreference; - const currentLevel = primary.levels[primary.loadLevel] || primary.levels[primary.currentLevel]; - if (videoPreference || currentLevel) { - videoPreference = _extends({}, videoPreference); - if (currentLevel.videoCodec) { - videoPreference.videoCodec = currentLevel.videoCodec; - } - if (currentLevel.videoRange) { - videoPreference.allowedVideoRanges = [currentLevel.videoRange]; - } - } - const selectedAudio = primary.audioTracks[primary.audioTrack]; - const selectedSubtitle = primary.subtitleTracks[primary.subtitleTrack]; - let startPosition = 0; - if (this.primaryLive) { - const timePastStart = this.timelinePos - assetItem.timelineStart; - if (timePastStart > 1) { - const duration = assetItem.duration; - if (duration && timePastStart < duration) { - startPosition = timePastStart; - } - } - } - const playerConfig = _objectSpread2(_objectSpread2({}, userConfig), {}, { - // autoStartLoad: false, - startFragPrefetch: true, - primarySessionId: primary.sessionId, - assetPlayerId: assetItem.identifier, - abrEwmaDefaultEstimate: primary.bandwidthEstimate, - interstitialsController: undefined, - startPosition, - liveDurationInfinity: false, - testBandwidth: false, - videoPreference, - audioPreference: selectedAudio || userConfig.audioPreference, - subtitlePreference: selectedSubtitle || userConfig.subtitlePreference - }); - if (interstitial.appendInPlace) { - interstitial.appendInPlaceStarted = true; - if (assetItem.timelineStart) { - playerConfig.timelineOffset = assetItem.timelineStart; - } - } - const cmcd = playerConfig.cmcd; - if (cmcd != null && cmcd.sessionId && cmcd.contentId) { - playerConfig.cmcd = _extends({}, cmcd, { - contentId: hash(assetItem.uri) - }); - } - const player = new HlsAssetPlayer(this.HlsPlayerClass, playerConfig, interstitial, assetItem); - this.playerQueue.push(player); - interstitial.assetList[assetListIndex] = assetItem; - const assetId = assetItem.identifier; - // Listen for LevelDetails and PTS change to update duration - const updateAssetPlayerDetails = details => { - if (details.live) { - const error = new Error(`Interstitials MUST be VOD assets ${interstitial}`); - const errorData = { - fatal: true, - type: ErrorTypes.OTHER_ERROR, - details: ErrorDetails.INTERSTITIAL_ASSET_ITEM_ERROR, - error - }; - this.handleAssetItemError(errorData, interstitial, this.schedule.findEventIndex(interstitial.identifier), assetListIndex, error.message); - return; - } - // Get time at end of last fragment - const duration = details.edge - details.fragmentStart; - const currentAssetDuration = assetItem.duration; - if (currentAssetDuration === null || duration > currentAssetDuration) { - this.log(`Interstitial asset "${assetId}" duration change ${currentAssetDuration} > ${duration}`); - assetItem.duration = duration; - // Update schedule with new event and asset duration - this.updateSchedule(); - } - }; - player.on(Events.LEVEL_UPDATED, (event, { - details - }) => updateAssetPlayerDetails(details)); - player.on(Events.LEVEL_PTS_UPDATED, (event, { - details - }) => updateAssetPlayerDetails(details)); - const onBufferCodecs = (event, data) => { - const inQueuPlayer = this.getAssetPlayer(assetId); - if (inQueuPlayer && data.tracks) { - inQueuPlayer.off(Events.BUFFER_CODECS, onBufferCodecs); - inQueuPlayer.tracks = data.tracks; - const media = this.primaryMedia; - if (this.bufferingAsset === inQueuPlayer.assetItem && media && !inQueuPlayer.media) { - this.bufferAssetPlayer(inQueuPlayer, media); - } - } - }; - player.on(Events.BUFFER_CODECS, onBufferCodecs); - const bufferedToEnd = name => { - var _this$schedule$items2; - const inQueuPlayer = this.getAssetPlayer(assetId); - this.log(`buffered to end of asset ${inQueuPlayer}`); - if (!inQueuPlayer) { - return; - } - inQueuPlayer.off(Events.BUFFERED_TO_END, bufferedToEnd); - - // Preload at end of asset - const scheduleIndex = this.schedule.findEventIndex(interstitial.identifier); - const assetListIndex = interstitial.findAssetIndex(assetItem); - const nextAssetIndex = assetListIndex + 1; - const item = (_this$schedule$items2 = this.schedule.items) == null ? void 0 : _this$schedule$items2[scheduleIndex]; - if (this.isInterstitial(item)) { - if (assetListIndex !== -1 && !interstitial.isAssetPastPlayoutLimit(nextAssetIndex) && !interstitial.assetList[nextAssetIndex].error) { - this.bufferedToItem(item, assetListIndex + 1); - } else { - var _this$schedule$items3; - const nextItem = (_this$schedule$items3 = this.schedule.items) == null ? void 0 : _this$schedule$items3[scheduleIndex + 1]; - if (nextItem) { - this.bufferedToItem(nextItem); - } - } - } - }; - player.on(Events.BUFFERED_TO_END, bufferedToEnd); - const endedWithAssetIndex = assetIndex => { - return () => { - const inQueuPlayer = this.getAssetPlayer(assetId); - if (!inQueuPlayer) { - return; - } - this.shouldPlay = true; - const scheduleIndex = this.schedule.findEventIndex(interstitial.identifier); - this.advanceAfterAssetEnded(interstitial, scheduleIndex, assetIndex); - }; - }; - player.once(Events.MEDIA_ENDED, endedWithAssetIndex(assetListIndex)); - player.once(Events.PLAYOUT_LIMIT_REACHED, endedWithAssetIndex(Infinity)); - player.on(Events.ERROR, (event, data) => { - this.handleAssetItemError(data, interstitial, this.schedule.findEventIndex(interstitial.identifier), assetListIndex, `Asset player error ${data.error} ${interstitial}`); - }); - player.on(Events.DESTROYING, () => { - const inQueuPlayer = this.getAssetPlayer(assetId); - if (!inQueuPlayer) { - return; - } - const error = new Error(`Asset player destroyed unexpectedly ${assetId}`); - const errorData = { - fatal: true, - type: ErrorTypes.OTHER_ERROR, - details: ErrorDetails.INTERSTITIAL_ASSET_ITEM_ERROR, - error - }; - this.handleAssetItemError(errorData, interstitial, this.schedule.findEventIndex(interstitial.identifier), assetListIndex, error.message); - }); - this.hls.trigger(Events.INTERSTITIAL_ASSET_PLAYER_CREATED, { - asset: assetItem, - assetListIndex, - event: interstitial, - player - }); - return player; - } - clearInterstitial(interstitial, toSegment) { - interstitial.assetList.forEach(asset => { - this.clearAssetPlayer(asset.identifier, toSegment); - }); - interstitial.appendInPlaceStarted = false; - } - clearAssetPlayer(assetId, toSegment) { - if (toSegment === null) { - return; - } - const playerIndex = this.getAssetPlayerQueueIndex(assetId); - if (playerIndex !== -1) { - this.log(`clearAssetPlayer "${assetId}" toSegment: ${toSegment ? segmentToString(toSegment) : toSegment}`); - const player = this.playerQueue[playerIndex]; - this.transferMediaFromPlayer(player, toSegment); - this.playerQueue.splice(playerIndex, 1); - player.destroy(); - } - } - emptyPlayerQueue() { - let player; - while (player = this.playerQueue.pop()) { - player.destroy(); - } - this.playerQueue = []; - } - startAssetPlayer(player, assetListIndex, scheduleItems, scheduleIndex, media) { - const { - interstitial, - assetItem, - assetId - } = player; - const assetListLength = interstitial.assetList.length; - const playingAsset = this.playingAsset; - this.playingAsset = assetItem; - if (!playingAsset || playingAsset.identifier !== assetId) { - if (playingAsset) { - // Exiting another Interstitial asset - this.clearAssetPlayer(playingAsset.identifier, scheduleItems[scheduleIndex]); - delete playingAsset.error; - } - this.log(`INTERSTITIAL_ASSET_STARTED ${assetListIndex + 1}/${assetListLength} ${player}`); - // player.resumeBuffering(); - this.hls.trigger(Events.INTERSTITIAL_ASSET_STARTED, { - asset: assetItem, - assetListIndex, - event: interstitial, - schedule: scheduleItems.slice(0), - scheduleIndex, - player - }); - } - - // detach media and attach to interstitial player if it does not have another element attached - if (!player.media) { - this.bufferAssetPlayer(player, media); - } - } - bufferAssetPlayer(player, media) { - var _this$schedule$items4, _this$detachedData4; - const { - interstitial, - assetItem, - assetId - } = player; - const scheduleIndex = this.schedule.findEventIndex(interstitial.identifier); - const item = (_this$schedule$items4 = this.schedule.items) == null ? void 0 : _this$schedule$items4[scheduleIndex]; - if (!item) { - return; - } - this.setBufferingItem(item); - this.bufferingAsset = assetItem; - const bufferingPlayer = this.getBufferingPlayer(); - if (bufferingPlayer === player) { - return; - } - const activeTracks = (bufferingPlayer == null ? void 0 : bufferingPlayer.tracks) || ((_this$detachedData4 = this.detachedData) == null ? void 0 : _this$detachedData4.tracks) || this.requiredTracks; - if (interstitial.appendInPlace && assetItem !== this.playingAsset) { - // Do not buffer another item if tracks are unknown or incompatible - if (!player.tracks) { - return; - } - if (activeTracks && !isCompatibleTrackChange(activeTracks, player.tracks)) { - const error = new Error(`Asset "${assetId}" SourceBuffer tracks ('${Object.keys(player.tracks)}') are not compatible with primary content tracks ('${Object.keys(activeTracks)}')`); - const errorData = { - fatal: true, - type: ErrorTypes.OTHER_ERROR, - details: ErrorDetails.INTERSTITIAL_ASSET_ITEM_ERROR, - error - }; - const assetListIndex = interstitial.findAssetIndex(assetItem); - this.handleAssetItemError(errorData, interstitial, scheduleIndex, assetListIndex, error.message); - return; - } - } - this.transferMediaTo(player, media); - } - handleAssetItemError(data, interstitial, scheduleIndex, assetListIndex, errorMessage) { - if (data.details === ErrorDetails.BUFFER_STALLED_ERROR) { - return; - } - const assetItem = interstitial.assetList[assetListIndex] || null; - let player = null; - if (assetItem) { - const playerIndex = this.getAssetPlayerQueueIndex(assetItem.identifier); - player = this.playerQueue[playerIndex] || null; - } - const items = this.schedule.items; - const interstitialAssetError = _extends({}, data, { - fatal: false, - errorAction: createDoNothingErrorAction(true), - asset: assetItem, - assetListIndex, - event: interstitial, - schedule: items, - scheduleIndex, - player - }); - this.warn(`Asset item error: ${data.error}`); - this.hls.trigger(Events.INTERSTITIAL_ASSET_ERROR, interstitialAssetError); - if (!data.fatal) { - return; - } - const error = new Error(errorMessage); - if (assetItem) { - if (this.playingAsset !== assetItem) { - this.clearAssetPlayer(assetItem.identifier); - } - assetItem.error = error; - } - - // If all assets in interstitial fail, mark the interstitial with an error - if (!interstitial.assetList.some(asset => !asset.error)) { - interstitial.error = error; - } else if (interstitial.appendInPlace) { - // Skip entire interstitial since moving up subsequent assets is error prone - interstitial.error = error; - } - this.primaryFallback(interstitial); - } - primaryFallback(interstitial) { - // Fallback to Primary by on current or future events by updating schedule to skip errored interstitials/assets - const flushStart = interstitial.timelineStart; - const playingItem = this.playingItem || this.waitingItem; - // Update schedule now that interstitial/assets are flagged with `error` for fallback - this.updateSchedule(); - if (playingItem) { - if (interstitial.appendInPlace) { - interstitial.appendInPlace = false; - this.attachPrimary(flushStart, null); - this.flushFrontBuffer(flushStart); - } - let timelinePos = this.timelinePos; - if (timelinePos === -1) { - timelinePos = this.hls.startPosition; - } - const newPlayingItem = this.updateItem(playingItem, timelinePos); - if (!this.itemsMatch(playingItem, newPlayingItem)) { - const scheduleIndex = this.schedule.findItemIndexAtTime(timelinePos); - this.setSchedulePosition(scheduleIndex); - } - } else { - this.checkStart(); - } - } - - // Asset List loading - onAssetListLoaded(event, data) { - var _this$bufferingItem2; - const interstitial = data.event; - const interstitialId = interstitial.identifier; - const assets = data.assetListResponse.ASSETS; - if (!this.schedule.hasEvent(interstitialId)) { - // Interstitial with id was removed - return; - } - const eventStart = interstitial.timelineStart; - let sumDuration = 0; - assets.forEach((asset, assetListIndex) => { - const duration = parseFloat(asset.DURATION); - this.createAsset(interstitial, assetListIndex, sumDuration, eventStart + sumDuration, duration, asset.URI); - sumDuration += duration; - }); - interstitial.duration = sumDuration; - const waitingItem = this.waitingItem; - const waitingForItem = (waitingItem == null ? void 0 : waitingItem.event.identifier) === interstitialId; - - // Update schedule now that asset.DURATION(s) are parsed - this.updateSchedule(); - const bufferingEvent = (_this$bufferingItem2 = this.bufferingItem) == null ? void 0 : _this$bufferingItem2.event; - - // If buffer reached Interstitial, start buffering first asset - if (waitingForItem) { - var _this$schedule$items5; - // Advance schedule when waiting for asset list data to play - const scheduleIndex = this.schedule.findEventIndex(interstitialId); - const item = (_this$schedule$items5 = this.schedule.items) == null ? void 0 : _this$schedule$items5[scheduleIndex]; - if (item) { - this.setBufferingItem(item); - } - this.setSchedulePosition(scheduleIndex); - } else if ((bufferingEvent == null ? void 0 : bufferingEvent.identifier) === interstitialId && bufferingEvent.appendInPlace) { - // If buffering (but not playback) has reached this item transfer media-source - const assetItem = interstitial.assetList[0]; - const player = this.getAssetPlayer(assetItem.identifier); - const media = this.primaryMedia; - if (assetItem && player && media) { - this.bufferAssetPlayer(player, media); - } - } - } - onError(event, data) { - switch (data.details) { - case ErrorDetails.ASSET_LIST_PARSING_ERROR: - case ErrorDetails.ASSET_LIST_LOAD_ERROR: - case ErrorDetails.ASSET_LIST_LOAD_TIMEOUT: - { - const interstitial = data.interstitial; - if (interstitial) { - this.primaryFallback(interstitial); - } - } - } - } -} - -const AGE_HEADER_LINE_REGEX = /^age:\s*[\d.]+\s*$/im; -class XhrLoader { - constructor(config) { - this.xhrSetup = void 0; - this.requestTimeout = void 0; - this.retryTimeout = void 0; - this.retryDelay = void 0; - this.config = null; - this.callbacks = null; - this.context = null; - this.loader = null; - this.stats = void 0; - this.xhrSetup = config ? config.xhrSetup || null : null; - this.stats = new LoadStats(); - this.retryDelay = 0; - } - destroy() { - this.callbacks = null; - this.abortInternal(); - this.loader = null; - this.config = null; - this.context = null; - this.xhrSetup = null; - } - abortInternal() { - const loader = this.loader; - self.clearTimeout(this.requestTimeout); - self.clearTimeout(this.retryTimeout); - if (loader) { - loader.onreadystatechange = null; - loader.onprogress = null; - if (loader.readyState !== 4) { - this.stats.aborted = true; - loader.abort(); - } - } - } - abort() { - var _this$callbacks; - this.abortInternal(); - if ((_this$callbacks = this.callbacks) != null && _this$callbacks.onAbort) { - this.callbacks.onAbort(this.stats, this.context, this.loader); - } - } - load(context, config, callbacks) { - if (this.stats.loading.start) { - throw new Error('Loader can only be used once.'); - } - this.stats.loading.start = self.performance.now(); - this.context = context; - this.config = config; - this.callbacks = callbacks; - this.loadInternal(); - } - loadInternal() { - const { - config, - context - } = this; - if (!config || !context) { - return; - } - const xhr = this.loader = new self.XMLHttpRequest(); - const stats = this.stats; - stats.loading.first = 0; - stats.loaded = 0; - stats.aborted = false; - const xhrSetup = this.xhrSetup; - if (xhrSetup) { - Promise.resolve().then(() => { - if (this.loader !== xhr || this.stats.aborted) return; - return xhrSetup(xhr, context.url); - }).catch(error => { - if (this.loader !== xhr || this.stats.aborted) return; - xhr.open('GET', context.url, true); - return xhrSetup(xhr, context.url); - }).then(() => { - if (this.loader !== xhr || this.stats.aborted) return; - this.openAndSendXhr(xhr, context, config); - }).catch(error => { - // IE11 throws an exception on xhr.open if attempting to access an HTTP resource over HTTPS - this.callbacks.onError({ - code: xhr.status, - text: error.message - }, context, xhr, stats); - return; - }); - } else { - this.openAndSendXhr(xhr, context, config); - } - } - openAndSendXhr(xhr, context, config) { - if (!xhr.readyState) { - xhr.open('GET', context.url, true); - } - const headers = context.headers; - const { - maxTimeToFirstByteMs, - maxLoadTimeMs - } = config.loadPolicy; - if (headers) { - for (const header in headers) { - xhr.setRequestHeader(header, headers[header]); - } - } - if (context.rangeEnd) { - xhr.setRequestHeader('Range', 'bytes=' + context.rangeStart + '-' + (context.rangeEnd - 1)); - } - xhr.onreadystatechange = this.readystatechange.bind(this); - xhr.onprogress = this.loadprogress.bind(this); - xhr.responseType = context.responseType; - // setup timeout before we perform request - self.clearTimeout(this.requestTimeout); - config.timeout = maxTimeToFirstByteMs && isFiniteNumber(maxTimeToFirstByteMs) ? maxTimeToFirstByteMs : maxLoadTimeMs; - this.requestTimeout = self.setTimeout(this.loadtimeout.bind(this), config.timeout); - xhr.send(); - } - readystatechange() { - const { - context, - loader: xhr, - stats - } = this; - if (!context || !xhr) { - return; - } - const readyState = xhr.readyState; - const config = this.config; - - // don't proceed if xhr has been aborted - if (stats.aborted) { - return; - } - - // >= HEADERS_RECEIVED - if (readyState >= 2) { - if (stats.loading.first === 0) { - stats.loading.first = Math.max(self.performance.now(), stats.loading.start); - // readyState >= 2 AND readyState !==4 (readyState = HEADERS_RECEIVED || LOADING) rearm timeout as xhr not finished yet - if (config.timeout !== config.loadPolicy.maxLoadTimeMs) { - self.clearTimeout(this.requestTimeout); - config.timeout = config.loadPolicy.maxLoadTimeMs; - this.requestTimeout = self.setTimeout(this.loadtimeout.bind(this), config.loadPolicy.maxLoadTimeMs - (stats.loading.first - stats.loading.start)); - } - } - if (readyState === 4) { - self.clearTimeout(this.requestTimeout); - xhr.onreadystatechange = null; - xhr.onprogress = null; - const status = xhr.status; - // http status between 200 to 299 are all successful - const useResponse = xhr.responseType !== 'text'; - if (status >= 200 && status < 300 && (useResponse && xhr.response || xhr.responseText !== null)) { - stats.loading.end = Math.max(self.performance.now(), stats.loading.first); - const data = useResponse ? xhr.response : xhr.responseText; - const len = xhr.responseType === 'arraybuffer' ? data.byteLength : data.length; - stats.loaded = stats.total = len; - stats.bwEstimate = stats.total * 8000 / (stats.loading.end - stats.loading.first); - if (!this.callbacks) { - return; - } - const onProgress = this.callbacks.onProgress; - if (onProgress) { - onProgress(stats, context, data, xhr); - } - if (!this.callbacks) { - return; - } - const response = { - url: xhr.responseURL, - data: data, - code: status - }; - this.callbacks.onSuccess(response, stats, context, xhr); - } else { - const retryConfig = config.loadPolicy.errorRetry; - const retryCount = stats.retry; - // if max nb of retries reached or if http status between 400 and 499 (such error cannot be recovered, retrying is useless), return error - const response = { - url: context.url, - data: undefined, - code: status - }; - if (shouldRetry(retryConfig, retryCount, false, response)) { - this.retry(retryConfig); - } else { - logger.error(`${status} while loading ${context.url}`); - this.callbacks.onError({ - code: status, - text: xhr.statusText - }, context, xhr, stats); - } - } - } - } - } - loadtimeout() { - if (!this.config) return; - const retryConfig = this.config.loadPolicy.timeoutRetry; - const retryCount = this.stats.retry; - if (shouldRetry(retryConfig, retryCount, true)) { - this.retry(retryConfig); - } else { - var _this$context; - logger.warn(`timeout while loading ${(_this$context = this.context) == null ? void 0 : _this$context.url}`); - const callbacks = this.callbacks; - if (callbacks) { - this.abortInternal(); - callbacks.onTimeout(this.stats, this.context, this.loader); - } - } - } - retry(retryConfig) { - const { - context, - stats - } = this; - this.retryDelay = getRetryDelay(retryConfig, stats.retry); - stats.retry++; - logger.warn(`${status ? 'HTTP Status ' + status : 'Timeout'} while loading ${context == null ? void 0 : context.url}, retrying ${stats.retry}/${retryConfig.maxNumRetry} in ${this.retryDelay}ms`); - // abort and reset internal state - this.abortInternal(); - this.loader = null; - // schedule retry - self.clearTimeout(this.retryTimeout); - this.retryTimeout = self.setTimeout(this.loadInternal.bind(this), this.retryDelay); - } - loadprogress(event) { - const stats = this.stats; - stats.loaded = event.loaded; - if (event.lengthComputable) { - stats.total = event.total; - } - } - getCacheAge() { - let result = null; - if (this.loader && AGE_HEADER_LINE_REGEX.test(this.loader.getAllResponseHeaders())) { - const ageHeader = this.loader.getResponseHeader('age'); - result = ageHeader ? parseFloat(ageHeader) : null; - } - return result; - } - getResponseHeader(name) { - if (this.loader && new RegExp(`^${name}:\\s*[\\d.]+\\s*$`, 'im').test(this.loader.getAllResponseHeaders())) { - return this.loader.getResponseHeader(name); - } - return null; - } -} - -function fetchSupported() { - if ( - // @ts-ignore - self.fetch && self.AbortController && self.ReadableStream && self.Request) { - try { - new self.ReadableStream({}); // eslint-disable-line no-new - return true; - } catch (e) { - /* noop */ - } - } - return false; -} -const BYTERANGE = /(\d+)-(\d+)\/(\d+)/; -class FetchLoader { - constructor(config) { - this.fetchSetup = void 0; - this.requestTimeout = void 0; - this.request = null; - this.response = null; - this.controller = void 0; - this.context = null; - this.config = null; - this.callbacks = null; - this.stats = void 0; - this.loader = null; - this.fetchSetup = config.fetchSetup || getRequest; - this.controller = new self.AbortController(); - this.stats = new LoadStats(); - } - destroy() { - this.loader = this.callbacks = this.context = this.config = this.request = null; - this.abortInternal(); - this.response = null; - // @ts-ignore - this.fetchSetup = this.controller = this.stats = null; - } - abortInternal() { - if (this.controller && !this.stats.loading.end) { - this.stats.aborted = true; - this.controller.abort(); - } - } - abort() { - var _this$callbacks; - this.abortInternal(); - if ((_this$callbacks = this.callbacks) != null && _this$callbacks.onAbort) { - this.callbacks.onAbort(this.stats, this.context, this.response); - } - } - load(context, config, callbacks) { - const stats = this.stats; - if (stats.loading.start) { - throw new Error('Loader can only be used once.'); - } - stats.loading.start = self.performance.now(); - const initParams = getRequestParameters(context, this.controller.signal); - const onProgress = callbacks.onProgress; - const isArrayBuffer = context.responseType === 'arraybuffer'; - const LENGTH = isArrayBuffer ? 'byteLength' : 'length'; - const { - maxTimeToFirstByteMs, - maxLoadTimeMs - } = config.loadPolicy; - this.context = context; - this.config = config; - this.callbacks = callbacks; - this.request = this.fetchSetup(context, initParams); - self.clearTimeout(this.requestTimeout); - config.timeout = maxTimeToFirstByteMs && isFiniteNumber(maxTimeToFirstByteMs) ? maxTimeToFirstByteMs : maxLoadTimeMs; - this.requestTimeout = self.setTimeout(() => { - this.abortInternal(); - callbacks.onTimeout(stats, context, this.response); - }, config.timeout); - const fetchPromise = isPromise(this.request) ? this.request.then(self.fetch) : self.fetch(this.request); - fetchPromise.then(response => { - this.response = this.loader = response; - const first = Math.max(self.performance.now(), stats.loading.start); - self.clearTimeout(this.requestTimeout); - config.timeout = maxLoadTimeMs; - this.requestTimeout = self.setTimeout(() => { - this.abortInternal(); - callbacks.onTimeout(stats, context, this.response); - }, maxLoadTimeMs - (first - stats.loading.start)); - if (!response.ok) { - const { - status, - statusText - } = response; - throw new FetchError(statusText || 'fetch, bad network response', status, response); - } - stats.loading.first = first; - stats.total = getContentLength(response.headers) || stats.total; - if (onProgress && isFiniteNumber(config.highWaterMark)) { - return this.loadProgressively(response, stats, context, config.highWaterMark, onProgress); - } - if (isArrayBuffer) { - return response.arrayBuffer(); - } - if (context.responseType === 'json') { - return response.json(); - } - return response.text(); - }).then(responseData => { - const response = this.response; - if (!response) { - throw new Error('loader destroyed'); - } - self.clearTimeout(this.requestTimeout); - stats.loading.end = Math.max(self.performance.now(), stats.loading.first); - const total = responseData[LENGTH]; - if (total) { - stats.loaded = stats.total = total; - } - const loaderResponse = { - url: response.url, - data: responseData, - code: response.status - }; - if (onProgress && !isFiniteNumber(config.highWaterMark)) { - onProgress(stats, context, responseData, response); - } - callbacks.onSuccess(loaderResponse, stats, context, response); - }).catch(error => { - self.clearTimeout(this.requestTimeout); - if (stats.aborted) { - return; - } - // CORS errors result in an undefined code. Set it to 0 here to align with XHR's behavior - // when destroying, 'error' itself can be undefined - const code = !error ? 0 : error.code || 0; - const text = !error ? null : error.message; - callbacks.onError({ - code, - text - }, context, error ? error.details : null, stats); - }); - } - getCacheAge() { - let result = null; - if (this.response) { - const ageHeader = this.response.headers.get('age'); - result = ageHeader ? parseFloat(ageHeader) : null; - } - return result; - } - getResponseHeader(name) { - return this.response ? this.response.headers.get(name) : null; - } - loadProgressively(response, stats, context, highWaterMark = 0, onProgress) { - const chunkCache = new ChunkCache(); - const reader = response.body.getReader(); - const pump = () => { - return reader.read().then(data => { - if (data.done) { - if (chunkCache.dataLength) { - onProgress(stats, context, chunkCache.flush(), response); - } - return Promise.resolve(new ArrayBuffer(0)); - } - const chunk = data.value; - const len = chunk.length; - stats.loaded += len; - if (len < highWaterMark || chunkCache.dataLength) { - // The current chunk is too small to to be emitted or the cache already has data - // Push it to the cache - chunkCache.push(chunk); - if (chunkCache.dataLength >= highWaterMark) { - // flush in order to join the typed arrays - onProgress(stats, context, chunkCache.flush(), response); - } - } else { - // If there's nothing cached already, and the chache is large enough - // just emit the progress event - onProgress(stats, context, chunk, response); - } - return pump(); - }).catch(() => { - /* aborted */ - return Promise.reject(); - }); - }; - return pump(); - } -} -function getRequestParameters(context, signal) { - const initParams = { - method: 'GET', - mode: 'cors', - credentials: 'same-origin', - signal, - headers: new self.Headers(_extends({}, context.headers)) - }; - if (context.rangeEnd) { - initParams.headers.set('Range', 'bytes=' + context.rangeStart + '-' + String(context.rangeEnd - 1)); - } - return initParams; -} -function getByteRangeLength(byteRangeHeader) { - const result = BYTERANGE.exec(byteRangeHeader); - if (result) { - return parseInt(result[2]) - parseInt(result[1]) + 1; - } -} -function getContentLength(headers) { - const contentRange = headers.get('Content-Range'); - if (contentRange) { - const byteRangeLength = getByteRangeLength(contentRange); - if (isFiniteNumber(byteRangeLength)) { - return byteRangeLength; - } - } - const contentLength = headers.get('Content-Length'); - if (contentLength) { - return parseInt(contentLength); - } -} -function getRequest(context, initParams) { - return new self.Request(context.url, initParams); -} -class FetchError extends Error { - constructor(message, code, details) { - super(message); - this.code = void 0; - this.details = void 0; - this.code = code; - this.details = details; - } -} - -const WHITESPACE_CHAR = /\s/; -const Cues = { - newCue(track, startTime, endTime, captionScreen) { - const result = []; - let row; - // the type data states this is VTTCue, but it can potentially be a TextTrackCue on old browsers - let cue; - let indenting; - let indent; - let text; - const Cue = self.VTTCue || self.TextTrackCue; - for (let r = 0; r < captionScreen.rows.length; r++) { - row = captionScreen.rows[r]; - indenting = true; - indent = 0; - text = ''; - if (!row.isEmpty()) { - var _track$cues; - for (let c = 0; c < row.chars.length; c++) { - if (WHITESPACE_CHAR.test(row.chars[c].uchar) && indenting) { - indent++; - } else { - text += row.chars[c].uchar; - indenting = false; - } - } - // To be used for cleaning-up orphaned roll-up captions - row.cueStartTime = startTime; - - // Give a slight bump to the endTime if it's equal to startTime to avoid a SyntaxError in IE - if (startTime === endTime) { - endTime += 0.0001; - } - if (indent >= 16) { - indent--; - } else { - indent++; - } - const cueText = fixLineBreaks(text.trim()); - const id = generateCueId(startTime, endTime, cueText); - - // If this cue already exists in the track do not push it - if (!(track != null && (_track$cues = track.cues) != null && _track$cues.getCueById(id))) { - cue = new Cue(startTime, endTime, cueText); - cue.id = id; - cue.line = r + 1; - cue.align = 'left'; - // Clamp the position between 10 and 80 percent (CEA-608 PAC indent code) - // https://dvcs.w3.org/hg/text-tracks/raw-file/default/608toVTT/608toVTT.html#positioning-in-cea-608 - // Firefox throws an exception and captions break with out of bounds 0-100 values - cue.position = 10 + Math.min(80, Math.floor(indent * 8 / 32) * 10); - result.push(cue); - } - } - } - if (track && result.length) { - // Sort bottom cues in reverse order so that they render in line order when overlapping in Chrome - result.sort((cueA, cueB) => { - if (cueA.line === 'auto' || cueB.line === 'auto') { - return 0; - } - if (cueA.line > 8 && cueB.line > 8) { - return cueB.line - cueA.line; - } - return cueA.line - cueB.line; - }); - result.forEach(cue => addCueToTrack(track, cue)); - } - return result; - } -}; - -/** - * @deprecated use fragLoadPolicy.default - */ - -/** - * @deprecated use manifestLoadPolicy.default and playlistLoadPolicy.default - */ - -const defaultLoadPolicy = { - maxTimeToFirstByteMs: 8000, - maxLoadTimeMs: 20000, - timeoutRetry: null, - errorRetry: null -}; - -/** - * @ignore - * If possible, keep hlsDefaultConfig shallow - * It is cloned whenever a new Hls instance is created, by keeping the config - * shallow the properties are cloned, and we don't end up manipulating the default - */ -const hlsDefaultConfig = _objectSpread2(_objectSpread2({ - autoStartLoad: true, - // used by stream-controller - startPosition: -1, - // used by stream-controller - defaultAudioCodec: undefined, - // used by stream-controller - debug: false, - // used by logger - capLevelOnFPSDrop: false, - // used by fps-controller - capLevelToPlayerSize: false, - // used by cap-level-controller - ignoreDevicePixelRatio: false, - // used by cap-level-controller - preferManagedMediaSource: true, - initialLiveManifestSize: 1, - // used by stream-controller - maxBufferLength: 30, - // used by stream-controller - backBufferLength: Infinity, - // used by buffer-controller - frontBufferFlushThreshold: Infinity, - maxBufferSize: 60 * 1000 * 1000, - // used by stream-controller - maxBufferHole: 0.1, - // used by stream-controller - highBufferWatchdogPeriod: 2, - // used by stream-controller - nudgeOffset: 0.1, - // used by stream-controller - nudgeMaxRetry: 3, - // used by stream-controller - maxFragLookUpTolerance: 0.25, - // used by stream-controller - liveSyncDurationCount: 3, - // used by latency-controller - liveSyncOnStallIncrease: 1, - // used by latency-controller - liveMaxLatencyDurationCount: Infinity, - // used by latency-controller - liveSyncDuration: undefined, - // used by latency-controller - liveMaxLatencyDuration: undefined, - // used by latency-controller - maxLiveSyncPlaybackRate: 1, - // used by latency-controller - liveDurationInfinity: false, - // used by buffer-controller - /** - * @deprecated use backBufferLength - */ - liveBackBufferLength: null, - // used by buffer-controller - maxMaxBufferLength: 600, - // used by stream-controller - enableWorker: true, - // used by transmuxer - workerPath: null, - // used by transmuxer - enableSoftwareAES: true, - // used by decrypter - startLevel: undefined, - // used by level-controller - startFragPrefetch: false, - // used by stream-controller - fpsDroppedMonitoringPeriod: 5000, - // used by fps-controller - fpsDroppedMonitoringThreshold: 0.2, - // used by fps-controller - appendErrorMaxRetry: 3, - // used by buffer-controller - loader: XhrLoader, - // loader: FetchLoader, - fLoader: undefined, - // used by fragment-loader - pLoader: undefined, - // used by playlist-loader - xhrSetup: undefined, - // used by xhr-loader - licenseXhrSetup: undefined, - // used by eme-controller - licenseResponseCallback: undefined, - // used by eme-controller - abrController: AbrController, - bufferController: BufferController, - capLevelController: CapLevelController, - errorController: ErrorController, - fpsController: FPSController, - stretchShortVideoTrack: false, - // used by mp4-remuxer - maxAudioFramesDrift: 1, - // used by mp4-remuxer - forceKeyFrameOnDiscontinuity: true, - // used by ts-demuxer - abrEwmaFastLive: 3, - // used by abr-controller - abrEwmaSlowLive: 9, - // used by abr-controller - abrEwmaFastVoD: 3, - // used by abr-controller - abrEwmaSlowVoD: 9, - // used by abr-controller - abrEwmaDefaultEstimate: 5e5, - // 500 kbps // used by abr-controller - abrEwmaDefaultEstimateMax: 5e6, - // 5 mbps - abrBandWidthFactor: 0.95, - // used by abr-controller - abrBandWidthUpFactor: 0.7, - // used by abr-controller - abrMaxWithRealBitrate: false, - // used by abr-controller - maxStarvationDelay: 4, - // used by abr-controller - maxLoadingDelay: 4, - // used by abr-controller - 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, - progressive: false, - lowLatencyMode: true, - cmcd: undefined, - enableDateRangeMetadataCues: true, - enableEmsgMetadataCues: true, - enableEmsgKLVMetadata: false, - enableID3MetadataCues: true, - enableInterstitialPlayback: true, - interstitialAppendInPlace: true, - interstitialLiveLookAhead: 10, - useMediaCapabilities: true, - certLoadPolicy: { - default: defaultLoadPolicy - }, - keyLoadPolicy: { - default: { - maxTimeToFirstByteMs: 8000, - maxLoadTimeMs: 20000, - timeoutRetry: { - maxNumRetry: 1, - retryDelayMs: 1000, - maxRetryDelayMs: 20000, - backoff: 'linear' - }, - errorRetry: { - maxNumRetry: 8, - retryDelayMs: 1000, - maxRetryDelayMs: 20000, - backoff: 'linear' - } - } - }, - manifestLoadPolicy: { - default: { - maxTimeToFirstByteMs: Infinity, - maxLoadTimeMs: 20000, - timeoutRetry: { - maxNumRetry: 2, - retryDelayMs: 0, - maxRetryDelayMs: 0 - }, - errorRetry: { - maxNumRetry: 1, - retryDelayMs: 1000, - maxRetryDelayMs: 8000 - } - } - }, - playlistLoadPolicy: { - default: { - maxTimeToFirstByteMs: 10000, - maxLoadTimeMs: 20000, - timeoutRetry: { - maxNumRetry: 2, - retryDelayMs: 0, - maxRetryDelayMs: 0 - }, - errorRetry: { - maxNumRetry: 2, - retryDelayMs: 1000, - maxRetryDelayMs: 8000 - } - } - }, - fragLoadPolicy: { - default: { - maxTimeToFirstByteMs: 10000, - maxLoadTimeMs: 120000, - timeoutRetry: { - maxNumRetry: 4, - retryDelayMs: 0, - maxRetryDelayMs: 0 - }, - errorRetry: { - maxNumRetry: 6, - retryDelayMs: 1000, - maxRetryDelayMs: 8000 - } - } - }, - steeringManifestLoadPolicy: { - default: { - maxTimeToFirstByteMs: 10000, - maxLoadTimeMs: 20000, - timeoutRetry: { - maxNumRetry: 2, - retryDelayMs: 0, - maxRetryDelayMs: 0 - }, - errorRetry: { - maxNumRetry: 1, - retryDelayMs: 1000, - maxRetryDelayMs: 8000 - } - } - }, - interstitialAssetListLoadPolicy: { - default: { - maxTimeToFirstByteMs: 10000, - maxLoadTimeMs: 30000, - timeoutRetry: { - maxNumRetry: 0, - retryDelayMs: 0, - maxRetryDelayMs: 0 - }, - errorRetry: { - maxNumRetry: 0, - retryDelayMs: 1000, - maxRetryDelayMs: 8000 - } - } - }, - // These default settings are deprecated in favor of the above policies - // and are maintained for backwards compatibility - manifestLoadingTimeOut: 10000, - manifestLoadingMaxRetry: 1, - manifestLoadingRetryDelay: 1000, - manifestLoadingMaxRetryTimeout: 64000, - levelLoadingTimeOut: 10000, - levelLoadingMaxRetry: 4, - levelLoadingRetryDelay: 1000, - levelLoadingMaxRetryTimeout: 64000, - fragLoadingTimeOut: 20000, - fragLoadingMaxRetry: 6, - fragLoadingRetryDelay: 1000, - fragLoadingMaxRetryTimeout: 64000 -}, timelineConfig()), {}, { - subtitleStreamController: SubtitleStreamController , - subtitleTrackController: SubtitleTrackController , - timelineController: TimelineController , - audioStreamController: AudioStreamController , - audioTrackController: AudioTrackController , - emeController: EMEController , - cmcdController: CMCDController , - contentSteeringController: ContentSteeringController , - interstitialsController: InterstitialsController -}); -function timelineConfig() { - return { - cueHandler: Cues, - // used by timeline-controller - enableWebVTT: true, - // used by timeline-controller - enableIMSC1: true, - // used by timeline-controller - enableCEA708Captions: true, - // used by timeline-controller - captionsTextTrack1Label: 'English', - // used by timeline-controller - captionsTextTrack1LanguageCode: 'en', - // used by timeline-controller - captionsTextTrack2Label: 'Spanish', - // used by timeline-controller - captionsTextTrack2LanguageCode: 'es', - // used by timeline-controller - captionsTextTrack3Label: 'Unknown CC', - // used by timeline-controller - captionsTextTrack3LanguageCode: '', - // used by timeline-controller - captionsTextTrack4Label: 'Unknown CC', - // used by timeline-controller - captionsTextTrack4LanguageCode: '', - // used by timeline-controller - renderTextTracksNatively: true - }; -} - -/** - * @ignore - */ -function mergeConfig(defaultConfig, userConfig, logger) { - if ((userConfig.liveSyncDurationCount || userConfig.liveMaxLatencyDurationCount) && (userConfig.liveSyncDuration || userConfig.liveMaxLatencyDuration)) { - throw new Error("Illegal hls.js config: don't mix up liveSyncDurationCount/liveMaxLatencyDurationCount and liveSyncDuration/liveMaxLatencyDuration"); - } - if (userConfig.liveMaxLatencyDurationCount !== undefined && (userConfig.liveSyncDurationCount === undefined || userConfig.liveMaxLatencyDurationCount <= userConfig.liveSyncDurationCount)) { - throw new Error('Illegal hls.js config: "liveMaxLatencyDurationCount" must be greater than "liveSyncDurationCount"'); - } - if (userConfig.liveMaxLatencyDuration !== undefined && (userConfig.liveSyncDuration === undefined || userConfig.liveMaxLatencyDuration <= userConfig.liveSyncDuration)) { - throw new Error('Illegal hls.js config: "liveMaxLatencyDuration" must be greater than "liveSyncDuration"'); - } - const defaultsCopy = deepCpy(defaultConfig); - - // Backwards compatibility with deprecated config values - const deprecatedSettingTypes = ['manifest', 'level', 'frag']; - const deprecatedSettings = ['TimeOut', 'MaxRetry', 'RetryDelay', 'MaxRetryTimeout']; - deprecatedSettingTypes.forEach(type => { - const policyName = `${type === 'level' ? 'playlist' : type}LoadPolicy`; - const policyNotSet = userConfig[policyName] === undefined; - const report = []; - deprecatedSettings.forEach(setting => { - const deprecatedSetting = `${type}Loading${setting}`; - const value = userConfig[deprecatedSetting]; - if (value !== undefined && policyNotSet) { - report.push(deprecatedSetting); - const settings = defaultsCopy[policyName].default; - userConfig[policyName] = { - default: settings - }; - switch (setting) { - case 'TimeOut': - settings.maxLoadTimeMs = value; - settings.maxTimeToFirstByteMs = value; - break; - case 'MaxRetry': - settings.errorRetry.maxNumRetry = value; - settings.timeoutRetry.maxNumRetry = value; - break; - case 'RetryDelay': - settings.errorRetry.retryDelayMs = value; - settings.timeoutRetry.retryDelayMs = value; - break; - case 'MaxRetryTimeout': - settings.errorRetry.maxRetryDelayMs = value; - settings.timeoutRetry.maxRetryDelayMs = value; - break; - } - } - }); - if (report.length) { - logger.warn(`hls.js config: "${report.join('", "')}" setting(s) are deprecated, use "${policyName}": ${JSON.stringify(userConfig[policyName])}`); - } - }); - return _objectSpread2(_objectSpread2({}, defaultsCopy), userConfig); -} -function deepCpy(obj) { - if (obj && typeof obj === 'object') { - if (Array.isArray(obj)) { - return obj.map(deepCpy); - } - return Object.keys(obj).reduce((result, key) => { - result[key] = deepCpy(obj[key]); - return result; - }, {}); - } - return obj; -} - -/** - * @ignore - */ -function enableStreamingMode(config, logger) { - const currentLoader = config.loader; - if (currentLoader !== FetchLoader && currentLoader !== XhrLoader) { - // If a developer has configured their own loader, respect that choice - logger.log('[config]: Custom loader detected, cannot enable progressive streaming'); - config.progressive = false; - } else { - const canStreamProgressively = fetchSupported(); - if (canStreamProgressively) { - config.loader = FetchLoader; - config.progressive = true; - config.enableSoftwareAES = true; - logger.log('[config]: Progressive streaming enabled, using FetchLoader'); - } - } -} - -/** - * The `Hls` class is the core of the HLS.js library used to instantiate player instances. - * @public - */ -class Hls { - /** - * Get the video-dev/hls.js package version. - */ - static get version() { - return version; - } - - /** - * Check if the required MediaSource Extensions are available. - */ - static isMSESupported() { - return isMSESupported(); - } - - /** - * Check if MediaSource Extensions are available and isTypeSupported checks pass for any baseline codecs. - */ - static isSupported() { - return isSupported(); - } - - /** - * Get the MediaSource global used for MSE playback (ManagedMediaSource, MediaSource, or WebKitMediaSource). - */ - static getMediaSource() { - return getMediaSource(); - } - static get Events() { - return Events; - } - static get MetadataSchema() { - return MetadataSchema; - } - static get ErrorTypes() { - return ErrorTypes; - } - static get ErrorDetails() { - return ErrorDetails; - } - - /** - * Get the default configuration applied to new instances. - */ - static get DefaultConfig() { - if (!Hls.defaultConfig) { - return hlsDefaultConfig; - } - return Hls.defaultConfig; - } - - /** - * Replace the default configuration applied to new instances. - */ - static set DefaultConfig(defaultConfig) { - Hls.defaultConfig = defaultConfig; - } - - /** - * Creates an instance of an HLS client that can attach to exactly one `HTMLMediaElement`. - * @param userConfig - Configuration options applied over `Hls.DefaultConfig` - */ - constructor(userConfig = {}) { - /** - * The runtime configuration used by the player. At instantiation this is combination of `hls.userConfig` merged over `Hls.DefaultConfig`. - */ - this.config = void 0; - /** - * The configuration object provided on player instantiation. - */ - this.userConfig = void 0; - /** - * The logger functions used by this player instance, configured on player instantiation. - */ - this.logger = void 0; - this.coreComponents = void 0; - this.networkControllers = void 0; - this._emitter = new EventEmitter(); - this._autoLevelCapping = -1; - this._maxHdcpLevel = null; - this.abrController = void 0; - this.bufferController = void 0; - this.capLevelController = void 0; - this.latencyController = void 0; - this.levelController = void 0; - this.streamController = void 0; - this.audioTrackController = void 0; - this.subtitleTrackController = void 0; - this.interstitialsController = void 0; - this.emeController = void 0; - this.cmcdController = void 0; - this._media = null; - this._url = null; - this.triggeringException = void 0; - this._sessionId = void 0; - const logger = this.logger = enableLogs(userConfig.debug || false, 'Hls instance', userConfig.assetPlayerId); - const config = this.config = mergeConfig(Hls.DefaultConfig, userConfig, logger); - this.userConfig = userConfig; - if (config.progressive) { - enableStreamingMode(config, logger); - } - - // core controllers and network loaders - const { - abrController: _AbrController, - bufferController: _BufferController, - capLevelController: _CapLevelController, - errorController: _ErrorController, - fpsController: _FpsController - } = config; - const errorController = new _ErrorController(this); - const abrController = this.abrController = new _AbrController(this); - // FragmentTracker must be defined before StreamController because the order of event handling is important - const fragmentTracker = new FragmentTracker(this); - const _InterstitialsController = config.interstitialsController; - const interstitialsController = _InterstitialsController ? this.interstitialsController = new _InterstitialsController(this, Hls) : null; - const bufferController = this.bufferController = new _BufferController(this, fragmentTracker); - const capLevelController = this.capLevelController = new _CapLevelController(this); - const fpsController = new _FpsController(this); - const playListLoader = new PlaylistLoader(this); - const _ContentSteeringController = config.contentSteeringController; - // Instantiate ConentSteeringController before LevelController to receive Multivariant Playlist events first - const contentSteering = _ContentSteeringController ? new _ContentSteeringController(this) : null; - const levelController = this.levelController = new LevelController(this, contentSteering); - const id3TrackController = new ID3TrackController(this); - const keyLoader = new KeyLoader(this.config); - const streamController = this.streamController = new StreamController(this, fragmentTracker, keyLoader); - - // Cap level controller uses streamController to flush the buffer - capLevelController.setStreamController(streamController); - // fpsController uses streamController to switch when frames are being dropped - fpsController.setStreamController(streamController); - const networkControllers = [playListLoader, levelController, streamController]; - if (interstitialsController) { - networkControllers.splice(1, 0, interstitialsController); - } - if (contentSteering) { - networkControllers.splice(1, 0, contentSteering); - } - this.networkControllers = networkControllers; - const coreComponents = [abrController, bufferController, capLevelController, fpsController, id3TrackController, fragmentTracker]; - this.audioTrackController = this.createController(config.audioTrackController, networkControllers); - const AudioStreamControllerClass = config.audioStreamController; - if (AudioStreamControllerClass) { - networkControllers.push(new AudioStreamControllerClass(this, fragmentTracker, keyLoader)); - } - // Instantiate subtitleTrackController before SubtitleStreamController to receive level events first - this.subtitleTrackController = this.createController(config.subtitleTrackController, networkControllers); - 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, coreComponents); - this.cmcdController = this.createController(config.cmcdController, coreComponents); - this.latencyController = this.createController(LatencyController, coreComponents); - this.coreComponents = coreComponents; - - // Error controller handles errors before and after all other controllers - // This listener will be invoked after all other controllers error listeners - networkControllers.push(errorController); - const onErrorOut = errorController.onErrorOut; - if (typeof onErrorOut === 'function') { - this.on(Events.ERROR, onErrorOut, errorController); - } - } - createController(ControllerClass, components) { - if (ControllerClass) { - const controllerInstance = new ControllerClass(this); - if (components) { - components.push(controllerInstance); - } - return controllerInstance; - } - return null; - } - - // Delegate the EventEmitter through the public API of Hls.js - on(event, listener, context = this) { - this._emitter.on(event, listener, context); - } - once(event, listener, context = this) { - this._emitter.once(event, listener, context); - } - removeAllListeners(event) { - this._emitter.removeAllListeners(event); - } - off(event, listener, context = this, once) { - this._emitter.off(event, listener, context, once); - } - listeners(event) { - return this._emitter.listeners(event); - } - emit(event, name, eventObject) { - return this._emitter.emit(event, name, eventObject); - } - trigger(event, eventObject) { - if (this.config.debug) { - return this.emit(event, event, eventObject); - } else { - try { - return this.emit(event, event, eventObject); - } catch (error) { - this.logger.error('An internal error happened while handling event ' + event + '. Error message: "' + error.message + '". Here is a stacktrace:', error); - // Prevent recursion in error event handlers that throw #5497 - if (!this.triggeringException) { - this.triggeringException = true; - const fatal = event === Events.ERROR; - this.trigger(Events.ERROR, { - type: ErrorTypes.OTHER_ERROR, - details: ErrorDetails.INTERNAL_EXCEPTION, - fatal, - event, - error - }); - this.triggeringException = false; - } - } - } - return false; - } - listenerCount(event) { - return this._emitter.listenerCount(event); - } - - /** - * Dispose of the instance - */ - destroy() { - this.logger.log('destroy'); - this.trigger(Events.DESTROYING, undefined); - this.detachMedia(); - this.removeAllListeners(); - this._autoLevelCapping = -1; - this._url = null; - this.networkControllers.forEach(component => component.destroy()); - this.networkControllers.length = 0; - this.coreComponents.forEach(component => component.destroy()); - this.coreComponents.length = 0; - // Remove any references that could be held in config options or callbacks - const config = this.config; - config.xhrSetup = config.fetchSetup = undefined; - // @ts-ignore - this.userConfig = null; - } - - /** - * Attaches Hls.js to a media element - */ - attachMedia(data) { - if (!data || 'media' in data && !data.media) { - const error = new Error(`attachMedia failed: invalid argument (${data})`); - this.trigger(Events.ERROR, { - type: ErrorTypes.OTHER_ERROR, - details: ErrorDetails.ATTACH_MEDIA_ERROR, - fatal: true, - error - }); - return; - } - this.logger.log(`attachMedia`); - const attachMediaSource = 'media' in data; - const media = attachMediaSource ? data.media : data; - const attachingData = attachMediaSource ? data : { - media - }; - this._media = media; - this.trigger(Events.MEDIA_ATTACHING, attachingData); - } - - /** - * Detach Hls.js from the media - */ - detachMedia() { - this.logger.log('detachMedia'); - this.trigger(Events.MEDIA_DETACHING, {}); - this._media = null; - } - - /** - * Detach HTMLMediaElement, MediaSource, and SourceBuffers without reset, for attaching to another instance - */ - transferMedia() { - this._media = null; - const transferMedia = this.bufferController.transferMedia(); - this.trigger(Events.MEDIA_DETACHING, { - transferMedia - }); - return transferMedia; - } - - /** - * Set the source URL. Can be relative or absolute. - */ - loadSource(url) { - this.stopLoad(); - const media = this.media; - const loadedSource = this._url; - const loadingSource = this._url = urlToolkitExports.buildAbsoluteURL(self.location.href, url, { - alwaysNormalize: true - }); - this._autoLevelCapping = -1; - this._maxHdcpLevel = null; - this.logger.log(`loadSource:${loadingSource}`); - if (media && loadedSource && (loadedSource !== loadingSource || this.bufferController.hasSourceTypes())) { - // Remove and re-create MediaSource - this.detachMedia(); - this.attachMedia(media); - } - // when attaching to a source URL, trigger a playlist load - this.trigger(Events.MANIFEST_LOADING, { - url: url - }); - } - - /** - * Gets the currently loaded URL - */ - get url() { - return this._url; - } - - /** - * Whether or not enough has been buffered to seek to start position or use `media.currentTime` to determine next load position - */ - get hasEnoughToStart() { - return this.streamController.hasEnoughToStart; - } - - /** - * Get the startPosition set on startLoad(position) or on autostart with config.startPosition - */ - get startPosition() { - return this.streamController.startPositionValue; - } - - /** - * Start loading data from the stream source. - * Depending on default config, client starts loading automatically when a source is set. - * - * @param startPosition - Set the start position to stream from. - * Defaults to -1 (None: starts from earliest point) - */ - startLoad(startPosition = -1, skipSeekToStartPosition) { - this.logger.log(`startLoad(${startPosition + (skipSeekToStartPosition ? ', <skip seek to start>' : '')})`); - this.resumeBuffering(); - this.networkControllers.forEach(controller => { - controller.startLoad(startPosition, skipSeekToStartPosition); - }); - } - - /** - * Stop loading of any stream data. - */ - stopLoad() { - this.logger.log('stopLoad'); - this.networkControllers.forEach(controller => { - controller.stopLoad(); - }); - } - - /** - * Returns state of fragment loading toggled by calling `pauseBuffering()` and `resumeBuffering()`. - */ - get bufferingEnabled() { - return this.streamController.bufferingEnabled; - } - - /** - * Resumes stream controller segment loading after `pauseBuffering` has been called. - */ - resumeBuffering() { - if (!this.bufferingEnabled) { - this.logger.log(`resume buffering`); - this.networkControllers.forEach(controller => { - if (controller.resumeBuffering) { - controller.resumeBuffering(); - } - }); - } - } - - /** - * Prevents stream controller from loading new segments until `resumeBuffering` is called. - * This allows for media buffering to be paused without interupting playlist loading. - */ - pauseBuffering() { - if (this.bufferingEnabled) { - this.logger.log(`pause buffering`); - this.networkControllers.forEach(controller => { - if (controller.pauseBuffering) { - controller.pauseBuffering(); - } - }); - } - } - - /** - * Swap through possible audio codecs in the stream (for example to switch from stereo to 5.1) - */ - swapAudioCodec() { - this.logger.log('swapAudioCodec'); - this.streamController.swapAudioCodec(); - } - - /** - * When the media-element fails, this allows to detach and then re-attach it - * as one call (convenience method). - * - * Automatic recovery of media-errors by this process is configurable. - */ - recoverMediaError() { - this.logger.log('recoverMediaError'); - const media = this._media; - const time = media == null ? void 0 : media.currentTime; - this.detachMedia(); - if (media) { - this.attachMedia(media); - if (time) { - this.startLoad(time); - } - } - } - removeLevel(levelIndex) { - this.levelController.removeLevel(levelIndex); - } - - /** - * @returns a UUID for this player instance - */ - get sessionId() { - let _sessionId = this._sessionId; - if (!_sessionId) { - _sessionId = this._sessionId = uuid(); - } - return _sessionId; - } - - /** - * @returns an array of levels (variants) sorted by HDCP-LEVEL, RESOLUTION (height), FRAME-RATE, CODECS, VIDEO-RANGE, and BANDWIDTH - */ - get levels() { - const levels = this.levelController.levels; - return levels ? levels : []; - } - get latestLevelDetails() { - return this.streamController.getLevelDetails() || null; - } - - /** - * Index of quality level (variant) currently played - */ - get currentLevel() { - return this.streamController.currentLevel; - } - - /** - * Set quality level index immediately. This will flush the current buffer to replace the quality asap. That means playback will interrupt at least shortly to re-buffer and re-sync eventually. Set to -1 for automatic level selection. - */ - set currentLevel(newLevel) { - this.logger.log(`set currentLevel:${newLevel}`); - this.levelController.manualLevel = newLevel; - this.streamController.immediateLevelSwitch(); - } - - /** - * Index of next quality level loaded as scheduled by stream controller. - */ - get nextLevel() { - return this.streamController.nextLevel; - } - - /** - * Set quality level index for next loaded data. - * This will switch the video quality asap, without interrupting playback. - * May abort current loading of data, and flush parts of buffer (outside currently played fragment region). - * @param newLevel - Pass -1 for automatic level selection - */ - set nextLevel(newLevel) { - this.logger.log(`set nextLevel:${newLevel}`); - this.levelController.manualLevel = newLevel; - this.streamController.nextLevelSwitch(); - } - - /** - * Return the quality level of the currently or last (of none is loaded currently) segment - */ - get loadLevel() { - return this.levelController.level; - } - - /** - * Set quality level index for next loaded data in a conservative way. - * This will switch the quality without flushing, but interrupt current loading. - * Thus the moment when the quality switch will appear in effect will only be after the already existing buffer. - * @param newLevel - Pass -1 for automatic level selection - */ - set loadLevel(newLevel) { - this.logger.log(`set loadLevel:${newLevel}`); - this.levelController.manualLevel = newLevel; - } - - /** - * get next quality level loaded - */ - get nextLoadLevel() { - return this.levelController.nextLoadLevel; - } - - /** - * Set quality level of next loaded segment in a fully "non-destructive" way. - * Same as `loadLevel` but will wait for next switch (until current loading is done). - */ - set nextLoadLevel(level) { - this.levelController.nextLoadLevel = level; - } - - /** - * Return "first level": like a default level, if not set, - * falls back to index of first level referenced in manifest - */ - get firstLevel() { - return Math.max(this.levelController.firstLevel, this.minAutoLevel); - } - - /** - * Sets "first-level", see getter. - */ - set firstLevel(newLevel) { - this.logger.log(`set firstLevel:${newLevel}`); - this.levelController.firstLevel = newLevel; - } - - /** - * Return the desired start level for the first fragment that will be loaded. - * The default value of -1 indicates automatic start level selection. - * Setting hls.nextAutoLevel without setting a startLevel will result in - * the nextAutoLevel value being used for one fragment load. - */ - get startLevel() { - const startLevel = this.levelController.startLevel; - if (startLevel === -1 && this.abrController.forcedAutoLevel > -1) { - return this.abrController.forcedAutoLevel; - } - return startLevel; - } - - /** - * set start level (level of first fragment that will be played back) - * if not overrided by user, first level appearing in manifest will be used as start level - * if -1 : automatic start level selection, playback will start from level matching download bandwidth - * (determined from download of first segment) - */ - set startLevel(newLevel) { - this.logger.log(`set startLevel:${newLevel}`); - // if not in automatic start level detection, ensure startLevel is greater than minAutoLevel - if (newLevel !== -1) { - newLevel = Math.max(newLevel, this.minAutoLevel); - } - this.levelController.startLevel = newLevel; - } - - /** - * Whether level capping is enabled. - * Default value is set via `config.capLevelToPlayerSize`. - */ - get capLevelToPlayerSize() { - return this.config.capLevelToPlayerSize; - } - - /** - * Enables or disables level capping. If disabled after previously enabled, `nextLevelSwitch` will be immediately called. - */ - set capLevelToPlayerSize(shouldStartCapping) { - const newCapLevelToPlayerSize = !!shouldStartCapping; - if (newCapLevelToPlayerSize !== this.config.capLevelToPlayerSize) { - if (newCapLevelToPlayerSize) { - this.capLevelController.startCapping(); // If capping occurs, nextLevelSwitch will happen based on size. - } else { - this.capLevelController.stopCapping(); - this.autoLevelCapping = -1; - this.streamController.nextLevelSwitch(); // Now we're uncapped, get the next level asap. - } - this.config.capLevelToPlayerSize = newCapLevelToPlayerSize; - } - } - - /** - * Capping/max level value that should be used by automatic level selection algorithm (`ABRController`) - */ - get autoLevelCapping() { - return this._autoLevelCapping; - } - - /** - * Returns the current bandwidth estimate in bits per second, when available. Otherwise, `NaN` is returned. - */ - get bandwidthEstimate() { - const { - bwEstimator - } = this.abrController; - if (!bwEstimator) { - return NaN; - } - return bwEstimator.getEstimate(); - } - set bandwidthEstimate(abrEwmaDefaultEstimate) { - this.abrController.resetEstimator(abrEwmaDefaultEstimate); - } - get abrEwmaDefaultEstimate() { - const { - bwEstimator - } = this.abrController; - if (!bwEstimator) { - return NaN; - } - return bwEstimator.defaultEstimate; - } - - /** - * get time to first byte estimate - * @type {number} - */ - get ttfbEstimate() { - const { - bwEstimator - } = this.abrController; - if (!bwEstimator) { - return NaN; - } - return bwEstimator.getEstimateTTFB(); - } - - /** - * Capping/max level value that should be used by automatic level selection algorithm (`ABRController`) - */ - set autoLevelCapping(newLevel) { - if (this._autoLevelCapping !== newLevel) { - this.logger.log(`set autoLevelCapping:${newLevel}`); - this._autoLevelCapping = newLevel; - this.levelController.checkMaxAutoUpdated(); - } - } - get maxHdcpLevel() { - return this._maxHdcpLevel; - } - set maxHdcpLevel(value) { - if (isHdcpLevel(value) && this._maxHdcpLevel !== value) { - this._maxHdcpLevel = value; - this.levelController.checkMaxAutoUpdated(); - } - } - - /** - * True when automatic level selection enabled - */ - get autoLevelEnabled() { - return this.levelController.manualLevel === -1; - } - - /** - * Level set manually (if any) - */ - get manualLevel() { - return this.levelController.manualLevel; - } - - /** - * min level selectable in auto mode according to config.minAutoBitrate - */ - get minAutoLevel() { - const { - levels, - config: { - minAutoBitrate - } - } = this; - if (!levels) return 0; - const len = levels.length; - for (let i = 0; i < len; i++) { - if (levels[i].maxBitrate >= minAutoBitrate) { - return i; - } - } - return 0; - } - - /** - * max level selectable in auto mode according to autoLevelCapping - */ - get maxAutoLevel() { - const { - levels, - autoLevelCapping, - maxHdcpLevel - } = this; - let maxAutoLevel; - if (autoLevelCapping === -1 && levels != null && levels.length) { - maxAutoLevel = levels.length - 1; - } else { - maxAutoLevel = autoLevelCapping; - } - if (maxHdcpLevel) { - for (let i = maxAutoLevel; i--;) { - const hdcpLevel = levels[i].attrs['HDCP-LEVEL']; - if (hdcpLevel && hdcpLevel <= maxHdcpLevel) { - return i; - } - } - } - return maxAutoLevel; - } - get firstAutoLevel() { - return this.abrController.firstAutoLevel; - } - - /** - * next automatically selected quality level - */ - get nextAutoLevel() { - return this.abrController.nextAutoLevel; - } - - /** - * this setter is used to force next auto level. - * this is useful to force a switch down in auto mode: - * in case of load error on level N, hls.js can set nextAutoLevel to N-1 for example) - * forced value is valid for one fragment. upon successful frag loading at forced level, - * this value will be resetted to -1 by ABR controller. - */ - set nextAutoLevel(nextLevel) { - this.abrController.nextAutoLevel = nextLevel; - } - - /** - * get the datetime value relative to media.currentTime for the active level Program Date Time if present - */ - get playingDate() { - return this.streamController.currentProgramDateTime; - } - get mainForwardBufferInfo() { - return this.streamController.getMainFwdBufferInfo(); - } - get maxBufferLength() { - return this.streamController.maxBufferLength; - } - - /** - * Find and select the best matching audio track, making a level switch when a Group change is necessary. - * Updates `hls.config.audioPreference`. Returns the selected track, or null when no matching track is found. - */ - setAudioOption(audioOption) { - var _this$audioTrackContr; - return ((_this$audioTrackContr = this.audioTrackController) == null ? void 0 : _this$audioTrackContr.setAudioOption(audioOption)) || null; - } - /** - * Find and select the best matching subtitle track, making a level switch when a Group change is necessary. - * Updates `hls.config.subtitlePreference`. Returns the selected track, or null when no matching track is found. - */ - setSubtitleOption(subtitleOption) { - var _this$subtitleTrackCo; - return ((_this$subtitleTrackCo = this.subtitleTrackController) == null ? void 0 : _this$subtitleTrackCo.setSubtitleOption(subtitleOption)) || null; - } - - /** - * Get the complete list of audio tracks across all media groups - */ - get allAudioTracks() { - const audioTrackController = this.audioTrackController; - return audioTrackController ? audioTrackController.allAudioTracks : []; - } - - /** - * Get the list of selectable audio tracks - */ - get audioTracks() { - const audioTrackController = this.audioTrackController; - return audioTrackController ? audioTrackController.audioTracks : []; - } - - /** - * index of the selected audio track (index in audio track lists) - */ - get audioTrack() { - const audioTrackController = this.audioTrackController; - return audioTrackController ? audioTrackController.audioTrack : -1; - } - - /** - * selects an audio track, based on its index in audio track lists - */ - set audioTrack(audioTrackId) { - const audioTrackController = this.audioTrackController; - if (audioTrackController) { - audioTrackController.audioTrack = audioTrackId; - } - } - - /** - * get the complete list of subtitle tracks across all media groups - */ - get allSubtitleTracks() { - const subtitleTrackController = this.subtitleTrackController; - return subtitleTrackController ? subtitleTrackController.allSubtitleTracks : []; - } - - /** - * get alternate subtitle tracks list from playlist - */ - get subtitleTracks() { - const subtitleTrackController = this.subtitleTrackController; - return subtitleTrackController ? subtitleTrackController.subtitleTracks : []; - } - - /** - * index of the selected subtitle track (index in subtitle track lists) - */ - get subtitleTrack() { - const subtitleTrackController = this.subtitleTrackController; - return subtitleTrackController ? subtitleTrackController.subtitleTrack : -1; - } - get media() { - return this._media; - } - - /** - * select an subtitle track, based on its index in subtitle track lists - */ - set subtitleTrack(subtitleTrackId) { - const subtitleTrackController = this.subtitleTrackController; - if (subtitleTrackController) { - subtitleTrackController.subtitleTrack = subtitleTrackId; - } - } - - /** - * Whether subtitle display is enabled or not - */ - get subtitleDisplay() { - const subtitleTrackController = this.subtitleTrackController; - return subtitleTrackController ? subtitleTrackController.subtitleDisplay : false; - } - - /** - * Enable/disable subtitle display rendering - */ - set subtitleDisplay(value) { - const subtitleTrackController = this.subtitleTrackController; - if (subtitleTrackController) { - subtitleTrackController.subtitleDisplay = value; - } - } - - /** - * get mode for Low-Latency HLS loading - */ - get lowLatencyMode() { - return this.config.lowLatencyMode; - } - - /** - * Enable/disable Low-Latency HLS part playlist and segment loading, and start live streams at playlist PART-HOLD-BACK rather than HOLD-BACK. - */ - set lowLatencyMode(mode) { - this.config.lowLatencyMode = mode; - } - - /** - * Position (in seconds) of live sync point (ie edge of live position minus safety delay defined by ```hls.config.liveSyncDuration```) - * @returns null prior to loading live Playlist - */ - get liveSyncPosition() { - return this.latencyController.liveSyncPosition; - } - - /** - * Estimated position (in seconds) of live edge (ie edge of live playlist plus time sync playlist advanced) - * @returns 0 before first playlist is loaded - */ - get latency() { - return this.latencyController.latency; - } - - /** - * maximum distance from the edge before the player seeks forward to ```hls.liveSyncPosition``` - * configured using ```liveMaxLatencyDurationCount``` (multiple of target duration) or ```liveMaxLatencyDuration``` - * @returns 0 before first playlist is loaded - */ - get maxLatency() { - return this.latencyController.maxLatency; - } - - /** - * target distance from the edge as calculated by the latency controller - */ - get targetLatency() { - return this.latencyController.targetLatency; - } - set targetLatency(latency) { - this.latencyController.targetLatency = latency; - } - - /** - * the rate at which the edge of the current live playlist is advancing or 1 if there is none - */ - get drift() { - return this.latencyController.drift; - } - - /** - * set to true when startLoad is called before MANIFEST_PARSED event - */ - get forceStartLoad() { - return this.streamController.forceStartLoad; - } - - /** - * ContentSteering pathwayPriority getter/setter - */ - get pathwayPriority() { - return this.levelController.pathwayPriority; - } - set pathwayPriority(pathwayPriority) { - this.levelController.pathwayPriority = pathwayPriority; - } - - /** - * returns true when all SourceBuffers are buffered to the end - */ - get bufferedToEnd() { - var _this$bufferControlle; - return !!((_this$bufferControlle = this.bufferController) != null && _this$bufferControlle.bufferedToEnd); - } - - /** - * returns Interstitials Program Manager - */ - get interstitialsManager() { - var _this$interstitialsCo; - return ((_this$interstitialsCo = this.interstitialsController) == null ? void 0 : _this$interstitialsCo.interstitialsManager) || null; - } - - /** - * returns mediaCapabilities.decodingInfo for a variant/rendition - */ - getMediaDecodingInfo(level, audioTracks = this.allAudioTracks) { - const audioTracksByGroup = getAudioTracksByGroup(audioTracks); - return getMediaDecodingInfoPromise(level, audioTracksByGroup, navigator.mediaCapabilities); - } -} -Hls.defaultConfig = void 0; - -export { AbrController, AttrList, AudioStreamController, AudioTrackController, BasePlaylistController, BaseSegment, BaseStreamController, BufferController, CMCDController, CapLevelController, ChunkMetadata, ContentSteeringController, Cues, DateRange, EMEController, ErrorActionFlags, ErrorController, ErrorDetails, ErrorTypes, Events, FPSController, FetchLoader, Fragment, Hls, HlsSkip, HlsUrlParameters, KeySystemFormats, KeySystems, Level, LevelDetails, LevelKey, LoadStats, MetadataSchema, NetworkErrorAction, Part, PlaylistLevelType, SubtitleStreamController, SubtitleTrackController, TimelineController, XhrLoader, Hls as default, fetchSupported, getMediaSource, isMSESupported, isSupported, requestMediaKeySystemAccess }; diff --git a/extern/hls.js/index.mjs b/extern/hls.js/index.mjs deleted file mode 100644 index 8f698b35..00000000 --- a/extern/hls.js/index.mjs +++ /dev/null @@ -1 +0,0 @@ -export * from "./hls.mjs"; diff --git a/extern/hls.js/package.json b/extern/hls.js/package.json deleted file mode 100644 index 690f0529..00000000 --- a/extern/hls.js/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "@mixwave/hls.js", - "version": "1.6.0", - "type": "module", - "main": "./hls.mjs", - "types": "./hls.js.d.ts" -} diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json index 1acb2ee5..25a3ce38 100644 --- a/packages/dashboard/package.json +++ b/packages/dashboard/package.json @@ -12,7 +12,6 @@ "@elysiajs/eden": "^1.1.3", "@hookform/resolvers": "^3.9.0", "@mixwave/api": "workspace:*", - "@mixwave/hls.js": "^1.6.0", "@mixwave/player": "workspace:*", "@monaco-editor/react": "^4.6.0", "@radix-ui/react-accordion": "^1.2.0", @@ -31,6 +30,7 @@ "@tanstack/react-query": "^5.51.21", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "hls.js": "1.6.0-beta.1", "lucide-react": "^0.424.0", "monaco-editor": "^0.51.0", "pretty-bytes": "^6.1.1", diff --git a/packages/dashboard/src/components/Player.tsx b/packages/dashboard/src/components/Player.tsx index 1ced1a45..60499971 100644 --- a/packages/dashboard/src/components/Player.tsx +++ b/packages/dashboard/src/components/Player.tsx @@ -1,4 +1,4 @@ -import Hls from "@mixwave/hls.js"; +import Hls from "hls.js"; import { HlsFacade, HlsUi } from "@mixwave/player"; import { useEffect, useRef, useState } from "react"; diff --git a/packages/player/README.md b/packages/player/README.md index 4632452b..7e02cacd 100644 --- a/packages/player/README.md +++ b/packages/player/README.md @@ -3,7 +3,7 @@ Player is a collection of React components and a `facade` for [HLS.js](https://github.com/video-dev/hls.js), providing a unified API. It streamlines working with HLS.js by handling state management with a strong emphasis on reactivity and it provides a set of methods that make more sense for those building their own player UI. ```sh -npm i @mixwave/hls.js +npm i hls.js@1.6.0-beta.1 npm i @mixwave/player ``` diff --git a/packages/player/package.json b/packages/player/package.json index 796ca746..937d87b1 100644 --- a/packages/player/package.json +++ b/packages/player/package.json @@ -22,7 +22,7 @@ "lint": "eslint \"./src/**/*.ts\" \"./src/**/*.tsx\" && prettier --check \"./src/**/*.ts\" \"./src/**/*.tsx\"" }, "peerDependencies": { - "@mixwave/hls.js": "^1.6.0", + "hls.js": "^1.6.0-beta.1", "react": "^18.3.1" }, "dependencies": { diff --git a/packages/player/src/facade.ts b/packages/player/src/facade.ts index e1101ba2..e27b4d91 100644 --- a/packages/player/src/facade.ts +++ b/packages/player/src/facade.ts @@ -1,4 +1,4 @@ -import Hls from "@mixwave/hls.js"; +import Hls from "hls.js"; import EventEmitter from "eventemitter3"; import { assert } from "./assert"; import { EventManager } from "./event-manager"; @@ -7,7 +7,7 @@ import type { InterstitialAssetStartedData, Level, MediaPlaylist, -} from "@mixwave/hls.js"; +} from "hls.js"; import type { MixType, Quality, diff --git a/packages/player/src/types.ts b/packages/player/src/types.ts index d1fb6cfa..96a985b3 100644 --- a/packages/player/src/types.ts +++ b/packages/player/src/types.ts @@ -1,4 +1,4 @@ -import type { MediaPlaylist, Level, HlsAssetPlayer } from "@mixwave/hls.js"; +import type { MediaPlaylist, Level, HlsAssetPlayer } from "hls.js"; /** * A custom type for each `ASSET`. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 59371b0c..e21c3b50 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,8 +33,6 @@ importers: specifier: ^1.3.2 version: 1.3.2(@algolia/client-search@4.24.0)(search-insights@2.16.2)(typescript@5.5.4) - extern/hls.js: {} - extern/vast-client: dependencies: '@xmldom/xmldom': @@ -147,9 +145,6 @@ importers: '@mixwave/api': specifier: workspace:* version: link:../api - '@mixwave/hls.js': - specifier: ^1.6.0 - version: link:../../extern/hls.js '@mixwave/player': specifier: workspace:* version: link:../player @@ -204,6 +199,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + hls.js: + specifier: 1.6.0-beta.1 + version: 1.6.0-beta.1 lucide-react: specifier: ^0.424.0 version: 0.424.0(react@18.3.1) @@ -283,15 +281,15 @@ importers: packages/player: dependencies: - '@mixwave/hls.js': - specifier: ^1.6.0 - version: link:../../extern/hls.js clsx: specifier: ^2.1.1 version: 2.1.1 eventemitter3: specifier: ^5.0.1 version: 5.0.1 + hls.js: + specifier: ^1.6.0-beta.1 + version: 1.6.0-beta.1 react: specifier: ^18.3.1 version: 18.3.1 @@ -5717,6 +5715,10 @@ packages: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} dev: false + /hls.js@1.6.0-beta.1: + resolution: {integrity: sha512-yVe7fsABEfsoHW7cG+hB0YC5ckuYyCIQV/cYrDCusVegSRjpvSqkX5JYbOFYMK+geL6s+a+R7+GSDD72a6OGTQ==} + dev: false + /hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}