Commit b14e0935 by aquilescanta Committed by Oliver Woodman

Add container format sniffing in HLS

Issue:#2025

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=211977802
parent da88b346
...@@ -32,6 +32,8 @@ ...@@ -32,6 +32,8 @@
[VR180](https://github.com/google/spatial-media/blob/master/docs/vr180.md). [VR180](https://github.com/google/spatial-media/blob/master/docs/vr180.md).
* HLS: * HLS:
* Support PlayReady. * Support PlayReady.
* Add container format sniffing
([#2025](https://github.com/google/ExoPlayer/issues/2025)).
* Support alternative `EXT-X-KEY` tags. * Support alternative `EXT-X-KEY` tags.
* Support `EXT-X-INDEPENDENT-SEGMENTS` in the master playlist. * Support `EXT-X-INDEPENDENT-SEGMENTS` in the master playlist.
* Support variable substitution * Support variable substitution
......
...@@ -21,14 +21,18 @@ import android.util.Pair; ...@@ -21,14 +21,18 @@ import android.util.Pair;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor;
import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
import com.google.android.exoplayer2.extractor.ts.Ac3Extractor; import com.google.android.exoplayer2.extractor.ts.Ac3Extractor;
import com.google.android.exoplayer2.extractor.ts.AdtsExtractor; import com.google.android.exoplayer2.extractor.ts.AdtsExtractor;
import com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory; import com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory;
import com.google.android.exoplayer2.extractor.ts.TsExtractor; import com.google.android.exoplayer2.extractor.ts.TsExtractor;
import com.google.android.exoplayer2.source.UnrecognizedInputFormatException;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.TimestampAdjuster;
import java.io.EOFException;
import java.io.IOException;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
...@@ -56,35 +60,120 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { ...@@ -56,35 +60,120 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
List<Format> muxedCaptionFormats, List<Format> muxedCaptionFormats,
DrmInitData drmInitData, DrmInitData drmInitData,
TimestampAdjuster timestampAdjuster, TimestampAdjuster timestampAdjuster,
Map<String, List<String>> responseHeaders) { Map<String, List<String>> responseHeaders,
ExtractorInput extractorInput)
throws InterruptedException, IOException {
if (previousExtractor != null) {
// A extractor has already been successfully used. Return one of the same type.
if (previousExtractor instanceof TsExtractor
|| previousExtractor instanceof FragmentedMp4Extractor) {
// TS and fMP4 extractors can be reused.
return buildResult(previousExtractor);
} else if (previousExtractor instanceof WebvttExtractor) {
return buildResult(new WebvttExtractor(format.language, timestampAdjuster));
} else if (previousExtractor instanceof AdtsExtractor) {
return buildResult(new AdtsExtractor());
} else if (previousExtractor instanceof Ac3Extractor) {
return buildResult(new Ac3Extractor());
} else if (previousExtractor instanceof Mp3Extractor) {
return buildResult(new Mp3Extractor());
} else {
throw new IllegalArgumentException(
"Unexpected previousExtractor type: " + previousExtractor.getClass().getSimpleName());
}
}
// Try selecting the extractor by the file extension.
Extractor extractorByFileExtension =
createExtractorByFileExtension(
uri, format, muxedCaptionFormats, drmInitData, timestampAdjuster);
extractorInput.resetPeekPosition();
if (sniffQuietly(extractorByFileExtension, extractorInput)) {
return buildResult(extractorByFileExtension);
}
// We need to manually sniff each known type, without retrying the one selected by file
// extension.
if (!(extractorByFileExtension instanceof WebvttExtractor)) {
WebvttExtractor webvttExtractor = new WebvttExtractor(format.language, timestampAdjuster);
if (sniffQuietly(webvttExtractor, extractorInput)) {
return buildResult(webvttExtractor);
}
}
if (!(extractorByFileExtension instanceof AdtsExtractor)) {
AdtsExtractor adtsExtractor = new AdtsExtractor();
if (sniffQuietly(adtsExtractor, extractorInput)) {
return buildResult(adtsExtractor);
}
}
if (!(extractorByFileExtension instanceof Ac3Extractor)) {
Ac3Extractor ac3Extractor = new Ac3Extractor();
if (sniffQuietly(ac3Extractor, extractorInput)) {
return buildResult(ac3Extractor);
}
}
if (!(extractorByFileExtension instanceof Mp3Extractor)) {
Mp3Extractor mp3Extractor =
new Mp3Extractor(/* flags= */ 0, /* forcedFirstSampleTimestampUs= */ 0);
if (sniffQuietly(mp3Extractor, extractorInput)) {
return buildResult(mp3Extractor);
}
}
if (!(extractorByFileExtension instanceof FragmentedMp4Extractor)) {
FragmentedMp4Extractor fragmentedMp4Extractor =
new FragmentedMp4Extractor(
/* flags= */ 0,
timestampAdjuster,
/* sideloadedTrack= */ null,
drmInitData,
muxedCaptionFormats != null ? muxedCaptionFormats : Collections.emptyList());
if (sniffQuietly(fragmentedMp4Extractor, extractorInput)) {
return buildResult(fragmentedMp4Extractor);
}
}
if (!(extractorByFileExtension instanceof TsExtractor)) {
TsExtractor tsExtractor = createTsExtractor(format, muxedCaptionFormats, timestampAdjuster);
if (sniffQuietly(tsExtractor, extractorInput)) {
return buildResult(tsExtractor);
}
}
throw new UnrecognizedInputFormatException(
"The segment does not seem to conform to any of the known HLS segment formats", uri);
}
private Extractor createExtractorByFileExtension(
Uri uri,
Format format,
List<Format> muxedCaptionFormats,
DrmInitData drmInitData,
TimestampAdjuster timestampAdjuster) {
String lastPathSegment = uri.getLastPathSegment(); String lastPathSegment = uri.getLastPathSegment();
if (lastPathSegment == null) { if (lastPathSegment == null) {
lastPathSegment = ""; lastPathSegment = "";
} }
boolean isPackedAudioExtractor = false;
Extractor extractor;
if (MimeTypes.TEXT_VTT.equals(format.sampleMimeType) if (MimeTypes.TEXT_VTT.equals(format.sampleMimeType)
|| lastPathSegment.endsWith(WEBVTT_FILE_EXTENSION) || lastPathSegment.endsWith(WEBVTT_FILE_EXTENSION)
|| lastPathSegment.endsWith(VTT_FILE_EXTENSION)) { || lastPathSegment.endsWith(VTT_FILE_EXTENSION)) {
extractor = new WebvttExtractor(format.language, timestampAdjuster); return new WebvttExtractor(format.language, timestampAdjuster);
} else if (lastPathSegment.endsWith(AAC_FILE_EXTENSION)) { } else if (lastPathSegment.endsWith(AAC_FILE_EXTENSION)) {
isPackedAudioExtractor = true; return new AdtsExtractor();
extractor = new AdtsExtractor();
} else if (lastPathSegment.endsWith(AC3_FILE_EXTENSION) } else if (lastPathSegment.endsWith(AC3_FILE_EXTENSION)
|| lastPathSegment.endsWith(EC3_FILE_EXTENSION)) { || lastPathSegment.endsWith(EC3_FILE_EXTENSION)) {
isPackedAudioExtractor = true; return new Ac3Extractor();
extractor = new Ac3Extractor();
} else if (lastPathSegment.endsWith(MP3_FILE_EXTENSION)) { } else if (lastPathSegment.endsWith(MP3_FILE_EXTENSION)) {
isPackedAudioExtractor = true; return new Mp3Extractor(/* flags= */ 0, /* forcedFirstSampleTimestampUs= */ 0);
extractor = new Mp3Extractor(0, 0);
} else if (previousExtractor != null) {
// Only reuse TS and fMP4 extractors.
extractor = previousExtractor;
} else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION) } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION)
|| lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4) || lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4)
|| lastPathSegment.startsWith(MP4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 5)) { || lastPathSegment.startsWith(MP4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 5)) {
extractor = return new FragmentedMp4Extractor(
new FragmentedMp4Extractor(
/* flags= */ 0, /* flags= */ 0,
timestampAdjuster, timestampAdjuster,
/* sideloadedTrack= */ null, /* sideloadedTrack= */ null,
...@@ -92,6 +181,12 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { ...@@ -92,6 +181,12 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
muxedCaptionFormats != null ? muxedCaptionFormats : Collections.emptyList()); muxedCaptionFormats != null ? muxedCaptionFormats : Collections.emptyList());
} else { } else {
// For any other file extension, we assume TS format. // For any other file extension, we assume TS format.
return createTsExtractor(format, muxedCaptionFormats, timestampAdjuster);
}
}
private static TsExtractor createTsExtractor(
Format format, List<Format> muxedCaptionFormats, TimestampAdjuster timestampAdjuster) {
@DefaultTsPayloadReaderFactory.Flags @DefaultTsPayloadReaderFactory.Flags
int esReaderFactoryFlags = DefaultTsPayloadReaderFactory.FLAG_IGNORE_SPLICE_INFO_STREAM; int esReaderFactoryFlags = DefaultTsPayloadReaderFactory.FLAG_IGNORE_SPLICE_INFO_STREAM;
if (muxedCaptionFormats != null) { if (muxedCaptionFormats != null) {
...@@ -120,10 +215,32 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { ...@@ -120,10 +215,32 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_H264_STREAM; esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_H264_STREAM;
} }
} }
extractor = new TsExtractor(TsExtractor.MODE_HLS, timestampAdjuster,
return new TsExtractor(
TsExtractor.MODE_HLS,
timestampAdjuster,
new DefaultTsPayloadReaderFactory(esReaderFactoryFlags, muxedCaptionFormats)); new DefaultTsPayloadReaderFactory(esReaderFactoryFlags, muxedCaptionFormats));
} }
return Pair.create(extractor, isPackedAudioExtractor);
private static Pair<Extractor, Boolean> buildResult(Extractor extractor) {
return new Pair<>(
extractor,
extractor instanceof AdtsExtractor
|| extractor instanceof Ac3Extractor
|| extractor instanceof Mp3Extractor);
}
private static boolean sniffQuietly(Extractor extractor, ExtractorInput input)
throws InterruptedException, IOException {
boolean result = false;
try {
result = extractor.sniff(input);
} catch (EOFException e) {
// Do nothing.
} finally {
input.resetPeekPosition();
}
return result;
} }
} }
...@@ -20,7 +20,10 @@ import android.util.Pair; ...@@ -20,7 +20,10 @@ import android.util.Pair;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.TimestampAdjuster;
import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
...@@ -45,9 +48,14 @@ public interface HlsExtractorFactory { ...@@ -45,9 +48,14 @@ public interface HlsExtractorFactory {
* @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number. * @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number.
* @param responseHeaders The HTTP response headers associated with the media segment or * @param responseHeaders The HTTP response headers associated with the media segment or
* initialization section to extract. * initialization section to extract.
* @param sniffingExtractorInput The first extractor input that will be passed to the returned
* extractor's {@link Extractor#read(ExtractorInput, PositionHolder)}. Must only be used to
* call {@link Extractor#sniff(ExtractorInput)}.
* @return A pair containing the {@link Extractor} and a boolean that indicates whether it is a * @return A pair containing the {@link Extractor} and a boolean that indicates whether it is a
* packed audio extractor. The first element may be {@code previousExtractor} if the factory * packed audio extractor. The first element may be {@code previousExtractor} if the factory
* has determined it can be re-used. * has determined it can be re-used.
* @throws InterruptedException If the thread is interrupted while sniffing.
* @throws IOException If an I/O error is encountered while sniffing.
*/ */
Pair<Extractor, Boolean> createExtractor( Pair<Extractor, Boolean> createExtractor(
Extractor previousExtractor, Extractor previousExtractor,
...@@ -56,5 +64,7 @@ public interface HlsExtractorFactory { ...@@ -56,5 +64,7 @@ public interface HlsExtractorFactory {
List<Format> muxedCaptionFormats, List<Format> muxedCaptionFormats,
DrmInitData drmInitData, DrmInitData drmInitData,
TimestampAdjuster timestampAdjuster, TimestampAdjuster timestampAdjuster,
Map<String, List<String>> responseHeaders); Map<String, List<String>> responseHeaders,
ExtractorInput sniffingExtractorInput)
throws InterruptedException, IOException;
} }
...@@ -73,15 +73,13 @@ import java.util.concurrent.atomic.AtomicInteger; ...@@ -73,15 +73,13 @@ import java.util.concurrent.atomic.AtomicInteger;
private final List<Format> muxedCaptionFormats; private final List<Format> muxedCaptionFormats;
private final DrmInitData drmInitData; private final DrmInitData drmInitData;
private final Extractor previousExtractor; private final Extractor previousExtractor;
private final Id3Decoder id3Decoder;
private final ParsableByteArray id3Data;
private Extractor extractor; private Extractor extractor;
private boolean isPackedAudioExtractor;
private Id3Decoder id3Decoder;
private ParsableByteArray id3Data;
private HlsSampleStreamWrapper output; private HlsSampleStreamWrapper output;
private int initSegmentBytesLoaded; private int initSegmentBytesLoaded;
private int nextLoadPosition; private int nextLoadPosition;
private boolean id3TimestampPeeked;
private boolean initLoadCompleted; private boolean initLoadCompleted;
private volatile boolean loadCanceled; private volatile boolean loadCanceled;
private boolean loadCompleted; private boolean loadCompleted;
...@@ -158,6 +156,8 @@ import java.util.concurrent.atomic.AtomicInteger; ...@@ -158,6 +156,8 @@ import java.util.concurrent.atomic.AtomicInteger;
previousExtractor = previousChunk.discontinuitySequenceNumber != discontinuitySequenceNumber previousExtractor = previousChunk.discontinuitySequenceNumber != discontinuitySequenceNumber
|| shouldSpliceIn ? null : previousChunk.extractor; || shouldSpliceIn ? null : previousChunk.extractor;
} else { } else {
id3Decoder = new Id3Decoder();
id3Data = new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH);
shouldSpliceIn = false; shouldSpliceIn = false;
} }
this.previousExtractor = previousExtractor; this.previousExtractor = previousExtractor;
...@@ -244,12 +244,6 @@ import java.util.concurrent.atomic.AtomicInteger; ...@@ -244,12 +244,6 @@ import java.util.concurrent.atomic.AtomicInteger;
} }
try { try {
ExtractorInput input = prepareExtraction(dataSource, loadDataSpec); ExtractorInput input = prepareExtraction(dataSource, loadDataSpec);
if (isPackedAudioExtractor && !id3TimestampPeeked) {
long id3Timestamp = peekId3PrivTimestamp(input);
id3TimestampPeeked = true;
output.setSampleOffsetUs(id3Timestamp != C.TIME_UNSET
? timestampAdjuster.adjustTsTimestamp(id3Timestamp) : startTimeUs);
}
if (skipLoadedBytes) { if (skipLoadedBytes) {
input.skipFully(nextLoadPosition); input.skipFully(nextLoadPosition);
} }
...@@ -267,10 +261,16 @@ import java.util.concurrent.atomic.AtomicInteger; ...@@ -267,10 +261,16 @@ import java.util.concurrent.atomic.AtomicInteger;
} }
private DefaultExtractorInput prepareExtraction(DataSource dataSource, DataSpec dataSpec) private DefaultExtractorInput prepareExtraction(DataSource dataSource, DataSpec dataSpec)
throws IOException { throws IOException, InterruptedException {
long bytesToRead = dataSource.open(dataSpec); long bytesToRead = dataSource.open(dataSpec);
DefaultExtractorInput extractorInput =
new DefaultExtractorInput(dataSource, dataSpec.absoluteStreamPosition, bytesToRead);
if (extractor == null) { if (extractor == null) {
long id3Timestamp = peekId3PrivTimestamp(extractorInput);
extractorInput.resetPeekPosition();
Pair<Extractor, Boolean> extractorData = Pair<Extractor, Boolean> extractorData =
extractorFactory.createExtractor( extractorFactory.createExtractor(
previousExtractor, previousExtractor,
...@@ -279,22 +279,26 @@ import java.util.concurrent.atomic.AtomicInteger; ...@@ -279,22 +279,26 @@ import java.util.concurrent.atomic.AtomicInteger;
muxedCaptionFormats, muxedCaptionFormats,
drmInitData, drmInitData,
timestampAdjuster, timestampAdjuster,
dataSource.getResponseHeaders()); dataSource.getResponseHeaders(),
extractorInput);
extractor = extractorData.first; extractor = extractorData.first;
isPackedAudioExtractor = extractorData.second;
boolean reusingExtractor = extractor == previousExtractor; boolean reusingExtractor = extractor == previousExtractor;
initLoadCompleted = reusingExtractor && initDataSpec != null; boolean isPackedAudioExtractor = extractorData.second;
if (isPackedAudioExtractor && id3Data == null) { if (isPackedAudioExtractor) {
id3Decoder = new Id3Decoder(); output.setSampleOffsetUs(
id3Data = new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH); id3Timestamp != C.TIME_UNSET
? timestampAdjuster.adjustTsTimestamp(id3Timestamp)
: startTimeUs);
} }
initLoadCompleted = reusingExtractor && initDataSpec != null;
output.init(uid, shouldSpliceIn, reusingExtractor); output.init(uid, shouldSpliceIn, reusingExtractor);
if (!reusingExtractor) { if (!reusingExtractor) {
extractor.init(output); extractor.init(output);
} }
} }
return new DefaultExtractorInput(dataSource, dataSpec.absoluteStreamPosition, bytesToRead); return extractorInput;
} }
/** /**
...@@ -309,7 +313,8 @@ import java.util.concurrent.atomic.AtomicInteger; ...@@ -309,7 +313,8 @@ import java.util.concurrent.atomic.AtomicInteger;
*/ */
private long peekId3PrivTimestamp(ExtractorInput input) throws IOException, InterruptedException { private long peekId3PrivTimestamp(ExtractorInput input) throws IOException, InterruptedException {
input.resetPeekPosition(); input.resetPeekPosition();
if (!input.peekFully(id3Data.data, 0, Id3Decoder.ID3_HEADER_LENGTH, true)) { if (input.getLength() < Id3Decoder.ID3_HEADER_LENGTH
|| !input.peekFully(id3Data.data, 0, Id3Decoder.ID3_HEADER_LENGTH, true)) {
return C.TIME_UNSET; return C.TIME_UNSET;
} }
id3Data.reset(Id3Decoder.ID3_HEADER_LENGTH); id3Data.reset(Id3Decoder.ID3_HEADER_LENGTH);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment