mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Add more details about why Extractor.sniff
returned false
PiperOrigin-RevId: 609335656
This commit is contained in:
parent
41886434ad
commit
d1ae9ffc52
@ -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<SniffFailure> 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<SniffFailure> 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);
|
||||
|
@ -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.
|
||||
*
|
||||
* <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 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);
|
||||
this.uri = uri;
|
||||
this.sniffFailures = ImmutableList.copyOf(sniffFailures);
|
||||
}
|
||||
}
|
||||
|
@ -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}.
|
||||
*
|
||||
* <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.
|
||||
*
|
||||
|
@ -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 {}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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<MetadataSampleInfo> pendingMetadataSampleInfos;
|
||||
@Nullable private final TrackOutput additionalEmsgTrackOutput;
|
||||
|
||||
private ImmutableList<SniffFailure> 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<SniffFailure> getSniffFailureDetails() {
|
||||
return lastSniffFailures;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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<Metadata.Entry> slowMotionMetadataEntries;
|
||||
|
||||
private ImmutableList<SniffFailure> 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(
|
||||
@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<SniffFailure> getSniffFailureDetails() {
|
||||
return lastSniffFailures;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -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() {}
|
||||
}
|
@ -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.
|
||||
int majorBrand = buffer.readInt();
|
||||
if (isCompatibleBrand(majorBrand, acceptHeic)) {
|
||||
foundGoodFileType = true;
|
||||
}
|
||||
// Skip the minorVersion.
|
||||
buffer.skipBytes(4);
|
||||
} else if (isCompatibleBrand(buffer.readInt(), acceptHeic)) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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<Object[]> params() {
|
@ -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();
|
||||
}
|
||||
}
|
@ -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<Object[]> 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<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(
|
||||
boolean subtitlesParsedDuringExtraction) {
|
||||
SubtitleParser.Factory subtitleParserFactory;
|
Binary file not shown.
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user