Home Reference Source

src/demux/mpegaudio.ts

  1. /**
  2. * MPEG parser helper
  3. */
  4. import { DemuxedAudioTrack } from '../types/demuxer';
  5.  
  6. let chromeVersion: number | null = null;
  7.  
  8. const BitratesMap = [
  9. 32,
  10. 64,
  11. 96,
  12. 128,
  13. 160,
  14. 192,
  15. 224,
  16. 256,
  17. 288,
  18. 320,
  19. 352,
  20. 384,
  21. 416,
  22. 448,
  23. 32,
  24. 48,
  25. 56,
  26. 64,
  27. 80,
  28. 96,
  29. 112,
  30. 128,
  31. 160,
  32. 192,
  33. 224,
  34. 256,
  35. 320,
  36. 384,
  37. 32,
  38. 40,
  39. 48,
  40. 56,
  41. 64,
  42. 80,
  43. 96,
  44. 112,
  45. 128,
  46. 160,
  47. 192,
  48. 224,
  49. 256,
  50. 320,
  51. 32,
  52. 48,
  53. 56,
  54. 64,
  55. 80,
  56. 96,
  57. 112,
  58. 128,
  59. 144,
  60. 160,
  61. 176,
  62. 192,
  63. 224,
  64. 256,
  65. 8,
  66. 16,
  67. 24,
  68. 32,
  69. 40,
  70. 48,
  71. 56,
  72. 64,
  73. 80,
  74. 96,
  75. 112,
  76. 128,
  77. 144,
  78. 160,
  79. ];
  80.  
  81. const SamplingRateMap = [
  82. 44100,
  83. 48000,
  84. 32000,
  85. 22050,
  86. 24000,
  87. 16000,
  88. 11025,
  89. 12000,
  90. 8000,
  91. ];
  92.  
  93. const SamplesCoefficients = [
  94. // MPEG 2.5
  95. [
  96. 0, // Reserved
  97. 72, // Layer3
  98. 144, // Layer2
  99. 12, // Layer1
  100. ],
  101. // Reserved
  102. [
  103. 0, // Reserved
  104. 0, // Layer3
  105. 0, // Layer2
  106. 0, // Layer1
  107. ],
  108. // MPEG 2
  109. [
  110. 0, // Reserved
  111. 72, // Layer3
  112. 144, // Layer2
  113. 12, // Layer1
  114. ],
  115. // MPEG 1
  116. [
  117. 0, // Reserved
  118. 144, // Layer3
  119. 144, // Layer2
  120. 12, // Layer1
  121. ],
  122. ];
  123.  
  124. const BytesInSlot = [
  125. 0, // Reserved
  126. 1, // Layer3
  127. 1, // Layer2
  128. 4, // Layer1
  129. ];
  130.  
  131. export function appendFrame(
  132. track: DemuxedAudioTrack,
  133. data: Uint8Array,
  134. offset: number,
  135. pts: number,
  136. frameIndex: number
  137. ) {
  138. // Using http://www.datavoyage.com/mpgscript/mpeghdr.htm as a reference
  139. if (offset + 24 > data.length) {
  140. return;
  141. }
  142.  
  143. const header = parseHeader(data, offset);
  144. if (header && offset + header.frameLength <= data.length) {
  145. const frameDuration = (header.samplesPerFrame * 90000) / header.sampleRate;
  146. const stamp = pts + frameIndex * frameDuration;
  147. const sample = {
  148. unit: data.subarray(offset, offset + header.frameLength),
  149. pts: stamp,
  150. dts: stamp,
  151. };
  152.  
  153. track.config = [];
  154. track.channelCount = header.channelCount;
  155. track.samplerate = header.sampleRate;
  156. track.samples.push(sample);
  157.  
  158. return { sample, length: header.frameLength };
  159. }
  160. }
  161.  
  162. export function parseHeader(data: Uint8Array, offset: number) {
  163. const mpegVersion = (data[offset + 1] >> 3) & 3;
  164. const mpegLayer = (data[offset + 1] >> 1) & 3;
  165. const bitRateIndex = (data[offset + 2] >> 4) & 15;
  166. const sampleRateIndex = (data[offset + 2] >> 2) & 3;
  167. if (
  168. mpegVersion !== 1 &&
  169. bitRateIndex !== 0 &&
  170. bitRateIndex !== 15 &&
  171. sampleRateIndex !== 3
  172. ) {
  173. const paddingBit = (data[offset + 2] >> 1) & 1;
  174. const channelMode = data[offset + 3] >> 6;
  175. const columnInBitrates =
  176. mpegVersion === 3 ? 3 - mpegLayer : mpegLayer === 3 ? 3 : 4;
  177. const bitRate =
  178. BitratesMap[columnInBitrates * 14 + bitRateIndex - 1] * 1000;
  179. const columnInSampleRates =
  180. mpegVersion === 3 ? 0 : mpegVersion === 2 ? 1 : 2;
  181. const sampleRate =
  182. SamplingRateMap[columnInSampleRates * 3 + sampleRateIndex];
  183. const channelCount = channelMode === 3 ? 1 : 2; // If bits of channel mode are `11` then it is a single channel (Mono)
  184. const sampleCoefficient = SamplesCoefficients[mpegVersion][mpegLayer];
  185. const bytesInSlot = BytesInSlot[mpegLayer];
  186. const samplesPerFrame = sampleCoefficient * 8 * bytesInSlot;
  187. const frameLength =
  188. Math.floor((sampleCoefficient * bitRate) / sampleRate + paddingBit) *
  189. bytesInSlot;
  190.  
  191. if (chromeVersion === null) {
  192. const userAgent = navigator.userAgent || '';
  193. const result = userAgent.match(/Chrome\/(\d+)/i);
  194. chromeVersion = result ? parseInt(result[1]) : 0;
  195. }
  196. const needChromeFix = !!chromeVersion && chromeVersion <= 87;
  197.  
  198. if (
  199. needChromeFix &&
  200. mpegLayer === 2 &&
  201. bitRate >= 224000 &&
  202. channelMode === 0
  203. ) {
  204. // Work around bug in Chromium by setting channelMode to dual-channel (01) instead of stereo (00)
  205. data[offset + 3] = data[offset + 3] | 0x80;
  206. }
  207.  
  208. return { sampleRate, channelCount, frameLength, samplesPerFrame };
  209. }
  210. }
  211.  
  212. export function isHeaderPattern(data: Uint8Array, offset: number): boolean {
  213. return (
  214. data[offset] === 0xff &&
  215. (data[offset + 1] & 0xe0) === 0xe0 &&
  216. (data[offset + 1] & 0x06) !== 0x00
  217. );
  218. }
  219.  
  220. export function isHeader(data: Uint8Array, offset: number): boolean {
  221. // Look for MPEG header | 1111 1111 | 111X XYZX | where X can be either 0 or 1 and Y or Z should be 1
  222. // Layer bits (position 14 and 15) in header should be always different from 0 (Layer I or Layer II or Layer III)
  223. // More info http://www.mp3-tech.org/programmer/frame_header.html
  224. return offset + 1 < data.length && isHeaderPattern(data, offset);
  225. }
  226.  
  227. export function canParse(data: Uint8Array, offset: number): boolean {
  228. const headerSize = 4;
  229.  
  230. return isHeaderPattern(data, offset) && headerSize <= data.length - offset;
  231. }
  232.  
  233. export function probe(data: Uint8Array, offset: number): boolean {
  234. // same as isHeader but we also check that MPEG frame follows last MPEG frame
  235. // or end of data is reached
  236. if (offset + 1 < data.length && isHeaderPattern(data, offset)) {
  237. // MPEG header Length
  238. const headerLength = 4;
  239. // MPEG frame Length
  240. const header = parseHeader(data, offset);
  241. let frameLength = headerLength;
  242. if (header?.frameLength) {
  243. frameLength = header.frameLength;
  244. }
  245.  
  246. const newOffset = offset + frameLength;
  247. return newOffset === data.length || isHeader(data, newOffset);
  248. }
  249. return false;
  250. }