Home Reference Source

src/controller/subtitle-stream-controller.js

  1. /**
  2. * @class SubtitleStreamController
  3. */
  4.  
  5. import Event from '../events';
  6. import { logger } from '../utils/logger';
  7. import Decrypter from '../crypt/decrypter';
  8. import { BufferHelper } from '../utils/buffer-helper';
  9. import { findFragmentByPDT, findFragmentByPTS } from './fragment-finders';
  10. import { FragmentState } from './fragment-tracker';
  11. import BaseStreamController, { State } from './base-stream-controller';
  12. import { mergeSubtitlePlaylists } from './level-helper';
  13.  
  14. const { performance } = window;
  15. const TICK_INTERVAL = 500; // how often to tick in ms
  16.  
  17. export class SubtitleStreamController extends BaseStreamController {
  18. constructor (hls, fragmentTracker) {
  19. super(hls,
  20. Event.MEDIA_ATTACHED,
  21. Event.MEDIA_DETACHING,
  22. Event.ERROR,
  23. Event.KEY_LOADED,
  24. Event.FRAG_LOADED,
  25. Event.SUBTITLE_TRACKS_UPDATED,
  26. Event.SUBTITLE_TRACK_SWITCH,
  27. Event.SUBTITLE_TRACK_LOADED,
  28. Event.SUBTITLE_FRAG_PROCESSED,
  29. Event.LEVEL_UPDATED);
  30.  
  31. this.fragmentTracker = fragmentTracker;
  32. this.config = hls.config;
  33. this.state = State.STOPPED;
  34. this.tracks = [];
  35. this.tracksBuffered = [];
  36. this.currentTrackId = -1;
  37. this.decrypter = new Decrypter(hls, hls.config);
  38. // lastAVStart stores the time in seconds for the start time of a level load
  39. this.lastAVStart = 0;
  40. this._onMediaSeeking = this.onMediaSeeking.bind(this);
  41. }
  42.  
  43. onSubtitleFragProcessed (data) {
  44. const { frag, success } = data;
  45. this.fragPrevious = frag;
  46. this.state = State.IDLE;
  47. if (!success) {
  48. return;
  49. }
  50.  
  51. const buffered = this.tracksBuffered[this.currentTrackId];
  52. if (!buffered) {
  53. return;
  54. }
  55.  
  56. // Create/update a buffered array matching the interface used by BufferHelper.bufferedInfo
  57. // so we can re-use the logic used to detect how much have been buffered
  58. let timeRange;
  59. const fragStart = frag.start;
  60. for (let i = 0; i < buffered.length; i++) {
  61. if (fragStart >= buffered[i].start && fragStart <= buffered[i].end) {
  62. timeRange = buffered[i];
  63. break;
  64. }
  65. }
  66.  
  67. const fragEnd = frag.start + frag.duration;
  68. if (timeRange) {
  69. timeRange.end = fragEnd;
  70. } else {
  71. timeRange = {
  72. start: fragStart,
  73. end: fragEnd
  74. };
  75. buffered.push(timeRange);
  76. }
  77. }
  78.  
  79. onMediaAttached ({ media }) {
  80. this.media = media;
  81. media.addEventListener('seeking', this._onMediaSeeking);
  82. this.state = State.IDLE;
  83. }
  84.  
  85. onMediaDetaching () {
  86. if (!this.media) {
  87. return;
  88. }
  89. this.media.removeEventListener('seeking', this._onMediaSeeking);
  90. this.fragmentTracker.removeAllFragments();
  91. this.currentTrackId = -1;
  92. this.tracks.forEach((track) => {
  93. this.tracksBuffered[track.id] = [];
  94. });
  95. this.media = null;
  96. this.state = State.STOPPED;
  97. }
  98.  
  99. // If something goes wrong, proceed to next frag, if we were processing one.
  100. onError (data) {
  101. let frag = data.frag;
  102. // don't handle error not related to subtitle fragment
  103. if (!frag || frag.type !== 'subtitle') {
  104. return;
  105. }
  106. this.state = State.IDLE;
  107. }
  108.  
  109. // Got all new subtitle tracks.
  110. onSubtitleTracksUpdated (data) {
  111. logger.log('subtitle tracks updated');
  112. this.tracksBuffered = [];
  113. this.tracks = data.subtitleTracks;
  114. this.tracks.forEach((track) => {
  115. this.tracksBuffered[track.id] = [];
  116. });
  117. }
  118.  
  119. onSubtitleTrackSwitch (data) {
  120. this.currentTrackId = data.id;
  121.  
  122. if (!this.tracks || !this.tracks.length || this.currentTrackId === -1) {
  123. this.clearInterval();
  124. return;
  125. }
  126.  
  127. // Check if track has the necessary details to load fragments
  128. const currentTrack = this.tracks[this.currentTrackId];
  129. if (currentTrack && currentTrack.details) {
  130. this.setInterval(TICK_INTERVAL);
  131. }
  132. }
  133.  
  134. // Got a new set of subtitle fragments.
  135. onSubtitleTrackLoaded (data) {
  136. const { id, details } = data;
  137. const { currentTrackId, tracks } = this;
  138. const currentTrack = tracks[currentTrackId];
  139. if (id >= tracks.length || id !== currentTrackId || !currentTrack) {
  140. return;
  141. }
  142.  
  143. if (details.live) {
  144. mergeSubtitlePlaylists(currentTrack.details, details, this.lastAVStart);
  145. }
  146. currentTrack.details = details;
  147. this.setInterval(TICK_INTERVAL);
  148. }
  149.  
  150. onKeyLoaded () {
  151. if (this.state === State.KEY_LOADING) {
  152. this.state = State.IDLE;
  153. }
  154. }
  155.  
  156. onFragLoaded (data) {
  157. const fragCurrent = this.fragCurrent;
  158. const decryptData = data.frag.decryptdata;
  159. const fragLoaded = data.frag;
  160. const hls = this.hls;
  161.  
  162. if (this.state === State.FRAG_LOADING &&
  163. fragCurrent &&
  164. data.frag.type === 'subtitle' &&
  165. fragCurrent.sn === data.frag.sn) {
  166. // check to see if the payload needs to be decrypted
  167. if (data.payload.byteLength > 0 && (decryptData && decryptData.key && decryptData.method === 'AES-128')) {
  168. let startTime = performance.now();
  169.  
  170. // decrypt the subtitles
  171. this.decrypter.decrypt(data.payload, decryptData.key.buffer, decryptData.iv.buffer, function (decryptedData) {
  172. let endTime = performance.now();
  173. hls.trigger(Event.FRAG_DECRYPTED, { frag: fragLoaded, payload: decryptedData, stats: { tstart: startTime, tdecrypt: endTime } });
  174. });
  175. }
  176. }
  177. }
  178.  
  179. onLevelUpdated ({ details }) {
  180. const frags = details.fragments;
  181. this.lastAVStart = frags.length ? frags[0].start : 0;
  182. }
  183.  
  184. doTick () {
  185. if (!this.media) {
  186. this.state = State.IDLE;
  187. return;
  188. }
  189.  
  190. switch (this.state) {
  191. case State.IDLE: {
  192. const { config, currentTrackId, fragmentTracker, media, tracks } = this;
  193. if (!tracks || !tracks[currentTrackId] || !tracks[currentTrackId].details) {
  194. break;
  195. }
  196.  
  197. const { maxBufferHole, maxFragLookUpTolerance } = config;
  198. const maxConfigBuffer = Math.min(config.maxBufferLength, config.maxMaxBufferLength);
  199. const bufferedInfo = BufferHelper.bufferedInfo(this._getBuffered(), media.currentTime, maxBufferHole);
  200. const { end: bufferEnd, len: bufferLen } = bufferedInfo;
  201.  
  202. const trackDetails = tracks[currentTrackId].details;
  203. const fragments = trackDetails.fragments;
  204. const fragLen = fragments.length;
  205. const end = fragments[fragLen - 1].start + fragments[fragLen - 1].duration;
  206.  
  207. if (bufferLen > maxConfigBuffer) {
  208. return;
  209. }
  210.  
  211. let foundFrag;
  212. const fragPrevious = this.fragPrevious;
  213. if (bufferEnd < end) {
  214. if (fragPrevious && trackDetails.hasProgramDateTime) {
  215. foundFrag = findFragmentByPDT(fragments, fragPrevious.endProgramDateTime, maxFragLookUpTolerance);
  216. }
  217. if (!foundFrag) {
  218. foundFrag = findFragmentByPTS(fragPrevious, fragments, bufferEnd, maxFragLookUpTolerance);
  219. }
  220. } else {
  221. foundFrag = fragments[fragLen - 1];
  222. }
  223.  
  224. if (foundFrag && foundFrag.encrypted) {
  225. logger.log(`Loading key for ${foundFrag.sn}`);
  226. this.state = State.KEY_LOADING;
  227. this.hls.trigger(Event.KEY_LOADING, { frag: foundFrag });
  228. } else if (foundFrag && fragmentTracker.getState(foundFrag) === FragmentState.NOT_LOADED) {
  229. // only load if fragment is not loaded
  230. this.fragCurrent = foundFrag;
  231. this.state = State.FRAG_LOADING;
  232. this.hls.trigger(Event.FRAG_LOADING, { frag: foundFrag });
  233. }
  234. }
  235. }
  236. }
  237.  
  238. stopLoad () {
  239. this.lastAVStart = 0;
  240. super.stopLoad();
  241. }
  242.  
  243. _getBuffered () {
  244. return this.tracksBuffered[this.currentTrackId] || [];
  245. }
  246.  
  247. onMediaSeeking () {
  248. this.fragPrevious = null;
  249. }
  250. }