diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java index 151d392ccb..cd46f11052 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java @@ -19,6 +19,7 @@ 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.HlsExtractorWrapper; import com.google.android.exoplayer.hls.parser.TsExtractor; import com.google.android.exoplayer.upstream.Aes128DataSource; import com.google.android.exoplayer.upstream.BandwidthMeter; @@ -341,16 +342,17 @@ public class HlsChunkSource { boolean isLastChunk = !mediaPlaylist.live && chunkIndex == mediaPlaylist.segments.size() - 1; // Configure the extractor that will read the chunk. - HlsExtractor extractor; + HlsExtractorWrapper extractorWrapper; if (previousTsChunk == null || segment.discontinuity || switchingVariant || liveDiscontinuity) { - extractor = chunkUri.getLastPathSegment().endsWith(AAC_FILE_EXTENSION) - ? new AdtsExtractor(switchingVariantSpliced, startTimeUs, bufferPool) - : new TsExtractor(switchingVariantSpliced, startTimeUs, bufferPool); + HlsExtractor extractor = chunkUri.getLastPathSegment().endsWith(AAC_FILE_EXTENSION) + ? new AdtsExtractor(startTimeUs) + : new TsExtractor(startTimeUs); + extractorWrapper = new HlsExtractorWrapper(bufferPool, extractor, switchingVariantSpliced); } else { - extractor = previousTsChunk.extractor; + extractorWrapper = previousTsChunk.extractor; } - return new TsChunk(dataSource, dataSpec, extractor, enabledVariants[variantIndex].index, + return new TsChunk(dataSource, dataSpec, extractorWrapper, enabledVariants[variantIndex].index, startTimeUs, endTimeUs, chunkMediaSequence, isLastChunk); } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java index 9db6c4ccc0..85fb2be5cc 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java @@ -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.HlsExtractor; +import com.google.android.exoplayer.hls.parser.HlsExtractorWrapper; 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 extractors; + private final LinkedList 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(); + extractors = new LinkedList(); } @Override @@ -96,7 +96,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { } continueBufferingInternal(); if (!extractors.isEmpty()) { - HlsExtractor extractor = extractors.getFirst(); + HlsExtractorWrapper 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; } - HlsExtractor extractor = getCurrentExtractor(); + HlsExtractorWrapper 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 HlsExtractor getCurrentExtractor() { - HlsExtractor extractor = extractors.getFirst(); + private HlsExtractorWrapper getCurrentExtractor() { + HlsExtractorWrapper 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(HlsExtractor extractor, long timeUs) { + private void discardSamplesForDisabledTracks(HlsExtractorWrapper extractor, long timeUs) { if (!extractor.isPrepared()) { return; } @@ -349,7 +349,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { } } - private boolean haveSamplesForEnabledTracks(HlsExtractor extractor) { + private boolean haveSamplesForEnabledTracks(HlsExtractorWrapper extractor) { if (!extractor.isPrepared()) { return false; } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java b/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java index a66330bb5c..863808d4ef 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java @@ -15,7 +15,7 @@ */ package com.google.android.exoplayer.hls; -import com.google.android.exoplayer.hls.parser.HlsExtractor; +import com.google.android.exoplayer.hls.parser.HlsExtractorWrapper; 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 HlsExtractor extractor; + public final HlsExtractorWrapper extractor; private int loadPosition; private volatile boolean loadFinished; @@ -67,7 +67,7 @@ public final class TsChunk extends HlsChunk { * @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, HlsExtractor extractor, + public TsChunk(DataSource dataSource, DataSpec dataSpec, HlsExtractorWrapper extractor, int variantIndex, long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk) { super(dataSource, dataSpec); this.extractor = extractor; diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsExtractor.java index af164a5f36..a22e17ce85 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsExtractor.java @@ -16,10 +16,7 @@ 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; @@ -28,73 +25,41 @@ 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 { +public class AdtsExtractor implements 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 AdtsReader adtsReader; 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); + public AdtsExtractor(long firstSampleTimestamp) { this.firstSampleTimestamp = firstSampleTimestamp; packetBuffer = new ParsableByteArray(MAX_PACKET_SIZE); - adtsReader = new AdtsReader(bufferPool); firstPacket = true; } + @Override + public void init(ExtractorOutput output) { + adtsReader = new AdtsReader(output.getTrackOutput(0)); + } + @Override public int getTrackCount() { - Assertions.checkState(prepared); return 1; } @Override public MediaFormat getFormat(int track) { - Assertions.checkState(prepared); - return adtsReader.getMediaFormat(); + return adtsReader.getFormat(); } @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(); + return adtsReader != null && adtsReader.hasFormat(); } @Override @@ -111,16 +76,7 @@ public class AdtsExtractor extends HlsExtractor { // 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; - } - } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsReader.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsReader.java index 0aa7b7c07b..81c643c7a8 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsReader.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsReader.java @@ -17,7 +17,7 @@ package com.google.android.exoplayer.hls.parser; import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; -import com.google.android.exoplayer.upstream.BufferPool; +import com.google.android.exoplayer.hls.parser.HlsExtractor.TrackOutput; import com.google.android.exoplayer.util.CodecSpecificDataUtil; import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.ParsableBitArray; @@ -55,8 +55,8 @@ import java.util.Collections; // Used when reading the samples. private long timeUs; - public AdtsReader(BufferPool bufferPool) { - super(bufferPool); + public AdtsReader(TrackOutput output) { + super(output); adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]); state = STATE_FINDING_SYNC; } @@ -78,17 +78,17 @@ import java.util.Collections; int targetLength = hasCrc ? HEADER_SIZE + CRC_SIZE : HEADER_SIZE; if (continueRead(data, adtsScratch.getData(), targetLength)) { parseHeader(); - startSample(timeUs); + output.startSample(timeUs, 0); bytesRead = 0; state = STATE_READING_SAMPLE; } break; case STATE_READING_SAMPLE: int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead); - appendData(data, bytesToRead); + output.appendData(data, bytesToRead); bytesRead += bytesToRead; if (bytesRead == sampleSize) { - commitSample(C.SAMPLE_FLAG_SYNC); + output.commitSample(C.SAMPLE_FLAG_SYNC, 0, null); timeUs += frameDurationUs; bytesRead = 0; state = STATE_FINDING_SYNC; @@ -152,7 +152,7 @@ import java.util.Collections; private void parseHeader() { adtsScratch.setPosition(0); - if (!hasMediaFormat()) { + if (!hasFormat()) { int audioObjectType = adtsScratch.readBits(2) + 1; int sampleRateIndex = adtsScratch.readBits(4); adtsScratch.skipBits(1); @@ -167,7 +167,7 @@ import java.util.Collections; MediaFormat.NO_VALUE, audioParams.second, audioParams.first, Collections.singletonList(audioSpecificConfig)); frameDurationUs = (C.MICROS_PER_SECOND * 1024L) / mediaFormat.sampleRate; - setMediaFormat(mediaFormat); + setFormat(mediaFormat); } else { adtsScratch.skipBits(10); } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/ElementaryStreamReader.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/ElementaryStreamReader.java index a8c5c7b562..8522cbeb18 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/parser/ElementaryStreamReader.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/ElementaryStreamReader.java @@ -15,16 +15,46 @@ */ package com.google.android.exoplayer.hls.parser; -import com.google.android.exoplayer.upstream.BufferPool; +import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.hls.parser.HlsExtractor.TrackOutput; import com.google.android.exoplayer.util.ParsableByteArray; /** * Extracts individual samples from an elementary media stream, preserving original order. */ -/* package */ abstract class ElementaryStreamReader extends SampleQueue { +/* package */ abstract class ElementaryStreamReader { - protected ElementaryStreamReader(BufferPool bufferPool) { - super(bufferPool); + protected final TrackOutput output; + private MediaFormat format; + + /** + * @param output A {@link TrackOutput} to which samples should be written. + */ + protected ElementaryStreamReader(TrackOutput output) { + this.output = output; + } + + /** + * True if the format of the stream is known. False otherwise. + */ + public boolean hasFormat() { + return format != null; + } + + /** + * Returns the format of the stream, or {@code null} if {@link #hasFormat()} is false. + */ + public MediaFormat getFormat() { + return format; + } + + /** + * Sets the format of the stream. + * + * @param format The format. + */ + protected void setFormat(MediaFormat format) { + this.format = format; } /** diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/H264Reader.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/H264Reader.java index 96bc76b24f..ae9d0fe7a9 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/parser/H264Reader.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/H264Reader.java @@ -17,8 +17,8 @@ package com.google.android.exoplayer.hls.parser; import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.hls.parser.HlsExtractor.TrackOutput; import com.google.android.exoplayer.mp4.Mp4Util; -import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.ParsableBitArray; @@ -44,18 +44,20 @@ import java.util.List; private final NalUnitTargetBuffer sps; private final NalUnitTargetBuffer pps; private final NalUnitTargetBuffer sei; + private final ParsableByteArray seiWrapper; private int scratchEscapeCount; private int[] scratchEscapePositions; private boolean isKeyframe; - public H264Reader(BufferPool bufferPool, SeiReader seiReader) { - super(bufferPool); + public H264Reader(TrackOutput output, SeiReader seiReader) { + super(output); this.seiReader = seiReader; prefixFlags = new boolean[3]; sps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SPS, 128); pps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_PPS, 128); sei = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SEI, 128); + seiWrapper = new ParsableByteArray(); scratchEscapePositions = new int[10]; } @@ -67,7 +69,7 @@ import java.util.List; byte[] dataArray = data.data; // Append the data to the buffer. - appendData(data, data.bytesLeft()); + output.appendData(data, data.bytesLeft()); // Scan the appended data, processing NAL units as they are encountered while (offset < limit) { @@ -85,13 +87,13 @@ import java.util.List; int nalUnitType = Mp4Util.getNalUnitType(dataArray, nextNalUnitOffset); int nalUnitOffsetInData = nextNalUnitOffset - limit; if (nalUnitType == NAL_UNIT_TYPE_AUD) { - if (writingSample()) { - if (isKeyframe && !hasMediaFormat() && sps.isCompleted() && pps.isCompleted()) { + if (output.isWritingSample()) { + if (isKeyframe && !hasFormat() && sps.isCompleted() && pps.isCompleted()) { parseMediaFormat(sps, pps); } - commitSample(isKeyframe ? C.SAMPLE_FLAG_SYNC : 0, nalUnitOffsetInData); + output.commitSample(isKeyframe ? C.SAMPLE_FLAG_SYNC : 0, nalUnitOffsetInData, null); } - startSample(pesTimeUs, nalUnitOffsetInData); + output.startSample(pesTimeUs, nalUnitOffsetInData); isKeyframe = false; } else if (nalUnitType == NAL_UNIT_TYPE_IDR) { isKeyframe = true; @@ -118,7 +120,7 @@ import java.util.List; } private void feedNalUnitTargetBuffersStart(int nalUnitType) { - if (!hasMediaFormat()) { + if (!hasFormat()) { sps.startNalUnit(nalUnitType); pps.startNalUnit(nalUnitType); } @@ -126,7 +128,7 @@ import java.util.List; } private void feedNalUnitTargetBuffersData(byte[] dataArray, int offset, int limit) { - if (!hasMediaFormat()) { + if (!hasFormat()) { sps.appendToNalUnit(dataArray, offset, limit); pps.appendToNalUnit(dataArray, offset, limit); } @@ -138,7 +140,8 @@ import java.util.List; pps.endNalUnit(discardPadding); if (sei.endNalUnit(discardPadding)) { int unescapedLength = unescapeStream(sei.nalData, sei.nalLength); - seiReader.read(sei.nalData, 0, unescapedLength, pesTimeUs); + seiWrapper.reset(sei.nalData, unescapedLength); + seiReader.consume(seiWrapper, pesTimeUs, true); } } @@ -230,7 +233,7 @@ import java.util.List; } // Set the format. - setMediaFormat(MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE, + setFormat(MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE, frameWidth, frameHeight, initializationData)); } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/HlsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/HlsExtractor.java index 88aef4a0d6..92a4ac6717 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/parser/HlsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/HlsExtractor.java @@ -16,55 +16,62 @@ 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 com.google.android.exoplayer.util.ParsableByteArray; 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 { +public interface HlsExtractor { - private final boolean shouldSpliceIn; + /** + * An object to which extracted data should be output. + */ + public interface ExtractorOutput { - // Accessed only by the consuming thread. - private boolean spliceConfigured; + /** + * Obtains a {@link TrackOutput} to which extracted data should be output for a given track. + * + * @param trackId A stable track id. + * @return The corresponding {@link TrackOutput}. + */ + TrackOutput getTrackOutput(int trackId); - public HlsExtractor(boolean shouldSpliceIn) { - this.shouldSpliceIn = shouldSpliceIn; } /** - * Attempts to configure a splice from this extractor to the next. - *

- * 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. - *

- * 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. + * An object to which extracted data belonging to a given track should be output. */ - 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; + public interface TrackOutput { + + int appendData(DataSource dataSource, int length) throws IOException; + + void appendData(ParsableByteArray data, int length); + + void startSample(long timeUs, int offset); + + void commitSample(int flags, int offset, byte[] encryptionKey); + + boolean isWritingSample(); + } + /** + * Initializes the extractor. + * + * @param output An {@link ExtractorOutput} to which extracted data should be output. + */ + void init(ExtractorOutput output); + + /** + * Whether the extractor is prepared. + * + * @return True if the extractor is prepared. False otherwise. + */ + boolean isPrepared(); + /** * Gets the number of available tracks. *

@@ -72,7 +79,7 @@ public abstract class HlsExtractor { * * @return The number of available tracks. */ - public abstract int getTrackCount(); + int getTrackCount(); /** * Gets the format of the specified track. @@ -82,54 +89,7 @@ public abstract class HlsExtractor { * @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. - *

- * 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); + MediaFormat getFormat(int track); /** * Reads up to a single TS packet. @@ -138,14 +98,6 @@ public abstract class HlsExtractor { * @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); + int read(DataSource dataSource) throws IOException; } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/HlsExtractorWrapper.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/HlsExtractorWrapper.java new file mode 100644 index 0000000000..b44626722f --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/HlsExtractorWrapper.java @@ -0,0 +1,198 @@ +/* + * 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.hls.parser.HlsExtractor.TrackOutput; +import com.google.android.exoplayer.upstream.BufferPool; +import com.google.android.exoplayer.upstream.DataSource; +import com.google.android.exoplayer.util.Assertions; + +import android.util.SparseArray; + +import java.io.IOException; + +/** + * Wraps a {@link HlsExtractor}, adding functionality to enable reading of the extracted samples. + */ +public final class HlsExtractorWrapper implements HlsExtractor.ExtractorOutput { + + private final BufferPool bufferPool; + private final HlsExtractor extractor; + private final boolean shouldSpliceIn; + + private SparseArray sampleQueues; + + // Accessed only by the consuming thread. + private boolean spliceConfigured; + + public HlsExtractorWrapper(BufferPool bufferPool, HlsExtractor extractor, + boolean shouldSpliceIn) { + this.bufferPool = bufferPool; + this.extractor = extractor; + this.shouldSpliceIn = shouldSpliceIn; + sampleQueues = new SparseArray(); + extractor.init(this); + } + + /** + * Attempts to configure a splice from this extractor to the next. + *

+ * 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. + *

+ * 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(HlsExtractorWrapper 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++) { + SampleQueue currentSampleQueue = sampleQueues.valueAt(i); + SampleQueue nextSampleQueue = nextExtractor.sampleQueues.valueAt(i); + spliceConfigured &= currentSampleQueue.configureSpliceTo(nextSampleQueue); + } + this.spliceConfigured = spliceConfigured; + return; + } + + /** + * Gets the number of available tracks. + *

+ * This method should only be called after the extractor has been prepared. + * + * @return The number of available tracks. + */ + public int getTrackCount() { + return extractor.getTrackCount(); + } + + /** + * Gets the format of the specified track. + *

+ * This method must only be called after the extractor has been prepared. + * + * @param track The track index. + * @return The corresponding format. + */ + public MediaFormat getFormat(int track) { + return extractor.getFormat(track); + } + + /** + * Whether the extractor is prepared. + * + * @return True if the extractor is prepared. False otherwise. + */ + public boolean isPrepared() { + return extractor.isPrepared(); + } + + /** + * Releases the extractor, recycling any pending or incomplete samples to the sample pool. + *

+ * This method should not be called whilst {@link #read(DataSource)} is also being invoked. + */ + public void release() { + for (int i = 0; i < sampleQueues.size(); i++) { + sampleQueues.valueAt(i).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 long getLargestSampleTimestamp() { + long largestParsedTimestampUs = Long.MIN_VALUE; + for (int i = 0; i < sampleQueues.size(); i++) { + largestParsedTimestampUs = Math.max(largestParsedTimestampUs, + sampleQueues.valueAt(i).getLargestParsedTimestampUs()); + } + 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. + */ + public boolean getSample(int track, SampleHolder holder) { + Assertions.checkState(isPrepared()); + 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. + */ + public void discardUntil(int track, long timeUs) { + Assertions.checkState(isPrepared()); + 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. + */ + public boolean hasSamples(int track) { + Assertions.checkState(isPrepared()); + return !sampleQueues.valueAt(track).isEmpty(); + } + + /** + * 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 int read(DataSource dataSource) throws IOException { + return extractor.read(dataSource); + } + + // ExtractorOutput implementation. + + @Override + public TrackOutput getTrackOutput(int id) { + SampleQueue sampleQueue = sampleQueues.get(id); + if (sampleQueue == null) { + sampleQueue = new SampleQueue(bufferPool); + sampleQueues.put(id, sampleQueue); + } + return sampleQueue; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/Id3Reader.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/Id3Reader.java index a9d51d1515..a76bcdbaa4 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/parser/Id3Reader.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/Id3Reader.java @@ -17,7 +17,7 @@ package com.google.android.exoplayer.hls.parser; import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; -import com.google.android.exoplayer.upstream.BufferPool; +import com.google.android.exoplayer.hls.parser.HlsExtractor.TrackOutput; import com.google.android.exoplayer.util.ParsableByteArray; /** @@ -25,24 +25,24 @@ import com.google.android.exoplayer.util.ParsableByteArray; */ /* package */ class Id3Reader extends ElementaryStreamReader { - public Id3Reader(BufferPool bufferPool) { - super(bufferPool); - setMediaFormat(MediaFormat.createId3Format()); + public Id3Reader(TrackOutput output) { + super(output); + setFormat(MediaFormat.createId3Format()); } @Override public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { if (startOfPacket) { - startSample(pesTimeUs); + output.startSample(pesTimeUs, 0); } - if (writingSample()) { - appendData(data, data.bytesLeft()); + if (output.isWritingSample()) { + output.appendData(data, data.bytesLeft()); } } @Override public void packetFinished() { - commitSample(C.SAMPLE_FLAG_SYNC); + output.commitSample(C.SAMPLE_FLAG_SYNC, 0, null); } } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/SampleQueue.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/SampleQueue.java index bc2ce2a0a6..5e9bd7950f 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/parser/SampleQueue.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/SampleQueue.java @@ -16,8 +16,8 @@ package com.google.android.exoplayer.hls.parser; import com.google.android.exoplayer.C; -import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.SampleHolder; +import com.google.android.exoplayer.hls.parser.HlsExtractor.TrackOutput; import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.util.ParsableByteArray; @@ -29,7 +29,7 @@ import java.io.IOException; * the first sample returned from the queue is a keyframe, allowing splicing to another queue, and * so on. */ -/* package */ abstract class SampleQueue { +public final class SampleQueue implements TrackOutput { private final RollingSampleBuffer rollingBuffer; private final SampleHolder sampleInfoHolder; @@ -43,10 +43,9 @@ import java.io.IOException; private boolean writingSample; // Accessed by both the loading and consuming threads. - private volatile MediaFormat mediaFormat; private volatile long largestParsedTimestampUs; - protected SampleQueue(BufferPool bufferPool) { + public SampleQueue(BufferPool bufferPool) { rollingBuffer = new RollingSampleBuffer(bufferPool); sampleInfoHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DISABLED); needKeyframe = true; @@ -65,14 +64,6 @@ import java.io.IOException; return largestParsedTimestampUs; } - public boolean hasMediaFormat() { - return mediaFormat != null; - } - - public MediaFormat getMediaFormat() { - return mediaFormat; - } - public boolean isEmpty() { return !advanceToEligibleSample(); } @@ -169,45 +160,34 @@ import java.io.IOException; return true; } - // Called by the loading thread. + // TrackOutput implementation. Called by the loading thread. - protected boolean writingSample() { - return writingSample; + @Override + public int appendData(DataSource dataSource, int length) throws IOException { + return rollingBuffer.appendData(dataSource, length); } - protected void setMediaFormat(MediaFormat mediaFormat) { - this.mediaFormat = mediaFormat; + @Override + public void appendData(ParsableByteArray buffer, int length) { + rollingBuffer.appendData(buffer, length); } - protected void startSample(long sampleTimeUs) { - startSample(sampleTimeUs, 0); - } - - protected void startSample(long sampleTimeUs, int offset) { + @Override + public void startSample(long sampleTimeUs, int offset) { writingSample = true; largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sampleTimeUs); rollingBuffer.startSample(sampleTimeUs, offset); } - protected int appendData(DataSource dataSource, int length) throws IOException { - return rollingBuffer.appendData(dataSource, length); - } - - protected void appendData(ParsableByteArray buffer, int length) { - rollingBuffer.appendData(buffer, length); - } - - protected void commitSample(int flags) { - commitSample(flags, 0, null); - } - - protected void commitSample(int flags, int offset) { - commitSample(flags, offset, null); - } - - protected void commitSample(int flags, int offset, byte[] encryptionKey) { + @Override + public void commitSample(int flags, int offset, byte[] encryptionKey) { rollingBuffer.commitSample(flags, offset, encryptionKey); writingSample = false; } + @Override + public boolean isWritingSample() { + return writingSample; + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/SeiReader.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/SeiReader.java index f2f1def89b..2bf48be730 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/parser/SeiReader.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/SeiReader.java @@ -17,8 +17,8 @@ package com.google.android.exoplayer.hls.parser; import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.hls.parser.HlsExtractor.TrackOutput; import com.google.android.exoplayer.text.eia608.Eia608Parser; -import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.util.ParsableByteArray; /** @@ -27,20 +27,17 @@ import com.google.android.exoplayer.util.ParsableByteArray; * TODO: Technically, we shouldn't allow a sample to be read from the queue until we're sure that * a sample with an earlier timestamp won't be added to it. */ -/* package */ class SeiReader extends SampleQueue { +/* package */ class SeiReader extends ElementaryStreamReader { - private final ParsableByteArray seiBuffer; - - public SeiReader(BufferPool bufferPool) { - super(bufferPool); - setMediaFormat(MediaFormat.createEia608Format()); - seiBuffer = new ParsableByteArray(); + public SeiReader(TrackOutput output) { + super(output); + setFormat(MediaFormat.createEia608Format()); } - public void read(byte[] data, int position, int limit, long pesTimeUs) { - seiBuffer.reset(data, limit); + @Override + public void consume(ParsableByteArray seiBuffer, long pesTimeUs, boolean startOfPacket) { // Skip the NAL prefix and type. - seiBuffer.setPosition(position + 4); + seiBuffer.skip(4); int b; while (seiBuffer.bytesLeft() > 1 /* last byte will be rbsp_trailing_bits */) { @@ -58,13 +55,18 @@ import com.google.android.exoplayer.util.ParsableByteArray; } while (b == 0xFF); // Process the payload. We only support EIA-608 payloads currently. if (Eia608Parser.isSeiMessageEia608(payloadType, payloadSize, seiBuffer)) { - startSample(pesTimeUs); - appendData(seiBuffer, payloadSize); - commitSample(C.SAMPLE_FLAG_SYNC); + output.startSample(pesTimeUs, 0); + output.appendData(seiBuffer, payloadSize); + output.commitSample(C.SAMPLE_FLAG_SYNC, 0, null); } else { seiBuffer.skip(payloadSize); } } } + @Override + public void packetFinished() { + // Do nothing. + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/TsExtractor.java index 8468254440..15eae10354 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/parser/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/TsExtractor.java @@ -17,8 +17,6 @@ package com.google.android.exoplayer.hls.parser; import com.google.android.exoplayer.C; 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.ParsableBitArray; @@ -32,7 +30,7 @@ import java.io.IOException; /** * Facilitates the extraction of data from the MPEG-2 TS container format. */ -public final class TsExtractor extends HlsExtractor { +public final class TsExtractor implements HlsExtractor { private static final String TAG = "TsExtractor"; @@ -48,13 +46,13 @@ public final class TsExtractor extends HlsExtractor { private static final long MAX_PTS = 0x1FFFFFFFFL; private final ParsableByteArray tsPacketBuffer; - private final SparseArray sampleQueues; // Indexed by streamType + private final SparseArray streamReaders; // Indexed by streamType private final SparseArray tsPayloadReaders; // Indexed by pid - private final BufferPool bufferPool; private final long firstSampleTimestamp; private final ParsableBitArray tsScratch; // Accessed only by the loading thread. + private ExtractorOutput output; private int tsPacketBytesRead; private long timestampOffsetUs; private long lastPts; @@ -62,28 +60,31 @@ public final class TsExtractor extends HlsExtractor { // Accessed by both the loading and consuming threads. private volatile boolean prepared; - public TsExtractor(boolean shouldSpliceIn, long firstSampleTimestamp, BufferPool bufferPool) { - super(shouldSpliceIn); + public TsExtractor(long firstSampleTimestamp) { this.firstSampleTimestamp = firstSampleTimestamp; - this.bufferPool = bufferPool; tsScratch = new ParsableBitArray(new byte[3]); tsPacketBuffer = new ParsableByteArray(TS_PACKET_SIZE); - sampleQueues = new SparseArray(); + streamReaders = new SparseArray(); tsPayloadReaders = new SparseArray(); tsPayloadReaders.put(TS_PAT_PID, new PatReader()); lastPts = Long.MIN_VALUE; } + @Override + public void init(ExtractorOutput output) { + this.output = output; + } + @Override public int getTrackCount() { Assertions.checkState(prepared); - return sampleQueues.size(); + return streamReaders.size(); } @Override public MediaFormat getFormat(int track) { Assertions.checkState(prepared); - return sampleQueues.valueAt(track).getMediaFormat(); + return streamReaders.valueAt(track).getFormat(); } @Override @@ -91,48 +92,13 @@ public final class TsExtractor extends HlsExtractor { return prepared; } - @Override - public void release() { - for (int i = 0; i < sampleQueues.size(); i++) { - sampleQueues.valueAt(i).release(); - } - } - - @Override - public long getLargestSampleTimestamp() { - long largestParsedTimestampUs = Long.MIN_VALUE; - for (int i = 0; i < sampleQueues.size(); i++) { - largestParsedTimestampUs = Math.max(largestParsedTimestampUs, - sampleQueues.valueAt(i).getLargestParsedTimestampUs()); - } - return largestParsedTimestampUs; - } - - @Override - public boolean getSample(int track, SampleHolder holder) { - Assertions.checkState(prepared); - return sampleQueues.valueAt(track).getSample(holder); - } - - @Override - public void discardUntil(int track, long timeUs) { - Assertions.checkState(prepared); - sampleQueues.valueAt(track).discardUntil(timeUs); - } - - @Override - public boolean hasSamples(int track) { - Assertions.checkState(prepared); - return !sampleQueues.valueAt(track).isEmpty(); - } - private boolean checkPrepared() { - int pesPayloadReaderCount = sampleQueues.size(); + int pesPayloadReaderCount = streamReaders.size(); if (pesPayloadReaderCount == 0) { return false; } for (int i = 0; i < pesPayloadReaderCount; i++) { - if (!sampleQueues.valueAt(i).hasMediaFormat()) { + if (!streamReaders.valueAt(i).hasFormat()) { return false; } } @@ -183,7 +149,7 @@ public final class TsExtractor extends HlsExtractor { if (payloadExists) { TsPayloadReader payloadReader = tsPayloadReaders.get(pid); if (payloadReader != null) { - payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator); + payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator, output); } } @@ -194,11 +160,6 @@ public final class TsExtractor extends HlsExtractor { return bytesRead; } - @Override - protected SampleQueue getSampleQueue(int track) { - return sampleQueues.valueAt(track); - } - /** * Adjusts a PTS value to the corresponding time in microseconds, accounting for PTS wraparound. * @@ -231,7 +192,8 @@ public final class TsExtractor extends HlsExtractor { */ private abstract static class TsPayloadReader { - public abstract void consume(ParsableByteArray data, boolean payloadUnitStartIndicator); + public abstract void consume(ParsableByteArray data, boolean payloadUnitStartIndicator, + ExtractorOutput output); } @@ -247,7 +209,8 @@ public final class TsExtractor extends HlsExtractor { } @Override - public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) { + public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator, + ExtractorOutput output) { // Skip pointer. if (payloadUnitStartIndicator) { int pointerField = data.readUnsignedByte(); @@ -286,7 +249,8 @@ public final class TsExtractor extends HlsExtractor { } @Override - public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) { + public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator, + ExtractorOutput output) { // Skip pointer. if (payloadUnitStartIndicator) { int pointerField = data.readUnsignedByte(); @@ -323,27 +287,28 @@ public final class TsExtractor extends HlsExtractor { data.skip(esInfoLength); entriesSize -= esInfoLength + 5; - if (sampleQueues.get(streamType) != null) { + if (streamReaders.get(streamType) != null) { continue; } ElementaryStreamReader pesPayloadReader = null; switch (streamType) { case TS_STREAM_TYPE_AAC: - pesPayloadReader = new AdtsReader(bufferPool); + pesPayloadReader = new AdtsReader(output.getTrackOutput(TS_STREAM_TYPE_AAC)); break; case TS_STREAM_TYPE_H264: - SeiReader seiReader = new SeiReader(bufferPool); - sampleQueues.put(TS_STREAM_TYPE_EIA608, seiReader); - pesPayloadReader = new H264Reader(bufferPool, seiReader); + SeiReader seiReader = new SeiReader(output.getTrackOutput(TS_STREAM_TYPE_EIA608)); + streamReaders.put(TS_STREAM_TYPE_EIA608, seiReader); + pesPayloadReader = new H264Reader(output.getTrackOutput(TS_STREAM_TYPE_H264), + seiReader); break; case TS_STREAM_TYPE_ID3: - pesPayloadReader = new Id3Reader(bufferPool); + pesPayloadReader = new Id3Reader(output.getTrackOutput(TS_STREAM_TYPE_ID3)); break; } if (pesPayloadReader != null) { - sampleQueues.put(streamType, pesPayloadReader); + streamReaders.put(streamType, pesPayloadReader); tsPayloadReaders.put(elementaryPid, new PesReader(pesPayloadReader)); } } @@ -387,7 +352,8 @@ public final class TsExtractor extends HlsExtractor { } @Override - public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) { + public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator, + ExtractorOutput output) { if (payloadUnitStartIndicator) { switch (state) { case STATE_FINDING_HEADER: