diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/Mp4ExtractorWrapper.java b/libraries/transformer/src/main/java/androidx/media3/transformer/Mp4MetadataInfo.java
similarity index 71%
rename from libraries/transformer/src/main/java/androidx/media3/transformer/Mp4ExtractorWrapper.java
rename to libraries/transformer/src/main/java/androidx/media3/transformer/Mp4MetadataInfo.java
index 8908f9a624..d1bc8b02c0 100644
--- a/libraries/transformer/src/main/java/androidx/media3/transformer/Mp4ExtractorWrapper.java
+++ b/libraries/transformer/src/main/java/androidx/media3/transformer/Mp4MetadataInfo.java
@@ -38,48 +38,47 @@ import java.util.HashMap;
import java.util.Map;
import org.checkerframework.checker.nullness.qual.Nullable;
-/** A wrapper around an {@link Mp4Extractor} providing methods to extract MP4 metadata. */
-/* package */ final class Mp4ExtractorWrapper {
- private final Context context;
- private final String filePath;
- private final Mp4Extractor mp4Extractor;
- private final ExtractorOutputImpl extractorOutput;
- private boolean initialized;
+/** Provides MP4 metadata like duration, last sync sample timestamp etc. */
+/* package */ final class Mp4MetadataInfo {
+ /**
+ * The duration (in microseconds) of the MP4 file or {@link C#TIME_UNSET} if the duration is
+ * unknown.
+ */
+ public final long durationUs;
/**
- * Creates an instance.
- *
- * @param context A {@link Context}.
- * @param filePath The file path of a valid MP4.
+ * The presentation timestamp (in microseconds) of the last sync sample or {@link C#TIME_UNSET} if
+ * there is no video track.
*/
- public Mp4ExtractorWrapper(Context context, String filePath) {
- this.context = context;
- this.filePath = filePath;
- mp4Extractor = new Mp4Extractor();
- extractorOutput = new ExtractorOutputImpl();
+ public final long lastSyncSampleTimestampUs;
+
+ private Mp4MetadataInfo(long durationUs, long lastSyncSampleTimestampUs) {
+ this.durationUs = durationUs;
+ this.lastSyncSampleTimestampUs = lastSyncSampleTimestampUs;
}
/**
- * Initializes the {@link Mp4ExtractorWrapper}.
+ * Extracts the MP4 metadata synchronously and returns {@link Mp4MetadataInfo}.
*
- *
This method must be called only once and it should be called before calling any other
- * method.
+ * @param context A {@link Context}.
+ * @param filePath The file path of a valid MP4.
+ * @throws IOException If an error occurs during metadata extraction.
*/
- public void init() throws IOException {
- checkState(!initialized);
-
+ public static Mp4MetadataInfo create(Context context, String filePath) throws IOException {
+ Mp4Extractor mp4Extractor = new Mp4Extractor();
+ ExtractorOutputImpl extractorOutput = new ExtractorOutputImpl();
DefaultDataSource dataSource =
new DefaultDataSource(context, /* allowCrossProtocolRedirects= */ false);
DataSpec dataSpec = new DataSpec.Builder().setUri(filePath).build();
- long length = dataSource.open(dataSpec);
- checkState(length != 0);
- DefaultExtractorInput extractorInput =
- new DefaultExtractorInput(dataSource, /* position= */ 0, length);
- checkState(mp4Extractor.sniff(extractorInput), "The MP4 file is invalid");
-
- mp4Extractor.init(extractorOutput);
- PositionHolder positionHolder = new PositionHolder();
try {
+ long length = dataSource.open(dataSpec);
+ checkState(length != 0);
+ DefaultExtractorInput extractorInput =
+ new DefaultExtractorInput(dataSource, /* position= */ 0, length);
+ checkState(mp4Extractor.sniff(extractorInput), "The MP4 file is invalid");
+
+ mp4Extractor.init(extractorOutput);
+ PositionHolder positionHolder = new PositionHolder();
while (!extractorOutput.seekMapInitialized) {
@Extractor.ReadResult int result = mp4Extractor.read(extractorInput, positionHolder);
if (result == Extractor.RESULT_SEEK) {
@@ -98,36 +97,31 @@ import org.checkerframework.checker.nullness.qual.Nullable;
throw new IllegalStateException("The MP4 file is invalid");
}
}
- initialized = true;
+
+ long durationUs = mp4Extractor.getDurationUs();
+ long lastSyncSampleTimestampUs = C.TIME_UNSET;
+
+ // Fetch last sync sample timestamp.
+ if (extractorOutput.videoTrackId != C.INDEX_UNSET) {
+ checkState(durationUs != C.TIME_UNSET);
+ SeekMap.SeekPoints seekPoints =
+ mp4Extractor.getSeekPoints(durationUs, extractorOutput.videoTrackId);
+ lastSyncSampleTimestampUs = seekPoints.first.timeUs;
+ }
+ return new Mp4MetadataInfo(durationUs, lastSyncSampleTimestampUs);
} finally {
DataSourceUtil.closeQuietly(dataSource);
+ mp4Extractor.release();
}
}
- /**
- * Returns the presentation timestamp (in microseconds) of the last sync sample or {@link
- * C#TIME_UNSET} if there is no video track.
- */
- public long getLastSyncSampleTimestampUs() {
- checkState(initialized);
-
- if (extractorOutput.videoTrackId == C.INDEX_UNSET) {
- return C.TIME_UNSET;
- }
- long durationUs = mp4Extractor.getDurationUs();
- checkState(durationUs != C.TIME_UNSET);
- SeekMap.SeekPoints seekPoints =
- mp4Extractor.getSeekPoints(durationUs, extractorOutput.videoTrackId);
- return seekPoints.first.timeUs;
- }
-
private static final class ExtractorOutputImpl implements ExtractorOutput {
public int videoTrackId;
public boolean seekMapInitialized;
private final Map trackTypeToTrackOutput;
- ExtractorOutputImpl() {
+ public ExtractorOutputImpl() {
videoTrackId = C.INDEX_UNSET;
trackTypeToTrackOutput = new HashMap<>();
}
@@ -159,7 +153,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
private final byte[] byteArray;
- private TrackOutputImpl() {
+ public TrackOutputImpl() {
byteArray = new byte[FIXED_BYTE_ARRAY_SIZE];
}
diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/Mp4ExtractorWrapperTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/Mp4ExtractorWrapperTest.java
deleted file mode 100644
index 41339e44e2..0000000000
--- a/libraries/transformer/src/test/java/androidx/media3/transformer/Mp4ExtractorWrapperTest.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * 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
- *
- * https://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.transformer;
-
-import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.assertThrows;
-
-import android.content.Context;
-import androidx.media3.common.C;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import java.io.IOException;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import org.junit.runner.RunWith;
-
-/** Unit tests for {@link Mp4ExtractorWrapper}. */
-@RunWith(AndroidJUnit4.class)
-public class Mp4ExtractorWrapperTest {
- @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
-
- private final Context context = ApplicationProvider.getApplicationContext();
-
- @Test
- public void init_withEmptyFile_throws() throws IOException {
- String emptyFilePath = temporaryFolder.newFile("EmptyFile").getPath();
- Mp4ExtractorWrapper mp4ExtractorWrapper = new Mp4ExtractorWrapper(context, emptyFilePath);
-
- assertThrows(IllegalStateException.class, mp4ExtractorWrapper::init);
- }
-
- @Test
- public void init_withNonMp4File_throws() {
- String mp4FilePath = "asset:///media/mkv/sample.mkv";
- Mp4ExtractorWrapper mp4ExtractorWrapper = new Mp4ExtractorWrapper(context, mp4FilePath);
-
- assertThrows(IllegalStateException.class, mp4ExtractorWrapper::init);
- }
-
- @Test
- public void getLastSyncSampleTimestampUs_ofSmallMp4File_outputsFirstTimestamp()
- throws IOException {
- String mp4FilePath = "asset:///media/mp4/sample.mp4";
- Mp4ExtractorWrapper mp4ExtractorWrapper = new Mp4ExtractorWrapper(context, mp4FilePath);
- mp4ExtractorWrapper.init();
-
- long lastSyncSampleTimeStampUs = mp4ExtractorWrapper.getLastSyncSampleTimestampUs();
-
- long expectedTimestamp = 0;
- assertThat(lastSyncSampleTimeStampUs).isEqualTo(expectedTimestamp);
- }
-
- @Test
- public void getLastSyncSampleTimestampUs_ofMp4File_outputMatchesExpected() throws IOException {
- String mp4FilePath = "asset:///media/mp4/hdr10-720p.mp4";
- Mp4ExtractorWrapper mp4ExtractorWrapper = new Mp4ExtractorWrapper(context, mp4FilePath);
- mp4ExtractorWrapper.init();
-
- long lastSyncSampleTimeStampUs = mp4ExtractorWrapper.getLastSyncSampleTimestampUs();
-
- long expectedTimestamp = 4_003_277L;
- assertThat(lastSyncSampleTimeStampUs).isEqualTo(expectedTimestamp);
- }
-
- @Test
- public void getLastSyncSampleTimestampUs_ofAudioOnlyMp4File_returnsUnsetValue()
- throws IOException {
- String mp4FilePath = "asset:///media/mp4/sample_ac3.mp4";
- Mp4ExtractorWrapper mp4ExtractorWrapper = new Mp4ExtractorWrapper(context, mp4FilePath);
- mp4ExtractorWrapper.init();
-
- assertThat(mp4ExtractorWrapper.getLastSyncSampleTimestampUs()).isEqualTo(C.TIME_UNSET);
- }
-}
diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/Mp4MetadataInfoTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/Mp4MetadataInfoTest.java
new file mode 100644
index 0000000000..1ec32a5df4
--- /dev/null
+++ b/libraries/transformer/src/test/java/androidx/media3/transformer/Mp4MetadataInfoTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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
+ *
+ * https://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.transformer;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import android.content.Context;
+import androidx.media3.common.C;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.io.IOException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+
+/** Unit tests for {@link Mp4MetadataInfo}. */
+@RunWith(AndroidJUnit4.class)
+public class Mp4MetadataInfoTest {
+ @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ private final Context context = ApplicationProvider.getApplicationContext();
+
+ @Test
+ public void create_withEmptyFile_throws() throws IOException {
+ String emptyFilePath = temporaryFolder.newFile("EmptyFile").getPath();
+
+ assertThrows(IllegalStateException.class, () -> Mp4MetadataInfo.create(context, emptyFilePath));
+ }
+
+ @Test
+ public void create_withNonMp4File_throws() {
+ String nonMp4FilePath = "asset:///media/mkv/sample.mkv";
+
+ assertThrows(
+ IllegalStateException.class, () -> Mp4MetadataInfo.create(context, nonMp4FilePath));
+ }
+
+ @Test
+ public void lastSyncSampleTimestampUs_ofSmallMp4File_outputsFirstTimestamp() throws IOException {
+ String mp4FilePath = "asset:///media/mp4/sample.mp4";
+ Mp4MetadataInfo mp4MetadataInfo = Mp4MetadataInfo.create(context, mp4FilePath);
+
+ assertThat(mp4MetadataInfo.lastSyncSampleTimestampUs)
+ .isEqualTo(0); // The timestamp of the first sample in sample.mp4.
+ }
+
+ @Test
+ public void lastSyncSampleTimestampUs_ofMp4File_outputMatchesExpected() throws IOException {
+ String mp4FilePath = "asset:///media/mp4/hdr10-720p.mp4";
+ Mp4MetadataInfo mp4MetadataInfo = Mp4MetadataInfo.create(context, mp4FilePath);
+
+ assertThat(mp4MetadataInfo.lastSyncSampleTimestampUs)
+ .isEqualTo(4_003_277L); // The timestamp of the last sync sample in hdr10-720p.mp4.
+ }
+
+ @Test
+ public void lastSyncSampleTimestampUs_ofAudioOnlyMp4File_isUnset() throws IOException {
+ String audioOnlyMp4FilePath = "asset:///media/mp4/sample_ac3.mp4";
+ Mp4MetadataInfo mp4MetadataInfo = Mp4MetadataInfo.create(context, audioOnlyMp4FilePath);
+
+ assertThat(mp4MetadataInfo.lastSyncSampleTimestampUs).isEqualTo(C.TIME_UNSET);
+ }
+
+ @Test
+ public void durationUs_ofMp4File_outputMatchesExpected() throws Exception {
+ String mp4FilePath = "asset:///media/mp4/hdr10-720p.mp4";
+ Mp4MetadataInfo mp4MetadataInfo = Mp4MetadataInfo.create(context, mp4FilePath);
+
+ assertThat(mp4MetadataInfo.durationUs).isEqualTo(4_236_600L); // The duration of hdr10-720p.mp4.
+ }
+}