Buckets:
ktongue/docker_container / simsite /frontend /node_modules /hls.js /src /controller /content-steering-controller.ts
| import { ErrorActionFlags, NetworkErrorAction } from './error-controller'; | |
| import { ErrorDetails } from '../errors'; | |
| import { Events } from '../events'; | |
| import { Level } from '../types/level'; | |
| import { | |
| type Loader, | |
| type LoaderCallbacks, | |
| type LoaderConfiguration, | |
| type LoaderContext, | |
| type LoaderResponse, | |
| type LoaderStats, | |
| PlaylistContextType, | |
| } from '../types/loader'; | |
| import { AttrList } from '../utils/attr-list'; | |
| import { reassignFragmentLevelIndexes } from '../utils/level-helper'; | |
| import { Logger } from '../utils/logger'; | |
| import { stringify } from '../utils/safe-json-stringify'; | |
| import type { RetryConfig } from '../config'; | |
| import type Hls from '../hls'; | |
| import type { NetworkComponentAPI } from '../types/component-api'; | |
| import type { | |
| ErrorData, | |
| ManifestLoadedData, | |
| ManifestParsedData, | |
| SteeringManifestLoadedData, | |
| } from '../types/events'; | |
| import type { MediaAttributes, MediaPlaylist } from '../types/media-playlist'; | |
| export type SteeringManifest = { | |
| VERSION: 1; | |
| TTL: number; | |
| 'RELOAD-URI'?: string; | |
| 'PATHWAY-PRIORITY': string[]; | |
| 'PATHWAY-CLONES'?: PathwayClone[]; | |
| }; | |
| export type PathwayClone = { | |
| 'BASE-ID': string; | |
| ID: string; | |
| 'URI-REPLACEMENT': UriReplacement; | |
| }; | |
| export type UriReplacement = { | |
| HOST?: string; | |
| PARAMS?: { [queryParameter: string]: string }; | |
| 'PER-VARIANT-URIS'?: { [stableVariantId: string]: string }; | |
| 'PER-RENDITION-URIS'?: { [stableRenditionId: string]: string }; | |
| }; | |
| const PATHWAY_PENALTY_DURATION_MS = 300000; | |
| export default class ContentSteeringController | |
| extends Logger | |
| implements NetworkComponentAPI | |
| { | |
| private readonly hls: Hls; | |
| private loader: Loader<LoaderContext> | null = null; | |
| private uri: string | null = null; | |
| private pathwayId: string = '.'; | |
| private _pathwayPriority: string[] | null = null; | |
| private timeToLoad: number = 300; | |
| private reloadTimer: number = -1; | |
| private updated: number = 0; | |
| private started: boolean = false; | |
| private enabled: boolean = true; | |
| private levels: Level[] | null = null; | |
| private audioTracks: MediaPlaylist[] | null = null; | |
| private subtitleTracks: MediaPlaylist[] | null = null; | |
| private penalizedPathways: { [pathwayId: string]: number } = {}; | |
| constructor(hls: Hls) { | |
| super('content-steering', hls.logger); | |
| this.hls = hls; | |
| this.registerListeners(); | |
| } | |
| private 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); | |
| } | |
| private 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; | |
| }, [] as string[]); | |
| } | |
| get pathwayPriority(): string[] | null { | |
| return this._pathwayPriority; | |
| } | |
| set pathwayPriority(pathwayPriority: string[]) { | |
| 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: Level) { | |
| const levels = this.levels; | |
| if (levels) { | |
| this.levels = levels.filter((level) => level !== levelToRemove); | |
| } | |
| } | |
| private onManifestLoading() { | |
| this.stopLoad(); | |
| this.enabled = true; | |
| this.timeToLoad = 300; | |
| this.updated = 0; | |
| this.uri = null; | |
| this.pathwayId = '.'; | |
| this.levels = this.audioTracks = this.subtitleTracks = null; | |
| } | |
| private onManifestLoaded( | |
| event: Events.MANIFEST_LOADED, | |
| data: ManifestLoadedData, | |
| ) { | |
| const { contentSteering } = data; | |
| if (contentSteering === null) { | |
| return; | |
| } | |
| this.pathwayId = contentSteering.pathwayId; | |
| this.uri = contentSteering.uri; | |
| if (this.started) { | |
| this.startLoad(); | |
| } | |
| } | |
| private onManifestParsed( | |
| event: Events.MANIFEST_PARSED, | |
| data: ManifestParsedData, | |
| ) { | |
| this.audioTracks = data.audioTracks; | |
| this.subtitleTracks = data.subtitleTracks; | |
| } | |
| private onError(event: Events.ERROR, data: ErrorData) { | |
| const { errorAction } = data; | |
| if ( | |
| 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 (data.details === ErrorDetails.BUFFER_APPEND_ERROR && !data.fatal) { | |
| // Error will become fatal in buffer-controller when reaching `appendErrorMaxRetry` | |
| // Stream-controllers are expected to reduce buffer length even if this is not deemed a QuotaExceededError | |
| errorAction.resolved = true; | |
| } else 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: ${stringify( | |
| pathwayPriority, | |
| )} penalized: ${stringify(this.penalizedPathways)}`, | |
| ); | |
| } | |
| } | |
| } | |
| public filterParsedLevels(levels: Level[]): Level[] { | |
| // 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; | |
| } | |
| private getLevelsForPathway(pathwayId: string): Level[] { | |
| if (this.levels === null) { | |
| return []; | |
| } | |
| return this.levels.filter((level) => pathwayId === level.pathwayId); | |
| } | |
| private updatePathwayPriority(pathwayPriority: string[]) { | |
| this._pathwayPriority = pathwayPriority; | |
| let levels: Level[] | undefined; | |
| // 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: Level = 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; | |
| } | |
| } | |
| } | |
| private getPathwayForGroupId( | |
| groupId: string, | |
| type: PlaylistContextType, | |
| defaultPathway: string, | |
| ): string { | |
| 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; | |
| } | |
| private clonePathways(pathwayClones: PathwayClone[]) { | |
| const levels = this.levels; | |
| if (!levels) { | |
| return; | |
| } | |
| const audioGroupCloneMap: Record<string, string> = {}; | |
| const subtitleGroupCloneMap: Record<string, string> = {}; | |
| 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: string | undefined = | |
| attributes.AUDIO && `${attributes.AUDIO}_clone_${cloneId}`; | |
| const clonedSubtitleGroupId: string | undefined = | |
| 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, | |
| ); | |
| }); | |
| } | |
| private loadSteeringManifest(uri: string) { | |
| const config = this.hls.config; | |
| const Loader = config.loader; | |
| if (this.loader) { | |
| this.loader.destroy(); | |
| } | |
| this.loader = new Loader(config) as Loader<LoaderContext>; | |
| let url: 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: LoaderContext = { | |
| responseType: 'json', | |
| url: url.href, | |
| }; | |
| const loadPolicy = config.steeringManifestLoadPolicy.default; | |
| const legacyRetryCompatibility: RetryConfig | Record<string, void> = | |
| loadPolicy.errorRetry || loadPolicy.timeoutRetry || {}; | |
| const loaderConfig: LoaderConfiguration = { | |
| loadPolicy, | |
| timeout: loadPolicy.maxLoadTimeMs, | |
| maxRetry: legacyRetryCompatibility.maxNumRetry || 0, | |
| retryDelay: legacyRetryCompatibility.retryDelayMs || 0, | |
| maxRetryDelay: legacyRetryCompatibility.maxRetryDelayMs || 0, | |
| }; | |
| const callbacks: LoaderCallbacks<LoaderContext> = { | |
| onSuccess: ( | |
| response: LoaderResponse, | |
| stats: LoaderStats, | |
| context: LoaderContext, | |
| networkDetails: any, | |
| ) => { | |
| this.log(`Loaded steering manifest: "${url}"`); | |
| const steeringData = response.data as SteeringManifest; | |
| if (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: SteeringManifestLoadedData = { | |
| steeringManifest: steeringData, | |
| url: url.toString(), | |
| }; | |
| this.hls.trigger(Events.STEERING_MANIFEST_LOADED, loadedSteeringData); | |
| if (pathwayPriority) { | |
| this.updatePathwayPriority(pathwayPriority); | |
| } | |
| }, | |
| onError: ( | |
| error: { code: number; text: string }, | |
| context: LoaderContext, | |
| networkDetails: any, | |
| stats: LoaderStats, | |
| ) => { | |
| 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?.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: LoaderStats, | |
| context: LoaderContext, | |
| networkDetails: any, | |
| ) => { | |
| 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); | |
| } | |
| private scheduleRefresh(uri: string, ttlMs: number = this.timeToLoad * 1000) { | |
| this.clearTimeout(); | |
| this.reloadTimer = self.setTimeout(() => { | |
| const media = this.hls?.media; | |
| if (media && !media.ended) { | |
| this.loadSteeringManifest(uri); | |
| return; | |
| } | |
| this.scheduleRefresh(uri, this.timeToLoad * 1000); | |
| }, ttlMs); | |
| } | |
| } | |
| function cloneRenditionGroups( | |
| tracks: MediaPlaylist[] | null, | |
| groupCloneMap: Record<string, string>, | |
| uriReplacement: UriReplacement, | |
| cloneId: string, | |
| ) { | |
| if (!tracks) { | |
| return; | |
| } | |
| Object.keys(groupCloneMap).forEach((audioGroupId) => { | |
| const clonedTracks = tracks | |
| .filter((track) => track.groupId === audioGroupId) | |
| .map((track) => { | |
| const clonedTrack = Object.assign({}, track); | |
| clonedTrack.details = undefined; | |
| clonedTrack.attrs = new AttrList(clonedTrack.attrs) as MediaAttributes; | |
| 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: string, | |
| stableId: string | undefined, | |
| perOptionKey: 'PER-VARIANT-URIS' | 'PER-RENDITION-URIS', | |
| uriReplacement: UriReplacement, | |
| ): string { | |
| const { | |
| HOST: host, | |
| PARAMS: params, | |
| [perOptionKey]: perOptionUris, | |
| } = uriReplacement; | |
| let perVariantUri; | |
| if (stableId) { | |
| perVariantUri = 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; | |
| } | |
Xet Storage Details
- Size:
- 19.5 kB
- Xet hash:
- ee1328ddbabe4bc1c29a0d7a4cf538f8334ea2752bda401ff49a53a083adbf3c
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.