download
raw
13.5 kB
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.