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 { 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) {
  186. this.fragCurrent.abortRequests();
  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. .decrypt(
  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. .catch((err) => {
  331. this.warn(`${err.name}: ${err.message}`);
  332. this.state = State.IDLE;
  333. });
  334. }
  335. }
  336.  
  337. doTick() {
  338. if (!this.media) {
  339. this.state = State.IDLE;
  340. return;
  341. }
  342.  
  343. if (this.state === State.IDLE) {
  344. const { currentTrackId, levels } = this;
  345. if (
  346. !levels.length ||
  347. !levels[currentTrackId] ||
  348. !levels[currentTrackId].details
  349. ) {
  350. return;
  351. }
  352.  
  353. // Expand range of subs loaded by one target-duration in either direction to make up for misaligned playlists
  354. const trackDetails = levels[currentTrackId].details as LevelDetails;
  355. const targetDuration = trackDetails.targetduration;
  356. const { config, media } = this;
  357. const bufferedInfo = BufferHelper.bufferedInfo(
  358. this.tracksBuffered[this.currentTrackId] || [],
  359. media.currentTime - targetDuration,
  360. config.maxBufferHole
  361. );
  362. const { end: targetBufferTime, len: bufferLen } = bufferedInfo;
  363.  
  364. const maxBufLen = this.getMaxBufferLength() + targetDuration;
  365.  
  366. if (bufferLen > maxBufLen) {
  367. return;
  368. }
  369.  
  370. console.assert(
  371. trackDetails,
  372. 'Subtitle track details are defined on idle subtitle stream controller tick'
  373. );
  374. const fragments = trackDetails.fragments;
  375. const fragLen = fragments.length;
  376. const end = trackDetails.edge;
  377.  
  378. let foundFrag: Fragment | null;
  379. const fragPrevious = this.fragPrevious;
  380. if (targetBufferTime < end) {
  381. const { maxFragLookUpTolerance } = config;
  382. foundFrag = findFragmentByPTS(
  383. fragPrevious,
  384. fragments,
  385. Math.max(fragments[0].start, targetBufferTime),
  386. maxFragLookUpTolerance
  387. );
  388. if (
  389. !foundFrag &&
  390. fragPrevious &&
  391. fragPrevious.start < fragments[0].start
  392. ) {
  393. foundFrag = fragments[0];
  394. }
  395. } else {
  396. foundFrag = fragments[fragLen - 1];
  397. }
  398.  
  399. foundFrag = this.mapToInitFragWhenRequired(foundFrag);
  400. if (!foundFrag) {
  401. return;
  402. }
  403.  
  404. if (
  405. this.fragmentTracker.getState(foundFrag) === FragmentState.NOT_LOADED
  406. ) {
  407. // only load if fragment is not loaded
  408. this.loadFragment(foundFrag, trackDetails, targetBufferTime);
  409. }
  410. }
  411. }
  412.  
  413. protected loadFragment(
  414. frag: Fragment,
  415. levelDetails: LevelDetails,
  416. targetBufferTime: number
  417. ) {
  418. this.fragCurrent = frag;
  419. if (frag.sn === 'initSegment') {
  420. this._loadInitSegment(frag, levelDetails);
  421. } else {
  422. super.loadFragment(frag, levelDetails, targetBufferTime);
  423. }
  424. }
  425.  
  426. get mediaBufferTimeRanges(): Bufferable {
  427. return new BufferableInstance(
  428. this.tracksBuffered[this.currentTrackId] || []
  429. );
  430. }
  431. }
  432.  
  433. class BufferableInstance implements Bufferable {
  434. public readonly buffered: TimeRanges;
  435.  
  436. constructor(timeranges: TimeRange[]) {
  437. const getRange = (
  438. name: 'start' | 'end',
  439. index: number,
  440. length: number
  441. ): number => {
  442. index = index >>> 0;
  443. if (index > length - 1) {
  444. throw new DOMException(
  445. `Failed to execute '${name}' on 'TimeRanges': The index provided (${index}) is greater than the maximum bound (${length})`
  446. );
  447. }
  448. return timeranges[index][name];
  449. };
  450. this.buffered = {
  451. get length() {
  452. return timeranges.length;
  453. },
  454. end(index: number): number {
  455. return getRange('end', index, timeranges.length);
  456. },
  457. start(index: number): number {
  458. return getRange('start', index, timeranges.length);
  459. },
  460. };
  461. }
  462. }