Skip to content

Commit

Permalink
feat: Add AC3 transmuxer (shaka-project#5297)
Browse files Browse the repository at this point in the history
  • Loading branch information
avelad committed Jun 24, 2023
1 parent 263a17b commit 6f83997
Show file tree
Hide file tree
Showing 7 changed files with 411 additions and 7 deletions.
2 changes: 2 additions & 0 deletions build/types/transmuxer
@@ -1,6 +1,8 @@
# Optional plugins related to transmuxer.

+../../lib/transmuxer/aac_transmuxer.js
+../../lib/transmuxer/ac3.js
+../../lib/transmuxer/ac3_transmuxer.js
+../../lib/transmuxer/adts.js
+../../lib/transmuxer/mp3_transmuxer.js
+../../lib/transmuxer/mpeg_audio.js
Expand Down
151 changes: 151 additions & 0 deletions lib/transmuxer/ac3.js
@@ -0,0 +1,151 @@
/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

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


/**
* AC3 utils
*/
shaka.transmuxer.Ac3 = class {
/**
* @param {!Uint8Array} data
* @param {!number} offset
* @return {?{sampleRate: number, channelCount: number,
* audioConfig: !Uint8Array, frameLength: number}}
*/
static parseFrame(data, offset) {
if (offset + 8 > data.length) {
// not enough bytes left
return null;
}

if (data[offset] !== 0x0b || data[offset + 1] !== 0x77) {
// invalid magic
return null;
}

// get sample rate
const samplingRateCode = data[offset + 4] >> 6;
if (samplingRateCode >= 3) {
// invalid sampling rate
return null;
}

const samplingRateMap = [48000, 44100, 32000];

// get frame size
const frameSizeCode = data[offset + 4] & 0x3f;
const frameSizeMap = [
64, 69, 96, 64, 70, 96, 80, 87, 120, 80, 88, 120, 96, 104, 144, 96, 105,
144, 112, 121, 168, 112, 122, 168, 128, 139, 192, 128, 140, 192, 160,
174, 240, 160, 175, 240, 192, 208, 288, 192, 209, 288, 224, 243, 336,
224, 244, 336, 256, 278, 384, 256, 279, 384, 320, 348, 480, 320, 349,
480, 384, 417, 576, 384, 418, 576, 448, 487, 672, 448, 488, 672, 512,
557, 768, 512, 558, 768, 640, 696, 960, 640, 697, 960, 768, 835, 1152,
768, 836, 1152, 896, 975, 1344, 896, 976, 1344, 1024, 1114, 1536, 1024,
1115, 1536, 1152, 1253, 1728, 1152, 1254, 1728, 1280, 1393, 1920, 1280,
1394, 1920,
];

const frameLength = frameSizeMap[frameSizeCode * 3 + samplingRateCode] * 2;
if (offset + frameLength > data.length) {
return null;
}

// get channel count
const channelMode = data[offset + 6] >> 5;
let skipCount = 0;
if (channelMode === 2) {
skipCount += 2;
} else {
if ((channelMode & 1) && channelMode !== 1) {
skipCount += 2;
}
if (channelMode & 4) {
skipCount += 2;
}
}

const lowFrequencyEffectsChannelOn =
(((data[offset + 6] << 8) | data[offset + 7]) >> (12 - skipCount)) & 1;

const channelsMap = [2, 1, 2, 3, 3, 4, 4, 5];

// Audio config for DAC3 box
const bitStreamIdentification = data[offset + 5] >> 3;
const bitStreamMode = data[offset + 5] & 7;

const config = new Uint8Array([
(samplingRateCode << 6) |
(bitStreamIdentification << 1) |
(bitStreamMode >> 2),
((bitStreamMode & 3) << 6) |
(channelMode << 3) |
(lowFrequencyEffectsChannelOn << 2) |
(frameSizeCode >> 4),
(frameSizeCode << 4) & 0xe0,
]);

return {
sampleRate: samplingRateMap[samplingRateCode],
channelCount: channelsMap[channelMode] + lowFrequencyEffectsChannelOn,
audioConfig: config,
frameLength: frameLength,
};
}

/**
* @param {!Uint8Array} data
* @param {!number} offset
* @return {boolean}
*/
static canParse(data, offset) {
return offset + 64 < data.length;
}

/**
* @param {!Uint8Array} data
* @param {!number} offset
* @return {boolean}
*/
static probe(data, offset) {
// look for the ac-3 sync bytes
if (data[offset] === 0x0b &&
data[offset + 1] === 0x77) {
// check the bsid (bitStreamIdentification) to confirm ac-3
let bsid = 0;
let numBits = 5;
offset += numBits;
/** @type {?number} */
let temp = null;
/** @type {?number} */
let mask = null;
/** @type {?number} */
let byte = null;
while (numBits > 0) {
byte = data[offset];
// read remaining bits, upto 8 bits at a time
const bits = Math.min(numBits, 8);
const shift = 8 - bits;
mask = (0xff000000 >>> (24 + shift)) << shift;
temp = (byte & mask) >> shift;
bsid = !bsid ? temp : (bsid << bits) | temp;
offset += 1;
numBits -= bits;
}
if (bsid < 16) {
return true;
}
}
return false;
}
};

/**
* @const {number}
*/
shaka.transmuxer.Ac3.AC3_SAMPLES_PER_FRAME = 1536;
208 changes: 208 additions & 0 deletions lib/transmuxer/ac3_transmuxer.js
@@ -0,0 +1,208 @@
/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

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

goog.require('shaka.media.Capabilities');
goog.require('shaka.transmuxer.Ac3');
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.Ac3Transmuxer = class {
/**
* @param {string} mimeType
*/
constructor(mimeType) {
/** @private {string} */
this.originalMimeType_ = mimeType;

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

/** @private {!Map.<number, !Uint8Array>} */
this.initSegments = new Map();
}


/**
* @override
* @export
*/
destroy() {
this.initSegments.clear();
}


/**
* 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.isAc3Container_(mimeType)) {
return false;
}
const ContentType = shaka.util.ManifestParserUtils.ContentType;
return Capabilities.isTypeSupported(
this.convertCodecs(ContentType.AUDIO, mimeType));
}


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


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


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


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

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

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

let timestamp = reference.endTime * 1000;

const frames = shaka.util.Id3Utils.getID3Frames(id3Data);
if (frames.length && reference) {
const metadataTimestamp = frames.find((frame) => {
return frame.description ===
'com.apple.streaming.transportStreamTimestamp';
});
if (metadataTimestamp) {
timestamp = /** @type {!number} */(metadataTimestamp.data);
}
}

/** @type {number} */
let sampleRate = 0;

/** @type {!Uint8Array} */
let audioConfig = new Uint8Array([]);

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

while (offset < uint8ArrayData.length) {
const frame = Ac3.parseFrame(uint8ArrayData, offset);
if (!frame) {
return Promise.reject(new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.TRANSMUXING_FAILED));
}
stream.audioSamplingRate = frame.sampleRate;
stream.channelsCount = frame.channelCount;
sampleRate = frame.sampleRate;
audioConfig = frame.audioConfig;

const frameData = uint8ArrayData.subarray(
offset, offset + frame.frameLength);

samples.push({
data: frameData,
size: frame.frameLength,
duration: Ac3.AC3_SAMPLES_PER_FRAME,
cts: 0,
flags: {
isLeading: 0,
isDependedOn: 0,
hasRedundancy: 0,
degradPrio: 0,
dependsOn: 2,
isNonSync: 0,
},
});
offset += frame.frameLength;
}
/** @type {number} */
const baseMediaDecodeTime = Math.floor(timestamp * sampleRate / 1000);

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

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

shaka.transmuxer.TransmuxerEngine.registerTransmuxer(
'audio/ac3',
() => new shaka.transmuxer.Ac3Transmuxer('audio/ac3'),
shaka.transmuxer.TransmuxerEngine.PluginPriority.FALLBACK);
10 changes: 6 additions & 4 deletions lib/util/mp4_generator.js
Expand Up @@ -310,12 +310,14 @@ shaka.util.Mp4Generator = class {
break;
case ContentType.AUDIO:
if (this.stream_.mimeType === 'audio/mpeg' ||
this.stream_.codecs.includes('mp3') ||
this.stream_.codecs.includes('mp4a.40.34')) {
this.stream_.codecs.includes('mp3') ||
this.stream_.codecs.includes('mp4a.40.34')) {
bytes = this.mp3_();
} else if (this.stream_.codecs.includes('ac-3')) {
} else if (this.stream_.mimeType === 'audio/ac3' ||
this.stream_.codecs.includes('ac-3')) {
bytes = this.ac3_();
} else if (this.stream_.codecs.includes('ec-3')) {
} else if (this.stream_.mimeType === 'audio/ec3' ||
this.stream_.codecs.includes('ec-3')) {
bytes = this.ec3_();
} else {
bytes = this.mp4a_();
Expand Down

0 comments on commit 6f83997

Please sign in to comment.