Home Reference Source

src/demux/mp4demuxer.ts

  1. /**
  2. * MP4 demuxer
  3. */
  4. import {
  5. Demuxer,
  6. DemuxerResult,
  7. PassthroughTrack,
  8. DemuxedAudioTrack,
  9. DemuxedUserdataTrack,
  10. DemuxedMetadataTrack,
  11. KeyData,
  12. MetadataSchema,
  13. } from '../types/demuxer';
  14. import {
  15. findBox,
  16. segmentValidRange,
  17. appendUint8Array,
  18. parseEmsg,
  19. parseSamples,
  20. parseInitSegment,
  21. RemuxerTrackIdConfig,
  22. } from '../utils/mp4-tools';
  23. import { dummyTrack } from './dummy-demuxed-track';
  24. import type { HlsEventEmitter } from '../events';
  25. import type { HlsConfig } from '../config';
  26.  
  27. const emsgSchemePattern = /\/emsg[-/]ID3/i;
  28.  
  29. class MP4Demuxer implements Demuxer {
  30. private remainderData: Uint8Array | null = null;
  31. private timeOffset: number = 0;
  32. private config: HlsConfig;
  33. private videoTrack?: PassthroughTrack;
  34. private audioTrack?: DemuxedAudioTrack;
  35. private id3Track?: DemuxedMetadataTrack;
  36. private txtTrack?: DemuxedUserdataTrack;
  37.  
  38. constructor(observer: HlsEventEmitter, config: HlsConfig) {
  39. this.config = config;
  40. }
  41.  
  42. public resetTimeStamp() {}
  43.  
  44. public resetInitSegment(
  45. initSegment: Uint8Array | undefined,
  46. audioCodec: string | undefined,
  47. videoCodec: string | undefined,
  48. trackDuration: number
  49. ) {
  50. const videoTrack = (this.videoTrack = dummyTrack(
  51. 'video',
  52. 1
  53. ) as PassthroughTrack);
  54. const audioTrack = (this.audioTrack = dummyTrack(
  55. 'audio',
  56. 1
  57. ) as DemuxedAudioTrack);
  58. const captionTrack = (this.txtTrack = dummyTrack(
  59. 'text',
  60. 1
  61. ) as DemuxedUserdataTrack);
  62.  
  63. this.id3Track = dummyTrack('id3', 1) as DemuxedMetadataTrack;
  64. this.timeOffset = 0;
  65.  
  66. if (!initSegment || !initSegment.byteLength) {
  67. return;
  68. }
  69. const initData = parseInitSegment(initSegment);
  70.  
  71. if (initData.video) {
  72. const { id, timescale, codec } = initData.video;
  73. videoTrack.id = id;
  74. videoTrack.timescale = captionTrack.timescale = timescale;
  75. videoTrack.codec = codec;
  76. }
  77.  
  78. if (initData.audio) {
  79. const { id, timescale, codec } = initData.audio;
  80. audioTrack.id = id;
  81. audioTrack.timescale = timescale;
  82. audioTrack.codec = codec;
  83. }
  84.  
  85. captionTrack.id = RemuxerTrackIdConfig.text;
  86. videoTrack.sampleDuration = 0;
  87. videoTrack.duration = audioTrack.duration = trackDuration;
  88. }
  89.  
  90. public resetContiguity(): void {}
  91.  
  92. static probe(data: Uint8Array) {
  93. // ensure we find a moof box in the first 16 kB
  94. data = data.length > 16384 ? data.subarray(0, 16384) : data;
  95. return findBox(data, ['moof']).length > 0;
  96. }
  97.  
  98. public demux(data: Uint8Array, timeOffset: number): DemuxerResult {
  99. this.timeOffset = timeOffset;
  100. // Load all data into the avc track. The CMAF remuxer will look for the data in the samples object; the rest of the fields do not matter
  101. let videoSamples = data;
  102. const videoTrack = this.videoTrack as PassthroughTrack;
  103. const textTrack = this.txtTrack as DemuxedUserdataTrack;
  104. if (this.config.progressive) {
  105. // Split the bytestream into two ranges: one encompassing all data up until the start of the last moof, and everything else.
  106. // This is done to guarantee that we're sending valid data to MSE - when demuxing progressively, we have no guarantee
  107. // that the fetch loader gives us flush moof+mdat pairs. If we push jagged data to MSE, it will throw an exception.
  108. if (this.remainderData) {
  109. videoSamples = appendUint8Array(this.remainderData, data);
  110. }
  111. const segmentedData = segmentValidRange(videoSamples);
  112. this.remainderData = segmentedData.remainder;
  113. videoTrack.samples = segmentedData.valid || new Uint8Array();
  114. } else {
  115. videoTrack.samples = videoSamples;
  116. }
  117.  
  118. const id3Track = this.extractID3Track(videoTrack, timeOffset);
  119. textTrack.samples = parseSamples(timeOffset, videoTrack);
  120.  
  121. return {
  122. videoTrack,
  123. audioTrack: this.audioTrack as DemuxedAudioTrack,
  124. id3Track,
  125. textTrack: this.txtTrack as DemuxedUserdataTrack,
  126. };
  127. }
  128.  
  129. public flush() {
  130. const timeOffset = this.timeOffset;
  131. const videoTrack = this.videoTrack as PassthroughTrack;
  132. const textTrack = this.txtTrack as DemuxedUserdataTrack;
  133. videoTrack.samples = this.remainderData || new Uint8Array();
  134. this.remainderData = null;
  135.  
  136. const id3Track = this.extractID3Track(videoTrack, this.timeOffset);
  137. textTrack.samples = parseSamples(timeOffset, videoTrack);
  138.  
  139. return {
  140. videoTrack,
  141. audioTrack: dummyTrack() as DemuxedAudioTrack,
  142. id3Track,
  143. textTrack: dummyTrack() as DemuxedUserdataTrack,
  144. };
  145. }
  146.  
  147. private extractID3Track(
  148. videoTrack: PassthroughTrack,
  149. timeOffset: number
  150. ): DemuxedMetadataTrack {
  151. const id3Track = this.id3Track as DemuxedMetadataTrack;
  152. if (videoTrack.samples.length) {
  153. const emsgs = findBox(videoTrack.samples, ['emsg']);
  154. if (emsgs) {
  155. emsgs.forEach((data: Uint8Array) => {
  156. const emsgInfo = parseEmsg(data);
  157. if (emsgSchemePattern.test(emsgInfo.schemeIdUri)) {
  158. const pts = Number.isFinite(emsgInfo.presentationTime)
  159. ? emsgInfo.presentationTime! / emsgInfo.timeScale
  160. : timeOffset +
  161. emsgInfo.presentationTimeDelta! / emsgInfo.timeScale;
  162. let duration =
  163. emsgInfo.eventDuration === 0xffffffff
  164. ? Number.POSITIVE_INFINITY
  165. : emsgInfo.eventDuration / emsgInfo.timeScale;
  166. // Safari takes anything <= 0.001 seconds and maps it to Infinity
  167. if (duration <= 0.001) {
  168. duration = Number.POSITIVE_INFINITY;
  169. }
  170. const payload = emsgInfo.payload;
  171. id3Track.samples.push({
  172. data: payload,
  173. len: payload.byteLength,
  174. dts: pts,
  175. pts: pts,
  176. type: MetadataSchema.emsg,
  177. duration: duration,
  178. });
  179. }
  180. });
  181. }
  182. }
  183. return id3Track;
  184. }
  185.  
  186. demuxSampleAes(
  187. data: Uint8Array,
  188. keyData: KeyData,
  189. timeOffset: number
  190. ): Promise<DemuxerResult> {
  191. return Promise.reject(
  192. new Error('The MP4 demuxer does not support SAMPLE-AES decryption')
  193. );
  194. }
  195.  
  196. destroy() {}
  197. }
  198.  
  199. export default MP4Demuxer;