diff --git a/library/common/src/main/java/com/google/android/exoplayer2/metadata/mp4/MdtaMetadataEntry.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/mp4/MdtaMetadataEntry.java
index 0f41c46c74..5b2db4945c 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/metadata/mp4/MdtaMetadataEntry.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/metadata/mp4/MdtaMetadataEntry.java
@@ -30,14 +30,6 @@ public final class MdtaMetadataEntry implements Metadata.Entry {
/** Key for the capture frame rate (in frames per second). */
public static final String KEY_ANDROID_CAPTURE_FPS = "com.android.capture.fps";
- /** Key for the temporal SVC layer count. */
- public static final String KEY_ANDROID_TEMPORAL_LAYER_COUNT =
- "com.android.video.temporal_layers_count";
-
- /** Type indicator for a 32-bit floating point value. */
- public static final int TYPE_INDICATOR_FLOAT = 23;
- /** Type indicator for a 32-bit integer. */
- public static final int TYPE_INDICATOR_INT = 67;
/** The metadata key name. */
public final String key;
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/metadata/mp4/SmtaMetadataEntry.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/mp4/SmtaMetadataEntry.java
new file mode 100644
index 0000000000..6654a9dbb6
--- /dev/null
+++ b/library/common/src/main/java/com/google/android/exoplayer2/metadata/mp4/SmtaMetadataEntry.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2020 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 com.google.android.exoplayer2.metadata.mp4;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import androidx.annotation.Nullable;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.common.primitives.Floats;
+
+/**
+ * Stores metadata from the Samsung smta box.
+ *
+ *
See [Internal: b/150138465#comment76].
+ */
+public final class SmtaMetadataEntry implements Metadata.Entry {
+
+ /**
+ * The capture frame rate, in fps, or {@link C#RATE_UNSET} if it is unknown.
+ *
+ *
If known, the capture frame rate should always be an integer value.
+ */
+ public final float captureFrameRate;
+ /** The number of layers in the SVC extended frames. */
+ public final int svcTemporalLayerCount;
+
+ /** Creates an instance. */
+ public SmtaMetadataEntry(float captureFrameRate, int svcTemporalLayerCount) {
+ this.captureFrameRate = captureFrameRate;
+ this.svcTemporalLayerCount = svcTemporalLayerCount;
+ }
+
+ private SmtaMetadataEntry(Parcel in) {
+ captureFrameRate = in.readFloat();
+ svcTemporalLayerCount = in.readInt();
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ SmtaMetadataEntry other = (SmtaMetadataEntry) obj;
+ return captureFrameRate == other.captureFrameRate
+ && svcTemporalLayerCount == other.svcTemporalLayerCount;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + Floats.hashCode(captureFrameRate);
+ result = 31 * result + svcTemporalLayerCount;
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "smta: captureFrameRate="
+ + captureFrameRate
+ + ", svcTemporalLayerCount="
+ + svcTemporalLayerCount;
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeFloat(captureFrameRate);
+ dest.writeInt(svcTemporalLayerCount);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Parcelable.Creator CREATOR =
+ new Parcelable.Creator() {
+
+ @Override
+ public SmtaMetadataEntry createFromParcel(Parcel in) {
+ return new SmtaMetadataEntry(in);
+ }
+
+ @Override
+ public SmtaMetadataEntry[] newArray(int size) {
+ return new SmtaMetadataEntry[size];
+ }
+ };
+}
diff --git a/library/common/src/test/java/com/google/android/exoplayer2/metadata/mp4/SmtaMetadataEntryTest.java b/library/common/src/test/java/com/google/android/exoplayer2/metadata/mp4/SmtaMetadataEntryTest.java
new file mode 100644
index 0000000000..7cc48a8021
--- /dev/null
+++ b/library/common/src/test/java/com/google/android/exoplayer2/metadata/mp4/SmtaMetadataEntryTest.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2020 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 com.google.android.exoplayer2.metadata.mp4;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Parcel;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Test for {@link SmtaMetadataEntry}. */
+@RunWith(AndroidJUnit4.class)
+public class SmtaMetadataEntryTest {
+
+ @Test
+ public void parcelable() {
+ SmtaMetadataEntry smtaMetadataEntryToParcel =
+ new SmtaMetadataEntry(/* captureFrameRate= */ 120, /* svcTemporalLayerCount= */ 4);
+
+ Parcel parcel = Parcel.obtain();
+ smtaMetadataEntryToParcel.writeToParcel(parcel, /* flags= */ 0);
+ parcel.setDataPosition(0);
+
+ SmtaMetadataEntry smtaMetadataEntryFromParcel =
+ SmtaMetadataEntry.CREATOR.createFromParcel(parcel);
+ assertThat(smtaMetadataEntryFromParcel).isEqualTo(smtaMetadataEntryToParcel);
+
+ parcel.recycle();
+ }
+}
diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java
index 1a19358b57..95cd1e2c17 100644
--- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java
+++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java
@@ -334,6 +334,12 @@ import java.util.List;
@SuppressWarnings("ConstantCaseForConstants")
public static final int TYPE_meta = 0x6d657461;
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_smta = 0x736d7461;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ public static final int TYPE_saut = 0x73617574;
+
@SuppressWarnings("ConstantCaseForConstants")
public static final int TYPE_keys = 0x6b657973;
diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java
index 551ebc3ea3..2571df954d 100644
--- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java
+++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java
@@ -31,6 +31,7 @@ import com.google.android.exoplayer2.audio.OpusUtil;
import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.extractor.GaplessInfoHolder;
import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.metadata.mp4.SmtaMetadataEntry;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.CodecSpecificDataUtil;
import com.google.android.exoplayer2.util.Log;
@@ -145,28 +146,30 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
*
* @param udtaAtom The udta (user data) atom to decode.
* @param isQuickTime True for QuickTime media. False otherwise.
- * @return Parsed metadata, or null.
+ * @return A {@link Pair} containing the metadata from the meta child atom as first value (if
+ * any), and the metadata from the smta child atom as second value (if any).
*/
- @Nullable
- public static Metadata parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime) {
- if (isQuickTime) {
- // Meta boxes are regular boxes rather than full boxes in QuickTime. For now, don't try and
- // decode one.
- return null;
- }
+ public static Pair<@NullableType Metadata, @NullableType Metadata> parseUdta(
+ Atom.LeafAtom udtaAtom, boolean isQuickTime) {
ParsableByteArray udtaData = udtaAtom.data;
udtaData.setPosition(Atom.HEADER_SIZE);
+ @Nullable Metadata metaMetadata = null;
+ @Nullable Metadata smtaMetadata = null;
while (udtaData.bytesLeft() >= Atom.HEADER_SIZE) {
int atomPosition = udtaData.getPosition();
int atomSize = udtaData.readInt();
int atomType = udtaData.readInt();
- if (atomType == Atom.TYPE_meta) {
+ // Meta boxes are regular boxes rather than full boxes in QuickTime. Ignore them for now.
+ if (atomType == Atom.TYPE_meta && !isQuickTime) {
udtaData.setPosition(atomPosition);
- return parseUdtaMeta(udtaData, atomPosition + atomSize);
+ metaMetadata = parseUdtaMeta(udtaData, atomPosition + atomSize);
+ } else if (atomType == Atom.TYPE_smta) {
+ udtaData.setPosition(atomPosition);
+ smtaMetadata = parseSmta(udtaData, atomPosition + atomSize);
}
udtaData.setPosition(atomPosition + atomSize);
}
- return null;
+ return Pair.create(metaMetadata, smtaMetadata);
}
/**
@@ -701,6 +704,38 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
return entries.isEmpty() ? null : new Metadata(entries);
}
+ /**
+ * Parses metadata from a Samsung smta atom.
+ *
+ * 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) {
+ smta.skipBytes(5); // author (4), reserved = 0 (1).
+ int recordingMode = smta.readUnsignedByte();
+ float captureFrameRate;
+ if (recordingMode == 12) {
+ captureFrameRate = 240;
+ } else if (recordingMode == 13) {
+ captureFrameRate = 120;
+ } else {
+ captureFrameRate = C.RATE_UNSET;
+ }
+ 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 mvhd atom (defined in ISO/IEC 14496-12), returning the timescale for the movie.
*
diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java
index 416a63348c..4b00aa6452 100644
--- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java
+++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java
@@ -31,8 +31,6 @@ import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
import com.google.android.exoplayer2.metadata.mp4.MdtaMetadataEntry;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.ParsableByteArray;
-import java.util.ArrayList;
-import java.util.List;
/** Utilities for handling metadata in MP4. */
/* package */ final class MetadataUtil {
@@ -290,32 +288,34 @@ import java.util.List;
/** Updates a {@link Format.Builder} to include metadata from the provided sources. */
public static void setFormatMetadata(
int trackType,
- @Nullable Metadata udtaMetadata,
+ @Nullable Metadata udtaMetaMetadata,
@Nullable Metadata mdtaMetadata,
+ @Nullable Metadata smtaMetadata,
Format.Builder formatBuilder,
Metadata.Entry... additionalEntries) {
Metadata formatMetadata = new Metadata();
if (trackType == C.TRACK_TYPE_AUDIO) {
- // We assume all udta metadata is associated with the audio track.
- if (udtaMetadata != null) {
- formatMetadata = udtaMetadata;
+ // We assume all meta metadata in the udta box is associated with the audio track.
+ if (udtaMetaMetadata != null) {
+ formatMetadata = udtaMetaMetadata;
}
- } else if (trackType == C.TRACK_TYPE_VIDEO && mdtaMetadata != null) {
+ } else if (trackType == C.TRACK_TYPE_VIDEO) {
// Populate only metadata keys that are known to be specific to video.
- List mdtaMetadataEntries = new ArrayList<>();
- for (int i = 0; i < mdtaMetadata.length(); i++) {
- Metadata.Entry entry = mdtaMetadata.get(i);
- if (entry instanceof MdtaMetadataEntry) {
- MdtaMetadataEntry mdtaMetadataEntry = (MdtaMetadataEntry) entry;
- if (MdtaMetadataEntry.KEY_ANDROID_CAPTURE_FPS.equals(mdtaMetadataEntry.key)
- || MdtaMetadataEntry.KEY_ANDROID_TEMPORAL_LAYER_COUNT.equals(mdtaMetadataEntry.key)) {
- mdtaMetadataEntries.add(mdtaMetadataEntry);
+ if (mdtaMetadata != null) {
+ for (int i = 0; i < mdtaMetadata.length(); i++) {
+ Metadata.Entry entry = mdtaMetadata.get(i);
+ if (entry instanceof MdtaMetadataEntry) {
+ MdtaMetadataEntry mdtaMetadataEntry = (MdtaMetadataEntry) entry;
+ if (MdtaMetadataEntry.KEY_ANDROID_CAPTURE_FPS.equals(mdtaMetadataEntry.key)) {
+ formatMetadata = new Metadata(mdtaMetadataEntry);
+ break;
+ }
}
}
}
- if (!mdtaMetadataEntries.isEmpty()) {
- formatMetadata = new Metadata(mdtaMetadataEntries);
+ if (smtaMetadata != null) {
+ formatMetadata = formatMetadata.copyWithAppendedEntriesFrom(smtaMetadata);
}
}
diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java
index ed6c948c96..506ceacaa5 100644
--- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java
+++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java
@@ -23,6 +23,7 @@ import static com.google.android.exoplayer2.util.Util.castNonNull;
import static java.lang.Math.max;
import static java.lang.Math.min;
+import android.util.Pair;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
@@ -53,6 +54,7 @@ import java.lang.annotation.RetentionPolicy;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@@ -461,14 +463,18 @@ public final class Mp4Extractor implements Extractor, SeekMap {
List tracks = new ArrayList<>();
// Process metadata.
- @Nullable Metadata udtaMetadata = null;
+ @Nullable Metadata udtaMetaMetadata = null;
+ @Nullable Metadata smtaMetadata = null;
boolean isQuickTime = fileType == FILE_TYPE_QUICKTIME;
GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder();
@Nullable Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta);
if (udta != null) {
- udtaMetadata = AtomParsers.parseUdta(udta, isQuickTime);
- if (udtaMetadata != null) {
- gaplessInfoHolder.setFromMetadata(udtaMetadata);
+ Pair<@NullableType Metadata, @NullableType Metadata> udtaMetadata =
+ AtomParsers.parseUdta(udta, isQuickTime);
+ udtaMetaMetadata = udtaMetadata.first;
+ smtaMetadata = udtaMetadata.second;
+ if (udtaMetaMetadata != null) {
+ gaplessInfoHolder.setFromMetadata(udtaMetaMetadata);
}
}
@Nullable Metadata mdtaMetadata = null;
@@ -517,8 +523,9 @@ public final class Mp4Extractor implements Extractor, SeekMap {
MetadataUtil.setFormatGaplessInfo(track.type, gaplessInfoHolder, formatBuilder);
MetadataUtil.setFormatMetadata(
track.type,
- udtaMetadata,
+ udtaMetaMetadata,
mdtaMetadata,
+ smtaMetadata,
formatBuilder,
/* additionalEntries...= */ slowMotionMetadataEntries.toArray(new Metadata.Entry[0]));
mp4Track.trackOutput.format(formatBuilder.build());
diff --git a/testdata/src/test/assets/media/mp4/sample_sef_slow_motion.mp4 b/testdata/src/test/assets/media/mp4/sample_sef_slow_motion.mp4
index 8b436e0c94..1440b883c2 100644
Binary files a/testdata/src/test/assets/media/mp4/sample_sef_slow_motion.mp4 and b/testdata/src/test/assets/media/mp4/sample_sef_slow_motion.mp4 differ
diff --git a/testdata/src/test/assets/media/mp4/sample_sef_super_slow_motion.mp4 b/testdata/src/test/assets/media/mp4/sample_sef_super_slow_motion.mp4
index ab3b2da134..3999e7129f 100644
Binary files a/testdata/src/test/assets/media/mp4/sample_sef_super_slow_motion.mp4 and b/testdata/src/test/assets/media/mp4/sample_sef_super_slow_motion.mp4 differ