Home Reference Source

src/remux/passthrough-remuxer.ts

  1. import type { InitData, InitDataTrack } from '../utils/mp4-tools';
  2. import {
  3. getDuration,
  4. getStartDTS,
  5. offsetStartDTS,
  6. parseInitSegment,
  7. } from '../utils/mp4-tools';
  8. import { ElementaryStreamTypes } from '../loader/fragment';
  9. import { logger } from '../utils/logger';
  10. import type { TrackSet } from '../types/track';
  11. import type {
  12. InitSegmentData,
  13. RemuxedTrack,
  14. Remuxer,
  15. RemuxerResult,
  16. } from '../types/remuxer';
  17. import type {
  18. DemuxedAudioTrack,
  19. DemuxedMetadataTrack,
  20. DemuxedUserdataTrack,
  21. PassthroughVideoTrack,
  22. } from '../types/demuxer';
  23.  
  24. class PassThroughRemuxer implements Remuxer {
  25. private emitInitSegment: boolean = false;
  26. private audioCodec?: string;
  27. private videoCodec?: string;
  28. private initData?: InitData;
  29. private initPTS?: number;
  30. private initTracks?: TrackSet;
  31. private lastEndDTS: number | null = null;
  32.  
  33. destroy() {}
  34.  
  35. resetTimeStamp(defaultInitPTS) {
  36. this.initPTS = defaultInitPTS;
  37. this.lastEndDTS = null;
  38. }
  39.  
  40. resetNextTimestamp() {
  41. this.lastEndDTS = null;
  42. }
  43.  
  44. resetInitSegment(
  45. initSegment: Uint8Array | undefined,
  46. audioCodec: string | undefined,
  47. videoCodec: string | undefined
  48. ) {
  49. this.audioCodec = audioCodec;
  50. this.videoCodec = videoCodec;
  51. this.generateInitSegment(initSegment);
  52. this.emitInitSegment = true;
  53. }
  54.  
  55. generateInitSegment(initSegment: Uint8Array | undefined): void {
  56. let { audioCodec, videoCodec } = this;
  57. if (!initSegment || !initSegment.byteLength) {
  58. this.initTracks = undefined;
  59. this.initData = undefined;
  60. return;
  61. }
  62. const initData = (this.initData = parseInitSegment(initSegment));
  63.  
  64. // Get codec from initSegment or fallback to default
  65. if (!audioCodec) {
  66. audioCodec = getParsedTrackCodec(
  67. initData.audio,
  68. ElementaryStreamTypes.AUDIO
  69. );
  70. }
  71.  
  72. if (!videoCodec) {
  73. videoCodec = getParsedTrackCodec(
  74. initData.video,
  75. ElementaryStreamTypes.VIDEO
  76. );
  77. }
  78.  
  79. const tracks: TrackSet = {};
  80. if (initData.audio && initData.video) {
  81. tracks.audiovideo = {
  82. container: 'video/mp4',
  83. codec: audioCodec + ',' + videoCodec,
  84. initSegment,
  85. id: 'main',
  86. };
  87. } else if (initData.audio) {
  88. tracks.audio = {
  89. container: 'audio/mp4',
  90. codec: audioCodec,
  91. initSegment,
  92. id: 'audio',
  93. };
  94. } else if (initData.video) {
  95. tracks.video = {
  96. container: 'video/mp4',
  97. codec: videoCodec,
  98. initSegment,
  99. id: 'main',
  100. };
  101. } else {
  102. logger.warn(
  103. '[passthrough-remuxer.ts]: initSegment does not contain moov or trak boxes.'
  104. );
  105. }
  106. this.initTracks = tracks;
  107. }
  108.  
  109. remux(
  110. audioTrack: DemuxedAudioTrack,
  111. videoTrack: PassthroughVideoTrack,
  112. id3Track: DemuxedMetadataTrack,
  113. textTrack: DemuxedUserdataTrack,
  114. timeOffset: number
  115. ): RemuxerResult {
  116. let { initPTS, lastEndDTS } = this;
  117. const result: RemuxerResult = {
  118. audio: undefined,
  119. video: undefined,
  120. text: textTrack,
  121. id3: id3Track,
  122. initSegment: undefined,
  123. };
  124.  
  125. // If we haven't yet set a lastEndDTS, or it was reset, set it to the provided timeOffset. We want to use the
  126. // lastEndDTS over timeOffset whenever possible; during progressive playback, the media source will not update
  127. // the media duration (which is what timeOffset is provided as) before we need to process the next chunk.
  128. if (!Number.isFinite(lastEndDTS!)) {
  129. lastEndDTS = this.lastEndDTS = timeOffset || 0;
  130. }
  131.  
  132. // The binary segment data is added to the videoTrack in the mp4demuxer. We don't check to see if the data is only
  133. // audio or video (or both); adding it to video was an arbitrary choice.
  134. const data = videoTrack.samples;
  135. if (!data || !data.length) {
  136. return result;
  137. }
  138.  
  139. const initSegment: InitSegmentData = {
  140. initPTS: undefined,
  141. timescale: 1,
  142. };
  143. let initData = this.initData;
  144. if (!initData || !initData.length) {
  145. this.generateInitSegment(data);
  146. initData = this.initData;
  147. }
  148. if (!initData || !initData.length) {
  149. // We can't remux if the initSegment could not be generated
  150. logger.warn('[passthrough-remuxer.ts]: Failed to generate initSegment.');
  151. return result;
  152. }
  153. if (this.emitInitSegment) {
  154. initSegment.tracks = this.initTracks as TrackSet;
  155. this.emitInitSegment = false;
  156. }
  157.  
  158. if (!Number.isFinite(initPTS!)) {
  159. this.initPTS = initSegment.initPTS = initPTS = computeInitPTS(
  160. initData,
  161. data,
  162. lastEndDTS
  163. );
  164. }
  165.  
  166. const duration = getDuration(data, initData);
  167. const startDTS = lastEndDTS as number;
  168. const endDTS = duration + startDTS;
  169. offsetStartDTS(initData, data, initPTS as number);
  170.  
  171. if (duration > 0) {
  172. this.lastEndDTS = endDTS;
  173. } else {
  174. logger.warn('Duration parsed from mp4 should be greater than zero');
  175. this.resetNextTimestamp();
  176. }
  177.  
  178. const hasAudio = !!initData.audio;
  179. const hasVideo = !!initData.video;
  180.  
  181. let type: any = '';
  182. if (hasAudio) {
  183. type += 'audio';
  184. }
  185.  
  186. if (hasVideo) {
  187. type += 'video';
  188. }
  189.  
  190. const track: RemuxedTrack = {
  191. data1: data,
  192. startPTS: startDTS,
  193. startDTS,
  194. endPTS: endDTS,
  195. endDTS,
  196. type,
  197. hasAudio,
  198. hasVideo,
  199. nb: 1,
  200. dropped: 0,
  201. };
  202.  
  203. result.audio = track.type === 'audio' ? track : undefined;
  204. result.video = track.type !== 'audio' ? track : undefined;
  205. result.text = textTrack;
  206. result.id3 = id3Track;
  207. result.initSegment = initSegment;
  208.  
  209. return result;
  210. }
  211. }
  212.  
  213. const computeInitPTS = (initData, data, timeOffset) =>
  214. getStartDTS(initData, data) - timeOffset;
  215.  
  216. function getParsedTrackCodec(
  217. track: InitDataTrack | undefined,
  218. type: ElementaryStreamTypes.AUDIO | ElementaryStreamTypes.VIDEO
  219. ): string {
  220. const parsedCodec = track?.codec;
  221. if (parsedCodec && parsedCodec.length > 4) {
  222. return parsedCodec;
  223. }
  224. // Since mp4-tools cannot parse full codec string (see 'TODO: Parse codec details'... in mp4-tools)
  225. // Provide defaults based on codec type
  226. // This allows for some playback of some fmp4 playlists without CODECS defined in manifest
  227. if (parsedCodec === 'hvc1') {
  228. return 'hvc1.1.c.L120.90';
  229. }
  230. if (parsedCodec === 'av01') {
  231. return 'av01.0.04M.08';
  232. }
  233. if (parsedCodec === 'avc1' || type === ElementaryStreamTypes.VIDEO) {
  234. return 'avc1.42e01e';
  235. }
  236. return 'mp4a.40.5';
  237. }
  238. export default PassThroughRemuxer;