Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add EC3 transmuxer #5352

Merged
merged 2 commits into from
Jun 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build/types/transmuxer
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here too.

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();
theodab marked this conversation as resolved.
Show resolved Hide resolved
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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