diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/BundledExtractorsAdapter.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/BundledExtractorsAdapter.java index 7ca883fade..f0b2f7e816 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/BundledExtractorsAdapter.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/BundledExtractorsAdapter.java @@ -28,7 +28,9 @@ import androidx.media3.extractor.ExtractorInput; import androidx.media3.extractor.ExtractorOutput; import androidx.media3.extractor.ExtractorsFactory; import androidx.media3.extractor.PositionHolder; +import androidx.media3.extractor.SniffFailure; import androidx.media3.extractor.mp3.Mp3Extractor; +import com.google.common.collect.ImmutableList; import java.io.EOFException; import java.io.IOException; import java.util.List; @@ -70,6 +72,8 @@ public final class BundledExtractorsAdapter implements ProgressiveMediaExtractor return; } Extractor[] extractors = extractorsFactory.createExtractors(uri, responseHeaders); + ImmutableList.Builder sniffFailures = + ImmutableList.builderWithExpectedSize(extractors.length); if (extractors.length == 1) { this.extractor = extractors[0]; } else { @@ -78,6 +82,9 @@ public final class BundledExtractorsAdapter implements ProgressiveMediaExtractor if (extractor.sniff(extractorInput)) { this.extractor = extractor; break; + } else { + List sniffFailureDetails = extractor.getSniffFailureDetails(); + sniffFailures.addAll(sniffFailureDetails); } } catch (EOFException e) { // Do nothing. @@ -91,7 +98,8 @@ public final class BundledExtractorsAdapter implements ProgressiveMediaExtractor "None of the available extractors (" + Util.getCommaDelimitedSimpleClassNames(extractors) + ") could read the stream.", - Assertions.checkNotNull(uri)); + Assertions.checkNotNull(uri), + sniffFailures.build()); } } extractor.init(output); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/UnrecognizedInputFormatException.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/UnrecognizedInputFormatException.java index 8c6e944556..f058c5ec71 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/UnrecognizedInputFormatException.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/UnrecognizedInputFormatException.java @@ -19,6 +19,11 @@ import android.net.Uri; import androidx.media3.common.C; import androidx.media3.common.ParserException; 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. */ @UnstableApi @@ -28,11 +33,36 @@ public class UnrecognizedInputFormatException extends ParserException { public final Uri uri; /** + * Sniff failures from {@link Extractor#getSniffFailureDetails()} from any extractors that were + * checked while trying to recognize the input data. + * + *

May be empty if no extractors provided additional sniffing failure details. + */ + public final ImmutableList 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 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 sniffFailures) { super(message, /* cause= */ null, /* contentIsMalformed= */ false, C.DATA_TYPE_MEDIA); this.uri = uri; + this.sniffFailures = ImmutableList.copyOf(sniffFailures); } } diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/Extractor.java b/libraries/extractor/src/main/java/androidx/media3/extractor/Extractor.java index 3322cf69ff..ad297263ec 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/Extractor.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/Extractor.java @@ -20,11 +20,13 @@ import static java.lang.annotation.ElementType.TYPE_USE; import androidx.annotation.IntDef; import androidx.media3.common.C; import androidx.media3.common.util.UnstableApi; +import com.google.common.collect.ImmutableList; import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.List; import org.checkerframework.dataflow.qual.SideEffectFree; /** Extracts media data from a container format. */ @@ -74,6 +76,19 @@ public interface Extractor { */ 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}. + * + *

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 getSniffFailureDetails() { + return ImmutableList.of(); + } + /** * Initializes the extractor with an {@link ExtractorOutput}. Called at most once. * diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/SniffFailure.java b/libraries/extractor/src/main/java/androidx/media3/extractor/SniffFailure.java new file mode 100644 index 0000000000..72473463d6 --- /dev/null +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/SniffFailure.java @@ -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 {} diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomSizeTooSmallSniffFailure.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomSizeTooSmallSniffFailure.java new file mode 100644 index 0000000000..8f09929c99 --- /dev/null +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomSizeTooSmallSniffFailure.java @@ -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; + } +} diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/FragmentedMp4Extractor.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/FragmentedMp4Extractor.java index 3cc1ec8b1a..4e039697c0 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/FragmentedMp4Extractor.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/FragmentedMp4Extractor.java @@ -49,6 +49,7 @@ import androidx.media3.extractor.ExtractorsFactory; import androidx.media3.extractor.GaplessInfoHolder; import androidx.media3.extractor.PositionHolder; import androidx.media3.extractor.SeekMap; +import androidx.media3.extractor.SniffFailure; import androidx.media3.extractor.TrackOutput; import androidx.media3.extractor.metadata.emsg.EventMessage; import androidx.media3.extractor.metadata.emsg.EventMessageEncoder; @@ -187,6 +188,7 @@ public class FragmentedMp4Extractor implements Extractor { private final ArrayDeque pendingMetadataSampleInfos; @Nullable private final TrackOutput additionalEmsgTrackOutput; + private ImmutableList lastSniffFailures; private int parserState; private int atomType; private long atomSize; @@ -383,6 +385,7 @@ public class FragmentedMp4Extractor implements Extractor { containerAtoms = new ArrayDeque<>(); pendingMetadataSampleInfos = new ArrayDeque<>(); trackBundles = new SparseArray<>(); + lastSniffFailures = ImmutableList.of(); durationUs = C.TIME_UNSET; pendingSeekTimeUs = C.TIME_UNSET; segmentIndexEarliestPresentationTimeUs = C.TIME_UNSET; @@ -393,7 +396,14 @@ public class FragmentedMp4Extractor implements Extractor { @Override 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 getSniffFailureDetails() { + return lastSniffFailures; } @Override diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/IncorrectFragmentationSniffFailure.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/IncorrectFragmentationSniffFailure.java new file mode 100644 index 0000000000..7268ea7b01 --- /dev/null +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/IncorrectFragmentationSniffFailure.java @@ -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; + } +} diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/Mp4Extractor.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/Mp4Extractor.java index 942ff61c86..047d9fe8c1 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/Mp4Extractor.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/Mp4Extractor.java @@ -45,6 +45,7 @@ import androidx.media3.extractor.GaplessInfoHolder; import androidx.media3.extractor.PositionHolder; import androidx.media3.extractor.SeekMap; import androidx.media3.extractor.SeekPoint; +import androidx.media3.extractor.SniffFailure; import androidx.media3.extractor.TrackOutput; import androidx.media3.extractor.TrueHdSampleRechunker; 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.text.SubtitleParser; import androidx.media3.extractor.text.SubtitleTranscodingExtractorOutput; +import com.google.common.collect.ImmutableList; import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -182,6 +184,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { private final SefReader sefReader; private final List slowMotionMetadataEntries; + private ImmutableList lastSniffFailures; private @State int parserState; private int atomType; private long atomSize; @@ -241,6 +244,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { public Mp4Extractor(SubtitleParser.Factory subtitleParserFactory, @Flags int flags) { this.subtitleParserFactory = subtitleParserFactory; this.flags = flags; + lastSniffFailures = ImmutableList.of(); parserState = ((flags & FLAG_READ_SEF_DATA) != 0) ? STATE_READING_SEF : STATE_READING_ATOM_HEADER; sefReader = new SefReader(); @@ -257,8 +261,17 @@ public final class Mp4Extractor implements Extractor, SeekMap { @Override public boolean sniff(ExtractorInput input) throws IOException { - return Sniffer.sniffUnfragmented( - input, /* acceptHeic= */ (flags & FLAG_READ_MOTION_PHOTO_METADATA) != 0); + @Nullable + 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 getSniffFailureDetails() { + return lastSniffFailures; } @Override diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/NoDeclaredBrandSniffFailure.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/NoDeclaredBrandSniffFailure.java new file mode 100644 index 0000000000..3013e37672 --- /dev/null +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/NoDeclaredBrandSniffFailure.java @@ -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() {} +} diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/Sniffer.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/Sniffer.java index 846e527441..cb2c16b948 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/Sniffer.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/Sniffer.java @@ -15,9 +15,11 @@ */ package androidx.media3.extractor.mp4; +import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.util.ParsableByteArray; import androidx.media3.extractor.ExtractorInput; +import androidx.media3.extractor.SniffFailure; 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 - * input being a fragmented MP4 file. + * Returns {@code null} if data peeked from the current position in {@code input} is consistent + * 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. - * @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. */ - public static boolean sniffFragmented(ExtractorInput input) throws IOException { + @Nullable + public static SniffFailure sniffFragmented(ExtractorInput input) throws IOException { return sniffInternal(input, /* fragmented= */ true, /* acceptHeic= */ false); } /** - * Returns whether data peeked from the current position in {@code input} is consistent with the - * input being an unfragmented MP4 file. + * Returns {@code null} if data peeked from the current position in {@code input} is consistent + * 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. - * @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. */ - public static boolean sniffUnfragmented(ExtractorInput input) throws IOException { - return sniffInternal(input, /* fragmented= */ false, /* acceptHeic= */ false); - } - - /** - * 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) + @Nullable + public static SniffFailure sniffUnfragmented(ExtractorInput input, boolean acceptHeic) throws IOException { return sniffInternal(input, /* fragmented= */ false, acceptHeic); } - private static boolean sniffInternal(ExtractorInput input, boolean fragmented, boolean acceptHeic) - throws IOException { + @Nullable + private static SniffFailure sniffInternal( + ExtractorInput input, boolean fragmented, boolean acceptHeic) throws IOException { long inputLength = input.getLength(); int bytesToSearch = (int) @@ -148,7 +145,7 @@ import java.io.IOException; if (atomSize < headerSize) { // The file is invalid because the atom size is too small for its header. - return false; + return new AtomSizeTooSmallSniffFailure(atomType, atomSize, headerSize); } bytesSearched += headerSize; @@ -186,30 +183,46 @@ import java.io.IOException; if (atomType == Atom.TYPE_ftyp) { // Parse the atom and check the file type/brand is compatible with the extractors. if (atomDataSize < 8) { - return false; + return new AtomSizeTooSmallSniffFailure(atomType, atomDataSize, 8); } buffer.reset(atomDataSize); input.peekFully(buffer.getData(), 0, atomDataSize); - int brandsCount = atomDataSize / 4; - for (int i = 0; i < brandsCount; i++) { - if (i == 1) { - // This index refers to the minorVersion, not a brand, so skip it. - buffer.skipBytes(4); - } else if (isCompatibleBrand(buffer.readInt(), acceptHeic)) { - foundGoodFileType = true; - break; + int majorBrand = buffer.readInt(); + if (isCompatibleBrand(majorBrand, acceptHeic)) { + foundGoodFileType = true; + } + // Skip the minorVersion. + buffer.skipBytes(4); + int compatibleBrandsCount = buffer.bytesLeft() / 4; + @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) { // 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) { // Skip the atom. 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; + } } /** diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/UnsupportedBrandsSniffFailure.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/UnsupportedBrandsSniffFailure.java new file mode 100644 index 0000000000..3934b9a327 --- /dev/null +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/UnsupportedBrandsSniffFailure.java @@ -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(); + } +} diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/FragmentedMp4ExtractorNonParameterizedTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/FragmentedMp4ExtractorNonParameterizedTest.java new file mode 100644 index 0000000000..b3a7a51900 --- /dev/null +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/FragmentedMp4ExtractorNonParameterizedTest.java @@ -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 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 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(); + } +} diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/FragmentedMp4ExtractorTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/FragmentedMp4ExtractorParameterizedTest.java similarity index 97% rename from libraries/extractor/src/test/java/androidx/media3/extractor/mp4/FragmentedMp4ExtractorTest.java rename to libraries/extractor/src/test/java/androidx/media3/extractor/mp4/FragmentedMp4ExtractorParameterizedTest.java index 8bbf75ba00..5ffda621f9 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/FragmentedMp4ExtractorTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/FragmentedMp4ExtractorParameterizedTest.java @@ -33,9 +33,12 @@ import org.robolectric.ParameterizedRobolectricTestRunner; import org.robolectric.ParameterizedRobolectricTestRunner.Parameter; 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) -public final class FragmentedMp4ExtractorTest { +public final class FragmentedMp4ExtractorParameterizedTest { @Parameters(name = "{0},subtitlesParsedDuringExtraction={1}") public static List params() { diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/Mp4ExtractorNonParameterizedTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/Mp4ExtractorNonParameterizedTest.java new file mode 100644 index 0000000000..cf22ab78e7 --- /dev/null +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/Mp4ExtractorNonParameterizedTest.java @@ -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 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 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 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(); + } +} diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/Mp4ExtractorTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/Mp4ExtractorParameterizedTest.java similarity index 79% rename from libraries/extractor/src/test/java/androidx/media3/extractor/mp4/Mp4ExtractorTest.java rename to libraries/extractor/src/test/java/androidx/media3/extractor/mp4/Mp4ExtractorParameterizedTest.java index 42adc72fd8..ac4bd64b18 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/Mp4ExtractorTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/Mp4ExtractorParameterizedTest.java @@ -16,19 +16,10 @@ package androidx.media3.extractor.mp4; 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.SubtitleParser; 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.List; import org.junit.Test; @@ -37,9 +28,9 @@ import org.robolectric.ParameterizedRobolectricTestRunner; import org.robolectric.ParameterizedRobolectricTestRunner.Parameter; import org.robolectric.ParameterizedRobolectricTestRunner.Parameters; -/** Tests for {@link Mp4Extractor}. */ +/** Parameterized tests for {@link Mp4Extractor} using {@link ExtractorAsserts}. */ @RunWith(ParameterizedRobolectricTestRunner.class) -public final class Mp4ExtractorTest { +public final class Mp4ExtractorParameterizedTest { @Parameters(name = "{0},subtitlesParsedDuringExtraction={1}") public static List params() { @@ -243,43 +234,6 @@ public final class Mp4ExtractorTest { 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 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( boolean subtitlesParsedDuringExtraction) { SubtitleParser.Factory subtitleParserFactory; diff --git a/libraries/test_data/src/test/assets/media/mp4/sample_fragmented_unsupported_brands.mp4 b/libraries/test_data/src/test/assets/media/mp4/sample_fragmented_unsupported_brands.mp4 new file mode 100644 index 0000000000..6c21684857 Binary files /dev/null and b/libraries/test_data/src/test/assets/media/mp4/sample_fragmented_unsupported_brands.mp4 differ diff --git a/libraries/test_data/src/test/assets/media/mp4/sample_unsupported_brands.mp4 b/libraries/test_data/src/test/assets/media/mp4/sample_unsupported_brands.mp4 new file mode 100644 index 0000000000..fc796b0efb Binary files /dev/null and b/libraries/test_data/src/test/assets/media/mp4/sample_unsupported_brands.mp4 differ