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. // The Media Initialization Section for this segment
  143. public initSegment: Fragment | null = null;
  144.  
  145. constructor(type: PlaylistLevelType, baseurl: string) {
  146. super(baseurl);
  147. this.type = type;
  148. }
  149.  
  150. get decryptdata(): LevelKey | null {
  151. if (!this.levelkey && !this._decryptdata) {
  152. return null;
  153. }
  154.  
  155. if (!this._decryptdata && this.levelkey) {
  156. let sn = this.sn;
  157. if (typeof sn !== 'number') {
  158. // We are fetching decryption data for a initialization segment
  159. // If the segment was encrypted with AES-128
  160. // It must have an IV defined. We cannot substitute the Segment Number in.
  161. if (
  162. this.levelkey &&
  163. this.levelkey.method === 'AES-128' &&
  164. !this.levelkey.iv
  165. ) {
  166. logger.warn(
  167. `missing IV for initialization segment with method="${this.levelkey.method}" - compliance issue`
  168. );
  169. }
  170.  
  171. /*
  172. Be converted to a Number.
  173. 'initSegment' will become NaN.
  174. NaN, which when converted through ToInt32() -> +0.
  175. ---
  176. Explicitly set sn to resulting value from implicit conversions 'initSegment' values for IV generation.
  177. */
  178. sn = 0;
  179. }
  180. this._decryptdata = this.setDecryptDataFromLevelKey(this.levelkey, sn);
  181. }
  182.  
  183. return this._decryptdata;
  184. }
  185.  
  186. get end(): number {
  187. return this.start + this.duration;
  188. }
  189.  
  190. get endProgramDateTime() {
  191. if (this.programDateTime === null) {
  192. return null;
  193. }
  194.  
  195. if (!Number.isFinite(this.programDateTime)) {
  196. return null;
  197. }
  198.  
  199. const duration = !Number.isFinite(this.duration) ? 0 : this.duration;
  200.  
  201. return this.programDateTime + duration * 1000;
  202. }
  203.  
  204. get encrypted() {
  205. // At the m3u8-parser level we need to add support for manifest signalled keyformats
  206. // when we want the fragment to start reporting that it is encrypted.
  207. // Currently, keyFormat will only be set for identity keys
  208. if (this.decryptdata?.keyFormat && this.decryptdata.uri) {
  209. return true;
  210. }
  211.  
  212. return false;
  213. }
  214.  
  215. /**
  216. * Utility method for parseLevelPlaylist to create an initialization vector for a given segment
  217. * @param {number} segmentNumber - segment number to generate IV with
  218. * @returns {Uint8Array}
  219. */
  220. createInitializationVector(segmentNumber: number): Uint8Array {
  221. const uint8View = new Uint8Array(16);
  222.  
  223. for (let i = 12; i < 16; i++) {
  224. uint8View[i] = (segmentNumber >> (8 * (15 - i))) & 0xff;
  225. }
  226.  
  227. return uint8View;
  228. }
  229.  
  230. /**
  231. * Utility method for parseLevelPlaylist to get a fragment's decryption data from the currently parsed encryption key data
  232. * @param levelkey - a playlist's encryption info
  233. * @param segmentNumber - the fragment's segment number
  234. * @returns {LevelKey} - an object to be applied as a fragment's decryptdata
  235. */
  236. setDecryptDataFromLevelKey(
  237. levelkey: LevelKey,
  238. segmentNumber: number
  239. ): LevelKey {
  240. let decryptdata = levelkey;
  241.  
  242. if (levelkey?.method === 'AES-128' && levelkey.uri && !levelkey.iv) {
  243. decryptdata = LevelKey.fromURI(levelkey.uri);
  244. decryptdata.method = levelkey.method;
  245. decryptdata.iv = this.createInitializationVector(segmentNumber);
  246. decryptdata.keyFormat = 'identity';
  247. }
  248.  
  249. return decryptdata;
  250. }
  251.  
  252. setElementaryStreamInfo(
  253. type: ElementaryStreamTypes,
  254. startPTS: number,
  255. endPTS: number,
  256. startDTS: number,
  257. endDTS: number,
  258. partial: boolean = false
  259. ) {
  260. const { elementaryStreams } = this;
  261. const info = elementaryStreams[type];
  262. if (!info) {
  263. elementaryStreams[type] = {
  264. startPTS,
  265. endPTS,
  266. startDTS,
  267. endDTS,
  268. partial,
  269. };
  270. return;
  271. }
  272.  
  273. info.startPTS = Math.min(info.startPTS, startPTS);
  274. info.endPTS = Math.max(info.endPTS, endPTS);
  275. info.startDTS = Math.min(info.startDTS, startDTS);
  276. info.endDTS = Math.max(info.endDTS, endDTS);
  277. }
  278.  
  279. clearElementaryStreamInfo() {
  280. const { elementaryStreams } = this;
  281. elementaryStreams[ElementaryStreamTypes.AUDIO] = null;
  282. elementaryStreams[ElementaryStreamTypes.VIDEO] = null;
  283. elementaryStreams[ElementaryStreamTypes.AUDIOVIDEO] = null;
  284. }
  285. }
  286.  
  287. export class Part extends BaseSegment {
  288. public readonly fragOffset: number = 0;
  289. public readonly duration: number = 0;
  290. public readonly gap: boolean = false;
  291. public readonly independent: boolean = false;
  292. public readonly relurl: string;
  293. public readonly fragment: Fragment;
  294. public readonly index: number;
  295. public stats: LoadStats = new LoadStats();
  296.  
  297. constructor(
  298. partAttrs: AttrList,
  299. frag: Fragment,
  300. baseurl: string,
  301. index: number,
  302. previous?: Part
  303. ) {
  304. super(baseurl);
  305. this.duration = partAttrs.decimalFloatingPoint('DURATION');
  306. this.gap = partAttrs.bool('GAP');
  307. this.independent = partAttrs.bool('INDEPENDENT');
  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. }