Home Reference Source

src/controller/subtitle-stream-controller.ts

  1. import { Events } from '../events';
  2. import { BufferHelper } from '../utils/buffer-helper';
  3. import { findFragmentByPTS } from './fragment-finders';
  4. import { alignMediaPlaylistByPDT } from '../utils/discontinuities';
  5. import { addSliding } from './level-helper';
  6. import { FragmentState } from './fragment-tracker';
  7. import BaseStreamController, { State } from './base-stream-controller';
  8. import { PlaylistLevelType } from '../types/loader';
  9. import { Level } from '../types/level';
  10. import type { FragmentTracker } from './fragment-tracker';
  11. import type { NetworkComponentAPI } from '../types/component-api';
  12. import type Hls from '../hls';
  13. import type { LevelDetails } from '../loader/level-details';
  14. import type { Fragment } from '../loader/fragment';
  15. import type {
  16. ErrorData,
  17. FragLoadedData,
  18. SubtitleFragProcessed,
  19. SubtitleTracksUpdatedData,
  20. TrackLoadedData,
  21. TrackSwitchedData,
  22. BufferFlushingData,
  23. LevelLoadedData,
  24. } from '../types/events';
  25.  
  26. const TICK_INTERVAL = 500; // how often to tick in ms
  27.  
  28. interface TimeRange {
  29. start: number;
  30. end: number;
  31. }
  32.  
  33. export class SubtitleStreamController
  34. extends BaseStreamController
  35. implements NetworkComponentAPI
  36. {
  37. protected levels: Array<Level> = [];
  38.  
  39. private currentTrackId: number = -1;
  40. private tracksBuffered: Array<TimeRange[]> = [];
  41. private mainDetails: LevelDetails | null = null;
  42.  
  43. constructor(hls: Hls, fragmentTracker: FragmentTracker) {
  44. super(hls, fragmentTracker, '[subtitle-stream-controller]');
  45. this._registerListeners();
  46. }
  47.  
  48. protected onHandlerDestroying() {
  49. this._unregisterListeners();
  50. this.mainDetails = null;
  51. }
  52.  
  53. private _registerListeners() {
  54. const { hls } = this;
  55. hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
  56. hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
  57. hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this);
  58. hls.on(Events.LEVEL_LOADED, this.onLevelLoaded, this);
  59. hls.on(Events.ERROR, this.onError, this);
  60. hls.on(Events.SUBTITLE_TRACKS_UPDATED, this.onSubtitleTracksUpdated, this);
  61. hls.on(Events.SUBTITLE_TRACK_SWITCH, this.onSubtitleTrackSwitch, this);
  62. hls.on(Events.SUBTITLE_TRACK_LOADED, this.onSubtitleTrackLoaded, this);
  63. hls.on(Events.SUBTITLE_FRAG_PROCESSED, this.onSubtitleFragProcessed, this);
  64. hls.on(Events.BUFFER_FLUSHING, this.onBufferFlushing, this);
  65. }
  66.  
  67. private _unregisterListeners() {
  68. const { hls } = this;
  69. hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
  70. hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
  71. hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this);
  72. hls.off(Events.LEVEL_LOADED, this.onLevelLoaded, this);
  73. hls.off(Events.ERROR, this.onError, this);
  74. hls.off(Events.SUBTITLE_TRACKS_UPDATED, this.onSubtitleTracksUpdated, this);
  75. hls.off(Events.SUBTITLE_TRACK_SWITCH, this.onSubtitleTrackSwitch, this);
  76. hls.off(Events.SUBTITLE_TRACK_LOADED, this.onSubtitleTrackLoaded, this);
  77. hls.off(Events.SUBTITLE_FRAG_PROCESSED, this.onSubtitleFragProcessed, this);
  78. hls.off(Events.BUFFER_FLUSHING, this.onBufferFlushing, this);
  79. }
  80.  
  81. startLoad() {
  82. this.stopLoad();
  83. this.state = State.IDLE;
  84.  
  85. this.setInterval(TICK_INTERVAL);
  86. this.tick();
  87. }
  88.  
  89. onManifestLoading() {
  90. this.mainDetails = null;
  91. this.fragmentTracker.removeAllFragments();
  92. }
  93.  
  94. onLevelLoaded(event: Events.LEVEL_LOADED, data: LevelLoadedData) {
  95. this.mainDetails = data.details;
  96. }
  97.  
  98. onSubtitleFragProcessed(
  99. event: Events.SUBTITLE_FRAG_PROCESSED,
  100. data: SubtitleFragProcessed
  101. ) {
  102. const { frag, success } = data;
  103. this.fragPrevious = frag;
  104. this.state = State.IDLE;
  105. if (!success) {
  106. return;
  107. }
  108.  
  109. const buffered = this.tracksBuffered[this.currentTrackId];
  110. if (!buffered) {
  111. return;
  112. }
  113.  
  114. // Create/update a buffered array matching the interface used by BufferHelper.bufferedInfo
  115. // so we can re-use the logic used to detect how much has been buffered
  116. let timeRange: TimeRange | undefined;
  117. const fragStart = frag.start;
  118. for (let i = 0; i < buffered.length; i++) {
  119. if (fragStart >= buffered[i].start && fragStart <= buffered[i].end) {
  120. timeRange = buffered[i];
  121. break;
  122. }
  123. }
  124.  
  125. const fragEnd = frag.start + frag.duration;
  126. if (timeRange) {
  127. timeRange.end = fragEnd;
  128. } else {
  129. timeRange = {
  130. start: fragStart,
  131. end: fragEnd,
  132. };
  133. buffered.push(timeRange);
  134. }
  135. this.fragmentTracker.fragBuffered(frag);
  136. }
  137.  
  138. onBufferFlushing(event: Events.BUFFER_FLUSHING, data: BufferFlushingData) {
  139. const { startOffset, endOffset } = data;
  140. if (startOffset === 0 && endOffset !== Number.POSITIVE_INFINITY) {
  141. const { currentTrackId, levels } = this;
  142. if (
  143. !levels.length ||
  144. !levels[currentTrackId] ||
  145. !levels[currentTrackId].details
  146. ) {
  147. return;
  148. }
  149. const trackDetails = levels[currentTrackId].details as LevelDetails;
  150. const targetDuration = trackDetails.targetduration;
  151. const endOffsetSubtitles = endOffset - targetDuration;
  152. if (endOffsetSubtitles <= 0) {
  153. return;
  154. }
  155. data.endOffsetSubtitles = Math.max(0, endOffsetSubtitles);
  156. this.tracksBuffered.forEach((buffered) => {
  157. for (let i = 0; i < buffered.length; ) {
  158. if (buffered[i].end <= endOffsetSubtitles) {
  159. buffered.shift();
  160. continue;
  161. } else if (buffered[i].start < endOffsetSubtitles) {
  162. buffered[i].start = endOffsetSubtitles;
  163. } else {
  164. break;
  165. }
  166. i++;
  167. }
  168. });
  169. this.fragmentTracker.removeFragmentsInRange(
  170. startOffset,
  171. endOffsetSubtitles,
  172. PlaylistLevelType.SUBTITLE
  173. );
  174. }
  175. }
  176.  
  177. // If something goes wrong, proceed to next frag, if we were processing one.
  178. onError(event: Events.ERROR, data: ErrorData) {
  179. const frag = data.frag;
  180. // don't handle error not related to subtitle fragment
  181. if (!frag || frag.type !== PlaylistLevelType.SUBTITLE) {
  182. return;
  183. }
  184.  
  185. if (this.fragCurrent?.loader) {
  186. this.fragCurrent.loader.abort();
  187. }
  188.  
  189. this.state = State.IDLE;
  190. }
  191.  
  192. // Got all new subtitle levels.
  193. onSubtitleTracksUpdated(
  194. event: Events.SUBTITLE_TRACKS_UPDATED,
  195. { subtitleTracks }: SubtitleTracksUpdatedData
  196. ) {
  197. this.tracksBuffered = [];
  198. this.levels = subtitleTracks.map(
  199. (mediaPlaylist) => new Level(mediaPlaylist)
  200. );
  201. this.fragmentTracker.removeAllFragments();
  202. this.fragPrevious = null;
  203. this.levels.forEach((level: Level) => {
  204. this.tracksBuffered[level.id] = [];
  205. });
  206. this.mediaBuffer = null;
  207. }
  208.  
  209. onSubtitleTrackSwitch(
  210. event: Events.SUBTITLE_TRACK_SWITCH,
  211. data: TrackSwitchedData
  212. ) {
  213. this.currentTrackId = data.id;
  214.  
  215. if (!this.levels.length || this.currentTrackId === -1) {
  216. this.clearInterval();
  217. return;
  218. }
  219.  
  220. // Check if track has the necessary details to load fragments
  221. const currentTrack = this.levels[this.currentTrackId];
  222. if (currentTrack?.details) {
  223. this.mediaBuffer = this.mediaBufferTimeRanges;
  224. } else {
  225. this.mediaBuffer = null;
  226. }
  227. if (currentTrack) {
  228. this.setInterval(TICK_INTERVAL);
  229. }
  230. }
  231.  
  232. // Got a new set of subtitle fragments.
  233. onSubtitleTrackLoaded(
  234. event: Events.SUBTITLE_TRACK_LOADED,
  235. data: TrackLoadedData
  236. ) {
  237. const { details: newDetails, id: trackId } = data;
  238. const { currentTrackId, levels } = this;
  239. if (!levels.length) {
  240. return;
  241. }
  242. const track: Level = levels[currentTrackId];
  243. if (trackId >= levels.length || trackId !== currentTrackId || !track) {
  244. return;
  245. }
  246. this.mediaBuffer = this.mediaBufferTimeRanges;
  247. if (newDetails.live || track.details?.live) {
  248. const mainDetails = this.mainDetails;
  249. if (newDetails.deltaUpdateFailed || !mainDetails) {
  250. return;
  251. }
  252. const mainSlidingStartFragment = mainDetails.fragments[0];
  253. if (!track.details) {
  254. if (newDetails.hasProgramDateTime && mainDetails.hasProgramDateTime) {
  255. alignMediaPlaylistByPDT(newDetails, mainDetails);
  256. } else if (mainSlidingStartFragment) {
  257. // line up live playlist with main so that fragments in range are loaded
  258. addSliding(newDetails, mainSlidingStartFragment.start);
  259. }
  260. } else {
  261. const sliding = this.alignPlaylists(newDetails, track.details);
  262. if (sliding === 0 && mainSlidingStartFragment) {
  263. // realign with main when there is no overlap with last refresh
  264. addSliding(newDetails, mainSlidingStartFragment.start);
  265. }
  266. }
  267. }
  268. track.details = newDetails;
  269. this.levelLastLoaded = trackId;
  270.  
  271. // trigger handler right now
  272. this.tick();
  273.  
  274. // If playlist is misaligned because of bad PDT or drift, delete details to resync with main on reload
  275. if (
  276. newDetails.live &&
  277. !this.fragCurrent &&
  278. this.media &&
  279. this.state === State.IDLE
  280. ) {
  281. const foundFrag = findFragmentByPTS(
  282. null,
  283. newDetails.fragments,
  284. this.media.currentTime,
  285. 0
  286. );
  287. if (!foundFrag) {
  288. this.warn('Subtitle playlist not aligned with playback');
  289. track.details = undefined;
  290. }
  291. }
  292. }
  293.  
  294. _handleFragmentLoadComplete(fragLoadedData: FragLoadedData) {
  295. const { frag, payload } = fragLoadedData;
  296. const decryptData = frag.decryptdata;
  297. const hls = this.hls;
  298.  
  299. if (this.fragContextChanged(frag)) {
  300. return;
  301. }
  302. // check to see if the payload needs to be decrypted
  303. if (
  304. payload &&
  305. payload.byteLength > 0 &&
  306. decryptData &&
  307. decryptData.key &&
  308. decryptData.iv &&
  309. decryptData.method === 'AES-128'
  310. ) {
  311. const startTime = performance.now();
  312. // decrypt the subtitles
  313. this.decrypter
  314. .webCryptoDecrypt(
  315. new Uint8Array(payload),
  316. decryptData.key.buffer,
  317. decryptData.iv.buffer
  318. )
  319. .then((decryptedData) => {
  320. const endTime = performance.now();
  321. hls.trigger(Events.FRAG_DECRYPTED, {
  322. frag,
  323. payload: decryptedData,
  324. stats: {
  325. tstart: startTime,
  326. tdecrypt: endTime,
  327. },
  328. });
  329. });
  330. }
  331. }
  332.  
  333. doTick() {
  334. if (!this.media) {
  335. this.state = State.IDLE;
  336. return;
  337. }
  338.  
  339. if (this.state === State.IDLE) {
  340. const { currentTrackId, levels } = this;
  341. if (
  342. !levels.length ||
  343. !levels[currentTrackId] ||
  344. !levels[currentTrackId].details
  345. ) {
  346. return;
  347. }
  348.  
  349. // Expand range of subs loaded by one target-duration in either direction to make up for misaligned playlists
  350. const trackDetails = levels[currentTrackId].details as LevelDetails;
  351. const targetDuration = trackDetails.targetduration;
  352. const { config, media } = this;
  353. const bufferedInfo = BufferHelper.bufferedInfo(
  354. this.mediaBufferTimeRanges,
  355. media.currentTime - targetDuration,
  356. config.maxBufferHole
  357. );
  358. const { end: targetBufferTime, len: bufferLen } = bufferedInfo;
  359.  
  360. const maxBufLen = this.getMaxBufferLength() + targetDuration;
  361.  
  362. if (bufferLen > maxBufLen) {
  363. return;
  364. }
  365.  
  366. console.assert(
  367. trackDetails,
  368. 'Subtitle track details are defined on idle subtitle stream controller tick'
  369. );
  370. const fragments = trackDetails.fragments;
  371. const fragLen = fragments.length;
  372. const end = trackDetails.edge;
  373.  
  374. let foundFrag;
  375. const fragPrevious = this.fragPrevious;
  376. if (targetBufferTime < end) {
  377. const { maxFragLookUpTolerance } = config;
  378. foundFrag = findFragmentByPTS(
  379. fragPrevious,
  380. fragments,
  381. Math.max(fragments[0].start, targetBufferTime),
  382. maxFragLookUpTolerance
  383. );
  384. if (
  385. !foundFrag &&
  386. fragPrevious &&
  387. fragPrevious.start < fragments[0].start
  388. ) {
  389. foundFrag = fragments[0];
  390. }
  391. } else {
  392. foundFrag = fragments[fragLen - 1];
  393. }
  394.  
  395. if (foundFrag?.encrypted) {
  396. this.loadKey(foundFrag, trackDetails);
  397. } else if (
  398. foundFrag &&
  399. this.fragmentTracker.getState(foundFrag) === FragmentState.NOT_LOADED
  400. ) {
  401. // only load if fragment is not loaded
  402. this.loadFragment(foundFrag, trackDetails, targetBufferTime);
  403. }
  404. }
  405. }
  406.  
  407. protected loadFragment(
  408. frag: Fragment,
  409. levelDetails: LevelDetails,
  410. targetBufferTime: number
  411. ) {
  412. this.fragCurrent = frag;
  413. super.loadFragment(frag, levelDetails, targetBufferTime);
  414. }
  415.  
  416. get mediaBufferTimeRanges(): TimeRange[] {
  417. return this.tracksBuffered[this.currentTrackId] || [];
  418. }
  419. }