Home Reference Source

src/remux/passthrough-remuxer.ts

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