Home Reference Source

src/remux/passthrough-remuxer.ts

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