Optimize DefaultExtractorsFactory order using MIME types

PiperOrigin-RevId: 315485985
This commit is contained in:
kimvde 2020-06-09 16:12:36 +01:00 committed by Oliver Woodman
parent c759b5b1a9
commit 1f17756ad2
8 changed files with 83 additions and 29 deletions

View file

@ -174,8 +174,8 @@
* Change the order of extractors for sniffing to reduce start-up latency
in `DefaultExtractorsFactory` and `DefaultHlsExtractorsFactory`
([#6410](https://github.com/google/ExoPlayer/issues/6410)).
* Select first extractors based on the filename extension in
`DefaultExtractorsFactory`.
* Select first extractors based on the filename extension and the response
headers mime type in `DefaultExtractorsFactory`.
* Testing
* Add `TestExoPlayer`, a utility class with APIs to create
`SimpleExoPlayer` instances with fake components for testing.

View file

@ -22,6 +22,7 @@ import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor;
import com.google.android.exoplayer2.upstream.DataReader;
@ -29,6 +30,8 @@ import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.io.EOFException;
import java.io.IOException;
import java.util.List;
import java.util.Map;
/**
* {@link ProgressiveMediaExtractor} built on top of {@link Extractor} instances, whose
@ -36,7 +39,7 @@ import java.io.IOException;
*/
/* package */ final class BundledExtractorsAdapter implements ProgressiveMediaExtractor {
private final Extractor[] extractors;
private final ExtractorsFactory extractorsFactory;
@Nullable private Extractor extractor;
@Nullable private ExtractorInput extractorInput;
@ -44,21 +47,27 @@ import java.io.IOException;
/**
* Creates a holder that will select an extractor and initialize it using the specified output.
*
* @param extractors One or more extractors to choose from.
* @param extractorsFactory The {@link ExtractorsFactory} providing the extractors to choose from.
*/
public BundledExtractorsAdapter(Extractor[] extractors) {
this.extractors = extractors;
public BundledExtractorsAdapter(ExtractorsFactory extractorsFactory) {
this.extractorsFactory = extractorsFactory;
}
@Override
public void init(
DataReader dataReader, Uri uri, long position, long length, ExtractorOutput output)
DataReader dataReader,
Uri uri,
Map<String, List<String>> responseHeaders,
long position,
long length,
ExtractorOutput output)
throws IOException {
ExtractorInput extractorInput = new DefaultExtractorInput(dataReader, position, length);
this.extractorInput = extractorInput;
if (extractor != null) {
return;
}
Extractor[] extractors = extractorsFactory.createExtractors(uri, responseHeaders);
if (extractors.length == 1) {
this.extractor = extractors[0];
} else {

View file

@ -22,6 +22,8 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.upstream.DataReader;
import java.io.IOException;
import java.util.List;
import java.util.Map;
/** Extracts the contents of a container file from a progressive media stream. */
/* package */ interface ProgressiveMediaExtractor {
@ -31,6 +33,7 @@ import java.io.IOException;
*
* @param dataReader The {@link DataReader} from which data should be read.
* @param uri The {@link Uri} from which the media is obtained.
* @param responseHeaders The response headers of the media, or an empty map if there are none.
* @param position The initial position of the {@code dataReader} in the stream.
* @param length The length of the stream, or {@link C#LENGTH_UNSET} if length is unknown.
* @param output The {@link ExtractorOutput} that will be used to initialize the selected
@ -38,7 +41,13 @@ import java.io.IOException;
* @throws UnrecognizedInputFormatException Thrown if the input format could not be detected.
* @throws IOException Thrown if the input could not be read.
*/
void init(DataReader dataReader, Uri uri, long position, long length, ExtractorOutput output)
void init(
DataReader dataReader,
Uri uri,
Map<String, List<String>> responseHeaders,
long position,
long length,
ExtractorOutput output)
throws IOException;
/** Releases any held resources. */

View file

@ -27,6 +27,7 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.SeekMap.SeekPoints;
@ -143,7 +144,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* @param uri The {@link Uri} of the media stream.
* @param dataSource The data source to read the media.
* @param extractors The extractors to use to read the data source.
* @param extractorsFactory The {@link ExtractorsFactory} to use to read the data source.
* @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}.
* @param eventDispatcher A dispatcher to notify of events.
* @param listener A listener to notify when information about the period changes.
@ -161,7 +162,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public ProgressiveMediaPeriod(
Uri uri,
DataSource dataSource,
Extractor[] extractors,
ExtractorsFactory extractorsFactory,
DrmSessionManager drmSessionManager,
LoadErrorHandlingPolicy loadErrorHandlingPolicy,
EventDispatcher eventDispatcher,
@ -179,7 +180,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
this.customCacheKey = customCacheKey;
this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
loader = new Loader("Loader:ProgressiveMediaPeriod");
ProgressiveMediaExtractor progressiveMediaExtractor = new BundledExtractorsAdapter(extractors);
ProgressiveMediaExtractor progressiveMediaExtractor =
new BundledExtractorsAdapter(extractorsFactory);
this.progressiveMediaExtractor = progressiveMediaExtractor;
loadCondition = new ConditionVariable();
maybeFinishPrepareRunnable = this::maybeFinishPrepare;
@ -1022,7 +1024,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
icyTrackOutput.format(ICY_FORMAT);
}
progressiveMediaExtractor.init(
extractorDataSource, uri, position, length, extractorOutput);
extractorDataSource,
uri,
dataSource.getResponseHeaders(),
position,
length,
extractorOutput);
if (icyHeaders != null) {
progressiveMediaExtractor.disableSeekingOnMp3Streams();

View file

@ -276,7 +276,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource
return new ProgressiveMediaPeriod(
playbackProperties.uri,
dataSource,
extractorsFactory.createExtractors(playbackProperties.uri),
extractorsFactory,
drmSessionManager,
loadableLoadErrorHandlingPolicy,
createEventDispatcher(id),

View file

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.extractor;
import static com.google.android.exoplayer2.util.FileTypes.inferFileTypeFromResponseHeaders;
import static com.google.android.exoplayer2.util.FileTypes.inferFileTypeFromUri;
import android.net.Uri;
@ -39,7 +40,9 @@ import com.google.android.exoplayer2.util.FileTypes;
import com.google.android.exoplayer2.util.TimestampAdjuster;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* An {@link ExtractorsFactory} that provides an array of extractors for the following formats:
@ -265,18 +268,28 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
@Override
public synchronized Extractor[] createExtractors() {
return createExtractors(Uri.EMPTY);
return createExtractors(Uri.EMPTY, new HashMap<>());
}
@Override
public synchronized Extractor[] createExtractors(Uri uri) {
public synchronized Extractor[] createExtractors(
Uri uri, Map<String, List<String>> responseHeaders) {
List<Extractor> extractors = new ArrayList<>(/* initialCapacity= */ 14);
@FileTypes.Type int inferredFileType = inferFileTypeFromUri(uri);
addExtractorsForFormat(inferredFileType, extractors);
@FileTypes.Type
int responseHeadersInferredFileType = inferFileTypeFromResponseHeaders(responseHeaders);
if (responseHeadersInferredFileType != FileTypes.UNKNOWN) {
addExtractorsForFormat(responseHeadersInferredFileType, extractors);
}
@FileTypes.Type int uriInferredFileType = inferFileTypeFromUri(uri);
if (uriInferredFileType != FileTypes.UNKNOWN
&& uriInferredFileType != responseHeadersInferredFileType) {
addExtractorsForFormat(uriInferredFileType, extractors);
}
for (int format : DEFAULT_EXTRACTOR_ORDER) {
if (format != inferredFileType) {
if (format != responseHeadersInferredFileType && format != uriInferredFileType) {
addExtractorsForFormat(format, extractors);
}
}

View file

@ -16,6 +16,8 @@
package com.google.android.exoplayer2.extractor;
import android.net.Uri;
import java.util.List;
import java.util.Map;
/** Factory for arrays of {@link Extractor} instances. */
public interface ExtractorsFactory {
@ -24,10 +26,14 @@ public interface ExtractorsFactory {
Extractor[] createExtractors();
/**
* Returns an array of new {@link Extractor} instances to extract the stream corresponding to the
* provided {@link Uri}.
* Returns an array of new {@link Extractor} instances.
*
* @param uri The {@link Uri} of the media to extract.
* @param responseHeaders The response headers of the media to extract, or an empty map if there
* are none. The map lookup should be case-insensitive.
* @return The {@link Extractor} instances.
*/
default Extractor[] createExtractors(Uri uri) {
default Extractor[] createExtractors(Uri uri, Map<String, List<String>> responseHeaders) {
return createExtractors();
}
}

View file

@ -33,8 +33,12 @@ import com.google.android.exoplayer2.extractor.ts.AdtsExtractor;
import com.google.android.exoplayer2.extractor.ts.PsExtractor;
import com.google.android.exoplayer2.extractor.ts.TsExtractor;
import com.google.android.exoplayer2.extractor.wav.WavExtractor;
import com.google.android.exoplayer2.util.MimeTypes;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -43,7 +47,7 @@ import org.junit.runner.RunWith;
public final class DefaultExtractorsFactoryTest {
@Test
public void createExtractors_withoutUri_optimizesSniffingOrder() {
public void createExtractors_withoutMediaInfo_optimizesSniffingOrder() {
DefaultExtractorsFactory defaultExtractorsFactory = new DefaultExtractorsFactory();
Extractor[] extractors = defaultExtractorsFactory.createExtractors();
@ -69,24 +73,31 @@ public final class DefaultExtractorsFactoryTest {
}
@Test
public void createExtractors_withUri_startsWithExtractorsMatchingExtension() {
public void createExtractors_withMediaInfo_startsWithExtractorsMatchingHeadersAndThenUri() {
DefaultExtractorsFactory defaultExtractorsFactory = new DefaultExtractorsFactory();
Uri uri = Uri.parse("test.mp3");
Map<String, List<String>> responseHeaders = new HashMap<>();
responseHeaders.put("Content-Type", Collections.singletonList(MimeTypes.VIDEO_MP4));
Extractor[] extractors = defaultExtractorsFactory.createExtractors(Uri.parse("test.mp4"));
Extractor[] extractors = defaultExtractorsFactory.createExtractors(uri, responseHeaders);
List<Class<? extends Extractor>> extractorClasses = getExtractorClasses(extractors);
assertThat(extractorClasses.subList(0, 2))
.containsExactly(Mp4Extractor.class, FragmentedMp4Extractor.class);
assertThat(extractorClasses.get(2)).isEqualTo(Mp3Extractor.class);
}
@Test
public void createExtractors_withUri_optimizesSniffingOrder() {
public void createExtractors_withMediaInfo_optimizesSniffingOrder() {
DefaultExtractorsFactory defaultExtractorsFactory = new DefaultExtractorsFactory();
Uri uri = Uri.parse("test.mp3");
Map<String, List<String>> responseHeaders = new HashMap<>();
responseHeaders.put("Content-Type", Collections.singletonList(MimeTypes.VIDEO_MP4));
Extractor[] extractors = defaultExtractorsFactory.createExtractors(Uri.parse("test.mp4"));
Extractor[] extractors = defaultExtractorsFactory.createExtractors(uri, responseHeaders);
List<Class<? extends Extractor>> extractorClasses = getExtractorClasses(extractors);
assertThat(extractorClasses.subList(2, extractors.length))
assertThat(extractorClasses.subList(3, extractors.length))
.containsExactly(
FlvExtractor.class,
FlacExtractor.class,
@ -98,8 +109,7 @@ public final class DefaultExtractorsFactoryTest {
MatroskaExtractor.class,
AdtsExtractor.class,
Ac3Extractor.class,
Ac4Extractor.class,
Mp3Extractor.class)
Ac4Extractor.class)
.inOrder();
}