Parse metadata from srfr atom in SMTA.
PiperOrigin-RevId: 573829216
This commit is contained in:
parent
f53e1bc6f6
commit
fe1144487a
@ -25,6 +25,7 @@ import android.content.Context;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
|
import androidx.media3.common.Metadata;
|
||||||
import androidx.media3.common.MimeTypes;
|
import androidx.media3.common.MimeTypes;
|
||||||
import androidx.media3.common.util.Util;
|
import androidx.media3.common.util.Util;
|
||||||
import androidx.media3.container.MdtaMetadataEntry;
|
import androidx.media3.container.MdtaMetadataEntry;
|
||||||
@ -36,6 +37,7 @@ import androidx.media3.extractor.metadata.mp4.SmtaMetadataEntry;
|
|||||||
import androidx.media3.test.utils.FakeClock;
|
import androidx.media3.test.utils.FakeClock;
|
||||||
import androidx.test.core.app.ApplicationProvider;
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -151,7 +153,7 @@ public class MetadataRetrieverTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void retrieveMetadata_sefSlowMotion_outputsExpectedMetadata() throws Exception {
|
public void retrieveMetadata_sefSlowMotionAvc_outputsExpectedMetadata() throws Exception {
|
||||||
MediaItem mediaItem =
|
MediaItem mediaItem =
|
||||||
MediaItem.fromUri(Uri.parse("asset://android_asset/media/mp4/sample_sef_slow_motion.mp4"));
|
MediaItem.fromUri(Uri.parse("asset://android_asset/media/mp4/sample_sef_slow_motion.mp4"));
|
||||||
MdtaMetadataEntry expectedAndroidVersionMetadata =
|
MdtaMetadataEntry expectedAndroidVersionMetadata =
|
||||||
@ -216,6 +218,60 @@ public class MetadataRetrieverTest {
|
|||||||
assertThat(trackGroups.get(1).getFormat(0).metadata.get(5)).isEqualTo(expectedMp4TimestampData);
|
assertThat(trackGroups.get(1).getFormat(0).metadata.get(5)).isEqualTo(expectedMp4TimestampData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void retrieveMetadata_sefSlowMotionHevc_outputsExpectedMetadata() throws Exception {
|
||||||
|
MediaItem mediaItem =
|
||||||
|
MediaItem.fromUri(
|
||||||
|
Uri.parse("asset://android_asset/media/mp4/sample_sef_slow_motion_hevc.mp4"));
|
||||||
|
MdtaMetadataEntry expectedAndroidVersionMetadata =
|
||||||
|
new MdtaMetadataEntry(
|
||||||
|
/* key= */ "com.android.version",
|
||||||
|
/* value= */ Util.getUtf8Bytes("13"),
|
||||||
|
/* localeIndicator= */ 0,
|
||||||
|
MdtaMetadataEntry.TYPE_INDICATOR_STRING);
|
||||||
|
SmtaMetadataEntry expectedSmtaEntry =
|
||||||
|
new SmtaMetadataEntry(/* captureFrameRate= */ 240, /* svcTemporalLayerCount= */ 4);
|
||||||
|
SlowMotionData expectedSlowMotionData =
|
||||||
|
new SlowMotionData(
|
||||||
|
ImmutableList.of(
|
||||||
|
new SlowMotionData.Segment(
|
||||||
|
/* startTimeMs= */ 2128, /* endTimeMs= */ 9856, /* speedDivisor= */ 8)));
|
||||||
|
MdtaMetadataEntry expectedCaptureFpsMdtaEntry =
|
||||||
|
new MdtaMetadataEntry(
|
||||||
|
KEY_ANDROID_CAPTURE_FPS,
|
||||||
|
/* value= */ new byte[] {67, 112, 0, 0},
|
||||||
|
/* localeIndicator= */ 0,
|
||||||
|
/* typeIndicator= */ 23);
|
||||||
|
|
||||||
|
ListenableFuture<TrackGroupArray> trackGroupsFuture =
|
||||||
|
retrieveMetadata(context, mediaItem, clock);
|
||||||
|
ShadowLooper.idleMainLooper();
|
||||||
|
TrackGroupArray trackGroups = trackGroupsFuture.get(TEST_TIMEOUT_SEC, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
assertThat(trackGroups.length).isEqualTo(2); // Video and audio
|
||||||
|
|
||||||
|
// Video
|
||||||
|
Metadata videoFormatMetadata = trackGroups.get(0).getFormat(0).metadata;
|
||||||
|
List<Metadata.Entry> videoMetadataEntries = new ArrayList<>();
|
||||||
|
for (int i = 0; i < videoFormatMetadata.length(); i++) {
|
||||||
|
videoMetadataEntries.add(videoFormatMetadata.get(i));
|
||||||
|
}
|
||||||
|
assertThat(videoMetadataEntries).contains(expectedAndroidVersionMetadata);
|
||||||
|
assertThat(videoMetadataEntries).contains(expectedSlowMotionData);
|
||||||
|
assertThat(videoMetadataEntries).contains(expectedSmtaEntry);
|
||||||
|
assertThat(videoMetadataEntries).contains(expectedCaptureFpsMdtaEntry);
|
||||||
|
|
||||||
|
// Audio
|
||||||
|
Metadata audioFormatMetadata = trackGroups.get(1).getFormat(0).metadata;
|
||||||
|
List<Metadata.Entry> audioMetadataEntries = new ArrayList<>();
|
||||||
|
for (int i = 0; i < audioFormatMetadata.length(); i++) {
|
||||||
|
audioMetadataEntries.add(audioFormatMetadata.get(i));
|
||||||
|
}
|
||||||
|
assertThat(audioMetadataEntries).contains(expectedAndroidVersionMetadata);
|
||||||
|
assertThat(audioMetadataEntries).contains(expectedSlowMotionData);
|
||||||
|
assertThat(audioMetadataEntries).contains(expectedSmtaEntry);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void retrieveMetadata_invalidMediaItem_throwsError() {
|
public void retrieveMetadata_invalidMediaItem_throwsError() {
|
||||||
MediaItem mediaItem =
|
MediaItem mediaItem =
|
||||||
|
@ -26,7 +26,7 @@ import com.google.common.primitives.Floats;
|
|||||||
/**
|
/**
|
||||||
* Stores metadata from the Samsung smta box.
|
* Stores metadata from the Samsung smta box.
|
||||||
*
|
*
|
||||||
* <p>See [Internal: b/150138465#comment76].
|
* <p>See [Internal: b/150138465#comment76], [Internal: b/301273734#comment17].
|
||||||
*/
|
*/
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
public final class SmtaMetadataEntry implements Metadata.Entry {
|
public final class SmtaMetadataEntry implements Metadata.Entry {
|
||||||
|
@ -359,6 +359,9 @@ import java.util.List;
|
|||||||
@SuppressWarnings("ConstantCaseForConstants")
|
@SuppressWarnings("ConstantCaseForConstants")
|
||||||
public static final int TYPE_saut = 0x73617574;
|
public static final int TYPE_saut = 0x73617574;
|
||||||
|
|
||||||
|
@SuppressWarnings("ConstantCaseForConstants")
|
||||||
|
public static final int TYPE_srfr = 0x73726672;
|
||||||
|
|
||||||
@SuppressWarnings("ConstantCaseForConstants")
|
@SuppressWarnings("ConstantCaseForConstants")
|
||||||
public static final int TYPE_keys = 0x6b657973;
|
public static final int TYPE_keys = 0x6b657973;
|
||||||
|
|
||||||
|
@ -46,7 +46,6 @@ import androidx.media3.extractor.GaplessInfoHolder;
|
|||||||
import androidx.media3.extractor.HevcConfig;
|
import androidx.media3.extractor.HevcConfig;
|
||||||
import androidx.media3.extractor.OpusUtil;
|
import androidx.media3.extractor.OpusUtil;
|
||||||
import androidx.media3.extractor.VorbisUtil;
|
import androidx.media3.extractor.VorbisUtil;
|
||||||
import androidx.media3.extractor.metadata.mp4.SmtaMetadataEntry;
|
|
||||||
import androidx.media3.extractor.mp4.Atom.LeafAtom;
|
import androidx.media3.extractor.mp4.Atom.LeafAtom;
|
||||||
import com.google.common.base.Function;
|
import com.google.common.base.Function;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
@ -177,7 +176,8 @@ import java.util.List;
|
|||||||
} else if (atomType == Atom.TYPE_smta) {
|
} else if (atomType == Atom.TYPE_smta) {
|
||||||
udtaData.setPosition(atomPosition);
|
udtaData.setPosition(atomPosition);
|
||||||
metadata =
|
metadata =
|
||||||
metadata.copyWithAppendedEntriesFrom(parseSmta(udtaData, atomPosition + atomSize));
|
metadata.copyWithAppendedEntriesFrom(
|
||||||
|
SmtaAtomUtil.parseSmta(udtaData, atomPosition + atomSize));
|
||||||
} else if (atomType == Atom.TYPE_xyz) {
|
} else if (atomType == Atom.TYPE_xyz) {
|
||||||
metadata = metadata.copyWithAppendedEntriesFrom(parseXyz(udtaData));
|
metadata = metadata.copyWithAppendedEntriesFrom(parseXyz(udtaData));
|
||||||
}
|
}
|
||||||
@ -825,37 +825,6 @@ import java.util.List;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses metadata from a Samsung smta atom.
|
|
||||||
*
|
|
||||||
* <p>See [Internal: b/150138465#comment76].
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
private static Metadata parseSmta(ParsableByteArray smta, int limit) {
|
|
||||||
smta.skipBytes(Atom.FULL_HEADER_SIZE);
|
|
||||||
while (smta.getPosition() < limit) {
|
|
||||||
int atomPosition = smta.getPosition();
|
|
||||||
int atomSize = smta.readInt();
|
|
||||||
int atomType = smta.readInt();
|
|
||||||
if (atomType == Atom.TYPE_saut) {
|
|
||||||
if (atomSize < 14) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
smta.skipBytes(5); // author (4), reserved = 0 (1).
|
|
||||||
int recordingMode = smta.readUnsignedByte();
|
|
||||||
if (recordingMode != 12 && recordingMode != 13) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
float captureFrameRate = recordingMode == 12 ? 240 : 120;
|
|
||||||
smta.skipBytes(1); // reserved = 1 (1).
|
|
||||||
int svcTemporalLayerCount = smta.readUnsignedByte();
|
|
||||||
return new Metadata(new SmtaMetadataEntry(captureFrameRate, svcTemporalLayerCount));
|
|
||||||
}
|
|
||||||
smta.setPosition(atomPosition + atomSize);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses a tkhd atom (defined in ISO/IEC 14496-12).
|
* Parses a tkhd atom (defined in ISO/IEC 14496-12).
|
||||||
*
|
*
|
||||||
|
@ -0,0 +1,141 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package androidx.media3.extractor.mp4;
|
||||||
|
|
||||||
|
import static java.lang.annotation.ElementType.TYPE_USE;
|
||||||
|
|
||||||
|
import androidx.annotation.IntDef;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.media3.common.C;
|
||||||
|
import androidx.media3.common.Metadata;
|
||||||
|
import androidx.media3.common.util.ParsableByteArray;
|
||||||
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
import androidx.media3.extractor.metadata.mp4.SmtaMetadataEntry;
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility methods for handling SMTA atoms.
|
||||||
|
*
|
||||||
|
* <p>See [Internal: b/150138465#comment76], [Internal: b/301273734#comment17].
|
||||||
|
*/
|
||||||
|
@UnstableApi
|
||||||
|
public final class SmtaAtomUtil {
|
||||||
|
|
||||||
|
@Documented
|
||||||
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
|
@Target(TYPE_USE)
|
||||||
|
@IntDef(
|
||||||
|
open = true,
|
||||||
|
value = {
|
||||||
|
NO_VALUE,
|
||||||
|
CAMCORDER_NORMAL,
|
||||||
|
CAMCORDER_SINGLE_SUPERSLOW_MOTION,
|
||||||
|
CAMCORDER_FRC_SUPERSLOW_MOTION,
|
||||||
|
CAMCORDER_SLOW_MOTION_V2,
|
||||||
|
CAMCORDER_SLOW_MOTION_V2_120,
|
||||||
|
CAMCORDER_SLOW_MOTION_V2_HEVC,
|
||||||
|
CAMCORDER_FRC_SUPERSLOW_MOTION_HEVC,
|
||||||
|
CAMCORDER_QFRC_SUPERSLOW_MOTION,
|
||||||
|
})
|
||||||
|
private @interface RecordingMode {}
|
||||||
|
|
||||||
|
private static final int NO_VALUE = -1;
|
||||||
|
private static final int CAMCORDER_NORMAL = 0;
|
||||||
|
private static final int CAMCORDER_SINGLE_SUPERSLOW_MOTION = 7;
|
||||||
|
private static final int CAMCORDER_FRC_SUPERSLOW_MOTION = 9;
|
||||||
|
private static final int CAMCORDER_SLOW_MOTION_V2 = 12;
|
||||||
|
private static final int CAMCORDER_SLOW_MOTION_V2_120 = 13;
|
||||||
|
private static final int CAMCORDER_SLOW_MOTION_V2_HEVC = 21;
|
||||||
|
private static final int CAMCORDER_FRC_SUPERSLOW_MOTION_HEVC = 22;
|
||||||
|
private static final int CAMCORDER_QFRC_SUPERSLOW_MOTION = 23;
|
||||||
|
|
||||||
|
private SmtaAtomUtil() {}
|
||||||
|
|
||||||
|
/** Parses metadata from a Samsung smta atom. */
|
||||||
|
@Nullable
|
||||||
|
public static Metadata parseSmta(ParsableByteArray smta, int limit) {
|
||||||
|
smta.skipBytes(Atom.FULL_HEADER_SIZE);
|
||||||
|
while (smta.getPosition() < limit) {
|
||||||
|
int atomPosition = smta.getPosition();
|
||||||
|
int atomSize = smta.readInt();
|
||||||
|
int atomType = smta.readInt();
|
||||||
|
if (atomType == Atom.TYPE_saut) {
|
||||||
|
// Size (4), Type (4), Author (4), Recording mode (2), SVC layer count (2).
|
||||||
|
if (atomSize < 16) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
smta.skipBytes(4); // Author (4)
|
||||||
|
|
||||||
|
// Each field is stored as a key (1 byte) value (1 byte) pairs.
|
||||||
|
// The order of the fields is not guaranteed.
|
||||||
|
@RecordingMode int recordingMode = NO_VALUE;
|
||||||
|
int svcTemporalLayerCount = 0;
|
||||||
|
for (int i = 0; i < 2; i++) {
|
||||||
|
int key = smta.readUnsignedByte();
|
||||||
|
int value = smta.readUnsignedByte();
|
||||||
|
if (key == 0x00) { // recordingMode key
|
||||||
|
recordingMode = value;
|
||||||
|
} else if (key == 0x01) { // svcTemporalLayerCount key
|
||||||
|
svcTemporalLayerCount = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int captureFrameRate = getCaptureFrameRate(recordingMode, smta, limit);
|
||||||
|
if (captureFrameRate == C.RATE_UNSET_INT) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Metadata(new SmtaMetadataEntry(captureFrameRate, svcTemporalLayerCount));
|
||||||
|
}
|
||||||
|
smta.setPosition(atomPosition + atomSize);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the capture frame rate for the given recording mode, if supported.
|
||||||
|
*
|
||||||
|
* <p>For {@link #CAMCORDER_SLOW_MOTION_V2_HEVC}, this is done by parsing the Samsung 'srfr' atom.
|
||||||
|
*
|
||||||
|
* @return The capture frame rate value, or {@link C#RATE_UNSET_INT} if unavailable.
|
||||||
|
*/
|
||||||
|
private static int getCaptureFrameRate(
|
||||||
|
@RecordingMode int recordingMode, ParsableByteArray smta, int limit) {
|
||||||
|
// V2 and V2_120 have fixed capture frame rates.
|
||||||
|
if (recordingMode == CAMCORDER_SLOW_MOTION_V2) {
|
||||||
|
return 240;
|
||||||
|
} else if (recordingMode == CAMCORDER_SLOW_MOTION_V2_120) {
|
||||||
|
return 120;
|
||||||
|
} else if (recordingMode != CAMCORDER_SLOW_MOTION_V2_HEVC) {
|
||||||
|
return C.RATE_UNSET_INT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (smta.bytesLeft() < Atom.HEADER_SIZE || smta.getPosition() + Atom.HEADER_SIZE > limit) {
|
||||||
|
return C.RATE_UNSET_INT;
|
||||||
|
}
|
||||||
|
|
||||||
|
int atomSize = smta.readInt();
|
||||||
|
int atomType = smta.readInt();
|
||||||
|
if (atomSize < 12 || atomType != Atom.TYPE_srfr) {
|
||||||
|
return C.RATE_UNSET_INT;
|
||||||
|
}
|
||||||
|
// Capture frame rate is in Q16 format.
|
||||||
|
return smta.readUnsignedFixedPoint1616();
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user