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
This commit is contained in:
sheenachhabra 2023-10-25 08:59:11 -07:00 committed by Copybara-Service
parent 3204da41fe
commit 1e2815cade
3 changed files with 130 additions and 138 deletions

View File

@ -38,39 +38,39 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
/** A wrapper around an {@link Mp4Extractor} providing methods to extract MP4 metadata. */ /** Provides MP4 metadata like duration, last sync sample timestamp etc. */
/* package */ final class Mp4ExtractorWrapper { /* package */ final class Mp4MetadataInfo {
private final Context context; /**
private final String filePath; * The duration (in microseconds) of the MP4 file or {@link C#TIME_UNSET} if the duration is
private final Mp4Extractor mp4Extractor; * unknown.
private final ExtractorOutputImpl extractorOutput; */
private boolean initialized; public final long durationUs;
/** /**
* Creates an instance. * The presentation timestamp (in microseconds) of the last sync sample or {@link C#TIME_UNSET} if
* * there is no video track.
* @param context A {@link Context}.
* @param filePath The file path of a valid MP4.
*/ */
public Mp4ExtractorWrapper(Context context, String filePath) { public final long lastSyncSampleTimestampUs;
this.context = context;
this.filePath = filePath; private Mp4MetadataInfo(long durationUs, long lastSyncSampleTimestampUs) {
mp4Extractor = new Mp4Extractor(); this.durationUs = durationUs;
extractorOutput = new ExtractorOutputImpl(); this.lastSyncSampleTimestampUs = lastSyncSampleTimestampUs;
} }
/** /**
* Initializes the {@link Mp4ExtractorWrapper}. * Extracts the MP4 metadata synchronously and returns {@link Mp4MetadataInfo}.
* *
* <p>This method must be called only once and it should be called before calling any other * @param context A {@link Context}.
* method. * @param filePath The file path of a valid MP4.
* @throws IOException If an error occurs during metadata extraction.
*/ */
public void init() throws IOException { public static Mp4MetadataInfo create(Context context, String filePath) throws IOException {
checkState(!initialized); Mp4Extractor mp4Extractor = new Mp4Extractor();
ExtractorOutputImpl extractorOutput = new ExtractorOutputImpl();
DefaultDataSource dataSource = DefaultDataSource dataSource =
new DefaultDataSource(context, /* allowCrossProtocolRedirects= */ false); new DefaultDataSource(context, /* allowCrossProtocolRedirects= */ false);
DataSpec dataSpec = new DataSpec.Builder().setUri(filePath).build(); DataSpec dataSpec = new DataSpec.Builder().setUri(filePath).build();
try {
long length = dataSource.open(dataSpec); long length = dataSource.open(dataSpec);
checkState(length != 0); checkState(length != 0);
DefaultExtractorInput extractorInput = DefaultExtractorInput extractorInput =
@ -79,7 +79,6 @@ import org.checkerframework.checker.nullness.qual.Nullable;
mp4Extractor.init(extractorOutput); mp4Extractor.init(extractorOutput);
PositionHolder positionHolder = new PositionHolder(); PositionHolder positionHolder = new PositionHolder();
try {
while (!extractorOutput.seekMapInitialized) { while (!extractorOutput.seekMapInitialized) {
@Extractor.ReadResult int result = mp4Extractor.read(extractorInput, positionHolder); @Extractor.ReadResult int result = mp4Extractor.read(extractorInput, positionHolder);
if (result == Extractor.RESULT_SEEK) { if (result == Extractor.RESULT_SEEK) {
@ -98,27 +97,22 @@ import org.checkerframework.checker.nullness.qual.Nullable;
throw new IllegalStateException("The MP4 file is invalid"); throw new IllegalStateException("The MP4 file is invalid");
} }
} }
initialized = true;
} finally {
DataSourceUtil.closeQuietly(dataSource);
}
}
/**
* 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(); 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); checkState(durationUs != C.TIME_UNSET);
SeekMap.SeekPoints seekPoints = SeekMap.SeekPoints seekPoints =
mp4Extractor.getSeekPoints(durationUs, extractorOutput.videoTrackId); mp4Extractor.getSeekPoints(durationUs, extractorOutput.videoTrackId);
return seekPoints.first.timeUs; lastSyncSampleTimestampUs = seekPoints.first.timeUs;
}
return new Mp4MetadataInfo(durationUs, lastSyncSampleTimestampUs);
} finally {
DataSourceUtil.closeQuietly(dataSource);
mp4Extractor.release();
}
} }
private static final class ExtractorOutputImpl implements ExtractorOutput { private static final class ExtractorOutputImpl implements ExtractorOutput {
@ -127,7 +121,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
private final Map<Integer, TrackOutput> trackTypeToTrackOutput; private final Map<Integer, TrackOutput> trackTypeToTrackOutput;
ExtractorOutputImpl() { public ExtractorOutputImpl() {
videoTrackId = C.INDEX_UNSET; videoTrackId = C.INDEX_UNSET;
trackTypeToTrackOutput = new HashMap<>(); trackTypeToTrackOutput = new HashMap<>();
} }
@ -159,7 +153,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
private final byte[] byteArray; private final byte[] byteArray;
private TrackOutputImpl() { public TrackOutputImpl() {
byteArray = new byte[FIXED_BYTE_ARRAY_SIZE]; byteArray = new byte[FIXED_BYTE_ARRAY_SIZE];
} }

View File

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

View File

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