From 1e2815cadede99d73a08fa3a6029a86146f08603 Mon Sep 17 00:00:00 2001 From: sheenachhabra Date: Wed, 25 Oct 2023 08:59:11 -0700 Subject: [PATCH] Simplify Mp4ExtractorWrapper implementation Changes includes: 1. Create static factory method and hide constructor. 2. Move all the fetching logic to init() method. PiperOrigin-RevId: 576544902 --- ...actorWrapper.java => Mp4MetadataInfo.java} | 94 +++++++++---------- .../transformer/Mp4ExtractorWrapperTest.java | 88 ----------------- .../transformer/Mp4MetadataInfoTest.java | 86 +++++++++++++++++ 3 files changed, 130 insertions(+), 138 deletions(-) rename libraries/transformer/src/main/java/androidx/media3/transformer/{Mp4ExtractorWrapper.java => Mp4MetadataInfo.java} (71%) delete mode 100644 libraries/transformer/src/test/java/androidx/media3/transformer/Mp4ExtractorWrapperTest.java create mode 100644 libraries/transformer/src/test/java/androidx/media3/transformer/Mp4MetadataInfoTest.java 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. + } +}