Buckets:
ktongue/docker_container / simsite /frontend /node_modules /hls.js /src /controller /level-controller.ts
| import BasePlaylistController from './base-playlist-controller'; | |
| import { ErrorDetails, ErrorTypes } from '../errors'; | |
| import { Events } from '../events'; | |
| import { isVideoRange, Level, VideoRangeValues } from '../types/level'; | |
| import { PlaylistContextType, PlaylistLevelType } from '../types/loader'; | |
| import { | |
| areCodecsMediaSourceSupported, | |
| codecsSetSelectionPreferenceValue, | |
| convertAVC1ToAVCOTI, | |
| getCodecCompatibleName, | |
| videoCodecPreferenceValue, | |
| } from '../utils/codecs'; | |
| import { reassignFragmentLevelIndexes } from '../utils/level-helper'; | |
| import { getUnsupportedResult } from '../utils/mediacapabilities-helper'; | |
| import { stringify } from '../utils/safe-json-stringify'; | |
| import type ContentSteeringController from './content-steering-controller'; | |
| import type Hls from '../hls'; | |
| import type { | |
| ErrorData, | |
| FragBufferedData, | |
| LevelLoadedData, | |
| LevelsUpdatedData, | |
| LevelSwitchingData, | |
| ManifestLoadedData, | |
| ManifestLoadingData, | |
| ManifestParsedData, | |
| } from '../types/events'; | |
| import type { HlsUrlParameters, LevelParsed } from '../types/level'; | |
| import type { MediaPlaylist } from '../types/media-playlist'; | |
| export default class LevelController extends BasePlaylistController { | |
| private _levels: Level[] = []; | |
| private _firstLevel: number = -1; | |
| private _maxAutoLevel: number = -1; | |
| private _startLevel?: number; | |
| private currentLevel: Level | null = null; | |
| private currentLevelIndex: number = -1; | |
| private manualLevelIndex: number = -1; | |
| private steering: ContentSteeringController | null; | |
| public onParsedComplete!: Function; | |
| constructor( | |
| hls: Hls, | |
| contentSteeringController: ContentSteeringController | null, | |
| ) { | |
| super(hls, 'level-controller'); | |
| this.steering = contentSteeringController; | |
| this._registerListeners(); | |
| } | |
| private _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); | |
| } | |
| private _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); | |
| } | |
| public destroy() { | |
| this._unregisterListeners(); | |
| this.steering = null; | |
| this.resetLevels(); | |
| super.destroy(); | |
| } | |
| public stopLoad(): void { | |
| 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(); | |
| } | |
| private resetLevels() { | |
| this._startLevel = undefined; | |
| this.manualLevelIndex = -1; | |
| this.currentLevelIndex = -1; | |
| this.currentLevel = null; | |
| this._levels = []; | |
| this._maxAutoLevel = -1; | |
| } | |
| private onManifestLoading( | |
| event: Events.MANIFEST_LOADING, | |
| data: ManifestLoadingData, | |
| ) { | |
| this.resetLevels(); | |
| } | |
| protected onManifestLoaded( | |
| event: Events.MANIFEST_LOADED, | |
| data: ManifestLoadedData, | |
| ) { | |
| const preferManagedMediaSource = this.hls.config.preferManagedMediaSource; | |
| const levels: Level[] = []; | |
| const redundantSet: { [key: string]: Level } = {}; | |
| const generatePathwaySet: { [key: string]: number } = {}; | |
| let resolutionFound = false; | |
| let videoCodecFound = false; | |
| let audioCodecFound = false; | |
| data.levels.forEach((levelParsed: LevelParsed) => { | |
| 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 = levelParsed.videoCodec = convertAVC1ToAVCOTI(videoCodec); | |
| } | |
| // only keep levels with supported audio/video codecs | |
| const { width, height, unknownCodecs } = levelParsed; | |
| const unknownUnsupportedCodecCount = unknownCodecs?.length || 0; | |
| resolutionFound ||= !!(width && height); | |
| videoCodecFound ||= !!videoCodec; | |
| audioCodecFound ||= !!audioCodec; | |
| if ( | |
| unknownUnsupportedCodecCount || | |
| (audioCodec && !this.isAudioSupported(audioCodec)) || | |
| (videoCodec && !this.isVideoSupported(videoCodec)) | |
| ) { | |
| this.log(`Some or all CODECS not supported "${attributes.CODECS}"`); | |
| 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 = this.createLevel(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 = this.createLevel(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, | |
| ); | |
| } | |
| private createLevel(levelParsed: LevelParsed): Level { | |
| const level = new Level(levelParsed); | |
| const supplemental = levelParsed.supplemental; | |
| if ( | |
| supplemental?.videoCodec && | |
| !this.isVideoSupported(supplemental.videoCodec) | |
| ) { | |
| const error = new Error( | |
| `SUPPLEMENTAL-CODECS not supported "${supplemental.videoCodec}"`, | |
| ); | |
| this.log(error.message); | |
| level.supportedResult = getUnsupportedResult(error, []); | |
| } | |
| return level; | |
| } | |
| private isAudioSupported(codec: string): boolean { | |
| return areCodecsMediaSourceSupported( | |
| codec, | |
| 'audio', | |
| this.hls.config.preferManagedMediaSource, | |
| ); | |
| } | |
| private isVideoSupported(codec: string): boolean { | |
| return areCodecsMediaSourceSupported( | |
| codec, | |
| 'video', | |
| this.hls.config.preferManagedMediaSource, | |
| ); | |
| } | |
| private filterAndSortMediaOptions( | |
| filteredLevels: Level[], | |
| data: ManifestLoadedData, | |
| resolutionFound: boolean, | |
| videoCodecFound: boolean, | |
| audioCodecFound: boolean, | |
| ) { | |
| let audioTracks: MediaPlaylist[] = []; | |
| let subtitleTracks: MediaPlaylist[] = []; | |
| let levels = filteredLevels; | |
| const statsParsing = data.stats?.parsing || {}; | |
| // 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 | |
| // eslint-disable-next-line @typescript-eslint/no-floating-promises | |
| Promise.resolve().then(() => { | |
| if (this.hls) { | |
| let message = 'no level with compatible codecs found in manifest'; | |
| let reason = message; | |
| if (data.levels.length) { | |
| reason = `one or more CODECS in variant not supported: ${stringify( | |
| data.levels | |
| .map((level) => level.attrs.CODECS) | |
| .filter( | |
| (value, index, array) => array.indexOf(value) === index, | |
| ), | |
| )}`; | |
| this.warn(reason); | |
| message += ` (${reason})`; | |
| } | |
| const error = new Error(message); | |
| this.hls.trigger(Events.ERROR, { | |
| type: ErrorTypes.MEDIA_ERROR, | |
| details: ErrorDetails.MANIFEST_INCOMPATIBLE_CODECS_ERROR, | |
| fatal: true, | |
| url: data.url, | |
| error, | |
| reason, | |
| }); | |
| } | |
| }); | |
| statsParsing.end = performance.now(); | |
| 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) { | |
| 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?.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 config = this.hls.config; | |
| const altAudioEnabled = !!( | |
| config.audioStreamController && config.audioTrackController | |
| ); | |
| const edata: ManifestParsedData = { | |
| levels, | |
| audioTracks, | |
| subtitleTracks, | |
| sessionData: data.sessionData, | |
| sessionKeys: data.sessionKeys, | |
| firstLevel: this._firstLevel, | |
| stats: data.stats, | |
| audio: audioCodecFound, | |
| video: videoCodecFound, | |
| altAudio: | |
| altAudioEnabled && !audioOnly && audioTracks.some((t) => !!t.url), | |
| }; | |
| statsParsing.end = performance.now(); | |
| this.hls.trigger(Events.MANIFEST_PARSED, edata); | |
| } | |
| get levels(): Level[] | null { | |
| if (this._levels.length === 0) { | |
| return null; | |
| } | |
| return this._levels; | |
| } | |
| get loadLevelObj(): Level | null { | |
| return this.currentLevel; | |
| } | |
| get level(): number { | |
| return this.currentLevelIndex; | |
| } | |
| set level(newLevel: number) { | |
| 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 && | |
| 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: 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?.details, | |
| levelDetails, | |
| ); | |
| this.loadPlaylist(hlsUrlParameters); | |
| } | |
| } | |
| get manualLevel(): number { | |
| return this.manualLevelIndex; | |
| } | |
| set manualLevel(newLevel) { | |
| this.manualLevelIndex = newLevel; | |
| if (this._startLevel === undefined) { | |
| this._startLevel = newLevel; | |
| } | |
| if (newLevel !== -1) { | |
| this.level = newLevel; | |
| } | |
| } | |
| get firstLevel(): number { | |
| return this._firstLevel; | |
| } | |
| set firstLevel(newLevel) { | |
| this._firstLevel = newLevel; | |
| } | |
| get startLevel(): number { | |
| // 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: number) { | |
| this._startLevel = newLevel; | |
| } | |
| get pathways(): string[] { | |
| if (this.steering) { | |
| return this.steering.pathways(); | |
| } | |
| return []; | |
| } | |
| get pathwayPriority(): string[] | null { | |
| if (this.steering) { | |
| return this.steering.pathwayPriority; | |
| } | |
| return null; | |
| } | |
| set pathwayPriority(pathwayPriority: string[]) { | |
| 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; | |
| } | |
| } | |
| protected onError(event: Events.ERROR, data: ErrorData) { | |
| 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 | |
| protected onFragBuffered( | |
| event: Events.FRAG_BUFFERED, | |
| { frag }: FragBufferedData, | |
| ) { | |
| 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?.loadError) { | |
| this.log( | |
| `Resetting level error count of ${level.loadError} on frag buffered`, | |
| ); | |
| level.loadError = 0; | |
| } | |
| } | |
| } | |
| protected onLevelLoaded(event: Events.LEVEL_LOADED, data: LevelLoadedData) { | |
| const { level, details } = data; | |
| const curLevel = data.levelInfo; | |
| if (!curLevel) { | |
| this.warn(`Invalid level index ${level}`); | |
| if (data.deliveryDirectives?.skip) { | |
| details.deltaUpdateFailed = true; | |
| } | |
| return; | |
| } | |
| // only process level loaded events matching with expected level or prior to switch when media playlist is loaded directly | |
| if (curLevel === this.currentLevel || data.withoutMultiVariant) { | |
| // 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.deliveryDirectives?.skip) { | |
| // received a delta playlist update that cannot be merged | |
| details.deltaUpdateFailed = true; | |
| } | |
| } | |
| protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters) { | |
| super.loadPlaylist(); | |
| if (this.shouldLoadPlaylist(this.currentLevel)) { | |
| this.scheduleLoading(this.currentLevel, hlsUrlParameters); | |
| } | |
| } | |
| protected loadingPlaylist( | |
| currentLevel: Level, | |
| hlsUrlParameters: HlsUrlParameters | undefined, | |
| ) { | |
| super.loadingPlaylist(currentLevel, hlsUrlParameters); | |
| const url = this.getUrlWithDirectives(currentLevel.uri, hlsUrlParameters); | |
| const currentLevelIndex = this.currentLevelIndex; | |
| const pathwayId = currentLevel.attrs['PATHWAY-ID']; | |
| const details = currentLevel.details; | |
| const age = details?.age; | |
| this.log( | |
| `Loading level index ${currentLevelIndex}${ | |
| hlsUrlParameters?.msn !== undefined | |
| ? ' at sn ' + hlsUrlParameters.msn + ' part ' + hlsUrlParameters.part | |
| : '' | |
| }${pathwayId ? ' Pathway ' + pathwayId : ''}${age && details.live ? ' age ' + age.toFixed(1) + (details.type ? ' ' + details.type || '' : '') : ''} ${url}`, | |
| ); | |
| this.hls.trigger(Events.LEVEL_LOADING, { | |
| url, | |
| level: currentLevelIndex, | |
| levelInfo: currentLevel, | |
| 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: number) { | |
| if (this._levels.length === 1) { | |
| return; | |
| } | |
| 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?.details) { | |
| this.currentLevelIndex = this.currentLevel.details.fragments[0].level; | |
| } | |
| if (this.manualLevelIndex > -1) { | |
| this.manualLevelIndex = this.currentLevelIndex; | |
| } | |
| const maxLevel = levels.length - 1; | |
| this._firstLevel = Math.min(this._firstLevel, maxLevel); | |
| if (this._startLevel) { | |
| this._startLevel = Math.min(this._startLevel, maxLevel); | |
| } | |
| this.hls.trigger(Events.LEVELS_UPDATED, { levels }); | |
| } | |
| private onLevelsUpdated( | |
| event: Events.LEVELS_UPDATED, | |
| { levels }: LevelsUpdatedData, | |
| ) { | |
| this._levels = levels; | |
| } | |
| public 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: MediaPlaylist[]): void { | |
| const groups = {}; | |
| tracks.forEach((track) => { | |
| const groupId = track.groupId || ''; | |
| track.id = groups[groupId] = groups[groupId] || 0; | |
| groups[groupId]++; | |
| }); | |
| } | |
Xet Storage Details
- Size:
- 23.7 kB
- Xet hash:
- ac5d3bea8a15e081b18c3fda61751ece38b1ba0d38fc2d952ff92dc8a39fed60
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.