diff --git a/libraries/muxer/src/androidTest/java/androidx/media3/muxer/Mp4MuxerEndToEndParameterizedAndroidTest.java b/libraries/muxer/src/androidTest/java/androidx/media3/muxer/Mp4MuxerEndToEndParameterizedAndroidTest.java index 5055f88009..7741e9bb1b 100644 --- a/libraries/muxer/src/androidTest/java/androidx/media3/muxer/Mp4MuxerEndToEndParameterizedAndroidTest.java +++ b/libraries/muxer/src/androidTest/java/androidx/media3/muxer/Mp4MuxerEndToEndParameterizedAndroidTest.java @@ -55,6 +55,7 @@ public class Mp4MuxerEndToEndParameterizedAndroidTest { private static final String AMR_NB = "bbb_mono_8kHz_12.2kbps_amrnb.3gp"; private static final String AMR_WB = "bbb_mono_16kHz_23.05kbps_amrwb.3gp"; private static final String MPEG4_MP4 = "bbb_176x144_192kbps_15fps_mpeg4.mp4"; + private static final String OPUS_OGG = "bbb_6ch_8kHz_opus.ogg"; @Parameters(name = "{0}") public static ImmutableList mediaSamples() { @@ -68,7 +69,8 @@ public class Mp4MuxerEndToEndParameterizedAndroidTest { AV1_MP4, AMR_NB, AMR_WB, - MPEG4_MP4); + MPEG4_MP4, + OPUS_OGG); } @Parameter public @MonotonicNonNull String inputFile; 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 cae311bdb3..6ad4d98869 100644 --- a/libraries/muxer/src/main/java/androidx/media3/muxer/Boxes.java +++ b/libraries/muxer/src/main/java/androidx/media3/muxer/Boxes.java @@ -550,6 +550,8 @@ import java.util.List; return damrBox(/* mode= */ (short) 0x83FF); // mode set: all enabled for AMR-WB case MimeTypes.VIDEO_H263: return d263Box(); + case MimeTypes.AUDIO_OPUS: + return dOpsBox(format); case MimeTypes.VIDEO_H264: return avcCBox(format); case MimeTypes.VIDEO_H265: @@ -1345,6 +1347,8 @@ import java.util.List; return "sawb"; case MimeTypes.VIDEO_H263: return "s263"; + case MimeTypes.AUDIO_OPUS: + return "Opus"; case MimeTypes.VIDEO_H264: return "avc1"; case MimeTypes.VIDEO_H265: @@ -1444,6 +1448,24 @@ import java.util.List; return BoxUtils.wrapIntoBox("damr", contents); } + /** Returns the audio dOps box for Opus codec as per RFC-7845: 5.1. */ + private static ByteBuffer dOpsBox(Format format) { + checkArgument(!format.initializationData.isEmpty()); + + int opusHeaderLength = 8; + byte[] csd0 = format.initializationData.get(0); + checkArgument( + csd0.length >= opusHeaderLength, + "As csd0 contains 'OpusHead' in first 8 bytes, csd0 length should be greater than 8"); + ByteBuffer contents = ByteBuffer.allocate(csd0.length); + // Skip 8 bytes containing "OpusHead". + contents.put( + /* src */ csd0, /* offset */ opusHeaderLength, /* length */ csd0.length - opusHeaderLength); + contents.flip(); + + return BoxUtils.wrapIntoBox("dOps", contents); + } + /** Packs a three-letter language code into a short, packing 3x5 bits. */ private static short languageCodeFromString(@Nullable String code) { if (code == null) { 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 5ab501fd31..fc56869723 100644 --- a/libraries/muxer/src/main/java/androidx/media3/muxer/FragmentedMp4Muxer.java +++ b/libraries/muxer/src/main/java/androidx/media3/muxer/FragmentedMp4Muxer.java @@ -35,7 +35,7 @@ import java.nio.ByteBuffer; /** * A muxer for creating a fragmented MP4 file. * - *

The muxer supports writing H263, H264, H265 and AV1 video, AAC audio and metadata. + *

The muxer supports writing H263, H264, H265 and AV1 video, AAC and Opus audio, and metadata. * *

All the operations are performed on the caller thread. * 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 6f2172eddc..2877a9eac7 100644 --- a/libraries/muxer/src/main/java/androidx/media3/muxer/Mp4Muxer.java +++ b/libraries/muxer/src/main/java/androidx/media3/muxer/Mp4Muxer.java @@ -54,7 +54,7 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNull; /** * A muxer for creating an MP4 container file. * - *

The muxer supports writing H263, H264, H265 and AV1 video, AAC audio and metadata. + *

The muxer supports writing H263, H264, H265 and AV1 video, AAC and Opus audio, and metadata. * *

All the operations are performed on the caller thread. * 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 0d6062bc39..77e0cc9691 100644 --- a/libraries/muxer/src/test/java/androidx/media3/muxer/BoxesTest.java +++ b/libraries/muxer/src/test/java/androidx/media3/muxer/BoxesTest.java @@ -296,6 +296,31 @@ public class BoxesTest { context, dumpableBox, getExpectedDumpFilePath("audio_sample_entry_box_sawb")); } + @Test + public void createAudioSampleEntryBox_forOpus_matchesExpected() throws Exception { + Format format = + new Format.Builder() + .setPeakBitrate(128000) + .setSampleRate(48000) + .setId(3) + .setSampleMimeType(MimeTypes.AUDIO_OPUS) + .setChannelCount(6) + .setAverageBitrate(128000) + .setLanguage("```") + .setMaxInputSize(502) + .setInitializationData( + ImmutableList.of( + BaseEncoding.base16() + .decode("4F7075734865616401063801401F00000000010402000401020305"))) + .build(); + + ByteBuffer audioSampleEntryBox = Boxes.audioSampleEntry(format); + + DumpableMp4Box dumpableBox = new DumpableMp4Box(audioSampleEntryBox); + DumpFileAsserts.assertOutput( + context, dumpableBox, getExpectedDumpFilePath("audio_sample_entry_box_opus")); + } + @Test public void createAudioSampleEntryBox_withUnknownAudioFormat_throws() { // The audio format contains an unknown MIME type. diff --git a/libraries/test_data/src/test/assets/media/mp4/bbb_6ch_8kHz_opus.ogg b/libraries/test_data/src/test/assets/media/mp4/bbb_6ch_8kHz_opus.ogg new file mode 100644 index 0000000000..2696bad278 Binary files /dev/null and b/libraries/test_data/src/test/assets/media/mp4/bbb_6ch_8kHz_opus.ogg differ diff --git a/libraries/test_data/src/test/assets/muxerdumps/audio_sample_entry_box_opus.dump b/libraries/test_data/src/test/assets/muxerdumps/audio_sample_entry_box_opus.dump new file mode 100644 index 0000000000..4d7f7beaf7 --- /dev/null +++ b/libraries/test_data/src/test/assets/muxerdumps/audio_sample_entry_box_opus.dump @@ -0,0 +1,2 @@ +Opus (63 bytes): + Data = length 55, hash CF9222D3 diff --git a/libraries/test_data/src/test/assets/muxerdumps/bbb_6ch_8kHz_opus.ogg.dump b/libraries/test_data/src/test/assets/muxerdumps/bbb_6ch_8kHz_opus.ogg.dump new file mode 100644 index 0000000000..5222799a74 --- /dev/null +++ b/libraries/test_data/src/test/assets/muxerdumps/bbb_6ch_8kHz_opus.ogg.dump @@ -0,0 +1,628 @@ +seekMap: + isSeekable = true + duration = 2993500 + getPosition(0) = [[timeUs=0, position=400052]] + getPosition(1) = [[timeUs=1, position=400052]] + getPosition(1496750) = [[timeUs=1496750, position=449844]] + getPosition(2993500) = [[timeUs=2993500, position=499769]] +numberOfTracks = 1 +track 0: + total output bytes = 100872 + sample count = 151 + format 0: + id = 1 + sampleMimeType = audio/opus + maxInputSize = 1185 + channelCount = 6 + sampleRate = 48000 + language = ``` + metadata = entries=[Mp4Timestamp: creation time=100000000, modification time=500000000, timescale=10000] + initializationData: + data = length 27, hash 9EE6F879 + data = length 8, hash CA22068C + data = length 8, hash 79C07075 + sample 0: + time = 0 + flags = 1 + data = length 949, hash E49C1A9D + sample 1: + time = 13500 + flags = 1 + data = length 608, hash F65C6881 + sample 2: + time = 33500 + flags = 1 + data = length 650, hash 914AF7E1 + sample 3: + time = 53500 + flags = 1 + data = length 603, hash B3EC7A47 + sample 4: + time = 73500 + flags = 1 + data = length 650, hash 162CA2A3 + sample 5: + time = 93500 + flags = 1 + data = length 678, hash 192785A9 + sample 6: + time = 113500 + flags = 1 + data = length 685, hash 96C2F2BB + sample 7: + time = 133500 + flags = 1 + data = length 665, hash 5922AAEB + sample 8: + time = 153500 + flags = 1 + data = length 646, hash 8619B832 + sample 9: + time = 173500 + flags = 1 + data = length 644, hash AFAFE536 + sample 10: + time = 193500 + flags = 1 + data = length 629, hash 9E73B0B7 + sample 11: + time = 213500 + flags = 1 + data = length 610, hash 669587E0 + sample 12: + time = 233500 + flags = 1 + data = length 663, hash D7642DB7 + sample 13: + time = 253500 + flags = 1 + data = length 648, hash 6D0CD0D2 + sample 14: + time = 273500 + flags = 1 + data = length 642, hash 412DC4E2 + sample 15: + time = 293500 + flags = 1 + data = length 607, hash F9582E56 + sample 16: + time = 313500 + flags = 1 + data = length 645, hash 657B023B + sample 17: + time = 333500 + flags = 1 + data = length 672, hash AA362E4A + sample 18: + time = 353500 + flags = 1 + data = length 629, hash 9E527BB + sample 19: + time = 373500 + flags = 1 + data = length 646, hash F3B96232 + sample 20: + time = 393500 + flags = 1 + data = length 611, hash 9B3061C + sample 21: + time = 413500 + flags = 1 + data = length 664, hash 50774FBE + sample 22: + time = 433500 + flags = 1 + data = length 650, hash 52AA7B95 + sample 23: + time = 453500 + flags = 1 + data = length 636, hash F5CB417D + sample 24: + time = 473500 + flags = 1 + data = length 665, hash 34C938BB + sample 25: + time = 493500 + flags = 1 + data = length 626, hash 5CD7E3F8 + sample 26: + time = 513500 + flags = 1 + data = length 651, hash AF7BDD5F + sample 27: + time = 533500 + flags = 1 + data = length 635, hash FF8E92FE + sample 28: + time = 553500 + flags = 1 + data = length 655, hash CC0821A7 + sample 29: + time = 573500 + flags = 1 + data = length 636, hash 1BDC263F + sample 30: + time = 593500 + flags = 1 + data = length 641, hash 5ECE0258 + sample 31: + time = 613500 + flags = 1 + data = length 632, hash FA762551 + sample 32: + time = 633500 + flags = 1 + data = length 656, hash 97F5501A + sample 33: + time = 653500 + flags = 1 + data = length 657, hash 462517A5 + sample 34: + time = 673500 + flags = 1 + data = length 651, hash 8863CCAD + sample 35: + time = 693500 + flags = 1 + data = length 648, hash B0330C37 + sample 36: + time = 713500 + flags = 1 + data = length 674, hash 60E12F33 + sample 37: + time = 733500 + flags = 1 + data = length 674, hash DEFEB445 + sample 38: + time = 753500 + flags = 1 + data = length 583, hash C8326130 + sample 39: + time = 773500 + flags = 1 + data = length 677, hash 5FADD0 + sample 40: + time = 793500 + flags = 1 + data = length 637, hash CB97E2B8 + sample 41: + time = 813500 + flags = 1 + data = length 637, hash 7D230BE4 + sample 42: + time = 833500 + flags = 1 + data = length 651, hash 230EE90B + sample 43: + time = 853500 + flags = 1 + data = length 637, hash F0CCCAE + sample 44: + time = 873500 + flags = 1 + data = length 634, hash D0F5AEEB + sample 45: + time = 893500 + flags = 1 + data = length 620, hash 680A7962 + sample 46: + time = 913500 + flags = 1 + data = length 701, hash 18050131 + sample 47: + time = 933500 + flags = 1 + data = length 700, hash B948FC3B + sample 48: + time = 953500 + flags = 1 + data = length 732, hash 5F8C11AD + sample 49: + time = 973500 + flags = 1 + data = length 632, hash 66D306D1 + sample 50: + time = 993500 + flags = 1 + data = length 666, hash C7130EE0 + sample 51: + time = 1013500 + flags = 1 + data = length 706, hash A3199353 + sample 52: + time = 1033500 + flags = 1 + data = length 717, hash E98C450C + sample 53: + time = 1053500 + flags = 1 + data = length 699, hash E55D6BF1 + sample 54: + time = 1073500 + flags = 1 + data = length 654, hash 6788EEF5 + sample 55: + time = 1093500 + flags = 1 + data = length 668, hash 104C1F00 + sample 56: + time = 1113500 + flags = 1 + data = length 632, hash 57EEBA22 + sample 57: + time = 1133500 + flags = 1 + data = length 633, hash 8ECAA570 + sample 58: + time = 1153500 + flags = 1 + data = length 631, hash 2583F94B + sample 59: + time = 1173500 + flags = 1 + data = length 657, hash 47C82A61 + sample 60: + time = 1193500 + flags = 1 + data = length 734, hash CB4105D8 + sample 61: + time = 1213500 + flags = 1 + data = length 626, hash FFFFF6A0 + sample 62: + time = 1233500 + flags = 1 + data = length 647, hash 12B4EE0 + sample 63: + time = 1253500 + flags = 1 + data = length 625, hash 33781766 + sample 64: + time = 1273500 + flags = 1 + data = length 642, hash 4EFBA7BD + sample 65: + time = 1293500 + flags = 1 + data = length 802, hash 1E1ECCCC + sample 66: + time = 1313500 + flags = 1 + data = length 831, hash 3B6EB119 + sample 67: + time = 1333500 + flags = 1 + data = length 803, hash 1A6D1B26 + sample 68: + time = 1353500 + flags = 1 + data = length 724, hash 373CBA2 + sample 69: + time = 1373500 + flags = 1 + data = length 697, hash B88E6225 + sample 70: + time = 1393500 + flags = 1 + data = length 684, hash 582095ED + sample 71: + time = 1413500 + flags = 1 + data = length 642, hash E3396634 + sample 72: + time = 1433500 + flags = 1 + data = length 660, hash 6B7A6B93 + sample 73: + time = 1453500 + flags = 1 + data = length 658, hash BD7B7172 + sample 74: + time = 1473500 + flags = 1 + data = length 682, hash 1B77F311 + sample 75: + time = 1493500 + flags = 1 + data = length 635, hash 4993E2E8 + sample 76: + time = 1513500 + flags = 1 + data = length 670, hash 1F1F426A + sample 77: + time = 1533500 + flags = 1 + data = length 633, hash 1A6AE4 + sample 78: + time = 1553500 + flags = 1 + data = length 653, hash 55540278 + sample 79: + time = 1573500 + flags = 1 + data = length 618, hash A50A27BC + sample 80: + time = 1593500 + flags = 1 + data = length 657, hash C61CC3D5 + sample 81: + time = 1613500 + flags = 1 + data = length 608, hash 2691ABBB + sample 82: + time = 1633500 + flags = 1 + data = length 673, hash 6CFC36BC + sample 83: + time = 1653500 + flags = 1 + data = length 646, hash C12DD9A3 + sample 84: + time = 1673500 + flags = 1 + data = length 665, hash FF2B77B0 + sample 85: + time = 1693500 + flags = 1 + data = length 644, hash 2BBF36A5 + sample 86: + time = 1713500 + flags = 1 + data = length 750, hash 4510EA7E + sample 87: + time = 1733500 + flags = 1 + data = length 763, hash 22955796 + sample 88: + time = 1753500 + flags = 1 + data = length 638, hash 8AC8492E + sample 89: + time = 1773500 + flags = 1 + data = length 659, hash 6CCFCDC4 + sample 90: + time = 1793500 + flags = 1 + data = length 653, hash ED5CB356 + sample 91: + time = 1813500 + flags = 1 + data = length 625, hash A49FB079 + sample 92: + time = 1833500 + flags = 1 + data = length 681, hash 29FCE7E6 + sample 93: + time = 1853500 + flags = 1 + data = length 719, hash B6E2BE04 + sample 94: + time = 1873500 + flags = 1 + data = length 659, hash 455A8590 + sample 95: + time = 1893500 + flags = 1 + data = length 711, hash D970780F + sample 96: + time = 1913500 + flags = 1 + data = length 649, hash 7B05E203 + sample 97: + time = 1933500 + flags = 1 + data = length 621, hash A06A54BD + sample 98: + time = 1953500 + flags = 1 + data = length 740, hash F5B727FA + sample 99: + time = 1973500 + flags = 1 + data = length 829, hash 24C8AD34 + sample 100: + time = 1993500 + flags = 1 + data = length 646, hash E8292CD3 + sample 101: + time = 2013500 + flags = 1 + data = length 628, hash D2CF6E04 + sample 102: + time = 2033500 + flags = 1 + data = length 621, hash B1DEB04E + sample 103: + time = 2053500 + flags = 1 + data = length 783, hash 35A1171 + sample 104: + time = 2073500 + flags = 1 + data = length 661, hash 7BFCA7CB + sample 105: + time = 2093500 + flags = 1 + data = length 616, hash 27080B66 + sample 106: + time = 2113500 + flags = 1 + data = length 700, hash E3450B94 + sample 107: + time = 2133500 + flags = 1 + data = length 631, hash 4223054B + sample 108: + time = 2153500 + flags = 1 + data = length 650, hash DB573283 + sample 109: + time = 2173500 + flags = 1 + data = length 649, hash 619E7468 + sample 110: + time = 2193500 + flags = 1 + data = length 628, hash 9BD367C7 + sample 111: + time = 2213500 + flags = 1 + data = length 693, hash 85E9290 + sample 112: + time = 2233500 + flags = 1 + data = length 645, hash CAC17B47 + sample 113: + time = 2253500 + flags = 1 + data = length 639, hash 88A52980 + sample 114: + time = 2273500 + flags = 1 + data = length 657, hash DAF95EA7 + sample 115: + time = 2293500 + flags = 1 + data = length 635, hash 2F950D30 + sample 116: + time = 2313500 + flags = 1 + data = length 649, hash 48E900E + sample 117: + time = 2333500 + flags = 1 + data = length 657, hash 4505A6CE + sample 118: + time = 2353500 + flags = 1 + data = length 682, hash DC4C8CD8 + sample 119: + time = 2373500 + flags = 1 + data = length 711, hash 21B58FD3 + sample 120: + time = 2393500 + flags = 1 + data = length 692, hash 4CDFB1C7 + sample 121: + time = 2413500 + flags = 1 + data = length 693, hash B531D8C8 + sample 122: + time = 2433500 + flags = 1 + data = length 650, hash AF15E260 + sample 123: + time = 2453500 + flags = 1 + data = length 643, hash EDF00663 + sample 124: + time = 2473500 + flags = 1 + data = length 623, hash 1B0243A + sample 125: + time = 2493500 + flags = 1 + data = length 639, hash 21B0BA43 + sample 126: + time = 2513500 + flags = 1 + data = length 694, hash 46FA242C + sample 127: + time = 2533500 + flags = 1 + data = length 637, hash 1CEAEA46 + sample 128: + time = 2553500 + flags = 1 + data = length 672, hash A8FE01B1 + sample 129: + time = 2573500 + flags = 1 + data = length 655, hash F4ACFC61 + sample 130: + time = 2593500 + flags = 1 + data = length 674, hash 3B3D15B6 + sample 131: + time = 2613500 + flags = 1 + data = length 695, hash DF376589 + sample 132: + time = 2633500 + flags = 1 + data = length 688, hash A2425D57 + sample 133: + time = 2653500 + flags = 1 + data = length 690, hash BBD3DA9B + sample 134: + time = 2673500 + flags = 1 + data = length 732, hash F7B00D40 + sample 135: + time = 2693500 + flags = 1 + data = length 684, hash 9158FEAB + sample 136: + time = 2713500 + flags = 1 + data = length 674, hash 40C7501A + sample 137: + time = 2733500 + flags = 1 + data = length 680, hash BB132A40 + sample 138: + time = 2753500 + flags = 1 + data = length 648, hash 1114376E + sample 139: + time = 2773500 + flags = 1 + data = length 661, hash DE71BACA + sample 140: + time = 2793500 + flags = 1 + data = length 642, hash A2316E7 + sample 141: + time = 2813500 + flags = 1 + data = length 662, hash C7117D4 + sample 142: + time = 2833500 + flags = 1 + data = length 638, hash 2CF77EF4 + sample 143: + time = 2853500 + flags = 1 + data = length 655, hash E5A75C8B + sample 144: + time = 2873500 + flags = 1 + data = length 635, hash C9E2B5B1 + sample 145: + time = 2893500 + flags = 1 + data = length 655, hash 6F99C63E + sample 146: + time = 2913500 + flags = 1 + data = length 638, hash 64436E98 + sample 147: + time = 2933500 + flags = 1 + data = length 671, hash 5DFDE09A + sample 148: + time = 2953500 + flags = 1 + data = length 661, hash 5203D894 + sample 149: + time = 2973500 + flags = 1 + data = length 664, hash CF30C6D7 + sample 150: + time = 2993500 + flags = 536870913 + data = length 1155, hash F5AFA236 +tracksEnded = true