Skip to content

Commit

Permalink
feat: Add EC3 transmuxer (shaka-project#5352)
Browse files Browse the repository at this point in the history
  • Loading branch information
avelad committed Jun 27, 2023
1 parent 226ffa9 commit 7d24e14
Show file tree
Hide file tree
Showing 5 changed files with 349 additions and 0 deletions.
2 changes: 2 additions & 0 deletions build/types/transmuxer
Expand Up @@ -4,6 +4,8 @@
+../../lib/transmuxer/ac3.js
+../../lib/transmuxer/ac3_transmuxer.js
+../../lib/transmuxer/adts.js
+../../lib/transmuxer/ec3.js
+../../lib/transmuxer/ec3_transmuxer.js
+../../lib/transmuxer/mp3_transmuxer.js
+../../lib/transmuxer/mpeg_audio.js
+../../lib/transmuxer/muxjs_transmuxer.js
103 changes: 103 additions & 0 deletions lib/transmuxer/ec3.js
@@ -0,0 +1,103 @@
/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

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

goog.require('shaka.util.ExpGolomb');


/**
* EC3 utils
*/
shaka.transmuxer.Ec3 = 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 (!shaka.transmuxer.Ec3.probe(data, offset)) {
return null;
}

const gb = new shaka.util.ExpGolomb(data.subarray(offset + 2));
// Skip stream_type
gb.skipBits(2);
// Skip sub_stream_id
gb.skipBits(3);
const frameLength = (gb.readBits(11) + 1) << 1;
let samplingRateCode = gb.readBits(2);
let sampleRate = null;
let numBlocksCode = null;
if (samplingRateCode == 0x03) {
samplingRateCode = gb.readBits(2);
sampleRate = [24000, 22060, 16000][samplingRateCode];
numBlocksCode = 3;
} else {
sampleRate = [48000, 44100, 32000][samplingRateCode];
numBlocksCode = gb.readBits(2);
}

const channelMode = gb.readBits(3);
const lowFrequencyEffectsChannelOn = gb.readBits(1);
const bitStreamIdentification = gb.readBits(5);

if (offset + frameLength > data.byteLength) {
return null;
}

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

const numBlocksMap = [1, 2, 3, 6];

const numBlocks = numBlocksMap[numBlocksCode];

const dataRateSub =
Math.floor((frameLength * sampleRate) / (numBlocks * 16));

const config = new Uint8Array([
((dataRateSub & 0x1FE0) >> 5),
((dataRateSub & 0x001F) << 3), // num_ind_sub = zero
(sampleRate << 6) | (bitStreamIdentification << 1) | (0 << 0),
(0 << 7) | (0 << 4) |
(channelMode << 1) | (lowFrequencyEffectsChannelOn << 0),
(0 << 5) | (0 << 1) | (0 << 0),
]);

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

/**
* @param {!Uint8Array} data
* @param {!number} offset
* @return {boolean}
*/
static probe(data, offset) {
// search 16-bit 0x0B77 syncword
const syncword = (data[offset] << 8) | (data[offset + 1] << 0);
if (syncword === 0x0B77) {
return true;
} else {
return false;
}
}
};

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

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

goog.require('shaka.media.Capabilities');
goog.require('shaka.transmuxer.Ec3');
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.Ec3Transmuxer = 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.isEc3Container_(mimeType)) {
return false;
}
const ContentType = shaka.util.ManifestParserUtils.ContentType;
return Capabilities.isTypeSupported(
this.convertCodecs(ContentType.AUDIO, mimeType));
}


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


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


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


/**
* @override
* @export
*/
transmux(data, stream, reference, duration) {
const Ec3 = shaka.transmuxer.Ec3;
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 (Ec3.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 = Ec3.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: Ec3.EC3_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/ec3',
() => new shaka.transmuxer.Ec3Transmuxer('audio/ec3'),
shaka.transmuxer.TransmuxerEngine.PluginPriority.FALLBACK);
2 changes: 2 additions & 0 deletions shaka-player.uncompiled.js
Expand Up @@ -69,6 +69,8 @@ goog.require('shaka.transmuxer.AacTransmuxer');
goog.require('shaka.transmuxer.Ac3');
goog.require('shaka.transmuxer.Ac3Transmuxer');
goog.require('shaka.transmuxer.ADTS');
goog.require('shaka.transmuxer.Ec3');
goog.require('shaka.transmuxer.Ec3Transmuxer');
goog.require('shaka.transmuxer.Mp3Transmuxer');
goog.require('shaka.transmuxer.MpegAudio');
goog.require('shaka.transmuxer.TransmuxerEngine');
Expand Down
34 changes: 34 additions & 0 deletions test/transmuxer/transmuxer_integration.js
Expand Up @@ -145,6 +145,40 @@ describe('Transmuxer Player', () => {
await player.unload();
});

it('raw EC3', async () => {
if (!MediaSource.isTypeSupported('audio/mp4; codecs="ec-3"')) {
return;
}
// It seems that AC3 on Edge Windows from github actions is not working
// (in the lab AC3 is working). The AC3 detection is currently hard-coded
// to true, which leads to a failure in GitHub's environment.
// We must enable this, once it is resolved:
// https://bugs.chromium.org/p/chromium/issues/detail?id=1450313
const chromeVersion = shaka.util.Platform.chromeVersion();
if (shaka.util.Platform.isEdge() &&
chromeVersion && chromeVersion <= 116) {
return;
}

// eslint-disable-next-line max-len
const url = 'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/a3/prog_index.m3u8';

await player.load(url, /* startTime= */ null,
/* mimeType= */ undefined);
video.play();
expect(player.isLive()).toBe(false);

// Wait for the video to start playback. If it takes longer than 10
// seconds, fail the test.
await waiter.waitForMovementOrFailOnTimeout(video, 10);

// Play for 15 seconds, but stop early if the video ends. If it takes
// longer than 45 seconds, fail the test.
await waiter.waitUntilPlayheadReachesOrFailOnTimeout(video, 15, 45);

await player.unload();
});

it('muxed H.264+AAC in TS', async () => {
// eslint-disable-next-line max-len
const url = 'https://cf-sf-video.wmspanel.com/local/raw/BigBuckBunny_320x180.mp4/playlist.m3u8';
Expand Down

0 comments on commit 7d24e14

Please sign in to comment.