Buckets:
ktongue/docker_container / simsite /frontend /node_modules /hls.js /src /controller /audio-track-controller.ts
| import BasePlaylistController from './base-playlist-controller'; | |
| import { ErrorDetails, ErrorTypes } from '../errors'; | |
| import { Events } from '../events'; | |
| import { PlaylistContextType } from '../types/loader'; | |
| import { mediaAttributesIdentical } from '../utils/media-option-attributes'; | |
| import { | |
| audioMatchPredicate, | |
| findClosestLevelWithAudioGroup, | |
| findMatchingOption, | |
| matchesOption, | |
| useAlternateAudio, | |
| } from '../utils/rendition-helper'; | |
| import type Hls from '../hls'; | |
| import type { | |
| AudioTrackLoadedData, | |
| AudioTracksUpdatedData, | |
| ErrorData, | |
| LevelLoadingData, | |
| LevelSwitchingData, | |
| ManifestParsedData, | |
| } from '../types/events'; | |
| import type { HlsUrlParameters } from '../types/level'; | |
| import type { | |
| AudioSelectionOption, | |
| MediaPlaylist, | |
| } from '../types/media-playlist'; | |
| class AudioTrackController extends BasePlaylistController { | |
| private tracks: MediaPlaylist[] = []; | |
| private groupIds: (string | undefined)[] | null = null; | |
| private tracksInGroup: MediaPlaylist[] = []; | |
| private trackId: number = -1; | |
| private currentTrack: MediaPlaylist | null = null; | |
| private selectDefaultTrack: boolean = true; | |
| constructor(hls: Hls) { | |
| super(hls, 'audio-track-controller'); | |
| this.registerListeners(); | |
| } | |
| private 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); | |
| } | |
| private 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); | |
| } | |
| public destroy() { | |
| this.unregisterListeners(); | |
| this.tracks.length = 0; | |
| this.tracksInGroup.length = 0; | |
| this.currentTrack = null; | |
| super.destroy(); | |
| } | |
| protected onManifestLoading(): void { | |
| this.tracks = []; | |
| this.tracksInGroup = []; | |
| this.groupIds = null; | |
| this.currentTrack = null; | |
| this.trackId = -1; | |
| this.selectDefaultTrack = true; | |
| } | |
| protected onManifestParsed( | |
| event: Events.MANIFEST_PARSED, | |
| data: ManifestParsedData, | |
| ): void { | |
| this.tracks = data.audioTracks || []; | |
| } | |
| protected onAudioTrackLoaded( | |
| event: Events.AUDIO_TRACK_LOADED, | |
| data: AudioTrackLoadedData, | |
| ): void { | |
| 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?.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); | |
| } | |
| } | |
| protected onLevelLoading( | |
| event: Events.LEVEL_LOADING, | |
| data: LevelLoadingData, | |
| ): void { | |
| this.switchLevel(data.level); | |
| } | |
| protected onLevelSwitching( | |
| event: Events.LEVEL_SWITCHING, | |
| data: LevelSwitchingData, | |
| ): void { | |
| this.switchLevel(data.level); | |
| } | |
| private switchLevel(levelIndex: number) { | |
| 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?.length !== audioGroups?.length || | |
| audioGroups?.some((groupId) => currentGroups?.indexOf(groupId) === -1) | |
| ) { | |
| this.groupIds = audioGroups; | |
| this.trackId = -1; | |
| this.currentTrack = null; | |
| const audioTracks = this.tracks.filter( | |
| (track): boolean => | |
| !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: AudioTracksUpdatedData = { audioTracks }; | |
| this.log( | |
| `Updating audio tracks, ${ | |
| audioTracks.length | |
| } track(s) found in group(s): ${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) { | |
| const error = new Error( | |
| `No audio track selected for current audio group-ID(s): ${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, | |
| }); | |
| } | |
| } | |
| } | |
| protected onError(event: Events.ERROR, data: ErrorData): void { | |
| 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.checkRetry(data); | |
| } | |
| } | |
| get allAudioTracks(): MediaPlaylist[] { | |
| return this.tracks; | |
| } | |
| get audioTracks(): MediaPlaylist[] { | |
| return this.tracksInGroup; | |
| } | |
| get audioTrack(): number { | |
| return this.trackId; | |
| } | |
| set audioTrack(newId: number) { | |
| // If audio track is selected from API then don't choose from the manifest default track | |
| this.selectDefaultTrack = false; | |
| this.setAudioTrack(newId); | |
| } | |
| public setAudioOption( | |
| audioOption: MediaPlaylist | AudioSelectionOption | undefined, | |
| ): MediaPlaylist | null { | |
| 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; | |
| } | |
| private setAudioTrack(newId: number): void { | |
| const tracks = this.tracksInGroup; | |
| // check if level idx is valid | |
| if (newId < 0 || newId >= tracks.length) { | |
| this.warn(`Invalid audio track id: ${newId}`); | |
| return; | |
| } | |
| 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, { ...track }); | |
| // Do not reload track unless live | |
| if (trackLoaded) { | |
| return; | |
| } | |
| const hlsUrlParameters = this.switchParams( | |
| track.url, | |
| lastTrack?.details, | |
| track.details, | |
| ); | |
| this.loadPlaylist(hlsUrlParameters); | |
| } | |
| private findTrackId(currentTrack: MediaPlaylist | null): number { | |
| 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; | |
| } | |
| protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters): void { | |
| super.loadPlaylist(); | |
| const audioTrack = this.currentTrack; | |
| if (!this.shouldLoadPlaylist(audioTrack)) { | |
| return; | |
| } | |
| // Do not load audio rendition with URI matching main variant URI | |
| if (useAlternateAudio(audioTrack.url, this.hls)) { | |
| this.scheduleLoading(audioTrack, hlsUrlParameters); | |
| } | |
| } | |
| protected loadingPlaylist( | |
| audioTrack: MediaPlaylist, | |
| hlsUrlParameters: HlsUrlParameters | undefined, | |
| ) { | |
| super.loadingPlaylist(audioTrack, hlsUrlParameters); | |
| const id = audioTrack.id; | |
| const groupId = audioTrack.groupId as string; | |
| const url = this.getUrlWithDirectives(audioTrack.url, hlsUrlParameters); | |
| const details = audioTrack.details; | |
| const age = details?.age; | |
| this.log( | |
| `Loading audio-track ${id} "${audioTrack.name}" lang:${audioTrack.lang} group:${groupId}${ | |
| hlsUrlParameters?.msn !== undefined | |
| ? ' at sn ' + hlsUrlParameters.msn + ' part ' + hlsUrlParameters.part | |
| : '' | |
| }${age && details.live ? ' age ' + age.toFixed(1) + (details.type ? ' ' + details.type || '' : '') : ''} ${url}`, | |
| ); | |
| this.hls.trigger(Events.AUDIO_TRACK_LOADING, { | |
| url, | |
| id, | |
| groupId, | |
| deliveryDirectives: hlsUrlParameters || null, | |
| track: audioTrack, | |
| }); | |
| } | |
| } | |
| export default AudioTrackController; | |
Xet Storage Details
- Size:
- 13.5 kB
- Xet hash:
- 2db0db3e7a9044292b2d3d0b59c3f1da180a0f09b8f75932945b80aeb66d6165
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.