From 011659b32677d7df383f5d138ab6a8ce98a7659a Mon Sep 17 00:00:00 2001 From: Googler Date: Sun, 15 Sep 2024 22:05:46 -0700 Subject: [PATCH] Add profile and level for H263 codec. To support for 3gpp h263 codec in Mp4Muxer currently profile and level is hardcoded and provided to h263 box. Parse profile and level from MediaFormat and use those value to write h263 box. PiperOrigin-RevId: 675004590 --- .../common/util/CodecSpecificDataUtil.java | 30 +++++++++++++++++++ .../media3/common/util/MediaFormatUtil.java | 29 +++++++++++++++++- .../util/CodecSpecificDataUtilTest.java | 9 ++++++ .../java/androidx/media3/muxer/Boxes.java | 21 +++++++++---- .../java/androidx/media3/muxer/BoxesTest.java | 7 ++++- .../video_sample_entry_box_h263.dump | 2 +- 6 files changed, 89 insertions(+), 9 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/util/CodecSpecificDataUtil.java b/libraries/common/src/main/java/androidx/media3/common/util/CodecSpecificDataUtil.java index 58d8573f3a..44c42ff85e 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/CodecSpecificDataUtil.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/CodecSpecificDataUtil.java @@ -48,6 +48,8 @@ public final class CodecSpecificDataUtil { private static final int RECTANGULAR = 0x00; // Codecs to constant mappings. + // H263 + private static final String CODEC_ID_H263 = "s263"; // AVC. private static final String CODEC_ID_AVC1 = "avc1"; private static final String CODEC_ID_AVC2 = "avc2"; @@ -259,6 +261,11 @@ public final class CodecSpecificDataUtil { return builder.toString(); } + /** Builds an RFC 6381 H263 codec string using profile and level. */ + public static String buildH263CodecString(int profile, int level) { + return Util.formatInvariant("s263.%d.%d", profile, level); + } + /** * Returns profile and level (as defined by {@link MediaCodecInfo.CodecProfileLevel}) * corresponding to the codec description string (as defined by RFC 6381) of the given format. @@ -278,6 +285,8 @@ public final class CodecSpecificDataUtil { return getDolbyVisionProfileAndLevel(format.codecs, parts); } switch (parts[0]) { + case CODEC_ID_H263: + return getH263ProfileAndLevel(format.codecs, parts); case CODEC_ID_AVC1: case CODEC_ID_AVC2: return getAvcProfileAndLevel(format.codecs, parts); @@ -463,6 +472,27 @@ public final class CodecSpecificDataUtil { return new Pair<>(profile, level); } + /** Returns H263 profile and level from codec string. */ + private static Pair getH263ProfileAndLevel(String codec, String[] parts) { + Pair defaultProfileAndLevel = + new Pair<>( + MediaCodecInfo.CodecProfileLevel.H263ProfileBaseline, + MediaCodecInfo.CodecProfileLevel.H263Level10); + if (parts.length < 3) { + Log.w(TAG, "Ignoring malformed H263 codec string: " + codec); + return defaultProfileAndLevel; + } + + try { + int profile = Integer.parseInt(parts[1]); + int level = Integer.parseInt(parts[2]); + return new Pair<>(profile, level); + } catch (NumberFormatException e) { + Log.w(TAG, "Ignoring malformed H263 codec string: " + codec); + return defaultProfileAndLevel; + } + } + @Nullable private static Pair getAvcProfileAndLevel(String codec, String[] parts) { if (parts.length < 2) { diff --git a/libraries/common/src/main/java/androidx/media3/common/util/MediaFormatUtil.java b/libraries/common/src/main/java/androidx/media3/common/util/MediaFormatUtil.java index 1824b33f2d..f90f4ccc51 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/MediaFormatUtil.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/MediaFormatUtil.java @@ -28,6 +28,7 @@ import androidx.media3.common.MimeTypes; import com.google.common.collect.ImmutableList; import java.nio.ByteBuffer; import java.util.List; +import java.util.Objects; /** Helper class containing utility methods for managing {@link MediaFormat} instances. */ @UnstableApi @@ -79,7 +80,7 @@ public final class MediaFormatUtil { .setAverageBitrate( getInteger( mediaFormat, MediaFormat.KEY_BIT_RATE, /* defaultValue= */ Format.NO_VALUE)) - .setCodecs(mediaFormat.getString(MediaFormat.KEY_CODECS_STRING)) + .setCodecs(getCodecString(mediaFormat)) .setFrameRate(getFrameRate(mediaFormat, /* defaultValue= */ Format.NO_VALUE)) .setWidth( getInteger(mediaFormat, MediaFormat.KEY_WIDTH, /* defaultValue= */ Format.NO_VALUE)) @@ -332,6 +333,32 @@ public final class MediaFormatUtil { return mediaFormat.containsKey(name) ? mediaFormat.getFloat(name) : defaultValue; } + /** Supports {@link MediaFormat#getString(String, String)} for {@code API < 29}. */ + @Nullable + public static String getString( + MediaFormat mediaFormat, String name, @Nullable String defaultValue) { + return mediaFormat.containsKey(name) ? mediaFormat.getString(name) : defaultValue; + } + + /** + * Returns a {@code Codecs string} of {@link MediaFormat}. In case of an H263 codec string, builds + * and returns an RFC 6381 H263 codec string using profile and level. + */ + @Nullable + @SuppressLint("InlinedApi") // Inlined MediaFormat keys. + private static String getCodecString(MediaFormat mediaFormat) { + // Add H263 profile and level to codec string as per RFC 6381. + if (Objects.equals(mediaFormat.getString(MediaFormat.KEY_MIME), MimeTypes.VIDEO_H263) + && mediaFormat.containsKey(MediaFormat.KEY_PROFILE) + && mediaFormat.containsKey(MediaFormat.KEY_LEVEL)) { + return CodecSpecificDataUtil.buildH263CodecString( + mediaFormat.getInteger(MediaFormat.KEY_PROFILE), + mediaFormat.getInteger(MediaFormat.KEY_LEVEL)); + } else { + return getString(mediaFormat, MediaFormat.KEY_CODECS_STRING, /* defaultValue= */ null); + } + } + /** * Returns the frame rate from a {@link MediaFormat}. * diff --git a/libraries/common/src/test/java/androidx/media3/common/util/CodecSpecificDataUtilTest.java b/libraries/common/src/test/java/androidx/media3/common/util/CodecSpecificDataUtilTest.java index 74c98fd882..4708f05d21 100644 --- a/libraries/common/src/test/java/androidx/media3/common/util/CodecSpecificDataUtilTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/util/CodecSpecificDataUtilTest.java @@ -53,6 +53,15 @@ public class CodecSpecificDataUtilTest { assertThat(sampleRateAndChannelCount.second).isEqualTo(2); } + @Test + public void getCodecProfileAndLevel_handlesH263CodecString() { + assertCodecProfileAndLevelForCodecsString( + MimeTypes.VIDEO_H263, + "s263.1.1", + MediaCodecInfo.CodecProfileLevel.H263ProfileBaseline, + MediaCodecInfo.CodecProfileLevel.H263Level10); + } + @Test public void getCodecProfileAndLevel_handlesVp9Profile1CodecString() { assertCodecProfileAndLevelForCodecsString( 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 0480bca848..55cb2c2b12 100644 --- a/libraries/muxer/src/main/java/androidx/media3/muxer/Boxes.java +++ b/libraries/muxer/src/main/java/androidx/media3/muxer/Boxes.java @@ -26,11 +26,14 @@ import static java.nio.charset.StandardCharsets.UTF_8; import android.media.MediaCodec; import android.media.MediaCodec.BufferInfo; +import android.media.MediaCodecInfo; +import android.util.Pair; import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.ColorInfo; import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; +import androidx.media3.common.util.CodecSpecificDataUtil; import androidx.media3.common.util.Util; import androidx.media3.container.MdtaMetadataEntry; import androidx.media3.container.Mp4LocationData; @@ -678,7 +681,7 @@ import org.checkerframework.checker.nullness.qual.PolyNull; case MimeTypes.AUDIO_OPUS: return dOpsBox(format); case MimeTypes.VIDEO_H263: - return d263Box(); + return d263Box(format); case MimeTypes.VIDEO_H264: return avcCBox(format); case MimeTypes.VIDEO_H265: @@ -1258,13 +1261,19 @@ import org.checkerframework.checker.nullness.qual.PolyNull; } /** Returns the d263Box box as per 3GPP ETSI TS 126 244: 6.8. */ - private static ByteBuffer d263Box() { + private static ByteBuffer d263Box(Format format) { ByteBuffer d263Box = ByteBuffer.allocate(7); d263Box.put(" ".getBytes(UTF_8)); // 4 spaces (vendor) - d263Box.put((byte) 0x0); // decoder version - // TODO: b/352000778 - Get profile and level from format. - d263Box.put((byte) 0x10); // level - d263Box.put((byte) 0x0); // profile + d263Box.put((byte) 0x00); // decoder version + Pair profileAndLevel = CodecSpecificDataUtil.getCodecProfileAndLevel(format); + if (profileAndLevel == null) { + profileAndLevel = + new Pair<>( + MediaCodecInfo.CodecProfileLevel.H263ProfileBaseline, + MediaCodecInfo.CodecProfileLevel.H263Level10); + } + d263Box.put(profileAndLevel.second.byteValue()); // level + d263Box.put(profileAndLevel.first.byteValue()); // profile d263Box.flip(); return BoxUtils.wrapIntoBox("d263", d263Box); 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 e98f16b6cb..f137ce66fd 100644 --- a/libraries/muxer/src/test/java/androidx/media3/muxer/BoxesTest.java +++ b/libraries/muxer/src/test/java/androidx/media3/muxer/BoxesTest.java @@ -368,7 +368,12 @@ public class BoxesTest { @Test public void createVideoSampleEntryBox_forH263_matchesExpected() throws Exception { - Format format = FAKE_VIDEO_FORMAT.buildUpon().setSampleMimeType(MimeTypes.VIDEO_H263).build(); + Format format = + FAKE_VIDEO_FORMAT + .buildUpon() + .setSampleMimeType(MimeTypes.VIDEO_H263) + .setCodecs("s263.1.10") + .build(); ByteBuffer videoSampleEntryBox = Boxes.videoSampleEntry(format); diff --git a/libraries/test_data/src/test/assets/muxerdumps/video_sample_entry_box_h263.dump b/libraries/test_data/src/test/assets/muxerdumps/video_sample_entry_box_h263.dump index 2af4e7ea7e..8718495755 100644 --- a/libraries/test_data/src/test/assets/muxerdumps/video_sample_entry_box_h263.dump +++ b/libraries/test_data/src/test/assets/muxerdumps/video_sample_entry_box_h263.dump @@ -1,2 +1,2 @@ s263 (117 bytes): - Data = length 109, hash EABCBA75 + Data = length 109, hash 9FFB4BBC