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