Source: lib/transmuxer/mp3_transmuxer.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.transmuxer.Mp3Transmuxer');
  7. goog.require('shaka.media.Capabilities');
  8. goog.require('shaka.transmuxer.MpegAudio');
  9. goog.require('shaka.transmuxer.TransmuxerEngine');
  10. goog.require('shaka.util.BufferUtils');
  11. goog.require('shaka.util.Error');
  12. goog.require('shaka.util.Id3Utils');
  13. goog.require('shaka.util.ManifestParserUtils');
  14. goog.require('shaka.util.Mp4Generator');
  15. goog.require('shaka.util.Uint8ArrayUtils');
  16. /**
  17. * @implements {shaka.extern.Transmuxer}
  18. * @export
  19. */
  20. shaka.transmuxer.Mp3Transmuxer = class {
  21. /**
  22. * @param {string} mimeType
  23. */
  24. constructor(mimeType) {
  25. /** @private {string} */
  26. this.originalMimeType_ = mimeType;
  27. /** @private {number} */
  28. this.frameIndex_ = 0;
  29. /** @private {!Map.<string, !Uint8Array>} */
  30. this.initSegments = new Map();
  31. }
  32. /**
  33. * @override
  34. * @export
  35. */
  36. destroy() {
  37. this.initSegments.clear();
  38. }
  39. /**
  40. * Check if the mime type and the content type is supported.
  41. * @param {string} mimeType
  42. * @param {string=} contentType
  43. * @return {boolean}
  44. * @override
  45. * @export
  46. */
  47. isSupported(mimeType, contentType) {
  48. const Capabilities = shaka.media.Capabilities;
  49. if (!this.isMpegContainer_(mimeType)) {
  50. return false;
  51. }
  52. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  53. return Capabilities.isTypeSupported(
  54. this.convertCodecs(ContentType.AUDIO, mimeType));
  55. }
  56. /**
  57. * Check if the mimetype is 'audio/mpeg'.
  58. * @param {string} mimeType
  59. * @return {boolean}
  60. * @private
  61. */
  62. isMpegContainer_(mimeType) {
  63. return mimeType.toLowerCase().split(';')[0] == 'audio/mpeg';
  64. }
  65. /**
  66. * @override
  67. * @export
  68. */
  69. convertCodecs(contentType, mimeType) {
  70. if (this.isMpegContainer_(mimeType)) {
  71. return 'audio/mp4; codecs="mp3"';
  72. }
  73. return mimeType;
  74. }
  75. /**
  76. * @override
  77. * @export
  78. */
  79. getOriginalMimeType() {
  80. return this.originalMimeType_;
  81. }
  82. /**
  83. * @override
  84. * @export
  85. */
  86. transmux(data, stream, reference, duration) {
  87. const MpegAudio = shaka.transmuxer.MpegAudio;
  88. const Uint8ArrayUtils = shaka.util.Uint8ArrayUtils;
  89. const uint8ArrayData = shaka.util.BufferUtils.toUint8(data);
  90. const id3Data = shaka.util.Id3Utils.getID3Data(uint8ArrayData);
  91. let offset = id3Data.length;
  92. for (; offset < uint8ArrayData.length; offset++) {
  93. if (MpegAudio.probe(uint8ArrayData, offset)) {
  94. break;
  95. }
  96. }
  97. const timescale = 90000;
  98. let firstHeader;
  99. /** @type {!Array.<shaka.util.Mp4Generator.Mp4Sample>} */
  100. const samples = [];
  101. while (offset < uint8ArrayData.length) {
  102. const header = MpegAudio.parseHeader(uint8ArrayData, offset);
  103. if (!header) {
  104. return Promise.reject(new shaka.util.Error(
  105. shaka.util.Error.Severity.CRITICAL,
  106. shaka.util.Error.Category.MEDIA,
  107. shaka.util.Error.Code.TRANSMUXING_FAILED,
  108. reference ? reference.getUris()[0] : null));
  109. }
  110. if (!firstHeader) {
  111. firstHeader = header;
  112. }
  113. if (offset + header.frameLength <= uint8ArrayData.length) {
  114. samples.push({
  115. data: uint8ArrayData.subarray(offset, offset + header.frameLength),
  116. size: header.frameLength,
  117. duration: MpegAudio.MPEG_AUDIO_SAMPLE_PER_FRAME,
  118. cts: 0,
  119. flags: {
  120. isLeading: 0,
  121. isDependedOn: 0,
  122. hasRedundancy: 0,
  123. degradPrio: 0,
  124. dependsOn: 2,
  125. isNonSync: 0,
  126. },
  127. });
  128. }
  129. offset += header.frameLength;
  130. }
  131. if (!firstHeader) {
  132. return Promise.reject(new shaka.util.Error(
  133. shaka.util.Error.Severity.CRITICAL,
  134. shaka.util.Error.Category.MEDIA,
  135. shaka.util.Error.Code.TRANSMUXING_FAILED,
  136. reference ? reference.getUris()[0] : null));
  137. }
  138. /** @type {number} */
  139. const sampleRate = firstHeader.sampleRate;
  140. /** @type {number} */
  141. const frameDuration =
  142. firstHeader.samplesPerFrame * timescale / firstHeader.sampleRate;
  143. /** @type {number} */
  144. const baseMediaDecodeTime = this.frameIndex_ * frameDuration;
  145. /** @type {shaka.util.Mp4Generator.StreamInfo} */
  146. const streamInfo = {
  147. id: stream.id,
  148. type: shaka.util.ManifestParserUtils.ContentType.AUDIO,
  149. codecs: 'mp3',
  150. encrypted: stream.encrypted && stream.drmInfos.length > 0,
  151. timescale: sampleRate,
  152. duration: duration,
  153. videoNalus: [],
  154. audioConfig: new Uint8Array([]),
  155. videoConfig: new Uint8Array([]),
  156. hSpacing: 0,
  157. vSpacing: 0,
  158. data: {
  159. sequenceNumber: this.frameIndex_,
  160. baseMediaDecodeTime: baseMediaDecodeTime,
  161. samples: samples,
  162. },
  163. stream: stream,
  164. };
  165. const mp4Generator = new shaka.util.Mp4Generator([streamInfo]);
  166. let initSegment;
  167. const initSegmentKey = stream.id + '_' + reference.discontinuitySequence;
  168. if (!this.initSegments.has(initSegmentKey)) {
  169. initSegment = mp4Generator.initSegment();
  170. this.initSegments.set(initSegmentKey, initSegment);
  171. } else {
  172. initSegment = this.initSegments.get(initSegmentKey);
  173. }
  174. const segmentData = mp4Generator.segmentData();
  175. this.frameIndex_++;
  176. const transmuxData = Uint8ArrayUtils.concat(initSegment, segmentData);
  177. return Promise.resolve(transmuxData);
  178. }
  179. };
  180. shaka.transmuxer.TransmuxerEngine.registerTransmuxer(
  181. 'audio/mpeg',
  182. () => new shaka.transmuxer.Mp3Transmuxer('audio/mpeg'),
  183. shaka.transmuxer.TransmuxerEngine.PluginPriority.FALLBACK);