Home Reference Source

src/controller/subtitle-track-controller.js

  1. import Event from '../events';
  2. import EventHandler from '../event-handler';
  3. import { logger } from '../utils/logger';
  4. import { computeReloadInterval } from './level-helper';
  5. import { clearCurrentCues } from '../utils/texttrack-utils';
  6.  
  7. class SubtitleTrackController extends EventHandler {
  8. constructor (hls) {
  9. super(hls,
  10. Event.MEDIA_ATTACHED,
  11. Event.MEDIA_DETACHING,
  12. Event.MANIFEST_LOADED,
  13. Event.SUBTITLE_TRACK_LOADED);
  14. this.tracks = [];
  15. this.trackId = -1;
  16. this.media = null;
  17. this.stopped = true;
  18.  
  19. /**
  20. * @member {boolean} subtitleDisplay Enable/disable subtitle display rendering
  21. */
  22. this.subtitleDisplay = true;
  23.  
  24. /**
  25. * Keeps reference to a default track id when media has not been attached yet
  26. * @member {number}
  27. */
  28. this.queuedDefaultTrack = null;
  29. }
  30.  
  31. destroy () {
  32. EventHandler.prototype.destroy.call(this);
  33. }
  34.  
  35. // Listen for subtitle track change, then extract the current track ID.
  36. onMediaAttached (data) {
  37. this.media = data.media;
  38. if (!this.media) {
  39. return;
  40. }
  41.  
  42. if (Number.isFinite(this.queuedDefaultTrack)) {
  43. this.subtitleTrack = this.queuedDefaultTrack;
  44. this.queuedDefaultTrack = null;
  45. }
  46.  
  47. this.trackChangeListener = this._onTextTracksChanged.bind(this);
  48.  
  49. this.useTextTrackPolling = !(this.media.textTracks && 'onchange' in this.media.textTracks);
  50. if (this.useTextTrackPolling) {
  51. this.subtitlePollingInterval = setInterval(() => {
  52. this.trackChangeListener();
  53. }, 500);
  54. } else {
  55. this.media.textTracks.addEventListener('change', this.trackChangeListener);
  56. }
  57. }
  58.  
  59. onMediaDetaching () {
  60. if (!this.media) {
  61. return;
  62. }
  63.  
  64. if (this.useTextTrackPolling) {
  65. clearInterval(this.subtitlePollingInterval);
  66. } else {
  67. this.media.textTracks.removeEventListener('change', this.trackChangeListener);
  68. }
  69.  
  70. if (Number.isFinite(this.subtitleTrack)) {
  71. this.queuedDefaultTrack = this.subtitleTrack;
  72. }
  73.  
  74. const textTracks = filterSubtitleTracks(this.media.textTracks);
  75. // Clear loaded cues on media detachment from tracks
  76. textTracks.forEach((track) => {
  77. clearCurrentCues(track);
  78. });
  79. // Disable all subtitle tracks before detachment so when reattached only tracks in that content are enabled.
  80. this.subtitleTrack = -1;
  81. this.media = null;
  82. }
  83.  
  84. // Fired whenever a new manifest is loaded.
  85. onManifestLoaded (data) {
  86. let tracks = data.subtitles || [];
  87. this.tracks = tracks;
  88. this.hls.trigger(Event.SUBTITLE_TRACKS_UPDATED, { subtitleTracks: tracks });
  89.  
  90. // loop through available subtitle tracks and autoselect default if needed
  91. // TODO: improve selection logic to handle forced, etc
  92. tracks.forEach(track => {
  93. if (track.default) {
  94. // setting this.subtitleTrack will trigger internal logic
  95. // if media has not been attached yet, it will fail
  96. // we keep a reference to the default track id
  97. // and we'll set subtitleTrack when onMediaAttached is triggered
  98. if (this.media) {
  99. this.subtitleTrack = track.id;
  100. } else {
  101. this.queuedDefaultTrack = track.id;
  102. }
  103. }
  104. });
  105. }
  106.  
  107. onSubtitleTrackLoaded (data) {
  108. const { id, details } = data;
  109. const { trackId, tracks } = this;
  110. const currentTrack = tracks[trackId];
  111. if (id >= tracks.length || id !== trackId || !currentTrack || this.stopped) {
  112. this._clearReloadTimer();
  113. return;
  114. }
  115.  
  116. logger.log(`subtitle track ${id} loaded`);
  117. if (details.live) {
  118. const reloadInterval = computeReloadInterval(currentTrack.details, details, data.stats.trequest);
  119. logger.log(`Reloading live subtitle playlist in ${reloadInterval}ms`);
  120. this.timer = setTimeout(() => {
  121. this._loadCurrentTrack();
  122. }, reloadInterval);
  123. } else {
  124. this._clearReloadTimer();
  125. }
  126. }
  127.  
  128. startLoad () {
  129. this.stopped = false;
  130. this._loadCurrentTrack();
  131. }
  132.  
  133. stopLoad () {
  134. this.stopped = true;
  135. this._clearReloadTimer();
  136. }
  137.  
  138. /** get alternate subtitle tracks list from playlist **/
  139. get subtitleTracks () {
  140. return this.tracks;
  141. }
  142.  
  143. /** get index of the selected subtitle track (index in subtitle track lists) **/
  144. get subtitleTrack () {
  145. return this.trackId;
  146. }
  147.  
  148. /** select a subtitle track, based on its index in subtitle track lists**/
  149. set subtitleTrack (subtitleTrackId) {
  150. if (this.trackId !== subtitleTrackId) {
  151. this._toggleTrackModes(subtitleTrackId);
  152. this._setSubtitleTrackInternal(subtitleTrackId);
  153. }
  154. }
  155.  
  156. _clearReloadTimer () {
  157. if (this.timer) {
  158. clearTimeout(this.timer);
  159. this.timer = null;
  160. }
  161. }
  162.  
  163. _loadCurrentTrack () {
  164. const { trackId, tracks, hls } = this;
  165. const currentTrack = tracks[trackId];
  166. if (trackId < 0 || !currentTrack || (currentTrack.details && !currentTrack.details.live)) {
  167. return;
  168. }
  169. logger.log(`Loading subtitle track ${trackId}`);
  170. hls.trigger(Event.SUBTITLE_TRACK_LOADING, { url: currentTrack.url, id: trackId });
  171. }
  172.  
  173. /**
  174. * Disables the old subtitleTrack and sets current mode on the next subtitleTrack.
  175. * This operates on the DOM textTracks.
  176. * A value of -1 will disable all subtitle tracks.
  177. * @param newId - The id of the next track to enable
  178. * @private
  179. */
  180. _toggleTrackModes (newId) {
  181. const { media, subtitleDisplay, trackId } = this;
  182. if (!media) {
  183. return;
  184. }
  185.  
  186. const textTracks = filterSubtitleTracks(media.textTracks);
  187. if (newId === -1) {
  188. [].slice.call(textTracks).forEach(track => {
  189. track.mode = 'disabled';
  190. });
  191. } else {
  192. const oldTrack = textTracks[trackId];
  193. if (oldTrack) {
  194. oldTrack.mode = 'disabled';
  195. }
  196. }
  197.  
  198. const nextTrack = textTracks[newId];
  199. if (nextTrack) {
  200. nextTrack.mode = subtitleDisplay ? 'showing' : 'hidden';
  201. }
  202. }
  203.  
  204. /**
  205. * This method is responsible for validating the subtitle index and periodically reloading if live.
  206. * Dispatches the SUBTITLE_TRACK_SWITCH event, which instructs the subtitle-stream-controller to load the selected track.
  207. * @param newId - The id of the subtitle track to activate.
  208. */
  209. _setSubtitleTrackInternal (newId) {
  210. const { hls, tracks } = this;
  211. if (!Number.isFinite(newId) || newId < -1 || newId >= tracks.length) {
  212. return;
  213. }
  214.  
  215. this.trackId = newId;
  216. logger.log(`Switching to subtitle track ${newId}`);
  217. hls.trigger(Event.SUBTITLE_TRACK_SWITCH, { id: newId });
  218. this._loadCurrentTrack();
  219. }
  220.  
  221. _onTextTracksChanged () {
  222. // Media is undefined when switching streams via loadSource()
  223. if (!this.media || !this.hls.config.renderTextTracksNatively) {
  224. return;
  225. }
  226.  
  227. let trackId = -1;
  228. let tracks = filterSubtitleTracks(this.media.textTracks);
  229. for (let id = 0; id < tracks.length; id++) {
  230. if (tracks[id].mode === 'hidden') {
  231. // Do not break in case there is a following track with showing.
  232. trackId = id;
  233. } else if (tracks[id].mode === 'showing') {
  234. trackId = id;
  235. break;
  236. }
  237. }
  238.  
  239. // Setting current subtitleTrack will invoke code.
  240. this.subtitleTrack = trackId;
  241. }
  242. }
  243.  
  244. function filterSubtitleTracks (textTrackList) {
  245. let tracks = [];
  246. for (let i = 0; i < textTrackList.length; i++) {
  247. const track = textTrackList[i];
  248. // Edge adds a track without a label; we don't want to use it
  249. if (track.kind === 'subtitles' && track.label) {
  250. tracks.push(textTrackList[i]);
  251. }
  252. }
  253. return tracks;
  254. }
  255.  
  256. export default SubtitleTrackController;