diff --git a/libraries/muxer/src/main/java/androidx/media3/muxer/Boxes.java b/libraries/muxer/src/main/java/androidx/media3/muxer/Boxes.java index 3494b62a113..d036a35ad61 100644 --- a/libraries/muxer/src/main/java/androidx/media3/muxer/Boxes.java +++ b/libraries/muxer/src/main/java/androidx/media3/muxer/Boxes.java @@ -742,6 +742,9 @@ public static ByteBuffer codecSpecificBox(Format format) { return damrBox(/* mode= */ (short) 0x83FF); // mode set: all enabled for AMR-WB case MimeTypes.AUDIO_OPUS: return dOpsBox(format); + case MimeTypes.AUDIO_E_AC3: + case MimeTypes.AUDIO_E_AC3_JOC: + return dec3Box(format); case MimeTypes.AUDIO_RAW: return ByteBuffer.allocate(0); // No codec specific box for raw audio. case MimeTypes.VIDEO_H263: @@ -1590,6 +1593,19 @@ private static ByteBuffer av1CBox(Format format) { return BoxUtils.wrapIntoBox("av1C", ByteBuffer.wrap(csd0)); } + /** + * Returns the dec3 box (EC3SpecificBox) for E-AC-3 / E-AC-3 JOC (Dolby Atmos) audio. + * + *

The dec3 box body is the codec-specific configuration, carried verbatim in csd-0. Stream-copy + * callers must populate {@code format.initializationData} with the source track's raw dec3 payload. + */ + private static ByteBuffer dec3Box(Format format) { + checkArgument( + !format.initializationData.isEmpty(), "csd-0 (dec3 payload) not found for dec3 box."); + byte[] csd0 = format.initializationData.get(0); + return BoxUtils.wrapIntoBox("dec3", ByteBuffer.wrap(csd0)); + } + /** Returns a dvcC/dvwC/dvvC vision box which will be included in dolby vision box. */ private static ByteBuffer doviBox(int profile, byte[] csd) { checkArgument(csd.length > 0, "csd is empty for dovi box."); @@ -1792,6 +1808,9 @@ private static String codecSpecificFourcc(Format format) { return "s263"; case MimeTypes.AUDIO_OPUS: return "Opus"; + case MimeTypes.AUDIO_E_AC3: + case MimeTypes.AUDIO_E_AC3_JOC: + return "ec-3"; case MimeTypes.AUDIO_RAW: if (format.pcmEncoding == C.ENCODING_PCM_16BIT) { return "sowt"; diff --git a/libraries/muxer/src/main/java/androidx/media3/muxer/FragmentedMp4Muxer.java b/libraries/muxer/src/main/java/androidx/media3/muxer/FragmentedMp4Muxer.java index 9ccf0320165..aee1d88b727 100644 --- a/libraries/muxer/src/main/java/androidx/media3/muxer/FragmentedMp4Muxer.java +++ b/libraries/muxer/src/main/java/androidx/media3/muxer/FragmentedMp4Muxer.java @@ -178,7 +178,9 @@ public FragmentedMp4Muxer build() { MimeTypes.AUDIO_AMR_WB, MimeTypes.AUDIO_OPUS, MimeTypes.AUDIO_VORBIS, - MimeTypes.AUDIO_RAW); + MimeTypes.AUDIO_RAW, + MimeTypes.AUDIO_E_AC3, + MimeTypes.AUDIO_E_AC3_JOC); // LINT.ThenChange(Boxes.java:codec_specific_boxes) diff --git a/libraries/muxer/src/main/java/androidx/media3/muxer/Mp4Muxer.java b/libraries/muxer/src/main/java/androidx/media3/muxer/Mp4Muxer.java index 7670afeeefe..9917d5dd5f1 100644 --- a/libraries/muxer/src/main/java/androidx/media3/muxer/Mp4Muxer.java +++ b/libraries/muxer/src/main/java/androidx/media3/muxer/Mp4Muxer.java @@ -404,7 +404,9 @@ public Mp4Muxer build() { MimeTypes.AUDIO_AMR_WB, MimeTypes.AUDIO_OPUS, MimeTypes.AUDIO_VORBIS, - MimeTypes.AUDIO_RAW); + MimeTypes.AUDIO_RAW, + MimeTypes.AUDIO_E_AC3, + MimeTypes.AUDIO_E_AC3_JOC); // LINT.ThenChange(Boxes.java:codec_specific_boxes) diff --git a/libraries/muxer/src/test/java/androidx/media3/muxer/BoxesTest.java b/libraries/muxer/src/test/java/androidx/media3/muxer/BoxesTest.java index 562b81991d4..805b0f9a999 100644 --- a/libraries/muxer/src/test/java/androidx/media3/muxer/BoxesTest.java +++ b/libraries/muxer/src/test/java/androidx/media3/muxer/BoxesTest.java @@ -400,6 +400,63 @@ public void createAudioSampleEntryBox_forVorbis_matchesExpected() throws Excepti MuxerTestUtil.getExpectedMp4DumpFilePath("audio_sample_entry_box_vorbis")); } + @Test + public void createCodecSpecificBox_forEAc3_wrapsDec3PayloadVerbatim() { + byte[] payload = new byte[] {0x0F, (byte) 0x80, 0x00}; + Format format = + FAKE_AUDIO_FORMAT + .buildUpon() + .setSampleMimeType(MimeTypes.AUDIO_E_AC3) + .setInitializationData(ImmutableList.of(payload)) + .build(); + + ByteBuffer box = Boxes.codecSpecificBox(format); + + // Box layout: 4 bytes size + 4 bytes "dec3" type + payload bytes. + assertThat(box.remaining()).isEqualTo(8 + payload.length); + byte[] typeBytes = new byte[4]; + box.position(4); + box.get(typeBytes); + assertThat(new String(typeBytes)).isEqualTo("dec3"); + byte[] actual = new byte[payload.length]; + box.get(actual); + assertThat(actual).isEqualTo(payload); + } + + @Test + public void createCodecSpecificBox_forEAc3Joc_wrapsDec3PayloadVerbatim() { + byte[] payload = new byte[] {0x0F, (byte) 0x80, 0x00}; + Format format = + FAKE_AUDIO_FORMAT + .buildUpon() + .setSampleMimeType(MimeTypes.AUDIO_E_AC3_JOC) + .setInitializationData(ImmutableList.of(payload)) + .build(); + + ByteBuffer box = Boxes.codecSpecificBox(format); + + assertThat(box.remaining()).isEqualTo(8 + payload.length); + byte[] typeBytes = new byte[4]; + box.position(4); + box.get(typeBytes); + assertThat(new String(typeBytes)).isEqualTo("dec3"); + byte[] actual = new byte[payload.length]; + box.get(actual); + assertThat(actual).isEqualTo(payload); + } + + @Test + public void createCodecSpecificBox_forEAc3WithNoCsd_throws() { + Format format = + FAKE_AUDIO_FORMAT + .buildUpon() + .setSampleMimeType(MimeTypes.AUDIO_E_AC3) + .setInitializationData(ImmutableList.of()) + .build(); + + assertThrows(IllegalArgumentException.class, () -> Boxes.codecSpecificBox(format)); + } + @Test public void createAudioSampleEntryBox_withUnknownAudioFormat_throws() { // The audio format contains an unknown MIME type.