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