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. startLoad () {
  44. this.stopLoad();
  45. this.state = State.IDLE;
  46.  
  47. // Check if we already have a track with necessary details to load fragments
  48. const currentTrack = this.tracks[this.currentTrackId];
  49. if (currentTrack && currentTrack.details) {
  50. this.setInterval(TICK_INTERVAL);
  51. this.tick();
  52. }
  53. }
  54.  
  55. onSubtitleFragProcessed (data) {
  56. const { frag, success } = data;
  57. this.fragPrevious = frag;
  58. this.state = State.IDLE;
  59. if (!success) {
  60. return;
  61. }
  62.  
  63. const buffered = this.tracksBuffered[this.currentTrackId];
  64. if (!buffered) {
  65. return;
  66. }
  67.  
  68. // Create/update a buffered array matching the interface used by BufferHelper.bufferedInfo
  69. // so we can re-use the logic used to detect how much have been buffered
  70. let timeRange;
  71. const fragStart = frag.start;
  72. for (let i = 0; i < buffered.length; i++) {
  73. if (fragStart >= buffered[i].start && fragStart <= buffered[i].end) {
  74. timeRange = buffered[i];
  75. break;
  76. }
  77. }
  78.  
  79. const fragEnd = frag.start + frag.duration;
  80. if (timeRange) {
  81. timeRange.end = fragEnd;
  82. } else {
  83. timeRange = {
  84. start: fragStart,
  85. end: fragEnd
  86. };
  87. buffered.push(timeRange);
  88. }
  89. }
  90.  
  91. onMediaAttached ({ media }) {
  92. this.media = media;
  93. media.addEventListener('seeking', this._onMediaSeeking);
  94. this.state = State.IDLE;
  95. }
  96.  
  97. onMediaDetaching () {
  98. if (!this.media) {
  99. return;
  100. }
  101. this.media.removeEventListener('seeking', this._onMediaSeeking);
  102. this.fragmentTracker.removeAllFragments();
  103. this.currentTrackId = -1;
  104. this.tracks.forEach((track) => {
  105. this.tracksBuffered[track.id] = [];
  106. });
  107. this.media = null;
  108. this.state = State.STOPPED;
  109. }
  110.  
  111. // If something goes wrong, proceed to next frag, if we were processing one.
  112. onError (data) {
  113. let frag = data.frag;
  114. // don't handle error not related to subtitle fragment
  115. if (!frag || frag.type !== 'subtitle') {
  116. return;
  117. }
  118.  
  119. if (this.fragCurrent && this.fragCurrent.loader) {
  120. this.fragCurrent.loader.abort();
  121. }
  122.  
  123. this.state = State.IDLE;
  124. }
  125.  
  126. // Got all new subtitle tracks.
  127. onSubtitleTracksUpdated (data) {
  128. logger.log('subtitle tracks updated');
  129. this.tracksBuffered = [];
  130. this.tracks = data.subtitleTracks;
  131. this.tracks.forEach((track) => {
  132. this.tracksBuffered[track.id] = [];
  133. });
  134. }
  135.  
  136. onSubtitleTrackSwitch (data) {
  137. this.currentTrackId = data.id;
  138.  
  139. if (!this.tracks || !this.tracks.length || this.currentTrackId === -1) {
  140. this.clearInterval();
  141. return;
  142. }
  143.  
  144. // Check if track has the necessary details to load fragments
  145. const currentTrack = this.tracks[this.currentTrackId];
  146. if (currentTrack && currentTrack.details) {
  147. this.setInterval(TICK_INTERVAL);
  148. }
  149. }
  150.  
  151. // Got a new set of subtitle fragments.
  152. onSubtitleTrackLoaded (data) {
  153. const { id, details } = data;
  154. const { currentTrackId, tracks } = this;
  155. const currentTrack = tracks[currentTrackId];
  156. if (id >= tracks.length || id !== currentTrackId || !currentTrack) {
  157. return;
  158. }
  159.  
  160. if (details.live) {
  161. mergeSubtitlePlaylists(currentTrack.details, details, this.lastAVStart);
  162. }
  163. currentTrack.details = details;
  164. this.setInterval(TICK_INTERVAL);
  165. }
  166.  
  167. onKeyLoaded () {
  168. if (this.state === State.KEY_LOADING) {
  169. this.state = State.IDLE;
  170. }
  171. }
  172.  
  173. onFragLoaded (data) {
  174. const fragCurrent = this.fragCurrent;
  175. const decryptData = data.frag.decryptdata;
  176. const fragLoaded = data.frag;
  177. const hls = this.hls;
  178.  
  179. if (this.state === State.FRAG_LOADING &&
  180. fragCurrent &&
  181. data.frag.type === 'subtitle' &&
  182. fragCurrent.sn === data.frag.sn) {
  183. // check to see if the payload needs to be decrypted
  184. if (data.payload.byteLength > 0 && (decryptData && decryptData.key && decryptData.method === 'AES-128')) {
  185. let startTime = performance.now();
  186.  
  187. // decrypt the subtitles
  188. this.decrypter.decrypt(data.payload, decryptData.key.buffer, decryptData.iv.buffer, function (decryptedData) {
  189. let endTime = performance.now();
  190. hls.trigger(Event.FRAG_DECRYPTED, { frag: fragLoaded, payload: decryptedData, stats: { tstart: startTime, tdecrypt: endTime } });
  191. });
  192. }
  193. }
  194. }
  195.  
  196. onLevelUpdated ({ details }) {
  197. const frags = details.fragments;
  198. this.lastAVStart = frags.length ? frags[0].start : 0;
  199. }
  200.  
  201. doTick () {
  202. if (!this.media) {
  203. this.state = State.IDLE;
  204. return;
  205. }
  206.  
  207. switch (this.state) {
  208. case State.IDLE: {
  209. const { config, currentTrackId, fragmentTracker, media, tracks } = this;
  210. if (!tracks || !tracks[currentTrackId] || !tracks[currentTrackId].details) {
  211. break;
  212. }
  213.  
  214. const { maxBufferHole, maxFragLookUpTolerance } = config;
  215. const maxConfigBuffer = Math.min(config.maxBufferLength, config.maxMaxBufferLength);
  216. const bufferedInfo = BufferHelper.bufferedInfo(this._getBuffered(), media.currentTime, maxBufferHole);
  217. const { end: bufferEnd, len: bufferLen } = bufferedInfo;
  218.  
  219. const trackDetails = tracks[currentTrackId].details;
  220. const fragments = trackDetails.fragments;
  221. const fragLen = fragments.length;
  222. const end = fragments[fragLen - 1].start + fragments[fragLen - 1].duration;
  223.  
  224. if (bufferLen > maxConfigBuffer) {
  225. return;
  226. }
  227.  
  228. let foundFrag;
  229. const fragPrevious = this.fragPrevious;
  230. if (bufferEnd < end) {
  231. if (fragPrevious && trackDetails.hasProgramDateTime) {
  232. foundFrag = findFragmentByPDT(fragments, fragPrevious.endProgramDateTime, maxFragLookUpTolerance);
  233. }
  234. if (!foundFrag) {
  235. foundFrag = findFragmentByPTS(fragPrevious, fragments, bufferEnd, maxFragLookUpTolerance);
  236. }
  237. } else {
  238. foundFrag = fragments[fragLen - 1];
  239. }
  240.  
  241. if (foundFrag && foundFrag.encrypted) {
  242. logger.log(`Loading key for ${foundFrag.sn}`);
  243. this.state = State.KEY_LOADING;
  244. this.hls.trigger(Event.KEY_LOADING, { frag: foundFrag });
  245. } else if (foundFrag && fragmentTracker.getState(foundFrag) === FragmentState.NOT_LOADED) {
  246. // only load if fragment is not loaded
  247. this.fragCurrent = foundFrag;
  248. this.state = State.FRAG_LOADING;
  249. this.hls.trigger(Event.FRAG_LOADING, { frag: foundFrag });
  250. }
  251. }
  252. }
  253. }
  254.  
  255. stopLoad () {
  256. this.lastAVStart = 0;
  257. this.fragPrevious = null;
  258. super.stopLoad();
  259. }
  260.  
  261. _getBuffered () {
  262. return this.tracksBuffered[this.currentTrackId] || [];
  263. }
  264.  
  265. onMediaSeeking () {
  266. if (this.fragCurrent) {
  267. const currentTime = this.media ? this.media.currentTime : 0;
  268. const tolerance = this.config.maxFragLookUpTolerance;
  269. const fragStartOffset = this.fragCurrent.start - tolerance;
  270. const fragEndOffset = this.fragCurrent.start + this.fragCurrent.duration + tolerance;
  271.  
  272. // check if position will be out of currently loaded frag range after seeking : if out, cancel frag load, if in, don't do anything
  273. if (currentTime < fragStartOffset || currentTime > fragEndOffset) {
  274. if (this.fragCurrent.loader) {
  275. this.fragCurrent.loader.abort();
  276. }
  277.  
  278. this.fragmentTracker.removeFragment(this.fragCurrent);
  279. this.fragCurrent = null;
  280. this.fragPrevious = null;
  281.  
  282. // switch to IDLE state to load new fragment
  283. this.state = State.IDLE;
  284.  
  285. // speed up things
  286. this.tick();
  287. }
  288. }
  289. }
  290. }