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
This commit is contained in:
Googler 2024-09-15 22:05:46 -07:00 committed by Copybara-Service
parent 6c92402fbb
commit 011659b326
6 changed files with 89 additions and 9 deletions

View File

@ -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<Integer, Integer> getH263ProfileAndLevel(String codec, String[] parts) {
Pair<Integer, Integer> 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<Integer, Integer> getAvcProfileAndLevel(String codec, String[] parts) {
if (parts.length < 2) {

View File

@ -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}.
*

View File

@ -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(

View File

@ -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<Integer, Integer> 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);

View File

@ -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);

View File

@ -1,2 +1,2 @@
s263 (117 bytes):
Data = length 109, hash EABCBA75
Data = length 109, hash 9FFB4BBC