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