Home Reference Source

src/controller/audio-track-controller.ts

  1. import { Events } from '../events';
  2. import { ErrorTypes, ErrorDetails } from '../errors';
  3. import {
  4. ManifestParsedData,
  5. AudioTracksUpdatedData,
  6. ErrorData,
  7. LevelLoadingData,
  8. AudioTrackLoadedData,
  9. LevelSwitchingData,
  10. } from '../types/events';
  11. import BasePlaylistController from './base-playlist-controller';
  12. import { PlaylistContextType } from '../types/loader';
  13. import type Hls from '../hls';
  14. import type { HlsUrlParameters } from '../types/level';
  15. import type { MediaPlaylist } from '../types/media-playlist';
  16.  
  17. class AudioTrackController extends BasePlaylistController {
  18. private tracks: MediaPlaylist[] = [];
  19. private groupId: string | null = null;
  20. private tracksInGroup: MediaPlaylist[] = [];
  21. private trackId: number = -1;
  22. private selectDefaultTrack: boolean = true;
  23.  
  24. constructor(hls: Hls) {
  25. super(hls, '[audio-track-controller]');
  26. this.registerListeners();
  27. }
  28.  
  29. private registerListeners() {
  30. const { hls } = this;
  31. hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this);
  32. hls.on(Events.MANIFEST_PARSED, this.onManifestParsed, this);
  33. hls.on(Events.LEVEL_LOADING, this.onLevelLoading, this);
  34. hls.on(Events.LEVEL_SWITCHING, this.onLevelSwitching, this);
  35. hls.on(Events.AUDIO_TRACK_LOADED, this.onAudioTrackLoaded, this);
  36. hls.on(Events.ERROR, this.onError, this);
  37. }
  38.  
  39. private unregisterListeners() {
  40. const { hls } = this;
  41. hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this);
  42. hls.off(Events.MANIFEST_PARSED, this.onManifestParsed, this);
  43. hls.off(Events.LEVEL_LOADING, this.onLevelLoading, this);
  44. hls.off(Events.LEVEL_SWITCHING, this.onLevelSwitching, this);
  45. hls.off(Events.AUDIO_TRACK_LOADED, this.onAudioTrackLoaded, this);
  46. hls.off(Events.ERROR, this.onError, this);
  47. }
  48.  
  49. public destroy() {
  50. this.unregisterListeners();
  51. super.destroy();
  52. }
  53.  
  54. protected onManifestLoading(): void {
  55. this.tracks = [];
  56. this.groupId = null;
  57. this.tracksInGroup = [];
  58. this.trackId = -1;
  59. this.selectDefaultTrack = true;
  60. }
  61.  
  62. protected onManifestParsed(
  63. event: Events.MANIFEST_PARSED,
  64. data: ManifestParsedData
  65. ): void {
  66. this.tracks = data.audioTracks || [];
  67. }
  68.  
  69. protected onAudioTrackLoaded(
  70. event: Events.AUDIO_TRACK_LOADED,
  71. data: AudioTrackLoadedData
  72. ): void {
  73. const { id, details } = data;
  74. const currentTrack = this.tracksInGroup[id];
  75.  
  76. if (!currentTrack) {
  77. this.warn(`Invalid audio track id ${id}`);
  78. return;
  79. }
  80.  
  81. const curDetails = currentTrack.details;
  82. currentTrack.details = data.details;
  83. this.log(`audioTrack ${id} loaded [${details.startSN}-${details.endSN}]`);
  84.  
  85. if (id === this.trackId) {
  86. this.retryCount = 0;
  87. this.playlistLoaded(id, data, curDetails);
  88. }
  89. }
  90.  
  91. protected onLevelLoading(
  92. event: Events.LEVEL_LOADING,
  93. data: LevelLoadingData
  94. ): void {
  95. this.switchLevel(data.level);
  96. }
  97.  
  98. protected onLevelSwitching(
  99. event: Events.LEVEL_SWITCHING,
  100. data: LevelSwitchingData
  101. ): void {
  102. this.switchLevel(data.level);
  103. }
  104.  
  105. private switchLevel(levelIndex: number) {
  106. const levelInfo = this.hls.levels[levelIndex];
  107.  
  108. if (!levelInfo?.audioGroupIds) {
  109. return;
  110. }
  111.  
  112. const audioGroupId = levelInfo.audioGroupIds[levelInfo.urlId];
  113. if (this.groupId !== audioGroupId) {
  114. this.groupId = audioGroupId;
  115.  
  116. const audioTracks = this.tracks.filter(
  117. (track): boolean => !audioGroupId || track.groupId === audioGroupId
  118. );
  119.  
  120. // Disable selectDefaultTrack if there are no default tracks
  121. if (
  122. this.selectDefaultTrack &&
  123. !audioTracks.some((track) => track.default)
  124. ) {
  125. this.selectDefaultTrack = false;
  126. }
  127.  
  128. this.tracksInGroup = audioTracks;
  129. const audioTracksUpdated: AudioTracksUpdatedData = { audioTracks };
  130. this.log(
  131. `Updating audio tracks, ${audioTracks.length} track(s) found in "${audioGroupId}" group-id`
  132. );
  133. this.hls.trigger(Events.AUDIO_TRACKS_UPDATED, audioTracksUpdated);
  134.  
  135. this.selectInitialTrack();
  136. }
  137. }
  138.  
  139. protected onError(event: Events.ERROR, data: ErrorData): void {
  140. super.onError(event, data);
  141. if (data.fatal || !data.context) {
  142. return;
  143. }
  144.  
  145. if (
  146. data.context.type === PlaylistContextType.AUDIO_TRACK &&
  147. data.context.id === this.trackId &&
  148. data.context.groupId === this.groupId
  149. ) {
  150. this.retryLoadingOrFail(data);
  151. }
  152. }
  153.  
  154. get audioTracks(): MediaPlaylist[] {
  155. return this.tracksInGroup;
  156. }
  157.  
  158. get audioTrack(): number {
  159. return this.trackId;
  160. }
  161.  
  162. set audioTrack(newId: number) {
  163. // If audio track is selected from API then don't choose from the manifest default track
  164. this.selectDefaultTrack = false;
  165. this.setAudioTrack(newId);
  166. }
  167.  
  168. private setAudioTrack(newId: number): void {
  169. const tracks = this.tracksInGroup;
  170. // noop on same audio track id as already set
  171. if (this.trackId === newId && tracks[newId]?.details) {
  172. return;
  173. }
  174.  
  175. // check if level idx is valid
  176. if (newId < 0 || newId >= tracks.length) {
  177. this.warn('Invalid id passed to audio-track controller');
  178. return;
  179. }
  180.  
  181. // stopping live reloading timer if any
  182. this.clearTimer();
  183.  
  184. const lastTrack = tracks[this.trackId];
  185. const track = tracks[newId];
  186. this.log(`Now switching to audio-track index ${newId}`);
  187. this.trackId = newId;
  188. const { url, type, id } = track;
  189. this.hls.trigger(Events.AUDIO_TRACK_SWITCHING, { id, type, url });
  190. const hlsUrlParameters = this.switchParams(track.url, lastTrack?.details);
  191. this.loadPlaylist(hlsUrlParameters);
  192. }
  193.  
  194. private selectInitialTrack(): void {
  195. const audioTracks = this.tracksInGroup;
  196. console.assert(
  197. audioTracks.length,
  198. 'Initial audio track should be selected when tracks are known'
  199. );
  200. const currentAudioTrackName = audioTracks[this.trackId]?.name;
  201. const trackId =
  202. this.findTrackId(currentAudioTrackName) || this.findTrackId();
  203.  
  204. if (trackId !== -1) {
  205. this.setAudioTrack(trackId);
  206. } else {
  207. this.warn(`No track found for running audio group-ID: ${this.groupId}`);
  208.  
  209. this.hls.trigger(Events.ERROR, {
  210. type: ErrorTypes.MEDIA_ERROR,
  211. details: ErrorDetails.AUDIO_TRACK_LOAD_ERROR,
  212. fatal: true,
  213. });
  214. }
  215. }
  216.  
  217. private findTrackId(name?: string): number {
  218. const audioTracks = this.tracksInGroup;
  219. for (let i = 0; i < audioTracks.length; i++) {
  220. const track = audioTracks[i];
  221. if (!this.selectDefaultTrack || track.default) {
  222. if (!name || name === track.name) {
  223. return track.id;
  224. }
  225. }
  226. }
  227. return -1;
  228. }
  229.  
  230. protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters): void {
  231. const audioTrack = this.tracksInGroup[this.trackId];
  232. if (this.shouldLoadTrack(audioTrack)) {
  233. const id = audioTrack.id;
  234. const groupId = audioTrack.groupId as string;
  235. let url = audioTrack.url;
  236. if (hlsUrlParameters) {
  237. try {
  238. url = hlsUrlParameters.addDirectives(url);
  239. } catch (error) {
  240. this.warn(
  241. `Could not construct new URL with HLS Delivery Directives: ${error}`
  242. );
  243. }
  244. }
  245. // track not retrieved yet, or live playlist we need to (re)load it
  246. this.log(`loading audio-track playlist for id: ${id}`);
  247. this.clearTimer();
  248. this.hls.trigger(Events.AUDIO_TRACK_LOADING, {
  249. url,
  250. id,
  251. groupId,
  252. deliveryDirectives: hlsUrlParameters || null,
  253. });
  254. }
  255. }
  256. }
  257.  
  258. export default AudioTrackController;