Add support for elementary AAC/ADTS streams.

- This change:

1. Extracts HlsExtractor interface from TsExtractor.
2. Adds AdtsExtractor for AAC/ADTS streams, which turned out to be
   really easy.

Selection of the ADTS extractor relies on seeing the .aac extension.
This is at least guaranteed not to break anything that works already
(since no-one is going to be using .aac as the extension for something
that's not elementary AAC/ADTS).

Issue: #209
This commit is contained in:
Oliver Woodman 2015-02-17 15:41:59 +00:00
parent b46d1fc7cc
commit a1e196fe20
11 changed files with 329 additions and 119 deletions

View File

@ -117,9 +117,12 @@ import java.util.Locale;
new Sample("Apple master playlist advanced",
"https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_16x9/"
+ "bipbop_16x9_variant.m3u8", DemoUtil.TYPE_HLS),
new Sample("Apple single media playlist",
new Sample("Apple TS media playlist",
"https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear1/"
+ "prog_index.m3u8", DemoUtil.TYPE_HLS),
new Sample("Apple AAC media playlist",
"https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear0/"
+ "prog_index.m3u8", DemoUtil.TYPE_HLS),
};
public static final Sample[] MISC = new Sample[] {

View File

@ -17,6 +17,8 @@ package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.hls.parser.AdtsExtractor;
import com.google.android.exoplayer.hls.parser.HlsExtractor;
import com.google.android.exoplayer.hls.parser.TsExtractor;
import com.google.android.exoplayer.upstream.Aes128DataSource;
import com.google.android.exoplayer.upstream.BandwidthMeter;
@ -105,6 +107,7 @@ public class HlsChunkSource {
public static final long DEFAULT_MAX_BUFFER_TO_SWITCH_DOWN_MS = 20000;
private static final String TAG = "HlsChunkSource";
private static final String AAC_FILE_EXTENSION = ".aac";
private static final float BANDWIDTH_FRACTION = 0.8f;
private final BufferPool bufferPool;
@ -332,9 +335,11 @@ public class HlsChunkSource {
boolean isLastChunk = !mediaPlaylist.live && chunkIndex == mediaPlaylist.segments.size() - 1;
// Configure the extractor that will read the chunk.
TsExtractor extractor;
HlsExtractor extractor;
if (previousTsChunk == null || segment.discontinuity || switchingVariant || liveDiscontinuity) {
extractor = new TsExtractor(startTimeUs, switchingVariantSpliced, bufferPool);
extractor = chunkUri.getLastPathSegment().endsWith(AAC_FILE_EXTENSION)
? new AdtsExtractor(switchingVariantSpliced, startTimeUs, bufferPool)
: new TsExtractor(switchingVariantSpliced, startTimeUs, bufferPool);
} else {
extractor = previousTsChunk.extractor;
}

View File

@ -21,7 +21,7 @@ import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackInfo;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.hls.parser.TsExtractor;
import com.google.android.exoplayer.hls.parser.HlsExtractor;
import com.google.android.exoplayer.upstream.Loader;
import com.google.android.exoplayer.upstream.Loader.Loadable;
import com.google.android.exoplayer.util.Assertions;
@ -44,7 +44,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
private static final int NO_RESET_PENDING = -1;
private final HlsChunkSource chunkSource;
private final LinkedList<TsExtractor> extractors;
private final LinkedList<HlsExtractor> extractors;
private final boolean frameAccurateSeeking;
private final int minLoadableRetryCount;
@ -83,7 +83,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
this.frameAccurateSeeking = frameAccurateSeeking;
this.remainingReleaseCount = downstreamRendererCount;
this.minLoadableRetryCount = minLoadableRetryCount;
extractors = new LinkedList<TsExtractor>();
extractors = new LinkedList<HlsExtractor>();
}
@Override
@ -96,7 +96,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
}
continueBufferingInternal();
if (!extractors.isEmpty()) {
TsExtractor extractor = extractors.getFirst();
HlsExtractor extractor = extractors.getFirst();
if (extractor.isPrepared()) {
trackCount = extractor.getTrackCount();
trackEnabledStates = new boolean[trackCount];
@ -195,7 +195,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
return NOTHING_READ;
}
TsExtractor extractor = getCurrentExtractor();
HlsExtractor extractor = getCurrentExtractor();
if (extractors.size() > 1) {
// If there's more than one extractor, attempt to configure a seamless splice from the
// current one to the next one.
@ -328,8 +328,8 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
*
* @return The current extractor from which samples should be read. Guaranteed to be non-null.
*/
private TsExtractor getCurrentExtractor() {
TsExtractor extractor = extractors.getFirst();
private HlsExtractor getCurrentExtractor() {
HlsExtractor extractor = extractors.getFirst();
while (extractors.size() > 1 && !haveSamplesForEnabledTracks(extractor)) {
// We're finished reading from the extractor for all tracks, and so can discard it.
extractors.removeFirst().release();
@ -338,7 +338,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
return extractor;
}
private void discardSamplesForDisabledTracks(TsExtractor extractor, long timeUs) {
private void discardSamplesForDisabledTracks(HlsExtractor extractor, long timeUs) {
if (!extractor.isPrepared()) {
return;
}
@ -349,7 +349,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
}
}
private boolean haveSamplesForEnabledTracks(TsExtractor extractor) {
private boolean haveSamplesForEnabledTracks(HlsExtractor extractor) {
if (!extractor.isPrepared()) {
return false;
}

View File

@ -15,7 +15,7 @@
*/
package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.hls.parser.TsExtractor;
import com.google.android.exoplayer.hls.parser.HlsExtractor;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
@ -51,7 +51,7 @@ public final class TsChunk extends HlsChunk {
/**
* The extractor into which this chunk is being consumed.
*/
public final TsExtractor extractor;
public final HlsExtractor extractor;
private int loadPosition;
private volatile boolean loadFinished;
@ -60,16 +60,17 @@ public final class TsChunk extends HlsChunk {
/**
* @param dataSource A {@link DataSource} for loading the data.
* @param dataSpec Defines the data to be loaded.
* @param extractor An extractor to parse samples from the data.
* @param variantIndex The index of the variant in the master playlist.
* @param startTimeUs The start time of the media contained by the chunk, in microseconds.
* @param endTimeUs The end time of the media contained by the chunk, in microseconds.
* @param chunkIndex The index of the chunk.
* @param isLastChunk True if this is the last chunk in the media. False otherwise.
*/
public TsChunk(DataSource dataSource, DataSpec dataSpec, TsExtractor tsExtractor,
public TsChunk(DataSource dataSource, DataSpec dataSpec, HlsExtractor extractor,
int variantIndex, long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk) {
super(dataSource, dataSpec);
this.extractor = tsExtractor;
this.extractor = extractor;
this.variantIndex = variantIndex;
this.startTimeUs = startTimeUs;
this.endTimeUs = endTimeUs;

View File

@ -0,0 +1,126 @@
/*
* Copyright (C) 2014 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
*
* http://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 com.google.android.exoplayer.hls.parser;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.ParsableByteArray;
import java.io.IOException;
/**
* Facilitates the extraction of AAC samples from elementary audio files formatted as AAC with ADTS
* headers.
*/
public class AdtsExtractor extends HlsExtractor {
private static final int MAX_PACKET_SIZE = 200;
private final long firstSampleTimestamp;
private final ParsableByteArray packetBuffer;
private final AdtsReader adtsReader;
// Accessed only by the loading thread.
private boolean firstPacket;
// Accessed by both the loading and consuming threads.
private volatile boolean prepared;
public AdtsExtractor(boolean shouldSpliceIn, long firstSampleTimestamp, BufferPool bufferPool) {
super(shouldSpliceIn);
this.firstSampleTimestamp = firstSampleTimestamp;
packetBuffer = new ParsableByteArray(MAX_PACKET_SIZE);
adtsReader = new AdtsReader(bufferPool);
firstPacket = true;
}
@Override
public int getTrackCount() {
Assertions.checkState(prepared);
return 1;
}
@Override
public MediaFormat getFormat(int track) {
Assertions.checkState(prepared);
return adtsReader.getMediaFormat();
}
@Override
public boolean isPrepared() {
return prepared;
}
@Override
public void release() {
adtsReader.release();
}
@Override
public long getLargestSampleTimestamp() {
return adtsReader.getLargestParsedTimestampUs();
}
@Override
public boolean getSample(int track, SampleHolder holder) {
Assertions.checkState(prepared);
Assertions.checkState(track == 0);
return adtsReader.getSample(holder);
}
@Override
public void discardUntil(int track, long timeUs) {
Assertions.checkState(prepared);
Assertions.checkState(track == 0);
adtsReader.discardUntil(timeUs);
}
@Override
public boolean hasSamples(int track) {
Assertions.checkState(prepared);
Assertions.checkState(track == 0);
return !adtsReader.isEmpty();
}
@Override
public int read(DataSource dataSource) throws IOException {
int bytesRead = dataSource.read(packetBuffer.data, 0, MAX_PACKET_SIZE);
if (bytesRead == -1) {
return -1;
}
packetBuffer.setPosition(0);
packetBuffer.setLimit(bytesRead);
// TODO: Make it possible for adtsReader to consume the dataSource directly, so that it becomes
// unnecessary to copy the data through packetBuffer.
adtsReader.consume(packetBuffer, firstSampleTimestamp, firstPacket);
firstPacket = false;
if (!prepared) {
prepared = adtsReader.hasMediaFormat();
}
return bytesRead;
}
@Override
protected SampleQueue getSampleQueue(int track) {
Assertions.checkState(track == 0);
return adtsReader;
}
}

View File

@ -30,7 +30,7 @@ import java.util.Collections;
/**
* Parses a continuous ADTS byte stream and extracts individual frames.
*/
/* package */ class AdtsReader extends PesPayloadReader {
/* package */ class AdtsReader extends ElementaryStreamReader {
private static final int STATE_FINDING_SYNC = 0;
private static final int STATE_READING_HEADER = 1;

View File

@ -19,11 +19,11 @@ import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.util.ParsableByteArray;
/**
* Extracts individual samples from continuous byte stream, preserving original order.
* Extracts individual samples from an elementary media stream, preserving original order.
*/
/* package */ abstract class PesPayloadReader extends SampleQueue {
/* package */ abstract class ElementaryStreamReader extends SampleQueue {
protected PesPayloadReader(BufferPool bufferPool) {
protected ElementaryStreamReader(BufferPool bufferPool) {
super(bufferPool);
}

View File

@ -30,7 +30,7 @@ import java.util.List;
/**
* Parses a continuous H264 byte stream and extracts individual frames.
*/
/* package */ class H264Reader extends PesPayloadReader {
/* package */ class H264Reader extends ElementaryStreamReader {
private static final int NAL_UNIT_TYPE_IDR = 5;
private static final int NAL_UNIT_TYPE_SEI = 6;

View File

@ -0,0 +1,151 @@
/*
* Copyright (C) 2014 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
*
* http://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 com.google.android.exoplayer.hls.parser;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.upstream.DataSource;
import java.io.IOException;
/**
* Facilitates extraction of media samples for HLS playbacks.
*/
// TODO: Consider consolidating more common logic in this base class.
public abstract class HlsExtractor {
private final boolean shouldSpliceIn;
// Accessed only by the consuming thread.
private boolean spliceConfigured;
public HlsExtractor(boolean shouldSpliceIn) {
this.shouldSpliceIn = shouldSpliceIn;
}
/**
* Attempts to configure a splice from this extractor to the next.
* <p>
* The splice is performed such that for each track the samples read from the next extractor
* start with a keyframe, and continue from where the samples read from this extractor finish.
* A successful splice may discard samples from either or both extractors.
* <p>
* Splice configuration may fail if the next extractor is not yet in a state that allows the
* splice to be performed. Calling this method is a noop if the splice has already been
* configured. Hence this method should be called repeatedly during the window within which a
* splice can be performed.
*
* @param nextExtractor The extractor being spliced to.
*/
public final void configureSpliceTo(HlsExtractor nextExtractor) {
if (spliceConfigured || !nextExtractor.shouldSpliceIn || !nextExtractor.isPrepared()) {
// The splice is already configured, or the next extractor doesn't want to be spliced in, or
// the next extractor isn't ready to be spliced in.
return;
}
boolean spliceConfigured = true;
int trackCount = getTrackCount();
for (int i = 0; i < trackCount; i++) {
spliceConfigured &= getSampleQueue(i).configureSpliceTo(nextExtractor.getSampleQueue(i));
}
this.spliceConfigured = spliceConfigured;
return;
}
/**
* Gets the number of available tracks.
* <p>
* This method should only be called after the extractor has been prepared.
*
* @return The number of available tracks.
*/
public abstract int getTrackCount();
/**
* Gets the format of the specified track.
* <p>
* This method must only be called after the extractor has been prepared.
*
* @param track The track index.
* @return The corresponding format.
*/
public abstract MediaFormat getFormat(int track);
/**
* Whether the extractor is prepared.
*
* @return True if the extractor is prepared. False otherwise.
*/
public abstract boolean isPrepared();
/**
* Releases the extractor, recycling any pending or incomplete samples to the sample pool.
* <p>
* This method should not be called whilst {@link #read(DataSource)} is also being invoked.
*/
public abstract void release();
/**
* Gets the largest timestamp of any sample parsed by the extractor.
*
* @return The largest timestamp, or {@link Long#MIN_VALUE} if no samples have been parsed.
*/
public abstract long getLargestSampleTimestamp();
/**
* Gets the next sample for the specified track.
*
* @param track The track from which to read.
* @param holder A {@link SampleHolder} into which the sample should be read.
* @return True if a sample was read. False otherwise.
*/
public abstract boolean getSample(int track, SampleHolder holder);
/**
* Discards samples for the specified track up to the specified time.
*
* @param track The track from which samples should be discarded.
* @param timeUs The time up to which samples should be discarded, in microseconds.
*/
public abstract void discardUntil(int track, long timeUs);
/**
* Whether samples are available for reading from {@link #getSample(int, SampleHolder)} for the
* specified track.
*
* @return True if samples are available for reading from {@link #getSample(int, SampleHolder)}
* for the specified track. False otherwise.
*/
public abstract boolean hasSamples(int track);
/**
* Reads up to a single TS packet.
*
* @param dataSource The {@link DataSource} from which to read.
* @throws IOException If an error occurred reading from the source.
* @return The number of bytes read from the source.
*/
public abstract int read(DataSource dataSource) throws IOException;
/**
* Gets the {@link SampleQueue} for the specified track.
*
* @param track The track index.
* @return The corresponding sample queue.
*/
protected abstract SampleQueue getSampleQueue(int track);
}

View File

@ -22,7 +22,7 @@ import com.google.android.exoplayer.util.ParsableByteArray;
/**
* Parses ID3 data and extracts individual text information frames.
*/
/* package */ class Id3Reader extends PesPayloadReader {
/* package */ class Id3Reader extends ElementaryStreamReader {
public Id3Reader(BufferPool bufferPool) {
super(bufferPool);

View File

@ -32,7 +32,7 @@ import java.io.IOException;
/**
* Facilitates the extraction of data from the MPEG-2 TS container format.
*/
public final class TsExtractor {
public final class TsExtractor extends HlsExtractor {
private static final String TAG = "TsExtractor";
@ -51,13 +51,9 @@ public final class TsExtractor {
private final SparseArray<SampleQueue> sampleQueues; // Indexed by streamType
private final SparseArray<TsPayloadReader> tsPayloadReaders; // Indexed by pid
private final BufferPool bufferPool;
private final boolean shouldSpliceIn;
private final long firstSampleTimestamp;
private final ParsableBitArray tsScratch;
// Accessed only by the consuming thread.
private boolean spliceConfigured;
// Accessed only by the loading thread.
private int tsPacketBytesRead;
private long timestampOffsetUs;
@ -66,9 +62,9 @@ public final class TsExtractor {
// Accessed by both the loading and consuming threads.
private volatile boolean prepared;
public TsExtractor(long firstSampleTimestamp, boolean shouldSpliceIn, BufferPool bufferPool) {
public TsExtractor(boolean shouldSpliceIn, long firstSampleTimestamp, BufferPool bufferPool) {
super(shouldSpliceIn);
this.firstSampleTimestamp = firstSampleTimestamp;
this.shouldSpliceIn = shouldSpliceIn;
this.bufferPool = bufferPool;
tsScratch = new ParsableBitArray(new byte[3]);
tsPacketBuffer = new ParsableByteArray(TS_PACKET_SIZE);
@ -78,86 +74,31 @@ public final class TsExtractor {
lastPts = Long.MIN_VALUE;
}
/**
* Gets the number of available tracks.
* <p>
* This method should only be called after the extractor has been prepared.
*
* @return The number of available tracks.
*/
@Override
public int getTrackCount() {
Assertions.checkState(prepared);
return sampleQueues.size();
}
/**
* Gets the format of the specified track.
* <p>
* This method must only be called after the extractor has been prepared.
*
* @param track The track index.
* @return The corresponding format.
*/
@Override
public MediaFormat getFormat(int track) {
Assertions.checkState(prepared);
return sampleQueues.valueAt(track).getMediaFormat();
}
/**
* Whether the extractor is prepared.
*
* @return True if the extractor is prepared. False otherwise.
*/
@Override
public boolean isPrepared() {
return prepared;
}
/**
* Releases the extractor, recycling any pending or incomplete samples to the sample pool.
* <p>
* This method should not be called whilst {@link #read(DataSource)} is also being invoked.
*/
@Override
public void release() {
for (int i = 0; i < sampleQueues.size(); i++) {
sampleQueues.valueAt(i).release();
}
}
/**
* Attempts to configure a splice from this extractor to the next.
* <p>
* The splice is performed such that for each track the samples read from the next extractor
* start with a keyframe, and continue from where the samples read from this extractor finish.
* A successful splice may discard samples from either or both extractors.
* <p>
* Splice configuration may fail if the next extractor is not yet in a state that allows the
* splice to be performed. Calling this method is a noop if the splice has already been
* configured. Hence this method should be called repeatedly during the window within which a
* splice can be performed.
*
* @param nextExtractor The extractor being spliced to.
*/
public void configureSpliceTo(TsExtractor nextExtractor) {
Assertions.checkState(prepared);
if (spliceConfigured || !nextExtractor.shouldSpliceIn || !nextExtractor.isPrepared()) {
// The splice is already configured, or the next extractor doesn't want to be spliced in, or
// the next extractor isn't ready to be spliced in.
return;
}
boolean spliceConfigured = true;
for (int i = 0; i < sampleQueues.size(); i++) {
spliceConfigured &= sampleQueues.valueAt(i).configureSpliceTo(
nextExtractor.sampleQueues.valueAt(i));
}
this.spliceConfigured = spliceConfigured;
return;
}
/**
* Gets the largest timestamp of any sample parsed by the extractor.
*
* @return The largest timestamp, or {@link Long#MIN_VALUE} if no samples have been parsed.
*/
@Override
public long getLargestSampleTimestamp() {
long largestParsedTimestampUs = Long.MIN_VALUE;
for (int i = 0; i < sampleQueues.size(); i++) {
@ -167,36 +108,19 @@ public final class TsExtractor {
return largestParsedTimestampUs;
}
/**
* Gets the next sample for the specified track.
*
* @param track The track from which to read.
* @param holder A {@link SampleHolder} into which the sample should be read.
* @return True if a sample was read. False otherwise.
*/
@Override
public boolean getSample(int track, SampleHolder holder) {
Assertions.checkState(prepared);
return sampleQueues.valueAt(track).getSample(holder);
}
/**
* Discards samples for the specified track up to the specified time.
*
* @param track The track from which samples should be discarded.
* @param timeUs The time up to which samples should be discarded, in microseconds.
*/
@Override
public void discardUntil(int track, long timeUs) {
Assertions.checkState(prepared);
sampleQueues.valueAt(track).discardUntil(timeUs);
}
/**
* Whether samples are available for reading from {@link #getSample(int, SampleHolder)} for the
* specified track.
*
* @return True if samples are available for reading from {@link #getSample(int, SampleHolder)}
* for the specified track. False otherwise.
*/
@Override
public boolean hasSamples(int track) {
Assertions.checkState(prepared);
return !sampleQueues.valueAt(track).isEmpty();
@ -215,13 +139,7 @@ public final class TsExtractor {
return true;
}
/**
* Reads up to a single TS packet.
*
* @param dataSource The {@link DataSource} from which to read.
* @throws IOException If an error occurred reading from the source.
* @return The number of bytes read from the source.
*/
@Override
public int read(DataSource dataSource) throws IOException {
int bytesRead = dataSource.read(tsPacketBuffer.data, tsPacketBytesRead,
TS_PACKET_SIZE - tsPacketBytesRead);
@ -276,6 +194,12 @@ public final class TsExtractor {
return bytesRead;
}
@Override
protected SampleQueue getSampleQueue(int track) {
Assertions.checkState(track == 0);
return sampleQueues.valueAt(track);
}
/**
* Adjusts a PTS value to the corresponding time in microseconds, accounting for PTS wraparound.
*
@ -404,7 +328,7 @@ public final class TsExtractor {
continue;
}
PesPayloadReader pesPayloadReader = null;
ElementaryStreamReader pesPayloadReader = null;
switch (streamType) {
case TS_STREAM_TYPE_AAC:
pesPayloadReader = new AdtsReader(bufferPool);
@ -444,7 +368,7 @@ public final class TsExtractor {
private static final int MAX_HEADER_EXTENSION_SIZE = 5;
private final ParsableBitArray pesScratch;
private final PesPayloadReader pesPayloadReader;
private final ElementaryStreamReader pesPayloadReader;
private int state;
private int bytesRead;
@ -457,7 +381,7 @@ public final class TsExtractor {
private long timeUs;
public PesReader(PesPayloadReader pesPayloadReader) {
public PesReader(ElementaryStreamReader pesPayloadReader) {
this.pesPayloadReader = pesPayloadReader;
pesScratch = new ParsableBitArray(new byte[HEADER_SIZE]);
state = STATE_FINDING_HEADER;