Skip to content

Commit bd97fb1

Browse files
nrcrastNicholas Crastavelad
authored
fix: Add optional content workaround to force ENCA.ChannelCount to 2 for EAC-3 Audio init segments (#8517)
In short, we've discovered a packaging issue on some of our streams where the ChannelCount field of the enca box contains an invalid value of 7. According to the dolby spec (and confirmed by their validator): - This field should always be 2 - This field should be ignored by the decoder/user agent From talking to the Dolby folks, it seems that some devices erroneously do not ignore this value. Chromecast is one of said devices. Playing back an EAC-3 audio track on Chromecast packaged this way causes an immediate fatal decoding failure. -- rewriting that box content client side to force a value of 2 fixes playback without any other issues. This fix will take the init segment (if it's audio and the workaround is enabled), and replace the ChannelCount with 2 if and only if the original audio codec was EC3. Given that these streams were packaged by Bento4, I figure it may be possible for this to happen for others. The workaround is generic enough that I think it's reasonable to have as an optional content workaround in Shaka. Here's some output from the Dolby validator indicating the requirement here: ``` ERROR: For an [EC3] stream, in MP4 EC3SampleEntryBox, the "ChannelCount" must set to 2. (ETSI TS 102 366 V1.4.1 (2017-09) F.5.1) ``` --------- Co-authored-by: Nicholas Crast <[email protected]> Co-authored-by: Álvaro Velad Galván <[email protected]>
1 parent ddd97f0 commit bd97fb1

File tree

7 files changed

+109
-0
lines changed

7 files changed

+109
-0
lines changed

demo/config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,8 @@ shakaDemo.Config = class {
694694
.addBoolInput_('Force Transmux', 'mediaSource.forceTransmux')
695695
.addBoolInput_('Insert fake encryption in init segments when needed ' +
696696
'by the platform.', 'mediaSource.insertFakeEncryptionInInit')
697+
.addBoolInput_('Force enca.ChannelCount to 2 for EC-3 audio if ' +
698+
'needed by the platform.', 'mediaSource.correctEc3Enca')
697699
.addSelectInput_(
698700
'Codec Switching Strategy',
699701
'mediaSource.codecSwitchingStrategy',

externs/shaka/player.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2183,6 +2183,7 @@ shaka.extern.NetworkingConfiguration;
21832183
* addExtraFeaturesToSourceBuffer: function(string): string,
21842184
* forceTransmux: boolean,
21852185
* insertFakeEncryptionInInit: boolean,
2186+
* correctEc3Enca: boolean,
21862187
* modifyCueCallback: shaka.extern.TextParser.ModifyCueCallback,
21872188
* dispatchAllEmsgBoxes: boolean,
21882189
* useSourceElements: boolean,
@@ -2221,6 +2222,13 @@ shaka.extern.NetworkingConfiguration;
22212222
* <br><br>
22222223
* <br>
22232224
* Defaults to <code>true</code>.
2225+
* @property {boolean} correctEc3Enca
2226+
* If true, will apply a work-around for Audio init segments signaling
2227+
* EC-3 codec with protection. This will force the ChannelCount field
2228+
* of the 'enca' box to be set to 2, which is required via the dolby
2229+
* spec.
2230+
* <br>
2231+
* This value defaults to <code>false</code>.
22242232
* @property {shaka.extern.TextParser.ModifyCueCallback} modifyCueCallback
22252233
* A callback called for each cue after it is parsed, but right before it
22262234
* is appended to the presentation.

lib/media/content_workarounds.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,62 @@ goog.require('shaka.util.Uint8ArrayUtils');
2222
* A collection of methods to work around content issues on various platforms.
2323
*/
2424
shaka.media.ContentWorkarounds = class {
25+
/**
26+
* For EAC-3 audio, the "enca" box ChannelCount field
27+
* MUST always be set to 2.
28+
* See ETSI TS 102 366 V1.2.1 Sec F.3
29+
*
30+
* Clients SHOULD ignore this value; however, it has been discovered that
31+
* some user agents don't, and instead, invalid values here can cause decoder
32+
* failures to be thrown (this has been observed with Chromecast).
33+
*
34+
* Given that this value MUST be hard-coded to 2, and that clients "SHALL"
35+
* ignore the value, it seems safe to manipulate the value to be 2 even when
36+
* the packager has provided a different value.
37+
* @param {!BufferSource} initSegmentBuffer
38+
* @return {!Uint8Array}
39+
*/
40+
static correctEnca(initSegmentBuffer) {
41+
const initSegment = shaka.util.BufferUtils.toUint8(initSegmentBuffer);
42+
const modifiedInitSegment = initSegment;
43+
44+
/**
45+
* Skip reserved bytes: 6
46+
* Skip data_reference_index: 2
47+
* Skip reserved bytes: 8
48+
*/
49+
const encaChannelCountOffset = 16;
50+
51+
/** @type {?DataView} */
52+
let encaBoxDataView = null;
53+
54+
new shaka.util.Mp4Parser()
55+
.box('moov', shaka.util.Mp4Parser.children)
56+
.box('trak', shaka.util.Mp4Parser.children)
57+
.box('mdia', shaka.util.Mp4Parser.children)
58+
.box('minf', shaka.util.Mp4Parser.children)
59+
.box('stbl', shaka.util.Mp4Parser.children)
60+
.fullBox('stsd', shaka.util.Mp4Parser.sampleDescription)
61+
.box('enca', (box) => {
62+
encaBoxDataView = box.reader.getDataView();
63+
return shaka.util.Mp4Parser.audioSampleEntry(box);
64+
})
65+
.box('sinf', shaka.util.Mp4Parser.children)
66+
.box('frma', (box) => {
67+
box.parser.stop();
68+
const {codec} = shaka.util.Mp4BoxParsers.parseFRMA(box.reader);
69+
if (codec === 'ec-3' &&
70+
encaBoxDataView &&
71+
encaBoxDataView.getUint16(encaChannelCountOffset) !== 2
72+
) {
73+
encaBoxDataView.setUint16(encaChannelCountOffset, 2);
74+
}
75+
})
76+
.parse(initSegment);
77+
78+
return modifiedInitSegment;
79+
}
80+
2581
/**
2682
* Transform the init segment into a new init segment buffer that indicates
2783
* encryption. If the init segment already indicates encryption, return the

lib/media/media_source_engine.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2165,6 +2165,7 @@ shaka.media.MediaSourceEngine = class {
21652165
* @private
21662166
*/
21672167
workAroundBrokenPlatforms_(stream, segment, reference, contentType) {
2168+
const ContentType = shaka.util.ManifestParserUtils.ContentType;
21682169
const Platform = shaka.util.Platform;
21692170

21702171
const isMp4 = shaka.util.MimeUtils.getContainerType(
@@ -2182,6 +2183,12 @@ shaka.media.MediaSourceEngine = class {
21822183
}
21832184
const uri = reference ? reference.getUris()[0] : null;
21842185

2186+
if (this.config_.correctEc3Enca &&
2187+
isInitSegment &&
2188+
contentType === ContentType.AUDIO) {
2189+
segment = shaka.media.ContentWorkarounds.correctEnca(segment);
2190+
}
2191+
21852192
// If:
21862193
// 1. the configuration tells to insert fake encryption,
21872194
// 2. and this is an init segment or media segment,

lib/util/player_configuration.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,7 @@ shaka.util.PlayerConfiguration = class {
433433
},
434434
forceTransmux: false,
435435
insertFakeEncryptionInInit: true,
436+
correctEc3Enca: false,
436437
modifyCueCallback: (cue, uri) => {
437438
return shaka.util.ConfigUtils.referenceParametersAndReturn(
438439
[cue, uri],

test/media/content_workarounds_unit.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,41 @@ describe('ContentWorkarounds', () => {
257257
.parse(faked);
258258
expect(spy).toHaveBeenCalled();
259259
});
260+
261+
describe('enca correction', () => {
262+
const initSegmentUri = '/base/test/test/assets/incorrect-enca-init.mp4';
263+
/** @type {!ArrayBuffer} */
264+
let initSegmentData;
265+
266+
const getChannelCount = (initSegment) => {
267+
let channelCount = 0;
268+
new shaka.util.Mp4Parser()
269+
.box('moov', shaka.util.Mp4Parser.children)
270+
.box('trak', shaka.util.Mp4Parser.children)
271+
.box('mdia', shaka.util.Mp4Parser.children)
272+
.box('minf', shaka.util.Mp4Parser.children)
273+
.box('stbl', shaka.util.Mp4Parser.children)
274+
.fullBox('stsd', shaka.util.Mp4Parser.sampleDescription)
275+
.box('enca', (box) => {
276+
const data = shaka.util.Mp4BoxParsers
277+
.audioSampleEntry(box.reader);
278+
channelCount = data.channelCount;
279+
}).parse(initSegment);
280+
return channelCount;
281+
};
282+
283+
beforeEach(async () => {
284+
initSegmentData = await shaka.test.Util.fetch(initSegmentUri);
285+
});
286+
287+
it('should replace the ChannelCount in the enca box', () => {
288+
const ContentWorkarounds = shaka.media.ContentWorkarounds;
289+
expect(getChannelCount(initSegmentData)).toBe(7);
290+
291+
const modified = ContentWorkarounds.correctEnca(initSegmentData);
292+
expect(getChannelCount(modified)).toBe(2);
293+
});
294+
});
260295
});
261296

262297
/**
1.54 KB
Binary file not shown.

0 commit comments

Comments
 (0)