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