Home Reference Source

src/loader/fragment.ts

  1. import { buildAbsoluteURL } from 'url-toolkit';
  2. import { logger } from '../utils/logger';
  3. import { LevelKey } from './level-key';
  4. import { LoadStats } from './load-stats';
  5. import { AttrList } from '../utils/attr-list';
  6. import type {
  7. FragmentLoaderContext,
  8. Loader,
  9. PlaylistLevelType,
  10. } from '../types/loader';
  11.  
  12. export enum ElementaryStreamTypes {
  13. AUDIO = 'audio',
  14. VIDEO = 'video',
  15. AUDIOVIDEO = 'audiovideo',
  16. }
  17.  
  18. export interface ElementaryStreamInfo {
  19. startPTS: number;
  20. endPTS: number;
  21. startDTS: number;
  22. endDTS: number;
  23. partial?: boolean;
  24. }
  25.  
  26. export type ElementaryStreams = Record<
  27. ElementaryStreamTypes,
  28. ElementaryStreamInfo | null
  29. >;
  30.  
  31. export class BaseSegment {
  32. private _byteRange: number[] | null = null;
  33. private _url: string | null = null;
  34.  
  35. // baseurl is the URL to the playlist
  36. public readonly baseurl: string;
  37. // relurl is the portion of the URL that comes from inside the playlist.
  38. public relurl?: string;
  39. // Holds the types of data this fragment supports
  40. public elementaryStreams: ElementaryStreams = {
  41. [ElementaryStreamTypes.AUDIO]: null,
  42. [ElementaryStreamTypes.VIDEO]: null,
  43. [ElementaryStreamTypes.AUDIOVIDEO]: null,
  44. };
  45.  
  46. constructor(baseurl: string) {
  47. this.baseurl = baseurl;
  48. }
  49.  
  50. // setByteRange converts a EXT-X-BYTERANGE attribute into a two element array
  51. setByteRange(value: string, previous?: BaseSegment) {
  52. const params = value.split('@', 2);
  53. const byteRange: number[] = [];
  54. if (params.length === 1) {
  55. byteRange[0] = previous ? previous.byteRangeEndOffset : 0;
  56. } else {
  57. byteRange[0] = parseInt(params[1]);
  58. }
  59. byteRange[1] = parseInt(params[0]) + byteRange[0];
  60. this._byteRange = byteRange;
  61. }
  62.  
  63. get byteRange(): number[] {
  64. if (!this._byteRange) {
  65. return [];
  66. }
  67.  
  68. return this._byteRange;
  69. }
  70.  
  71. get byteRangeStartOffset(): number {
  72. return this.byteRange[0];
  73. }
  74.  
  75. get byteRangeEndOffset(): number {
  76. return this.byteRange[1];
  77. }
  78.  
  79. get url(): string {
  80. if (!this._url && this.baseurl && this.relurl) {
  81. this._url = buildAbsoluteURL(this.baseurl, this.relurl, {
  82. alwaysNormalize: true,
  83. });
  84. }
  85. return this._url || '';
  86. }
  87.  
  88. set url(value: string) {
  89. this._url = value;
  90. }
  91. }
  92.  
  93. export class Fragment extends BaseSegment {
  94. private _decryptdata: LevelKey | null = null;
  95.  
  96. public rawProgramDateTime: string | null = null;
  97. public programDateTime: number | null = null;
  98. public tagList: Array<string[]> = [];
  99.  
  100. // EXTINF has to be present for a m38 to be considered valid
  101. public duration: number = 0;
  102. // sn notates the sequence number for a segment, and if set to a string can be 'initSegment'
  103. public sn: number | 'initSegment' = 0;
  104. // levelkey is the EXT-X-KEY that applies to this segment for decryption
  105. // core difference from the private field _decryptdata is the lack of the initialized IV
  106. // _decryptdata will set the IV for this segment based on the segment number in the fragment
  107. public levelkey?: LevelKey;
  108. // A string representing the fragment type
  109. public readonly type: PlaylistLevelType;
  110. // A reference to the loader. Set while the fragment is loading, and removed afterwards. Used to abort fragment loading
  111. public loader: Loader<FragmentLoaderContext> | null = null;
  112. // The level/track index to which the fragment belongs
  113. public level: number = -1;
  114. // The continuity counter of the fragment
  115. public cc: number = 0;
  116. // The starting Presentation Time Stamp (PTS) of the fragment. Set after transmux complete.
  117. public startPTS?: number;
  118. // The ending Presentation Time Stamp (PTS) of the fragment. Set after transmux complete.
  119. public endPTS?: number;
  120. // The latest Presentation Time Stamp (PTS) appended to the buffer.
  121. public appendedPTS?: number;
  122. // The starting Decode Time Stamp (DTS) of the fragment. Set after transmux complete.
  123. public startDTS!: number;
  124. // The ending Decode Time Stamp (DTS) of the fragment. Set after transmux complete.
  125. public endDTS!: number;
  126. // The start time of the fragment, as listed in the manifest. Updated after transmux complete.
  127. public start: number = 0;
  128. // Set by `updateFragPTSDTS` in level-helper
  129. public deltaPTS?: number;
  130. // The maximum starting Presentation Time Stamp (audio/video PTS) of the fragment. Set after transmux complete.
  131. public maxStartPTS?: number;
  132. // The minimum ending Presentation Time Stamp (audio/video PTS) of the fragment. Set after transmux complete.
  133. public minEndPTS?: number;
  134. // Load/parse timing information
  135. public stats: LoadStats = new LoadStats();
  136. public urlId: number = 0;
  137. public data?: Uint8Array;
  138. // A flag indicating whether the segment was downloaded in order to test bitrate, and was not buffered
  139. public bitrateTest: boolean = false;
  140. // #EXTINF segment title
  141. public title: string | null = null;
  142.  
  143. constructor(type: PlaylistLevelType, baseurl: string) {
  144. super(baseurl);
  145. this.type = type;
  146. }
  147.  
  148. get decryptdata(): LevelKey | null {
  149. if (!this.levelkey && !this._decryptdata) {
  150. return null;
  151. }
  152.  
  153. if (!this._decryptdata && this.levelkey) {
  154. let sn = this.sn;
  155. if (typeof sn !== 'number') {
  156. // We are fetching decryption data for a initialization segment
  157. // If the segment was encrypted with AES-128
  158. // It must have an IV defined. We cannot substitute the Segment Number in.
  159. if (
  160. this.levelkey &&
  161. this.levelkey.method === 'AES-128' &&
  162. !this.levelkey.iv
  163. ) {
  164. logger.warn(
  165. `missing IV for initialization segment with method="${this.levelkey.method}" - compliance issue`
  166. );
  167. }
  168.  
  169. /*
  170. Be converted to a Number.
  171. 'initSegment' will become NaN.
  172. NaN, which when converted through ToInt32() -> +0.
  173. ---
  174. Explicitly set sn to resulting value from implicit conversions 'initSegment' values for IV generation.
  175. */
  176. sn = 0;
  177. }
  178. this._decryptdata = this.setDecryptDataFromLevelKey(this.levelkey, sn);
  179. }
  180.  
  181. return this._decryptdata;
  182. }
  183.  
  184. get end(): number {
  185. return this.start + this.duration;
  186. }
  187.  
  188. get endProgramDateTime() {
  189. if (this.programDateTime === null) {
  190. return null;
  191. }
  192.  
  193. if (!Number.isFinite(this.programDateTime)) {
  194. return null;
  195. }
  196.  
  197. const duration = !Number.isFinite(this.duration) ? 0 : this.duration;
  198.  
  199. return this.programDateTime + duration * 1000;
  200. }
  201.  
  202. get encrypted() {
  203. // At the m3u8-parser level we need to add support for manifest signalled keyformats
  204. // when we want the fragment to start reporting that it is encrypted.
  205. // Currently, keyFormat will only be set for identity keys
  206. if (this.decryptdata?.keyFormat && this.decryptdata.uri) {
  207. return true;
  208. }
  209.  
  210. return false;
  211. }
  212.  
  213. /**
  214. * Utility method for parseLevelPlaylist to create an initialization vector for a given segment
  215. * @param {number} segmentNumber - segment number to generate IV with
  216. * @returns {Uint8Array}
  217. */
  218. createInitializationVector(segmentNumber: number): Uint8Array {
  219. const uint8View = new Uint8Array(16);
  220.  
  221. for (let i = 12; i < 16; i++) {
  222. uint8View[i] = (segmentNumber >> (8 * (15 - i))) & 0xff;
  223. }
  224.  
  225. return uint8View;
  226. }
  227.  
  228. /**
  229. * Utility method for parseLevelPlaylist to get a fragment's decryption data from the currently parsed encryption key data
  230. * @param levelkey - a playlist's encryption info
  231. * @param segmentNumber - the fragment's segment number
  232. * @returns {LevelKey} - an object to be applied as a fragment's decryptdata
  233. */
  234. setDecryptDataFromLevelKey(
  235. levelkey: LevelKey,
  236. segmentNumber: number
  237. ): LevelKey {
  238. let decryptdata = levelkey;
  239.  
  240. if (levelkey?.method === 'AES-128' && levelkey.uri && !levelkey.iv) {
  241. decryptdata = LevelKey.fromURI(levelkey.uri);
  242. decryptdata.method = levelkey.method;
  243. decryptdata.iv = this.createInitializationVector(segmentNumber);
  244. decryptdata.keyFormat = 'identity';
  245. }
  246.  
  247. return decryptdata;
  248. }
  249.  
  250. setElementaryStreamInfo(
  251. type: ElementaryStreamTypes,
  252. startPTS: number,
  253. endPTS: number,
  254. startDTS: number,
  255. endDTS: number,
  256. partial: boolean = false
  257. ) {
  258. const { elementaryStreams } = this;
  259. const info = elementaryStreams[type];
  260. if (!info) {
  261. elementaryStreams[type] = {
  262. startPTS,
  263. endPTS,
  264. startDTS,
  265. endDTS,
  266. partial,
  267. };
  268. return;
  269. }
  270.  
  271. info.startPTS = Math.min(info.startPTS, startPTS);
  272. info.endPTS = Math.max(info.endPTS, endPTS);
  273. info.startDTS = Math.min(info.startDTS, startDTS);
  274. info.endDTS = Math.max(info.endDTS, endDTS);
  275. }
  276.  
  277. clearElementaryStreamInfo() {
  278. const { elementaryStreams } = this;
  279. elementaryStreams[ElementaryStreamTypes.AUDIO] = null;
  280. elementaryStreams[ElementaryStreamTypes.VIDEO] = null;
  281. elementaryStreams[ElementaryStreamTypes.AUDIOVIDEO] = null;
  282. }
  283. }
  284.  
  285. export class Part extends BaseSegment {
  286. public readonly fragOffset: number = 0;
  287. public readonly duration: number = 0;
  288. public readonly gap: boolean = false;
  289. public readonly independent: boolean = false;
  290. public readonly relurl: string;
  291. public readonly fragment: Fragment;
  292. public readonly index: number;
  293. public stats: LoadStats = new LoadStats();
  294.  
  295. constructor(
  296. partAttrs: AttrList,
  297. frag: Fragment,
  298. baseurl: string,
  299. index: number,
  300. previous?: Part
  301. ) {
  302. super(baseurl);
  303. this.duration = partAttrs.decimalFloatingPoint('DURATION');
  304. this.gap = partAttrs.bool('GAP');
  305. this.independent = partAttrs.INDEPENDENT
  306. ? partAttrs.bool('INDEPENDENT')
  307. : true;
  308. this.relurl = partAttrs.enumeratedString('URI') as string;
  309. this.fragment = frag;
  310. this.index = index;
  311. const byteRange = partAttrs.enumeratedString('BYTERANGE');
  312. if (byteRange) {
  313. this.setByteRange(byteRange, previous);
  314. }
  315. if (previous) {
  316. this.fragOffset = previous.fragOffset + previous.duration;
  317. }
  318. }
  319.  
  320. get start(): number {
  321. return this.fragment.start + this.fragOffset;
  322. }
  323.  
  324. get end(): number {
  325. return this.start + this.duration;
  326. }
  327.  
  328. get loaded(): boolean {
  329. const { elementaryStreams } = this;
  330. return !!(
  331. elementaryStreams.audio ||
  332. elementaryStreams.video ||
  333. elementaryStreams.audiovideo
  334. );
  335. }
  336. }