Skip to content

Commit

Permalink
feat: Add MP3 transmuxer
Browse files Browse the repository at this point in the history
  • Loading branch information
avelad committed May 3, 2023
1 parent bb40d3b commit f77fc0a
Show file tree
Hide file tree
Showing 10 changed files with 472 additions and 4 deletions.
2 changes: 2 additions & 0 deletions build/types/transmuxer
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Optional plugins related to transmuxer.

+../../lib/transmuxer/mp3_transmuxer.js
+../../lib/transmuxer/mpeg_audio.js
+../../lib/transmuxer/muxjs_transmuxer.js
2 changes: 1 addition & 1 deletion docs/tutorials/upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,4 @@ application:
(deprecated in v4.3.0)

- Plugin changes:
- `Transmuxer` plugins now has two new parameters in `transmux()` method.
- `Transmuxer` plugins now has three new parameters in `transmux()` method.
3 changes: 2 additions & 1 deletion externs/shaka/transmuxer.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,10 @@ shaka.extern.Transmuxer = class {
* @param {shaka.extern.Stream} stream
* @param {?shaka.media.SegmentReference} reference The segment reference, or
* null for init segments
* @param {number} duration
* @return {!Promise.<!Uint8Array>}
*/
transmux(data, stream, reference) {}
transmux(data, stream, reference, duration) {}
};


Expand Down
2 changes: 1 addition & 1 deletion lib/media/media_source_engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -771,7 +771,7 @@ shaka.media.MediaSourceEngine = class {

if (this.transmuxers_[contentType]) {
data = await this.transmuxers_[contentType].transmux(
data, stream, reference);
data, stream, reference, this.mediaSource_.duration);
}

data = this.workAroundBrokenPlatforms_(
Expand Down
195 changes: 195 additions & 0 deletions lib/transmuxer/mp3_transmuxer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

goog.provide('shaka.transmuxer.Mp3Transmuxer');

goog.require('shaka.media.Capabilities');
goog.require('shaka.transmuxer.MpegAudio');
goog.require('shaka.transmuxer.TransmuxerEngine');
goog.require('shaka.util.BufferUtils');
goog.require('shaka.util.Error');
goog.require('shaka.util.Id3Utils');
goog.require('shaka.util.ManifestParserUtils');
goog.require('shaka.util.Mp4Generator');
goog.require('shaka.util.Uint8ArrayUtils');


/**
* @implements {shaka.extern.Transmuxer}
* @export
*/
shaka.transmuxer.Mp3Transmuxer = class {
/**
* @param {string} mimeType
*/
constructor(mimeType) {
/** @private {string} */
this.originalMimeType_ = mimeType;

/** @private {number} */
this.frameIndex_ = 0;

/** @private {?Uint8Array} */
this.initSegment = null;
}


/**
* @override
* @export
*/
destroy() {
// Nothing
}


/**
* Check if the mime type and the content type is supported.
* @param {string} mimeType
* @param {string=} contentType
* @return {boolean}
* @override
* @export
*/
isSupported(mimeType, contentType) {
const Capabilities = shaka.media.Capabilities;

if (!this.isMpegContainer_(mimeType)) {
return false;
}
const ContentType = shaka.util.ManifestParserUtils.ContentType;
return Capabilities.isTypeSupported(
this.convertCodecs(ContentType.AUDIO, mimeType));
}


/**
* Check if the mimetype is 'audio/mpeg'.
* @param {string} mimeType
* @return {boolean}
* @private
*/
isMpegContainer_(mimeType) {
return mimeType.toLowerCase().split(';')[0] == 'audio/mpeg';
}


/**
* @override
* @export
*/
convertCodecs(contentType, mimeType) {
if (this.isMpegContainer_(mimeType)) {
return 'audio/mp4; codecs="mp3"';
}
return mimeType;
}


/**
* @override
* @export
*/
getOrginalMimeType() {
return this.originalMimeType_;
}


/**
* @override
* @export
*/
transmux(data, stream, reference, duration) {
const MpegAudio = shaka.transmuxer.MpegAudio;
const Uint8ArrayUtils = shaka.util.Uint8ArrayUtils;

const uint8ArrayData = shaka.util.BufferUtils.toUint8(data);

const id3Data = shaka.util.Id3Utils.getID3Data(uint8ArrayData);
let offset = id3Data.length;
for (let length = uint8ArrayData.length; offset < length; offset++) {
if (MpegAudio.probe(uint8ArrayData, offset)) {
break;
}
}

const timescale = 90000;
let firstHeader;

/** @type {!Array.<shaka.util.Mp4Generator.Mp4Sample>} */
const samples = [];

while (offset < uint8ArrayData.length) {
const header = MpegAudio.parseHeader(uint8ArrayData, offset);
if (!header) {
return Promise.reject(new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.TRANSMUXING_FAILED));
}
if (!firstHeader) {
firstHeader = header;
}
if (offset + header.frameLength <= uint8ArrayData.length) {
samples.push({
data: uint8ArrayData.subarray(offset, offset + header.frameLength),
size: header.frameLength,
duration: MpegAudio.MPEG_AUDIO_SAMPLE_PER_FRAME,
cts: 0,
flags: {
isLeading: 0,
isDependedOn: 0,
hasRedundancy: 0,
degradPrio: 0,
dependsOn: 2,
isNonSync: 0,
},
});
}
offset += header.frameLength;
}
if (!firstHeader) {
return Promise.reject(new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.TRANSMUXING_FAILED));
}
/** @type {number} */
const sampleRate = firstHeader.sampleRate;
/** @type {number} */
const frameDuration =
firstHeader.samplesPerFrame * timescale / firstHeader.sampleRate;
/** @type {number} */
const baseMediaDecodeTime = this.frameIndex_ * frameDuration;

/** @type {shaka.util.Mp4Generator.StreamInfo} */
const streamInfo = {
timescale: sampleRate,
duration: duration,
videoNalus: [],
data: {
sequenceNumber: this.frameIndex_,
baseMediaDecodeTime: baseMediaDecodeTime,
samples: samples,
},
stream: stream,
};
const mp4Generator = new shaka.util.Mp4Generator(streamInfo);
if (!this.initSegment) {
this.initSegment = mp4Generator.initSegment();
}
const segmentData = mp4Generator.segmentData();

this.frameIndex_++;
const transmuxData = Uint8ArrayUtils.concat(this.initSegment, segmentData);
return Promise.resolve(transmuxData);
}
};

shaka.transmuxer.TransmuxerEngine.registerTransmuxer(
'audio/mpeg',
() => new shaka.transmuxer.Mp3Transmuxer('audio/mpeg'),
shaka.transmuxer.TransmuxerEngine.PluginPriority.FALLBACK);
Loading

0 comments on commit f77fc0a

Please sign in to comment.