Home Reference Source

src/controller/subtitle-stream-controller.ts

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