Add more details about why Extractor.sniff returned false

PiperOrigin-RevId: 609335656
This commit is contained in:
ibaker 2024-02-22 05:17:30 -08:00 committed by Copybara-Service
parent 41886434ad
commit d1ae9ffc52
17 changed files with 496 additions and 90 deletions

View File

@ -28,7 +28,9 @@ import androidx.media3.extractor.ExtractorInput;
import androidx.media3.extractor.ExtractorOutput; import androidx.media3.extractor.ExtractorOutput;
import androidx.media3.extractor.ExtractorsFactory; import androidx.media3.extractor.ExtractorsFactory;
import androidx.media3.extractor.PositionHolder; import androidx.media3.extractor.PositionHolder;
import androidx.media3.extractor.SniffFailure;
import androidx.media3.extractor.mp3.Mp3Extractor; import androidx.media3.extractor.mp3.Mp3Extractor;
import com.google.common.collect.ImmutableList;
import java.io.EOFException; import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
@ -70,6 +72,8 @@ public final class BundledExtractorsAdapter implements ProgressiveMediaExtractor
return; return;
} }
Extractor[] extractors = extractorsFactory.createExtractors(uri, responseHeaders); Extractor[] extractors = extractorsFactory.createExtractors(uri, responseHeaders);
ImmutableList.Builder<SniffFailure> sniffFailures =
ImmutableList.builderWithExpectedSize(extractors.length);
if (extractors.length == 1) { if (extractors.length == 1) {
this.extractor = extractors[0]; this.extractor = extractors[0];
} else { } else {
@ -78,6 +82,9 @@ public final class BundledExtractorsAdapter implements ProgressiveMediaExtractor
if (extractor.sniff(extractorInput)) { if (extractor.sniff(extractorInput)) {
this.extractor = extractor; this.extractor = extractor;
break; break;
} else {
List<SniffFailure> sniffFailureDetails = extractor.getSniffFailureDetails();
sniffFailures.addAll(sniffFailureDetails);
} }
} catch (EOFException e) { } catch (EOFException e) {
// Do nothing. // Do nothing.
@ -91,7 +98,8 @@ public final class BundledExtractorsAdapter implements ProgressiveMediaExtractor
"None of the available extractors (" "None of the available extractors ("
+ Util.getCommaDelimitedSimpleClassNames(extractors) + Util.getCommaDelimitedSimpleClassNames(extractors)
+ ") could read the stream.", + ") could read the stream.",
Assertions.checkNotNull(uri)); Assertions.checkNotNull(uri),
sniffFailures.build());
} }
} }
extractor.init(output); extractor.init(output);

View File

@ -19,6 +19,11 @@ import android.net.Uri;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.ParserException; import androidx.media3.common.ParserException;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.extractor.Extractor;
import androidx.media3.extractor.SniffFailure;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.InlineMe;
import java.util.List;
/** Thrown if the input format was not recognized. */ /** Thrown if the input format was not recognized. */
@UnstableApi @UnstableApi
@ -28,11 +33,36 @@ public class UnrecognizedInputFormatException extends ParserException {
public final Uri uri; public final Uri uri;
/** /**
* Sniff failures from {@link Extractor#getSniffFailureDetails()} from any extractors that were
* checked while trying to recognize the input data.
*
* <p>May be empty if no extractors provided additional sniffing failure details.
*/
public final ImmutableList<SniffFailure> sniffFailures;
/**
* @deprecated Use {@link #UnrecognizedInputFormatException(String, Uri, List)} instead.
*/
@InlineMe(
replacement = "this(message, uri, ImmutableList.of())",
imports = "com.google.common.collect.ImmutableList")
@Deprecated
public UnrecognizedInputFormatException(String message, Uri uri) {
this(message, uri, ImmutableList.of());
}
/**
* Constructs a new instance.
*
* @param message The detail message for the exception. * @param message The detail message for the exception.
* @param uri The {@link Uri} from which the unrecognized data was read. * @param uri The {@link Uri} from which the unrecognized data was read.
* @param sniffFailures Sniff failures from any extractors that were used to sniff the data while
* trying to recognize it.
*/ */
public UnrecognizedInputFormatException(String message, Uri uri) { public UnrecognizedInputFormatException(
String message, Uri uri, List<? extends SniffFailure> sniffFailures) {
super(message, /* cause= */ null, /* contentIsMalformed= */ false, C.DATA_TYPE_MEDIA); super(message, /* cause= */ null, /* contentIsMalformed= */ false, C.DATA_TYPE_MEDIA);
this.uri = uri; this.uri = uri;
this.sniffFailures = ImmutableList.copyOf(sniffFailures);
} }
} }

View File

@ -20,11 +20,13 @@ import static java.lang.annotation.ElementType.TYPE_USE;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import com.google.common.collect.ImmutableList;
import java.io.IOException; import java.io.IOException;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import java.util.List;
import org.checkerframework.dataflow.qual.SideEffectFree; import org.checkerframework.dataflow.qual.SideEffectFree;
/** Extracts media data from a container format. */ /** Extracts media data from a container format. */
@ -74,6 +76,19 @@ public interface Extractor {
*/ */
boolean sniff(ExtractorInput input) throws IOException; boolean sniff(ExtractorInput input) throws IOException;
/**
* Returns additional details about the last call to {@link #sniff}. The returned list may be
* empty if no additional details are available, or the last {@link #sniff} call returned {@code
* true}.
*
* <p>This only contains details that were discovered before {@link #sniff} returned {@code
* false}, it is not an exhaustive list of issues which, if resolved, would cause the file to be
* successfully sniffed.
*/
default List<SniffFailure> getSniffFailureDetails() {
return ImmutableList.of();
}
/** /**
* Initializes the extractor with an {@link ExtractorOutput}. Called at most once. * Initializes the extractor with an {@link ExtractorOutput}. Called at most once.
* *

View File

@ -0,0 +1,22 @@
/*
* Copyright 2024 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.extractor;
import androidx.media3.common.util.UnstableApi;
/** Contains details about why {@link Extractor#sniff(ExtractorInput)} returned {@code false}. */
@UnstableApi
public interface SniffFailure {}

View File

@ -0,0 +1,36 @@
/*
* Copyright 2024 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.extractor.mp4;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.extractor.SniffFailure;
/**
* A {@link SniffFailure} indicating an atom declares a size that is too small for the header fields
* that must present for the given type.
*/
@UnstableApi
public final class AtomSizeTooSmallSniffFailure implements SniffFailure {
public final int atomType;
public final long atomSize;
public final int minimumHeaderSize;
public AtomSizeTooSmallSniffFailure(int atomType, long atomSize, int minimumHeaderSize) {
this.atomType = atomType;
this.atomSize = atomSize;
this.minimumHeaderSize = minimumHeaderSize;
}
}

View File

@ -49,6 +49,7 @@ import androidx.media3.extractor.ExtractorsFactory;
import androidx.media3.extractor.GaplessInfoHolder; import androidx.media3.extractor.GaplessInfoHolder;
import androidx.media3.extractor.PositionHolder; import androidx.media3.extractor.PositionHolder;
import androidx.media3.extractor.SeekMap; import androidx.media3.extractor.SeekMap;
import androidx.media3.extractor.SniffFailure;
import androidx.media3.extractor.TrackOutput; import androidx.media3.extractor.TrackOutput;
import androidx.media3.extractor.metadata.emsg.EventMessage; import androidx.media3.extractor.metadata.emsg.EventMessage;
import androidx.media3.extractor.metadata.emsg.EventMessageEncoder; import androidx.media3.extractor.metadata.emsg.EventMessageEncoder;
@ -187,6 +188,7 @@ public class FragmentedMp4Extractor implements Extractor {
private final ArrayDeque<MetadataSampleInfo> pendingMetadataSampleInfos; private final ArrayDeque<MetadataSampleInfo> pendingMetadataSampleInfos;
@Nullable private final TrackOutput additionalEmsgTrackOutput; @Nullable private final TrackOutput additionalEmsgTrackOutput;
private ImmutableList<SniffFailure> lastSniffFailures;
private int parserState; private int parserState;
private int atomType; private int atomType;
private long atomSize; private long atomSize;
@ -383,6 +385,7 @@ public class FragmentedMp4Extractor implements Extractor {
containerAtoms = new ArrayDeque<>(); containerAtoms = new ArrayDeque<>();
pendingMetadataSampleInfos = new ArrayDeque<>(); pendingMetadataSampleInfos = new ArrayDeque<>();
trackBundles = new SparseArray<>(); trackBundles = new SparseArray<>();
lastSniffFailures = ImmutableList.of();
durationUs = C.TIME_UNSET; durationUs = C.TIME_UNSET;
pendingSeekTimeUs = C.TIME_UNSET; pendingSeekTimeUs = C.TIME_UNSET;
segmentIndexEarliestPresentationTimeUs = C.TIME_UNSET; segmentIndexEarliestPresentationTimeUs = C.TIME_UNSET;
@ -393,7 +396,14 @@ public class FragmentedMp4Extractor implements Extractor {
@Override @Override
public boolean sniff(ExtractorInput input) throws IOException { public boolean sniff(ExtractorInput input) throws IOException {
return Sniffer.sniffFragmented(input); @Nullable SniffFailure sniffFailure = Sniffer.sniffFragmented(input);
lastSniffFailures = sniffFailure != null ? ImmutableList.of(sniffFailure) : ImmutableList.of();
return sniffFailure == null;
}
@Override
public ImmutableList<SniffFailure> getSniffFailureDetails() {
return lastSniffFailures;
} }
@Override @Override

View File

@ -0,0 +1,39 @@
/*
* Copyright 2024 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.extractor.mp4;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.extractor.SniffFailure;
/**
* {@link SniffFailure} indicating the file's fragmented flag is incompatible with this {@link
* androidx.media3.extractor.Extractor}.
*/
@UnstableApi
public final class IncorrectFragmentationSniffFailure implements SniffFailure {
public static final IncorrectFragmentationSniffFailure FILE_FRAGMENTED =
new IncorrectFragmentationSniffFailure(/* fileIsFragmented= */ true);
public static final IncorrectFragmentationSniffFailure FILE_NOT_FRAGMENTED =
new IncorrectFragmentationSniffFailure(/* fileIsFragmented= */ false);
public final boolean fileIsFragmented;
private IncorrectFragmentationSniffFailure(boolean fileIsFragmented) {
this.fileIsFragmented = fileIsFragmented;
}
}

View File

@ -45,6 +45,7 @@ import androidx.media3.extractor.GaplessInfoHolder;
import androidx.media3.extractor.PositionHolder; import androidx.media3.extractor.PositionHolder;
import androidx.media3.extractor.SeekMap; import androidx.media3.extractor.SeekMap;
import androidx.media3.extractor.SeekPoint; import androidx.media3.extractor.SeekPoint;
import androidx.media3.extractor.SniffFailure;
import androidx.media3.extractor.TrackOutput; import androidx.media3.extractor.TrackOutput;
import androidx.media3.extractor.TrueHdSampleRechunker; import androidx.media3.extractor.TrueHdSampleRechunker;
import androidx.media3.extractor.metadata.mp4.MotionPhotoMetadata; import androidx.media3.extractor.metadata.mp4.MotionPhotoMetadata;
@ -52,6 +53,7 @@ import androidx.media3.extractor.metadata.mp4.SlowMotionData;
import androidx.media3.extractor.mp4.Atom.ContainerAtom; import androidx.media3.extractor.mp4.Atom.ContainerAtom;
import androidx.media3.extractor.text.SubtitleParser; import androidx.media3.extractor.text.SubtitleParser;
import androidx.media3.extractor.text.SubtitleTranscodingExtractorOutput; import androidx.media3.extractor.text.SubtitleTranscodingExtractorOutput;
import com.google.common.collect.ImmutableList;
import java.io.IOException; import java.io.IOException;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
@ -182,6 +184,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
private final SefReader sefReader; private final SefReader sefReader;
private final List<Metadata.Entry> slowMotionMetadataEntries; private final List<Metadata.Entry> slowMotionMetadataEntries;
private ImmutableList<SniffFailure> lastSniffFailures;
private @State int parserState; private @State int parserState;
private int atomType; private int atomType;
private long atomSize; private long atomSize;
@ -241,6 +244,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
public Mp4Extractor(SubtitleParser.Factory subtitleParserFactory, @Flags int flags) { public Mp4Extractor(SubtitleParser.Factory subtitleParserFactory, @Flags int flags) {
this.subtitleParserFactory = subtitleParserFactory; this.subtitleParserFactory = subtitleParserFactory;
this.flags = flags; this.flags = flags;
lastSniffFailures = ImmutableList.of();
parserState = parserState =
((flags & FLAG_READ_SEF_DATA) != 0) ? STATE_READING_SEF : STATE_READING_ATOM_HEADER; ((flags & FLAG_READ_SEF_DATA) != 0) ? STATE_READING_SEF : STATE_READING_ATOM_HEADER;
sefReader = new SefReader(); sefReader = new SefReader();
@ -257,8 +261,17 @@ public final class Mp4Extractor implements Extractor, SeekMap {
@Override @Override
public boolean sniff(ExtractorInput input) throws IOException { public boolean sniff(ExtractorInput input) throws IOException {
return Sniffer.sniffUnfragmented( @Nullable
input, /* acceptHeic= */ (flags & FLAG_READ_MOTION_PHOTO_METADATA) != 0); SniffFailure sniffFailure =
Sniffer.sniffUnfragmented(
input, /* acceptHeic= */ (flags & FLAG_READ_MOTION_PHOTO_METADATA) != 0);
lastSniffFailures = sniffFailure != null ? ImmutableList.of(sniffFailure) : ImmutableList.of();
return sniffFailure == null;
}
@Override
public ImmutableList<SniffFailure> getSniffFailureDetails() {
return lastSniffFailures;
} }
@Override @Override

View File

@ -0,0 +1,28 @@
/*
* Copyright 2024 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.extractor.mp4;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.extractor.SniffFailure;
/** {@link SniffFailure} indicating the MP4 file didn't declare any brands. */
@UnstableApi
public final class NoDeclaredBrandSniffFailure implements SniffFailure {
public static final NoDeclaredBrandSniffFailure INSTANCE = new NoDeclaredBrandSniffFailure();
private NoDeclaredBrandSniffFailure() {}
}

View File

@ -15,9 +15,11 @@
*/ */
package androidx.media3.extractor.mp4; package androidx.media3.extractor.mp4;
import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.util.ParsableByteArray; import androidx.media3.common.util.ParsableByteArray;
import androidx.media3.extractor.ExtractorInput; import androidx.media3.extractor.ExtractorInput;
import androidx.media3.extractor.SniffFailure;
import java.io.IOException; import java.io.IOException;
/** /**
@ -69,45 +71,40 @@ import java.io.IOException;
}; };
/** /**
* Returns whether data peeked from the current position in {@code input} is consistent with the * Returns {@code null} if data peeked from the current position in {@code input} is consistent
* input being a fragmented MP4 file. * with the input being a fragmented MP4 file, otherwise returns a {@link SniffFailure} describing
* the first detected inconsistency..
* *
* @param input The extractor input from which to peek data. The peek position will be modified. * @param input The extractor input from which to peek data. The peek position will be modified.
* @return Whether the input appears to be in the fragmented MP4 format. * @return {@code null} if the input appears to be in the fragmented MP4 format, otherwise a
* {@link SniffFailure} describing why the input isn't deemed to be a fragmented MP4.
* @throws IOException If an error occurs reading from the input. * @throws IOException If an error occurs reading from the input.
*/ */
public static boolean sniffFragmented(ExtractorInput input) throws IOException { @Nullable
public static SniffFailure sniffFragmented(ExtractorInput input) throws IOException {
return sniffInternal(input, /* fragmented= */ true, /* acceptHeic= */ false); return sniffInternal(input, /* fragmented= */ true, /* acceptHeic= */ false);
} }
/** /**
* Returns whether data peeked from the current position in {@code input} is consistent with the * Returns {@code null} if data peeked from the current position in {@code input} is consistent
* input being an unfragmented MP4 file. * with the input being an unfragmented MP4 file, otherwise returns a {@link SniffFailure}
* describing the first detected inconsistency.
* *
* @param input The extractor input from which to peek data. The peek position will be modified. * @param input The extractor input from which to peek data. The peek position will be modified.
* @return Whether the input appears to be in the unfragmented MP4 format. * @param acceptHeic Whether {@code null} should be returned for HEIC photos.
* @return {@code null} if the input appears to be in the fragmented MP4 format, otherwise a
* {@link SniffFailure} describing why the input isn't deemed to be a fragmented MP4.
* @throws IOException If an error occurs reading from the input. * @throws IOException If an error occurs reading from the input.
*/ */
public static boolean sniffUnfragmented(ExtractorInput input) throws IOException { @Nullable
return sniffInternal(input, /* fragmented= */ false, /* acceptHeic= */ false); public static SniffFailure sniffUnfragmented(ExtractorInput input, boolean acceptHeic)
}
/**
* Returns whether data peeked from the current position in {@code input} is consistent with the
* input being an unfragmented MP4 file.
*
* @param input The extractor input from which to peek data. The peek position will be modified.
* @param acceptHeic Whether {@code true} should be returned for HEIC photos.
* @return Whether the input appears to be in the unfragmented MP4 format.
* @throws IOException If an error occurs reading from the input.
*/
public static boolean sniffUnfragmented(ExtractorInput input, boolean acceptHeic)
throws IOException { throws IOException {
return sniffInternal(input, /* fragmented= */ false, acceptHeic); return sniffInternal(input, /* fragmented= */ false, acceptHeic);
} }
private static boolean sniffInternal(ExtractorInput input, boolean fragmented, boolean acceptHeic) @Nullable
throws IOException { private static SniffFailure sniffInternal(
ExtractorInput input, boolean fragmented, boolean acceptHeic) throws IOException {
long inputLength = input.getLength(); long inputLength = input.getLength();
int bytesToSearch = int bytesToSearch =
(int) (int)
@ -148,7 +145,7 @@ import java.io.IOException;
if (atomSize < headerSize) { if (atomSize < headerSize) {
// The file is invalid because the atom size is too small for its header. // The file is invalid because the atom size is too small for its header.
return false; return new AtomSizeTooSmallSniffFailure(atomType, atomSize, headerSize);
} }
bytesSearched += headerSize; bytesSearched += headerSize;
@ -186,30 +183,46 @@ import java.io.IOException;
if (atomType == Atom.TYPE_ftyp) { if (atomType == Atom.TYPE_ftyp) {
// Parse the atom and check the file type/brand is compatible with the extractors. // Parse the atom and check the file type/brand is compatible with the extractors.
if (atomDataSize < 8) { if (atomDataSize < 8) {
return false; return new AtomSizeTooSmallSniffFailure(atomType, atomDataSize, 8);
} }
buffer.reset(atomDataSize); buffer.reset(atomDataSize);
input.peekFully(buffer.getData(), 0, atomDataSize); input.peekFully(buffer.getData(), 0, atomDataSize);
int brandsCount = atomDataSize / 4; int majorBrand = buffer.readInt();
for (int i = 0; i < brandsCount; i++) { if (isCompatibleBrand(majorBrand, acceptHeic)) {
if (i == 1) { foundGoodFileType = true;
// This index refers to the minorVersion, not a brand, so skip it. }
buffer.skipBytes(4); // Skip the minorVersion.
} else if (isCompatibleBrand(buffer.readInt(), acceptHeic)) { buffer.skipBytes(4);
foundGoodFileType = true; int compatibleBrandsCount = buffer.bytesLeft() / 4;
break; @Nullable int[] compatibleBrands = null;
if (!foundGoodFileType && compatibleBrandsCount > 0) {
compatibleBrands = new int[compatibleBrandsCount];
for (int i = 0; i < compatibleBrandsCount; i++) {
compatibleBrands[i] = buffer.readInt();
if (isCompatibleBrand(compatibleBrands[i], acceptHeic)) {
foundGoodFileType = true;
break;
}
} }
} }
if (!foundGoodFileType) { if (!foundGoodFileType) {
// The types were not compatible and there is only one ftyp atom, so reject the file. // The types were not compatible and there is only one ftyp atom, so reject the file.
return false; return new UnsupportedBrandsSniffFailure(majorBrand, compatibleBrands);
} }
} else if (atomDataSize != 0) { } else if (atomDataSize != 0) {
// Skip the atom. // Skip the atom.
input.advancePeekPosition(atomDataSize); input.advancePeekPosition(atomDataSize);
} }
} }
return foundGoodFileType && fragmented == isFragmented; if (!foundGoodFileType) {
return NoDeclaredBrandSniffFailure.INSTANCE;
} else if (fragmented != isFragmented) {
return isFragmented
? IncorrectFragmentationSniffFailure.FILE_FRAGMENTED
: IncorrectFragmentationSniffFailure.FILE_NOT_FRAGMENTED;
} else {
return null;
}
} }
/** /**

View File

@ -0,0 +1,43 @@
/*
* Copyright 2024 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.extractor.mp4;
import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.extractor.SniffFailure;
import com.google.common.primitives.ImmutableIntArray;
/**
* A {@link SniffFailure} indicating none of the brands declared in the {@code ftyp} box of the MP4
* file are supported (see ISO 14496-12:2012 section 4.3).
*/
@UnstableApi
public final class UnsupportedBrandsSniffFailure implements SniffFailure {
/** The {@code major_brand} from the {@code ftyp} box. */
public final int majorBrand;
/** The {@code compatible_brands} list from the {@code ftyp} box. */
public final ImmutableIntArray compatibleBrands;
public UnsupportedBrandsSniffFailure(int majorBrand, @Nullable int[] compatibleBrands) {
this.majorBrand = majorBrand;
this.compatibleBrands =
compatibleBrands != null
? ImmutableIntArray.copyOf(compatibleBrands)
: ImmutableIntArray.of();
}
}

View File

@ -0,0 +1,80 @@
/*
* Copyright 2024 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.extractor.mp4;
import static com.google.common.truth.Truth.assertThat;
import androidx.media3.extractor.SniffFailure;
import androidx.media3.extractor.text.SubtitleParser;
import androidx.media3.test.utils.FakeExtractorInput;
import androidx.media3.test.utils.TestUtil;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import java.io.IOException;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Non-parameterized tests for {@link FragmentedMp4Extractor}. */
@RunWith(AndroidJUnit4.class)
public final class FragmentedMp4ExtractorNonParameterizedTest {
@Test
public void sniff_reportsUnsupportedBrandsFailure() throws Exception {
FragmentedMp4Extractor extractor =
new FragmentedMp4Extractor(SubtitleParser.Factory.UNSUPPORTED);
FakeExtractorInput input = createInputForSample("sample_fragmented_unsupported_brands.mp4");
boolean sniffResult = extractor.sniff(input);
ImmutableList<SniffFailure> sniffFailures = extractor.getSniffFailureDetails();
assertThat(sniffResult).isFalse();
SniffFailure sniffFailure = Iterables.getOnlyElement(sniffFailures);
assertThat(sniffFailure).isInstanceOf(UnsupportedBrandsSniffFailure.class);
UnsupportedBrandsSniffFailure unsupportedBrandsSniffFailure =
(UnsupportedBrandsSniffFailure) sniffFailure;
assertThat(unsupportedBrandsSniffFailure.majorBrand).isEqualTo(1767992930);
assertThat(unsupportedBrandsSniffFailure.compatibleBrands.asList())
.containsExactly(1919903851, 1835102819, 1953459817, 1801548922)
.inOrder();
}
@Test
public void sniff_reportsWrongFragmentationFailure() throws Exception {
FragmentedMp4Extractor extractor =
new FragmentedMp4Extractor(SubtitleParser.Factory.UNSUPPORTED);
FakeExtractorInput input = createInputForSample("sample.mp4");
boolean sniffResult = extractor.sniff(input);
ImmutableList<SniffFailure> sniffFailures = extractor.getSniffFailureDetails();
assertThat(sniffResult).isFalse();
SniffFailure sniffFailure = Iterables.getOnlyElement(sniffFailures);
assertThat(sniffFailure).isInstanceOf(IncorrectFragmentationSniffFailure.class);
IncorrectFragmentationSniffFailure incorrectFragmentationSniffFailure =
(IncorrectFragmentationSniffFailure) sniffFailure;
assertThat(incorrectFragmentationSniffFailure.fileIsFragmented).isFalse();
}
private static FakeExtractorInput createInputForSample(String sample) throws IOException {
return new FakeExtractorInput.Builder()
.setData(
TestUtil.getByteArray(
ApplicationProvider.getApplicationContext(), "media/mp4/" + sample))
.build();
}
}

View File

@ -33,9 +33,12 @@ import org.robolectric.ParameterizedRobolectricTestRunner;
import org.robolectric.ParameterizedRobolectricTestRunner.Parameter; import org.robolectric.ParameterizedRobolectricTestRunner.Parameter;
import org.robolectric.ParameterizedRobolectricTestRunner.Parameters; import org.robolectric.ParameterizedRobolectricTestRunner.Parameters;
/** Tests for {@link FragmentedMp4Extractor} that test behaviours where sniffing must be tested. */ /**
* Tests for {@link FragmentedMp4Extractor} that test behaviours where sniffing must be tested using
* parameterization and {@link ExtractorAsserts}.
*/
@RunWith(ParameterizedRobolectricTestRunner.class) @RunWith(ParameterizedRobolectricTestRunner.class)
public final class FragmentedMp4ExtractorTest { public final class FragmentedMp4ExtractorParameterizedTest {
@Parameters(name = "{0},subtitlesParsedDuringExtraction={1}") @Parameters(name = "{0},subtitlesParsedDuringExtraction={1}")
public static List<Object[]> params() { public static List<Object[]> params() {

View File

@ -0,0 +1,112 @@
/*
* Copyright 2024 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.extractor.mp4;
import static com.google.common.truth.Truth.assertThat;
import androidx.media3.extractor.Extractor;
import androidx.media3.extractor.PositionHolder;
import androidx.media3.extractor.SniffFailure;
import androidx.media3.extractor.text.SubtitleParser;
import androidx.media3.test.utils.FakeExtractorInput;
import androidx.media3.test.utils.FakeExtractorOutput;
import androidx.media3.test.utils.FakeTrackOutput;
import androidx.media3.test.utils.TestUtil;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import java.io.IOException;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Non-parameterized tests for {@link Mp4Extractor}. */
@RunWith(AndroidJUnit4.class)
public final class Mp4ExtractorNonParameterizedTest {
@Test
public void sniff_reportsUnsupportedBrandsFailure() throws Exception {
Mp4Extractor extractor = new Mp4Extractor(SubtitleParser.Factory.UNSUPPORTED);
FakeExtractorInput input = createInputForSample("sample_unsupported_brands.mp4");
boolean sniffResult = extractor.sniff(input);
ImmutableList<SniffFailure> sniffFailures = extractor.getSniffFailureDetails();
assertThat(sniffResult).isFalse();
SniffFailure sniffFailure = Iterables.getOnlyElement(sniffFailures);
assertThat(sniffFailure).isInstanceOf(UnsupportedBrandsSniffFailure.class);
UnsupportedBrandsSniffFailure unsupportedBrandsSniffFailure =
(UnsupportedBrandsSniffFailure) sniffFailure;
assertThat(unsupportedBrandsSniffFailure.majorBrand).isEqualTo(1767992930);
assertThat(unsupportedBrandsSniffFailure.compatibleBrands.asList())
.containsExactly(1919903851, 1835102819, 1953459817, 1801548922)
.inOrder();
}
@Test
public void sniff_reportsWrongFragmentationFailure() throws Exception {
Mp4Extractor extractor = new Mp4Extractor(SubtitleParser.Factory.UNSUPPORTED);
FakeExtractorInput input = createInputForSample("sample_fragmented.mp4");
boolean sniffResult = extractor.sniff(input);
ImmutableList<SniffFailure> sniffFailures = extractor.getSniffFailureDetails();
assertThat(sniffResult).isFalse();
SniffFailure sniffFailure = Iterables.getOnlyElement(sniffFailures);
assertThat(sniffFailure).isInstanceOf(IncorrectFragmentationSniffFailure.class);
IncorrectFragmentationSniffFailure incorrectFragmentationSniffFailure =
(IncorrectFragmentationSniffFailure) sniffFailure;
assertThat(incorrectFragmentationSniffFailure.fileIsFragmented).isTrue();
}
@Test
public void getSeekPoints_withEmptyTracks_returnsValidInformation() throws Exception {
Mp4Extractor extractor = new Mp4Extractor(SubtitleParser.Factory.UNSUPPORTED);
FakeExtractorInput input = createInputForSample("sample_empty_track.mp4");
FakeExtractorOutput output =
new FakeExtractorOutput(
(id, type) -> new FakeTrackOutput(/* deduplicateConsecutiveFormats= */ true));
PositionHolder seekPositionHolder = new PositionHolder();
extractor.init(output);
int readResult = Extractor.RESULT_CONTINUE;
while (readResult != Extractor.RESULT_END_OF_INPUT) {
readResult = extractor.read(input, seekPositionHolder);
if (readResult == Extractor.RESULT_SEEK) {
long seekPosition = seekPositionHolder.position;
input.setPosition((int) seekPosition);
}
}
ImmutableList.Builder<Long> trackSeekTimesUs = ImmutableList.builder();
long testPositionUs = output.seekMap.getDurationUs() / 2;
for (int i = 0; i < output.numberOfTracks; i++) {
int trackId = output.trackOutputs.keyAt(i);
trackSeekTimesUs.add(extractor.getSeekPoints(testPositionUs, trackId).first.timeUs);
}
long extractorSeekTimeUs = extractor.getSeekPoints(testPositionUs).first.timeUs;
assertThat(output.numberOfTracks).isEqualTo(2);
assertThat(extractorSeekTimeUs).isIn(trackSeekTimesUs.build());
}
private static FakeExtractorInput createInputForSample(String sample) throws IOException {
return new FakeExtractorInput.Builder()
.setData(
TestUtil.getByteArray(
ApplicationProvider.getApplicationContext(), "media/mp4/" + sample))
.build();
}
}

View File

@ -16,19 +16,10 @@
package androidx.media3.extractor.mp4; package androidx.media3.extractor.mp4;
import static androidx.media3.extractor.mp4.FragmentedMp4Extractor.FLAG_EMIT_RAW_SUBTITLE_DATA; import static androidx.media3.extractor.mp4.FragmentedMp4Extractor.FLAG_EMIT_RAW_SUBTITLE_DATA;
import static com.google.common.truth.Truth.assertThat;
import androidx.media3.extractor.Extractor;
import androidx.media3.extractor.PositionHolder;
import androidx.media3.extractor.text.DefaultSubtitleParserFactory; import androidx.media3.extractor.text.DefaultSubtitleParserFactory;
import androidx.media3.extractor.text.SubtitleParser; import androidx.media3.extractor.text.SubtitleParser;
import androidx.media3.test.utils.ExtractorAsserts; import androidx.media3.test.utils.ExtractorAsserts;
import androidx.media3.test.utils.FakeExtractorInput;
import androidx.media3.test.utils.FakeExtractorOutput;
import androidx.media3.test.utils.FakeTrackOutput;
import androidx.media3.test.utils.TestUtil;
import androidx.test.core.app.ApplicationProvider;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.junit.Test; import org.junit.Test;
@ -37,9 +28,9 @@ import org.robolectric.ParameterizedRobolectricTestRunner;
import org.robolectric.ParameterizedRobolectricTestRunner.Parameter; import org.robolectric.ParameterizedRobolectricTestRunner.Parameter;
import org.robolectric.ParameterizedRobolectricTestRunner.Parameters; import org.robolectric.ParameterizedRobolectricTestRunner.Parameters;
/** Tests for {@link Mp4Extractor}. */ /** Parameterized tests for {@link Mp4Extractor} using {@link ExtractorAsserts}. */
@RunWith(ParameterizedRobolectricTestRunner.class) @RunWith(ParameterizedRobolectricTestRunner.class)
public final class Mp4ExtractorTest { public final class Mp4ExtractorParameterizedTest {
@Parameters(name = "{0},subtitlesParsedDuringExtraction={1}") @Parameters(name = "{0},subtitlesParsedDuringExtraction={1}")
public static List<Object[]> params() { public static List<Object[]> params() {
@ -243,43 +234,6 @@ public final class Mp4ExtractorTest {
simulationConfig); simulationConfig);
} }
@Test
public void getSeekPoints_withEmptyTracks_returnsValidInformation() throws Exception {
Mp4Extractor extractor =
(Mp4Extractor) getExtractorFactory(subtitlesParsedDuringExtraction).create();
FakeExtractorInput input =
new FakeExtractorInput.Builder()
.setData(
TestUtil.getByteArray(
ApplicationProvider.getApplicationContext(),
"media/mp4/sample_empty_track.mp4"))
.build();
FakeExtractorOutput output =
new FakeExtractorOutput(
(id, type) -> new FakeTrackOutput(/* deduplicateConsecutiveFormats= */ true));
PositionHolder seekPositionHolder = new PositionHolder();
extractor.init(output);
int readResult = Extractor.RESULT_CONTINUE;
while (readResult != Extractor.RESULT_END_OF_INPUT) {
readResult = extractor.read(input, seekPositionHolder);
if (readResult == Extractor.RESULT_SEEK) {
long seekPosition = seekPositionHolder.position;
input.setPosition((int) seekPosition);
}
}
ImmutableList.Builder<Long> trackSeekTimesUs = ImmutableList.builder();
long testPositionUs = output.seekMap.getDurationUs() / 2;
for (int i = 0; i < output.numberOfTracks; i++) {
int trackId = output.trackOutputs.keyAt(i);
trackSeekTimesUs.add(extractor.getSeekPoints(testPositionUs, trackId).first.timeUs);
}
long extractorSeekTimeUs = extractor.getSeekPoints(testPositionUs).first.timeUs;
assertThat(output.numberOfTracks).isEqualTo(2);
assertThat(extractorSeekTimeUs).isIn(trackSeekTimesUs.build());
}
private static ExtractorAsserts.ExtractorFactory getExtractorFactory( private static ExtractorAsserts.ExtractorFactory getExtractorFactory(
boolean subtitlesParsedDuringExtraction) { boolean subtitlesParsedDuringExtraction) {
SubtitleParser.Factory subtitleParserFactory; SubtitleParser.Factory subtitleParserFactory;