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:
parent
3204da41fe
commit
1e2815cade
@ -38,48 +38,47 @@ 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();
|
||||||
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 {
|
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) {
|
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,36 +97,31 @@ 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;
|
|
||||||
|
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 {
|
} finally {
|
||||||
DataSourceUtil.closeQuietly(dataSource);
|
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 {
|
private static final class ExtractorOutputImpl implements ExtractorOutput {
|
||||||
public int videoTrackId;
|
public int videoTrackId;
|
||||||
public boolean seekMapInitialized;
|
public boolean seekMapInitialized;
|
||||||
|
|
||||||
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];
|
||||||
}
|
}
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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.
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user